Add approved Oficina app icons
Wire the shared white Lucide artwork into product branding. Source review: GPT-6 Astra (independent agent).
Co-Authored-By: Codex <noreply@openai.com>
11 files changed,
+92
−100
internal/docs/brand/LICENSE.lucide+43 −0internal/docs/brand/apple-touch-icon.pngbinaryinternal/docs/brand/brand.css+19 −0internal/docs/brand/icon-32.pngbinaryinternal/docs/brand/icon.svg+5 −44internal/docs/list_test.go+3 −2internal/docs/render.go+10 −2internal/docs/views/head.html+9 −49internal/docs/views/list.html+1 −1internal/docs/views/public.html+1 −1internal/docs/views/show.html+1 −1
CI passed — run details
diff --git a/internal/docs/brand/LICENSE.lucide b/internal/docs/brand/LICENSE.lucide| new file mode 100644 |
| index 0000000..718bb3f |
| --- /dev/null |
| +++ b/internal/docs/brand/LICENSE.lucide |
| @@ -0,0 +1,43 @@ |
| +ISC License |
| + |
| +Copyright (c) 2026 Lucide Icons and Contributors |
| + |
| +Permission to use, copy, modify, and/or distribute this software for any |
| +purpose with or without fee is hereby granted, provided that the above |
| +copyright notice and this permission notice appear in all copies. |
| + |
| +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
| +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| + |
| +--- |
| + |
| +The following Lucide icons are derived from the Feather project: |
| + |
| +airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out |
| + |
| +The MIT License (MIT) (for the icons listed above) |
| + |
| +Copyright (c) 2013-present Cole Bemis |
| + |
| +Permission is hereby granted, free of charge, to any person obtaining a copy |
| +of this software and associated documentation files (the "Software"), to deal |
| +in the Software without restriction, including without limitation the rights |
| +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| +copies of the Software, and to permit persons to whom the Software is |
| +furnished to do so, subject to the following conditions: |
| + |
| +The above copyright notice and this permission notice shall be included in all |
| +copies or substantial portions of the Software. |
| + |
| +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| +SOFTWARE. |
diff --git a/internal/docs/brand/apple-touch-icon.png b/internal/docs/brand/apple-touch-icon.png| new file mode 100644 |
| index 0000000..d75bdba |
| Binary files /dev/null and b/internal/docs/brand/apple-touch-icon.png differ |
diff --git a/internal/docs/brand/brand.css b/internal/docs/brand/brand.css| new file mode 100644 |
| index 0000000..2e194c6 |
| --- /dev/null |
| +++ b/internal/docs/brand/brand.css |
| @@ -0,0 +1,19 @@ |
| +/* Oficina brand artwork. Source: oficina/estilo brand/. */ |
| +[estilo-brand] { |
| + display: inline-flex; |
| + align-items: center; |
| + gap: .5rem; |
| + min-block-size: 24px; |
| +} |
| +[estilo-brand-icon] { |
| + display: inline-block; |
| + flex-shrink: 0; |
| + inline-size: 1.75rem; |
| + block-size: 1.75rem; |
| + border-radius: 21%; |
| + vertical-align: middle; |
| +} |
| +[estilo-brand-icon="listing"] { |
| + inline-size: 2.5rem; |
| + block-size: 2.5rem; |
| +} |
diff --git a/internal/docs/brand/icon-32.png b/internal/docs/brand/icon-32.png| new file mode 100644 |
| index 0000000..b61cb5f |
| Binary files /dev/null and b/internal/docs/brand/icon-32.png differ |
diff --git a/internal/docs/brand/icon.svg b/internal/docs/brand/icon.svg| index 52f4376..9b7f4c6 100644 |
| --- a/internal/docs/brand/icon.svg |
| +++ b/internal/docs/brand/icon.svg |
| @@ -1,44 +1,5 @@ |
| -<!-- CARLOS Docs' tab icon. Drawn here rather than taken from an icon |
| - set: rastrillo ships no page or document glyph (rastrillo/icons.go |
| - carries thirteen, none of them this), and a Unicode character |
| - standing in for a mark is not an icon. |
| - |
| - A sheet with a turned corner, on a tile in the day theme's accent |
| - (#2464e0, the light value of the rst-accent custom property in |
| - ui/themes/day.css). Taken as a literal because an SVG served as a |
| - static file cannot read a CSS custom property, and because this |
| - mark must not repaint with a reader's colour scheme the way the |
| - chrome around it does. |
| - |
| - Sized for 16px first: one filled tile, one white silhouette, one |
| - turned corner, and two rules for the text. Everything is at least |
| - 1.5 units across at a 32 unit viewBox, so nothing thins below a |
| - device pixel at tab size. The rules are what make it read as a |
| - document rather than a blank file, and they stop at two because a |
| - third closes the gap between them into grey mush. |
| - |
| - Why brand/ and not static/, which is where this file was first |
| - written: internal/docs/static/ is build output, not a place to |
| - keep things. The gate rebuilds the editor bundle into it and then |
| - fails on any dirt at all in that directory |
| - (.amadan/ci.d/10-gate's "the editor bundle must match a fresh |
| - build"), before restoring it with git checkout and git clean. |
| - A hand written file there is therefore not merely untidy: it |
| - turns the gate red, and then the gate's own cleanup deletes it. |
| - It did, once. Hand authored assets belong beside the build |
| - output, never inside it. |
| - |
| - Note for whoever edits this comment: an XML comment may not |
| - contain two consecutive hyphens, so a CSS custom property cannot |
| - be written here with its leading pair. The first version of this |
| - file did exactly that, and the result was a well formed HTTP 200 |
| - serving a document no browser could parse: right status, right |
| - Content-Type, right byte count, broken image. assets_test.go's |
| - TestIconIsWellFormedXML exists because of it. --> |
| -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32"> |
| - <rect width="32" height="32" rx="7" fill="#2464e0"/> |
| - <path d="M9.5 8.5A2 2 0 0 1 11.5 6.5h6.6L23.5 11.9V23.5a2 2 0 0 1-2 2h-10a2 2 0 0 1-2-2Z" fill="#ffffff"/> |
| - <path d="M18.1 6.5 23.5 11.9h-5.4Z" fill="#a9c4f4"/> |
| - <rect x="12.9" y="15.6" width="7.6" height="1.7" rx="0.85" fill="#2464e0"/> |
| - <rect x="12.9" y="19.2" width="5.2" height="1.7" rx="0.85" fill="#2464e0"/> |
| -</svg> |
| +<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> |
diff --git a/internal/docs/list_test.go b/internal/docs/list_test.go| index 77bd794..95b96fd 100644 |
| --- a/internal/docs/list_test.go |
| +++ b/internal/docs/list_test.go |
| @@ -456,7 +456,7 @@ func TestDocumentPageLinksBackToTheList(t *testing.T) { |
| } |
| body := rec.Body.String() |
| - if !strings.Contains(body, `<a rst-shell-brand href="/">`) { |
| + if !strings.Contains(body, `<a rst-shell-brand estilo-brand href="/">`) { |
| t.Error("no brand link to / on the document page — a document reached directly has no route to the list") |
| } |
| @@ -481,7 +481,8 @@ func TestDocumentPageLinksBackToTheList(t *testing.T) { |
| t.Fatalf("list status %d", listRec.Code) |
| } |
| listTitle := betweenTags(t, listRec.Body.String(), "<h1>", "</h1>") |
| - backLabel := betweenTags(t, body, `<a rst-shell-brand href="/">`, "</a>") |
| + backLabel := betweenTags(t, body, `<a rst-shell-brand estilo-brand href="/">`, "</a>") |
| + backLabel = regexp.MustCompile(`<[^>]+>`).ReplaceAllString(backLabel, "") |
| if backLabel == "" || backLabel != listTitle { |
| t.Errorf("back link label = %q, want the list's own <h1> (%q)", backLabel, listTitle) |
| } |
diff --git a/internal/docs/render.go b/internal/docs/render.go| index e7dea7a..2e2e741 100644 |
| --- a/internal/docs/render.go |
| +++ b/internal/docs/render.go |
| @@ -597,7 +597,7 @@ func vendoredStaticFS() (fs.FS, error) { |
| if !ok { |
| return nil, fmt.Errorf("docs: rastrillo ui: theme %q not shipped", dayTheme) |
| } |
| - return fstest.MapFS{ |
| + 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()}, |
| @@ -605,5 +605,13 @@ func vendoredStaticFS() (fs.FS, error) { |
| "static/rastrillo.js": {Data: ui.ShimJS()}, |
| "static/select.js": {Data: ui.SelectJS()}, |
| "static/datetime.js": {Data: ui.DatetimeJS()}, |
| - }, nil |
| + } |
| + for _, name := range []string{"icon-32.png", "apple-touch-icon.png", "brand.css", "LICENSE.lucide"} { |
| + body, err := brandFS.ReadFile("brand/" + name) |
| + if err != nil { |
| + return nil, err |
| + } |
| + files["static/brand/"+name] = &fstest.MapFile{Data: body} |
| + } |
| + return files, nil |
| } |
diff --git a/internal/docs/views/head.html b/internal/docs/views/head.html| index 125e95d..f01046e 100644 |
| --- a/internal/docs/views/head.html |
| +++ b/internal/docs/views/head.html |
| @@ -1,49 +1,9 @@ |
| -{{/* head — this app's slot in the shell's <head>. |
| - |
| - rastrillo's three shells all end their head with |
| - {{block "head" .}}{{end}}, described in ui/layouts/column.html as |
| - "the app's own slot in <head>: a favicon, a meta tag, one more |
| - stylesheet". This file redefines that block once, in the base |
| - template every page is cloned from (render.go's newRenderer), so |
| - all three pages inherit it and there is exactly one place the |
| - icon is declared. A page that ever needs head content of its own |
| - defines "head" in its own template and overrides this, which is |
| - ordinary block behaviour and needs nothing added here. |
| - |
| - Before this existed the app declared no icon at all, so every |
| - browser fell back to asking for /favicon.ico, got a 404, and |
| - showed the generic blank page mark — on a document editor, where |
| - the tab strip is the thing a person navigates between open |
| - documents with, and where two sibling apps (Sheets, and the |
| - suite home) sit in the same tab strip under the same suite. |
| - |
| - The href is a literal rather than {{asset}}: icon.svg is served |
| - by assets.go's own staticHandler at a fixed, unhashed iconPath, |
| - not out of the rastrillo.Assets tree {{asset}} fingerprints. |
| - assets_test.go asserts this markup and that constant agree, so |
| - the pair cannot drift into a silent 404. */}} |
| -{{/* The one app-wide style rule, and it is here for the same reason |
| - the icon is: it belongs to every page and must be declared once. |
| - |
| - [rst-shell-brand] is now a real target on the signed-in screens — |
| - it is the route back to the list, which used to be [docs-back] in |
| - show.html's own header. tokens.css gives the brand colour, weight |
| - and no padding at all, so it measures 21px tall against 14px body |
| - text at line-height 1.5. AGENTS.md commits to WCAG 2.2 AA with no |
| - exemptions, and SC 2.5.8's 24x24 floor is one of the three it |
| - names. 21px clears that only through the spacing exception, which |
| - is a different argument that stops holding the moment anything |
| - lands beside the brand. |
| - |
| - [docs-back] had already learned this and stated the floor outright |
| - rather than hoping padding added up to it — measured, its first |
| - version came to 22px because --rst-fs-sm is 12.5px and not the |
| - 14px it was assumed to be. Deleting [docs-back] deleted that |
| - finding with it; this is the same rule at the link's new address. |
| - inline-flex is what makes min-block-size apply to a link at all. |
| - |
| - Written against the framework's own element rather than proposed |
| - upstream yet: spec §10 says a component we keep editing belongs |
| - upstream, and if the sibling apps want the same floor this is the |
| - rule to send. It is inert on sign-in, which hides the bar. */}} |
| -{{define "head"}}<link rel="icon" type="image/svg+xml" href="/static/icon.svg"><style>[rst-shell-brand]{display:inline-flex;align-items:center;min-block-size:24px}</style>{{end}} |
| +{{/* App-wide brand assets. SVG keeps its existing public URL; the other |
| + exports use the renderer's content-hashed asset tree. Source: Estilo brand/. */}} |
| +{{define "head"}} |
| +<link rel="icon" type="image/png" sizes="32x32" href="{{asset "static/brand/icon-32.png"}}"> |
| +<link rel="icon" type="image/svg+xml" href="/static/icon.svg"> |
| +<link rel="apple-touch-icon" sizes="180x180" href="{{asset "static/brand/apple-touch-icon.png"}}"> |
| +<link rel="stylesheet" href="{{asset "static/brand/brand.css"}}"> |
| +<style>[rst-shell-brand]{display:inline-flex;align-items:center;min-block-size:24px}</style> |
| +{{end}} |
diff --git a/internal/docs/views/list.html b/internal/docs/views/list.html| index cfe9fda..aeb4c84 100644 |
| --- a/internal/docs/views/list.html |
| +++ b/internal/docs/views/list.html |
| @@ -52,7 +52,7 @@ |
| do: the bar names the app, the heading names the screen, and on |
| the one screen where those are the same thing they read as the |
| same thing. */}} |
| -{{define "brand"}}<a rst-shell-brand href="/">{{.Title}}</a>{{end}} |
| +{{define "brand"}}<a rst-shell-brand estilo-brand href="/"><img estilo-brand-icon src="/static/icon.svg" alt="" width="28" height="28">{{.Title}}</a>{{end}} |
| {{/* account is the dropdown's MENU BODY, not the whole menu — see |
| show.html's own copy of this block for the reasoning; the two are |
| identical on purpose, and are two definitions rather than one |
diff --git a/internal/docs/views/public.html b/internal/docs/views/public.html| index c85172b..db3bbc3 100644 |
| --- a/internal/docs/views/public.html |
| +++ b/internal/docs/views/public.html |
| @@ -17,7 +17,7 @@ |
| guard anyway, so the mount point would only produce a page that |
| fails at the network. Ruling 15's other half: every write has a |
| named actor, and whoever is reading this has no name. */}} |
| -{{define "brand"}}<a rst-shell-brand href="/">{{.Title}}</a>{{end}} |
| +{{define "brand"}}<a rst-shell-brand estilo-brand href="/"><img estilo-brand-icon src="/static/icon.svg" alt="" width="28" height="28">{{.Title}}</a>{{end}} |
| {{define "content"}} |
| <article rst-stack docs-public> |
| <p rst-notice docs-public-notice>{{.ReadOnly}}</p> |
diff --git a/internal/docs/views/show.html b/internal/docs/views/show.html| index 6b3d88e..83ead77 100644 |
| --- a/internal/docs/views/show.html |
| +++ b/internal/docs/views/show.html |
| @@ -57,7 +57,7 @@ |
| The title is printed as-is, the same way list.html's row titles |
| are — it is the document's own data, not authored copy, so it |
| never goes through rastrillo.T. */}} |
| -{{define "brand"}}<a rst-shell-brand href="/">{{.BackLabel}}</a><span docs-shell-title>{{.Title}}</span>{{end}} |
| +{{define "brand"}}<a rst-shell-brand estilo-brand href="/"><img estilo-brand-icon src="/static/icon.svg" alt="" width="28" height="28">{{.BackLabel}}</a><span docs-shell-title>{{.Title}}</span>{{end}} |
| {{/* account is the dropdown's MENU BODY, not the whole menu: the |
| shell owns the <details>/<summary> and this block supplies only |
| what sits inside [rst-dropdown-menu]. A <form> rather than a |