@liebstoeckel/cli 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,81 @@
1
+ # Slide authoring conventions
2
+
3
+ A deck is a project (under `presentations/<name>/`) with:
4
+
5
+ - `index.html` — the page shell (a `<div id="root">` + the entry script). Rarely edited.
6
+ - `main.tsx` — the **entry**: renders `<Present>` with the ordered slide list.
7
+ - `slides/NN-name.tsx` (or `.mdx`) — one file per slide.
8
+ - `charts/` — scaffolded components (owned source from `liebstoeckel add`). Most
9
+ registry items are charts and land here, but `add` writes each item to its manifest's
10
+ `target` (see `references/components.md`), so other component types may land elsewhere.
11
+ - `package.json`, `build.ts`, `server.ts`, `bunfig.toml`.
12
+
13
+ ## A slide
14
+
15
+ A slide is a module with a **default-exported React component**, optionally a
16
+ **`notes`** export (presenter notes). The component renders onto a fixed
17
+ **1280×720** canvas that is scaled to fit (author at that size; don't set your own
18
+ viewport units for layout).
19
+
20
+ ```tsx
21
+ // slides/02-growth.tsx
22
+ import { BarChart } from "../charts/BarChart";
23
+
24
+ export const notes = (
25
+ <div><p>Revenue tripled; call out Q4.</p></div>
26
+ );
27
+
28
+ export default function Growth() {
29
+ return (
30
+ <div className="flex h-full w-full flex-col justify-center px-24">
31
+ <h2 className="font-heading text-5xl text-text">Revenue grew 3×</h2>
32
+ <BarChart data={[{ label: "Q1", value: 128 }, { label: "Q4", value: 384 }]} />
33
+ </div>
34
+ );
35
+ }
36
+ ```
37
+
38
+ Use Tailwind classes and the brand tokens (`text-text`, `bg-surface`, `text-accent`,
39
+ `border-border`, `font-heading`, `font-mono`). Charts already read the brand palette.
40
+
41
+ ## Wiring a slide into the deck (required)
42
+
43
+ A new slide file does nothing until it's listed in `main.tsx`. Add an import and put
44
+ it in the `slides` array **in display order**. To include a slide's `notes`, import it
45
+ as a namespace; for component-only, a default import is fine:
46
+
47
+ ```tsx
48
+ // main.tsx
49
+ import * as intro from "./slides/01-intro";
50
+ import * as growth from "./slides/02-growth"; // namespace → carries `notes`
51
+
52
+ createRoot(document.getElementById("root")!).render(
53
+ <StrictMode>
54
+ <Present title="Q4 Review" brands={["nocturne"]} slides={[intro, growth]} />
55
+ </StrictMode>,
56
+ );
57
+ ```
58
+
59
+ ## Steps (progressive reveal)
60
+
61
+ Wrap content in `<Step>` (from `@liebstoeckel/engine`) to reveal it across key
62
+ presses:
63
+
64
+ ```tsx
65
+ import { Step } from "@liebstoeckel/engine";
66
+ // …
67
+ <Step>▹ First point</Step>
68
+ <Step>▹ Second point</Step>
69
+ ```
70
+
71
+ ## Brands
72
+
73
+ `<Present brands={["nocturne"]} />` selects the active brand; `liebstoeckel new
74
+ --brand <brand>` sets the default. Re-theming a deck = change the brand; every
75
+ component and token follows. Don't hard-code hex colors — use tokens / `useBrandColors`.
76
+
77
+ ## MDX slides
78
+
79
+ A slide can be `.mdx` instead of `.tsx` for prose-heavy content; it still
80
+ default-exports its content and may `export const notes`. Import components at the top
81
+ of the MDX as usual.
@@ -0,0 +1,116 @@
1
+ # Building a custom plugin
2
+
3
+ Reach for this only when none of the built-ins (`poll`, `qa`, `reactions` — see
4
+ `references/plugins.md`) fits. A custom plugin is a `definePlugin({ … })` value placed
5
+ and registered exactly like a built-in; the three built-ins are good templates to copy.
6
+
7
+ ## Anatomy
8
+
9
+ ```tsx
10
+ import { definePlugin, schema, t, type Infer, type ClientProps } from "@liebstoeckel/plugin-sdk";
11
+
12
+ const mySchema = schema({
13
+ votes: t.record(t.string), // participantId -> choice
14
+ closed: t.boolean,
15
+ });
16
+ type MyState = Infer<typeof mySchema>;
17
+
18
+ export default definePlugin<MyState>({
19
+ id: "myplugin", // what authors write as <Plugin id="myplugin" />
20
+ state: mySchema, // the typed, synced CRDT state
21
+ client: {
22
+ Slide: MySlide, // interactive UI (live)
23
+ fallback: MyFallback, // static preview (offline + thumbnails)
24
+ },
25
+ });
26
+ ```
27
+
28
+ `definePlugin<T>({ id, state, server?, client })` — `id` names the plugin (and
29
+ namespaces its state at `plugin:<id>`); the rest are below.
30
+
31
+ ## State schema (`state`)
32
+
33
+ Build it with `schema({...})` and the `t` primitives:
34
+
35
+ - `t.string`, `t.number`, `t.boolean`
36
+ - `t.array(item)`
37
+ - `t.object({ … })`
38
+ - `t.record(value)` — a string-keyed map
39
+
40
+ **Concurrency rule:** anything many participants write at once must be a **top-level
41
+ `t.record`**, keyed by a composite string — there is no nested-record setter. For
42
+ example, store votes as a top-level `votes: t.record(t.string)` keyed by `participantId`
43
+ (or `"${questionId}|${participantId}"`), not as a nested object.
44
+
45
+ ### Reading & writing state
46
+
47
+ The client and server receive a typed `state` accessor:
48
+
49
+ ```ts
50
+ state.snapshot(); // current value as plain JS (defaults filled in)
51
+ state.set("closed", true); // replace a whole top-level field
52
+ state.recordSet("votes", pid, "Yes"); // set one entry of a top-level record field
53
+ state.recordDelete("votes", pid); // remove one entry
54
+ state.ensureDefaults({ … }); // seed defaults once, only if state is empty
55
+ const off = state.subscribe((snap) => …); // observe deep changes; returns unsubscribe
56
+ ```
57
+
58
+ ## Client surfaces (`client`)
59
+
60
+ `ClientProps<T>` is passed to every client component:
61
+ `{ doc, state, snapshot, role, live, participantId, theme, ui, props, instance }`
62
+ (`role` is `"presenter" | "viewer"`; `props` is the author's `<Plugin props={…}>`).
63
+
64
+ | field | renders when / what |
65
+ |----------------|--------------------------------------------------------------------------|
66
+ | `Slide` | live: the interactive in-deck UI (audience + presenter) |
67
+ | `fallback?` | offline `.html` + thumbnails — show real content from `snapshot`/`props` |
68
+ | `presenter?` | a tab in the presenter view: `{ label, icon?, badge?, title?, Console }` for live readout + privileged moderation |
69
+ | `global?` | deck-wide surfaces: an `Overlay`, plus a chrome control (`icon`+`label`) toggling a `Panel`. `pinned` keeps it in the mobile rail; `panelMode: "sheet"` opens full-viewport on touch |
70
+ | `surfaces?` | named override points an author may replace per placement (`<Plugin components={{ … }}>`) |
71
+ | `interactive?` | `false` to suppress the mobile tap-to-interact breakout (display-only) |
72
+
73
+ ### Canvas constraint
74
+
75
+ A slide renders on a fixed **1280×720 canvas** that is scaled to fit and **clipped**
76
+ (`overflow: hidden`) — it never scrolls, so the audience, presenter, and thumbnail views
77
+ stay pixel-identical. **Pin your header/input and scroll the growing part yourself**
78
+ using `ScrollArea` from `@liebstoeckel/plugin-ui`.
79
+
80
+ ## UI kit — `@liebstoeckel/plugin-ui`
81
+
82
+ Brand-token-aware primitives so plugin UI matches the active theme automatically:
83
+
84
+ `Card`, `Button`, `Bar` (result bar), `Stack`, `ScrollArea`, `Eyebrow`, `ChromeButton`,
85
+ plus `useTheme()` / `readTheme()` for the resolved tokens. Prefer these over raw markup.
86
+
87
+ ## Optional server part (`server`)
88
+
89
+ Only if the plugin needs host-side logic the audience's browsers can't do (compute an
90
+ official result, reach a secret/external HTTP). It runs **once**, on the machine running
91
+ `liebstoeckel live` — never in audience browsers, never on the relay.
92
+
93
+ ```ts
94
+ import type { PluginServerCtx } from "@liebstoeckel/plugin-sdk";
95
+ import { pluginState } from "@liebstoeckel/plugin-sdk";
96
+
97
+ export default function server(ctx: PluginServerCtx<MyState>) {
98
+ const state = pluginState(ctx.doc, "myplugin", mySchema);
99
+ const stop = state.subscribe((snap) => {
100
+ // host-only work; write results back to shared state
101
+ });
102
+ return () => stop(); // optional teardown
103
+ }
104
+ ```
105
+
106
+ `PluginServerCtx<T>` = `{ doc, state, session: { id }, instance }`. At build time the
107
+ server entry is bundled and embedded in the deck; a live server decodes and runs it with
108
+ an injected `ctx`. Because it executes on the presenter's machine, decks are trust-gated
109
+ (`references/plugins.md`).
110
+
111
+ ## After authoring
112
+
113
+ Register and place it like any built-in (see `references/plugins.md`), then run the
114
+ `liebstoeckel build --check` loop until clean.
115
+
116
+ Full guide with worked examples: https://docs.liebstoeckel.app/llms.txt
@@ -0,0 +1,107 @@
1
+ # Components: discover, scaffold, wire data
2
+
3
+ Components come from the registry as **owned source** — `liebstoeckel add` copies the
4
+ file into the deck and installs whatever npm packages it needs. After that the file is
5
+ yours to edit.
6
+
7
+ ## Component types
8
+
9
+ The registry is not just charts. Each item declares a `type` (one of the categories
10
+ below), and `add` writes it to wherever that item's manifest points — the **target path
11
+ is the manifest author's choice, not a fixed `charts/` rule**.
12
+
13
+ | category | `type` | what it is |
14
+ |------------|---------------------|---------------------------------------------------|
15
+ | `chart` | `registry:chart` | a data-viz component (visx-based) |
16
+ | `element` | `registry:element` | a smaller building block (axis, legend, …) |
17
+ | `hook` | `registry:hook` | a reusable hook (e.g. brand palette access) |
18
+ | `component`| `registry:component`| a general UI component |
19
+ | `layout` | `registry:layout` | a slide-layout component |
20
+ | `motion` | `registry:motion` | an animation helper |
21
+ | `brand` | `registry:brand` | brand/theme tokens |
22
+
23
+ Today the catalog is **chart-heavy** — most items are charts, alongside a couple of
24
+ chart `element`s (`brand-axis`, `legend`) and the `use-brand-colors` `hook` — and those
25
+ all land under the deck's `charts/`. But more types can be added, and they land wherever
26
+ their manifest's `target` says. **Don't assume the category from the folder; read the
27
+ `type` from the registry.**
28
+
29
+ `liebstoeckel add` also resolves `registryDependencies`, so adding a chart pulls in the
30
+ `element`s and `hook`s it depends on automatically — you rarely add those by hand.
31
+
32
+ ## 1. Discover (always, before using a component)
33
+
34
+ ```bash
35
+ liebstoeckel registry list --json # whole catalog: name, type, dataShape
36
+ liebstoeckel registry view <name> --json # one item: exports, props, dataShape, example
37
+ ```
38
+
39
+ `registry view` returns the authoritative contract:
40
+
41
+ ```jsonc
42
+ {
43
+ "name": "bar-chart",
44
+ "type": "registry:chart",
45
+ "meta": {
46
+ "exports": "BarChart",
47
+ "props": "{ data?: BarChartDatum[]; width?: number; height?: number }",
48
+ "dataShape": "{ label: string; value: number }[]",
49
+ "example": "<BarChart data={[{ label: 'Q1', value: 128 }]} />"
50
+ }
51
+ }
52
+ ```
53
+
54
+ Use `exports` for the import name (note some differ from the id — e.g. the `treemap`
55
+ item exports **`TreemapChart`**), and shape your data to `dataShape` exactly. A `hook`
56
+ or `element` has the same `meta` contract — read `exports`/`props`/`example` the same
57
+ way; only `chart`s carry a `dataShape`.
58
+
59
+ ## 2. Scaffold
60
+
61
+ ```bash
62
+ liebstoeckel add <name> --dir ./presentations/<deck>
63
+ liebstoeckel add <category> <name> --dir … # optional sugar: `add chart bar-chart`
64
+ # JSON when piped: { action, wrote[], skipped[], dependencies[], installed }
65
+ ```
66
+
67
+ `add` is idempotent — existing files are reported as `skipped` (pass `--force` to
68
+ overwrite something you've since edited). It also pulls in any `registryDependencies`
69
+ (e.g. every chart pulls `use-brand-colors`). Preview without writing: `add <name>
70
+ --dry --json` — the `wrote[]`/plan shows the exact target paths, so you never have to
71
+ guess where a component landed.
72
+
73
+ ## 3. Use it in a slide
74
+
75
+ Import from wherever `add` wrote it (the `wrote[]` paths) and pass data matching
76
+ `dataShape`:
77
+
78
+ ```tsx
79
+ import { BarChart } from "../charts/BarChart";
80
+ // …
81
+ <BarChart data={[{ label: "Q1", value: 128 }, { label: "Q2", value: 174 }]} />
82
+ ```
83
+
84
+ Notes (chart specifics):
85
+
86
+ - Charts size to a `width`/`height` prop (defaults provided); wrap in a sized
87
+ container or pass explicit numbers for grids of charts.
88
+ - `sparkline` requires a unique `id` prop.
89
+ - `stacked-bar-chart` / `grouped-bar-chart` take a `keys: string[]` listing the series.
90
+ - All charts read the active brand palette — no color props needed.
91
+
92
+ ## 4. Adjust the component to fit the slide (encouraged)
93
+
94
+ A scaffolded file is **the deck's own source**, not a fixed package API. Wiring data
95
+ through props is the common case, but when the slide needs more, **open the file and
96
+ change it** — that's why it was copied in, not imported. Common adjustments (charts):
97
+
98
+ - **Palette** — recolor a series, or map a category to a brand color (`useBrandColors`
99
+ gives `c.viz[]`, `c.accent`, …).
100
+ - **Shape** — add/remove a series, change the axis range, add a reference line,
101
+ threshold band, or label/annotation.
102
+ - **Chrome** — drop the legend, change the value-label format, restyle the axis.
103
+ - **Motion** — retime or remove the entrance animation.
104
+
105
+ Re-running `liebstoeckel add <name>` will **not** overwrite your edits unless you pass
106
+ `--force` (which restores the pristine registry version). So edit freely — your copy is
107
+ yours. Prefer adjusting the component over contorting the data or fighting the props.
@@ -0,0 +1,78 @@
1
+ # Editing an existing deck
2
+
3
+ Find the deck (a dir with `main.tsx` + `slides/`). The slide order is the `slides`
4
+ array in `main.tsx`.
5
+
6
+ ## Trust: building runs the deck's code
7
+
8
+ A deck is **code**. Building it (`build`, `build --check`, the `eject` rebuild,
9
+ `licenses` from source) executes the deck's build-time modules on this machine with
10
+ full filesystem and network access. A deck scaffolded here via `liebstoeckel new` is
11
+ trusted automatically; a deck you did **not** create here (cloned, downloaded, handed
12
+ over) is **untrusted**, and a non-interactive build of it fails fast:
13
+
14
+ ```json
15
+ { "ok": false, "error": "untrusted deck", "hint": "…re-run with --trust…" }
16
+ ```
17
+
18
+ **You MUST NOT silence this yourself.** Never pass `--trust` or set
19
+ `LIEBSTOECKEL_TRUST_BUILD=1` on your own initiative — that is a security decision the
20
+ user owns, not you. When you hit this error, **stop and ask the user** whether they
21
+ trust this specific deck, and only re-run with `--trust` after they explicitly say yes.
22
+ If they don't confirm, do not build it.
23
+
24
+ ## Add a slide
25
+
26
+ 1. Create `slides/NN-name.tsx` (pick `NN` for its position) with a default-exported
27
+ component (see `authoring.md`).
28
+ 2. Import it in `main.tsx` and insert it into the `slides` array at the right spot.
29
+ 3. `liebstoeckel build --check` until clean.
30
+
31
+ ## Replace / rewrite a slide
32
+
33
+ Edit the slide file in place. If you change what data a chart needs, keep the data
34
+ matching the chart's `dataShape` (`registry view <name> --json`).
35
+
36
+ ## Add a chart to an existing slide
37
+
38
+ ```bash
39
+ liebstoeckel registry view <name> --json
40
+ liebstoeckel add <name> --dir <deck>
41
+ ```
42
+ Then import it from `../charts/` and pass data. If `add` reports the file as
43
+ `skipped`, the chart is already in the deck — just import and use it.
44
+
45
+ ## Remove a slide
46
+
47
+ Delete the file and remove its import + array entry from `main.tsx`.
48
+
49
+ ## Re-theme
50
+
51
+ Change the brand in `main.tsx`'s `<Present brands={["…"]}>` (and/or run with a
52
+ different brand). Every chart and token follows; don't hand-edit colors.
53
+
54
+ ## Recover source from a built `.html` (eject)
55
+
56
+ `eject` reverses a `build`: it unpacks the editable project (the `main.tsx`, `slides/`,
57
+ `charts/`, assets) that was inlined into a single `.html`, so you can edit a deck you
58
+ only have as a built file. The result is a normal deck directory — edit it with the
59
+ workflows above, then rebuild.
60
+
61
+ ```bash
62
+ liebstoeckel eject <deck.html> [outdir] # default outdir: <deck>-source; --force to overwrite
63
+ cd <outdir>
64
+ bun install --ignore-scripts # installs deps WITHOUT running npm lifecycle scripts
65
+ liebstoeckel build # rebuild to a single .html
66
+ ```
67
+
68
+ `--ignore-scripts` only blocks npm lifecycle scripts — it does **not** sandbox the deck:
69
+ `build` still runs the deck's own macros and build plugins. An ejected deck is **not**
70
+ trusted (only `liebstoeckel new` auto-trusts), so this rebuild hits the trust gate. Per
71
+ the trust rule above: if it fails with `untrusted deck`, **ask the user** — never add
72
+ `--trust` yourself.
73
+
74
+ ## Always finish with the check loop
75
+
76
+ ```bash
77
+ liebstoeckel build --check --dir <deck> # fix diagnostics, repeat until ok:true
78
+ ```
@@ -0,0 +1,121 @@
1
+ # Adding live plugins to a deck
2
+
3
+ Plugins make a deck **interactive when presented live**: poll voting, an audience
4
+ Q&A queue, floating reactions. The same deck still builds to one self-contained
5
+ `.html` — offline it renders a static **fallback** preview of each plugin; in a live
6
+ session the real, synced UI takes over.
7
+
8
+ **Plugins are not in the registry.** Don't run `registry list/view` for them — that
9
+ catalog is charts/elements/hooks only. The built-ins and their props are listed here
10
+ and in `SKILL.md`; that is the contract.
11
+
12
+ ## The built-ins
13
+
14
+ | id | package | props |
15
+ |-------------|----------------------------------|----------------------------------------|
16
+ | `poll` | `@liebstoeckel/plugin-poll` | `{ question: string; options: string[] }` |
17
+ | `qa` | `@liebstoeckel/plugin-qa` | `{ prompt?: string }` (default `"Ask a question"`) |
18
+ | `reactions` | `@liebstoeckel/plugin-reactions` | _none_ |
19
+
20
+ ## Wiring is two steps (both required)
21
+
22
+ A `<Plugin>` tag alone does nothing. The plugin must also be a dependency **and**
23
+ registered on `<Present>`.
24
+
25
+ ### 1. Depend + register in `main.tsx`
26
+
27
+ Scaffolded decks ship **no** plugin packages, so add the ones you use:
28
+
29
+ ```bash
30
+ bun add @liebstoeckel/plugin-poll @liebstoeckel/plugin-qa @liebstoeckel/plugin-reactions
31
+ ```
32
+
33
+ Then import each (default export) and pass the set to `<Present plugins={…}>`:
34
+
35
+ ```tsx
36
+ // main.tsx
37
+ import { Present } from "@liebstoeckel/engine";
38
+ import poll from "@liebstoeckel/plugin-poll";
39
+ import qa from "@liebstoeckel/plugin-qa";
40
+ import reactions from "@liebstoeckel/plugin-reactions";
41
+
42
+ <Present
43
+ title="Q4 Review"
44
+ brands={["nocturne"]}
45
+ plugins={[poll, qa, reactions]} // ← register every plugin you place
46
+ slides={[…]}
47
+ />
48
+ ```
49
+
50
+ ### 2. Place `<Plugin>` on a slide
51
+
52
+ ```tsx
53
+ import { Plugin } from "@liebstoeckel/engine";
54
+
55
+ // poll
56
+ <Plugin id="poll" props={{ question: "What should we build next?", options: ["A", "B", "C"] }} />
57
+
58
+ // q&a
59
+ <Plugin id="qa" props={{ prompt: "What should we dig into?" }} />
60
+
61
+ // reactions (no props)
62
+ <Plugin id="reactions" />
63
+ ```
64
+
65
+ `<Plugin>` full signature:
66
+
67
+ ```tsx
68
+ <Plugin
69
+ id="poll" // required — matches the plugin's id
70
+ instance="pace" // optional — independent state slice (see below)
71
+ title="…" // optional — presenter-tab label for sibling instances
72
+ props={{ … }} // optional — author config passed to the plugin
73
+ components={{ … }} // optional — per-placement surface overrides
74
+ />
75
+ ```
76
+
77
+ ## Per-plugin notes
78
+
79
+ - **poll** — one shared poll per `id`. For **several independent polls**, give each a
80
+ stable `instance` (their votes/options stay separate, and each gets its own presenter
81
+ tab):
82
+ ```tsx
83
+ <Plugin id="poll" instance="pace" props={{ question: "How's the pace?", options: ["Slow", "Right", "Fast"] }} />
84
+ ```
85
+ - **qa** — also exposes a deck-wide 💬 panel, so the audience can ask from **any
86
+ slide**; a dedicated Q&A slide is optional. The presenter moderates (mark answered /
87
+ dismiss) from the presenter console.
88
+ - **reactions** — emoji float deck-wide over **every** slide once placed; ephemeral
89
+ (self-pruning, rate-limited). No configuration, no presenter tab.
90
+
91
+ ## Validate: registration is not build-checked
92
+
93
+ `liebstoeckel build --check` catches a missing **import** (`Could not resolve
94
+ "@liebstoeckel/plugin-poll"`), but it does **not** catch a `<Plugin id="poll">` that
95
+ was never added to `<Present plugins>` — that placement silently falls back to the
96
+ offline preview. So after the check passes, confirm by eye: **every `id` you placed
97
+ appears in the `plugins={[…]}` array.**
98
+
99
+ ## Present it live (how to see plugins activate)
100
+
101
+ Built or opened directly, a deck shows only fallbacks. Plugins go live with:
102
+
103
+ ```bash
104
+ liebstoeckel live <deck-dir|deck.html> # LAN: prints presenter + audience links
105
+ ```
106
+
107
+ Open the **presenter** link yourself; share the **audience** link (or its QR). Slide
108
+ navigation and all plugin state sync in real time. Present to a remote audience through
109
+ a public relay:
110
+
111
+ ```bash
112
+ liebstoeckel live <deck> --relay <https://relay-host> --relay-token <token>
113
+ ```
114
+
115
+ **Trust model:** a deck's server-side plugin code (if any) runs **on the presenter's
116
+ machine** — never on the relay. `liebstoeckel live` prints a trust warning for this
117
+ reason; only run decks you trust.
118
+
119
+ ## Building your own plugin
120
+
121
+ See `references/build-plugins.md` — only needed when no built-in fits.
@@ -0,0 +1,39 @@
1
+ # The check loop & common errors
2
+
3
+ `liebstoeckel build --check` is your correctness gate. It validates the deck
4
+ **bundles** (imports resolve, MDX/TSX transforms, visx interop holds) and returns
5
+ structured diagnostics. It does **not** type-check.
6
+
7
+ ```bash
8
+ liebstoeckel build --check --dir <deck>
9
+ # { "ok": true, "diagnostics": [] }
10
+ # or
11
+ # { "ok": false, "diagnostics": [ { "level": "error", "message": "...", "file": "...", "line": 12 } ] }
12
+ ```
13
+
14
+ Loop: run it, fix each `diagnostic` (use `file`/`line`/`message`), re-run until
15
+ `ok` is `true`. Only then `build` / `export`.
16
+
17
+ ## Common diagnostics
18
+
19
+ - **`Could not resolve "X"`** — a missing dependency. If `X` is a chart you used,
20
+ run `liebstoeckel add <chart> --dir <deck>`. If it's a node module, run
21
+ `bun install` in the deck. A relative path means a wrong import path to a slide or
22
+ `charts/` file.
23
+ - **`Could not resolve "../charts/Foo"`** — you imported a chart you haven't
24
+ scaffolded, or used the wrong export name. Check `registry view <name> --json`
25
+ (the `exports` field is the symbol; the file is `charts/<Exports>.tsx`).
26
+ - **Element type is invalid / undefined component** — usually a wrong import name
27
+ (default vs named) or a chart used before `add`. Confirm the export name from
28
+ `registry view`.
29
+ - **A blank or mis-laid-out slide** — author to the 1280×720 canvas; don't rely on
30
+ `vh`/`vw` for layout, and give charts a sized container or explicit `width`/`height`.
31
+
32
+ ## Verifying the visual result
33
+
34
+ `build --check` proves it compiles, not that it looks right. To eyeball it, build and
35
+ export a slide to PNG:
36
+
37
+ ```bash
38
+ liebstoeckel export <deck> --slides 1 -o ./out # PNG of slide 1 at 2560×1440
39
+ ```