@dididecks/shell Phase A+ — global nav, in-scroll rank pill, /play runtime, two seeded slides
Phase A shipped the integration that proved the architecture works. Phase A+ — landed the same day — closes the three concrete gaps the founder named seconds after first opening the smoke-test build. The shell now ships a global nav that auto-mounts on every shell-injected route, a floating `<SlideRankPill>` that lets a founder rank slots from inside a scroll deck via IntersectionObserver, and a `/play/[deck]/[variant]/[slot]` runtime with keyboard nav (← / → / Space / Home / End / F / C / T / Esc) plus a friendly empty-slot placeholder. Two seed per-slide files in chroma — a faithful cover recreation and a redesigned ask — prove the rank → decompose → recreate loop end-to-end. The whole release moves the shell from `0.0.1` to `0.1.0-rc.0`. Workspace-link mode throughout; Phase A.7 publish still queued.
@dididecks/shell Phase A+ — global nav, in-scroll rank pill, /play runtime, two seeded slides
Why Care?
Phase A — earlier today — shipped the integration. It proved an Astro
package consumed via astro:config:setup could inject routes into a
client-site, read that site's TS registries via in-memory esbuild, and
persist state back into the client-site's working tree without
copy-paste. That's an architecture proof. It wasn't yet a deck-iteration
tool the founder could actually use.
Phase A+ is the difference between "the integration loads" and "the
founder can rank a slot while scrolling, jump to /play to see how the
finished slot will present, and watch the rank persist across both
surfaces." Three new shell-injected surfaces — DididecksNav,
SlideRankPill, /play/[deck]/[variant]/[slot] — and two seeded slide
files in chroma turn the shell from "wired" into "usable."
If you are a Lossless contributor or a client iterating with us on a
deck: the practical thing that changed is that the shell now carries
reusable presentation chrome. Any future client-site that adopts
@dididecks/shell inherits the nav, the in-scroll ranking UI, and the
/play runtime as one dep. No per-client port. No per-client UI
debt.
What's New?
Three new shell components
<DididecksNav />— global navigation chrome. Auto-mounted on/toc/*and/play/*shell routes; also exported from@dididecks/shell/componentsfor opt-in import on any consumer-side route. Renders Scroll · TOC · Play · Changelog links derived from the consumer'sDECKSregistry (the first deck's first variant becomes the default target). Addsaria-current="page"on the active route. Brand<slot name="brand">for consumer wordmark overrides.<SlideRankPill />— floating overlay for in-place ranking from inside a scroll deck. The consumer drops it once at the end of the scroll-deck variant, addsdata-slot="NN" data-variant="..."to each<section>, and the pill follows the reader viaIntersectionObserver. Five status buttons (urgent-redo,non-urgent-could-be-better,passable,perfect,pending) post to/api/slide-rankoptimistically. Low-opacity at rest, full opacity on hover/focus. Reads/writes the same audits registry the TOC consumes — so ranking from anywhere updates the same source of truth.<PlayChrome />+<DecomposeFirstPlaceholder />— the internal building blocks of the new/playruntime. PlayChrome wraps a slot with the global nav at the top, a bottom strip showing{slot}/{total}+ ← / TOC / → buttons, and the keyboard handler. DecomposeFirstPlaceholder is the friendly empty-state shown when a slot has no per-slide file yet — links back to TOC with?focus={slot}.
A working /play runtime
/play/[deckSlug]/[variantSlug]/— redirect to slot 01 of the variant./play/[deckSlug]/[variantSlug]/[slot]/— renders the per-slide file atsrc/components/slides/{variant}/{slot}-{slug}.astrovia Vite'simport.meta.glob. If the file exists, renders it; if not, renders<DecomposeFirstPlaceholder>.getStaticPaths()enumerates every{deck, variant, slot}combo from the consumer's registries.
The keyboard contract matches calmstorm-decks's /play runtime by
convention so muscle memory transfers when calmstorm migrates onto
the shell in Phase B:
| Key | Action |
→ / Space / PageDown | Next slot |
← / Shift+Space / PageUp | Previous slot |
Home / End | First / Last slot |
F | Toggle fullscreen |
C | Toggle chrome (hide / show nav + bottom strip) |
T | Jump back to the TOC for this variant |
Esc | Exit fullscreen, show chrome |
Two seeded per-slide files in chroma
Both at client-sites/chroma-decks/src/components/slides/enhanced-v3/:
01-cover.astro— faithful recreation. Copied from the scroll-deck's cover section so/play/pitch/enhanced-v3/01/renders identically to its scroll-deck counterpart. Self-contained (importsglobal.cssso v3-* tokens resolve in standalone Play context).15-ask.astro— deliberate redesign. Same content as the scroll deck's section 15 ("Proposal") but composed at component-library quality — mega-numeral$12M(italic serif, 8–16rem clamp), two-column Round / Status<dl>blocks, milestones footer. No inline<pre>blocks. The deliberate departure proves Play can render a better version of a slot than the scroll deck currently has — the whole point of the deck-iteration-workflow's Phase 2.
TOC [view →] links
The TOC at /toc/[deck]/[variant]/ now shows a [view →] link on
each row whose per-slide file exists, linking to
/play/[deck]/[variant]/{slot}/. Closes the loop from the TOC side:
rank → scaffold → recreate → view in Play, all without leaving the
shell's surfaces.
Registry reconciliation
The chroma slides.ts slot registry had drifted from the actual
scroll-deck section order. Three renames + two slot-number shifts
reconciled the registry to the page reality (16 slots in their natural
order; ask moved from registry-slot 16 to slot 15 where it actually
lives on the page; colophon added at slot 16). The existing decompose
stub at 05-bottleneck.astro was unaffected; the audit registry's
pitch/enhanced-v3/05 key continues to map cleanly.
Shell version bump
apps/deck-shell/package.json moved from 0.0.1 → 0.1.0-rc.0.
Signals "Phase A+ included; pre-release until A.7 publishes."
The Story
What the founder said within minutes of opening the dev build
"Nice, now I see it the floating rank pill."
The first floating-overlay in-place ranking UI in any Lossless deck. Calmstorm doesn't have it. Chroma didn't have it. Phase A+ shipped it as a single shell-exported component the consumer drops into a scroll-deck variant once.
"TOC is there, needs iteration."
Captured as open question — founder will name specifics in the next session. Queued in the Phase A++ plan.
"Play works, doesn't have an elegant UI like calmdecks, needs more fidelity to that UI we worked on it hard."
The Phase A+ /play runtime is intentionally minimal — a thin top bar +
bottom strip wrapping the per-slide content. Calmstorm-decks's mature
/play UI has PageAsDeckWrapper, SlideCanvas, ContentFit,
tier-aware MetaTags, brand-mark slot patterns. Phase A+ shipped
none of them. Closing that gap is the headline of
[[../context-v/plans/Phase-A-Plus-Plus-Play-Fidelity-In-Play-Ranking-and-Variant-URL-Safety]]
— Phase A++ lifts SlideCanvas + ContentFit from calmstorm into the
shell as the seed of the larger Phase B primitive lift.
"Play doesn't seem to have the enum ranking UI to prioritize improvements."
Right — A+ shipped the pill in /scroll/ only. A++ adds a mode="play"
prop to <SlideRankPill> and mounts it inside PlayChrome, because
the founder reads-and-judges slides at presentation fidelity in /play/
too. That's where "this one needs work" judgments form.
404 at
/play/pitch/proto/01/.
The variant-index redirect in A+ is unconditional — it 302s to
{...}/01/ even for variants whose SLOTS[] entry is empty. Proto,
enhanced-v1, and enhanced-v2 have no slot entries (only enhanced-v3
does). A++.1 gates the redirect; renders a friendly "no slots yet"
page for empty variants.
The smoke-test loop is exactly how the framework is supposed to work: ship the smallest thing that proves the architecture, let the founder name what's missing, write the next plan.
The Astro slot-child quirk that cost an extra hour
Phase A+ surfaced a non-obvious Astro 6 behavior worth carrying
forward. The pattern below — passing a cross-package Astro component
as a direct slot child — rendered an empty <main> element in dev:
<PlayChrome ...>
{Slide ? (
<Slide />
) : (
<DecomposeFirstPlaceholder ... />
)}
</PlayChrome>Plain <div> children rendered fine. Plain Astro components
imported from within the same package rendered fine. Only components
imported via the pnpm workspace symlink (@dididecks/shell/components/...),
passed as direct slot children, silently failed.
The workaround that ships: wrap the slot's contents in a <div>.
<PlayChrome ...>
<div class="dididecks-play__slide">
{Slide ? (
<Slide />
) : (
<DecomposeFirstPlaceholder ... />
)}
</div>
</PlayChrome>Likely a Vite/Astro symlink-resolution edge case that resolves
once the shell publishes as a real package (Phase A.7) and consumers
stop relying on the workspace symlink. Until then, the <div> wrapper
is the defensive pattern for any cross-package slot child.
The intentional plan deviation — ask at slot 15, not 16
The Phase A+ plan called for the ask redesign at slot 16. The chroma scroll-deck's actual section ordering placed ask at section 15 and COLOPHON at section 16. Reconciled the registry to the page reality rather than restructuring the scroll-deck content; the redesign landed at slot 15. The plan's intent — that the ask is the redesigned slot — is preserved; the slot number is just one off.
This is the kind of small reconciliation the rank → decompose →
recreate loop is supposed to absorb gracefully. The audit registry
key for the existing decompose stub (pitch/enhanced-v3/05 →
05-bottleneck.astro) was unaffected, so no ranking state was lost.
How It Works (or "Under the Hood")
Glob-based per-slide rendering in /play
The [slot].astro route uses Vite's import.meta.glob to resolve
per-slide components at the consumer's project root:
// apps/deck-shell/src/routes/play/[deckSlug]/[variantSlug]/[slot].astro
const slideModules = import.meta.glob("/src/components/slides/**/*.astro");
// At render time, look up the slot's file via composite key:
const globKey = `/src/components/slides/${variantSlug}/${slot}-${slug}.astro`;
const slideLoader = slideModules[globKey];
const Slide = slideLoader ? (await slideLoader()).default : null;import.meta.glob requires a static literal pattern, so the pattern
is hardcoded to /src/components/slides/** for now. Future versions
could support an arbitrary slidesComponentsRoot if needed (Phase B).
The leading / resolves to the consumer's project root because Astro
processes injected routes as part of the consuming project's build —
so chroma's src/components/slides/... is what gets matched, not the
shell's own files.
The slot-page fallback renders <DecomposeFirstPlaceholder> if the
loader is undefined. The build never fails on an un-decomposed slot.
IntersectionObserver-based active-slot tracking
The scroll-deck <SlideRankPill> watches every
section[data-slot][data-variant="..."] element and tracks which one
is most centered in viewport:
const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) visibility.set(entry.target, entry.intersectionRatio);
else visibility.delete(entry.target);
}
// pick highest ratio
let bestSection = null;
let bestRatio = 0;
for (const [sec, ratio] of visibility) {
if (ratio > bestRatio) { bestRatio = ratio; bestSection = sec; }
}
if (bestSection) {
activeSlot = bestSection.getAttribute("data-slot");
renderActive();
}
},
{ threshold: [0, 0.25, 0.5, 0.75, 1] },
);Multiple thresholds give the pill room to track the most-visible
section rather than just the first one to cross 50%. Works smoothly
on chroma's scroll-snap-y page.
Server-side title hydration
The pill needs to show the active slot's title, but it's a client-side
script. Rather than adding another API endpoint, the component reads
the slots registry at render time on the server (via the same
__dididecksShellOptions global the TOC uses), embeds the titles as a
JSON blob in a data-titles attribute, and the inline script reads
the blob on mount:
const titleBySlot: Record<string, string> = Object.fromEntries(
slots.map((s) => [s.slot, s.title]),
);
---
<div data-slide-rank-pill data-titles={JSON.stringify(titleBySlot)}>
...
</div>The pattern generalizes: any future shell component that needs slot-aware metadata can hydrate the same way without a round-trip.
What's Next
The next plan is [[../context-v/plans/Phase-A-Plus-Plus-Play-Fidelity-In-Play-Ranking-and-Variant-URL-Safety]] — closes the founder's three smoke-test notes:
Variant URL safety (A++.1) — gate the
/play/{deck}/{variant}/redirect on the variant having slots; render a friendly empty-state page when it doesn't.In-play ranking (A++.2) — add a
mode="play"prop to<SlideRankPill>and mount it insidePlayChrome.Play visual fidelity (A++.3, A++.4) — lift
SlideCanvas+ContentFitfrom calmstorm into the shell; refactorPlayChrometo wrap its slot in the canvas. This is the seed of the larger Phase B calmstorm-primitive lift; A++ takes the slice the founder named, defers the rest.
Phase A.7 (publish) is still blocked on the org-name decision — the same one named in [[./2026-05-12_02]]. A++ doesn't unblock A.7; the user does, by picking a GitHub Packages strategy.
Phase B (full calmstorm primitive lift + calmstorm-onto-shell migration) is the larger followup. Phase A++ ships two of its components early because the founder asked for them.
Phase C polishes the iteration affordances. Phase D pulls auth + telemetry. Phase E carries the export pipeline. Each is a separate plan; none are blocking the next.
Related
[[../context-v/plans/Phase-A-Plus-In-Deck-Ranking-Shared-Nav-and-Play-Runtime]] — the plan this entry executes. All deliverables ✅.
[[../context-v/plans/Phase-A-Plus-Plus-Play-Fidelity-In-Play-Ranking-and-Variant-URL-Safety]] — the next plan, drafted same-day from the founder smoke-test feedback.
[[../context-v/plans/Stand-Up-Dididecks-Shell-and-Ship-Chroma-TOC-Ranking]] — Phase A. A.1–A.6 complete; A.7 still blocked.
[[../context-v/explorations/Chroma-Parity-and-the-Path-to-a-Shared-Deck-UI-Module]] — the architecture exploration. Phase A+ + A++ pull forward small slices of what would otherwise be Phase B.
[[../context-v/explorations/Plans-Inventory-and-Phase-A-Outcome]] — the gap-analysis exploration that framed Phase A+.
[[./2026-05-12_01.md]] — the four cross-site patterns ready to graduate out of calmstorm. SlideCanvas + ContentFit are next.
[[./2026-05-12_02.md]] — Phase A's ship note.
The
deck-iteration-workflowskill — the rhythm Phase A+ fully operationalizes (rank → decompose → recreate → present).The
pseudomonoreposskill —apps/continues to be the right home for this kind of parent-level capability.
