carlos / platform Public

plan: rastrillo icon vendoring implementation plan

Scoped to §3 of the UI addendum only (Lucide icons) -- §2/§4/§5 depend
on §9's component-vocabulary system, which doesn't exist in rastrillo's
code yet. One task: icons.go + icons_test.go, mirroring
carlosframework/platform's internal/console/icons.go exactly, reusing
its already-tested SVG path data verbatim.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Paul Campbell 6679738132221437c4ae874705c7052978556c46 parent 8d685a0
1 file changed, +227 −0
  • docs/superpowers/plans/2026-08-02-rastrillo-icon-vendoring.md +227 −0
diff --git a/docs/superpowers/plans/2026-08-02-rastrillo-icon-vendoring.md b/docs/superpowers/plans/2026-08-02-rastrillo-icon-vendoring.md
new file mode 100644
index 000000000..f2ffcaa35
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-02-rastrillo-icon-vendoring.md
@@ -0,0 +1,227 @@
+# Rastrillo Icon Vendoring Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Give rastrillo apps a vendored Lucide icon set, usable today from any hand-written `html/template.Template`, via a single `rastrillo.Icon(slug string) template.HTML` function.
+
+**Architecture:** One new file, `icons.go`, at the repo root of `github.com/carlosframework/rastrillo` (`package rastrillo`, alongside the existing `ctx.go`/`serve.go`). Mirrors `internal/console/icons.go` in `carlosframework/platform` exactly — same vendoring discipline (compile-time `template.HTML` constants, no CDN, no runtime fetch), same accessibility convention (`aria-hidden="true"`, icon always paired with a visible label or an explicit `aria-label` on the caller's side), same "unknown slug renders nothing rather than panicking" safety rule. The three icons vendored (`chevron-down`, `check`, `plus`) reuse the EXACT SVG path data already shipped and tested in `internal/console/icons.go` — not re-typed from memory, to avoid any risk of subtly wrong path syntax.
+
+**Tech Stack:** Go `html/template` (`template.HTML`), no new dependencies.
+
+## Global Constraints
+
+- Icons are vendored as Go source constants — never fetched from a CDN or npm package, matching the addendum spec's "no runtime or CDN dependency either way" (`docs/superpowers/specs/2026-08-02-rastrillo-ui-addendum-design.md` §3).
+- Every icon carries `aria-hidden="true"` — icons are always paired with a visible text label or the caller supplies an explicit `aria-label`; this package never renders an icon as a lone accessible name (same spec, §3).
+- ISC licence text for Lucide must be reproduced in the file, not just linked (matches `internal/console/icons.go`'s existing practice).
+- An unknown slug passed to `Icon` returns `""`, never panics (matches console's existing safety rule — a typo costs a missing icon, not a 500/crash).
+- No icon in the vendored set hardcodes its own `width`/`height` — icons size from the consuming app's own CSS (e.g. a `.icon { width: 1em; height: 1em }` rule the app defines), matching console's convention exactly.
+
+---
+
+## Task 1: Vendor the icon set and its lookup function
+
+**Files:**
+- Create: `icons.go` (repo root, `package rastrillo`)
+- Test: `icons_test.go` (repo root, `package rastrillo`)
+
+**Interfaces:**
+- Produces: `rastrillo.Icon(slug string) template.HTML` — exported, the only symbol other packages need. `slug` is the Lucide icon name (e.g. `"chevron-down"`).
+
+- [ ] **Step 1: Write the failing tests**
+
+Create `icons_test.go`:
+
+```go
+package rastrillo
+
+import (
+ "html/template"
+ "strings"
+ "testing"
+)
+
+// Every vendored icon is one self-contained, colour-inheriting,
+// screen-reader-silent 24x24 SVG. Mirrors
+// carlosframework/platform's internal/console/icons_test.go
+// TestVendoredIconsAreSelfContained.
+func TestVendoredIconsAreSelfContained(t *testing.T) {
+ if len(icons) == 0 {
+ t.Fatal("the icon set is empty")
+ }
+ for slug, svg := range icons {
+ s := string(svg)
+ for _, want := range []string{
+ "<svg", "</svg>", `viewBox="0 0 24 24"`,
+ `stroke="currentColor"`, `aria-hidden="true"`, `class="icon"`,
+ } {
+ if !strings.Contains(s, want) {
+ t.Errorf("icon %q is missing %s: %s", slug, want, s)
+ }
+ }
+ for _, bad := range []string{"http://", "https://", "<image", "xlink:href", "url("} {
+ if strings.Contains(s, bad) {
+ t.Errorf("icon %q reaches outside the page (%q): %s", slug, bad, s)
+ }
+ }
+ if strings.Contains(s, " width=") || strings.Contains(s, " height=") {
+ t.Errorf("icon %q hardcodes a size instead of sizing from the caller's CSS: %s", slug, s)
+ }
+ }
+}
+
+// An unknown slug must not panic a caller mid-render -- it renders nothing,
+// same rule as console's icon().
+func TestIconUnknownSlugRendersNothing(t *testing.T) {
+ if got := Icon("not-a-real-icon"); got != "" {
+ t.Errorf("Icon(%q) = %q, want empty string", "not-a-real-icon", got)
+ }
+}
+
+// The actual value proposition: Icon must work as a real html/template
+// FuncMap entry, end to end -- not just type-check as template.HTML.
+func TestIconWorksAsTemplateFunc(t *testing.T) {
+ tmpl := template.Must(
+ template.New("t").Funcs(template.FuncMap{"icon": Icon}).Parse(`{{icon "check"}}`),
+ )
+ var buf strings.Builder
+ if err := tmpl.Execute(&buf, nil); err != nil {
+ t.Fatalf("Execute: %v", err)
+ }
+ got := buf.String()
+ if !strings.Contains(got, "<svg") || !strings.Contains(got, `aria-hidden="true"`) {
+ t.Errorf("rendered template output missing expected icon markup: %s", got)
+ }
+}
+
+```
+
+- [ ] **Step 2: Run the tests to verify they fail**
+
+Run: `cd /path/to/rastrillo && GOCACHE=/tmp/gocache GOPATH=/tmp/gopath go test ./... -run "TestIcon|TestVendored" -v`
+Expected: FAIL to compile — `undefined: icons`, `undefined: Icon`. This is the expected Go TDD red state; no scaffolding needed to reach it.
+
+- [ ] **Step 3: Write `icons.go`**
+
+```go
+// The rastrillo default icon set — vendored, never a CDN. Mirrors
+// carlosframework/platform's internal/console/icons.go exactly: static,
+// in-code SVG strings in the Lucide idiom (24x24 viewBox, currentColor,
+// stroke, round caps and joins), safe to inline into any app's own
+// html/template output via {{icon "slug"}} (register with
+// template.FuncMap{"icon": rastrillo.Icon}).
+//
+// Vendored, never a CDN: a rastrillo app that ships this promises the
+// same thing the platform's own console does -- no build step, no
+// second origin a browser fetches from and the app can't vouch for or
+// serve when offline. Inlining the paths is the only option, so the
+// paths live here.
+//
+// Icons are vendored from Lucide -- https://lucide.dev -- used under the
+// ISC licence:
+//
+// Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as
+// part of Feather (MIT). All other copyright (c) for Lucide are held by
+// Lucide Contributors 2022.
+//
+// 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.
+//
+// Emoji policy, inherited from Eleven's docs/icons.md via the console:
+// emoji are CONTENT -- a mark a person or a model chose -- and never
+// chrome. Interface furniture uses these icons. Adding one = paste the
+// Lucide SVG's paths in below, keep the (Lucide: <slug>) comment,
+// register it in the icons map, use it as {{icon "<slug>"}}.
+//
+// Every icon carries aria-hidden="true" because it is meant to sit
+// beside its own visible text label -- the label is the accessible
+// name, and a second one from the icon would only be read out twice.
+// An app using an icon as a control's ONLY label must not use these
+// constants bare: give that button or link an explicit aria-label.
+package rastrillo
+
+import "html/template"
+
+// template.HTML is used HERE AND ONLY HERE in this package, for the same
+// reason it's safe in the console: every string in this file is a
+// compile-time constant of our own markup, never user input, a bucket
+// object, or anything derived from a request.
+const (
+ // iconOpen is the shared opening tag: no width or height, so the icon
+ // sizes from the consuming app's own CSS (e.g. a .icon { } rule) and
+ // tracks the text beside it.
+ iconOpen = `<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" ` +
+ `stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">`
+
+ // A disclosure caret -- dropdowns, expandable sections (Lucide: chevron-down).
+ iconChevronDown template.HTML = iconOpen + `<path d="m6 9 6 6 6-6"/></svg>`
+
+ // Confirmation, success states (Lucide: check).
+ iconCheck template.HTML = iconOpen + `<path d="M20 6 9 17l-5-5"/></svg>`
+
+ // "New" / "create" actions (Lucide: plus).
+ iconPlus template.HTML = iconOpen + `<path d="M5 12h14"/><path d="M12 5v14"/></svg>`
+)
+
+// icons maps each Lucide slug to its vendored markup. Slugs are the
+// Lucide names so the mapping back to lucide.dev stays obvious.
+var icons = map[string]template.HTML{
+ "chevron-down": iconChevronDown,
+ "check": iconCheck,
+ "plus": iconPlus,
+}
+
+// Icon renders one vendored icon by its Lucide slug for use as an
+// html/template FuncMap entry:
+//
+// tmpl.Funcs(template.FuncMap{"icon": rastrillo.Icon})
+// // then, in the template: {{icon "check"}}
+//
+// An unknown slug renders nothing rather than panicking a page
+// mid-response -- a typo must cost a missing icon, not a crash.
+func Icon(slug string) template.HTML { return icons[slug] }
+```
+
+- [ ] **Step 4: Run the tests to verify they pass**
+
+Run: `cd /path/to/rastrillo && GOCACHE=/tmp/gocache GOPATH=/tmp/gopath go test ./... -run "TestIcon|TestVendored" -v`
+Expected: `PASS` for `TestVendoredIconsAreSelfContained`, `TestIconUnknownSlugRendersNothing`, `TestIconWorksAsTemplateFunc`.
+
+- [ ] **Step 5: Run the full suite, vet, and format check**
+
+Run: `cd /path/to/rastrillo && GOCACHE=/tmp/gocache GOPATH=/tmp/gopath go build ./... && go vet ./... && go test ./... && gofmt -l icons.go icons_test.go`
+Expected: build/vet/test all clean; `gofmt -l` prints nothing (no files need reformatting).
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add icons.go icons_test.go
+git commit -m "$(cat <<'EOF'
+feat: vendor a default Lucide icon set
+
+rastrillo.Icon(slug) template.HTML -- register as a template.FuncMap
+entry ({"icon": rastrillo.Icon}) for use as {{icon "slug"}} in any
+hand-written app template. Mirrors carlosframework/platform's
+internal/console/icons.go exactly: same vendoring discipline (no CDN,
+compile-time constants), same aria-hidden + label-pairing convention,
+same "unknown slug renders nothing, never panics" safety rule. The
+three vendored icons (chevron-down, check, plus) reuse the console's
+own already-tested SVG path data verbatim, not re-typed from memory.
+
+Per docs/superpowers/specs/2026-08-02-rastrillo-ui-addendum-design.md §3
+in carlosframework/platform.
+EOF
+)"
+```
+
+---
+
+## Self-review notes
+
+- **Spec coverage:** This plan covers only §3 (icons) of the addendum spec's readiness-scoped slice. §2 (app shape/reactivity), §4 (WCAG vet enforcement), and §5 (dropdown/component set) are explicitly out of scope — they depend on §9's component-vocabulary system, which does not exist in rastrillo's code yet (confirmed: no `html/template` usage anywhere in the current codebase before this plan). This is a deliberate, user-confirmed scope cut, not an oversight.
+- **Placeholder scan:** none found — every step has real, complete code or a real, runnable command.
+- **Type consistency:** `Icon(slug string) template.HTML` is the only exported symbol; used identically in the doc comment's usage example, the `TestIconWorksAsTemplateFunc` test, and the Step 3 implementation.
+- **Not included, and why:** console's `TestConsoleTemplatesFetchNothingOffOrigin` and `TestTemplateIconSlugsAllExist` both walk a live template filesystem (`tmplFS`) checking real `.html` files — rastrillo has no template tree yet to walk (no component system built). Both tests are real, valuable, and directly portable in shape once §9 lands; they're not needed to make *this* package correct and complete on its own.