oficina / estilo Public

chrome: the sign-in pages are the Oficina door

Paul, 2026-09-29, of an app's old sign-in page: "Shouldn't all the apps
be using a version of the Oficina sign in screen?" estilo-signin,
-signin-sent and -signed-out now draw Home's door: one card on the
suite's sky, the app's own mark and name at its head, in the app's
colour. SignIn and SignInNotice gain App and Sky (sky.For(<instance
host>), so each instance's door has its own sky); both optional. The
door spans the page whatever the layout gives it. The sky markup matches
the sky package's template (tested).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev cc2c1e56fb4b8d0a761b031a2fcd0ec8a12861cd parent 77346e8
4 files changed, +94 −18
  • chrome/chrome.css +28 −8
  • chrome/chrome.go +9 −1
  • chrome/chrome_test.go +31 −0
  • chrome/templates/signin.html +26 −9

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.003s
ok  	amadan.net/oficina/estilo.git/busy	0.003s
ok  	amadan.net/oficina/estilo.git/callui	0.004s
ok  	amadan.net/oficina/estilo.git/chrome	0.196s
?   	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.007s
ok  	amadan.net/oficina/estilo.git/homeconnect	0.007s
ok  	amadan.net/oficina/estilo.git/internal/lint	0.006s
?   	amadan.net/oficina/estilo.git/internal/tokencheck	[no test files]
ok  	amadan.net/oficina/estilo.git/menu	0.010s
ok  	amadan.net/oficina/estilo.git/savestatus	0.088s
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..63d7b3f 100644
--- a/chrome/chrome.css
+++ b/chrome/chrome.css
@@ -203,18 +203,38 @@
/* 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: 70vh;
+ --estilo-sky-top: -140px;
+ --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..181de37 100644
--- a/chrome/chrome.go
+++ b/chrome/chrome.go
@@ -20,6 +20,7 @@
package chrome
import (
+ "amadan.net/oficina/estilo.git/sky"
"embed"
"net/http"
"strings"
@@ -196,7 +197,12 @@ 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.
+ App string
+ Sky sky.View
Error string
Action, Return string
Hidden []Hidden
@@ -208,6 +214,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..26e33c6 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>`, `</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..a191a06 100644
--- a/chrome/templates/signin.html
+++ b/chrome/templates/signin.html
@@ -6,8 +6,27 @@
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 that parses
+ chrome's templates needs nothing else. */}}
+{{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>
@@ -28,26 +47,24 @@
{{- 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}}