oficina / estilo Public

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

Plain git — no account needed to clone.

Download

Download this file

1package chrome
2
3import (
4 "fmt"
5 "strings"
6 "time"
7)
8
9// LinkLifetime is how long an emailed sign-in link works. It is
10// keymaildev/signin's LinkTTL default, which every sign-in in the suite
11// inherits: rastrillo's auth, Home's own flow and the keymail path all
12// leave LinkTTL unset (checked 2026-10-01 at signin v0.1.1, rastrillo
13// main 2fafa12 and v0.23.0, and Home 7a0f19c). The copy below says the
14// number, so a sign-in that changes its lifetime must change this too, or
15// the door and the email tell people something false.
16const LinkLifetime = 15 * time.Minute
17
18// DoorCopy is the door's words: the sign-in form, the check-your-email
19// state, the signed-out page and the sign-in email. English is the
20// suite's one copy set; a locale replaces the whole set through
21// SignIn.Copy and SignInNotice.Copy, never a word at a time, so no app
22// can drift from the others by overriding one line.
23//
24// The words are the copy deck in §9.2 of the shared-chrome spec
25// (docs/superpowers/specs/2026-09-23-shared-chrome.md, on estilo's
26// spec/shared-chrome branch), with Paul's later rulings noted where they
27// apply. "§9.2" and "§10" below are that document's sections.
28//
29// Translations are estilo's follow-up, never an app's. English is the
30// only set today. When the suite needs another locale, estilo adds its
31// whole DoorCopy here, beside English, with a way to pick one by locale,
32// and every app gets it on its next re-pin. An app passes Copy only a set
33// estilo publishes; it never writes or edits one, or the one copy set
34// fragments again the first time an app translates its own door.
35//
36// Slots are tokens, not Printf verbs: {app} is the app's name, and {link}
37// (the email body only) is the sign-in link. A translation may drop or
38// move a slot, or carry a literal "%", and still render as written.
39type DoorCopy struct {
40 // The form: heading (also the tab title's page), address label, the
41 // one help line under the field, and the submit.
42 Heading, AddressLabel, Help, Submit string
43 // The sent state, which replaces the form: heading (and tab title),
44 // body, and the link back to the form.
45 SentHeading, SentBody, Again string
46 // The signed-out page: the tab title's page, the heading, the body
47 // ({app} is the app's name) and the Sign in button.
48 SignedOutPage, SignedOutHeading, SignedOutBody, SignIn string
49 // The sign-in email: subject ({app}) and body ({app} and {link}).
50 EmailSubject, EmailBody string
51}
52
53// linkMinutes is LinkLifetime in words, so the number lives in one place.
54var linkMinutes = fmt.Sprintf("%d minutes", int(LinkLifetime/time.Minute))
55
56// English is the door's copy set (shared-chrome spec §9.2). The submit
57// is "Sign in": Paul, 2026-10-01, ruled out "Email me a link". The hub is
58// never named ("Home"); the suite is Oficina (Paul, 2026-09-23).
59var English = DoorCopy{
60 Heading: "Sign in",
61 AddressLabel: "Email address",
62 Help: "We’ll email you a link that signs you in. It works once and expires in " + linkMinutes + ".",
63 Submit: "Sign in",
64 SentHeading: "Check your email",
65 SentBody: "If that address can sign in here, we’ve sent it a link. It works once and expires in " + linkMinutes + ".",
66 Again: "Use a different address",
67 SignedOutPage: "Signed out",
68 SignedOutHeading: "You’re signed out",
69 SignedOutBody: "You’ve signed out of {app} on this device.",
70 SignIn: "Sign in",
71 EmailSubject: "Sign in to {app}",
72 EmailBody: "Here’s your link to sign in to {app}:\n\n{link}\n\nIt works once and expires in " + linkMinutes + ". If you didn’t ask for it, you can ignore this email.",
73}
74
75// signedOutBody names the app; with none it names the suite, as the hub's
76// own signed-out page does.
77func (c DoorCopy) signedOutBody(app string) string {
78 if app == "" {
79 app = "Oficina"
80 }
81 return fill(c.SignedOutBody, app, "")
82}
83
84// fill puts the app's name and the link into a copy string's {app} and
85// {link} slots, in one pass: a name or link that itself contains a token
86// is not filled again.
87func fill(s, app, link string) string {
88 return strings.NewReplacer("{app}", app, "{link}", link).Replace(s)
89}
90
91func copyOf(c *DoorCopy) DoorCopy {
92 if c == nil {
93 return English
94 }
95 return *c
96}
97
98// Title is a page's tab title: "<Page> · <App>" (shared-chrome spec
99// §10), or whichever half is set when one is empty.
100func Title(page, app string) string {
101 page, app = strings.TrimSpace(page), strings.TrimSpace(app)
102 switch {
103 case page == "":
104 return app
105 case app == "":
106 return page
107 }
108 return page + " · " + app
109}
110
111// The form's words, read by estilo-signin.
112func (s SignIn) Heading() string { return copyOf(s.Copy).Heading }
113func (s SignIn) AddressLabel() string { return copyOf(s.Copy).AddressLabel }
114func (s SignIn) Help() string { return copyOf(s.Copy).Help }
115func (s SignIn) Submit() string { return copyOf(s.Copy).Submit }
116
117// Page is the sign-in page's part of the tab title ("Sign in"), for a
118// layout that appends the app's name itself; Title is the whole title.
119func (s SignIn) Page() string { return s.Heading() }
120func (s SignIn) Title() string { return Title(s.Page(), s.App) }
121
122// Sent is this door's check-your-email state: the door's heading and
123// body, and "Use a different address" linking to again (the form,
124// keeping the return; empty drops the link).
125func (n SignInNotice) Sent(again string) SignInNotice {
126 c := copyOf(n.Copy)
127 n.Heading, n.Body, n.page, n.Link = c.SentHeading, c.SentBody, c.SentHeading, nil
128 if again != "" {
129 n.Link = &Link{Href: again, Label: c.Again}
130 }
131 return n
132}
133
134// SignedOut is this door's signed-out page: "You’re signed out", the
135// sentence naming the app, and the primary Sign in linking to signin.
136func (n SignInNotice) SignedOut(signin string) SignInNotice {
137 c := copyOf(n.Copy)
138 n.Heading, n.Body, n.page, n.Link = c.SignedOutHeading, c.signedOutBody(n.App), c.SignedOutPage, nil
139 if signin != "" {
140 n.Link = &Link{Href: signin, Label: c.SignIn}
141 }
142 return n
143}
144
145// Page is the notice's part of the tab title: the door's own for Sent and
146// SignedOut ("Check your email", "Signed out"), otherwise the heading.
147func (n SignInNotice) Page() string {
148 if n.page != "" {
149 return n.page
150 }
151 return n.Heading
152}
153
154// Title is the notice's whole tab title, "<Page> · <App>".
155func (n SignInNotice) Title() string { return Title(n.Page(), n.App) }
156
157// SignInEmail is this copy set's sign-in email for app: subject and body
158// around link.
159func (c DoorCopy) SignInEmail(app, link string) (subject, body string) {
160 return c.MailSubject(app), c.MailBody(app)(link)
161}
162
163// MailSubject is the sign-in email's subject, "Sign in to <App>" in
164// English. (Not EmailSubject: that is the field it reads.)
165func (c DoorCopy) MailSubject(app string) string { return fill(c.EmailSubject, app, "") }
166
167// MailBody renders the sign-in email's body around the link.
168func (c DoorCopy) MailBody(app string) func(link string) string {
169 return func(link string) string { return fill(c.EmailBody, app, link) }
170}
171
172// SignInEmail is the sign-in email for app in English: subject and body
173// around link.
174func SignInEmail(app, link string) (subject, body string) { return English.SignInEmail(app, link) }
175
176// EmailSubject is the sign-in email's subject in English, "Sign in to
177// <App>": what rastrillo's auth.Config.Subject takes.
178func EmailSubject(app string) string { return English.MailSubject(app) }
179
180// EmailBody renders the English sign-in email's body around the link: the
181// shape keymaildev/signin's Flow.Body and rastrillo's auth.Config.Body
182// take.
183func EmailBody(app string) func(link string) string { return English.MailBody(app) }
184