carlos / platform Public

spec: rastrillo UI addendum — app shape, reactivity, icons, WCAG 2.2 AA

Amends §9/§10 of the framework design doc. Two app shapes chosen at
rastrillo new time: server (titogo's zero-JS baseline, unchanged) and
client (Woodstar/Eleven's SPA model). Client shape defaults to VanJS
(vendored), framework-free as an opt-out -- grounded in Eleven's own
shell.js showing the real cost of the pattern it chose (31 manual call
sites to renderThreadList(), ~216 direct DOM-mutation sites). Lucide
icons, vendored -- now backed by two real precedents (Eleven's
web/static/icons.js AND the platform console's just-landed
internal/console/icons.go). WCAG 2.2 AA made enforced, not aspirational,
via carlos vet structural checks modeled directly on the console's own
already-shipped test suite (TestTemplateIconSlugsAllExist etc).
Dropdown and friends added to §9's component list as zero-JS-first
(<details>/<summary>), not a Web Awesome import.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Paul Campbell 8d685a012aabb4cb3b5a46e1647ad1417d957c92 parent 771c2f0
1 file changed, +207 −0
  • docs/superpowers/specs/2026-08-02-rastrillo-ui-addendum-design.md +207 −0
diff --git a/docs/superpowers/specs/2026-08-02-rastrillo-ui-addendum-design.md b/docs/superpowers/specs/2026-08-02-rastrillo-ui-addendum-design.md
new file mode 100644
index 000000000..cf1bfd0a3
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-02-rastrillo-ui-addendum-design.md
@@ -0,0 +1,207 @@
+# Rastrillo UI addendum: app shape, reactivity, icons, accessibility
+
+**Status:** approved 2026-08-02 (Paul).
+
+**Amends** §9 (Component vocabulary and UI generation) and §10
+(Localization) of `2026-08-01-carlos-framework-design.md`. That doc
+already commits rastrillo's default component vocabulary to titogo's
+proven system (server-rendered `{{define}}` partials, zero-JS baseline
+as a framework non-negotiable) and leaves reactivity, icons, and
+accessibility enforcement unaddressed. This fills those gaps — grounded
+in the same evidentiary standard §9 sets for itself: *"the only proven
+vocabulary among the six apps... becomes the framework's default — not
+because it's Tito's, but because it's the only one that exists and
+works."* Nothing here is invented without a real app (or a real,
+already-landed platform commit) behind it.
+
+## 1. Why this shape
+
+Three source apps, three real client architectures, none of them
+optional or speculative:
+
+- **titogo** — server-rendered, zero-JS baseline. Already §9's default;
+ unchanged here.
+- **Woodstar** — client-driven SPA. `app.js` is an explicit VanJS shell
+ (`CLAUDE.md`: *"no build step, no runtime dependencies, @noble and
+ VanJS vendored"*); `routeKey` remounts the view, `rev` re-derives
+ chrome on state changes.
+- **Eleven** — also client-driven (an 8,722-line `shell.js`: thread
+ list, DMs/groups, master-detail layout, live messaging), but
+ deliberately framework-free (`ui.js`: *"Small, framework-free UI
+ helpers"*) — at real production scale, in the most security-sensitive
+ app in the family.
+
+These aren't two variations on one theme; they're two genuinely
+different answers to "how does the client stay in sync with state,"
+and a single default would misrepresent whichever one it excluded. So
+rastrillo gets **two app shapes**, chosen once, at `rastrillo new`
+time:
+
+```
+rastrillo new myapp # -> server (default)
+rastrillo new myapp -shape client # -> client
+```
+
+**`server`** is exactly §9 as already written: zero-JS baseline stays
+non-negotiable, `{{define}}` partials, the existing component list.
+Nothing about this shape changes.
+
+**`client`** scaffolds a reactive JS shell instead of (or alongside —
+open question, see §5) the server-rendered partial tree, for apps that
+are genuinely feed/real-time/SPA-shaped and have decided the zero-JS
+floor isn't the right trade for them. `rastrillo.Serve` still serves
+the data/API layer underneath either shape; this only changes what
+`rastrillo new` scaffolds for the frontend.
+
+## 2. Reactivity inside `client` shape
+
+Not a forced choice between Woodstar's and Eleven's answers — evidence
+supports recommending one as the default while keeping the other fully
+legitimate.
+
+**Default: VanJS, vendored** (Woodstar's pattern — a single small file
+copied into the app, not an npm/CDN dependency, consistent with the
+family's existing "no build step, no runtime dependencies" convention
+already proven at `carlosframework/website` and in Woodstar itself).
+
+**Why default to it rather than framework-free**, given Eleven proves
+framework-free works at scale: Eleven's own code shows the concrete
+cost of the pattern it chose. `renderThreadList()` — a full
+teardown-and-rebuild (`list.innerHTML = ""`, then reconstruct) — is
+manually invoked from **31 separate call sites** across `shell.js`, one
+of 14 `render*`/`update*` functions responsible for **~216** direct DOM
+mutation call sites in an 8,722-line file. Every one of those 31 call
+sites is a place a future change can forget to call the render function
+after mutating state — exactly the bug class a reactive binding removes
+structurally, by making "call the render function" not a thing anyone
+writes by hand. VanJS's reactive cells (`van.derive`) replace "31 places
+that must remember" with "one derivation that always runs."
+
+This is a **default, not a requirement** — `-reactivity none` scaffolds
+the framework-free pattern instead, matching Eleven's own proof that it
+remains a legitimate, production-grade choice, in particular for a
+security-conscious app that wants to minimize even tiny dependencies on
+principle. Same "cheap no" discipline as the rest of the manifest
+system: the default is the well-evidenced common case, opting out is
+one flag, never a fork.
+
+## 3. Icons: Lucide, vendored
+
+Stronger footing than originally proposed — **two** real precedents,
+not zero:
+
+- **Eleven** (`web/static/icons.js`): *"The newer icons are vendored
+ from Lucide... inline, static, stroke-drawn SVGs in the
+ Feather/Lucide idiom (24×24 viewBox, currentColor, round caps),
+ injected only via innerHTML with these in-code strings — never user
+ content."*
+- **The platform's own console**, landed the same day as this addendum
+ (`internal/console/icons.go`): the Go-side twin of Eleven's
+ convention — `const` SVG-path strings in the Lucide idiom, an
+ `icons` map, a `{{icon "slug"}}` template helper, the full ISC
+ licence text reproduced in the file, never a CDN.
+
+Rastrillo's default is this exact pattern, generalized to whichever
+shape an app uses: vendored Lucide SVG-path constants (Go `const` for
+`server` shape's templates; a small JS module for `client` shape,
+matching Eleven's `icons.js` directly), never a runtime or CDN
+dependency either way. Replacing the set is opt-in-by-file-existence,
+the same mechanism as every other override in the framework (§3): an
+app supplies its own icon module at the same generated path and the
+generator stops touching it.
+
+**Accessibility convention, inherited directly from the console's
+implementation** (not invented here): every icon constant carries
+`aria-hidden="true"`, because the default components always pair an
+icon with a visible text label. A component that uses an icon as its
+*only* label (an icon-only button) must set an explicit `aria-label` —
+`carlos vet` checks for this (§4). That `aria-label` is copy, so it
+follows §10's existing rule exactly: it's a catalog key like any other
+component string ("Every piece of copy these components render...
+comes from the localization catalog, not a baked-in string"), not a
+hardcoded-English exception to it. No new localization mechanism —
+icon labels were simply missing from §9's "every piece of copy" until
+now.
+
+## 4. WCAG 2.2 AA, enforced not aspirational
+
+§9 already names "WCAG AA" as a visual-skin design goal; this pins the
+version (2.2, the current spec, superseding 2.1) and — the actual
+gap — makes it checked, not just intended, mirroring §13's own existing
+pattern for i18n catalog completeness (a `carlos vet` failure on a
+missing key).
+
+Full runtime WCAG 2.2 AA verification (contrast computed against actual
+rendered colors, real focus-order testing, screen-reader behavior) needs
+a browser-driven tool (axe-core or equivalent) and is **out of scope for
+what `carlos vet` can check statically** — naming that honestly rather
+than overclaiming "vet guarantees compliance." What *is* realistic, and
+has direct precedent already shipped in `internal/console/icons_test.go`
+today, is a class of structural/referential checks `carlos vet` can run
+without a browser:
+
+- Every `{{icon "slug"}}` (or JS equivalent) reference resolves to a
+ real vendored icon — `TestTemplateIconSlugsAllExist`'s pattern.
+- Every vendored icon/component asset is self-contained — no off-origin
+ `<script src>`/`<link href>`/`url()` — `TestVendoredIconsAreSelfContained`'s
+ and `TestConsoleTemplatesFetchNothingOffOrigin`'s pattern, generalized
+ from "the console" to "any rastrillo app's generated output."
+ Zero-JS-baseline compliance is *also* checkable this way: a
+ `server`-shape page's initial render must not require JS to reach any
+ of its content or primary actions — testable by rendering with a
+ script-stripped fetch and diffing against the full render for
+ presence (not pixel parity) of every interactive affordance.
+- Every default component that carries an icon-only control has an
+ `aria-label` (§3's convention) — a template/AST check, not a runtime
+ one.
+- Color tokens (§9's "token-based CSS") meet the WCAG 2.2 AA contrast
+ ratio (4.5:1 normal text, 3:1 large text/UI components) against the
+ backgrounds they're declared to pair with — computable from the token
+ values themselves, no rendering required.
+
+This is `carlos vet`'s guard-rail role (§13) extended, not a new
+subsystem: static, structural, fails the build loudly, same posture as
+every other guard rail already in the framework.
+
+## 5. Component set: `dropdown` and friends, zero-JS-first
+
+Added to §9's existing default component list (`page-header`, `box`,
+`list-bar`, ... `help-link`): `dropdown`, and other Web Awesome-style
+interactive components a real admin UI wants.
+
+**Not a Web Awesome import.** Web Awesome's components are JS Custom
+Elements — client-side state by construction, which is exactly what §9's
+zero-JS baseline rules out for `server` shape. Instead, each such
+component ships as real, working zero-JS HTML first — `dropdown` is
+`<details>/<summary>` (natively keyboard-accessible, no JS required for
+open/close) — with JS as pure enhancement when present (animation,
+click-outside-to-close), per §9's existing "JS is progressive
+enhancement only" rule. Nothing new architecturally; this is §9 applied
+to components it didn't yet enumerate.
+
+For `client` shape, this constraint doesn't apply — the shape has
+already opted out of the zero-JS floor, so its component set (VanJS or
+framework-free, per §2) can use richer client-owned interactive state
+directly. `client` shape's component set is **not** specified further
+here; see open questions.
+
+## 6. Open questions
+
+- **Does `client` shape still use §9's server-rendered partials for its
+ *chrome* (page shell, nav) and only the SPA'd portions in JS, or is it
+ JS-rendered top to bottom?** Woodstar and Eleven both lean toward
+ "mostly JS-rendered" for their core UI but keep some server-rendered
+ surfaces (Eleven's `web/pages/*.html`, `web/status/*.html`). Needs a
+ decision before `client` shape's scaffold can be written.
+- **`client` shape's own default component set** — a VanJS (or
+ framework-free) equivalent of §9's `dropdown`/`modal`/`menu` etc. is
+ not designed here. Real candidate: Woodstar's `ui/dom.js` `h()`
+ helper and its component patterns, unexamined in this addendum beyond
+ confirming VanJS is used.
+- **Exact shape of the vet-check implementation** (§4) — named as a
+ realistic, precedented capability, not yet designed as code (which
+ functions, which files, how a `server`-shape app's "renders without
+ JS" check actually diffs the two fetches).
+- **Icon set beyond Lucide's own coverage** — what happens when an app
+ needs a glyph Lucide doesn't have. Not addressed; likely "vendor it by
+ hand into the same file, same convention," but not decided here.