stagecraft 0.1.0
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/AGENT.md +792 -0
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/bin/cli.js +51 -0
- package/bin/export.js +137 -0
- package/bin/init.js +52 -0
- package/bin/lib/edit-ops.js +405 -0
- package/bin/serve.js +278 -0
- package/dist/stagecraft.bundle.css +4443 -0
- package/dist/stagecraft.bundle.js +7621 -0
- package/dist/themes/brand.bundle.css +5262 -0
- package/dist/themes/neon.bundle.css +5289 -0
- package/dist/themes/paper.bundle.css +5276 -0
- package/dist/themes/phosphor.bundle.css +4443 -0
- package/dist/themes/shopware.bundle.css +5850 -0
- package/examples/closing-card.js +74 -0
- package/examples/orchestration-graph.js +156 -0
- package/examples/terminal-log.js +109 -0
- package/examples/token-stream.js +96 -0
- package/examples/whoami.js +90 -0
- package/package.json +41 -0
- package/src/components/activity-list.js +75 -0
- package/src/components/agenda.js +79 -0
- package/src/components/bar-chart.js +162 -0
- package/src/components/before-after.js +135 -0
- package/src/components/bento.js +73 -0
- package/src/components/big-number.js +87 -0
- package/src/components/callout.js +75 -0
- package/src/components/checklist.js +81 -0
- package/src/components/code-block.js +141 -0
- package/src/components/code-diff.js +98 -0
- package/src/components/compare.js +85 -0
- package/src/components/counter.js +80 -0
- package/src/components/cta.js +69 -0
- package/src/components/cycle.js +146 -0
- package/src/components/definition.js +96 -0
- package/src/components/donut-chart.js +179 -0
- package/src/components/full-image.js +82 -0
- package/src/components/funnel.js +111 -0
- package/src/components/gauge.js +147 -0
- package/src/components/heatmap.js +141 -0
- package/src/components/image-grid.js +80 -0
- package/src/components/image-text.js +96 -0
- package/src/components/kinetic-text.js +72 -0
- package/src/components/kpi.js +106 -0
- package/src/components/line-chart.js +215 -0
- package/src/components/manifesto.js +104 -0
- package/src/components/marquee.js +63 -0
- package/src/components/matrix2x2.js +151 -0
- package/src/components/pillars.js +80 -0
- package/src/components/pricing.js +90 -0
- package/src/components/process-flow.js +133 -0
- package/src/components/progress.js +136 -0
- package/src/components/punchline.js +82 -0
- package/src/components/pyramid.js +107 -0
- package/src/components/qanda.js +60 -0
- package/src/components/quote.js +70 -0
- package/src/components/roadmap.js +130 -0
- package/src/components/section-card.js +45 -0
- package/src/components/shift-arrow.js +41 -0
- package/src/components/spark-line.js +147 -0
- package/src/components/spotlight.js +85 -0
- package/src/components/statement.js +106 -0
- package/src/components/stats.js +91 -0
- package/src/components/steps.js +83 -0
- package/src/components/swot.js +110 -0
- package/src/components/team-grid.js +87 -0
- package/src/components/testimonial.js +99 -0
- package/src/components/timeline.js +91 -0
- package/src/components/tip.js +63 -0
- package/src/components/venn.js +198 -0
- package/src/edit-mode.js +1256 -0
- package/src/engine.js +823 -0
- package/src/helpers.js +169 -0
- package/src/transitions.js +101 -0
- package/starter/index.html +40 -0
- package/starter/slides/00-title.js +12 -0
- package/starter/stagecraft.config.js +8 -0
- package/themes/brand/base.css +4 -0
- package/themes/brand/components-business.css +173 -0
- package/themes/brand/components-chart.css +65 -0
- package/themes/brand/components-content.css +126 -0
- package/themes/brand/components-data.css +162 -0
- package/themes/brand/components-diagram.css +115 -0
- package/themes/brand/components-layout.css +112 -0
- package/themes/brand/components.css +46 -0
- package/themes/brand/manifest.json +20 -0
- package/themes/brand/tokens.css +20 -0
- package/themes/brand/transitions.css +4 -0
- package/themes/neon/base.css +10 -0
- package/themes/neon/components-business.css +189 -0
- package/themes/neon/components-chart.css +70 -0
- package/themes/neon/components-content.css +112 -0
- package/themes/neon/components-data.css +160 -0
- package/themes/neon/components-diagram.css +109 -0
- package/themes/neon/components-layout.css +87 -0
- package/themes/neon/components.css +87 -0
- package/themes/neon/manifest.json +21 -0
- package/themes/neon/tokens.css +17 -0
- package/themes/neon/transitions.css +13 -0
- package/themes/paper/base.css +9 -0
- package/themes/paper/components-business.css +196 -0
- package/themes/paper/components-chart.css +74 -0
- package/themes/paper/components-content.css +108 -0
- package/themes/paper/components-data.css +168 -0
- package/themes/paper/components-diagram.css +89 -0
- package/themes/paper/components-layout.css +105 -0
- package/themes/paper/components.css +60 -0
- package/themes/paper/manifest.json +10 -0
- package/themes/paper/tokens.css +21 -0
- package/themes/paper/transitions.css +11 -0
- package/themes/phosphor/base.css +511 -0
- package/themes/phosphor/components-business.css +818 -0
- package/themes/phosphor/components-chart.css +415 -0
- package/themes/phosphor/components-content.css +530 -0
- package/themes/phosphor/components-data.css +824 -0
- package/themes/phosphor/components-diagram.css +427 -0
- package/themes/phosphor/components-layout.css +450 -0
- package/themes/phosphor/components.css +223 -0
- package/themes/phosphor/manifest.json +11 -0
- package/themes/phosphor/tokens.css +17 -0
- package/themes/phosphor/transitions.css +213 -0
- package/themes/shopware/base.css +94 -0
- package/themes/shopware/components-business.css +344 -0
- package/themes/shopware/components-chart.css +121 -0
- package/themes/shopware/components-content.css +169 -0
- package/themes/shopware/components-data.css +219 -0
- package/themes/shopware/components-diagram.css +129 -0
- package/themes/shopware/components-layout.css +166 -0
- package/themes/shopware/components.css +83 -0
- package/themes/shopware/manifest.json +21 -0
- package/themes/shopware/tokens.css +68 -0
- package/themes/shopware/transitions.css +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Nögel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Stagecraft
|
|
2
|
+
|
|
3
|
+
**Cinematic, agent-authored presentations in a single HTML file.**
|
|
4
|
+
|
|
5
|
+
▶ **[Live demo](https://noegel.io/stagecraft/)** — the deck about Stagecraft, built with Stagecraft. Auto-deployed from `main` via GitHub Pages.
|
|
6
|
+
|
|
7
|
+
A small JavaScript library for building animated slide decks that live in a single HTML file. Designed so an LLM can generate a deck that doesn't look like an LLM generated it.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Generic slide libraries produce generic decks. Stagecraft is built around a different premise: **the agent is a creative director, not a form-filler**. The library ships a catalog of building blocks (50 components across 7 families) AND five bespoke example slides — `examples/token-stream.js`, `examples/orchestration-graph.js`, `examples/terminal-log.js` — that show what the bar looks like. `AGENT.md` is the manifesto.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
mkdir my-deck && cd my-deck
|
|
17
|
+
npm init -y
|
|
18
|
+
npm install stagecraft
|
|
19
|
+
npx stagecraft init
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This scaffolds:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
my-deck/
|
|
26
|
+
├── index.html # opens directly in a browser to present
|
|
27
|
+
├── stagecraft.config.js # the manifest: theme + slide order + transitions
|
|
28
|
+
├── slides/
|
|
29
|
+
│ └── 00-title.js
|
|
30
|
+
└── AGENT.md # the manifesto the agent reads
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Present mode (no setup, no infrastructure):
|
|
34
|
+
```bash
|
|
35
|
+
open index.html
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Two modes, one HTML file
|
|
39
|
+
|
|
40
|
+
### Presentation mode
|
|
41
|
+
|
|
42
|
+
Open `index.html` directly. Use arrow keys, space, or page-up/down to navigate. `F` for fullscreen, `S` for storyboard, `1-9` to jump to a section, `R` to replay the current slide, `P` to open the presenter view.
|
|
43
|
+
|
|
44
|
+
### Edit mode (dev server)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx stagecraft serve
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The browser detects the server and unlocks the editing affordances:
|
|
51
|
+
|
|
52
|
+
- **Drag slides** in the storyboard to reorder
|
|
53
|
+
- **Click transitions** between storyboard tiles to pick from 15 effects with hover preview
|
|
54
|
+
- **Click any text** on a Layer-2 component to inline-edit (writes back to source via AST roundtrip)
|
|
55
|
+
- **Shift+Click** any element to pin a `// @note[stage-key=...]:` comment for the agent
|
|
56
|
+
- **N** in present mode opens a slide-level note dialog
|
|
57
|
+
- **🎙 button** per tile in storyboard: speaker notes (shown in presenter view, hidden from audience)
|
|
58
|
+
- **× button** per tile: delete slide
|
|
59
|
+
- **+ tile** at the end of the grid: add a new slide from a template
|
|
60
|
+
- **Theme picker** at the top of the storyboard: live-switch between Phosphor/Paper/Neon/Brand/Shopware
|
|
61
|
+
- **📋 Process notes** copies a ready-made agent prompt to the clipboard
|
|
62
|
+
|
|
63
|
+
When you ask the agent to "process my notes":
|
|
64
|
+
1. `grep -rn '@note' slides/`
|
|
65
|
+
2. Read each note + its slide
|
|
66
|
+
3. Edit the slide
|
|
67
|
+
4. Delete the `@note:` lines
|
|
68
|
+
|
|
69
|
+
That's the whole loop.
|
|
70
|
+
|
|
71
|
+
## Presenter view (multi-monitor)
|
|
72
|
+
|
|
73
|
+
Press `P` during a presentation → a new window opens with the presenter view:
|
|
74
|
+
|
|
75
|
+
- **Left:** current slide (live)
|
|
76
|
+
- **Right:** next slide thumbnail
|
|
77
|
+
- **Below:** speaker notes + elapsed timer + wall clock + reset
|
|
78
|
+
|
|
79
|
+
Drag the presenter window to your laptop screen, drag the main window to the beamer (or vice versa), full-screen the main one. Both windows stay synchronized via the browser's `BroadcastChannel` API: any nav action in either window updates the other.
|
|
80
|
+
|
|
81
|
+
## 50 components in 7 families
|
|
82
|
+
|
|
83
|
+
| Family | Count | Examples |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| **Core** | 6 | KineticText, SectionCard, ActivityList, Compare, Counter, ShiftArrow |
|
|
86
|
+
| **Layout** | 6 | ImageText, FullImage, Quote, BigNumber, Stats, Bento |
|
|
87
|
+
| **Diagram** | 5 | Pillars, Timeline, Pyramid, Cycle, Funnel |
|
|
88
|
+
| **Chart** | 5 | Matrix2x2, BarChart, Progress, ProcessFlow, Venn |
|
|
89
|
+
| **Data-viz** | 10 | KPI, DonutChart, LineChart, Gauge, SparkLine, Heatmap, Roadmap, SWOT, CodeBlock, CodeDiff |
|
|
90
|
+
| **Business** | 10 | Pricing, Testimonial, TeamGrid, Agenda, Checklist, Steps, CTA, Callout, Tip, BeforeAfter |
|
|
91
|
+
| **Content** | 8 | Statement, QandA, Manifesto, Punchline, Definition, ImageGrid, Spotlight, Marquee |
|
|
92
|
+
|
|
93
|
+
50 anchors total + 5 bespoke cookbook examples (`examples/`). See `AGENT.md` for the full catalog with code examples.
|
|
94
|
+
|
|
95
|
+
## 15 transitions
|
|
96
|
+
|
|
97
|
+
Set in the manifest:
|
|
98
|
+
```js
|
|
99
|
+
Stage.deck({
|
|
100
|
+
slides: [
|
|
101
|
+
{ src: 'slides/00-title.js' },
|
|
102
|
+
{ src: 'slides/01-shift.js', transition: 'glitch' }
|
|
103
|
+
]
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Built-in: `cut · fade · slide · dissolve · glitch · wipe · zoom-in · zoom-out · flip · iris · shutter · push · typewriter · shatter` (plus the `cut`-preview-only animation).
|
|
108
|
+
|
|
109
|
+
Themes override visuals. Unknown name → falls back to `fade`. Hover any transition in the picker to play it once.
|
|
110
|
+
|
|
111
|
+
## 5 themes
|
|
112
|
+
|
|
113
|
+
| Theme | Vibe | Typography |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| **Phosphor** | Cinematic, technical, dark. Phosphor-green + film grain | JetBrains Mono |
|
|
116
|
+
| **Paper** | Academic, restrained, light. Navy accent, no glow | Inter + Source Serif 4 |
|
|
117
|
+
| **Neon** | Cyberpunk, magenta + cyan, heavy glow | Space Grotesk + JetBrains Mono |
|
|
118
|
+
| **Brand** | Corporate, schlicht. Blue accent, GitHub-dark adjacent | Inter |
|
|
119
|
+
| **Shopware** | Official Shopware brand. Light, Meteor design tokens, brand-blue `#0870ff` | Inter |
|
|
120
|
+
|
|
121
|
+
Switch live from the edit-mode storyboard, or set `data-theme="..."` on `<html>` plus link the matching theme CSS bundle.
|
|
122
|
+
|
|
123
|
+
## Speaker notes
|
|
124
|
+
|
|
125
|
+
Slides can carry notes for the presenter view:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
Stage.register(Stage.KineticText({
|
|
129
|
+
section: 2,
|
|
130
|
+
lines: [...]
|
|
131
|
+
}), {
|
|
132
|
+
notes: 'Open by reminding them about last quarter\'s incident. Pause for ~5s after "the shift".'
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Notes are plain strings, displayed in the presenter window. Hidden from the audience. Edit them via the 🎙 button on each storyboard tile.
|
|
137
|
+
|
|
138
|
+
## Export to PDF
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
npm install --save-dev playwright pdf-lib
|
|
142
|
+
npx playwright install chromium
|
|
143
|
+
npx stagecraft export pdf --out my-deck.pdf
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Renders each slide at 1920×1080, concatenates into a single PDF. Skips transitions; respects each slide's `init()` animation timing (waits 1.2s by default; tune with `--wait`).
|
|
147
|
+
|
|
148
|
+
## Visual regression tests
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm install --save-dev playwright pixelmatch pngjs
|
|
152
|
+
npx playwright install chromium
|
|
153
|
+
npm run test:visual:update # one-time: capture baseline
|
|
154
|
+
# ... make changes ...
|
|
155
|
+
npm run test:visual # diff against baseline
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Catches the kind of bugs where a Venn label drifts or a Heatmap layout breaks under refactor. Diffs land in `tests/visual/diff/`.
|
|
159
|
+
|
|
160
|
+
## Accessibility
|
|
161
|
+
|
|
162
|
+
Stagecraft respects `prefers-reduced-motion`:
|
|
163
|
+
- All CSS transitions/animations reduced to ~0ms globally
|
|
164
|
+
- JS animations (typewriter, particle emission, stagger) shortcut to final state immediately
|
|
165
|
+
|
|
166
|
+
The reduced-motion preference is OS-level — Stagecraft itself doesn't expose a toggle. If a component's animation is essential to the message, give it a non-animated fallback.
|
|
167
|
+
|
|
168
|
+
## Architecture (4 layers + convention)
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
Layer 0 Engine Stage.register, deck loader, navigation, steps, storyboard, transitions
|
|
172
|
+
Layer 1 Primitives staggerIn, typewriter, emitParticle, revealByDataStep, ...
|
|
173
|
+
Layer 2 Components 50 building blocks across 7 families
|
|
174
|
+
Layer 3 Cookbook 5 bespoke examples in examples/ — the real teachers
|
|
175
|
+
Layer 4 Convention AGENT.md — calibrates the agent's taste
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
See `docs/specs/2026-05-22-stagecraft-sdk-design.md` for the full design rationale.
|
|
179
|
+
|
|
180
|
+
## Bundle vs. source
|
|
181
|
+
|
|
182
|
+
- Source: `src/` (~70 files). What you read, edit, contribute against.
|
|
183
|
+
- Bundle: `dist/stagecraft.bundle.js` + `dist/themes/<name>.bundle.css`. Generated by `npm run build`. One script tag instead of 60+. Consumers from npm load these directly via `node_modules/stagecraft/dist/...`.
|
|
184
|
+
|
|
185
|
+
## Deploying a deck to the web
|
|
186
|
+
|
|
187
|
+
Stagecraft decks are static HTML. Drop them anywhere:
|
|
188
|
+
|
|
189
|
+
### gh-pages
|
|
190
|
+
```bash
|
|
191
|
+
# in your deck repo
|
|
192
|
+
git checkout -b gh-pages
|
|
193
|
+
npm run build # if your index.html uses src/ paths, copy node_modules/stagecraft into the repo first
|
|
194
|
+
git add -A
|
|
195
|
+
git commit -m "deploy"
|
|
196
|
+
git push origin gh-pages
|
|
197
|
+
# Enable Pages in repo settings → branch: gh-pages
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Netlify / Vercel / S3 / anything that serves static files
|
|
201
|
+
|
|
202
|
+
Same idea: the deck is just HTML + JS + CSS. No runtime dependencies for present mode.
|
|
203
|
+
|
|
204
|
+
## Status
|
|
205
|
+
|
|
206
|
+
v0.2 — feature-complete, multi-theme, multi-monitor, exportable. Pre-publication: dogfooding against a real production deck post-talk before tagging 1.0.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT — see `LICENSE`.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* stagecraft — dispatcher CLI.
|
|
6
|
+
* stagecraft init scaffold a new project
|
|
7
|
+
* stagecraft serve start dev server with edit mode
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import url from 'node:url';
|
|
13
|
+
|
|
14
|
+
const cmd = process.argv[2];
|
|
15
|
+
const rest = process.argv.slice(3);
|
|
16
|
+
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
function run(script) {
|
|
19
|
+
const child = spawn('node', [path.join(HERE, script), ...rest], { stdio: 'inherit' });
|
|
20
|
+
child.on('exit', code => process.exit(code ?? 0));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
switch (cmd) {
|
|
24
|
+
case 'init': run('init.js'); break;
|
|
25
|
+
case 'serve': run('serve.js'); break;
|
|
26
|
+
case 'export':
|
|
27
|
+
if (rest[0] !== 'pdf') {
|
|
28
|
+
console.error('Only `export pdf` is supported.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
rest.shift();
|
|
32
|
+
run('export.js');
|
|
33
|
+
break;
|
|
34
|
+
case '--help':
|
|
35
|
+
case '-h':
|
|
36
|
+
case undefined:
|
|
37
|
+
console.log(`Stagecraft — cinematic, agent-authored presentations.
|
|
38
|
+
|
|
39
|
+
Usage:
|
|
40
|
+
stagecraft init scaffold a new project
|
|
41
|
+
stagecraft serve [--port N] [--root DIR] dev server (edit mode)
|
|
42
|
+
stagecraft export pdf [--out deck.pdf] [--root DIR] render deck to PDF
|
|
43
|
+
(needs playwright + pdf-lib)
|
|
44
|
+
|
|
45
|
+
Without the dev server, open index.html in a browser to present.
|
|
46
|
+
Press P during a presentation to open the presenter view in a second window.`);
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
console.error(`Unknown command: ${cmd}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
package/bin/export.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* stagecraft export pdf — render the deck to a PDF.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx stagecraft export pdf [--out deck.pdf] [--root .] [--port N]
|
|
9
|
+
*
|
|
10
|
+
* Requires (peer): playwright + pdf-lib.
|
|
11
|
+
* npm install --save-dev playwright pdf-lib
|
|
12
|
+
* npx playwright install chromium
|
|
13
|
+
*
|
|
14
|
+
* Strategy:
|
|
15
|
+
* 1) start the local dev server (no edit-mode UI needed; just static serve)
|
|
16
|
+
* 2) launch headless chromium
|
|
17
|
+
* 3) navigate to each slide via #hash, wait for animations to settle
|
|
18
|
+
* 4) page.pdf() per slide, then concatenate via pdf-lib
|
|
19
|
+
* 5) write to --out
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import http from 'node:http';
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import url from 'node:url';
|
|
26
|
+
import { spawn } from 'node:child_process';
|
|
27
|
+
|
|
28
|
+
// --- args ---
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
let outPath = 'deck.pdf';
|
|
31
|
+
let rootArg = '.';
|
|
32
|
+
let port = 4040;
|
|
33
|
+
let waitMs = 1200;
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
if (args[i] === '--out') outPath = args[++i];
|
|
36
|
+
else if (args[i] === '--root') rootArg = args[++i];
|
|
37
|
+
else if (args[i] === '--port') port = parseInt(args[++i], 10);
|
|
38
|
+
else if (args[i] === '--wait') waitMs = parseInt(args[++i], 10);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ROOT = path.resolve(rootArg);
|
|
42
|
+
|
|
43
|
+
// --- soft-import optional deps ---
|
|
44
|
+
async function softImport(name) {
|
|
45
|
+
try { return await import(name); }
|
|
46
|
+
catch (e) {
|
|
47
|
+
console.error(`\n[stagecraft] PDF export requires ${name}.`);
|
|
48
|
+
console.error(` Install with: npm install --save-dev playwright pdf-lib`);
|
|
49
|
+
console.error(` Then: npx playwright install chromium`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { chromium } = await softImport('playwright');
|
|
55
|
+
const { PDFDocument } = await softImport('pdf-lib');
|
|
56
|
+
|
|
57
|
+
// --- start the dev server as a child process (avoid port conflicts with running serve) ---
|
|
58
|
+
console.log(`[stagecraft] starting server on :${port}…`);
|
|
59
|
+
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
|
|
60
|
+
const child = spawn('node', [path.join(HERE, 'serve.js'), '--root', ROOT, '--port', String(port)], {
|
|
61
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Wait for "http://localhost:" line in the server output
|
|
65
|
+
let serverReady = false;
|
|
66
|
+
child.stdout.on('data', (b) => {
|
|
67
|
+
const s = b.toString();
|
|
68
|
+
if (s.includes('http://localhost:')) serverReady = true;
|
|
69
|
+
});
|
|
70
|
+
child.stderr.on('data', b => process.stderr.write(b));
|
|
71
|
+
|
|
72
|
+
await waitFor(() => serverReady, 5000);
|
|
73
|
+
|
|
74
|
+
// --- launch headless browser ---
|
|
75
|
+
const browser = await chromium.launch();
|
|
76
|
+
const ctx = await browser.newContext({
|
|
77
|
+
viewport: { width: 1920, height: 1080 },
|
|
78
|
+
deviceScaleFactor: 1
|
|
79
|
+
});
|
|
80
|
+
const page = await ctx.newPage();
|
|
81
|
+
|
|
82
|
+
const baseUrl = `http://localhost:${port}/`;
|
|
83
|
+
console.log(`[stagecraft] navigating to ${baseUrl}…`);
|
|
84
|
+
await page.goto(baseUrl);
|
|
85
|
+
|
|
86
|
+
// Discover slide count from the runtime
|
|
87
|
+
await page.waitForFunction(() => window.Stage && window.Stage.slides && window.Stage.slides.length > 0, {
|
|
88
|
+
timeout: 10000
|
|
89
|
+
});
|
|
90
|
+
const slideCount = await page.evaluate(() => window.Stage.slides.length);
|
|
91
|
+
console.log(`[stagecraft] ${slideCount} slides to export`);
|
|
92
|
+
|
|
93
|
+
// --- iterate slides, page.pdf() per slide, merge ---
|
|
94
|
+
const out = await PDFDocument.create();
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < slideCount; i++) {
|
|
97
|
+
process.stdout.write(`[stagecraft] slide ${String(i + 1).padStart(2, '0')}/${slideCount}…\r`);
|
|
98
|
+
await page.goto(`${baseUrl}#${i}`);
|
|
99
|
+
// Hide chrome that doesn't belong in print
|
|
100
|
+
await page.evaluate(() => {
|
|
101
|
+
document.querySelectorAll('.ui, .welcome').forEach(n => n.style.display = 'none');
|
|
102
|
+
});
|
|
103
|
+
// Wait for the slide to render + animations to settle
|
|
104
|
+
await page.waitForTimeout(waitMs);
|
|
105
|
+
const pdfBytes = await page.pdf({
|
|
106
|
+
width: '1920px',
|
|
107
|
+
height: '1080px',
|
|
108
|
+
printBackground: true,
|
|
109
|
+
pageRanges: '1',
|
|
110
|
+
preferCSSPageSize: false,
|
|
111
|
+
margin: { top: 0, bottom: 0, left: 0, right: 0 }
|
|
112
|
+
});
|
|
113
|
+
const tmp = await PDFDocument.load(pdfBytes);
|
|
114
|
+
const [copied] = await out.copyPages(tmp, [0]);
|
|
115
|
+
out.addPage(copied);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const finalBytes = await out.save();
|
|
119
|
+
fs.writeFileSync(outPath, finalBytes);
|
|
120
|
+
console.log(`\n[stagecraft] wrote ${outPath} (${finalBytes.length} bytes, ${slideCount} pages)`);
|
|
121
|
+
|
|
122
|
+
await browser.close();
|
|
123
|
+
child.kill();
|
|
124
|
+
process.exit(0);
|
|
125
|
+
|
|
126
|
+
// --- helpers ---
|
|
127
|
+
function waitFor(pred, ms) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const t0 = Date.now();
|
|
130
|
+
function tick() {
|
|
131
|
+
if (pred()) return resolve();
|
|
132
|
+
if (Date.now() - t0 > ms) return reject(new Error('timeout waiting for server'));
|
|
133
|
+
setTimeout(tick, 80);
|
|
134
|
+
}
|
|
135
|
+
tick();
|
|
136
|
+
});
|
|
137
|
+
}
|
package/bin/init.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* stagecraft init — scaffold a new stagecraft project in the current directory.
|
|
6
|
+
*
|
|
7
|
+
* Copies the starter/ template + AGENT.md to cwd.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import url from 'node:url';
|
|
13
|
+
|
|
14
|
+
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
|
|
15
|
+
const PKG_ROOT = path.resolve(HERE, '..');
|
|
16
|
+
const SRC = path.join(PKG_ROOT, 'starter');
|
|
17
|
+
const TARGET = process.cwd();
|
|
18
|
+
|
|
19
|
+
function copyRecursive(src, dst) {
|
|
20
|
+
const stat = fs.statSync(src);
|
|
21
|
+
if (stat.isDirectory()) {
|
|
22
|
+
if (!fs.existsSync(dst)) fs.mkdirSync(dst, { recursive: true });
|
|
23
|
+
for (const entry of fs.readdirSync(src)) {
|
|
24
|
+
copyRecursive(path.join(src, entry), path.join(dst, entry));
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
if (fs.existsSync(dst)) {
|
|
28
|
+
console.log(` skip (exists): ${path.relative(TARGET, dst)}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
fs.copyFileSync(src, dst);
|
|
32
|
+
console.log(` + ${path.relative(TARGET, dst)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`[stagecraft] scaffold → ${TARGET}`);
|
|
37
|
+
copyRecursive(SRC, TARGET);
|
|
38
|
+
|
|
39
|
+
// Also copy AGENT.md to project root
|
|
40
|
+
const agentMd = path.join(PKG_ROOT, 'AGENT.md');
|
|
41
|
+
if (fs.existsSync(agentMd)) {
|
|
42
|
+
const dst = path.join(TARGET, 'AGENT.md');
|
|
43
|
+
if (!fs.existsSync(dst)) {
|
|
44
|
+
fs.copyFileSync(agentMd, dst);
|
|
45
|
+
console.log(` + AGENT.md`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log('\nDone. Next steps:');
|
|
50
|
+
console.log(' npx stagecraft serve # dev server with edit mode');
|
|
51
|
+
console.log(' open index.html # presentation only');
|
|
52
|
+
console.log(' read AGENT.md # the manifesto');
|