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.
Files changed (40) hide show
  1. package/README.md +32 -4
  2. package/bin/ppt-agent.js +76 -36
  3. package/package.json +3 -4
  4. package/scripts/build-viewer.js +48 -21
  5. package/scripts/editor-server.js +113 -18
  6. package/scripts/figma-export.js +16 -1
  7. package/scripts/html2pdf.js +51 -23
  8. package/scripts/html2pptx.js +22 -1
  9. package/scripts/validate-slides.js +22 -5
  10. package/skills/slides-grab/SKILL.md +25 -20
  11. package/skills/slides-grab/references/presentation-workflow-reference.md +12 -11
  12. package/skills/slides-grab-card-news/SKILL.md +35 -0
  13. package/skills/slides-grab-design/SKILL.md +19 -16
  14. package/skills/slides-grab-design/references/design-rules.md +11 -7
  15. package/skills/slides-grab-design/references/design-system-full.md +7 -19
  16. package/skills/slides-grab-design/references/detailed-design-rules.md +6 -1
  17. package/skills/slides-grab-export/SKILL.md +15 -8
  18. package/skills/slides-grab-export/references/html2pptx.md +4 -4
  19. package/skills/slides-grab-plan/SKILL.md +7 -5
  20. package/src/design-styles-data.js +1928 -0
  21. package/src/design-styles.js +55 -0
  22. package/src/editor/codex-edit.js +57 -45
  23. package/src/editor/editor-codex-prompt.md +50 -0
  24. package/src/editor/js/editor-init.js +34 -2
  25. package/src/editor/js/editor-state.js +9 -2
  26. package/src/editor/screenshot.js +4 -3
  27. package/src/export-resolution.cjs +21 -11
  28. package/src/figma.js +11 -3
  29. package/src/pptx-raster-export.cjs +79 -21
  30. package/src/resolve.js +2 -51
  31. package/src/slide-mode.cjs +72 -0
  32. package/src/validation/cli.js +23 -0
  33. package/src/validation/core.js +39 -25
  34. package/templates/design-styles/README.md +19 -0
  35. package/templates/design-styles/preview.html +3356 -0
  36. package/themes/corporate.css +0 -8
  37. package/themes/executive.css +0 -10
  38. package/themes/modern-dark.css +0 -9
  39. package/themes/sage.css +0 -9
  40. 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
- All commands support `--slides-dir <path>` (default: `slides`).
73
+ Workflow commands support `--slides-dir <path>` (default: `slides`).
74
74
 
75
- On a fresh clone, only `--help`, `list-templates`, and `list-themes` work without a deck. `edit`, `build-viewer`, `validate`, `convert`, and `pdf` require an existing slides workspace containing `slide-*.html`.
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-themes # Show available color themes
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
- themes/ Color themes (modern-dark, executive, sage, ...)
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/themes via src/resolve.js.
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/theme discovery commands ---
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-themes')
249
- .description('List all available color themes (local overrides + package built-ins)')
284
+ .command('list-styles')
285
+ .description('List bundled design styles agents and users can reference during slide generation')
250
286
  .action(async () => {
251
- const { listThemes } = await import('../src/resolve.js');
252
- const themes = listThemes();
253
- if (themes.length === 0) {
254
- console.log('No themes found.');
255
- return;
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
- console.log('Available themes:\n');
258
- for (const t of themes) {
259
- const tag = t.source === 'local' ? '(local)' : '(built-in)';
260
- console.log(` ${t.name.padEnd(20)} ${tag}`);
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.1",
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
  },
@@ -9,11 +9,20 @@
9
9
  */
10
10
 
11
11
  import { readFileSync, writeFileSync, readdirSync } from 'fs';
12
- import { fileURLToPath } from 'url';
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-\d+\.html$/i.test(file))
79
- .sort((a, b) => {
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: 720pt;
197
- height: 405pt;
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: 720pt;
207
- height: 405pt;
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)">&#x26F6;</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
  }