@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.
- package/LICENSE +373 -0
- package/README.md +74 -0
- package/package.json +60 -0
- package/skill/AGENTS.md +18 -0
- package/skill/SKILL.md +144 -0
- package/skill/references/authoring.md +81 -0
- package/skill/references/build-plugins.md +116 -0
- package/skill/references/components.md +107 -0
- package/skill/references/editing.md +78 -0
- package/skill/references/plugins.md +121 -0
- package/skill/references/troubleshooting.md +39 -0
- package/src/add.ts +369 -0
- package/src/build.ts +354 -0
- package/src/cli.ts +78 -0
- package/src/cloud.ts +570 -0
- package/src/creds.ts +31 -0
- package/src/new.ts +310 -0
- package/src/registry.ts +124 -0
- package/src/skill.ts +167 -0
- package/src/targeting.ts +8 -0
- package/src/trust.ts +73 -0
- package/src/update.ts +148 -0
|
@@ -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
|
+
```
|