oficina / docs Public

WIP: the busy rule from estilo (spinner ≥650ms, reset on Back)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 46109ac39a5dbf1f7353604b4c3e4813d9dc62eb parent bf37f8b
5 files changed, +41 −5
  • go.mod +1 −1
  • go.sum +2 −0
  • internal/docs/render.go +12 −4
  • internal/docs/theme_assets_test.go +24 −0
  • internal/docs/views/layout.html +2 −0
diff --git a/go.mod b/go.mod
index 3de4f0f..350c675 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-20260929070530-5b2ce835e773
+ amadan.net/oficina/estilo.git v0.0.0-20260929095046-d0664ee49914
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 62acbd3..a137f12 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ amadan.net/oficina/estilo.git v0.0.0-20260924071126-894afdcb71a9 h1:vHOkWeJ7mRwL
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/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/render.go b/internal/docs/render.go
index 536f1cc..70f89c9 100644
--- a/internal/docs/render.go
+++ b/internal/docs/render.go
@@ -10,6 +10,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"
@@ -625,7 +626,8 @@ func (rd *renderer) pageStatus(w http.ResponseWriter, r *http.Request, name stri
// vendoredStaticFS builds the static/ tree rastrillo.Assets serves —
// 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 (ui.ShimJS/SelectJS/DatetimeJS)
+// 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
@@ -643,9 +645,15 @@ func vendoredStaticFS() (fs.FS, error) {
"static/estilo-save-status.mjs": {Data: savestatus.JS()},
"static/tokens.css": {Data: ui.TokensCSS()},
"static/theme.css": {Data: docsTheme},
- "static/rastrillo.js": {Data: ui.ShimJS()},
- "static/select.js": {Data: ui.SelectJS()},
- "static/datetime.js": {Data: ui.DatetimeJS()},
+ // 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()},
diff --git a/internal/docs/theme_assets_test.go b/internal/docs/theme_assets_test.go
index 0d04693..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"
@@ -76,3 +79,24 @@ func TestOnestIsLinkedAndServed(t *testing.T) {
}
}
}
+
+// 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 09f7e8e..afcdb10 100644
--- a/internal/docs/views/layout.html
+++ b/internal/docs/views/layout.html
@@ -19,12 +19,14 @@
<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"}}">
<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>