← Corpus / dididecks-ai / plan
Redesign TOC as a Deck-Level Dual-Surface Review Matrix
Today's `/toc/[deck]/[variant]/` answers a build-system question ('which slot has a Play-UI file?') when the deck-iteration-workflow centers on a workflow question ('where am I in the review cycle, and which variant is closest to shippable?'). The calmstorm-decks `/index` shows the right shape: variants as columns, slides as rows, a review-status chip per cell. We go one step further than calmstorm: each cell carries TWO chips, one per surface — scroll-review and play-review — because the workflow reviews each slide twice (once during scroll-iteration, once after porting to Play-UI). The goal the matrix should make obvious at a glance: find a single column where every slide is ≥ passable on both surfaces, with as many ★s as possible. Drift between scroll-rating and play-rating is itself a workflow signal (the port may have lost fidelity). This redesign migrates the audits schema from one rating per (slot, variant) to two ratings, makes the SlideRankPill surface-aware, builds a new `/toc/[deck]/` deck-level matrix route, and keeps the per-variant `/toc/[deck]/[variant]/` route alive as the variant landing/index — distinct purpose from the matrix. [Original lede said "folds the per-variant TOC into a redirect"; revised 2026-05-17 in-flight when in-browser review made clear the two surfaces serve different workflow needs.]
- Path
- plans/Redesign-TOC-as-Deck-Level-Dual-Surface-Review-Matrix.md
- Authors
- Michael Staton
- Augmented with
- Claude Code (Opus 4.7, 1M context)
- Tags
- Dididecks-Shell · TOC-Redesign · Deck-Level-Matrix · Dual-Surface-Rating · Scroll-UI-Play-UI-Paired-Review · Audits-Schema-Migration · SlideRankPill-Surface-Awareness · Deck-Iteration-Workflow · Calmstorm-Pattern-Adoption
Redesign TOC as a Deck-Level Dual-Surface Review Matrix
Plan of record for the next slice of
@dididecks/shell. Replaces the per-variant TOC with a deck-level matrix that surfaces the question the founder actually asks every day: “which variant is closest to ready, and what’s left to fix in it — on both the scroll surface and the play surface?”
Why now
The Phase A++ work (DeckOverlay—Play-UI, in-play SlideRankPill, the /play/ chooser, the Scroll-UI vs. Play-UI framing correction) leaves us with a fully two-surface workflow but a one-surface TOC. The founder reviewed today’s /toc/pitch/enhanced-v3/ next to calmstorm-decks’s /index and named the gap directly: dididecks’ TOC shows port-status (Play-UI file exists or not) as the headline information; calmstorm’s TOC shows review-status, with port-status folded in as a quieter “ADAPTED” chip. The calmstorm framing is the right one — port-status is plumbing, review-status is workflow.
This plan adopts the calmstorm pattern, and pushes one step further: where calmstorm rates a slide once per variant, dididecks rates it twice — once on Scroll-UI, once on Play-UI — because the deck-iteration-workflow walks both surfaces and they can drift.
The workflow this serves (literal, from founder’s articulation)
- Start from materials (PDF / per-slot markdown).
- Generate ~3 design variants, scroll form.
- Walk each variant in scroll mode, rating every slot U / C / P / ★.
- Pick a preferred variant — possibly remixing favorite slots across variants.
- Iterate “U” (urgent) slots first.
- Iterate “C” (could-be-better) slots.
- Confirm the preferred variant is not embarrassing in scroll form.
- Port each scroll slot to play mode (per-slide Play-UI files).
- Walk the play surface, rating every slot again (independently — porting may lose fidelity).
- Ship when one column has every cell ≥ passable on both surfaces, ideally most ★, ideally scroll-rating == play-rating.
The TOC is the answer to “where am I in this loop, right now?”
Scope boundary
In scope:
- Audits schema v2 migration. One entry per
(deck, variant, slot)grows from{status}to{scroll: {status, rankedAt, ...}, play: {status, rankedAt, ...}}. Forward-compat reader for schema v1 (treat v1statusasscroll.statussince that’s the surface the v1 ratings were entered against — Phase A+ pill was scroll-mounted only). /api/slide-rankPOST surface. Add asurface: "scroll" | "play"field to the POST body and to the on-disk record. GET unchanged in shape (returns the whole audit object; clients read whichever surface they care about).- SlideRankPill surface-aware. New
surfaceprop ("scroll" | "play"). The pill writes to that surface’s branch. Default behavior matches mount context — scroll routes passsurface="scroll";DeckOverlay--Play-UIpassessurface="play"to the pill it composes. - New
/toc/[deck]/matrix route. Variants × slots grid. Per-cell paired chips. Per-column shippability rollup (dominant). Per-row drift indicator (subtle, right-edge). - Two SVG icons. Scroll-glyph and play-glyph, balanced as a pair. Tooltips spell out “Scroll review” / “Play review”. The chip itself is rating-only.
/toc/[deck]/[variant]/stays alive as a variant landing.Deprecate to redirect.REVISED 2026-05-17: per-variant TOC is the “land in one variant” surface; the deck-level matrix is the cross-variant-comparison surface. Both have a place. Variant-name in matrix → per-variant TOC.- Cell click behavior — whole-half navigates, no inline editing. Scroll half →
/scroll/{deck}/{variant}/#slot-NN. Play half →/play/{deck}/{variant}/{slot}/. Rating editing happens via the SlideRankPill on the surface itself (one source of truth for the rate action). - Port-status folds into the cell. No separate “ADAPTED” column. If the per-slide Play-UI file is missing, the play half renders dimmed/disabled — the absence IS the port-status signal.
Out of scope (explicit defers):
- In-matrix rating. Tempting; rejected for v1. Adds a second rate-action surface, a re-render path, and complicates “which is canonical when the matrix and pill disagree mid-write?” Defer until the read-only matrix proves insufficient.
- Cross-deck dashboard.
/toc/indexshowing all decks side-by-side. Not needed; chroma is the only deck today. - Bulk re-rating tools. “Mark all v2 slots as passable” etc. Defer to workflow-quirk phase.
- Per-row “remix-from-multiple-variants” affordance. The workflow step 4 (“possibly prefer the layout of individual slides in another variant”) implies a slot-level variant-mixing feature. Out of scope here; the matrix surfaces the decision (clear cross-variant comparison) but doesn’t yet act on it.
- A.7 publish. Still orthogonal. This plan stays in workspace-link mode.
- Theming the new matrix. Defaults inherit chroma’s existing TOC palette; no new tokens. If the founder dislikes the visual register, that’s a separate small plan.
Preconditions
- Phase A++.2 shipped (today, 2026-05-17).
/play/[slot].astroconsumes<DeckOverlay--Play-UI>; in-play SlideRankPill renders and persists. Build green. - Audits file at
client-sites/chroma-decks/data/audits/slides.jsoncontains ~17 entries from the 2026-05-12 scroll-side ranking session. Those entries are valuable user data and must migrate cleanly to schema v2. - Working tree clean except for the documented submodule-content drift on
calmstorm-decks/. No other in-flight branches in dididecks-ai.
Architecture decisions to lock before code
A. Audits schema v2 shape
Schema v1 (today):
{
"schema": 1,
"ranks": {
"pitch/enhanced-v2/01": {
"status": "perfect",
"rankedAt": "2026-05-12T10:37:27.882Z",
"rankedBy": "founder",
"notes": null
}
}
}
Schema v2 (proposed):
{
"schema": 2,
"ranks": {
"pitch/enhanced-v2/01": {
"scroll": {
"status": "perfect",
"rankedAt": "2026-05-12T10:37:27.882Z",
"rankedBy": "founder",
"notes": null
},
"play": {
"status": "passable",
"rankedAt": "2026-05-17T19:02:15.881Z",
"rankedBy": "founder",
"notes": null
}
}
}
}
Migration rule. On first read of a schema 1 file, the loader rewrites it to schema 2 in-memory and persists the rewrite on the next write. The v1 status becomes scroll.status (since Phase A+ pill was scroll-only). play is absent for v1-rooted entries — the matrix renders an empty play chip (distinct from “pending” — empty means “never rated”; pending means “explicitly cleared”).
One-time migration script at apps/deck-shell/scripts/migrate-audits-v1-to-v2.ts for the founder to run manually after pulling this plan’s commits. The loader’s auto-rewrite covers it lazily, but a one-shot script lets the founder verify the migration before any writes happen.
B. surface field — required at write, optional at read
POST /api/slide-rank requires surface: "scroll" | "play". The route refuses requests without it (400) — eliminates “ambiguous old client” ambiguity. The SlideRankPill always sends it. GET returns the full audit blob; clients project the surface they care about.
The shell’s RankEntry type splits into SurfaceRankEntry (the inner {status, rankedAt, rankedBy, notes}) and RankEntryV2 (the outer {scroll?, play?}). The TOC reads both surfaces per row.
C. SlideRankPill — surface as a prop, no auto-detect
Auto-detection (“am I inside DeckOverlay—Play-UI? then I’m play-mode”) is too magical. Make it an explicit prop:
<SlideRankPill deckSlug variantSlug surface="scroll" /> <!-- scroll mount -->
<SlideRankPill deckSlug variantSlug surface="play" position="top-right" /> <!-- play mount -->
DeckOverlay--Play-UI passes surface="play" to the SlideRankPill it composes. Chroma’s scroll route updates to pass surface="scroll" explicitly. The pill’s inline script includes surface in its POST body. No backwards-compat default — if a consumer omits the prop, log a one-time console warning and default to "scroll" to preserve v1 behavior; this gives consumers time to update without breaking writes.
D. Two SVG icons — <ScrollIcon> and <PlayIcon>
Paired, visually balanced, ~16×16, currentColor-fillable, line-drawn. Initial sketch:
- ScrollIcon — a downward arrow inside a narrow vertical rectangle, suggesting “scroll down the page”. (Alternates: mouse-wheel; page-down glyph; column with horizontal lines + chevron.)
- PlayIcon — the classic ▶ rightward-pointing triangle inside a square frame, matching the calmstorm aesthetic.
Both as inline <svg> Astro components at apps/deck-shell/src/components/icons/. Draft three pairs at the start of the build, founder picks one.
E. Per-column shippability banner — the dominant rollup
At each variant-column header:
ENHANCED-V3
SCROLL ●14 ⬢2 ◐0 ✖0 ┐
PLAY ●9 ⬢4 ◐2 ✖1 ┘ → "READY" badge if both surfaces all ≥ passable
otherwise: "2 of 16 need work" (count of non-≥P)
Glyphs (★ ● vs P ⬢ vs C ◐ vs U ✖) carried directly into the rollup; tooltips on each. The “READY” badge is the binary signal that says “this column is shippable.” It’s green when both scroll and play for every slot in this variant are ≥ passable.
F. Per-row drift indicator — the subtle rollup
At each slot-row’s right edge, a small ≠ symbol when at least one cell in the row has scroll-status ≠ play-status. Hovering shows which variants drifted and what the drift was. Click does nothing; this is a glance affordance only.
For row aggregate “best across variants” — defer. Too much, and the drift indicator is the more workflow-aligned signal.
G. Route shape and registry
Inject one new route from the shell:
/toc/[deckSlug] → deck-level matrix (NEW)
/toc/[deckSlug]/[variantSlug] → 302 to /toc/[deckSlug]?variant=X (existing route becomes redirect)
The ?variant=X query param scrolls the matrix horizontally so that column is centered, and adds a subtle ring around it. No backend rendering change beyond the redirect.
Phase 1 — Audits schema v2 migration
Goal: the storage layer supports two surfaces per (slot, variant) without losing v1 data.
- Update
apps/deck-shell/src/types/index.ts:SurfaceRankEntry= today’sRankEntry({status, rankedAt, rankedBy, notes}).RankEntryV2={scroll?: SurfaceRankEntry, play?: SurfaceRankEntry}.Auditbecomes{schema: 2, ranks: Record<string, RankEntryV2>}.
- Update
registry-loader.tsloadAuditRegistry:- If file has
schema: 1, transform eachranks[k]from{status, ...}to{scroll: {status, ...}}. Return as schema 2 in memory. - On next
writeAuditRegistry, persist as schema 2 (the in-memory rewrite naturally flows through).
- If file has
- Add
apps/deck-shell/scripts/migrate-audits-v1-to-v2.ts— one-shot CLI that reads the audits file, applies the same transform, writes it back. Founder runs once per consumer site that has audits. - Run migration manually against
client-sites/chroma-decks/data/audits/slides.jsonand verify the 17 enhanced-v2 entries land underscroll.statuscleanly.
Acceptance: schema v1 file loads as v2; v1 ratings appear under scroll and not under play; pnpm build green.
Phase 2 — /api/slide-rank surface-aware POST
Goal: the rank endpoint writes to the correct surface.
- Extend POST body shape to require
surface: "scroll" | "play". - Validate: 400 if
surfacemissing or not in{scroll, play}. - Apply write:
audit.ranks[key] ??= {}; audit.ranks[key][surface] = {...entry}. Forstatus === "pending"delete only that surface; if both surfaces are absent after deletion, remove the key entirely. - GET unchanged (returns whole audit).
Acceptance: POST surface="scroll" writes only scroll.status; POST surface="play" writes only play.status; the other surface untouched.
Phase 3 — SlideRankPill surface-as-a-prop
Goal: the pill writes to whichever surface it’s mounted on.
- Add
surface: "scroll" | "play"prop toSlideRankPill.astro. - Pill renders
data-surface={surface}on the root element. - The pill’s inline GET reads
audit.ranks[key]?.[surface]?.status(not justaudit.ranks[key]?.status). - The pill’s inline POST adds
surfaceto its body. - The pill’s title-bar UI says “Scroll review” or “Play review” — small label change so the founder always knows which surface they’re rating from.
- Backcompat warning: if
surfaceprop is omitted, console.warn once and default to"scroll". Removed in a later release. - Update
DeckOverlay--Play-UI.astroto passsurface="play"to the composed pill. - Update
chroma-decks/src/pages/scroll/pitch/enhanced-v3/index.astro<DeckOverlayScrollUI>so the pill inside passessurface="scroll". (Or wireDeckOverlay--Scroll-UIto set it by default — likely the right move; check the component.)
Acceptance: rating a slot from /scroll/.../enhanced-v3/ writes to scroll.status; rating the same slot from /play/.../enhanced-v3/05/ writes to play.status; both visible in the audits file.
Phase 4 — Two SVG icons
Goal: scroll-glyph and play-glyph, balanced as a pair.
- Draft three pairs in
apps/deck-shell/src/components/icons/(e.g.ScrollIcon.astro,PlayIcon.astro). - Render all three pairs on a one-off
/dev/iconspage for founder review. - Founder picks one; remove the other two.
- Final pair exported via
@dididecks/shell/components/icons/ScrollIcon.astroand.../PlayIcon.astro.
Acceptance: two <svg> Astro components, currentColor-fillable, balanced when placed side-by-side at 16px, founder-approved.
Stop-and-show point: founder picks icon pair before Phase 5 begins.
Phase 5 — /toc/[deck]/ matrix route
Goal: the new deck-level matrix.
- Inject route
/toc/[deckSlug]atapps/deck-shell/src/routes/toc-deck.astro. getStaticPathsenumerates decks (not variants).- Page-level: load all variants for this deck, all slots for each variant (union into rows by slot/slug identity), and the full audit blob.
- Render the matrix:
- Header row: per-variant column with the shippability banner (scroll counts | play counts | READY badge).
- Body rows: per-slot, one cell per variant.
- Each cell:
<ScrollIcon><Chip status={ranks[k].scroll?.status} />link to/scroll/{deck}/{variant}/#slot-NN+<PlayIcon><Chip status={ranks[k].play?.status} />link to/play/{deck}/{variant}/{slot}/. - Disabled / dimmed play half if the per-slide Play-UI file does not exist (uses the existing
perSlideFileExistshelper). - Right-edge of each row:
≠if any variant in this row has scroll != play.
- Chip palette inherits from chroma’s existing TOC palette tokens; no new tokens introduced.
?variant=Xquery — vanilla anchor + a CSS ring on the matching column. No JS required if implemented via:targetor a query-param-driven class.
Acceptance: /toc/pitch/ renders the matrix; every cell links to the correct surface; the READY badge appears on variants where every cell is ≥ passable on both surfaces; the drift indicator appears on rows with scroll != play.
Stop-and-show point: founder opens /toc/pitch/, confirms the matrix answers “which variant is closest to ready” at a glance.
Phase 6 — Keep /toc/[deck]/[variant]/ as the variant landing (REVISED 2026-05-17)
Goal: the per-variant TOC stays alive as the variant landing/index — distinct from the deck-level matrix.
The original Phase 6 (“deprecate to a redirect”) was a wrong call surfaced during in-flight review. The two surfaces serve different needs:
/toc/[deck]/matrix — cross-variant comparison. “Which variant is closest to ready? Where’s the drift between scroll and play?”/toc/[deck]/[variant]/per-variant TOC — land in one variant. “What slots are in this variant? What’s each one’s status? What’s available for me to open?”
Both have a place. Folding the per-variant TOC into a redirect would lose the “land in one variant” surface — exactly the affordance the founder reaches for when clicking a variant name in the matrix header. Confirmed during 2026-05-17 in-flight review: variant-name in matrix → per-variant TOC.
Revised Phase 6 scope (smaller, more sensible):
- Keep
apps/deck-shell/src/routes/toc.astroas-is functionally. It already reads viapreferredSurfaceEntryfor dual-surface awareness. No deprecation. - Audit internal links to verify the per-variant URL is reachable as a landing target (matrix variant name → it; chroma’s variant cards → it). No regressions.
- Polish pass if needed — make the page feel like a variant landing, not just a slot list. Could include: variant lede above the table, a “Now showing in matrix view ↗” link back to
/toc/[deck]/?variant={v}, scroll/play surface launch buttons paralleling the matrix’s per-column rows. This is judgment-pass work, not load-bearing.
Acceptance: clicking a variant name in the matrix header lands at /toc/[deck]/[variant]/ and shows that variant’s slot list. No broken links anywhere in chroma’s build output. Per-variant TOC has a clear identity distinct from the deck-level matrix.
Phase 7 — Cross-cutting verification + version bump
Goal: ship cleanly with no regressions.
pnpm buildagainst chroma — all routes green, 46+ static pages emitted.- Manual smoke-test:
/toc/pitch/matrix renders with chroma’s audits.- Click scroll-chip in cell →
/scroll/pitch/enhanced-v3/#slot-05(or wherever). - Click play-chip in cell →
/play/pitch/enhanced-v3/05/. - Rate slot 05 in
/scroll/→ reload/toc/pitch/→ scroll-chip updates. - Rate slot 05 in
/play/→ reload/toc/pitch/→ play-chip updates. - Drift
≠appears on row 05.
- Old TOC URL
/toc/pitch/enhanced-v3/redirects cleanly. - Bump
apps/deck-shell/package.jsonfrom0.1.0-rc.0to0.1.0-rc.2. - Bump audits schema noted in changelog.
- Submodule pointer bump in
dididecks-ai.
Acceptance: build green, all surfaces round-trip, founder confirms the matrix is the right shape.
Stop-and-show points
- End of Phase 1 — audits migrated. Founder opens the audits JSON and sees v1 entries now nested under
scroll. Quick check. - End of Phase 4 — founder picks icon pair from three drafts. Blocks Phase 5.
- End of Phase 5 — the big one. Founder opens
/toc/pitch/and either says “yes, this is the workflow view I wanted” or names a specific further gap.
Open questions to resolve in flight
- Anchor format for
/scroll/{variant}/#slot-NN. Chroma’s scroll route uses<section data-slot="01">— does it also haveid="slot-01"? If not, anchors won’t work; either add the IDs or use?focus=01+ JS scroll-into-view. Verify in Phase 5. - READY badge threshold. “Every cell ≥ passable on both surfaces” — does “empty / never rated” count as ≥ passable, or as a blocker? Default: blocker (a never-rated slot is unverified, so the column isn’t ready). Founder confirms in Phase 5.
- Variant column width on narrow viewports. Four variants × two chips each gets crowded under 1200px. Sticky first column + horizontal scroll on small viewports? Or collapse to “stacked variant cards” under a breakpoint? Decide in flight; default to horizontal scroll.
- Drift severity gradation. A
U-on-scroll vs★-on-play is a much louder drift thanP-vs-★. Worth a coloured indicator (yellow vs red) or is ”≠ exists / doesn’t exist” enough? Defer; v1 binary, iterate if founder asks. - The “remix” affordance. Workflow step 4 says the founder may pick slot 03 from v2 and slot 09 from v3. The matrix surfaces the comparison but doesn’t act on it. Out of scope here — but worth a follow-up plan: a “blessed slot” concept where per-(slot,variant) carries an extra
blessed: trueflag, and a “build chimera variant” affordance walks blessings into a new variant. Capture as a follow-up. - /toc/index for multi-deck dashboard. Not needed today (chroma is the only deck). If/when dididecks grows a second client deck, revisit.
Follow-up plans this work queues
These are separate plans this work makes possible:
- Blessed-slot remix affordance. Per-(slot, variant) flag + a chimera-variant builder that walks blessings into a new variant. Workflow step 4 made literal.
- In-matrix rate-from-TOC. Add the SlideRankPill’s 4-button mini-pill to each cell so the founder can rate without navigating. Requires resolving “what’s canonical” — defer until the read-only matrix proves insufficient.
- Phase B — full calmstorm primitive lift. Still queued. Unchanged by this plan.
- Phase A.7 — publish. Still blocked on org-name decision. Unchanged by this plan.
Cross-references
- [[../explorations/Plans-Inventory-2026-05-16]] — current state of the union; this plan extends Path A and folds in a TOC-iteration thread the founder had flagged.
- [[Phase-A-Plus-Plus-Play-Fidelity-In-Play-Ranking-and-Variant-URL-Safety]] — A++.2 (just shipped) is the precondition for play-side rating writes.
- [[Restore-Calmstorm-Nav-Elegance-as-Themable-Shell-Primitives]] — Step 7 (chroma override sheet) is the other partial-shipped plan; both touch the chroma-side theming surface but don’t conflict.
- [[../sitemap/components/DeckOverlay—Play-UI]] — composes the surface-aware SlideRankPill this plan extends.
- [[../sitemap/components/SlideRankPill]] — surface prop lives here.
- [[../specs/Dididecks-AI-Slide-Decks-as-Code]] — parent spec; this plan operationalizes “review status as workflow state”.
deck-iteration-workflowskill — the workflow this plan literalizes into a UI.context-vigilanceskill (status-discipline reference) — for the status-sweep at the end of each phase.- Calmstorm-decks
client-sites/calmstorm-decks/src/pages/index.astro— the reference TOC pattern this plan adopts (with the dual-surface extension).
Status / next step
Status: Draft, ready for founder approval.
Immediate next step on approval: Phase 1 — audits schema v2 migration. Smallest contained change; lays groundwork for everything else.
Total estimated effort: Phases 1–3 are small (under an hour each). Phase 4 is bounded by founder-icon-picking. Phase 5 is the substance — half a day for the matrix to be the right shape. Phase 6 is trivial. Phase 7 is verification. Total: roughly one focused day plus a quick founder check-in for the icons.
Required from the founder during execution: (a) confirm v1 → v2 audits migration looks right after Phase 1; (b) pick the icon pair after Phase 4; (c) the big one — confirm the matrix is the workflow view they wanted after Phase 5.