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>
20 files changed,
+223
−10
internal/home/app.go+1 −0internal/home/brand.go+34 −0internal/home/brand_test.go+73 −0internal/home/directory.go+1 −0internal/home/directory_catalog.go+7 −7internal/home/origins_test.go+4 −2internal/home/page.go+4 −0internal/home/static/brand/LICENSE.lucide+43 −0internal/home/static/brand/apple-touch-icon.pngbinaryinternal/home/static/brand/brand.css+19 −0internal/home/static/brand/calendar.svg+10 −0internal/home/static/brand/docs.svg+5 −0internal/home/static/brand/icon-32.pngbinaryinternal/home/static/brand/icon.svg+3 −0internal/home/static/brand/meet.svg+2 −0internal/home/static/brand/memoria.svg+4 −0internal/home/static/brand/sheets.svg+4 −0internal/home/static/brand/slides.svg+3 −0internal/home/templates/directory.html+2 −1internal/home/templates/layout.html+4 −0
CI passed — run details
diff --git a/internal/home/app.go b/internal/home/app.go| index 5e4be83..c3cddc6 100644 |
| --- a/internal/home/app.go |
| +++ b/internal/home/app.go |
| @@ -234,6 +234,7 @@ func (a *App) Router() http.Handler { |
| // `rastrillo doctor --fix` is what reconciles it on an upgrade. |
| r.Get("/static/tokens.css", serveAsset(staticAsset("tokens.css"), "text/css; charset=utf-8")) |
| r.Get("/static/theme.css", serveAsset(theme.CSS(), "text/css; charset=utf-8")) |
| + mountBrand(r) |
| return r |
| } |
diff --git a/internal/home/brand.go b/internal/home/brand.go| new file mode 100644 |
| index 0000000..08bbe85 |
| --- /dev/null |
| +++ b/internal/home/brand.go |
| @@ -0,0 +1,34 @@ |
| +package home |
| + |
| +import ( |
| + "embed" |
| + |
| + "github.com/go-chi/chi/v5" |
| +) |
| + |
| +//go:embed static/brand |
| +var brandFS embed.FS |
| + |
| +// Brand assets are public on both Home origins. The closed file list avoids |
| +// exposing directory listings or treating a request path as a file name. |
| +func mountBrand(r chi.Router) { |
| + for name, contentType := range map[string]string{ |
| + "icon.svg": "image/svg+xml", |
| + "icon-32.png": "image/png", |
| + "apple-touch-icon.png": "image/png", |
| + "brand.css": "text/css; charset=utf-8", |
| + "LICENSE.lucide": "text/plain; charset=utf-8", |
| + "docs.svg": "image/svg+xml", |
| + "sheets.svg": "image/svg+xml", |
| + "slides.svg": "image/svg+xml", |
| + "calendar.svg": "image/svg+xml", |
| + "meet.svg": "image/svg+xml", |
| + "memoria.svg": "image/svg+xml", |
| + } { |
| + body, err := brandFS.ReadFile("static/brand/" + name) |
| + if err != nil { |
| + panic(err) // A missing embedded release asset is a build error. |
| + } |
| + r.Get("/static/brand/"+name, serveAsset(body, contentType)) |
| + } |
| +} |
diff --git a/internal/home/brand_test.go b/internal/home/brand_test.go| new file mode 100644 |
| index 0000000..9ead1f9 |
| --- /dev/null |
| +++ b/internal/home/brand_test.go |
| @@ -0,0 +1,73 @@ |
| +package home |
| + |
| +import ( |
| + "bytes" |
| + "image/png" |
| + "os" |
| + "path/filepath" |
| + "regexp" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// Mutation-checked 2026-09-12 with cases chosen by an independent reviewer: |
| +// wrong icon.svg MIME type and removal of the directory mount both fail. |
| +func TestDirectoryBrandAssetsArePublicAndListingKeepsNames(t *testing.T) { |
| + f := newMountedChooser(t) |
| + page := f.request("GET", "/", "", "", f.session(t, chooserPerson)) |
| + if page.Code != 200 { |
| + t.Fatal(page.Code) |
| + } |
| + body := page.Body.String() |
| + for _, name := range []string{"Docs", "Sheets", "Slides", "Calendar", "Meet", "Memoria"} { |
| + path := "/static/brand/" + strings.ToLower(name) + ".svg" |
| + if !strings.Contains(body, `src="`+path+`" alt=""`) || !strings.Contains(body, ">"+name+"<") { |
| + t.Fatal("listing lost icon or name", name) |
| + } |
| + // No session on the asset request: every sign-in surface needs its |
| + // branding before the reader has authenticated. |
| + asset := f.request("GET", path, "", "", nil) |
| + if asset.Code != 200 || asset.Header().Get("Content-Type") != "image/svg+xml" || !strings.Contains(asset.Body.String(), "<title>"+name+"</title>") || len(asset.Result().Cookies()) != 0 { |
| + t.Fatal("wrong or inaccessible product artwork", name, asset.Code) |
| + } |
| + } |
| + // Every asset linked by the rendered directory must work anonymously, |
| + // including Oficina's own favicon and the PNG home-screen exports. |
| + for _, match := range regexp.MustCompile(`(?:href|src)="(/static/[^" ]+)"`).FindAllStringSubmatch(body, -1) { |
| + asset := f.request("GET", match[1], "", "", nil) |
| + if asset.Code != 200 || asset.Body.Len() == 0 || len(asset.Result().Cookies()) != 0 { |
| + t.Fatal("public page asset unavailable", match[1]) |
| + } |
| + switch filepath.Ext(match[1]) { |
| + case ".svg": |
| + if asset.Header().Get("Content-Type") != "image/svg+xml" || !bytes.Contains(asset.Body.Bytes(), []byte("<svg")) { |
| + t.Fatal("SVG cannot render", match[1]) |
| + } |
| + case ".png": |
| + if _, err := png.DecodeConfig(bytes.NewReader(asset.Body.Bytes())); err != nil || asset.Header().Get("Content-Type") != "image/png" { |
| + t.Fatal("PNG cannot render", match[1], err) |
| + } |
| + case ".css": |
| + if !strings.HasPrefix(asset.Header().Get("Content-Type"), "text/css") { |
| + t.Fatal("stylesheet cannot load", match[1]) |
| + } |
| + } |
| + } |
| + if output := os.Getenv("HOME_BRAND_EVIDENCE_DIR"); output != "" { |
| + if err := os.MkdirAll(output, 0700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(output, "page.html"), page.Body.Bytes(), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, match := range regexp.MustCompile(`(?:href|src)="(/static/[^" ]+)"`).FindAllStringSubmatch(body, -1) { |
| + asset := f.request("GET", match[1], "", "", nil) |
| + if asset.Code != 200 { |
| + t.Fatal("page asset unavailable", match[1]) |
| + } |
| + if err := os.WriteFile(filepath.Join(output, filepath.Base(match[1])), asset.Body.Bytes(), 0600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + } |
| +} |
diff --git a/internal/home/directory.go b/internal/home/directory.go| index cead9f3..691e2be 100644 |
| --- a/internal/home/directory.go |
| +++ b/internal/home/directory.go |
| @@ -94,6 +94,7 @@ func (a *App) directoryRouter() http.Handler { |
| r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) |
| r.Get("/static/tokens.css", serveAsset(staticAsset("tokens.css"), "text/css; charset=utf-8")) |
| r.Get("/static/theme.css", serveAsset(theme.CSS(), "text/css; charset=utf-8")) |
| + mountBrand(r) |
| }) |
| return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| if req.URL.Path == rosterPath { |
diff --git a/internal/home/directory_catalog.go b/internal/home/directory_catalog.go| index 6cdab5a..fb0a76c 100644 |
| --- a/internal/home/directory_catalog.go |
| +++ b/internal/home/directory_catalog.go |
| @@ -3,24 +3,24 @@ package home |
| import "strings" |
| type catalogApp struct { |
| - Name, Href, Status string |
| + Name, Href, Status, Icon string |
| } |
| // This display catalog grants no integration or provisioning capability. |
| // Those remain governed by directoryApps and the destination checks. |
| func directoryCatalog(cfg Runtime) []catalogApp { |
| - docs := catalogApp{Name: "Docs", Href: docsEntryPath, Status: "Choose a team"} |
| + docs := catalogApp{Name: "Docs", Icon: "/static/brand/docs.svg", Href: docsEntryPath, Status: "Choose a team"} |
| if cfg.docsPreviewOrigin != "" { |
| docs.Href = cfg.docsPreviewOrigin + "/signin" |
| docs.Status = "Open preview" |
| } |
| apps := []catalogApp{ |
| docs, |
| - {Name: "Sheets", Status: "Not connected to Home yet"}, |
| - {Name: "Slides", Status: "Not connected to Home yet"}, |
| - {Name: "Calendar", Status: "Not connected to Home yet"}, |
| - {Name: "Meet", Status: "Coming soon"}, |
| - {Name: "Memoria", Status: "Not connected to Home yet"}, |
| + {Name: "Sheets", Icon: "/static/brand/sheets.svg", Status: "Not connected to Home yet"}, |
| + {Name: "Slides", Icon: "/static/brand/slides.svg", Status: "Not connected to Home yet"}, |
| + {Name: "Calendar", Icon: "/static/brand/calendar.svg", Status: "Not connected to Home yet"}, |
| + {Name: "Meet", Icon: "/static/brand/meet.svg", Status: "Coming soon"}, |
| + {Name: "Memoria", Icon: "/static/brand/memoria.svg", Status: "Not connected to Home yet"}, |
| {Name: "Mail", Status: "Separate sign-in"}, |
| {Name: "Files", Status: "Separate sign-in"}, |
| } |
diff --git a/internal/home/origins_test.go b/internal/home/origins_test.go| index 51b8fa6..89647ac 100644 |
| --- a/internal/home/origins_test.go |
| +++ b/internal/home/origins_test.go |
| @@ -15,7 +15,8 @@ func TestEveryCredentialRouteRefusesDirectoryHost(t *testing.T) { |
| count := 0 |
| err := chi.Walk(routes, func(method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error { |
| switch route { |
| - case "/", "/healthz", "/static/tokens.css", "/static/theme.css": |
| + case "/", "/healthz", "/static/tokens.css", "/static/theme.css", |
| + "/static/brand/icon.svg", "/static/brand/icon-32.png", "/static/brand/apple-touch-icon.png", "/static/brand/brand.css", "/static/brand/LICENSE.lucide", "/static/brand/docs.svg", "/static/brand/sheets.svg", "/static/brand/slides.svg", "/static/brand/calendar.svg", "/static/brand/meet.svg", "/static/brand/memoria.svg": |
| return nil |
| } |
| count++ |
| @@ -80,7 +81,8 @@ func TestDirectoryIsNotWithinCredentialRP(t *testing.T) { |
| func TestSharedRoutesOnBothOrigins(t *testing.T) { |
| app := newTestApp(t) |
| for _, host := range []string{credentialHost, directoryHost} { |
| - for _, path := range []string{"/healthz", "/static/tokens.css", "/static/theme.css"} { |
| + for _, path := range []string{"/healthz", "/static/tokens.css", "/static/theme.css", |
| + "/static/brand/icon.svg", "/static/brand/icon-32.png", "/static/brand/apple-touch-icon.png", "/static/brand/brand.css", "/static/brand/LICENSE.lucide", "/static/brand/docs.svg", "/static/brand/sheets.svg", "/static/brand/slides.svg", "/static/brand/calendar.svg", "/static/brand/meet.svg", "/static/brand/memoria.svg"} { |
| req := httptest.NewRequest(http.MethodGet, "https://"+host+path, nil) |
| rec := httptest.NewRecorder() |
| app.Router().ServeHTTP(rec, req) |
diff --git a/internal/home/page.go b/internal/home/page.go| index 43d8a7c..796e11d 100644 |
| --- a/internal/home/page.go |
| +++ b/internal/home/page.go |
| @@ -16,6 +16,10 @@ import "net/http" |
| const indexPage = `<!doctype html> |
| <html lang="en"> |
| <head> |
| +<link rel="icon" type="image/png" sizes="32x32" href="/static/brand/icon-32.png"> |
| +<link rel="icon" type="image/svg+xml" href="/static/brand/icon.svg"> |
| +<link rel="apple-touch-icon" sizes="180x180" href="/static/brand/apple-touch-icon.png"> |
| +<link rel="stylesheet" href="/static/brand/brand.css"> |
| <meta charset="utf-8"> |
| <title>Oficina home</title> |
| </head> |
diff --git a/internal/home/static/brand/LICENSE.lucide b/internal/home/static/brand/LICENSE.lucide| new file mode 100644 |
| index 0000000..718bb3f |
| --- /dev/null |
| +++ b/internal/home/static/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/home/static/brand/apple-touch-icon.png b/internal/home/static/brand/apple-touch-icon.png| new file mode 100644 |
| index 0000000..97b522c |
| Binary files /dev/null and b/internal/home/static/brand/apple-touch-icon.png differ |
diff --git a/internal/home/static/brand/brand.css b/internal/home/static/brand/brand.css| new file mode 100644 |
| index 0000000..2e194c6 |
| --- /dev/null |
| +++ b/internal/home/static/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/home/static/brand/calendar.svg b/internal/home/static/brand/calendar.svg| new file mode 100644 |
| index 0000000..97108f8 |
| --- /dev/null |
| +++ b/internal/home/static/brand/calendar.svg |
| @@ -0,0 +1,10 @@ |
| +<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-label="Calendar"><title>Calendar</title><rect x="0" y="0" width="1024" height="1024" rx="0" fill="#CB3F50"/><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="M8 2v3" /> |
| + <path d="M16 2v3" /> |
| + <rect x="3" y="3" width="18" height="18" rx="2" /> |
| + <path d="M3 9h18" /> |
| + <path d="M8 13h.01" /> |
| + <path d="M12 13h.01" /> |
| + <path d="M16 13h.01" /> |
| + <path d="M8 17h.01" /> |
| + <path d="M12 17h.01" /> |
| + <path d="M16 17h.01" /></g></svg> |
diff --git a/internal/home/static/brand/docs.svg b/internal/home/static/brand/docs.svg| new file mode 100644 |
| index 0000000..9b7f4c6 |
| --- /dev/null |
| +++ b/internal/home/static/brand/docs.svg |
| @@ -0,0 +1,5 @@ |
| +<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/home/static/brand/icon-32.png b/internal/home/static/brand/icon-32.png| new file mode 100644 |
| index 0000000..bbfaca7 |
| Binary files /dev/null and b/internal/home/static/brand/icon-32.png differ |
diff --git a/internal/home/static/brand/icon.svg b/internal/home/static/brand/icon.svg| new file mode 100644 |
| index 0000000..f30e545 |
| --- /dev/null |
| +++ b/internal/home/static/brand/icon.svg |
| @@ -0,0 +1,3 @@ |
| +<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-label="Oficina"><title>Oficina</title><rect x="0" y="0" width="1024" height="1024" rx="0" fill="#303642"/><g transform="translate(153.6 153.6) scale(29.866666666666664)" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" /> |
| + <path d="M3 9h18" /> |
| + <path d="M9 21V9" /></g></svg> |
diff --git a/internal/home/static/brand/meet.svg b/internal/home/static/brand/meet.svg| new file mode 100644 |
| index 0000000..a027150 |
| --- /dev/null |
| +++ b/internal/home/static/brand/meet.svg |
| @@ -0,0 +1,2 @@ |
| +<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-label="Meet"><title>Meet</title><rect x="0" y="0" width="1024" height="1024" rx="0" fill="#087F8C"/><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="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5" /> |
| + <rect x="2" y="6" width="14" height="12" rx="2" /></g></svg> |
diff --git a/internal/home/static/brand/memoria.svg b/internal/home/static/brand/memoria.svg| new file mode 100644 |
| index 0000000..6d7c4c8 |
| --- /dev/null |
| +++ b/internal/home/static/brand/memoria.svg |
| @@ -0,0 +1,4 @@ |
| +<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-label="Memoria"><title>Memoria</title><rect x="0" y="0" width="1024" height="1024" rx="0" fill="#7C3FC8"/><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="M11 22H5.5a1 1 0 0 1 0-5h4.501" /> |
| + <path d="m21 22-1.879-1.878" /> |
| + <path d="M3 19.5v-15A2.5 2.5 0 0 1 5.5 2H18a1 1 0 0 1 1 1v8" /> |
| + <circle cx="17" cy="18" r="3" /></g></svg> |
diff --git a/internal/home/static/brand/sheets.svg b/internal/home/static/brand/sheets.svg| new file mode 100644 |
| index 0000000..3739ca0 |
| --- /dev/null |
| +++ b/internal/home/static/brand/sheets.svg |
| @@ -0,0 +1,4 @@ |
| +<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-label="Sheets"><title>Sheets</title><rect x="0" y="0" width="1024" height="1024" rx="0" fill="#168049"/><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="M12 3v18" /> |
| + <rect width="18" height="18" x="3" y="3" rx="2" /> |
| + <path d="M3 9h18" /> |
| + <path d="M3 15h18" /></g></svg> |
diff --git a/internal/home/static/brand/slides.svg b/internal/home/static/brand/slides.svg| new file mode 100644 |
| index 0000000..ea6ead1 |
| --- /dev/null |
| +++ b/internal/home/static/brand/slides.svg |
| @@ -0,0 +1,3 @@ |
| +<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024" role="img" aria-label="Slides"><title>Slides</title><rect x="0" y="0" width="1024" height="1024" rx="0" fill="#C45B16"/><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="M2 3h20" /> |
| + <path d="M21 3v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V3" /> |
| + <path d="m7 21 5-5 5 5" /></g></svg> |
diff --git a/internal/home/templates/directory.html b/internal/home/templates/directory.html| index 10bebfb..91dfe76 100644 |
| --- a/internal/home/templates/directory.html |
| +++ b/internal/home/templates/directory.html |
| @@ -1,12 +1,13 @@ |
| {{define "title"}}Oficina{{end}} |
| {{define "content"}} |
| -<h1>Oficina</h1> |
| +<h1 estilo-brand><img estilo-brand-icon src="/static/brand/icon.svg" alt="" width="28" height="28">Oficina</h1> |
| <p>You are signed in.</p> |
| <section aria-labelledby="apps-heading"> |
| <h2 id="apps-heading">Apps</h2> |
| <div rst-list> |
| {{range .Apps}} |
| <div rst-row> |
| +{{if .Icon}}<img estilo-brand-icon="listing" src="{{.Icon}}" alt="" width="40" height="40">{{end}} |
| <div rst-row-main> |
| {{if .Href}}<a href="{{.Href}}">{{.Name}}</a>{{else}}<strong>{{.Name}}</strong>{{end}} |
| <span rst-row-sub>{{.Status}}</span> |
diff --git a/internal/home/templates/layout.html b/internal/home/templates/layout.html| index 6054667..4735f6f 100644 |
| --- a/internal/home/templates/layout.html |
| +++ b/internal/home/templates/layout.html |
| @@ -1,6 +1,10 @@ |
| {{define "layout"}}<!doctype html> |
| <html lang="{{block "lang" .}}en{{end}}"> |
| <head> |
| +<link rel="icon" type="image/png" sizes="32x32" href="/static/brand/icon-32.png"> |
| +<link rel="icon" type="image/svg+xml" href="/static/brand/icon.svg"> |
| +<link rel="apple-touch-icon" sizes="180x180" href="/static/brand/apple-touch-icon.png"> |
| +<link rel="stylesheet" href="/static/brand/brand.css"> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1"> |
| <title>{{block "title" .}}Oficina{{end}}</title> |