Skip to content

feat(storybook): expose the full brand theme set in the toolbar - #393

Merged
drewstone merged 2 commits into
mainfrom
feat/storybook-full-themes
Aug 6, 2026
Merged

feat(storybook): expose the full brand theme set in the toolbar#393
drewstone merged 2 commits into
mainfrom
feat/storybook-full-themes

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Summary

The Storybook theme toolbar listed 6 themes; @tangle-network/brand 1.1.0 ships named scopes that were never wired (arena and intelligence darks), and tangle-dark lands in the brand repo next (sibling PR, feat/tangle-dark-theme). The toolbar now carries the full brand vocabulary, grouped by darkness:

  • Dark: agent-dark (still the default), aubergine, arena, intelligence, tangle-dark
  • Light: agent-light, aubergine-light, arena-light, tangle-light, light

.dark class handling is now correct per darkness: arena and intelligence are dark themes, and intelligence's brand selector (.dark[data-theme="intelligence"]) matches nothing without the class. light is brand's canonical light spine — in this Storybook it resolves to the same tokens as agent-light (agent-app's tokens.css owns the :root light values); it's listed so the toolbar mirrors the brand vocabulary one for one. Legacy dark/light toolbar values still remap, so existing bookmarks keep working.

Bridge changes (.storybook/brand-themes.css)

  • arena joins the main bridge list (it defines the full --hsl-* shadcn set, like aubergine/arena-light/tangle-light).
  • intelligence gets a dedicated block bridging only the nine surface tokens its dark-only partial scope defines (background/card/popover/secondary/muted/muted-foreground/accent/border/input + --canvas-backdrop). Putting it in the full list would redeclare the rest against undefined --hsl-* sources and invalidate them; its foreground/primary/ring deliberately fall through to agent-app's .dark tokens — the same neutral tangle dark (indigo accent) brand's scope intends ("the accent stays Tangle").
  • tangle-dark gets a full bridge block whose var() fallbacks are agent-app's .dark values (its own --neutral-* ramp tokens): until the scope resolves, the toolbar entry renders as agent-dark instead of guaranteed-invalid tokens. Once the scope resolves, the fallbacks are dead code.

How tangle-dark resolves

  • Today (this machine): the scope exists in the brand repo's feat/tangle-dark-theme checkout but not in a published release. A conditional vite alias in .storybook/main.ts points the named-themes.css @import at /home/drew/code/.worktrees/brand-tangle-dark-theme/packages/brand/src/styles/named-themes.css (the main /home/drew/code/brand checkout is on an older branch whose tree predates named-themes.css; the worktree is where the sibling PR lands it). The alias is skipped wherever the path is absent (CI, other machines), so nothing breaks elsewhere — those builds resolve the published package and tangle-dark degrades to agent-dark via the fallbacks.
  • After the brand release: bump @tangle-network/brand, delete the alias block in main.ts and the tangle-dark fallbacks in brand-themes.css (both comments say so). The published scope then resolves for everyone.

Verification

Gates: tsc --noEmit ✓ · knip ✓ (exit 0; only pre-existing config hints) · build-storybook

Alias resolution proven with vite's own resolver: with the alias, @tangle-network/brand/styles/named-themes.css resolves to the local brand checkout; without it, to the published 1.1.0 package.

Every theme screenshot-verified on the App Shell/AppShell story (playwright, 1440×900), saved to /tmp/themes-full/<theme>.png on the build machine — all legible (canvas, text, muted meta, status chips, composer checked in each):

Theme Canvas Notes
agent-dark rgb(13,13,15) unchanged baseline
aubergine rgb(25,17,44) unchanged
arena rgb(13,18,16) newly wired — emerald dark, green accents
intelligence rgb(12,10,16) newly wired — violet surfaces, indigo accent kept
tangle-dark rgb(21,21,35) real sibling scope (graphite-indigo) via the alias
agent-light / light rgb(255,255,255) identical, as expected
aubergine-light rgb(242,239,251) unchanged
arena-light rgb(242,248,245) unchanged
tangle-light rgb(248,248,252) unchanged

The tangle-dark wiring was additionally proven against a deliberately loud temporary local scope (cyan) before the sibling's scope landed, so a fallback render could not masquerade as a pass; the temp block was removed before committing, and the final screenshot runs the real brand scope.

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Auto-approved drewstone PR — 98792eec

This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.

tangletools · auto-approval · reason: drewstone_author · 2026-08-06T08:53:57Z

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Value Audit — sound

Verdict sound
Concerns 2 (2 weak-concern)
Heuristic 0.0s
Duplication 0.0s
Interrogation 327.1s (2 bridge agents)
Total 327.1s

💰 Value — sound

Extends the existing storybook theme-bridge pattern to expose the full brand named-theme vocabulary, with correct per-scope-shape handling (full/partial/unpublished) — every technical claim verified against the real brand CSS.

  • What it does: Adds arena, intelligence, tangle-dark, and brand's canonical 'light' to the Storybook theme toolbar (6→10 entries), grouped by darkness. arena joins the existing full-scope CSS bridge; intelligence gets a partial bridge (only the 9 surface tokens its brand scope defines); tangle-dark gets a full bridge with agent-app-.dark-mirroring fallbacks plus a guarded vite alias to a local brand worktree whe
  • Goals it achieves: Make the Storybook toolbar reflect the complete brand theme vocabulary so every product skin is previewable in one place, ahead of the brand bump that publishes tangle-dark. Fixes a latent correctness bug too: intelligence is a dark-only PARTIAL scope, so without the .dark class and a partial bridge it either matched nothing or would invalidate un-defined --hsl-* tokens.
  • Assessment: A careful, correct change built squarely in the grain established by the prior commit (c1b12a6): the same three files own this concern, and the bridge mechanism is extended, not reinvented. The three scope shapes (full/partial/unpublished) are each handled the right way — I verified all 16 tangle-dark fallbacks byte-match agent-app's .dark scope (tokens.css:283-326), the intelligence partial bridg
  • Better / existing approach: none — this is the right approach. Searched for an existing equivalent (rg agentTheme/AGENT_THEMES/named-themes across src/ and fleet storybook configs; checked tests/theme/tokens-contract.test.ts which is token-completeness, not theme switching) and found nothing that already does this. The bridge pattern is the one the prior commit established and is the correct mechanism for re-pointing agent-a
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 2
  • Bridge warning: opencode/kimi-for-coding/k2p7: opencode: opencode error

🎯 Usefulness — sound

A careful, in-grain extension of the existing Storybook theme toolbar to cover the full brand named-theme vocabulary, with correct per-scope cascade handling (partial intelligence scope, fallback'd tangle-dark) and a guarded dev-only alias.

  • Integration: Fully reachable: every one of the 64 stories renders through the withAgentTheme decorator (preview.ts:59-81), which reads the AGENT_THEMES map (preview.ts:33-44) the PR extends. Selecting any new theme (arena/intelligence/tangle-dark/light) from the toolbar sets data-theme + toggles .dark on documentElement, which the brand-themes.css bridge blocks resolve. The legacy dark/light remap (preview.ts:
  • Fit with existing patterns: Extends the established pattern (AGENT_THEMES map + brand-themes.css cascade bridge + withAgentTheme decorator) rather than competing with it. The intelligence partial-scope block (brand-themes.css:86-97) and the tangle-dark fallback block (brand-themes.css:113-129) are MORE careful than the existing full-bridge list, correctly handling the nuance that intelligence defines only 9 surface tokens an
  • Real-world viability: Holds up across environments: the existsSync guard (main.ts:28) makes the machine-local brand worktree alias silently no-op on CI and other machines (falling back to the published package), the tangle-dark var() fallbacks prevent a page of invalid tokens where the scope is absent, and intelligence's .dark-prefixed selector (brand-themes.css:86) correctly matches brand's dark-only scope. The only e
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 1

💰 Value Audit

🟡 The vite alias points at a machine-local absolute worktree path [maintenance] ``

.storybook/main.ts:18-19 hardcodes /home/drew/code/.worktrees/brand-tangle-dark-theme/... — only present on one machine. It IS existsSync-guarded (degrades to published package elsewhere) and marked TEMP with delete-on-bump instructions, and the tangle-dark bridge fallbacks mean the absent case renders as agent-dark rather than broken. So it is safe, but it is a piece of disposable scaffolding that must be remembered on the brand bump. Not a better-architecture issue — the alias is the minimal w

🟡 'light' toolbar entry renders identically to 'agent-light' [proportion] ``

Both map to {dataTheme:'light', dark:false} (preview.ts:39,43), so selecting 'Light · Brand Light' produces the exact same tokens as 'Light · Agent Light'. The PR documents this as intentional vocabulary-mirroring (brand has a canonical light spine; agent-app's tokens.css owns the :root light values so they coincide here). It is observably redundant in this storybook today, but harmless and explicitly justified — flagging only so a reviewer knows the two are the same render.


What this audit checks

It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.

Pass What it asks
Heuristic Vague title? Whitespace-only or cruft-bearing diff? (content signals only)
Duplication Do added function/class names already exist elsewhere in the repo?
Value Audit What does it do? What goal does it achieve? Is it good? Better architecture or already-exists?
Usefulness Audit Does it integrate and fit? Will it hold up in real use and actually get used?

Findings are concerns, not blocks — the human reviewer decides what to do with them.

value-audit · 20260806T092325Z

@tangletools

Copy link
Copy Markdown

✅ No Blockers — 98792eec

Review health 100/100 · Reviewer score 80/100 · Confidence 75/100 · 10 findings (10 low)

opencode GLM 5.2 opencode DeepSeek v4 Pro opencode DeepSeek v4 Flash aggregate
Readiness 83 92 80 80
Confidence 75 75 75 75
Correctness 83 92 80 80
Security 83 92 80 80
Testing 83 92 80 80
Architecture 83 92 80 80

Reviewer score is advisory once the run is complete and the verdict has no blockers.

Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision.

🟡 LOW Bridge contract (every bridged var() must resolve inside the active scope) is unverified by any test — .storybook/brand-themes.css

The full-bridge list claims 'verified: background/foreground/... so none of these resolve empty', but this is hand-verified prose. The failure mode this file exists to avoid is real and silent: agent-app's tokens.css defines zero --hsl-* tokens (rg -- '--hsl-' src/theme/tokens.css: no matches), so a scope added to the full-bridge list that is partial (like intelligence, or a future brand scope) would make the bridged var() guaranteed-invalid and drop the property — colors disappear with no error, no log, no test failure. The repo already pins the identical contract class for tokens.css in tests/theme/tokens-contract.test.ts (every var(--...) a component references must be defined). Add a sibling test: parse brand-themes.css's bridge lists + the imported brand named scopes and assert every

🟡 LOW tangle-dark fallback values are a hand-copied snapshot of .dark and can silently drift — .storybook/brand-themes.css

The 16 fallback values (var(--neutral-16-hsl) background, 239 84% 74% primary/ring, etc.) are duplicated verbatim from the .dark scope in src/theme/tokens.css (lines 283-320). On any machine without the local brand worktree alias (CI, other developers), these fallbacks are the ONLY source for the toolbar's tangle-dark entry. tokens.css's .dark values are actively tuned (the file's own comments document contrast-driven revisions to --primary), so the copy can drift: tangle-dark would then render as a stale agent-dark that diverges from agent-dark itself, with no signal. Fix: a contract test comparing the fallback literals against parsed .dark declarations (same

🟡 LOW Committed machine-local absolute path with a silent-failure guard — .storybook/main.ts

A hardcoded /home/drew/code/.worktrees/brand-tangle-dark-theme/... absolute path is committed to a shared repo. It is guarded by existsSync (line 28) and documented as TEMP with a delete-on-bump instruction, so CI and other machines are functionally unaffected and this is dev-only. The risks: (a) the guard is SILENT — if the worktree is re-checked-out at a different path or the branch is deleted, the alias silently no-ops and Drew's local Storybook quietly diverges from what CI renders (tangle-dark falls back to agent-dark per brand-themes.css, so it degrades benignly but invisibly); (b) after the brand bump, if the block is not deleted (comment-only instruction), the primary

🟡 LOW Hardcoded /home/drew path with no removal tracker — .storybook/main.ts

The absolute machine-local path is documented as TEMP and gated by existsSync, so it is inert on CI and other machines (verified: the path resolves on this workstation only). The only residual risk is the 'delete on brand bump' instruction living in a comment with no mechanical gate. Low-impact if forgotten (one extra fs.stat per storybook boot); mentioning so it is on record. Optional: grep-guard via a CI step that fails when @tangle-network/brand ≥ the tangle-dark release still sees this block.

🟡 LOW One-time startup existsSync: no HMR for external file, no pickup mid-session — .storybook/main.ts

existsSync runs exactly once at config load. The aliased file lives outside the project root (/home/drew/code/.worktrees/...), so Vite's watcher does not watch it: edits to named-themes.css while pnpm storybook is running do not hot-reload, and if the file is created AFTER the dev server starts (e.g. the brand worktree is checked out mid-session), the alias was already skipped for the whole session — the comment's claim 'picks the scope up the moment it lands there' is only true across a restart. Low impact (dev ergonomics only); fix is to add the file to server.watch force-include or restart after the worktree changes.

🟡 LOW viteFinal alias merge assumes record shape; corrupts array-form alias — .storybook/main.ts

viteConfig.resolve.alias is Alias[] | Record<string, string> in Vite. { ...viteConfig.resolve.alias, '@tangle-network/brand/styles/named-themes.css': LOCAL_BRAND_NAMED_THEMES } produces a valid config ONLY when alias is already a record; if the react-vite preset or an addon leaves it as an array, the spread yields {0: aliasEntry, 1: aliasEntry, …, '@tangle-network/…': path} and Vite resolution breaks. Fix: branch on Array.isArray and append { find: '@tangle-network/brand/styles/named-themes.css', replacement: LOCAL_BRAND_NAMED_THEMES } in the array case. Impact is contained to the one machine where existsSync returns true, so severity stays low — but the merge is silently wrong rather than loud if alias ever ships as an array.

🟡 LOW AGENT_THEMES keys are not pinned to the brand scope list by any test — .storybook/preview.ts

The repo's own doctrine (AGENTS.md: 'a test that cannot fail is worse than no test') and the per-theme .dark requirements (intelligence breaks without .dark) make this map's correctness load-bearing for the Storybook. Nothing fails if a future brand bump drops [data-theme='intelligence'] from named-themes.css — the toolbar would still set the attribute + .dark class and silently fall back to agent-dark values with no signal. No new test is required to merge this Storybook-only file, but if the team relies on the toolbar to QA theme work, a one-shot test reading named-themes.css selectors and diffing against AGENT_THEMES keys would catch the drift the way tokens-contract.test.ts catches token drift. Cosmetic-only impact keeps this low.

🟡 LOW Legacy 'light' remap shadows the new 'light' toolbar entry — .storybook/preview.ts

LEGACY_THEME_VALUES still has light: 'agent-light' (line 50). The lookup key = LEGACY_THEME_VALUES[raw] ?? raw therefore intercepts the NEW toolbar value 'light' (line 104) and reroutes it to 'agent-light'. Both resolve identically (data-theme='light', no .dark class — verified: tokens.css has no [data-theme='light'] scope so both fall through to :root), so there is no observable rendering bug, but the resolution path the new toolbar item takes is not the one a reader of the comment block ([lines 23-32](https://github.com/tangle-network/agent-app/blob

🟡 LOW New light theme entry is dead code — legacy alias shadows it — .storybook/preview.ts

In withAgentTheme (preview.ts:61) LEGACY_THEME_VALUES[raw] ?? raw runs BEFORE the AGENT_THEMES lookup, and LEGACY_THEME_VALUES maps 'light' -> 'agent-light' (preview.ts:49). The new toolbar item { value: 'light', ... } (preview.ts:104) therefore resolves to the agent-light entry and AGENT_THEMES['light'] (preview.ts:43) is never consulted. Today both entries are identical ({dataTheme:'light', dark:false}, and brand's [data-theme='light'] scope defines only syntax tokens, not the --hsl-* spine, so the DOM output is byte-for-byte the same — no visible bug). But the doc comment's claim that the map mirrors the brand vocabulary one-for-one is false at the lookup level, and if agent-light's dataTheme or darkness ever diverges from the brand 'light' entry, the legacy alias silently masks i

🟡 LOW light entry in AGENT_THEMES unreachable via toolbar (shadowed by legacy mapping) — .storybook/preview.ts

The toolbar item { value: 'light', title: 'Light · Brand Light' } (line 104) is intercepted by LEGACY_THEME_VALUES['light']'agent-light' at line 61 before reaching AGENT_THEMES['light'] at line 43. The light key is dead code in the current flow. Both entries produce { dataTheme: 'light', dark: false }, so the visual result is identical and no user-facing bug exists. The entry appears to be a forward-compatibility placeholde


tangletools · 2026-08-06T09:28:42Z · trace

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Approved — 10 non-blocking findings — 98792eec

Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 3/3 planned shots over 3 changed files. Global verifier still owns final merge decision.

Full immutable report for this review: trace

Summary comment for this run: full summary


tangletools · 2026-08-06T09:28:42Z · immutable trace

The toolbar listed 6 of the brand named themes; brand 1.1.0 ships arena
and intelligence darks that were never wired, and tangle-dark lands in
the brand repo next. The toolbar now carries the whole vocabulary,
grouped by darkness — Dark: agent-dark (still the default), aubergine,
arena, intelligence, tangle-dark; Light: agent-light, aubergine-light,
arena-light, tangle-light, light — with the .dark class driven per theme
darkness (arena and intelligence are dark; intelligence's brand selector
.dark[data-theme="intelligence"] requires the class to match at all).

The brand bridge gains arena (full --hsl-* set, joins the main list), a
dedicated intelligence block bridging only the nine surface tokens that
dark-only partial scope defines — its foreground/primary/ring
deliberately fall through to agent-app's .dark tokens, which are the
same neutral tangle dark brand intends — and a tangle-dark block whose
var() fallbacks render agent-dark until the scope resolves, instead of a
page of guaranteed-invalid tokens.

tangle-dark exists today only in the brand repo's feat/tangle-dark-theme
checkout, so .storybook/main.ts aliases the named-themes.css @import to
that file where it exists (skipped elsewhere — CI and other machines
keep resolving the published package). Delete the alias and the
tangle-dark fallbacks on the brand bump.
…opies

- AGENT_THEMES owns title/dark/dataTheme per theme and the toolbar item
  list is derived from it, so the map and the toolbar cannot disagree;
  dataTheme defaults to the key (only agent-dark/agent-light rename)
- drop the dead `light` legacy alias: it shadowed AGENT_THEMES['light'],
  and the map entry resolves old URLs identically
- tangle-dark joins the full bridge; the decorator probes for the brand
  scope and remaps to agent-dark where neither brand >1.1.0 nor the local
  alias provides it — loudly, once — replacing 16 fallback literals copied
  from tokens.css's dark scope that were free to drift from it
- vite alias appends in record or {find,replacement}[] shape instead of
  assuming a record (spreading a list would corrupt every existing alias),
  and logs when the local alias is active
@drewstone
drewstone force-pushed the feat/storybook-full-themes branch from 98792ee to 16f66f8 Compare August 6, 2026 21:30
@drewstone

Copy link
Copy Markdown
Contributor Author

signoff PASS 16f66f8 (rebased onto main with the template-pin fix) — all steps ok. Clean tree, Node 22.23.1.

@drewstone
drewstone merged commit 1450ad0 into main Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants