oficina / estilo Public

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>
Paul Campbell pushed by paul@keymail.dev 485d60371724908fbb5292e99a620259d0942a23 parent 74550cc
3 files changed, +132 −1
  • README.md +11 −1
  • brand/brand.go +71 −0
  • brand/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)
+ }
+}