A reusable engine for node-canvas presentations. A presentation is a graph of nodes on a 3D-tilted canvas (future-dark styling, pointer parallax, zoom-in drill-down); each node opens a pane: a scrolling HTML document, a zoomable sub-graph with side-drawer documents, or a custom React component.
Content lives in one JSON file per deck. The engine owns layout, styling, 3D, and navigation — so presentation improvements and content authoring never collide.
Every node on the home canvas opens one of three pane types; canvas panes add a drawer for per-option documents.
There's also an escape hatch: a pane can mount a custom React component
(steppers, configurators, live figures) registered from src/custom/.
npm install
npm run dev # then open http://localhost:5173/?deck=<name>Every deck in src/decks/ is bundled automatically; ?deck=<name> switches at
runtime (default demo). Navigation: click nodes · ← → step through sections ·
Esc back out · ◐ theme toggles future-dark / light.
./ship <deck> # → dist/<deck>.html, fully self-contained single fileThe shipped file inlines everything — JS, CSS, all decks — so you can host it
anywhere a static HTML file works (or just send it). <deck> is only the
default; ?deck= still switches at runtime.
Decks are JSON files in src/decks/ — every file there is bundled automatically,
no registration step. Custom pane components live beside them in src/custom/.
For decks you don't want in this repo (personal or confidential content), point
DECKS_DIR at a directory outside it — via the environment or a gitignored
.env.local — and its decks/*.json and custom/*.tsx are bundled exactly like
src/decks/ and src/custom/. Your content can live in its own private repo
while the engine stays public.
# .env.local
DECKS_DIR=~/presentations # containing decks/ and custom/External custom components import engine types via the @engine alias
(import type { SectionComponentProps } from '@engine/deck') and can import
their own CSS files. Edit mode saves back to external deck files too.
Click ✎ edit in the top bar (dev server only) to tweak deck text in place:
every text field — thesis, node titles/subs, section headings, pane HTML, drawer
articles — becomes contentEditable. Changes save back to the deck's JSON file
on blur (Esc blurs; Enter too in single-line fields), as a surgical edit that
preserves the file's formatting. Nodes stop navigating while editing; toggle edit
off to move around. Two caveats: fields absent from the JSON aren't editable
(add the field in the file first), and the browser normalizes an edited field's
HTML serialization (e.g. class='x' → class="x"), so a field's first edit
reformats that one JSON value.
Copy src/decks/demo.json to src/decks/<name>.json and replace the content —
no code changes. The demo deck is self-documenting — run it and read the panes.
Schema lives in src/deck.ts (typed, commented). There's also a Claude Code
skill: .claude/skills/flow-presentation/ (works in-repo; symlink it into
~/.claude/skills/ to use it from anywhere).
Deck
├─ meta title / wordmark
├─ tokens? CSS var overrides, e.g. { "--brand": "#00b5ad" }
├─ accents? named accent colors nodes reference, e.g. { "trigger": "var(--accent)" }
├─ thesis the headline overlaid on the home canvas (HTML allowed)
├─ canvas nodes [{id,x,y,accent,kind,title,sub,step,big,decision}]
│ edges [[from,to]] · labels [{x,y,text}]
├─ sections[] ordered; one per canvas node id
│ doc: { id, num, eyebrow, title, lede, html?, component? }
│ canvas: { id, type:"canvas", nodes:[{x,y,title,…, article|goto, root?}] }
│ sequence: { id, type:"sequence", steps:[{kind,title,sub,detail[],accent, article|goto}] }
│ or rows:[{kind,title,lede,accent, steps:[…]}] — stacked chevron rows
└─ articles[] drawer documents { id, kind, title, html }
Notes:
- Pane bodies are raw HTML strings (single-quoted attributes keep JSON escaping sane).
All content primitives are available:
.card(+brand/alt),.callout,.tag,table.data,.bars,pre.code(+.k .s .c .fspans),.grid .cols-2/.cols-3,.pending,.configurator/.readout/.chips. - Cross-link any pane/drawer with
<a data-goto='section-id'>. - Ports and edges are automatic — home-canvas ports derive from
edges; sub-canvas edges are drawn root → options. - Sequence panes render a flat, left-to-right chain of interlocking arrow segments
(deliberately not the 3D node-graph look) — for linear roadmaps / ordered work.
Each step lists
detail[]work items beneath it and can open anarticledrawer orgotoanother section. Per-stepaccentkeys into the deck's accent map. See the demo deck'sroadmapsection andSequenceSectioninsrc/deck.ts. - Custom panes:
{"component": "name"}in a section mounts a React component from the registry. A module insrc/custom/(or$DECKS_DIR/custom/) registers by exporting acomponentsname → component map (seesrc/custom/DemoStepper.tsx). This is where bespoke interactive widgets (configurators, steppers, animated figures) live.
The engine/content split only pays off if it's maintained: layout, styling, and
interaction improvements belong in src/ (engine) and should be committed back to
this repo so every deck inherits them — don't fork engine behavior into a deck.
Content-specific interactivity belongs in a registered custom component
(src/custom/ or your external custom/), not in the engine.
Deck HTML is rendered unsanitized (dangerouslySetInnerHTML) by design — a deck file
is first-party authored content, the same trust level as writing the HTML page by hand.
Don't render deck JSON from untrusted sources; if that ever becomes a need, add
DOMPurify at the three render sites (DetailView, Drawer, NodeButton).
src/
├─ deck.ts deck schema (types + accent resolver)
├─ decks.ts bundles src/decks/*.json (+ $DECKS_DIR/decks/) + HMR deck swaps
├─ edit.tsx edit mode: Editable fields + save-to-deck-file context (dev only)
├─ App.tsx navigation state: section, drawer, theme, keyboard, hash links
├─ components/
│ ├─ HomeCanvas.tsx 3D plane, intro flip, parallax, edges, zoom-into transition
│ ├─ SubCanvasSection.tsx full-stage sub-graph, drawer focus glide, fit-to-viewport
│ ├─ SequenceSection.tsx flat left→right arrow-segment roadmap
│ ├─ DetailView.tsx doc panes + data-goto link handling
│ ├─ Drawer.tsx side panel for option documents
│ ├─ NodeButton.tsx the node card (home + sub-canvas variants via CSS)
│ └─ TopBar.tsx
├─ styles/
│ ├─ tokens.css future-dark + light tokens (--brand / --alt / --accent …)
│ ├─ app.css shell: canvas, nodes, drawer, detail, pager
│ └─ primitives.css content classes available to pane HTML
├─ custom/DemoStepper.tsx example registered component
└─ decks/demo.json self-documenting demo deck




