slides-grab 1.2.2 → 1.2.4

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 CHANGED
@@ -84,6 +84,8 @@ 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
@@ -153,12 +155,26 @@ slides-grab edit --slides-dir decks/my-deck
153
155
  slides-grab validate --slides-dir decks/my-deck
154
156
  slides-grab pdf --slides-dir decks/my-deck --output decks/my-deck.pdf
155
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
156
159
  slides-grab convert --slides-dir decks/my-deck --output decks/my-deck.pptx
157
160
  slides-grab figma --slides-dir decks/my-deck --output decks/my-deck-figma.pptx
158
161
  ```
159
162
 
160
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.
161
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
+
162
178
  ### Tldraw Diagram Assets
163
179
 
164
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:
package/bin/ppt-agent.js CHANGED
@@ -87,8 +87,9 @@ program
87
87
  .command('build-viewer')
88
88
  .description('Build viewer.html from slide HTML files')
89
89
  .option('--slides-dir <path>', 'Slide directory', 'slides')
90
+ .option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
90
91
  .action(async (options = {}) => {
91
- const args = ['--slides-dir', options.slidesDir];
92
+ const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
92
93
  await runCommand('scripts/build-viewer.js', args);
93
94
  });
94
95
 
@@ -98,9 +99,10 @@ program
98
99
  .description('Run structured validation on slide HTML files (Playwright-based)')
99
100
  .option('--slides-dir <path>', 'Slide directory', 'slides')
100
101
  .option('--format <format>', 'Output format: concise, json, json-full', 'concise')
102
+ .option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
101
103
  .option('--slide <file>', 'Validate only the named slide file (repeatable)', collectRepeatedOption, [])
102
104
  .action(async (options = {}) => {
103
- const args = ['--slides-dir', options.slidesDir, '--format', options.format];
105
+ const args = ['--slides-dir', options.slidesDir, '--format', options.format, '--mode', options.mode];
104
106
  for (const slide of options.slide || []) {
105
107
  args.push('--slide', String(slide));
106
108
  }
@@ -112,9 +114,10 @@ program
112
114
  .description('Convert slide HTML files to experimental / unstable PPTX')
113
115
  .option('--slides-dir <path>', 'Slide directory', 'slides')
114
116
  .option('--output <path>', 'Output PPTX file')
117
+ .option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
115
118
  .option('--resolution <preset>', 'Raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p)')
116
119
  .action(async (options = {}) => {
117
- const args = ['--slides-dir', options.slidesDir];
120
+ const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
118
121
  if (options.output) {
119
122
  args.push('--output', String(options.output));
120
123
  }
@@ -130,6 +133,7 @@ program
130
133
  .option('--slides-dir <path>', 'Slide directory', 'slides')
131
134
  .option('--output <path>', 'Output PDF file')
132
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')
133
137
  .option('--resolution <preset>', 'Capture raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p in capture mode)')
134
138
  .action(async (options = {}) => {
135
139
  const args = ['--slides-dir', options.slidesDir];
@@ -139,12 +143,36 @@ program
139
143
  if (options.mode) {
140
144
  args.push('--mode', String(options.mode));
141
145
  }
146
+ if (options.slideMode) {
147
+ args.push('--slide-mode', String(options.slideMode));
148
+ }
142
149
  if (options.resolution) {
143
150
  args.push('--resolution', String(options.resolution));
144
151
  }
145
152
  await runCommand('scripts/html2pdf.js', args);
146
153
  });
147
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
+
148
176
  program
149
177
  .command('fetch-video')
150
178
  .description('Download a video into <slides-dir>/assets via yt-dlp and print the ./assets reference')
@@ -165,9 +193,10 @@ program
165
193
  .helpOption('-h, --help', 'Show this help message')
166
194
  .option('--slides-dir <path>', 'Slide directory', 'slides')
167
195
  .option('--output <path>', 'Output PPTX file (default: <slides-dir>-figma.pptx)')
196
+ .option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
168
197
  .addHelpText('after', figmaHelpText)
169
198
  .action(async (options = {}) => {
170
- const args = ['--slides-dir', options.slidesDir];
199
+ const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
171
200
  if (options.output) {
172
201
  args.push('--output', String(options.output));
173
202
  }
@@ -222,8 +251,9 @@ program
222
251
  .description('Start interactive slide editor with Codex image-based edit flow')
223
252
  .option('--port <number>', 'Server port')
224
253
  .option('--slides-dir <path>', 'Slide directory', 'slides')
254
+ .option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
225
255
  .action(async (options = {}) => {
226
- const args = ['--slides-dir', options.slidesDir];
256
+ const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
227
257
  if (options.port) {
228
258
  args.push('--port', String(options.port));
229
259
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slides-grab",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
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",
@@ -53,18 +53,18 @@
53
53
  "build-viewer": "node scripts/build-viewer.js",
54
54
  "validate": "node scripts/validate-slides.js",
55
55
  "convert": "node convert.cjs",
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/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",
57
57
  "test:e2e": "node --test tests/editor/editor-ui.e2e.test.js tests/editor/editor-concurrency.e2e.test.js"
58
58
  },
59
59
  "dependencies": {
60
60
  "commander": "^12.1.0",
61
61
  "express": "^5.2.1",
62
+ "lucide-react": "^1.7.0",
62
63
  "pdf-lib": "^1.17.1",
63
64
  "playwright": "^1.40.0",
64
65
  "pptxgenjs": "^3.12.0",
65
66
  "react": "^19.2.4",
66
67
  "react-dom": "^19.2.4",
67
- "react-icons": "^5.0.0",
68
68
  "sharp": "^0.33.0",
69
69
  "tldraw": "^4.4.1"
70
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
  }
@@ -3,12 +3,12 @@
3
3
  import { readdir, readFile, writeFile, mkdtemp, rm, mkdir } from 'node:fs/promises';
4
4
  import { watch as fsWatch } from 'node:fs';
5
5
  import net from 'node:net';
6
+ import { createRequire } from 'node:module';
6
7
  import { basename, dirname, join, resolve, relative, sep } from 'node:path';
7
8
  import { fileURLToPath } from 'node:url';
8
9
  import { tmpdir } from 'node:os';
9
10
 
10
11
  import {
11
- SLIDE_SIZE,
12
12
  buildCodexEditPrompt,
13
13
  buildCodexExecArgs,
14
14
  buildClaudeExecArgs,
@@ -24,6 +24,14 @@ import {
24
24
  } from '../src/editor/edit-subprocess.js';
25
25
  import { buildSlideRuntimeHtml } from '../src/image-contract.js';
26
26
 
27
+ const require = createRequire(import.meta.url);
28
+ const {
29
+ DEFAULT_SLIDE_MODE,
30
+ getSlideModeChoices,
31
+ getSlideModeConfig,
32
+ normalizeSlideMode,
33
+ } = require('../src/slide-mode.cjs');
34
+
27
35
  const __filename = fileURLToPath(import.meta.url);
28
36
  const __dirname = dirname(__filename);
29
37
  const PACKAGE_ROOT = process.env.PPT_AGENT_PACKAGE_ROOT || resolve(__dirname, '..');
@@ -58,6 +66,7 @@ function printUsage() {
58
66
  process.stdout.write(`Options:\n`);
59
67
  process.stdout.write(` --port <number> Server port (default: ${DEFAULT_PORT})\n`);
60
68
  process.stdout.write(` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})\n`);
69
+ process.stdout.write(` --mode <mode> Slide mode: ${getSlideModeChoices().join(', ')} (default: ${DEFAULT_SLIDE_MODE})\n`);
61
70
  process.stdout.write(` Model is selected in editor UI dropdown.\n`);
62
71
  process.stdout.write(` -h, --help Show this help message\n`);
63
72
  }
@@ -66,6 +75,7 @@ function parseArgs(argv) {
66
75
  const opts = {
67
76
  port: DEFAULT_PORT,
68
77
  slidesDir: DEFAULT_SLIDES_DIR,
78
+ mode: DEFAULT_SLIDE_MODE,
69
79
  help: false,
70
80
  };
71
81
 
@@ -98,6 +108,17 @@ function parseArgs(argv) {
98
108
  continue;
99
109
  }
100
110
 
111
+ if (arg === '--mode') {
112
+ opts.mode = normalizeSlideMode(argv[i + 1]);
113
+ i += 1;
114
+ continue;
115
+ }
116
+
117
+ if (arg.startsWith('--mode=')) {
118
+ opts.mode = normalizeSlideMode(arg.slice('--mode='.length));
119
+ continue;
120
+ }
121
+
101
122
  if (arg === '--codex-model') {
102
123
  // Backward compatibility: ignore legacy CLI option.
103
124
  i += 1;
@@ -116,6 +137,7 @@ function parseArgs(argv) {
116
137
  }
117
138
 
118
139
  opts.slidesDir = opts.slidesDir.trim();
140
+ opts.mode = normalizeSlideMode(opts.mode);
119
141
 
120
142
  return opts;
121
143
  }
@@ -202,9 +224,9 @@ async function closeBrowser() {
202
224
  }
203
225
  }
204
226
 
205
- async function withScreenshotPage(callback) {
227
+ async function withScreenshotPage(callback, screenshotSize) {
206
228
  const { browser } = await getScreenshotBrowser();
207
- const { context, page } = await screenshotMod.createScreenshotPage(browser);
229
+ const { context, page } = await screenshotMod.createScreenshotPage(browser, screenshotSize);
208
230
  try {
209
231
  return await callback(page);
210
232
  } finally {
@@ -264,7 +286,7 @@ function sanitizeTargets(rawTargets) {
264
286
  .filter((target) => target.xpath);
265
287
  }
266
288
 
267
- function normalizeSelections(rawSelections) {
289
+ function normalizeSelections(rawSelections, slideSize) {
268
290
  if (!Array.isArray(rawSelections) || rawSelections.length === 0) {
269
291
  throw new Error('At least one selection is required.');
270
292
  }
@@ -274,7 +296,7 @@ function normalizeSelections(rawSelections) {
274
296
  ? selection.bbox
275
297
  : selection;
276
298
 
277
- const bbox = normalizeSelection(selectionSource, SLIDE_SIZE);
299
+ const bbox = normalizeSelection(selectionSource, slideSize);
278
300
  const targets = sanitizeTargets(selection?.targets);
279
301
 
280
302
  return { bbox, targets };
@@ -562,6 +584,18 @@ async function startServer(opts) {
562
584
  }
563
585
  });
564
586
 
587
+ app.get('/api/config', (_req, res) => {
588
+ const cfg = getSlideModeConfig(opts.mode);
589
+ res.json({
590
+ slideMode: opts.mode,
591
+ framePx: { width: cfg.framePx.width, height: cfg.framePx.height },
592
+ screenshotPx: { width: cfg.screenshotPx.width, height: cfg.screenshotPx.height },
593
+ sizeLabel: cfg.sizeLabel,
594
+ aspectRatioLabel: cfg.aspectRatioLabel,
595
+ coordinateSpaceLabel: cfg.coordinateSpaceLabel,
596
+ });
597
+ });
598
+
565
599
  app.get('/api/models', (_req, res) => {
566
600
  res.json({
567
601
  models: ALL_MODELS,
@@ -630,7 +664,7 @@ async function startServer(opts) {
630
664
 
631
665
  let normalizedSelections;
632
666
  try {
633
- normalizedSelections = normalizeSelections(selections);
667
+ normalizedSelections = normalizeSelections(selections, getSlideModeConfig(opts.mode).framePx);
634
668
  } catch (error) {
635
669
  return res.status(400).json({ error: error.message });
636
670
  }
@@ -665,15 +699,15 @@ async function startServer(opts) {
665
699
  slide,
666
700
  screenshotPath,
667
701
  `http://localhost:${opts.port}/slides`,
