← Corpus / dididecks-ai / plan
Stand Up @dididecks/shell and Ship the Chroma TOC + Slide-Ranking UI
Phase A of the exploration that landed on `@dididecks/shell` as the path to chroma-↔-calmstorm parity. Scaffold `apps/deck-shell/` inside the dididecks-ai monorepo as an Astro integration; ship two seed routes (TOC + slide-ranking pills) and one decomposition-stub generator that bridges the deck-iteration-workflow Phase 1 → Phase 2 transition; install in `client-sites/chroma-decks/` first via local link, then via a v0.1.0 publish to a private pnpm-scoped npm registry. Each client-site stays a standalone repo on its own Vercel project; the shell is the only thing that travels.
- Path
- plans/Stand-Up-Dididecks-Shell-and-Ship-Chroma-TOC-Ranking.md
- Authors
- Michael Staton
- Augmented with
- Claude Code (Opus 4.7, 1M context)
- Tags
- Dididecks-Shell · Astro-Integration · Monorepo-First-Capability · Chroma-Decks · TOC-and-Ranking · Phase-1-to-Phase-2-Bridge · Decomposition-Stub-Generator · Private-Pnpm-Scoped-Registry · GitHub-Packages · Deck-Iteration-Workflow · Non-Destructive-Adoption
Stand Up @dididecks/shell and Ship the Chroma TOC + Slide-Ranking UI
Plan of record for Phase A of [[../explorations/Chroma-Parity-and-the-Path-to-a-Shared-Deck-UI-Module]]. The exploration’s resolution:
@dididecks/shellis an Astro integration published to a private pnpm-scoped npm registry; each client-site stays a standalone repo on its own Vercel project; the shell is the single dependency they share. This plan executes the minimum-viable first slice of that architecture: a scaffolded integration package with just enough surface to ship the founder-facing TOC + slide-ranking UI that doubles as the Phase 1 → Phase 2 transition tooling from the [[../../../../context-v/skills/deck-iteration-workflow/SKILL]] skill.Everything past Phase A — lifting calmstorm’s mature primitives, the
/playruntime, auth + telemetry, the export pipeline — gets its own follow-up plan once this one has been stress-tested by chroma. This plan is intentionally small.
Why this plan exists
This is the first monorepo capability the dididecks-ai pseudomonorepo ships. Until now dididecks-ai/ has been a host for two unrelated submodules (client-sites/calmstorm-decks and client-sites/reach-edu-hub, plus the newer client-sites/chroma-decks) and a context-v/. There has been no parent-level shared code. The exploration documents three forces pushing toward @dididecks/shell:
- Vercel-deployability without architectural compromise. Each client-site’s deploy stays identical to today — standalone repo, standalone Vercel project, single
dist/, single domain — while the shell evolves as one coherent unit consumed via npm. - The empirical-prior commitment from the parent spec. Chroma is greenfield enough to absorb a v0.1 shell without legacy drag; calmstorm migrates onto the shell second, after the API has been stress-tested by one real consumer.
- The non-destructive-iteration commitment (Design Principle #3 of [[../specs/Dididecks-AI-Slide-Decks-as-Code]]). Adding the shell is purely additive in chroma’s repo: install dep, register integration, gradually shrink
src/as shell-provided surfaces take over. The current working state is never broken.
The TOC + slide-ranking UI is chosen as the Phase A deliverable for one specific reason: it is the tooling the founder needs to drive the Phase 1 → Phase 2 transition. Today chroma’s enhanced-v1 / v2 / v3 each live as a single-page .astro variant. The deck-iteration-workflow says: rank each slide of a single-page variant, then decompose the urgent-redo + non-urgent-could-be-better slides into per-slide files first. Ranking is the input; decomposition is the output. The shell wires both ends together: rank → click “scaffold per-slide file” → empty src/components/slides/{variant}/{nn}-{slot}.astro exists in the chroma repo, ready for the recreation pass that makes the /play runtime work.
Scope boundary
In scope for Phase A (this plan):
- A new package at
dididecks-ai/apps/deck-shell/, named@dididecks/shell, structured as an Astro integration. - Two routes injected by the integration:
/toc/[deckSlug]/[variantSlug](TOC view) and/api/slide-rank(POST upsert, GET registry;prerender = false). - One developer-facing action: a “scaffold per-slide file” button per ranked-as-urgent-redo or non-urgent-could-be-better row that writes an empty stub into the client-site’s working tree at the canonical per-slide path.
- Local-link install in
client-sites/chroma-decks/for development. - v0.1.0 publish to a private pnpm-scoped npm registry once the shell stabilizes.
- Vercel verification: chroma-decks builds with the published shell as a dep, the TOC + ranking route renders, the static build cleanly skips the dev-only write-back route.
Explicitly out of scope (defers to follow-up plans):
- Lifting calmstorm’s
PageAsDeckWrapper,SlideCanvas,ContentFit,MetaTags,DeckHeader,DeckNav,GateScript, mode-switcher, brand-mark, etc., into the shell. That is Phase B of the exploration. - The
/playkeyboard presenter runtime (Phase D). - Auth + telemetry + admin (Phase D).
- Export pipeline (Phase E).
- The componentize-slides destination shift (now lands in
@dididecks/shell/components/instead ofclient-sites/calmstorm-decks/src/components/) — flagged for follow-up edit, not executed in this plan. - Calmstorm’s migration onto the shell. Calmstorm is mid-iteration with a sensitive partner audience and stays untouched by this plan.
- SSR / realtime / per-client database hydration. V2 hook per the exploration.
Preconditions
These have to land before Phase A.1 starts:
- Registry decision. [[../explorations/Chroma-Parity-and-the-Path-to-a-Shared-Deck-UI-Module]] flags GitHub Packages as the strong default (free for private, integrates with
lossless-grouporg auth, singleNPM_AUTH_TOKENenv var for Vercel). Confirm before scaffolding the package — thepublishConfig.registryfield inapps/deck-shell/package.jsonand the.npmrctemplate that ships with chroma both depend on it. Default to GitHub Packages unless a reason to choose otherwise surfaces. - pnpm in dev; Vercel does whatever Vercel does. The discipline is dev-mode strict: every local install, lockfile, script, doc snippet, and the
pnpm-lock.yamlchecked into each repo is pnpm — notnpm, notyarn, notbun. The shell publishes to the npm registry (because that is the registry), but every CLI invocation a human runs in this plan ispnpm. On the deploy side, Vercel auto-detects the package manager from the lockfile and that’s fine — no need to pin a custom build command unless something breaks. If the plan shows annpmcommand in the dev-side instructions, that is a bug to fix before executing; if Vercel internally falls back tonpmto install once the lockfile is committed, that’s a Vercel implementation detail and not a violation. - Branch tier.
dididecks-aiis ondevelopment;client-sites/chroma-decksis ondevelopment. Both stay ondevelopmentfor the duration of Phase A. Phase A does not promote either tomain— promotion happens after the v0.1.0 shell publish is verified on a Vercel preview build.
Architecture decisions to lock before code
These are choices the implementation will encode. Lock them up front so a downstream reader of the integration’s source can see the rationale without re-deriving it.
A. Package identity and layout
- Name:
@dididecks/shell(scoped to@dididecks). - Location in monorepo:
dididecks-ai/apps/deck-shell/. - Package manager: pnpm.
apps/deck-shell/is a pnpm workspace member ofdididecks-ai/— but the published artifact is what client-sites consume; workspace linkage is dev-only. - Module entrypoint:
apps/deck-shell/src/index.tsexportsdididecksShell(options)as a default. Astro integration contract. - Side entrypoints exposed via
package.jsonexports:@dididecks/shell— the integration factory (default).@dididecks/shell/routes/*— route components the integration injects (Astro doesn’t import these from user code; they live here so the injection paths are stable).@dididecks/shell/components/*— empty in Phase A; will holdPageAsDeckWrapper,SlideCanvas, etc. in Phase B.@dididecks/shell/runtime/*— empty in Phase A; will hold mode-switcher etc. in Phase B.
- Astro version: declared as a
peerDependency("astro": "^5.0.0"), never a runtime dep. The client-site brings its own Astro. publishConfig:{ "access": "restricted", "registry": "<the registry decided in Precondition 1>" }.
B. Integration options shape
The factory takes one options object. v0.1.0 surface, kept deliberately small:
export interface DididecksShellOptions {
/** Required. The client-site identifier — e.g. "chroma-decks". Logged + used in route titles. */
client: string;
/** Where the client's deck registry lives, relative to the client-site root. Default: "./src/data/decks.ts". */
decksRegistryPath?: string;
/** Where slide-ranking state persists, relative to the client-site root. Default: "./data/audits/slides.json". */
auditsPath?: string;
/** Default tier resolver fallback for unmapped routes. Default: "private". Phase B fills this in. */
distributionTier?: "private" | "shared" | "public";
}
Phase B adds runtime, auth, telemetry, theme options. Phase A does not, to keep the v0.1.0 surface minimal and the cost of breaking changes low.
C. The audit registry’s shape
Calmstorm’s data/audits/slides.json is a flat { slideId: status } map keyed by {nn}-{slug}-v{N}. That key shape is specific to calmstorm’s 51-file by-title/ pattern. Chroma’s variants are single-page composites (/scroll/pitch/enhanced-v3/index.astro), so the shell needs a richer key.
New key shape:
{
"schema": 1,
"ranks": {
"pitch/enhanced-v3/01-cover": {
"status": "urgent-redo",
"rankedAt": "2026-05-12T18:24:00.000Z",
"rankedBy": "founder",
"notes": null
},
"pitch/enhanced-v3/02-tldr": { "status": "passable", "rankedAt": "...", "rankedBy": "founder", "notes": null }
}
}
Backwards-compat for calmstorm: calmstorm’s existing flat-map shape stays untouched. When the shell eventually replaces calmstorm’s local slide-status.ts (Phase B / Phase C), a one-time migration translates the flat keys to {deck}/{variant}/{slot}. Not in this plan; flagged for the calmstorm migration plan.
D. Status enum
Verbatim from calmstorm — proven vocabulary, no need to redesign:
urgent-redonon-urgent-could-be-betterpassableperfectpending(implicit default for unranked slides; not persisted)
The shell exports this as a runtime const and a TS type. Client-sites that wrap or extend it (calmstorm later, reach-edu-hub eventually) override at the integration option level — not in v0.1.0.
E. Route paths the integration injects
| Route | Method | Purpose | Static or dev-only |
|---|---|---|---|
/toc/[deckSlug]/[variantSlug]/ | GET | TOC view: list slots in the variant, current rank per slot, pills to upsert rank, “scaffold per-slide file” button for ranks ∈ {urgent-redo, non-urgent-could-be-better} that have no existing per-slide file yet. | Static at build, hydrates ranks via fetch in dev. |
/api/slide-rank | GET / POST | Read full registry or POST { deckKey, variantKey, slot, status, notes? } to upsert. prerender = false. | Dev-only — static build skips it cleanly. |
/api/slide-decompose | POST | Body { deckKey, variantKey, slot, slug }. Writes empty stub file at src/components/slides/{variant}/{nn}-{slug}.astro in the client-site working tree. Refuses if file already exists (non-destructive). | Dev-only — prerender = false. |
Three routes total in Phase A. No /admin, no /access, no auth in front of any of them — /api/slide-rank and /api/slide-decompose are dev-only and depend on a running pnpm dev against the local working tree. They never ship to a Vercel build artifact.
F. The “scaffold per-slide file” contract (the Phase 1 → Phase 2 bridge)
The most architecturally specific decision in the plan. The button on the TOC fires POST /api/slide-decompose with the slot to decompose. The shell writes:
client-sites/chroma-decks/src/components/slides/{variant}/{NN}-{slug}.astro
…with content that is deliberately empty (just the Astro frontmatter ---\n--- and a placeholder <section> element). The single-page variant at src/pages/scroll/pitch/{variant}/index.astro keeps rendering as it is — the new file is dormant until something imports it. This honors Design Principle #3 (“never destroy”): the working scroll deck is untouched; the per-slide file is a new fork off the single-page source the founder recreates non-destructively during the next iteration session.
The shell never inlines content into the stub. It does not parse the single-page variant, does not extract the slot’s existing HTML, does not heuristically guess what should go in the per-slide file. That is the founder’s recreation pass — the whole point of the deck-iteration-workflow Phase 2 boundary is that the per-slide file is recreated, not extracted. Extraction encodes whatever inline shape happened to exist; recreation forces the founder to re-author the slot at component-library quality. The shell only sets up the empty container.
G. How the shell discovers the deck registry
Phase A reads client-sites/chroma-decks/src/data/decks.ts via a build-time dynamic import. The path comes from decksRegistryPath integration option, resolved relative to the Astro project root. The Deck/VariantRef types are the shell’s canonical types (re-exported as @dididecks/shell/types); chroma’s decks.ts already conforms because the shape was authored before the shell existed but already matches the canonical-types contract — that is a useful empirical signal.
How the shell discovers the slot list inside a variant in Phase A: it does NOT parse the single-page .astro files. Instead, the integration looks for an optional src/data/slides.ts registry in the client-site. If present, it reads SLOTS[variantSlug] → Array<{ slot: string; title: string; slug: string }>. If absent, the TOC route renders a “no slot registry found” empty state with copy that points to a one-paragraph how-to.
This is deliberate: the slot registry is a small, hand-authored file (chroma’s enhanced-v3 has ~14 slots; 30 lines of TS); maintaining it is cheap; auto-extracting from .astro ASTs is expensive engineering for a feature that should be founder-authored anyway. Phase B may add an opt-in auto-extract; Phase A does not.
H. Verifying ranks change the build without it
/api/slide-rank is dev-only — the static build skips it. But the TOC route is static. When does a Vercel rebuild pick up new ranks?
Answer: the static build reads data/audits/slides.json at build time and emits it into the TOC page. Pushing a commit that updates data/audits/slides.json triggers a Vercel redeploy and the new ranks appear in the next build. The dev write-back loop posts to /api/slide-rank which writes to the same JSON file — the founder rank → commit → push → Vercel rebuild flow is “rank, then commit.” The shell does not maintain a separate database; the JSON file is the database.
This is a deliberate v1 simplification. Multi-author live ranking and conflict resolution are v2 hooks (the SSR + per-client DB direction the exploration flags).
Phase A.1 — Scaffold apps/deck-shell/
The first commit. Pure structure; no behavior yet.
- Confirm
dididecks-ai/has a rootpnpm-workspace.yaml. If not, create one declaringpackages: ['apps/*', 'client-sites/*']. Note:client-sites/*are git submodules with their ownpackage.jsons — they are in the workspace for dev-link purposes but each is independently publishable / deployable. mkdir -p apps/deck-shell/src/{routes,components,runtime,types}from the dididecks-ai root.- Author
apps/deck-shell/package.json:"name": "@dididecks/shell""version": "0.0.1""type": "module""main": "./src/index.ts""types": "./src/index.ts""exports"field with the four entrypoints from decision A."peerDependencies": { "astro": "^5.0.0" }"publishConfig": { "access": "restricted", "registry": "<from Precondition 1>" }
- Author
apps/deck-shell/tsconfig.jsonextending the dididecks-ai root config if one exists, otherwise a minimal Astro-compatible TS config. - Author
apps/deck-shell/src/index.tsexporting a placeholder factory:import type { AstroIntegration } from "astro"; export interface DididecksShellOptions { /* per decision B */ } export default function dididecksShell(options: DididecksShellOptions): AstroIntegration { return { name: "@dididecks/shell", hooks: { "astro:config:setup": ({ logger }) => { logger.info(`@dididecks/shell loaded for client: ${options.client}`); }, }, }; } - Author
apps/deck-shell/README.md— one screen: what it is, install command, minimalastro.config.mjssnippet, link back to this plan. - From the dididecks-ai root:
pnpm install. Verifynode_modules/@dididecks/shellsymlinks back toapps/deck-shell/. - Commit:
init(deck-shell): scaffold @dididecks/shell as Astro integration package.
Acceptance: pnpm --filter @dididecks/shell typecheck passes; the placeholder factory is importable from a sibling workspace member.
Phase A.2 — Wire the integration to inject routes
Second commit. The integration starts injecting routes — but the routes are still empty placeholders. Separating route-injection from route-content keeps the two diffs reviewable.
- In
apps/deck-shell/src/index.ts, expand theastro:config:setuphook to callinjectRoutefor each of the three routes from decision E. Pattern entry points reference./routes/toc.astro,./routes/api/slide-rank.ts,./routes/api/slide-decompose.tsfrom the package’s own directory. - Author each of the three route files as minimal placeholders that return a 200 with a stub body —
<h1>TOC — deck: {deckSlug}, variant: {variantSlug}</h1>for the TOC route;Response.json({ stub: true })for the API routes. - Verify route-injection works by creating a throwaway
apps/deck-shell/.test-host/minimal Astro project that consumes the integration and runspnpm astro dev. The three routes should be reachable. - Commit:
add(deck-shell): inject TOC + slide-rank + slide-decompose route placeholders via astro:config:setup.
Acceptance: the placeholder TOC route renders, the placeholder API routes respond.
Phase A.3 — Implement the TOC view
Third commit. The TOC route gains real content. The slide-rank API is still placeholder.
- In
apps/deck-shell/src/routes/toc.astro:- Read the deck registry from the path the integration was given. Use dynamic import keyed on the resolved absolute path.
- Read the audits file from the path the integration was given. Tolerate missing-file → empty registry.
- Read the slot registry (
src/data/slides.tsshape from decision G). Tolerate missing-file → empty-state copy. - Cross-reference per-slide file existence: for each slot, check whether
src/components/slides/{variant}/{nn}-{slug}.astroexists in the client-site. This drives whether the “scaffold per-slide file” button shows. - Emit one row per slot with: number, title, current rank (or “Pending”), five pill buttons (one per status), and a conditional “scaffold per-slide file” button.
- Style with inline
<style>scoped to the route — no external CSS dependencies. The shell’s v0.1.0 visual cost should be zero; chroma’s theme tokens are not consumed yet.
- Verify in the test-host: visit
/toc/pitch/enhanced-v3/, see all 14-ish slots of enhanced-v3 listed. - Verify in chroma: register the shell as a workspace dep (Phase A.6 covers the full install path; this is just enough to render the route from chroma’s own variants).
- Commit:
add(deck-shell): render TOC route with slot rows + rank pills + decompose button visibility.
Acceptance: the TOC route lists slots, shows their current rank, and the decompose button shows up for the right rows (no per-slide file yet + rank is urgent-redo or non-urgent-could-be-better).
Phase A.4 — Implement the rank write-back API
Fourth commit. The slide-rank API now reads + writes the audits file.
- Replace
apps/deck-shell/src/routes/api/slide-rank.tsplaceholder with a real implementation:export const prerender = false;at module scope.- On
GET: read the audits file from the integration’s resolved path. Return the full registry as JSON. - On
POST: validate body{ deckKey: string; variantKey: string; slot: string; status: Status; notes?: string|null }. Build the composite key${deckKey}/${variantKey}/${slot}. Upsert into theranksmap withrankedAt: new Date().toISOString(),rankedBy: "founder"(placeholder; later integration options will provide the identity). Onstatus === "pending", delete the key. - Persist back to the audits file with sorted keys for deterministic diffs (same discipline as calmstorm’s
api/slide-status.ts).
- Update the TOC route’s client-side pill
onClickto POST to/api/slide-rankand optimistically update the displayed rank. - Verify the round-trip: rank a slot in dev, hard-refresh the TOC route, see the rank persist; check
git diff data/audits/slides.jsonand see the expected upsert. - Commit:
add(deck-shell): implement /api/slide-rank read/write with per-deck/variant/slot composite key.
Acceptance: ranking a slot from the dev TOC writes to the chroma working tree’s data/audits/slides.json with the new key shape, and re-rendering picks up the change.
Phase A.5 — Implement the decomposition stub generator
Fifth commit. The decompose API and the button wire together.
- Replace
apps/deck-shell/src/routes/api/slide-decompose.tsplaceholder:export const prerender = false;.- Validate body
{ deckKey, variantKey, slot, slug }. Compute the target path inside the client-site:src/components/slides/{variantKey}/{slot}-{slug}.astro. - Refuse to overwrite if the file already exists. Return 409 with a clear error.
- Otherwise, write the empty stub. The stub is exactly:
--- // Generated by @dididecks/shell as a Phase 1 → Phase 2 decomposition stub. // Recreate the slot content here; do not extract from the single-page variant. --- <section data-slot="{slot}" data-variant="{variantKey}"></section> - Return 200 with the new file path.
- Wire the TOC route’s decompose button to POST to this endpoint with the right body, then re-render the row (the button hides, the file-exists indicator shows).
- Verify: rank a slot as
urgent-redo, click decompose, see the empty file appear inclient-sites/chroma-decks/src/components/slides/enhanced-v3/{nn}-{slug}.astro. Confirm the existing scroll deck at/scroll/pitch/enhanced-v3/is unchanged. - Commit:
add(deck-shell): scaffold per-slide stub files on rank-driven decompose action.
Acceptance: the Phase 1 → Phase 2 boundary is now operational — a founder can rank, then one-click decompose the slots they want to recreate, and end up with an empty per-slide file in their working tree without touching the working single-page variant.
Phase A.6 — Install in chroma-decks via local link
Sixth commit. Chroma-decks gains its first dependency on the shell.
- From
client-sites/chroma-decks/:pnpm add '@dididecks/shell@workspace:*'(since chroma is a workspace member, pnpm resolves to the localapps/deck-shell/source). Lockfile updates. - Add the integration to
client-sites/chroma-decks/astro.config.mjs:import dididecksShell from "@dididecks/shell"; export default defineConfig({ integrations: [ dididecksShell({ client: "chroma-decks", decksRegistryPath: "./src/data/decks.ts", auditsPath: "./data/audits/slides.json", distributionTier: "private", }), ], }); - Create
client-sites/chroma-decks/src/data/slides.tswith the slot registry for at least one variant (start with enhanced-v3 — the densest, most ready). 14-ish entries. Hand-authored. - Create
client-sites/chroma-decks/data/audits/slides.jsonas{"schema": 1, "ranks": {}}. pnpm --filter chroma-decks dev→ visit/toc/pitch/enhanced-v3/. Verify the TOC renders, ranks persist, decompose works.- Commit (in the chroma-decks submodule):
add(deps,toc): consume @dididecks/shell; wire TOC + slide-ranking for enhanced-v3. - Commit (in dididecks-ai): submodule pointer bump for chroma-decks.
Acceptance: the founder can drive the TOC + ranking + decompose flow end-to-end in pnpm dev against chroma’s actual content.
Phase A.7 — Publish v0.1.0 to the private registry and switch chroma to the published version
Seventh commit (in the deck-shell package + chroma-decks separately). The point where the workspace-link goes away and chroma starts consuming the shell exactly as a future client-site would.
- Bump
apps/deck-shell/package.jsonversion to0.1.0. pnpm --filter @dididecks/shell pack→ inspect the resulting tarball. Verify the exports map is correct, no source-only files like tsconfig or test-host leak, the README is included.- Authenticate to the chosen registry (Precondition 1 — GitHub Packages default). For GitHub Packages: add
~/.npmrcline//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}; ensure the token hasread:packagesandwrite:packagesscopes. pnpm --filter @dididecks/shell publish(with--access restrictedif not already in publishConfig).- Confirm visibility:
pnpm view @dididecks/shell --registry=<registry>returns the just-published version. - In
client-sites/chroma-decks/: change thepackage.jsondep from"workspace:*"to"^0.1.0". Add an.npmrcin the chroma-decks repo root pointing the@dididecksscope at the registry. Document the requiredNPM_AUTH_TOKEN(orGITHUB_TOKEN) env var. pnpm installfrom chroma’s repo (outside the workspace context — simulate the standalone-repo install). Verify resolution succeeds against the registry, not the workspace.- Run chroma’s dev server again; the integration should behave identically.
- Commits: in deck-shell,
ship(deck-shell): publish v0.1.0 to private registry; in chroma-decks,update(deps): consume @dididecks/shell from private registry (was workspace-linked).
Acceptance: chroma-decks resolves @dididecks/shell from the published private registry; the workspace-link is no longer relied on; the dev server still works identically.
Phase A.8 — Verify on Vercel
Final commit on the chroma side. Phase A is not done until a Vercel production-shape build succeeds with the shell as a dep.
- Configure the Vercel project for
lossless-group/chroma-decksto passNPM_AUTH_TOKEN(orGITHUB_TOKEN) as an env var so the private-registry resolution works. Let Vercel auto-detect the package manager from the committedpnpm-lock.yaml; do not pin a custom build command unless detection fails. (Per Precondition 2, the pnpm discipline is dev-mode strict; Vercel’s internal choice of installer is not constrained.) - Push the chroma-decks
developmentbranch. Watch the Vercel preview build. - Verify in the build log:
@dididecks/shell@0.1.0resolves cleanly from the private registry; the build completes; the staticdist/includes/toc/pitch/enhanced-v3/index.html. - Visit the preview URL. The TOC route should render with the seeded ranks (whatever was committed to
data/audits/slides.jsonat push time). The/api/slide-rankand/api/slide-decomposeroutes should 404 cleanly on the static build — that is the intended behavior; they are dev-only. - Verify the existing scroll deck at
/scroll/pitch/enhanced-v3/is unchanged — same content, same styling, same behavior. The shell adoption was purely additive.
Acceptance: Vercel preview build succeeds, TOC renders, scroll deck is unchanged. Phase A is complete.
What’s left untouched after Phase A
A reading of the chroma-decks working tree after Phase A should show:
package.jsonhas one new dep (@dididecks/shell).astro.config.mjshas one new integration registered.src/data/slides.tsis new (the slot registry).data/audits/slides.jsonis new.- A handful of
src/components/slides/{variant}/{nn}-{slug}.astroempty stubs (zero to many, depending on how much decomposition the founder did during testing). .npmrcis new (scope-to-registry routing).- Everything else —
src/layouts/PageAsDeckWrapper.astro,SlideShell.astro,ModeToggle.astro,Wordmark.astro, the foursrc/pages/scroll/pitch/{variant}/index.astrofiles,DESIGN.md,corpus/,changelog/— is unchanged. The shell adoption is purely additive in Phase A. Phase B is where the lighter parallel copies of calmstorm’s primitives get deleted in favor of shell-provided versions.
This is the non-destructive-iteration commitment made concrete.
Open questions to resolve in flight
These do not block starting. Flagged for resolution during the build:
- Private registry final choice. Precondition 1 leans GitHub Packages. If something specific to GitHub Packages’ auth flow surfaces during Phase A.7, fall back to npm-private or self-hosted Verdaccio. The architecture cost of switching is bounded: change
publishConfig.registryin the shell, change.npmrcin chroma, re-publish. slides.tsslot-registry shape. The plan assumesRecord<variantSlug, Array<{slot, title, slug}>>keyed by variant. If chroma’s enhanced-v1/v2/v3 turn out to have meaningfully different slot orderings or titles, the shape might need to be richer (per-slot per-variant overrides). Keep the registry hand-authored for v0.1; revisit only if the founder reports friction.- Composite-key format
{deck}/{variant}/{slot}. Forward-slashes are nice for URLs but awkward when used as JSON object keys (the dot-and-slash combos some tools parse weirdly). If issues surface, switch to::separator. Cheap to change before the registry has real data. - Decompose stub content. The current stub is just
<section data-slot="..." data-variant="..."></section>. If founders consistently end up adding the same boilerplate by hand on top, promote that boilerplate into the stub template. Otherwise leave the stub deliberately minimal. - What about decompose for a
passableorperfectrank? The current rule hides the button for those ranks. Counter-argument: a slot could be passable for the scroll deck but still want a separate per-slide file for/play. If founders ask for the button to always show, change the rule. Currently bias toward hiding to prevent accidental “I clicked it without meaning to” stub files.
Follow-up plans this work queues
These are separate plans that Phase A makes possible. None of them block Phase A; all of them are easier once Phase A has shipped:
- Edit [[../plans/Componentize-Slides-and-Establish-Component-Library]] to retarget the destination from
calmstorm-decks/src/components/to@dididecks/shell/components/. The substance of that plan is unchanged — the taxonomy split (primitives/,patterns/,share/,publish/,audit/,diagrams/), the per-slide cadence, the “promote on ≥2 uses” rule, the tier-aware-MetaTags discipline — all of it stays. The only change is where the files land. A small Phase-0-update edit per [[../explorations/Chroma-Parity-and-the-Path-to-a-Shared-Deck-UI-Module]] Open Question #4. - Phase B plan — Lift calmstorm’s mature primitives into
@dididecks/shell.PageAsDeckWrapper,SlideCanvas,ContentFit, tier-awareMetaTags,DeckHeader,DeckNav,GateScript, mode-switcher behavior, brand-mark slot. Chroma deletes its lighter parallel copies; calmstorm migrates second in a quiet window. - Phase D plan — The
/playruntime in the shell. Once enough per-slide files exist (the output of Phase A’s decompose action), the/playkeyboard presenter has something to render. Lift wholesale from calmstorm’s/play,/play/section/[slot],/play/variant/[variant]. - Phase E plan — Export pipeline as
pnpm dlx @dididecks/shell export. Calmstorm’s Playwright-basedscripts/export-decks.tslifted into the shell as a CLI subcommand. Both clients benefit immediately.
Each gets its own plan in context-v/plans/ when its turn comes.
Cross-references
- Parent exploration: [[../explorations/Chroma-Parity-and-the-Path-to-a-Shared-Deck-UI-Module]] — the document this plan executes Phase A of. Specifically §“Where the dialog landed (2026-05-12)” → §“Per-feature parity plan (post-dialog)” → Phase A items 1–8.
- Parent spec: [[../specs/Dididecks-AI-Slide-Decks-as-Code]] — NS-1 commits to the two-sided system;
@dididecks/shellis the architectural seed of NS-1 Side 2’s white-label publish surface. - Sibling spec: [[../specs/Dididecks-AI-Visual-and-Diagram-Component-Library]] — the diagram primitives live in
@dididecks/shell/diagrams/(or split into a@dididecks/visual-librarysibling) once Phase B + Phase C have stabilized. - Sibling plan: [[../plans/Componentize-Slides-and-Establish-Component-Library]] — its destination shifts from calmstorm in-place to the shell; queued as follow-up #1 above.
- Sibling plan: [[../plans/Init-Chroma-Decks-Client-Site]] — chroma’s scaffold plan explicitly deferred
/play, auth, and the variant chooser pattern. This plan honors all of those deferrals; only the TOC + rank UI lands. - Sibling plan: [[../plans/Init-DidiDecks-as-core-Submodule-of-AI-Labs]] — the monorepo-init plan.
apps/deck-shell/is the first inhabitant ofapps/; the init plan’s later phases (splash roll-up, package surfaces) inherit this without further edits. - Calmstorm reference files the shell takes its pattern from (not lifted in Phase A, but Phase B’s source-of-truth):
client-sites/calmstorm-decks/src/lib/slide-status.ts— the build-time row-builder pattern.client-sites/calmstorm-decks/src/pages/api/slide-status.ts— the dev-only POST/GET API pattern withprerender = false.client-sites/calmstorm-decks/data/audits/slides.json— the flat-map shape Phase A’s composite-key registry supersedes.
- Skills loaded for this work:
pseudomonorepos—apps/is the right home for the shell per the placement rules.astro-knots— hard prohibitions the shell inherits (no React, no JSX, no Angular). pnpm-only.context-vigilance— this plan’s own framework.deck-iteration-workflow— the six-phase rhythm Phase A’s TOC + rank + decompose operationalizes.theme-system— relevant in Phase B when the mode-switcher behavior lifts into the shell.maintain-splash-pages— relevant when the dididecks-ai splash surfaces the shell’s version + adoption status (Open Question #5 in the exploration).
Status / next step
Status: Draft, awaiting user sign-off.
Immediate next step on approval: Begin Phase A.1 — scaffold apps/deck-shell/ as an Astro integration package, commit the bare structure, verify it imports cleanly from a sibling workspace member. No behavior yet; just the package shape.
Stop-and-show points during execution (where the work pauses for user verification before continuing):
- End of Phase A.1 — package scaffolds, but does nothing.
- End of Phase A.4 — TOC renders + rank persists in dev. Founder can take it for a spin before the decompose generator goes in.
- End of Phase A.6 — chroma is consuming the shell via workspace-link. The full dev loop is exercisable end-to-end.
- End of Phase A.7 — chroma is consuming the published shell. The architecture is real.
- End of Phase A.8 — Vercel preview build is green. Phase A done.
Remaining work (as of 2026-05-16)
- Phase A.7 — publish
@dididecks/shell@0.1.0to a private registry. Still blocked on the GitHub-org-name decision. Three branches remain:- Create a
dididecksGitHub org (~2 minutes user-side). - Rename the package to
@lossless-group/dididecks-shell. - Use a non-GitHub registry (npm-private paid, self-hosted Verdaccio).
- Create a
- Phase A.8 — Vercel preview build verification post-publish. Trivially follows once A.7 resolves.
All other Phase A milestones (A.1–A.6) are shipped — apps/deck-shell/ exists, TOC + /api/slide-rank + /api/slide-decompose work, chroma consumes via workspace:*. The shell is at 0.1.0-rc.0; A.7 graduates it to 0.1.0.