oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package docs
2
3// The shared chrome (estilo chrome, round 2): every page's bar, account
4// menu and <title> are built here, once, so no page template can drift
5// from another. Before this, each page defined its own rastrillo shell
6// blocks, and the share page fell back to rastrillo's default "Home"
7// brand because it forgot one.
8//
9// Spec: estilo spec/shared-chrome, docs/superpowers/specs/
10// 2026-09-23-shared-chrome.md, §5 (bar), §6 (switcher), §8 (account),
11// §10 (titles).
12
13import (
14 "net/http"
15 "net/url"
16
17 "amadan.net/oficina/estilo.git/brand"
18 "amadan.net/oficina/estilo.git/chrome"
19 estilomenu "amadan.net/oficina/estilo.git/menu"
20 "amadan.net/rastrillo/rastrillo"
21 "amadan.net/rastrillo/rastrillo/sessions"
22)
23
24// Frame is what views/layout.html reads from every page: the bar, the
25// parts of the <title> before the app's name, and the Appearance
26// preference for <html data-theme>.
27type Frame struct {
28 Chrome chrome.Bar
29 TitleParts []string
30 Theme string
31}
32
33// frameOptions describes one page's chrome. The zero value is a plain
34// signed-in-or-out page with no context.
35type frameOptions struct {
36 // titleParts precede "Docs" in <title>, most specific first.
37 titleParts []string
38 // context is the open object's title; edit replaces it with the
39 // editable title when the person may rename.
40 context string
41 edit *chrome.TitleEdit
42 actions []chrome.Action
43 // public is a read-only page a stranger reaches: the brand is plain
44 // text, and "Sign in" carries no return path (the path is a
45 // capability and must not be copied into another URL).
46 public bool
47 // signinPage is one of the sign-in, sent and signed-out pages: the
48 // corner is empty.
49 signinPage bool
50}
51
52// frame builds a page's Frame. The viewer is read optionally: an error
53// page, the public page and a sign-in page all show whatever the
54// request's session says, without requiring one.
55func (h *handlers) frame(r *http.Request, o frameOptions) Frame {
56 bar := chrome.Bar{
57 App: chrome.App{
58 Name: rastrillo.T(r, "docs.app.name"),
59 Slug: "docs",
60 Home: "/",
61 Icon: brand.Path("docs"),
62 },
63 Context: o.context,
64 ContextEdit: o.edit,
65 Actions: o.actions,
66 Sticky: true,
67 Labels: chrome.BarLabels{Skip: rastrillo.T(r, "docs.chrome.skip"), Menu: rastrillo.T(r, "docs.chrome.menu")},
68 }
69 if o.public {
70 bar.App.Home = ""
71 }
72 address, signedIn := "", false
73 if !o.signinPage {
74 address, signedIn = h.viewer(r)
75 }
76 switch {
77 case o.signinPage:
78 // The corner is empty on the pages that are themselves sign-in.
79 case signedIn:
80 bar.Account = h.account(r, address)
81 bar.Switcher = h.switcher(r)
82 case o.public:
83 bar.SignIn = &chrome.Link{Href: "/signin", Label: rastrillo.T(r, "docs.chrome.signin")}
84 default:
85 bar.SignIn = &chrome.Link{Href: "/signin?return_to=" + url.QueryEscape(returnTarget(r)), Label: rastrillo.T(r, "docs.chrome.signin")}
86 }
87 return Frame{Chrome: bar, TitleParts: o.titleParts, Theme: chrome.ThemeAttr(chrome.Appearance(r))}
88}
89
90// returnTarget is where a person should come back to: this page, if it
91// is a page (a GET), otherwise the list.
92func returnTarget(r *http.Request) string {
93 if r.Method != http.MethodGet {
94 return "/"
95 }
96 return r.URL.RequestURI()
97}
98
99func (h *handlers) account(r *http.Request, address string) *chrome.Account {
100 a := &chrome.Account{
101 TriggerLabel: rastrillo.T(r, "docs.chrome.account.anonymous"),
102 Initials: chrome.Initials("", address),
103 Address: address,
104 Labels: chrome.AccountLabels{
105 SignedInAs: rastrillo.T(r, "docs.chrome.signedinas"),
106 Appearance: rastrillo.T(r, "docs.chrome.appearance"),
107 Help: rastrillo.T(r, "docs.chrome.help"),
108 },
109 Appearance: &chrome.AppearanceMenu{
110 Action: "/appearance",
111 Return: returnTarget(r),
112 Options: chrome.AppearanceOptions(chrome.Appearance(r),
113 rastrillo.T(r, "docs.chrome.appearance.light"),
114 rastrillo.T(r, "docs.chrome.appearance.dark"),
115 rastrillo.T(r, "docs.chrome.appearance.system")),
116 },
117 SignOut: chrome.SignOut{Action: "/signout", Label: rastrillo.T(r, "docs.signout.label")},
118 }
119 if address != "" {
120 a.TriggerLabel = rastrillo.Tf(r, "docs.chrome.account", "address", address)
121 }
122 if label := h.homeLinkLabel(r); label != "" {
123 a.Items = []estilomenu.Item{{Href: "/account/home", Label: label}}
124 }
125 return a
126}
127
128// switcher is the app switcher, or nil when no hub directory is
129// configured. Until Home's /apps/<slug> changes and the hidden id.*
130// interstitial have merged (spec §6.2 a, b and the §9.3 hide), it shows
131// "All apps" alone and no tiles: a tile today costs two assertion
132// exchanges and flashes an interstitial mid-switch.
133func (h *handlers) switcher(r *http.Request) *chrome.Switcher {
134 if h.directory == "" {
135 return nil
136 }
137 return &chrome.Switcher{
138 Label: rastrillo.T(r, "docs.chrome.apps"),
139 CurrentLabel: rastrillo.T(r, "docs.chrome.thisapp"),
140 AllApps: estilomenu.Item{Href: h.directory + "/", Label: rastrillo.T(r, "docs.chrome.allapps")},
141 }
142}
143
144// viewer is who the request is signed in as, read optionally. Inside a
145// signed-in route the session middleware has already resolved it; on a
146// route no session guards (an error page, the public page) peek reads
147// the cookie without requiring it and without writing anything.
148//
149// The address is empty for a session whose person has no verified
150// address; the account menu then shows the person glyph.
151func (h *handlers) viewer(r *http.Request) (string, bool) {
152 if id, ok := currentIdentity(r); ok {
153 return actorAddress(id.Actor), true
154 }
155 if s, ok := sessions.Current(r); ok && s.Subject != "" {
156 if s.Method == "magiclink" || s.Method == "keymail" {
157 return s.Subject, true
158 }
159 }
160 if h.peek != nil {
161 return h.peek(r)
162 }
163 return "", false
164}
165
166func actorAddress(a Actor) string {
167 if a.VerifiedAddress == nil {
168 return ""
169 }
170 return *a.VerifiedAddress
171}
172
173// pageHeaders marks a rendered page as depending on the platform its
174// shortcut labels were formatted for: menu.Shortcut reads the
175// User-Agent, so a shared cache must not hand a Mac's page to Windows.
176func pageHeaders(w http.ResponseWriter) {
177 w.Header().Add("Vary", "User-Agent")
178}
179