Serve the app icons from Go, and note the shared chrome in the README
brand.SVG and brand.Handler serve brand/svg/ at /static/estilo/brand/, so a switcher can show a sibling's icon without vendoring it. calls maps to meet.svg, which the rename left alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+132
−1
README.md+11 −1brand/brand.go+71 −0brand/brand_test.go+50 −0
CI failed — run details
CI log
=== vet === CGO_ENABLED=0 go vet ./... go: downloading gorm.io/gorm v1.31.2 go: downloading gorm.io/plugin/dbresolver v1.6.2 go: downloading github.com/jinzhu/now v1.1.5 go: downloading golang.org/x/text v0.20.0 go: downloading github.com/jinzhu/inflection v1.0.0 ../../cache/go-mod/github.com/carlosframework/rastrillo@v0.23.0/sessions/sessions.go:72:12: pattern migrations/*.sql: no matching files found make: *** [Makefile:22: test-vet] Error 1 amadan: step vet failed: exit status 2 amadan: job failed at step vet — its output is above, not at the tail of this log
diff --git a/README.md b/README.md| index 96db7ba..302947e 100644 |
| --- a/README.md |
| +++ b/README.md |
| @@ -18,7 +18,17 @@ apps. It carries no authentication logic — callers validate the |
| destination and supply translated labels. The screens run without |
| scripts. |
| -App shells and permission/session logic stay app-owned. There is no |
| +`base`, `menu` and `chrome` are the shared chrome from the round-2 spec |
| +(`docs/superpowers/specs/2026-09-23-shared-chrome.md` on |
| +`spec/shared-chrome`): link styling; the one floating menu panel and the |
| +document menu bar, with `menu.Shortcut` for platform-correct shortcut |
| +labels; and the top bar, account menu, app switcher, error page and |
| +sign-in pages, with the per-app Appearance preference. Link the three |
| +stylesheets in that order, after `tokens.css`, the theme and |
| +`brand.css`. `brand` serves the app icons from Go. Docs adopts first and |
| +Sheets second; until Sheets has, they are a home, not an agreed interface. |
| + |
| +Permission and session logic stay app-owned. There is no |
| shared component gallery yet. |
| ## Building |
diff --git a/brand/brand.go b/brand/brand.go| new file mode 100644 |
| index 0000000..211861a |
| --- /dev/null |
| +++ b/brand/brand.go |
| @@ -0,0 +1,71 @@ |
| +// Package brand serves the suite's app icons from Go, so a page can link |
| +// any sibling app's icon (the app switcher) without vendoring the file. |
| +// The artwork itself is brand/svg/, unchanged; see README.md. |
| +package brand |
| + |
| +import ( |
| + "embed" |
| + "net/http" |
| + "strings" |
| +) |
| + |
| +//go:embed svg/*.svg |
| +var svgFS embed.FS |
| + |
| +// Prefix is the URL path Handler serves under. |
| +const Prefix = "/static/estilo/brand/" |
| + |
| +// files maps a public slug to its master. Calls keeps meet.svg: the |
| +// rename to Calls left the artwork's file name alone. |
| +var files = map[string]string{ |
| + "oficina": "oficina.svg", |
| + "docs": "docs.svg", |
| + "sheets": "sheets.svg", |
| + "slides": "slides.svg", |
| + "calendar": "calendar.svg", |
| + "calls": "meet.svg", |
| + "meet": "meet.svg", |
| + "memoria": "memoria.svg", |
| +} |
| + |
| +// SVG returns the icon for slug, or nil for an unknown slug. |
| +func SVG(slug string) []byte { |
| + name, ok := files[slug] |
| + if !ok { |
| + return nil |
| + } |
| + body, err := svgFS.ReadFile("svg/" + name) |
| + if err != nil { |
| + return nil |
| + } |
| + return body |
| +} |
| + |
| +// Path is the URL Handler serves slug's icon at. |
| +func Path(slug string) string { return Prefix + slug + ".svg" } |
| + |
| +// Handler serves GET and HEAD for Prefix + "<slug>.svg". Mount it at |
| +// Prefix. Anything else is 404; the icons are public artwork and need no |
| +// session. |
| +func Handler() http.Handler { |
| + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| + slug, ok := strings.CutSuffix(strings.TrimPrefix(r.URL.Path, Prefix), ".svg") |
| + body := SVG(slug) |
| + if !ok || body == nil || !strings.HasPrefix(r.URL.Path, Prefix) { |
| + http.NotFound(w, r) |
| + return |
| + } |
| + if r.Method != http.MethodGet && r.Method != http.MethodHead { |
| + w.Header().Set("Allow", "GET, HEAD") |
| + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) |
| + return |
| + } |
| + w.Header().Set("Content-Type", "image/svg+xml") |
| + w.Header().Set("Cache-Control", "public, max-age=86400") |
| + w.Header().Set("X-Content-Type-Options", "nosniff") |
| + if r.Method == http.MethodHead { |
| + return |
| + } |
| + _, _ = w.Write(body) |
| + }) |
| +} |
diff --git a/brand/brand_test.go b/brand/brand_test.go| new file mode 100644 |
| index 0000000..d392e9e |
| --- /dev/null |
| +++ b/brand/brand_test.go |
| @@ -0,0 +1,50 @@ |
| +package brand |
| + |
| +import ( |
| + "bytes" |
| + "net/http" |
| + "net/http/httptest" |
| + "os" |
| + "testing" |
| +) |
| + |
| +func TestEveryAppHasAnIcon(t *testing.T) { |
| + for _, slug := range []string{"oficina", "docs", "sheets", "slides", "calendar", "calls", "memoria"} { |
| + body := SVG(slug) |
| + if !bytes.HasPrefix(bytes.TrimSpace(body), []byte("<svg")) { |
| + t.Errorf("%s: not an svg", slug) |
| + } |
| + } |
| + calls, _ := os.ReadFile("svg/meet.svg") |
| + if !bytes.Equal(SVG("calls"), calls) { |
| + t.Error("calls is not meet.svg") |
| + } |
| + if SVG("../go.mod") != nil || SVG("") != nil || SVG("home") != nil { |
| + t.Error("an unknown slug returned artwork") |
| + } |
| +} |
| + |
| +func TestHandler(t *testing.T) { |
| + h := Handler() |
| + get := func(method, path string) *httptest.ResponseRecorder { |
| + w := httptest.NewRecorder() |
| + h.ServeHTTP(w, httptest.NewRequest(method, path, nil)) |
| + return w |
| + } |
| + w := get("GET", Path("docs")) |
| + docs, _ := os.ReadFile("svg/docs.svg") |
| + if w.Code != 200 || w.Header().Get("Content-Type") != "image/svg+xml" || w.Header().Get("X-Content-Type-Options") != "nosniff" || !bytes.Equal(w.Body.Bytes(), docs) { |
| + t.Fatalf("GET docs: %d %q", w.Code, w.Header()) |
| + } |
| + if w := get("HEAD", Path("sheets")); w.Code != 200 || w.Body.Len() != 0 { |
| + t.Fatalf("HEAD: %d %d", w.Code, w.Body.Len()) |
| + } |
| + for _, p := range []string{Prefix + "docs", Prefix + "nope.svg", Prefix + "../brand.css", "/elsewhere/docs.svg", Prefix} { |
| + if w := get("GET", p); w.Code != http.StatusNotFound { |
| + t.Errorf("%s: %d", p, w.Code) |
| + } |
| + } |
| + if w := get("POST", Path("docs")); w.Code != http.StatusMethodNotAllowed { |
| + t.Errorf("POST: %d", w.Code) |
| + } |
| +} |