oficina / docs Public

Merge remote-tracking branch 'origin/main' into feat/k3-outbox-lane

Paul Campbell pushed by paul@keymail.dev 9ef7e3cd00a5e17a3be2f6b8464e8f29d1f46f7d parent fbaf11dparent ee9ecb1
20 files changed, +166 −39
  • go.mod +1 −1
  • go.sum +12 −0
  • internal/docs/app.go +6 −0
  • internal/docs/auth.go +8 −0
  • internal/docs/auth_navigation.go +4 −0
  • internal/docs/auth_navigation_test.go +6 −0
  • internal/docs/auth_test.go +2 −2
  • internal/docs/brand/apple-touch-icon.png binary
  • internal/docs/brand/brand.css +2 −2
  • internal/docs/brand/icon-32.png binary
  • internal/docs/brand/icon.svg +1 −5
  • internal/docs/home_render.go +3 −0
  • internal/docs/render.go +21 −13
  • internal/docs/signin_door_test.go +42 −0
  • internal/docs/theme_assets_test.go +48 −14
  • internal/docs/views/layout.html +4 −0
  • internal/docs/views/signin.html +1 −1
  • native/ios/Assets.xcassets/AppIcon.appiconset/AppIcon.png binary
  • native/macos/AppIcon.icns binary
  • scripts/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