Chroma migrated off its local primitive copies in one commit — and the same shell DeckMatrix that powers Chroma's TOC route now lives on humain's landing too
After this morning's onboarding flurry we kept going. Chroma — the older sibling that originally carried every deck-generic primitive locally before @dididecks/shell existed — now consumes the shell for five of those primitives via thin shim re-exports. Net effect: 781 lines of duplicated implementation deleted in chroma, 119 lines of shim added, ~25 downstream importers untouched. Then a small but instructive correction: humain's landing was showing the lightweight SlideStatusMatrix where it should have been showing the rich audit-rated DeckMatrix that Chroma's landing already uses. The rich version was in the shell the whole time. Now both clients land on the same matrix UI.
Why Care?
This morning's first entry closed a structural gap — six deck-generic primitives that had been trapped inside chroma-decks since before the @dididecks/shell carve-out finally got lifted into the shell, and humain-vc-decks (the new third client) consumed them clean from day one. But chroma itself was still running off its own local copies of those same primitives. The shell had the lifted versions; chroma had the originals. Two implementations of the same thing, sitting next to each other, doing the same work.
Tonight closed that loop. Chroma now imports five of its primitives from the shell. Net effect: 781 lines of duplicated implementation deleted in chroma, 119 lines of shim added — one source of truth for what scroll-snap, the keyboard contract, the mode toggle, and the build-status matrix actually do across both clients. Twenty-five downstream importers in chroma (the proto + four enhanced scroll-deck index files, every per-slide play file, the landing page, the scroll gallery) kept their original import paths working because the shim file lives at the original local path — only the implementation behind the path swapped to the shell.
Then a smaller, instructive course-correction: humain's landing was showing the lightweight SlideStatusMatrix (a basic glyph table — file-existence dots, no audit data) when it should have been showing the rich DeckMatrix that Chroma's landing already uses — variant-chooser pills at the top, dual-surface ratings per cell (Scroll-UI rating on the left, Play-UI rating on the right), per-variant shippability rollups in the column headers, drift indicators on the right edge for cells where Scroll and Play diverged. The rich one was in the shell the whole time. Humain just wasn't reaching for it.
The screenshot a few hours later — humain's landing now rendering the same matrix Chroma ships — is the kind of moment that justifies the whole "build it once in the shell" discipline. Two clients, one component, same UI.
What's New?
Chroma migrated onto the shell — five primitives, one commit
client-sites/chroma-decks/ commit 0eef7fe rewrites five files as thin shim re-exports of the corresponding @dididecks/shell versions:
| Chroma file (now a shim) | Shell implementation it re-exports |
src/layouts/PageAsDeckWrapper.astro | @dididecks/shell/components/PageAsDeckWrapper.astro |
src/layouts/SlideShell.astro | @dididecks/shell/components/SlideShell.astro |
src/utils/mode-switcher.js | @dididecks/shell/runtime/mode-switcher.ts |
src/components/basics/ModeToggle.astro | @dididecks/shell/components/ModeToggle.astro |
src/components/basics/SlideStatusMatrix.astro | @dididecks/shell/components/SlideStatusMatrix.astro (both files deleted later in the same session — see "Then we deleted SlideStatusMatrix" below) |
Each shim preserves the original public API. SlideShell plugs Chroma's brand mark (<ChromaMark size={32} />) into the shell's <slot name="mark"> only when the consumer passes showMark={true}, so the on-screen result is identical to the original. The mode-switcher shim installs the runtime singleton at module top-level via createModeSwitcher({ client: "chroma-decks", defaultMode: "light", respectSystemPreference: true }) — preserves the chroma-decks:mode localStorage key namespace and Chroma's original brand-canon behavior (light by default; honor prefers-color-scheme: dark on first visit).
One file held — src/lib/deck-overview.ts. Two reasons documented in the lift plan:
Sync→async drift. The shell version is
asyncbecause itawaitsloadDecksRegistry(opts.absolute.decksRegistry)(the shell esbuild-evaluates the consumer's TS registry on its side of the boundary). Chroma's local copy is sync; chroma's landing-page callsite calls it sync. A drop-in swap would crash.Substantiation counts. Chroma's
DeckOverview.totalscarriespeopleCount,headshotCount,investorFirmCount,portfolioCompanyCountfrom walkingdata/team,data/investors,public/people. The shell version intentionally dropped these as per-client concerns. The chroma landing reads them — a drop-in swap would null them out.
Migrating later means either updating chroma's callsites to await plus extending chroma's local file as a thin wrapper that adds the counts on top of await loadShellOverview(deckSlug), or leaving the sync chroma copy intact and documenting that new code uses the shell's async version. Held for a separate, deliberate pass.
Humain landing swapped to the rich DeckMatrix
client-sites/humain-vc-decks/ commit 9e2e234 swaps the lightweight matrix for the rich one. Humain's landing now imports and renders:
import DeckMatrix from "@dididecks/shell/components/DeckMatrix.astro";
<DeckMatrix
deckSlug={pitch.slug}
title="Slide status · all variants"
lede="Per-slide review status across the deck's design variants — Scroll-UI rating on the left of each cell, Play-UI rating on the right..."
/>The shell's registry-loader gracefully handles the no-audit-data case (humain doesn't have a populated data/audits/slides.json yet) — loadAuditRegistry returns { schema: 2, ranks: {} } when the file is absent, and DeckMatrix renders cells in unrated state instead of crashing. As humain accrues audit ratings, the same matrix will progressively light up.
Plan updated to reflect status
context-v/plans/Lift-Chroma-Decks-Generic-Code-into-Shared-Shell.md was already the authoritative plan; tonight's session bumped its status from In-Progress to Mostly-Shipped and added a verbatim per-phase status block:
| Phase | What it covers | Status |
| 1 | Lift SlideShell into shell | ✓ shipped (this morning, c87c643 / 07802d1) |
| 2 | Lift ModeToggle + mode-switcher | ✓ shipped (this morning, 07802d1) |
| 3 | Migrate chroma off local copies | ✓ shipped via shim re-exports tonight (0eef7fe) |
| 4 | Lift deck-overview lib | ◐ partial — humain consumes shell; chroma held due to sync→async API drift + substantiation-counts dependency |
| 5 | Reconcile SlideStatusMatrix vs DeckMatrix | ✓ shipped — verdict revised: DeckMatrix is the canonical landing matrix; SlideStatusMatrix deleted from both the shell and chroma's shim |
How it Works
The shim-re-export pattern, in detail
The plan originally called for "swap chroma's importers to @dididecks/shell/... paths directly, then delete the local files." That sounds clean but it touches ~25 chroma files. Shim re-exports were the safer move because chroma is in active production with the calmstorm-pattern auth surface and four authored deck variants; touching 25 importer paths to refactor for "the same behavior, different import path" is a lot of surface area for zero behavior change.
The shim shape for an .astro component:
---
import ShellPageAsDeckWrapper from "@dididecks/shell/components/PageAsDeckWrapper.astro";
---
<ShellPageAsDeckWrapper {...Astro.props}>
<slot />
</ShellPageAsDeckWrapper>Four lines. Spreads props through. Forwards the default slot. Anyone importing the original local path gets the shell implementation transparently, no awareness required at the call site. The local file's ~150 lines of original implementation becomes 4 lines of shim — that's where the 781-vs-119 line ratio comes from.
For the mode-switcher.js (side-effect import, not a component), the shim shape is even simpler:
import { createModeSwitcher } from "@dididecks/shell/runtime/mode-switcher.ts";
createModeSwitcher({
client: "chroma-decks",
defaultMode: "light",
respectSystemPreference: true,
});Top-level call to the factory installs the window.modeSwitcher singleton; every ModeToggle instance on the page reads from it; the localStorage key gets namespaced by client. Same external behavior as the original chroma mode-switcher.js; zero ambiguity about which implementation is running.
The "Chroma version is already in the shell" correction
The screenshot the user shared made the right call obvious: the rich DeckMatrix UI (variant-chooser pills, dual-surface ratings, shippability rollups, drift indicator) is what Chroma's /toc/pitch route shows — and the same shell-injected route + the same shell component already work for humain. Humain just wasn't embedding DeckMatrix on its landing page. Three-line fix: drop the SlideStatusMatrix import + section, add the DeckMatrix import + section with the same lede chroma uses.
The structural learning: when you discover the shell already has the right component, the answer is usually "consume it," not "extend the lighter sibling." The Phase 5 verdict from the lift plan said DeckMatrix and SlideStatusMatrix were distinct siblings — true in principle, but neither client actually reached for the lightweight one. Keeping ~250 lines of well-documented but unused code in the shell for a hypothetical future client was the wrong default; deletion is.
Under the Hood
Why the shim approach lets us delete files later without rushing
The shim files at chroma's original paths are an explicit checkpoint, not a permanent state. The plan's Phase 3 acceptance criterion was originally "delete the local copies after parity verification." We met the spirit of that: parity is verified, the implementation lives in one place. The remaining work — literally deleting the shim files and updating chroma's ~25 importers to point at @dididecks/shell/components/PageAsDeckWrapper.astro directly — is a mechanical follow-up that can happen any time without rush. Each shim file's docblock names the path it re-exports, so the eventual delete-and-update is a five-minute pass per file.
The reason not to do it tonight: chroma is in active production. Touching production importers for "no behavior change, different import path" carries deployment risk that isn't proportional to the benefit. The migration is already done in the sense that matters; the cleanup is a tidying pass.
CSS class-name drift, named explicitly
Each shim file's docblock notes that the shell versions use ddd-prefixed CSS classes (.ddd-mode-toggle, .ddd-ssm-cell, etc.) whereas the chroma originals used unprefixed or chroma-prefixed names (.chroma-mode-toggle, .ssm-cell). Anywhere chroma's global CSS targets the old class names will no longer match. Visual output is preserved across both clients because the shell versions read the same consumer-side semantic tokens (--color-text, --color-border, etc.) via fallback chains, but any project-specific override needs to be re-targeted to the ddd- class names or routed through the --ddd-* token interface.
This is the kind of drift that's easy to miss in a quiet refactor and produce a "why does this look subtly different now?" bug a week later. Naming it in the shim docblocks at the time of the lift is cheap insurance.
Then we deleted SlideStatusMatrix
After the DeckMatrix swap shipped, the obvious follow-up question was: does the lighter sibling still earn its keep in the shell? Audit said no. A grep across all three trees (apps/deck-shell/, client-sites/chroma-decks/src/, client-sites/humain-vc-decks/src/) for any importer of SlideStatusMatrix returned zero hits outside the component files themselves. Neither client reaches for it. The Phase 5 plan-verdict from this morning — "distinct siblings, both promoted" — was correct in principle but didn't survive contact with reality: the rich DeckMatrix covers every landing-page use case we have.
So we deleted both:
apps/deck-shell/src/components/SlideStatusMatrix.astro— the shell component itselfclient-sites/chroma-decks/src/components/basics/SlideStatusMatrix.astro— chroma's shim file from earlier the same session (~25 lines of re-export)
~275 lines removed across the two repos. Zero importers had to be touched because there were zero importers. The "keep it for a hypothetical future client" rationale would have left ~250 lines of well-documented but unused code in the shell for an event that hasn't happened and might not. Future-us can always lift the pattern back if a client genuinely wants a smaller widget; until then, one matrix component, one source of truth, less drift.
The plan's Phase 5 status note was revised in the same commit to reflect the verdict change: DeckMatrix is the canonical landing matrix; SlideStatusMatrix is gone.
What's Next
deck-overviewlib finishes the chroma migration. Either update chroma's landing-page callsite toawait+ extend chroma's localdeck-overview.tsas a thin wrapper that adds the substantiation counts on top of the shell base, or formally bless the sync chroma copy as a parallel path. Either way: surface the choice in the plan and execute one of them deliberately.Browser-test the chroma migration end-to-end. Tonight's shim swap preserves API contracts on paper; an actual deploy of chroma's enhanced-v3 deck against the shell-backed primitives is the next deliberate check. (Local dev already passes per the user's screenshot.)
First brand-iteration session for humain's dark + vibrant modes. Currently functionally present but first-pass;
DESIGN.md'smodes_iterated: [light]tracks the remaining work.Install the auth surface for humain per its dedicated plan (
client-sites/humain-vc-decks/context-v/plans/Install-Auth-Surface-from-Calmstorm-Pattern.md).Push. Eight commits accumulated locally across two repos; ready to leave development → remote.
References
[[2026-06-06_01]] — this morning's first entry (the rollup that named the gap this entry closes)
context-v/plans/Lift-Chroma-Decks-Generic-Code-into-Shared-Shell.md— the authoritative plan for the migrationclient-sites/humain-vc-decks/context-v/plans/Install-Auth-Surface-from-Calmstorm-Pattern.md— humain's next deliberate install
