oficina / estilo Public

Clone
git clone https://amadan.net/oficina/estilo

Plain git — no account needed to clone.

Download

Download this file

1// Package busy carries rastrillo's busy rule to the Oficina apps (Paul,
2// 2026-09-29): a submit button's label gives way to a spinner for at
3// least 650ms, the button disables, its form refuses a second submit, and
4// Back hands the form back.
5//
6// The rule is rastrillo's (ui/busy.js). The apps vendor older rastrillo
7// files, and moving every app to a newer rastrillo would carry far more
8// than this, so estilo carries three byte-exact copies instead, each
9// recorded in provenance.json and checked against it:
10//
11// - ShimJS: rastrillo.js as it is now the busy rule has left it. An app
12// replaces its vendored static/rastrillo.js with these bytes — its old
13// copy still carries the old busy rule, and two busy rules on one
14// page fight over every submit.
15// - JS: busy.js, the rule itself.
16// - CSS: the spinner-in-place block from rastrillo's tokens.css, for a
17// tokens.css that predates it. Link it after tokens.css.
18//
19// When an app next re-vendors rastrillo for real, it takes all three from
20// there and drops this package.
21package busy
22
23import (
24 "crypto/sha256"
25 _ "embed"
26 "encoding/hex"
27 "net/http"
28 "strconv"
29 "strings"
30)
31
32//go:embed rastrillo.js
33var shimJS []byte
34
35//go:embed busy.js
36var busyJS []byte
37
38//go:embed busy.css
39var busyCSS []byte
40
41// ShimJS is rastrillo.js without the busy rule, for an app's static/.
42func ShimJS() []byte { return append([]byte(nil), shimJS...) }
43
44// JS is busy.js.
45func JS() []byte { return append([]byte(nil), busyJS...) }
46
47// CSS is the spinner-in-place block.
48func CSS() []byte { return append([]byte(nil), busyCSS...) }
49
50// Prefix is the URL path Handler serves under. Its version segment is the
51// two files' own digest, so new bytes are always a new URL: both are
52// cached for a year, immutably, and no one has to remember to bump it.
53var Prefix = func() string {
54 sum := sha256.Sum256(append(append([]byte(nil), busyJS...), busyCSS...))
55 return "/static/estilo/busy/" + hex.EncodeToString(sum[:6]) + "/"
56}()
57
58// Handler serves GET for Prefix + "busy.js" and Prefix + "busy.css", and
59// nothing else. Public and sessionless: the sign-in form needs it too.
60func Handler() http.Handler {
61 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
62 var body []byte
63 var kind string
64 switch strings.TrimPrefix(r.URL.Path, Prefix) {
65 case "busy.js":
66 body, kind = busyJS, "text/javascript; charset=utf-8"
67 case "busy.css":
68 body, kind = busyCSS, "text/css; charset=utf-8"
69 }
70 if body == nil || !strings.HasPrefix(r.URL.Path, Prefix) {
71 http.NotFound(w, r)
72 return
73 }
74 if r.Method != http.MethodGet && r.Method != http.MethodHead {
75 w.Header().Set("Allow", "GET, HEAD")
76 http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
77 return
78 }
79 w.Header().Set("Content-Type", kind)
80 w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
81 w.Header().Set("X-Content-Type-Options", "nosniff")
82 w.Header().Set("Content-Length", strconv.Itoa(len(body)))
83 if r.Method == http.MethodHead {
84 return
85 }
86 _, _ = w.Write(body)
87 })
88}
89