feat(themes): grow the roster to seven and make the picker a popover - #278
Merged
Conversation
The deck shipped three Themes behind a header control that cycled — a shape ADR 0024 capped at four, because cycling trades discoverability for width and the trade only holds while any Theme is two activations away. The design work produced four more Themes from the Sprawl trilogy's interior vocabulary, and the deck had nowhere to put seven a user could navigate. Grow the roster to seven and replace the cycling control with a popover (issue #272): - `kuang` (arterial red; danger vacates to magenta), `ougou` (blued-steel under ember), `solitude` (rust with chem-yellow), `onyx` (silver on true black) join `ice`, `construct`, `chiba`. Each is one semantic-layer block; each cleared the Theme Contract, auto-discovered by the contrast guard. - `ThemeControl` becomes a popover: lists the whole roster, marks the Theme in force, picks by name, keyboard-operable with focus returning to the trigger, accessible names throughout. `menuitemradio` + `aria-checked` semantics. - `useTheme` now exposes `{ theme, themes, setTheme }`; `nextTheme`/`cycle` are retired. The four-Theme ceiling assertion is removed; ice-first, default and named-once invariants stay. - The three pre-paint scripts name the full roster in order; the roster guard keeps the TypeScript, the stylesheet and the scripts in step, and keeps SPRAWL//Atlas excluded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The comment convention mandates JSDoc on module-level declarations; the trigger() helper carried its rationale as a plain // block. Format only — wording unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ard nav The findIndex callback only tests equality against a single value, so indexOf expresses it directly (Biome useIndexOf). activeElement is cast to the ref array's element type, which the equality check did implicitly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner
Author
Code review — findings & dispositionA two-axis Resolved
Reviewed, no change (judgement calls / accepted partials)
No scope creep and no wrong implementations found; all palette values match the spec table, roster parity and the Theme Contract are guard-enforced. |
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #272.
Problem
The deck shipped three Themes behind a header control that cycled — a shape ADR 0024 capped at four, because cycling trades discoverability for width and the trade only holds while any Theme is at most two activations away. The design work produced four more Themes from the Sprawl trilogy's interior vocabulary, and there was nowhere to put seven a user could navigate.
Solution
Grow the roster to seven and replace the cycling control with a popover.
The four new Themes
Each is one semantic-layer block in
tokens.css— no new tokens, no component-token changes — and each cleared the Theme Contract, auto-discovered by the contrast guard:kuang— Neuromancer's icebreaker: arterial red on near-black. The accent owns red, so danger vacates to hot magenta (#ff43a8) — the one palette where a role had to move, and the tightest against the contract.ougou— Count Zero's loa of iron: a blued-steel field under a warm ember accent.solitude— Mona Lisa Overdrive's toxic junkyard: rust corrosion under a sickly chem-yellow warning.onyx— the deck with the neon off: silver on true black, colour spent only on status.The picker becomes a popover
ThemeControlstops cycling: the trigger opens a panel listing the whole roster, marks the Theme in force (menuitemradio+aria-checked), and picks by name. It opens/moves/dismisses by keyboard, returns focus to the trigger on close, and carries accessible names throughout. It anchors small to the trigger so it fits the contested header at mobile widths.Interfaces
useThemenow exposes{ theme, themes, setTheme };nextTheme/cycleare retired. Resolution of an absent/unknown stored value toiceis unchanged.length <= 4) is removed;ice-first, default and named-once invariants stay in their own tests.Testing
setThemehook effects — written test-first.npm run test:run), typecheck and biome clean.Changesets
@cyberdeck/deck-kit,ascii,glitch,golem— minor (control change);sprawl— patch (kit dependency, no on-screen change).Review follow-ups
theme-control.test.tsx: thetrigger()helper's rationale moved from a plain//block to JSDoc, per the deck's comment convention (JSDoc on module-level declarations).theme-control.tsx: keyboard-nav lookup switched fromfindIndextoindexOf(BiomeuseIndexOf), withactiveElementcast to the ref array's element type. No behaviour change; no changeset needed (both are format/lint-only).🤖 Generated with Claude Code