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>
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. |