Merge remote-tracking branch 'origin/main' into feat/k3-outbox-lane
20 files changed,
+166
−39
go.mod+1 −1go.sum+12 −0internal/docs/app.go+6 −0internal/docs/auth.go+8 −0internal/docs/auth_navigation.go+4 −0internal/docs/auth_navigation_test.go+6 −0internal/docs/auth_test.go+2 −2internal/docs/brand/apple-touch-icon.pngbinaryinternal/docs/brand/brand.css+2 −2internal/docs/brand/icon-32.pngbinaryinternal/docs/brand/icon.svg+1 −5internal/docs/home_render.go+3 −0internal/docs/render.go+21 −13internal/docs/signin_door_test.go+42 −0internal/docs/theme_assets_test.go+48 −14internal/docs/views/layout.html+4 −0internal/docs/views/signin.html+1 −1native/ios/Assets.xcassets/AppIcon.appiconset/AppIcon.pngbinarynative/macos/AppIcon.icnsbinaryscripts/build-release.sh+5 −1
diff --git a/go.mod b/go.mod| index cf54336..027a701 100644 |
| --- a/go.mod |
| +++ b/go.mod |
| @@ -3,7 +3,7 @@ module github.com/carlosframework/docs |
| go 1.26.5 |
| require ( |
| - amadan.net/oficina/estilo.git v0.0.0-20260924071126-894afdcb71a9 |
| + amadan.net/oficina/estilo.git v0.0.0-20260929185827-41201439fd22 |
| amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 |
| amadan.net/oficina/home/roster v0.0.0-20260908123429-74dda570e0cf |
| amadan.net/rastrillo/native/server v0.0.0-20260923173546-2a5d07b2c8d3 |
diff --git a/go.sum b/go.sum| index 7251bc8..4ca18d2 100644 |
| --- a/go.sum |
| +++ b/go.sum |
| @@ -1,5 +1,17 @@ |
| amadan.net/oficina/estilo.git v0.0.0-20260924071126-894afdcb71a9 h1:vHOkWeJ7mRwLjPbMihf0f7aqz4CLSe0/NseQsHcJUYQ= |
| amadan.net/oficina/estilo.git v0.0.0-20260924071126-894afdcb71a9/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929070530-5b2ce835e773 h1:9DIAhG3TaGG+LE2+8LvU73ZEhfK+oNn2y2p4kFjAAi0= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929070530-5b2ce835e773/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929095046-d0664ee49914 h1:V0AyxXYCxqGZClOLqvppw+cCTXsn3b9NmRQ8CV5ZlXY= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929095046-d0664ee49914/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929095838-0994675f496e h1:LTSmIWUS8RjPb6m94Ynf7moyFYeBXr08rdOr3nmNqvg= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929095838-0994675f496e/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929165053-846b39772f3c h1:YYocIz3r3b4oIA0K/58JkSsmyTOCzMT/2GaW/MFbbPo= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929165053-846b39772f3c/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929183653-cc2c1e56fb4b h1:UOxYBKsOSRHcHHYNKt0i6cB1Z4yT2soywOTWURYvBWo= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929183653-cc2c1e56fb4b/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929185827-41201439fd22 h1:RR9qP85FU1cZdn788v1zSLgZbKMGBmSk6Q7Rezj5JYI= |
| +amadan.net/oficina/estilo.git v0.0.0-20260929185827-41201439fd22/go.mod h1:4u0jNBtIdMLOsJyrScfqoEuYDJF7BBM3qM9YWbzt6aw= |
| amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489 h1:RPKtyry/71i06o8L+gjAaUyexbmG56kO1YPs601URAM= |
| amadan.net/oficina/home/handoff v0.0.0-20260908102137-481d2d57f489/go.mod h1:YISpzFJ+iMgrM0rUL032OSzsAQbP4/nozvXLCCOcn1k= |
| amadan.net/oficina/home/roster v0.0.0-20260908123429-74dda570e0cf h1:SRKjCUgtk42WwR7VSn+V0eLNk20mfiY8dQmzdw13Sbo= |
diff --git a/internal/docs/app.go b/internal/docs/app.go| index 4bde5b7..9e325c8 100644 |
| --- a/internal/docs/app.go |
| +++ b/internal/docs/app.go |
| @@ -11,6 +11,7 @@ |
| package docs |
| import ( |
| + "amadan.net/oficina/estilo.git/sky" |
| "context" |
| "errors" |
| "fmt" |
| @@ -22,6 +23,7 @@ import ( |
| "amadan.net/oficina/estilo.git/brand" |
| "amadan.net/oficina/estilo.git/chrome" |
| + "amadan.net/oficina/estilo.git/theme" |
| "amadan.net/oficina/home/handoff" |
| nativeauth "amadan.net/rastrillo/native/server" |
| "github.com/carlosframework/docs/internal/docs/linkstore" |
| @@ -611,6 +613,10 @@ func New(cfg Config) (*App, error) { |
| // The suite's app icons, served from estilo (the bar's brand, and the |
| // switcher's tiles once they render). |
| r.Handle(brand.Prefix+"*", brand.Handler()) |
| + // Onest, Calma's typeface, served by estilo under its versioned prefix. |
| + r.Handle(theme.FontPrefix+"*", theme.FontHandler()) |
| + // The sky behind the sign-in door: a closed set of SVGs, public. |
| + r.Handle(sky.Prefix+"*", sky.Handler()) |
| // Everything else needs a real, signed-in person — rastrillo |
| // SKILL.md §5/§1's "signed-in routes... in a Group with |
diff --git a/internal/docs/auth.go b/internal/docs/auth.go| index 7a0c508..61b44eb 100644 |
| --- a/internal/docs/auth.go |
| +++ b/internal/docs/auth.go |
| @@ -15,6 +15,7 @@ import ( |
| "amadan.net/oficina/estilo.git/brand" |
| "amadan.net/oficina/estilo.git/chrome" |
| + "amadan.net/oficina/estilo.git/sky" |
| rauth "github.com/carlosframework/rastrillo/auth" |
| "github.com/carlosframework/rastrillo/mail" |
| "github.com/carlosframework/rastrillo/sessions" |
| @@ -435,6 +436,9 @@ type signinPage struct { |
| Sent *chrome.SignInNotice |
| ContinueURL string |
| SubmitLabel string |
| + // Door heads the continuation card (Icon, App, Sky), so the moment |
| + // between the form and Keymail looks like the same door. |
| + Door chrome.SignInNotice |
| } |
| // signinAgainPath is where "Use a different address" goes: the local |
| @@ -488,6 +492,8 @@ func (h *handlers) signin(w http.ResponseWriter, r *http.Request) { |
| title = rastrillo.T(r, "docs.signin.sent.title") |
| data.Sent = &chrome.SignInNotice{ |
| Icon: icon, |
| + App: "Docs", |
| + Sky: sky.For(r.Host), |
| Heading: title, |
| Body: rastrillo.T(r, "docs.signin.sent.body"), |
| Link: &chrome.Link{Href: h.signinAgainHref(r), Label: rastrillo.T(r, "docs.signin.sent.again")}, |
| @@ -495,6 +501,8 @@ func (h *handlers) signin(w http.ResponseWriter, r *http.Request) { |
| } else { |
| form := &chrome.SignIn{ |
| Icon: icon, |
| + App: "Docs", |
| + Sky: sky.For(r.Host), |
| Heading: title, |
| Action: "/signin", |
| AddressLabel: rastrillo.T(r, "docs.signin.address.label"), |
diff --git a/internal/docs/auth_navigation.go b/internal/docs/auth_navigation.go| index 0496657..6d5f261 100644 |
| --- a/internal/docs/auth_navigation.go |
| +++ b/internal/docs/auth_navigation.go |
| @@ -1,6 +1,9 @@ |
| package docs |
| import ( |
| + "amadan.net/oficina/estilo.git/brand" |
| + "amadan.net/oficina/estilo.git/chrome" |
| + "amadan.net/oficina/estilo.git/sky" |
| "bytes" |
| "net/http" |
| "net/url" |
| @@ -32,6 +35,7 @@ func (h *handlers) beginSignin(next http.HandlerFunc) http.HandlerFunc { |
| Frame: h.frame(r, frameOptions{titleParts: []string{rastrillo.T(r, "docs.signin.title")}, signinPage: true}), |
| SubmitLabel: rastrillo.T(r, "docs.signin.keymail"), |
| ContinueURL: destination, |
| + Door: chrome.SignInNotice{Icon: brand.Path("docs"), App: "Docs", Sky: sky.For(r.Host)}, |
| } |
| if err := h.render.page(w, r, "signin", data); err != nil { |
| h.logger.Error("docs: render Keymail continuation", "err", err) |
diff --git a/internal/docs/auth_navigation_test.go b/internal/docs/auth_navigation_test.go| index a1f9584..9288162 100644 |
| --- a/internal/docs/auth_navigation_test.go |
| +++ b/internal/docs/auth_navigation_test.go |
| @@ -48,6 +48,12 @@ func TestKeymailContinuation(t *testing.T) { |
| if strings.Contains(body, `action="/signin"`) { |
| t.Fatal("continuation rendered another sign-in form") |
| } |
| + // It is the door's card on the sky, not a bare page. |
| + for _, part := range []string{"estilo-door", "estilo-sky-layer", "<span>Docs</span>"} { |
| + if !strings.Contains(body, part) { |
| + t.Fatalf("continuation is not the door: lacks %q", part) |
| + } |
| + } |
| if dir := os.Getenv("DOCS_OAUTH_BROWSER_FIXTURE"); dir != "" { |
| if err := os.MkdirAll(dir, 0700); err != nil { |
| t.Fatal(err) |
diff --git a/internal/docs/auth_test.go b/internal/docs/auth_test.go| index 1d5d5dd..24b9a64 100644 |
| --- a/internal/docs/auth_test.go |
| +++ b/internal/docs/auth_test.go |
| @@ -118,8 +118,8 @@ func TestSignInPageRendersSignedOut(t *testing.T) { |
| if !strings.Contains(body, `<label rst-field-label for="estilo-signin-address">docs.signin.address.label</label>`) { |
| t.Error("email input has no persistent associated translated label") |
| } |
| - if !strings.Contains(body, `type="email" id="estilo-signin-address" name="address" required autocomplete="email"`) { |
| - t.Error("label target must remain the required email input with email autocomplete") |
| + if !strings.Contains(body, `type="email" id="estilo-signin-address" name="address" required autofocus autocomplete="email"`) { |
| + t.Error("label target must remain the required, focused email input with email autocomplete") |
| } |
| } |
diff --git a/internal/docs/brand/apple-touch-icon.png b/internal/docs/brand/apple-touch-icon.png| index d75bdba..7b09d4d 100644 |
| Binary files a/internal/docs/brand/apple-touch-icon.png and b/internal/docs/brand/apple-touch-icon.png differ |
diff --git a/internal/docs/brand/brand.css b/internal/docs/brand/brand.css| index 2e194c6..27e25ad 100644 |
| --- a/internal/docs/brand/brand.css |
| +++ b/internal/docs/brand/brand.css |
| @@ -1,4 +1,5 @@ |
| -/* Oficina brand artwork. Source: oficina/estilo brand/. */ |
| +/* Oficina brand artwork. Source: oficina/estilo brand/. The icons carry |
| + their own squircle and transparent corners, so nothing here rounds them. */ |
| [estilo-brand] { |
| display: inline-flex; |
| align-items: center; |
| @@ -10,7 +11,6 @@ |
| flex-shrink: 0; |
| inline-size: 1.75rem; |
| block-size: 1.75rem; |
| - border-radius: 21%; |
| vertical-align: middle; |
| } |
| [estilo-brand-icon="listing"] { |
diff --git a/internal/docs/brand/icon-32.png b/internal/docs/brand/icon-32.png| index b61cb5f..95ca847 100644 |
| Binary files a/internal/docs/brand/icon-32.png and b/internal/docs/brand/icon-32.png differ |
diff --git a/internal/docs/brand/icon.svg b/internal/docs/brand/icon.svg| index 9b7f4c6..75e0a4f 100644 |
| --- a/internal/docs/brand/icon.svg |
| +++ b/internal/docs/brand/icon.svg |
| @@ -1,5 +1 @@ |
| -<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-label="Docs"><title>Docs</title><rect x="0" y="0" width="1024" height="1024" rx="0" fill="#2563EB"/><g transform="translate(153.6 153.6) scale(29.866666666666664)" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" /> |
| - <path d="M14 2v5a1 1 0 0 0 1 1h5" /> |
| - <path d="M10 9H8" /> |
| - <path d="M16 13H8" /> |
| - <path d="M16 17H8" /></g></svg> |
| +<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 100 100" role="img" aria-label="Docs"><title>Docs</title><defs><clipPath id="s"><path d="M96.00 50.00L95.97 64.13L95.90 68.64L95.77 71.90L95.60 74.54L95.37 76.79L95.09 78.76L94.75 80.51L94.37 82.10L93.92 83.54L93.43 84.86L92.87 86.07L92.26 87.19L91.59 88.22L90.85 89.17L90.05 90.05L89.17 90.85L88.22 91.59L87.19 92.26L86.07 92.87L84.86 93.43L83.54 93.92L82.10 94.37L80.51 94.75L78.76 95.09L76.79 95.37L74.54 95.60L71.90 95.77L68.64 95.90L64.13 95.97L50.00 96.00L35.87 95.97L31.36 95.90L28.10 95.77L25.46 95.60L23.21 95.37L21.24 95.09L19.49 94.75L17.90 94.37L16.46 93.92L15.14 93.43L13.93 92.87L12.81 92.26L11.78 91.59L10.83 90.85L9.95 90.05L9.15 89.17L8.41 88.22L7.74 87.19L7.13 86.07L6.57 84.86L6.08 83.54L5.63 82.10L5.25 80.51L4.91 78.76L4.63 76.79L4.40 74.54L4.23 71.90L4.10 68.64L4.03 64.13L4.00 50.00L4.03 35.87L4.10 31.36L4.23 28.10L4.40 25.46L4.63 23.21L4.91 21.24L5.25 19.49L5.63 17.90L6.08 16.46L6.57 15.14L7.13 13.93L7.74 12.81L8.41 11.78L9.15 10.83L9.95 9.95L10.83 9.15L11.78 8.41L12.81 7.74L13.93 7.13L15.14 6.57L16.46 6.08L17.90 5.63L19.49 5.25L21.24 4.91L23.21 4.63L25.46 4.40L28.10 4.23L31.36 4.10L35.87 4.03L50.00 4.00L64.13 4.03L68.64 4.10L71.90 4.23L74.54 4.40L76.79 4.63L78.76 4.91L80.51 5.25L82.10 5.63L83.54 6.08L84.86 6.57L86.07 7.13L87.19 7.74L88.22 8.41L89.17 9.15L90.05 9.95L90.85 10.83L91.59 11.78L92.26 12.81L92.87 13.93L93.43 15.14L93.92 16.46L94.37 17.90L94.75 19.49L95.09 21.24L95.37 23.21L95.60 25.46L95.77 28.10L95.90 31.36L95.97 35.87Z"/></clipPath><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#5c8ae1"/><stop offset="1" stop-color="#4170cb"/></linearGradient><filter id="h" x="-40%" y="-60%" width="180%" height="220%"><feGaussianBlur stdDeviation="5.5"/></filter></defs><g clip-path="url(#s)"><rect width="100" height="100" fill="url(#g)"/><g filter="url(#h)" opacity=".7"><rect x="4.00" y="29.3" width="0.66" height="25.1" fill="#d5e3ff"/><rect x="6.12" y="31.1" width="0.88" height="23.1" fill="#c6dcff"/><rect x="7.79" y="21.7" width="0.96" height="32.3" fill="#a8cbfe"/><rect x="8.99" y="21.1" width="0.72" height="32.6" fill="#c1d4fe"/><rect x="11.18" y="37.2" width="1.25" height="16.0" fill="#b9d2fe"/><rect x="12.52" y="41.7" width="1.26" height="11.1" fill="#a0bfff"/><rect x="14.31" y="32.4" width="1.15" height="19.8" fill="#abc8ff"/><rect x="16.50" y="27.7" width="1.02" height="23.5" fill="#d0e0fe"/><rect x="18.25" y="36.0" width="0.76" height="14.3" fill="#bcd6fe"/><rect x="20.22" y="30.9" width="1.04" height="18.4" fill="#c0d5fe"/><rect x="21.74" y="17.1" width="0.77" height="31.2" fill="#d7e6fe"/><rect x="23.46" y="13.1" width="0.65" height="34.2" fill="#d2e3fe"/><rect x="25.33" y="23.0" width="1.17" height="23.2" fill="#a7c9fe"/><rect x="27.60" y="19.4" width="0.64" height="25.2" fill="#a3c2fe"/><rect x="29.54" y="11.2" width="1.13" height="32.3" fill="#afcdf9"/><rect x="31.49" y="22.4" width="1.07" height="20.1" fill="#bfd3fe"/><rect x="33.07" y="30.7" width="0.86" height="11.0" fill="#d7e7fe"/><rect x="34.74" y="25.0" width="1.23" height="16.0" fill="#d4e5ff"/><rect x="36.20" y="15.9" width="0.91" height="24.6" fill="#aecaff"/><rect x="38.14" y="19.1" width="1.22" height="20.8" fill="#bcd1fe"/><rect x="39.62" y="25.4" width="1.01" height="14.3" fill="#9fbffe"/><rect x="40.96" y="21.4" width="0.96" height="18.2" fill="#a2bffb"/><rect x="42.82" y="6.4" width="0.67" height="33.1" fill="#c7dbfe"/><rect x="45.04" y="8.5" width="0.76" height="31.2" fill="#a9c8ff"/><rect x="46.90" y="19.4" width="1.08" height="20.6" fill="#a4c1fb"/><rect x="49.12" y="13.1" width="0.96" height="27.5" fill="#b2cffe"/><rect x="51.29" y="13.2" width="0.82" height="28.1" fill="#9fc1fe"/><rect x="53.25" y="29.4" width="0.85" height="12.7" fill="#aac7fe"/><rect x="54.68" y="31.6" width="0.65" height="11.0" fill="#bdd2ff"/><rect x="55.92" y="25.5" width="1.12" height="17.7" fill="#d6e5fe"/><rect x="58.20" y="18.5" width="1.03" height="25.6" fill="#b5cdfe"/><rect x="59.34" y="24.1" width="0.65" height="20.4" fill="#aecefe"/><rect x="61.07" y="29.5" width="0.62" height="15.7" fill="#cfe0fe"/><rect x="63.29" y="15.2" width="1.29" height="30.7" fill="#abcaff"/><rect x="64.74" y="9.7" width="0.75" height="36.6" fill="#c5dbff"/><rect x="66.94" y="22.5" width="0.91" height="24.3" fill="#c6d8fe"/><rect x="69.15" y="25.6" width="1.24" height="21.6" fill="#aecdfe"/><rect x="70.98" y="17.6" width="0.94" height="29.7" fill="#c1d5ff"/><rect x="72.91" y="25.3" width="0.79" height="22.1" fill="#a2c3fa"/><rect x="75.16" y="36.4" width="1.20" height="11.0" fill="#a8c7fe"/><rect x="76.37" y="33.2" width="1.05" height="14.2" fill="#9ec0fe"/><rect x="77.93" y="22.3" width="1.26" height="25.1" fill="#b6d3fe"/><rect x="79.65" y="22.4" width="0.99" height="24.8" fill="#afc6fd"/><rect x="81.08" y="29.4" width="1.29" height="17.9" fill="#dce7fe"/><rect x="82.64" y="28.0" width="1.12" height="19.2" fill="#d6e3fe"/><rect x="83.93" y="18.2" width="0.65" height="29.1" fill="#a5c7fe"/><rect x="86.14" y="11.7" width="0.71" height="35.7" fill="#d2e3fe"/><rect x="88.11" y="25.4" width="0.79" height="22.2" fill="#9fc1f2"/><rect x="89.90" y="28.3" width="0.64" height="19.7" fill="#c0d7ff"/><rect x="91.43" y="21.3" width="0.77" height="27.1" fill="#b1c8fe"/><rect x="93.60" y="26.0" width="1.15" height="23.1" fill="#c7dbfe"/><rect x="95.21" y="38.7" width="1.29" height="11.1" fill="#b1ccfc"/></g></g><path d="M96.00 50.00L95.97 64.13L95.90 68.64L95.77 71.90L95.60 74.54L95.37 76.79L95.09 78.76L94.75 80.51L94.37 82.10L93.92 83.54L93.43 84.86L92.87 86.07L92.26 87.19L91.59 88.22L90.85 89.17L90.05 90.05L89.17 90.85L88.22 91.59L87.19 92.26L86.07 92.87L84.86 93.43L83.54 93.92L82.10 94.37L80.51 94.75L78.76 95.09L76.79 95.37L74.54 95.60L71.90 95.77L68.64 95.90L64.13 95.97L50.00 96.00L35.87 95.97L31.36 95.90L28.10 95.77L25.46 95.60L23.21 95.37L21.24 95.09L19.49 94.75L17.90 94.37L16.46 93.92L15.14 93.43L13.93 92.87L12.81 92.26L11.78 91.59L10.83 90.85L9.95 90.05L9.15 89.17L8.41 88.22L7.74 87.19L7.13 86.07L6.57 84.86L6.08 83.54L5.63 82.10L5.25 80.51L4.91 78.76L4.63 76.79L4.40 74.54L4.23 71.90L4.10 68.64L4.03 64.13L4.00 50.00L4.03 35.87L4.10 31.36L4.23 28.10L4.40 25.46L4.63 23.21L4.91 21.24L5.25 19.49L5.63 17.90L6.08 16.46L6.57 15.14L7.13 13.93L7.74 12.81L8.41 11.78L9.15 10.83L9.95 9.95L10.83 9.15L11.78 8.41L12.81 7.74L13.93 7.13L15.14 6.57L16.46 6.08L17.90 5.63L19.49 5.25L21.24 4.91L23.21 4.63L25.46 4.40L28.10 4.23L31.36 4.10L35.87 4.03L50.00 4.00L64.13 4.03L68.64 4.10L71.90 4.23L74.54 4.40L76.79 4.63L78.76 4.91L80.51 5.25L82.10 5.63L83.54 6.08L84.86 6.57L86.07 7.13L87.19 7.74L88.22 8.41L89.17 9.15L90.05 9.95L90.85 10.83L91.59 11.78L92.26 12.81L92.87 13.93L93.43 15.14L93.92 16.46L94.37 17.90L94.75 19.49L95.09 21.24L95.37 23.21L95.60 25.46L95.77 28.10L95.90 31.36L95.97 35.87Z" fill="none" stroke="#fff" stroke-opacity=".18" stroke-width="1"/><path d="M31 18h28l14 14v50H31z" fill="#fff"/><path d="M59 18v14h14" fill="#fff" opacity=".6"/><g fill="#2e5bb4"><rect x="38" y="44" width="28" height="4" rx="2"/><rect x="38" y="54" width="28" height="4" rx="2"/><rect x="38" y="64" width="18" height="4" rx="2"/></g></svg> |
diff --git a/internal/docs/home_render.go b/internal/docs/home_render.go| index 7baf3bd..f2a22be 100644 |
| --- a/internal/docs/home_render.go |
| +++ b/internal/docs/home_render.go |
| @@ -5,6 +5,7 @@ import ( |
| "amadan.net/oficina/estilo.git/brand" |
| "amadan.net/oficina/estilo.git/chrome" |
| + "amadan.net/oficina/estilo.git/sky" |
| "github.com/carlosframework/rastrillo" |
| ) |
| @@ -35,6 +36,8 @@ func (rd *renderer) homeLink(w http.ResponseWriter, r *http.Request, state, addr |
| // in going back through Home. |
| page.SignedOut = &chrome.SignInNotice{ |
| Icon: brand.Path("docs"), |
| + App: "Docs", |
| + Sky: sky.For(r.Host), |
| Heading: rastrillo.T(r, "docs.signedout.heading"), |
| Body: rastrillo.T(r, "docs.signedout.body"), |
| Link: &chrome.Link{Href: "/signin", Label: rastrillo.T(r, "docs.chrome.signin")}, |
diff --git a/internal/docs/render.go b/internal/docs/render.go| index 1867450..7755cb1 100644 |
| --- a/internal/docs/render.go |
| +++ b/internal/docs/render.go |
| @@ -1,6 +1,7 @@ |
| package docs |
| import ( |
| + "amadan.net/oficina/estilo.git/sky" |
| "bytes" |
| "embed" |
| "fmt" |
| @@ -10,6 +11,7 @@ import ( |
| "testing/fstest" |
| "amadan.net/oficina/estilo.git/base" |
| + "amadan.net/oficina/estilo.git/busy" |
| "amadan.net/oficina/estilo.git/chrome" |
| estilomenu "amadan.net/oficina/estilo.git/menu" |
| "amadan.net/oficina/estilo.git/savestatus" |
| @@ -18,16 +20,12 @@ import ( |
| "github.com/carlosframework/rastrillo/ui" |
| ) |
| -// dayTheme is the design system's day theme, which estilo's theme is |
| -// byte-for-byte (estilo AGENTS.md). |
| -// |
| // There is no rastrillo shell any more. The topbar shell's bar hard-coded |
| // an "Account" summary and a tail that wrapped (45px on the public page, |
| // 51px elsewhere, 96px at 390px), and estilo may not restyle rastrillo's |
| // markup, so every page now renders views/layout.html with estilo's own |
| // bar (chrome.go builds its data). That is the change the whole suite |
| // makes in round 2; Docs is the first to make it. |
| -const dayTheme = "day" |
| // Catalog is CARLOS Docs' own English strings. Every user-visible |
| // string this app writes is a key here (never a literal in a |
| @@ -627,8 +625,10 @@ func (rd *renderer) pageStatus(w http.ResponseWriter, r *http.Request, name stri |
| } |
| // vendoredStaticFS builds the static/ tree rastrillo.Assets serves — |
| -// the shared Estilo day theme, plus tokens.css and the three scripts |
| -// the shell's <head> links unconditionally (ui.ShimJS/SelectJS/DatetimeJS) |
| +// estilo's theme for Docs (Calma, with Docs' accent; estilo AGENTS.md, |
| +// decisions table, row 8), plus tokens.css and the three scripts |
| +// the shell's <head> links unconditionally (the shim and busy rule from |
| +// estilo's busy; ui.SelectJS/DatetimeJS) |
| // taken straight from the selected rastrillo module. Nothing here is copied to |
| // static/ by hand: rastrillo new would normally vendor these to disk |
| // once at scaffold time (ui.VendoredAssets's own doc comment), but |
| @@ -637,23 +637,31 @@ func (rd *renderer) pageStatus(w http.ResponseWriter, r *http.Request, name stri |
| // testing/fstest.MapFS is the standard library's only in-memory |
| // fs.FS — used here as a data structure, not as a test helper. |
| func vendoredStaticFS() (fs.FS, error) { |
| - _, ok := ui.ThemeCSS(dayTheme) |
| + docsTheme, ok := theme.ForApp("docs") |
| if !ok { |
| - return nil, fmt.Errorf("docs: rastrillo ui: theme %q not shipped", dayTheme) |
| + return nil, fmt.Errorf("docs: estilo's theme ring has no docs") |
| } |
| files := fstest.MapFS{ |
| "static/estilo-save-status.css": {Data: savestatus.CSS()}, |
| "static/estilo-save-status.mjs": {Data: savestatus.JS()}, |
| "static/tokens.css": {Data: ui.TokensCSS()}, |
| - "static/theme.css": {Data: theme.CSS()}, |
| - "static/rastrillo.js": {Data: ui.ShimJS()}, |
| - "static/select.js": {Data: ui.SelectJS()}, |
| - "static/datetime.js": {Data: ui.DatetimeJS()}, |
| + "static/theme.css": {Data: docsTheme}, |
| + // rastrillo's busy rule (Paul, 2026-09-29): a submit button's |
| + // label gives way to a spinner for at least 650ms, the button |
| + // disables, and Back resets it. estilo carries rastrillo's shim |
| + // without the old busy rule, busy.js and the spinner-in-place CSS. |
| + "static/rastrillo.js": {Data: busy.ShimJS()}, |
| + "static/estilo/busy.js": {Data: busy.JS()}, |
| + "static/estilo/busy.css": {Data: busy.CSS()}, |
| + "static/select.js": {Data: ui.SelectJS()}, |
| + "static/datetime.js": {Data: ui.DatetimeJS()}, |
| // estilo's shared chrome, fingerprinted like the theme. |
| "static/estilo/base.css": {Data: base.CSS()}, |
| "static/estilo/menu.css": {Data: estilomenu.CSS()}, |
| "static/estilo/chrome.css": {Data: chrome.CSS()}, |
| - "static/estilo/chrome.js": {Data: chrome.JS()}, |
| + // The sky behind the sign-in door; images from sky.Handler. |
| + "static/estilo/sky.css": {Data: sky.CSS()}, |
| + "static/estilo/chrome.js": {Data: chrome.JS()}, |
| } |
| for _, name := range []string{"icon-32.png", "apple-touch-icon.png", "brand.css", "LICENSE.lucide"} { |
| body, err := brandFS.ReadFile("brand/" + name) |
diff --git a/internal/docs/signin_door_test.go b/internal/docs/signin_door_test.go| new file mode 100644 |
| index 0000000..7554dde |
| --- /dev/null |
| +++ b/internal/docs/signin_door_test.go |
| @@ -0,0 +1,42 @@ |
| +package docs |
| + |
| +import ( |
| + "net/http" |
| + "net/http/httptest" |
| + "regexp" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// The sign-in page is the Oficina door (estilo chrome): a card on the |
| +// suite sky with this app's mark and name, its sky layers served signed |
| +// out by estilo's handler rather than shadowed by the app's /static/. |
| +func TestSignInPageIsTheDoor(t *testing.T) { |
| + app, err := NewForTest(t) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + GET := func(p string) *httptest.ResponseRecorder { |
| + return serve(app, httptest.NewRequest(http.MethodGet, p, nil)) |
| + } |
| + rec := GET("/signin") |
| + body := rec.Body.String() |
| + for _, want := range []string{"estilo-door", "<span>Docs</span>", `name="address"`, " autofocus"} { |
| + if !strings.Contains(body, want) { |
| + t.Errorf("sign-in page lacks %q", want) |
| + } |
| + } |
| + if !regexp.MustCompile(`<img estilo-sky-layer="placeholder"[^>]*src="data:image/png;base64,`).MatchString(body) { |
| + t.Error("sign-in page has no inline sky placeholder") |
| + } |
| + layer := regexp.MustCompile(`<img estilo-sky-layer="[a-z]+"[^>]*src="(/static/estilo/sky/v1/[^"]+)"`).FindStringSubmatch(body) |
| + if layer == nil { |
| + t.Fatal("sign-in page links no served sky layer") |
| + } |
| + if got := GET(layer[1]); got.Code != http.StatusOK || got.Header().Get("Content-Type") != "image/svg+xml" { |
| + t.Errorf("GET %s signed out: %d %q, want 200 image/svg+xml", layer[1], got.Code, got.Header().Get("Content-Type")) |
| + } |
| + if !regexp.MustCompile(`/static/estilo/sky\.[0-9a-f]{16}\.css`).MatchString(body) { |
| + t.Error("sign-in page does not link sky.css") |
| + } |
| +} |
diff --git a/internal/docs/theme_assets_test.go b/internal/docs/theme_assets_test.go| index 6a0b61b..55d3e16 100644 |
| --- a/internal/docs/theme_assets_test.go |
| +++ b/internal/docs/theme_assets_test.go |
| @@ -1,6 +1,9 @@ |
| package docs |
| import ( |
| + "path" |
| + |
| + "amadan.net/oficina/estilo.git/busy" |
| "bytes" |
| "crypto/sha256" |
| "fmt" |
| @@ -22,26 +25,16 @@ func TestSharedThemeAssetFingerprintAndCache(t *testing.T) { |
| if page.Code != http.StatusOK { |
| t.Fatalf("signin status %d", page.Code) |
| } |
| - day, ok := ui.ThemeCSS(dayTheme) |
| + // Calma with Docs' accent (estilo row 8), not rastrillo's day. |
| + docsTheme, ok := theme.ForApp("docs") |
| if !ok { |
| - t.Fatal("selected day missing") |
| - } |
| - body := func(css []byte) []byte { |
| - if bytes.HasPrefix(css, []byte("/*")) { |
| - if end := bytes.Index(css, []byte("*/")); end >= 0 { |
| - return css[end+2:] |
| - } |
| - } |
| - return css |
| - } |
| - if !bytes.Equal(body(theme.CSS()), body(day)) { |
| - t.Fatal("shared theme differs from selected day") |
| + t.Fatal("estilo's theme ring has no docs") |
| } |
| for _, item := range []struct { |
| name string |
| data []byte |
| }{ |
| - {"theme", theme.CSS()}, {"tokens", ui.TokensCSS()}, |
| + {"theme", docsTheme}, {"tokens", ui.TokensCSS()}, |
| } { |
| sum := sha256.Sum256(item.data) |
| hashed := fmt.Sprintf("/static/%s.%x.css", item.name, sum[:8]) |
| @@ -66,3 +59,44 @@ func TestSharedThemeAssetFingerprintAndCache(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// The layout names Onest's stylesheet under estilo's versioned prefix, and |
| +// the app serves it there: when estilo moves the fonts, this fails rather |
| +// than every page quietly falling back to the system face. |
| +func TestOnestIsLinkedAndServed(t *testing.T) { |
| + app, err := NewForTest(t) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + page := serve(app, httptest.NewRequest(http.MethodGet, "/signin", nil)) |
| + css := theme.FontPrefix + "fonts.css" |
| + if !strings.Contains(page.Body.String(), `href="`+css+`"`) { |
| + t.Fatalf("layout does not link %s", css) |
| + } |
| + for _, path := range []string{css, theme.FontPrefix + "onest-latin.woff2"} { |
| + if res := serve(app, httptest.NewRequest(http.MethodGet, path, nil)); res.Code != http.StatusOK { |
| + t.Fatalf("GET %s = %d", path, res.Code) |
| + } |
| + } |
| +} |
| + |
| +// The busy rule (estilo's busy): the page links busy.css and busy.js, and |
| +// the shim it serves is estilo's — rastrillo's without the old busy rule, |
| +// which would fight the new one. |
| +func TestBusyRuleIsLinkedAndServed(t *testing.T) { |
| + app, err := NewForTest(t) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + page := serve(app, httptest.NewRequest(http.MethodGet, "/signin", nil)).Body.String() |
| + for name, body := range map[string][]byte{"static/estilo/busy.css": busy.CSS(), "static/rastrillo.js": busy.ShimJS(), "static/estilo/busy.js": busy.JS()} { |
| + sum := sha256.Sum256(body) |
| + hashed := "/" + strings.TrimSuffix(name, path.Ext(name)) + fmt.Sprintf(".%x", sum[:8]) + path.Ext(name) |
| + if !strings.Contains(page, `"`+hashed+`"`) { |
| + t.Errorf("sign-in page does not link %s", hashed) |
| + } |
| + if res := serve(app, httptest.NewRequest(http.MethodGet, hashed, nil)); res.Code != http.StatusOK || !bytes.Equal(res.Body.Bytes(), body) { |
| + t.Errorf("GET %s = %d or the wrong bytes", hashed, res.Code) |
| + } |
| + } |
| +} |
diff --git a/internal/docs/views/layout.html b/internal/docs/views/layout.html| index b992966..bfffd90 100644 |
| --- a/internal/docs/views/layout.html |
| +++ b/internal/docs/views/layout.html |
| @@ -17,13 +17,17 @@ |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| <title>{{range .Frame.TitleParts}}{{.}} · {{end}}{{.Frame.Chrome.App.Name}}</title> |
| +<link rel="stylesheet" href="/static/estilo/fonts/v1/fonts.css"> |
| <link rel="stylesheet" href="{{asset "static/tokens.css"}}"> |
| +<link rel="stylesheet" href="{{asset "static/estilo/busy.css"}}"> |
| <link rel="stylesheet" href="{{asset "static/theme.css"}}"> |
| {{template "head" .}} |
| <link rel="stylesheet" href="{{asset "static/estilo/base.css"}}"> |
| <link rel="stylesheet" href="{{asset "static/estilo/menu.css"}}"> |
| <link rel="stylesheet" href="{{asset "static/estilo/chrome.css"}}"> |
| +<link rel="stylesheet" href="{{asset "static/estilo/sky.css"}}"> |
| <script defer src="{{asset "static/rastrillo.js"}}"></script> |
| +<script defer src="{{asset "static/estilo/busy.js"}}"></script> |
| {{iconAssets}} |
| <script defer src="{{asset "static/select.js"}}"></script> |
| <script defer src="{{asset "static/datetime.js"}}"></script> |
diff --git a/internal/docs/views/signin.html b/internal/docs/views/signin.html| index 0a37e1a..fc34fbc 100644 |
| --- a/internal/docs/views/signin.html |
| +++ b/internal/docs/views/signin.html |
| @@ -13,7 +13,7 @@ |
| {{define "content"}} |
| {{- if .ContinueURL}} |
| <meta http-equiv="refresh" content="0;url={{.ContinueURL}}"> |
| -<section estilo-signin><p><a rst-btn="primary" href="{{.ContinueURL}}">{{.SubmitLabel}}</a></p></section> |
| +{{template "estilo-door-open" .Door}}<p><a rst-btn="primary" href="{{.ContinueURL}}">{{.SubmitLabel}}</a></p>{{template "estilo-door-close"}} |
| {{- else if .Sent}}{{template "estilo-signin-sent" .Sent}} |
| {{- else}}{{template "estilo-signin" .Form}}{{end}} |
| {{end}} |
diff --git a/native/ios/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/native/ios/Assets.xcassets/AppIcon.appiconset/AppIcon.png| index 8d1d92c..39385d8 100644 |
| Binary files a/native/ios/Assets.xcassets/AppIcon.appiconset/AppIcon.png and b/native/ios/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ |
diff --git a/native/macos/AppIcon.icns b/native/macos/AppIcon.icns| index da1a204..af5e5fc 100644 |
| Binary files a/native/macos/AppIcon.icns and b/native/macos/AppIcon.icns differ |
diff --git a/scripts/build-release.sh b/scripts/build-release.sh| index 5ecafa1..d829974 100755 |
| --- a/scripts/build-release.sh |
| +++ b/scripts/build-release.sh |
| @@ -29,8 +29,12 @@ docs_source=$(mktemp -d) |
| trap 'rm -rf -- "$docs_source"' EXIT |
| git archive "$docs_revision" | tar -x -C "$docs_source" |
| cd "$docs_source" |
| +# -buildvcs=false: this tree is a git archive with no repository of its |
| +# own, and the version is stamped above. Left on, Go walks up from the |
| +# temp directory into whatever repository encloses it — on a CI runner, |
| +# an unrelated checkout it cannot read — and fails the build (2026-09-29). |
| CGO_ENABLED=0 GOOS="${GOOS:-linux}" GOARCH="${GOARCH:-arm64}" \ |
| - go build -mod=readonly -trimpath \ |
| + go build -mod=readonly -trimpath -buildvcs=false \ |
| -ldflags "-s -w -X github.com/carlosframework/rastrillo.BuildVersion=$docs_version" \ |
| -o "$docs_output" ./cmd/docs |