slides-grab 1.2.1 → 1.2.3
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/README.md +32 -4
- package/bin/ppt-agent.js +76 -36
- package/package.json +3 -4
- package/scripts/build-viewer.js +48 -21
- package/scripts/editor-server.js +113 -18
- package/scripts/figma-export.js +16 -1
- package/scripts/html2pdf.js +51 -23
- package/scripts/html2pptx.js +22 -1
- package/scripts/validate-slides.js +22 -5
- package/skills/slides-grab/SKILL.md +25 -20
- package/skills/slides-grab/references/presentation-workflow-reference.md +12 -11
- package/skills/slides-grab-card-news/SKILL.md +35 -0
- package/skills/slides-grab-design/SKILL.md +19 -16
- package/skills/slides-grab-design/references/design-rules.md +11 -7
- package/skills/slides-grab-design/references/design-system-full.md +7 -19
- package/skills/slides-grab-design/references/detailed-design-rules.md +6 -1
- package/skills/slides-grab-export/SKILL.md +15 -8
- package/skills/slides-grab-export/references/html2pptx.md +4 -4
- package/skills/slides-grab-plan/SKILL.md +7 -5
- package/src/design-styles-data.js +1928 -0
- package/src/design-styles.js +55 -0
- package/src/editor/codex-edit.js +57 -45
- package/src/editor/editor-codex-prompt.md +50 -0
- package/src/editor/js/editor-init.js +34 -2
- package/src/editor/js/editor-state.js +9 -2
- package/src/editor/screenshot.js +4 -3
- package/src/export-resolution.cjs +21 -11
- package/src/figma.js +11 -3
- package/src/pptx-raster-export.cjs +79 -21
- package/src/resolve.js +2 -51
- package/src/slide-mode.cjs +72 -0
- package/src/validation/cli.js +23 -0
- package/src/validation/core.js +39 -25
- package/templates/design-styles/README.md +19 -0
- package/templates/design-styles/preview.html +3356 -0
- package/themes/corporate.css +0 -8
- package/themes/executive.css +0 -10
- package/themes/modern-dark.css +0 -9
- package/themes/sage.css +0 -9
- package/themes/warm.css +0 -8
package/README.md
CHANGED
|
@@ -70,9 +70,9 @@ There are many AI tools that generate slide HTML. Almost none let you **visually
|
|
|
70
70
|
|
|
71
71
|
## CLI Commands
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Workflow commands support `--slides-dir <path>` (default: `slides`).
|
|
74
74
|
|
|
75
|
-
On a fresh clone,
|
|
75
|
+
On a fresh clone, the discovery commands (`--help`, `list-templates`, `list-styles`, and `preview-styles`) work without a deck. `edit`, `build-viewer`, `validate`, `convert`, and `pdf` require an existing slides workspace containing `slide-*.html`.
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
slides-grab edit # Launch visual slide editor
|
|
@@ -84,13 +84,27 @@ slides-grab figma # Export an experimental / unstable Figma Slides i
|
|
|
84
84
|
slides-grab pdf # Export PDF in capture mode (default)
|
|
85
85
|
slides-grab pdf --resolution 2160p # Higher-resolution image-backed PDF export
|
|
86
86
|
slides-grab pdf --mode print # Export searchable/selectable text PDF
|
|
87
|
+
slides-grab png # Render one PNG per slide (default 2160p)
|
|
88
|
+
slides-grab png --slide-mode card-news # Render square 1:1 PNGs for Instagram
|
|
87
89
|
slides-grab image --prompt "..." # Generate a local slide image with Nano Banana Pro
|
|
88
90
|
slides-grab fetch-video --url <youtube-url> --slides-dir decks/my-deck # Download a local video asset with yt-dlp
|
|
89
91
|
slides-grab tldraw # Render a .tldr diagram into a slide-sized local SVG asset
|
|
90
92
|
slides-grab list-templates # Show available slide templates
|
|
91
|
-
slides-grab list-
|
|
93
|
+
slides-grab list-styles # Show 35 bundled design styles (browse, preview, select)
|
|
94
|
+
slides-grab preview-styles # Open the 35-style visual gallery in browser
|
|
92
95
|
```
|
|
93
96
|
|
|
97
|
+
## Design Style Collections
|
|
98
|
+
|
|
99
|
+
slides-grab bundles 35 design styles: 30 derived from [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles) plus 5 slides-grab originals. Agents can also create fully custom designs beyond the bundled collection.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
slides-grab list-styles # Browse the catalog
|
|
103
|
+
slides-grab preview-styles # Local HTML preview
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Tell the agent which style to use (or ask for something custom) — no config files needed.
|
|
107
|
+
|
|
94
108
|
## Asset Contract
|
|
95
109
|
|
|
96
110
|
Slides should store local image and video files in `<slides-dir>/assets/` and reference them as `./assets/<file>` from each `slide-XX.html`.
|
|
@@ -141,12 +155,26 @@ slides-grab edit --slides-dir decks/my-deck
|
|
|
141
155
|
slides-grab validate --slides-dir decks/my-deck
|
|
142
156
|
slides-grab pdf --slides-dir decks/my-deck --output decks/my-deck.pdf
|
|
143
157
|
slides-grab pdf --slides-dir decks/my-deck --mode print --output decks/my-deck-searchable.pdf
|
|
158
|
+
slides-grab png --slides-dir decks/my-deck --output-dir decks/my-deck/out-png
|
|
144
159
|
slides-grab convert --slides-dir decks/my-deck --output decks/my-deck.pptx
|
|
145
160
|
slides-grab figma --slides-dir decks/my-deck --output decks/my-deck-figma.pptx
|
|
146
161
|
```
|
|
147
162
|
|
|
148
163
|
> **Warning:** `slides-grab convert` and `slides-grab figma` are currently **experimental / unstable**. Expect best-effort output, layout shifts, and manual cleanup in PowerPoint or Figma.
|
|
149
164
|
|
|
165
|
+
### Card News (Square 1:1) Workflow
|
|
166
|
+
|
|
167
|
+
Instagram-style card news uses a 720pt × 720pt frame end-to-end. Pass `--mode card-news` (or `--slide-mode card-news` for `pdf`/`png`) at every stage and prefer `slides-grab png` as the primary export so each card becomes an Instagram-ready PNG.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
slides-grab edit --slides-dir decks/my-cards --mode card-news
|
|
171
|
+
slides-grab validate --slides-dir decks/my-cards --mode card-news
|
|
172
|
+
slides-grab png --slides-dir decks/my-cards --slide-mode card-news --resolution 2160p
|
|
173
|
+
# Optional extras (PPTX / Figma remain experimental / unstable)
|
|
174
|
+
slides-grab pdf --slides-dir decks/my-cards --slide-mode card-news --output decks/my-cards.pdf
|
|
175
|
+
slides-grab convert --slides-dir decks/my-cards --mode card-news --output decks/my-cards.pptx
|
|
176
|
+
```
|
|
177
|
+
|
|
150
178
|
### Tldraw Diagram Assets
|
|
151
179
|
|
|
152
180
|
Use `slides-grab tldraw` when you want a newly authored `tldraw` diagram to fit an exact slide region and remain export-friendly as a local SVG asset. The command supports current-format `.tldr` files and store-snapshot JSON; legacy pre-records `.tldr` files must be reopened and resaved in a current `tldraw` build first:
|
|
@@ -204,7 +232,7 @@ bin/ CLI entry point
|
|
|
204
232
|
src/editor/ Visual editor (HTML + JS client modules)
|
|
205
233
|
scripts/ Build, validate, convert, editor server
|
|
206
234
|
templates/ Slide HTML templates (cover, content, chart, ...)
|
|
207
|
-
|
|
235
|
+
src/ Design styles data, style config, path resolution
|
|
208
236
|
skills/ Shared Vercel-installable agent skills + references
|
|
209
237
|
docs/ Installation & usage guides
|
|
210
238
|
```
|
package/bin/ppt-agent.js
CHANGED
|
@@ -27,7 +27,7 @@ const figmaHelpText = [
|
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Run a Node.js script from the package, with CWD set to the user's directory.
|
|
30
|
-
* Scripts resolve slide paths via --slides-dir and templates
|
|
30
|
+
* Scripts resolve slide paths via --slides-dir and templates via src/resolve.js.
|
|
31
31
|
*/
|
|
32
32
|
function runNodeScript(relativePath, args = []) {
|
|
33
33
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
@@ -68,6 +68,12 @@ function collectRepeatedOption(value, previous = []) {
|
|
|
68
68
|
return [...previous, value];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function reportCliError(error) {
|
|
72
|
+
console.error(`[slides-grab] ${error.message}`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
71
77
|
const program = new Command();
|
|
72
78
|
|
|
73
79
|
program
|
|
@@ -81,8 +87,9 @@ program
|
|
|
81
87
|
.command('build-viewer')
|
|
82
88
|
.description('Build viewer.html from slide HTML files')
|
|
83
89
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
90
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
84
91
|
.action(async (options = {}) => {
|
|
85
|
-
const args = ['--slides-dir', options.slidesDir];
|
|
92
|
+
const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
86
93
|
await runCommand('scripts/build-viewer.js', args);
|
|
87
94
|
});
|
|
88
95
|
|
|
@@ -92,9 +99,10 @@ program
|
|
|
92
99
|
.description('Run structured validation on slide HTML files (Playwright-based)')
|
|
93
100
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
94
101
|
.option('--format <format>', 'Output format: concise, json, json-full', 'concise')
|
|
102
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
95
103
|
.option('--slide <file>', 'Validate only the named slide file (repeatable)', collectRepeatedOption, [])
|
|
96
104
|
.action(async (options = {}) => {
|
|
97
|
-
const args = ['--slides-dir', options.slidesDir, '--format', options.format];
|
|
105
|
+
const args = ['--slides-dir', options.slidesDir, '--format', options.format, '--mode', options.mode];
|
|
98
106
|
for (const slide of options.slide || []) {
|
|
99
107
|
args.push('--slide', String(slide));
|
|
100
108
|
}
|
|
@@ -106,9 +114,10 @@ program
|
|
|
106
114
|
.description('Convert slide HTML files to experimental / unstable PPTX')
|
|
107
115
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
108
116
|
.option('--output <path>', 'Output PPTX file')
|
|
117
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
109
118
|
.option('--resolution <preset>', 'Raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p)')
|
|
110
119
|
.action(async (options = {}) => {
|
|
111
|
-
const args = ['--slides-dir', options.slidesDir];
|
|
120
|
+
const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
112
121
|
if (options.output) {
|
|
113
122
|
args.push('--output', String(options.output));
|
|
114
123
|
}
|
|
@@ -124,6 +133,7 @@ program
|
|
|
124
133
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
125
134
|
.option('--output <path>', 'Output PDF file')
|
|
126
135
|
.option('--mode <mode>', 'PDF export mode: capture for visual fidelity, print for searchable text', 'capture')
|
|
136
|
+
.option('--slide-mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
127
137
|
.option('--resolution <preset>', 'Capture raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p in capture mode)')
|
|
128
138
|
.action(async (options = {}) => {
|
|
129
139
|
const args = ['--slides-dir', options.slidesDir];
|
|
@@ -133,12 +143,36 @@ program
|
|
|
133
143
|
if (options.mode) {
|
|
134
144
|
args.push('--mode', String(options.mode));
|
|
135
145
|
}
|
|
146
|
+
if (options.slideMode) {
|
|
147
|
+
args.push('--slide-mode', String(options.slideMode));
|
|
148
|
+
}
|
|
136
149
|
if (options.resolution) {
|
|
137
150
|
args.push('--resolution', String(options.resolution));
|
|
138
151
|
}
|
|
139
152
|
await runCommand('scripts/html2pdf.js', args);
|
|
140
153
|
});
|
|
141
154
|
|
|
155
|
+
program
|
|
156
|
+
.command('png')
|
|
157
|
+
.description('Render slide HTML files to one PNG per slide')
|
|
158
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
159
|
+
.option('--output-dir <path>', 'Output directory for PNG files (default: <slides-dir>/out-png)')
|
|
160
|
+
.option('--slide-mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
161
|
+
.option('--resolution <preset>', 'Raster size preset: 720p, 1080p, 1440p, 2160p, or 4k', '2160p')
|
|
162
|
+
.action(async (options = {}) => {
|
|
163
|
+
const args = ['--slides-dir', options.slidesDir];
|
|
164
|
+
if (options.outputDir) {
|
|
165
|
+
args.push('--output-dir', String(options.outputDir));
|
|
166
|
+
}
|
|
167
|
+
if (options.slideMode) {
|
|
168
|
+
args.push('--slide-mode', String(options.slideMode));
|
|
169
|
+
}
|
|
170
|
+
if (options.resolution) {
|
|
171
|
+
args.push('--resolution', String(options.resolution));
|
|
172
|
+
}
|
|
173
|
+
await runCommand('scripts/html2png.js', args);
|
|
174
|
+
});
|
|
175
|
+
|
|
142
176
|
program
|
|
143
177
|
.command('fetch-video')
|
|
144
178
|
.description('Download a video into <slides-dir>/assets via yt-dlp and print the ./assets reference')
|
|
@@ -159,9 +193,10 @@ program
|
|
|
159
193
|
.helpOption('-h, --help', 'Show this help message')
|
|
160
194
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
161
195
|
.option('--output <path>', 'Output PPTX file (default: <slides-dir>-figma.pptx)')
|
|
196
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
162
197
|
.addHelpText('after', figmaHelpText)
|
|
163
198
|
.action(async (options = {}) => {
|
|
164
|
-
const args = ['--slides-dir', options.slidesDir];
|
|
199
|
+
const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
165
200
|
if (options.output) {
|
|
166
201
|
args.push('--output', String(options.output));
|
|
167
202
|
}
|
|
@@ -216,15 +251,16 @@ program
|
|
|
216
251
|
.description('Start interactive slide editor with Codex image-based edit flow')
|
|
217
252
|
.option('--port <number>', 'Server port')
|
|
218
253
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
254
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
219
255
|
.action(async (options = {}) => {
|
|
220
|
-
const args = ['--slides-dir', options.slidesDir];
|
|
256
|
+
const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
221
257
|
if (options.port) {
|
|
222
258
|
args.push('--port', String(options.port));
|
|
223
259
|
}
|
|
224
260
|
await runCommand('scripts/editor-server.js', args);
|
|
225
261
|
});
|
|
226
262
|
|
|
227
|
-
// --- Template/
|
|
263
|
+
// --- Template/style discovery commands ---
|
|
228
264
|
|
|
229
265
|
program
|
|
230
266
|
.command('list-templates')
|
|
@@ -245,21 +281,42 @@ program
|
|
|
245
281
|
});
|
|
246
282
|
|
|
247
283
|
program
|
|
248
|
-
.command('list-
|
|
249
|
-
.description('List
|
|
284
|
+
.command('list-styles')
|
|
285
|
+
.description('List bundled design styles agents and users can reference during slide generation')
|
|
250
286
|
.action(async () => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
287
|
+
try {
|
|
288
|
+
const { listDesignStyles } = await import('../src/design-styles.js');
|
|
289
|
+
const styles = listDesignStyles();
|
|
290
|
+
|
|
291
|
+
if (styles.length === 0) {
|
|
292
|
+
console.log('No bundled design styles found.');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log('Available design styles:\n');
|
|
297
|
+
for (const style of styles) {
|
|
298
|
+
console.log(` ${style.id.padEnd(22)} ${style.title}`);
|
|
299
|
+
console.log(` ${style.mood} · ${style.bestFor}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log(`\nTotal: ${styles.length} styles`);
|
|
303
|
+
console.log('Preview: slides-grab preview-styles [--style <id>]');
|
|
304
|
+
} catch (error) {
|
|
305
|
+
reportCliError(error);
|
|
256
306
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
program
|
|
310
|
+
.command('preview-styles')
|
|
311
|
+
.description('Print the path to the bundled 35-style visual preview gallery')
|
|
312
|
+
.action(async () => {
|
|
313
|
+
try {
|
|
314
|
+
const { getPreviewHtmlPath } = await import('../src/design-styles.js');
|
|
315
|
+
const previewPath = getPreviewHtmlPath();
|
|
316
|
+
console.log(previewPath);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
reportCliError(error);
|
|
261
319
|
}
|
|
262
|
-
console.log(`\nTotal: ${themes.length} themes`);
|
|
263
320
|
});
|
|
264
321
|
|
|
265
322
|
program
|
|
@@ -280,22 +337,5 @@ program
|
|
|
280
337
|
console.log(content);
|
|
281
338
|
});
|
|
282
339
|
|
|
283
|
-
program
|
|
284
|
-
.command('show-theme')
|
|
285
|
-
.description('Print the contents of a theme file')
|
|
286
|
-
.argument('<name>', 'Theme name (e.g. "modern-dark", "executive")')
|
|
287
|
-
.action(async (name) => {
|
|
288
|
-
const { resolveTheme } = await import('../src/resolve.js');
|
|
289
|
-
const result = resolveTheme(name);
|
|
290
|
-
if (!result) {
|
|
291
|
-
console.error(`Theme "${name}" not found.`);
|
|
292
|
-
process.exitCode = 1;
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
const content = readFileSync(result.path, 'utf-8');
|
|
296
|
-
console.log(`/* Theme: ${name} (${result.source}) */`);
|
|
297
|
-
console.log(`/* Path: ${result.path} */\n`);
|
|
298
|
-
console.log(content);
|
|
299
|
-
});
|
|
300
340
|
|
|
301
341
|
await program.parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slides-grab",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Agent-first presentation framework — plan, design, and visually edit HTML slides with Claude Code or Codex, then export to PDF or experimental/unstable PPTX/Figma formats",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vkehfdl1",
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"skills/",
|
|
46
46
|
"src/",
|
|
47
47
|
"templates/",
|
|
48
|
-
"themes/",
|
|
49
48
|
"LICENSE",
|
|
50
49
|
"README.md"
|
|
51
50
|
],
|
|
@@ -54,18 +53,18 @@
|
|
|
54
53
|
"build-viewer": "node scripts/build-viewer.js",
|
|
55
54
|
"validate": "node scripts/validate-slides.js",
|
|
56
55
|
"convert": "node convert.cjs",
|
|
57
|
-
"test": "node --test --test-concurrency=1 tests/editor/editor-codex-edit.test.js tests/nano-banana/nano-banana.test.js tests/pdf/html2pdf.test.js tests/pdf/html2pdf.e2e.test.js tests/figma/figma-export.test.js tests/image-contract/image-contract.test.js tests/tldraw/render-tldraw.test.js tests/validation/validate-slides.test.js tests/skills/installable-skills.test.js tests/video/download-video.test.js",
|
|
56
|
+
"test": "node --test --test-concurrency=1 tests/design/design-styles.test.js tests/editor/editor-codex-edit.test.js tests/editor/editor-server.test.js tests/nano-banana/nano-banana.test.js tests/pdf/html2pdf.test.js tests/pdf/html2pdf.e2e.test.js tests/figma/figma-export.test.js tests/image-contract/image-contract.test.js tests/tldraw/render-tldraw.test.js tests/validation/validate-slides.test.js tests/viewer/build-viewer.test.js tests/skills/installable-skills.test.js tests/video/download-video.test.js",
|
|
58
57
|
"test:e2e": "node --test tests/editor/editor-ui.e2e.test.js tests/editor/editor-concurrency.e2e.test.js"
|
|
59
58
|
},
|
|
60
59
|
"dependencies": {
|
|
61
60
|
"commander": "^12.1.0",
|
|
62
61
|
"express": "^5.2.1",
|
|
62
|
+
"lucide-react": "^1.7.0",
|
|
63
63
|
"pdf-lib": "^1.17.1",
|
|
64
64
|
"playwright": "^1.40.0",
|
|
65
65
|
"pptxgenjs": "^3.12.0",
|
|
66
66
|
"react": "^19.2.4",
|
|
67
67
|
"react-dom": "^19.2.4",
|
|
68
|
-
"react-icons": "^5.0.0",
|
|
69
68
|
"sharp": "^0.33.0",
|
|
70
69
|
"tldraw": "^4.4.1"
|
|
71
70
|
},
|
package/scripts/build-viewer.js
CHANGED
|
@@ -9,11 +9,20 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
12
|
-
import {
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
13
|
import { join, resolve } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
14
15
|
|
|
15
16
|
import { buildSlideRuntimeHtml } from '../src/image-contract.js';
|
|
16
17
|
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const {
|
|
20
|
+
DEFAULT_SLIDE_MODE,
|
|
21
|
+
getSlideModeChoices,
|
|
22
|
+
getSlideModeConfig,
|
|
23
|
+
normalizeSlideMode,
|
|
24
|
+
} = require('../src/slide-mode.cjs');
|
|
25
|
+
|
|
17
26
|
const DEFAULT_SLIDES_DIR = 'slides';
|
|
18
27
|
|
|
19
28
|
function printUsage() {
|
|
@@ -23,6 +32,7 @@ function printUsage() {
|
|
|
23
32
|
'',
|
|
24
33
|
'Options:',
|
|
25
34
|
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
35
|
+
` --mode <mode> Slide mode: ${getSlideModeChoices().join(', ')} (default: ${DEFAULT_SLIDE_MODE})`,
|
|
26
36
|
' -h, --help Show this help message',
|
|
27
37
|
].join('\n'),
|
|
28
38
|
);
|
|
@@ -40,6 +50,7 @@ function readOptionValue(args, index, optionName) {
|
|
|
40
50
|
export function parseCliArgs(args) {
|
|
41
51
|
const options = {
|
|
42
52
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
53
|
+
mode: DEFAULT_SLIDE_MODE,
|
|
43
54
|
help: false,
|
|
44
55
|
};
|
|
45
56
|
|
|
@@ -62,6 +73,17 @@ export function parseCliArgs(args) {
|
|
|
62
73
|
continue;
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
if (arg === '--mode') {
|
|
77
|
+
options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
|
|
78
|
+
i += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (arg.startsWith('--mode=')) {
|
|
83
|
+
options.mode = normalizeSlideMode(arg.slice('--mode='.length));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
throw new Error(`Unknown option: ${arg}`);
|
|
66
88
|
}
|
|
67
89
|
|
|
@@ -70,17 +92,28 @@ export function parseCliArgs(args) {
|
|
|
70
92
|
}
|
|
71
93
|
|
|
72
94
|
options.slidesDir = options.slidesDir.trim();
|
|
95
|
+
options.mode = normalizeSlideMode(options.mode);
|
|
73
96
|
return options;
|
|
74
97
|
}
|
|
75
98
|
|
|
99
|
+
function toSlideOrder(fileName) {
|
|
100
|
+
const match = fileName.match(/\d+/);
|
|
101
|
+
return match ? Number.parseInt(match[0], 10) : Number.POSITIVE_INFINITY;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sortSlideFiles(a, b) {
|
|
105
|
+
const orderA = toSlideOrder(a);
|
|
106
|
+
const orderB = toSlideOrder(b);
|
|
107
|
+
if (orderA !== orderB) {
|
|
108
|
+
return orderA - orderB;
|
|
109
|
+
}
|
|
110
|
+
return a.localeCompare(b);
|
|
111
|
+
}
|
|
112
|
+
|
|
76
113
|
export function findSlideFiles(slidesDir) {
|
|
77
114
|
return readdirSync(slidesDir)
|
|
78
|
-
.filter((file) => /^slide
|
|
79
|
-
.sort(
|
|
80
|
-
const numA = parseInt(a.match(/\d+/)[0], 10);
|
|
81
|
-
const numB = parseInt(b.match(/\d+/)[0], 10);
|
|
82
|
-
return numA - numB || a.localeCompare(b);
|
|
83
|
-
});
|
|
115
|
+
.filter((file) => /^slide-.*\.html$/i.test(file))
|
|
116
|
+
.sort(sortSlideFiles);
|
|
84
117
|
}
|
|
85
118
|
|
|
86
119
|
/**
|
|
@@ -106,7 +139,9 @@ export function loadSlides(slidesDir) {
|
|
|
106
139
|
});
|
|
107
140
|
}
|
|
108
141
|
|
|
109
|
-
export function buildViewerHtml(slides) {
|
|
142
|
+
export function buildViewerHtml(slides, { slideMode = DEFAULT_SLIDE_MODE } = {}) {
|
|
143
|
+
const { framePt } = getSlideModeConfig(slideMode);
|
|
144
|
+
|
|
110
145
|
return `<!DOCTYPE html>
|
|
111
146
|
<html lang="ko">
|
|
112
147
|
<head>
|
|
@@ -131,7 +166,6 @@ export function buildViewerHtml(slides) {
|
|
|
131
166
|
flex-direction: column;
|
|
132
167
|
}
|
|
133
168
|
|
|
134
|
-
/* Navigation bar */
|
|
135
169
|
.nav-bar {
|
|
136
170
|
height: 48px;
|
|
137
171
|
background: #1a1a1a;
|
|
@@ -182,7 +216,6 @@ export function buildViewerHtml(slides) {
|
|
|
182
216
|
padding: 6px 10px !important;
|
|
183
217
|
}
|
|
184
218
|
|
|
185
|
-
/* Slide viewport */
|
|
186
219
|
.slide-viewport {
|
|
187
220
|
flex: 1;
|
|
188
221
|
display: flex;
|
|
@@ -193,18 +226,17 @@ export function buildViewerHtml(slides) {
|
|
|
193
226
|
}
|
|
194
227
|
|
|
195
228
|
.slide-scaler {
|
|
196
|
-
width:
|
|
197
|
-
height:
|
|
229
|
+
width: ${framePt.width}pt;
|
|
230
|
+
height: ${framePt.height}pt;
|
|
198
231
|
position: relative;
|
|
199
232
|
transform-origin: center center;
|
|
200
233
|
}
|
|
201
234
|
|
|
202
|
-
/* Slide frames (iframes) */
|
|
203
235
|
.slide-frame {
|
|
204
236
|
position: absolute;
|
|
205
237
|
inset: 0;
|
|
206
|
-
width:
|
|
207
|
-
height:
|
|
238
|
+
width: ${framePt.width}pt;
|
|
239
|
+
height: ${framePt.height}pt;
|
|
208
240
|
border: none;
|
|
209
241
|
overflow: hidden;
|
|
210
242
|
opacity: 0;
|
|
@@ -220,7 +252,6 @@ export function buildViewerHtml(slides) {
|
|
|
220
252
|
</head>
|
|
221
253
|
<body>
|
|
222
254
|
<div class="viewer-container">
|
|
223
|
-
<!-- Navigation -->
|
|
224
255
|
<div class="nav-bar">
|
|
225
256
|
<button id="btn-prev" title="Previous (\\u2190)">Prev</button>
|
|
226
257
|
<span class="slide-counter" id="counter">1 / ${slides.length}</span>
|
|
@@ -228,7 +259,6 @@ export function buildViewerHtml(slides) {
|
|
|
228
259
|
<button class="btn-fullscreen" id="btn-fs" title="Fullscreen (F)">⛶</button>
|
|
229
260
|
</div>
|
|
230
261
|
|
|
231
|
-
<!-- Slide viewport -->
|
|
232
262
|
<div class="slide-viewport" id="viewport">
|
|
233
263
|
<div class="slide-scaler" id="scaler">
|
|
234
264
|
${slides.map((s, i) => ` <iframe class="slide-frame${i === 0 ? ' active' : ''}" data-slide="${i + 1}" srcdoc="${escapeForSrcdoc(s.html)}" sandbox="allow-same-origin"></iframe>`).join('\n')}
|
|
@@ -265,7 +295,6 @@ ${slides.map((s, i) => ` <iframe class="slide-frame${i === 0 ? ' active'
|
|
|
265
295
|
btnNext.addEventListener('click', next);
|
|
266
296
|
btnPrev.disabled = true;
|
|
267
297
|
|
|
268
|
-
// Keyboard navigation
|
|
269
298
|
document.addEventListener('keydown', (e) => {
|
|
270
299
|
if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); next(); }
|
|
271
300
|
else if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); }
|
|
@@ -277,7 +306,6 @@ ${slides.map((s, i) => ` <iframe class="slide-frame${i === 0 ? ' active'
|
|
|
277
306
|
}
|
|
278
307
|
});
|
|
279
308
|
|
|
280
|
-
// Fullscreen
|
|
281
309
|
function toggleFullscreen() {
|
|
282
310
|
if (!document.fullscreenElement) {
|
|
283
311
|
document.documentElement.requestFullscreen().catch(() => {});
|
|
@@ -287,7 +315,6 @@ ${slides.map((s, i) => ` <iframe class="slide-frame${i === 0 ? ' active'
|
|
|
287
315
|
}
|
|
288
316
|
document.getElementById('btn-fs').addEventListener('click', toggleFullscreen);
|
|
289
317
|
|
|
290
|
-
// Auto-scale to fit viewport (95% fit)
|
|
291
318
|
function rescale() {
|
|
292
319
|
const vw = viewport.clientWidth;
|
|
293
320
|
const vh = viewport.clientHeight;
|
|
@@ -332,7 +359,7 @@ export function main(args = process.argv.slice(2)) {
|
|
|
332
359
|
}
|
|
333
360
|
|
|
334
361
|
console.log(`Found ${slides.length} slides`);
|
|
335
|
-
writeFileSync(output, buildViewerHtml(slides), 'utf-8');
|
|
362
|
+
writeFileSync(output, buildViewerHtml(slides, { slideMode: options.mode }), 'utf-8');
|
|
336
363
|
console.log(`Built viewer: ${output}`);
|
|
337
364
|
return { slidesDir, output, slides };
|
|
338
365
|
}
|