Merge branch 'signin-door' into main
chrome: the sign-in pages are the Oficina door Paul: every app's sign-in should be a version of the Oficina sign-in screen. estilo-signin and friends draw Home's door — a card on the suite's sky with the app's mark and name, in its colour. Reviewed by Claude Fable 5.1 (discussion 17).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+104
−19
chrome/chrome.css+34 −8chrome/chrome.go+10 −1chrome/chrome_test.go+31 −0chrome/templates/signin.html+29 −10
CI passed — run details
CI log
=== modcache === all modules verified === vet === CGO_ENABLED=0 go vet ./... === fmt === === build === CGO_ENABLED=0 go build -buildvcs=false ./... === go-test === CGO_ENABLED=0 go test -count=1 ./... ok amadan.net/oficina/estilo.git/base 0.005s ok amadan.net/oficina/estilo.git/brand 0.004s ok amadan.net/oficina/estilo.git/busy 0.003s ok amadan.net/oficina/estilo.git/callui 0.005s ok amadan.net/oficina/estilo.git/chrome 0.181s ? amadan.net/oficina/estilo.git/cmd/oficina-notices [no test files] ok amadan.net/oficina/estilo.git/credits 0.007s ok amadan.net/oficina/estilo.git/credits/catalog 0.006s ok amadan.net/oficina/estilo.git/homeconnect 0.007s ok amadan.net/oficina/estilo.git/internal/lint 0.008s ? amadan.net/oficina/estilo.git/internal/tokencheck [no test files] ok amadan.net/oficina/estilo.git/menu 0.009s ok amadan.net/oficina/estilo.git/savestatus 0.074s ok amadan.net/oficina/estilo.git/sky 0.023s ok amadan.net/oficina/estilo.git/theme 0.008s
diff --git a/chrome/chrome.css b/chrome/chrome.css| index 4a3fe5d..1ff46e3 100644 |
| --- a/chrome/chrome.css |
| +++ b/chrome/chrome.css |
| @@ -203,18 +203,44 @@ |
| /* Visually hidden, still announced. */ |
| [estilo-visually-hidden] { position: absolute; clip-path: inset(50%); inline-size: 1px; block-size: 1px; overflow: hidden; white-space: nowrap; } |
| -/* Sign-in, check-your-email and signed-out pages: one narrow, |
| - left-aligned column, not a card, so nothing in the flow is framed. */ |
| +/* Sign-in, check-your-email and signed-out pages: the Oficina door. The |
| + suite's sky behind (estilo sky.css places and animates it), one card on |
| + it with the app's mark and name at its head — the same door Home draws |
| + at id.oficina.works, in the app's own colour. The door spans the page |
| + whatever the app's layout gives it, so the sky is never boxed in. */ |
| +:root:has([estilo-door]) { overflow-x: clip; --rst-scrollbar-gutter: auto; scrollbar-gutter: auto; } |
| +[estilo-door] { |
| + box-sizing: border-box; |
| + margin-inline: calc(50% - 50vw); |
| + padding: max(40px, 9vh) var(--rst-sp-4) var(--rst-sp-6); |
| + min-block-size: 680px; |
| + /* The whole sky lies inside the door, fading in and out, so the door's |
| + edges never cut it: under an app's bar there is no page top to hide |
| + an edge behind, as there is on Home. A sky's layers overhang its box |
| + by 12% above and below (sky.css), and fade in over their first 22% |
| + and out from 58% to 92% — so a 560px sky placed 67px down starts |
| + transparent at the door's top and has faded out by 640px. */ |
| + --estilo-sky-top: 67px; |
| + --estilo-sky-height: 560px; |
| +} |
| [estilo-signin] { |
| box-sizing: border-box; |
| - max-inline-size: 26rem; |
| - margin: 4rem auto var(--rst-sp-6); |
| - padding-inline: var(--rst-sp-4); |
| + max-inline-size: 460px; |
| + margin: 0 auto; |
| + padding: 34px 40px 32px; |
| + background: var(--rst-surface); |
| + border-radius: 20px; |
| + box-shadow: 0 0 0 1px light-dark(transparent, var(--rst-line)), var(--rst-shadow-lift); |
| } |
| -[estilo-signin] > [estilo-brand-icon] { display: block; } |
| -[estilo-signin] h1 { margin: var(--rst-sp-3) 0 0; font-size: 1.375rem; font-weight: 600; } |
| -[estilo-signin-lede] { margin: 0.25rem 0 var(--rst-sp-4); color: var(--rst-text-muted); } |
| +[estilo-signin] > [estilo-brand] { display: flex; align-items: center; gap: 10px; margin: 0 0 22px; font-size: 1.2rem; font-weight: 650; letter-spacing: -0.01em; } |
| +[estilo-signin] > [estilo-brand] [estilo-brand-icon] { inline-size: 36px; block-size: 36px; } |
| +[estilo-signin] h1 { margin: 0; font-size: 1.75rem; font-weight: 620; letter-spacing: -0.02em; line-height: 1.15; } |
| +[estilo-signin-lede] { margin: 0.5rem 0 var(--rst-sp-4); color: var(--rst-text-muted); } |
| [estilo-signin] > h1 + :not([estilo-signin-lede]) { margin-block-start: var(--rst-sp-4); } |
| [estilo-signin-form] { max-inline-size: none; margin-block-start: var(--rst-sp-4); } |
| [estilo-signin-body] { margin: var(--rst-sp-4) 0; } |
| +@media (max-width: 520px) { |
| + [estilo-door] { padding-inline: 14px; padding-block-start: 28px; } |
| + [estilo-signin] { padding: 26px 22px 24px; } |
| +} |
| [estilo-signin-foot] { margin-block-start: var(--rst-sp-6); font-size: var(--rst-fs-xs); color: var(--rst-text-muted); } |
diff --git a/chrome/chrome.go b/chrome/chrome.go| index 8bbf752..895441e 100644 |
| --- a/chrome/chrome.go |
| +++ b/chrome/chrome.go |
| @@ -27,6 +27,7 @@ import ( |
| "unicode" |
| "amadan.net/oficina/estilo.git/menu" |
| + "amadan.net/oficina/estilo.git/sky" |
| "github.com/carlosframework/rastrillo/sessions" |
| ) |
| @@ -196,7 +197,13 @@ type Hidden struct { |
| // SignIn is estilo-signin's data. Error is shown in a negative callout |
| // above the form; AddressError in the field's own error slot. |
| type SignIn struct { |
| - Icon, Heading, Lede string |
| + Icon, Heading, Lede string |
| + // App is the product's name, beside its mark at the head of the card |
| + // ("Calendar"); Sky is the sky behind it, sky.For(<the instance's |
| + // host>). Both optional. A Sky needs sky.css linked, sky.Handler |
| + // mounted at sky.Prefix and img-src data: allowed (the template). |
| + App string |
| + Sky sky.View |
| Error string |
| Action, Return string |
| Hidden []Hidden |
| @@ -208,6 +215,8 @@ type SignIn struct { |
| // SignInNotice is the data of estilo-signin-sent and estilo-signed-out. |
| type SignInNotice struct { |
| Icon, Heading, Body string |
| + App string |
| + Sky sky.View |
| Link *Link |
| } |
diff --git a/chrome/chrome_test.go b/chrome/chrome_test.go| index 97a312a..05913d6 100644 |
| --- a/chrome/chrome_test.go |
| +++ b/chrome/chrome_test.go |
| @@ -13,6 +13,7 @@ import ( |
| "amadan.net/oficina/estilo.git/internal/tokencheck" |
| "amadan.net/oficina/estilo.git/menu" |
| + "amadan.net/oficina/estilo.git/sky" |
| "github.com/carlosframework/rastrillo/ui" |
| ) |
| @@ -448,3 +449,33 @@ func TestScriptBehaviour(t *testing.T) { |
| t.Fatalf("node --test chrome.test.mjs: %v\n%s", err, out) |
| } |
| } |
| + |
| +// The door: the sign-in pages are one card on the suite's sky, headed by |
| +// the app's mark and name. Its sky markup must be exactly the sky |
| +// package's own, or the two would drift. |
| +func TestSignInIsTheOficinaDoor(t *testing.T) { |
| + v := sky.For("calendar.oficina.works") |
| + html := render(t, "estilo-signin", SignIn{Icon: "/i.svg", App: "Calendar", Sky: v, Heading: "Sign in", Action: "/signin", AddressLabel: "Email address", Submit: "Email me a link"}) |
| + var want bytes.Buffer |
| + if err := template.Must(template.ParseFS(sky.Templates(), "templates/*.html")).ExecuteTemplate(&want, "estilo-sky", v); err != nil { |
| + t.Fatal(err) |
| + } |
| + if !strings.Contains(html, strings.TrimSpace(want.String())) { |
| + t.Errorf("the door's sky differs from estilo-sky's:\n%s\nwant it to contain:\n%s", html, want.String()) |
| + } |
| + for _, s := range []string{`<div estilo-door estilo-sky-host>`, `<section estilo-signin>`, `<span>Calendar</span>`, ` autofocus`, `</section> |
| +</div>`} { |
| + if !strings.Contains(html, s) { |
| + t.Errorf("door lacks %q", s) |
| + } |
| + } |
| + for _, name := range []string{"estilo-signin-sent", "estilo-signed-out"} { |
| + if h := render(t, name, SignInNotice{Icon: "/i.svg", App: "Calendar", Sky: v, Heading: "h", Body: "b"}); !strings.Contains(h, `<div estilo-door estilo-sky-host>`) || !strings.Contains(h, "estilo-sky-layer") { |
| + t.Errorf("%s is not drawn as the door", name) |
| + } |
| + } |
| + // No sky given, no sky drawn: the card still stands. |
| + if h := render(t, "estilo-signin", SignIn{Icon: "/i.svg", Heading: "h", Action: "/s", AddressLabel: "a", Submit: "s"}); strings.Contains(h, "estilo-sky-layer") { |
| + t.Error("a sign-in with no Sky drew one") |
| + } |
| +} |
diff --git a/chrome/templates/signin.html b/chrome/templates/signin.html| index 689cab0..b845801 100644 |
| --- a/chrome/templates/signin.html |
| +++ b/chrome/templates/signin.html |
| @@ -6,8 +6,29 @@ |
| ui.Templates() with ui.Funcs() alongside this package. The form sets |
| novalidate: an address error goes in the field's own error slot, never |
| the browser's bubble. Data: chrome.SignIn. */}} |
| -{{define "estilo-signin"}}<section estilo-signin> |
| - <img estilo-brand-icon="listing" src="{{.Icon}}" alt="" width="40" height="40"> |
| + |
| +{{/* estilo-door-open / -close: the Oficina sign-in door around a card — |
| + the suite's sky behind (sky.View in .Sky; none if it is empty) and the |
| + app's own mark and name at the card's head (.App; the mark alone if |
| + empty). The sky's markup is the same as the sky package's estilo-sky |
| + template (chrome_test checks), repeated here so an app need not parse |
| + the sky package's templates. An app that passes a Sky must also link |
| + sky.css, mount sky.Handler at sky.Prefix, and allow img-src data: (the |
| + placeholder is inline) — without sky.css the layers flow in the page. */}} |
| +{{define "estilo-door-open"}}<div estilo-door estilo-sky-host> |
| +{{- with .Sky.Layers}} |
| +<div estilo-sky aria-hidden="true"> |
| + {{- range .}} |
| + <img estilo-sky-layer="{{.Name}}"{{with .Tone}} estilo-sky-tone="{{.}}"{{end}} src="{{with .Inline}}{{.}}{{else}}{{.Src}}{{end}}" alt=""{{if .Src}} decoding="async"{{end}}> |
| + {{- end}} |
| +</div> |
| +{{- end}} |
| +<section estilo-signin> |
| + <p estilo-brand><img estilo-brand-icon="listing" src="{{.Icon}}" alt="" width="36" height="36">{{with .App}}<span>{{.}}</span>{{end}}</p> |
| +{{- end}} |
| +{{define "estilo-door-close"}}</section> |
| +</div>{{end}} |
| +{{define "estilo-signin"}}{{template "estilo-door-open" .}} |
| <h1>{{.Heading}}</h1> |
| {{- with .Lede}} |
| <p estilo-signin-lede>{{.}}</p> |
| @@ -22,32 +43,30 @@ |
| {{- range .Hidden}} |
| <input type="hidden" name="{{.Name}}" value="{{.Value}}"> |
| {{- end}} |
| - {{template "field" (dict "ID" "estilo-signin-address" "Name" "address" "Type" "email" "Label" .AddressLabel "Autocomplete" "email" "Required" true "Value" .Address "Help" .Help "Error" .AddressError)}} |
| + {{template "field" (dict "ID" "estilo-signin-address" "Name" "address" "Type" "email" "Label" .AddressLabel "Autocomplete" "email" "Required" true "Autofocus" true "Value" .Address "Help" .Help "Error" .AddressError)}} |
| <div rst-form-foot><button rst-btn="primary" type="submit">{{.Submit}}</button></div> |
| </form> |
| {{- with .OpenSource}} |
| <p estilo-signin-foot><a href="{{.Href}}">{{.Label}}</a></p> |
| {{- end}} |
| -</section>{{end}} |
| +{{template "estilo-door-close"}}{{end}} |
| {{/* estilo-signin-sent: replaces the form once a link has been sent. Link |
| goes back to the form, keeping the return. Data: chrome.SignInNotice. */}} |
| -{{define "estilo-signin-sent"}}<section estilo-signin> |
| - <img estilo-brand-icon="listing" src="{{.Icon}}" alt="" width="40" height="40"> |
| +{{define "estilo-signin-sent"}}{{template "estilo-door-open" .}} |
| <h1>{{.Heading}}</h1> |
| <p estilo-signin-body role="status">{{.Body}}</p> |
| {{- with .Link}} |
| <p><a href="{{.Href}}">{{.Label}}</a></p> |
| {{- end}} |
| -</section>{{end}} |
| +{{template "estilo-door-close"}}{{end}} |
| {{/* estilo-signed-out: where Sign out lands. Link is the primary |
| "Sign in". Data: chrome.SignInNotice. */}} |
| -{{define "estilo-signed-out"}}<section estilo-signin> |
| - <img estilo-brand-icon="listing" src="{{.Icon}}" alt="" width="40" height="40"> |
| +{{define "estilo-signed-out"}}{{template "estilo-door-open" .}} |
| <h1>{{.Heading}}</h1> |
| <p estilo-signin-body>{{.Body}}</p> |
| {{- with .Link}} |
| <p><a rst-btn="primary" href="{{.Href}}">{{.Label}}</a></p> |
| {{- end}} |
| -</section>{{end}} |
| +{{template "estilo-door-close"}}{{end}} |