668
- { useHttp: true },
702
+ { useHttp: true, screenshotSize: getSlideModeConfig(opts.mode).screenshotPx },
669
703
  );
670
- });
704
+ }, getSlideModeConfig(opts.mode).screenshotPx);
671
705
 
672
706
  const scaledBoxes = normalizedSelections.map((selection) =>
673
707
  scaleSelectionToScreenshot(
674
708
  selection.bbox,
675
- SLIDE_SIZE,
676
- screenshotMod.SCREENSHOT_SIZE,
709
+ getSlideModeConfig(opts.mode).framePx,
710
+ getSlideModeConfig(opts.mode).screenshotPx,
677
711
  ),
678
712
  );
679
713
 
@@ -683,6 +717,7 @@ async function startServer(opts) {
683
717
  slideFile: slide,
684
718
  slidePath: toSlidePathLabel(slidesDirectory, slide),
685
719
  userPrompt: prompt,
720
+ slideMode: opts.mode,
686
721
  selections: normalizedSelections,
687
722
  });
688
723
 
@@ -17,6 +17,7 @@ import {
17
17
 
18
18
  const require = createRequire(import.meta.url);
19
19
  const html2pptx = require('../src/html2pptx.cjs');
20
+ const { DEFAULT_SLIDE_MODE, getSlideModeChoices, normalizeSlideMode } = require('../src/slide-mode.cjs');
20
21
 
21
22
  const DEFAULT_SLIDES_DIR = 'slides';
22
23
 
@@ -28,6 +29,7 @@ function printUsage() {
28
29
  'Options:',
29
30
  ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
30
31
  ' --output <path> Output PPTX file (default: <slides-dir>-figma.pptx)',
32
+ ` --mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
31
33
  ' -h, --help Show this help message',
32
34
  '',
33
35
  'Exports an experimental / unstable Figma Slides importable PPTX using the existing html2pptx pipeline.',
@@ -49,6 +51,7 @@ function parseArgs(args) {
49
51
  const options = {
50
52
  slidesDir: DEFAULT_SLIDES_DIR,
51
53
  output: '',
54
+ mode: DEFAULT_SLIDE_MODE,
52
55
  help: false,
53
56
  };
54
57
 
@@ -81,6 +84,17 @@ function parseArgs(args) {
81
84
  continue;
82
85
  }
83
86
 
87
+ if (arg === '--mode') {
88
+ options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
89
+ i += 1;
90
+ continue;
91
+ }
92
+
93
+ if (arg.startsWith('--mode=')) {
94
+ options.mode = normalizeSlideMode(arg.slice('--mode='.length));
95
+ continue;
96
+ }
97
+
84
98
  throw new Error(`Unknown option: ${arg}`);
85
99
  }
86
100
 
@@ -89,6 +103,7 @@ function parseArgs(args) {
89
103
  }
90
104
 
91
105
  options.slidesDir = options.slidesDir.trim();
106
+ options.mode = normalizeSlideMode(options.mode);
92
107
  options.output = normalizeFigmaOutput(options.slidesDir, options.output);
93
108
  return options;
94
109
  }
@@ -121,7 +136,7 @@ async function main() {
121
136
  const files = getHtmlSlides(slidesDir);
122
137
 
123
138
  const pres = new PptxGenJS();
124
- configureFigmaExportPresentation(pres);
139
+ configureFigmaExportPresentation(pres, options.mode);
125
140
 
126
141
  console.log(`Exporting ${files.length} slide(s) for Figma from ${slidesDir}`);
127
142