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
@@ -2,12 +2,13 @@
2
2
 
3
3
  import { readdir, readFile, writeFile, mkdtemp, rm, mkdir } from 'node:fs/promises';
4
4
  import { watch as fsWatch } from 'node:fs';
5
+ import net from 'node:net';
6
+ import { createRequire } from 'node:module';
5
7
  import { basename, dirname, join, resolve, relative, sep } from 'node:path';
6
8
  import { fileURLToPath } from 'node:url';
7
9
  import { tmpdir } from 'node:os';
8
10
 
9
11
  import {
10
- SLIDE_SIZE,
11
12
  buildCodexEditPrompt,
12
13
  buildCodexExecArgs,
13
14
  buildClaudeExecArgs,
@@ -23,6 +24,14 @@ import {
23
24
  } from '../src/editor/edit-subprocess.js';
24
25
  import { buildSlideRuntimeHtml } from '../src/image-contract.js';
25
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
+
26
35
  const __filename = fileURLToPath(import.meta.url);
27
36
  const __dirname = dirname(__filename);
28
37
  const PACKAGE_ROOT = process.env.PPT_AGENT_PACKAGE_ROOT || resolve(__dirname, '..');
@@ -45,6 +54,8 @@ const CODEX_MODELS = ['gpt-5.4', 'gpt-5.3-codex', 'gpt-5.3-codex-spark'];
45
54
  const ALL_MODELS = [...CODEX_MODELS, ...CLAUDE_MODELS];
46
55
  const DEFAULT_CODEX_MODEL = CODEX_MODELS[0];
47
56
  const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
57
+ const PORT_PROBE_HOSTS = ['::', '127.0.0.1'];
58
+ const PORT_PROBE_IGNORED_CODES = new Set(['EAFNOSUPPORT', 'EADDRNOTAVAIL']);
48
59
 
49
60
  const MAX_RUNS = 200;
50
61
  const MAX_LOG_CHARS = 800_000;
@@ -55,6 +66,7 @@ function printUsage() {
55
66
  process.stdout.write(`Options:\n`);
56
67
  process.stdout.write(` --port <number> Server port (default: ${DEFAULT_PORT})\n`);
57
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`);
58
70
  process.stdout.write(` Model is selected in editor UI dropdown.\n`);
59
71
  process.stdout.write(` -h, --help Show this help message\n`);
60
72
  }
@@ -63,6 +75,7 @@ function parseArgs(argv) {
63
75
  const opts = {
64
76
  port: DEFAULT_PORT,
65
77
  slidesDir: DEFAULT_SLIDES_DIR,
78
+ mode: DEFAULT_SLIDE_MODE,
66
79
  help: false,
67
80
  };
68
81
 
@@ -95,6 +108,17 @@ function parseArgs(argv) {
95
108
  continue;
96
109
  }
97
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
+
98
122
  if (arg === '--codex-model') {
99
123
  // Backward compatibility: ignore legacy CLI option.
100
124
  i += 1;
@@ -113,10 +137,67 @@ function parseArgs(argv) {
113
137
  }
114
138
 
115
139
  opts.slidesDir = opts.slidesDir.trim();
140
+ opts.mode = normalizeSlideMode(opts.mode);
116
141
 
117
142
  return opts;
118
143
  }
119
144
 
145
+ function buildPortInUseError(port) {
146
+ return new Error(`Editor port ${port} is already in use. Choose another port with \`--port <number>\` and try again.`);
147
+ }
148
+
149
+ async function assertHostPortAvailable(port, host) {
150
+ const probe = net.createServer();
151
+ try {
152
+ await new Promise((resolve, reject) => {
153
+ probe.once('error', reject);
154
+ probe.listen({ port, host, exclusive: true }, resolve);
155
+ });
156
+ } catch (error) {
157
+ if (error?.code === 'EADDRINUSE') {
158
+ throw buildPortInUseError(port);
159
+ }
160
+
161
+ if (PORT_PROBE_IGNORED_CODES.has(error?.code)) {
162
+ return;
163
+ }
164
+
165
+ throw error;
166
+ } finally {
167
+ if (probe.listening) {
168
+ await new Promise((resolve, reject) => {
169
+ probe.close((error) => {
170
+ if (error) {
171
+ reject(error);
172
+ return;
173
+ }
174
+ resolve();
175
+ });
176
+ });
177
+ }
178
+ }
179
+ }
180
+
181
+ async function assertPortUsable(port) {
182
+ for (const host of PORT_PROBE_HOSTS) {
183
+ await assertHostPortAvailable(port, host);
184
+ }
185
+ }
186
+
187
+ async function listenOnPort(app, port) {
188
+ return new Promise((resolve, reject) => {
189
+ const server = app.listen(port, () => resolve(server));
190
+ server.once('error', (error) => {
191
+ if (error?.code === 'EADDRINUSE') {
192
+ reject(buildPortInUseError(port));
193
+ return;
194
+ }
195
+
196
+ reject(error);
197
+ });
198
+ });
199
+ }
200
+
120
201
  const sseClients = new Set();
121
202
 
122
203
  function broadcastSSE(event, data) {
@@ -143,9 +224,9 @@ async function closeBrowser() {
143
224
  }
144
225
  }
145
226
 
146
- async function withScreenshotPage(callback) {
227
+ async function withScreenshotPage(callback, screenshotSize) {
147
228
  const { browser } = await getScreenshotBrowser();
148
- const { context, page } = await screenshotMod.createScreenshotPage(browser);
229
+ const { context, page } = await screenshotMod.createScreenshotPage(browser, screenshotSize);
149
230
  try {
150
231
  return await callback(page);
151
232
  } finally {
@@ -205,7 +286,7 @@ function sanitizeTargets(rawTargets) {
205
286
  .filter((target) => target.xpath);
206
287
  }
207
288
 
208
- function normalizeSelections(rawSelections) {
289
+ function normalizeSelections(rawSelections, slideSize) {
209
290
  if (!Array.isArray(rawSelections) || rawSelections.length === 0) {
210
291
  throw new Error('At least one selection is required.');
211
292
  }
@@ -215,7 +296,7 @@ function normalizeSelections(rawSelections) {
215
296
  ? selection.bbox
216
297
  : selection;
217
298
 
218
- const bbox = normalizeSelection(selectionSource, SLIDE_SIZE);
299
+ const bbox = normalizeSelection(selectionSource, slideSize);
219
300
  const targets = sanitizeTargets(selection?.targets);
220
301
 
221
302
  return { bbox, targets };
@@ -392,6 +473,7 @@ function createRunStore() {
392
473
  }
393
474
 
394
475
  async function startServer(opts) {
476
+ await assertPortUsable(opts.port);
395
477
  await loadDeps();
396
478
  const slidesDirectory = resolve(process.cwd(), opts.slidesDir);
397
479
  await mkdir(slidesDirectory, { recursive: true });
@@ -502,6 +584,18 @@ async function startServer(opts) {
502
584
  }
503
585
  });
504
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
+
505
599
  app.get('/api/models', (_req, res) => {
506
600
  res.json({
507
601
  models: ALL_MODELS,
@@ -570,7 +664,7 @@ async function startServer(opts) {
570
664
 
571
665
  let normalizedSelections;
572
666
  try {
573
- normalizedSelections = normalizeSelections(selections);
667
+ normalizedSelections = normalizeSelections(selections, getSlideModeConfig(opts.mode).framePx);
574
668
  } catch (error) {
575
669
  return res.status(400).json({ error: error.message });
576
670
  }
@@ -605,15 +699,15 @@ async function startServer(opts) {
605
699
  slide,
606
700
  screenshotPath,
607
701
  `http://localhost:${opts.port}/slides`,
608
- { useHttp: true },
702
+ { useHttp: true, screenshotSize: getSlideModeConfig(opts.mode).screenshotPx },
609
703
  );
610
- });
704
+ }, getSlideModeConfig(opts.mode).screenshotPx);
611
705
 
612
706
  const scaledBoxes = normalizedSelections.map((selection) =>
613
707
  scaleSelectionToScreenshot(
614
708
  selection.bbox,
615
- SLIDE_SIZE,
616
- screenshotMod.SCREENSHOT_SIZE,
709
+ getSlideModeConfig(opts.mode).framePx,
710
+ getSlideModeConfig(opts.mode).screenshotPx,
617
711
  ),
618
712
  );
619
713
 
@@ -623,6 +717,7 @@ async function startServer(opts) {
623
717
  slideFile: slide,
624
718
  slidePath: toSlidePathLabel(slidesDirectory, slide),
625
719
  userPrompt: prompt,
720
+ slideMode: opts.mode,
626
721
  selections: normalizedSelections,
627
722
  });
628
723
 
@@ -708,14 +803,14 @@ async function startServer(opts) {
708
803
  }, 300);
709
804
  });
710
805
 
711
- const server = app.listen(opts.port, () => {
712
- process.stdout.write('\n slides-grab editor\n');
713
- process.stdout.write(' ─────────────────────────────────────\n');
714
- process.stdout.write(` Local: http://localhost:${opts.port}\n`);
715
- process.stdout.write(` Models: ${ALL_MODELS.join(', ')}\n`);
716
- process.stdout.write(` Slides: ${slidesDirectory}\n`);
717
- process.stdout.write(' ─────────────────────────────────────\n\n');
718
- });
806
+ const server = await listenOnPort(app, opts.port);
807
+
808
+ process.stdout.write('\n slides-grab editor\n');
809
+ process.stdout.write(' ─────────────────────────────────────\n');
810
+ process.stdout.write(` Local: http://localhost:${opts.port}\n`);
811
+ process.stdout.write(` Models: ${ALL_MODELS.join(', ')}\n`);
812
+ process.stdout.write(` Slides: ${slidesDirectory}\n`);
813
+ process.stdout.write(' ─────────────────────────────────────\n\n');
719
814
 
720
815
  async function shutdown() {
721
816
  process.stdout.write('\n[editor] Shutting down...\n');
@@ -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
 
@@ -16,6 +16,12 @@ const {
16
16
  getResolutionSize,
17
17
  normalizeResolutionPreset,
18
18
  } = require('../src/export-resolution.cjs');
19
+ const {
20
+ DEFAULT_SLIDE_MODE,
21
+ getSlideModeChoices,
22
+ getSlideModeConfig,
23
+ normalizeSlideMode,
24
+ } = require('../src/slide-mode.cjs');
19
25
 
20
26
  const DEFAULT_OUTPUT = 'slides.pdf';
21
27
  const DEFAULT_SLIDES_DIR = 'slides';
@@ -23,9 +29,7 @@ const DEFAULT_MODE = 'capture';
23
29
  const DEFAULT_CAPTURE_RESOLUTION = '2160p';
24
30
  const PDF_MODES = new Set(['capture', 'print']);
25
31
  const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
26
- const FALLBACK_SLIDE_SIZE = { width: 960, height: 540 };
27
32
  const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
28
- const TARGET_ASPECT_RATIO = 16 / 9;
29
33
  const RENDER_SETTLE_MS = 120;
30
34
  const CSS_PIXELS_PER_INCH = 96;
31
35
  const PDF_POINTS_PER_INCH = 72;
@@ -40,6 +44,7 @@ function printUsage() {
40
44
  ` --output <path> Output PDF path (default: ${DEFAULT_OUTPUT})`,
41
45
  ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
42
46
  ` --mode <mode> PDF export mode: capture|print (default: ${DEFAULT_MODE})`,
47
+ ` --slide-mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
43
48
  ` --resolution <preset> Capture raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_CAPTURE_RESOLUTION}; ignored in print mode)`,
44
49
  ' -h, --help Show this help message',
45
50
  '',
@@ -89,8 +94,8 @@ function cssPixelsToPdfPoints(value) {
89
94
  return Math.round((normalizeDimension(value, 0) * PDF_POINTS_PER_INCH) / CSS_PIXELS_PER_INCH);
90
95
  }
91
96
 
92
- async function normalizeCaptureRasterSize(pngBytes, resolution = '') {
93
- const targetSize = getResolutionSize(resolution);
97
+ async function normalizeCaptureRasterSize(pngBytes, resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
98
+ const targetSize = getResolutionSize(resolution, slideMode);
94
99
  if (!targetSize) {
95
100
  return pngBytes;
96
101
  }
@@ -140,6 +145,7 @@ export function parseCliArgs(args) {
140
145
  output: DEFAULT_OUTPUT,
141
146
  slidesDir: DEFAULT_SLIDES_DIR,
142
147
  mode: DEFAULT_MODE,
148
+ slideMode: DEFAULT_SLIDE_MODE,
143
149
  resolution: DEFAULT_CAPTURE_RESOLUTION,
144
150
  help: false,
145
151
  };
@@ -185,6 +191,17 @@ export function parseCliArgs(args) {
185
191
  continue;
186
192
  }
187
193
 
194
+ if (arg === '--slide-mode') {
195
+ options.slideMode = normalizeSlideMode(readOptionValue(args, i, '--slide-mode'), { optionName: '--slide-mode' });
196
+ i += 1;
197
+ continue;
198
+ }
199
+
200
+ if (arg.startsWith('--slide-mode=')) {
201
+ options.slideMode = normalizeSlideMode(arg.slice('--slide-mode='.length), { optionName: '--slide-mode' });
202
+ continue;
203
+ }
204
+
188
205
  if (arg === '--resolution') {
189
206
  options.resolution = normalizeResolutionPreset(readOptionValue(args, i, '--resolution'));
190
207
  i += 1;
@@ -209,6 +226,7 @@ export function parseCliArgs(args) {
209
226
  options.output = options.output.trim();
210
227
  options.slidesDir = options.slidesDir.trim();
211
228
  options.mode = normalizeMode(options.mode);
229
+ options.slideMode = normalizeSlideMode(options.slideMode, { optionName: '--slide-mode' });
212
230
  options.resolution = normalizeResolutionPreset(options.resolution);
213
231
  if (options.mode === 'print') {
214
232
  options.resolution = '';
@@ -226,9 +244,10 @@ export async function findSlideFiles(slidesDir = resolve(process.cwd(), DEFAULT_
226
244
  }
227
245
 
228
246
  export function buildPdfOptions(widthPx, heightPx) {
247
+ const fallbackSize = getSlideModeConfig(DEFAULT_SLIDE_MODE).framePx;
229
248
  return {
230
- width: `${normalizeDimension(widthPx, FALLBACK_SLIDE_SIZE.width)}px`,
231
- height: `${normalizeDimension(heightPx, FALLBACK_SLIDE_SIZE.height)}px`,
249
+ width: `${normalizeDimension(widthPx, fallbackSize.width)}px`,
250
+ height: `${normalizeDimension(heightPx, fallbackSize.height)}px`,
232
251
  printBackground: true,
233
252
  pageRanges: '1',
234
253
  margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' },
@@ -236,22 +255,25 @@ export function buildPdfOptions(widthPx, heightPx) {
236
255
  };
237
256
  }
238
257
 
239
- export function buildPageOptions(mode = DEFAULT_MODE, resolution = '') {
240
- const targetResolution = normalizeMode(mode) === 'capture' ? getResolutionSize(resolution) : null;
258
+ export function buildPageOptions(mode = DEFAULT_MODE, resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
259
+ const { framePx } = getSlideModeConfig(slideMode);
260
+ const targetResolution = normalizeMode(mode) === 'capture' ? getResolutionSize(resolution, slideMode) : null;
241
261
  return {
242
262
  viewport: {
243
- width: FALLBACK_SLIDE_SIZE.width,
244
- height: FALLBACK_SLIDE_SIZE.height,
263
+ width: framePx.width,
264
+ height: framePx.height,
245
265
  },
246
266
  deviceScaleFactor: normalizeMode(mode) === 'capture'
247
267
  ? targetResolution
248
- ? targetResolution.height / FALLBACK_SLIDE_SIZE.height
268
+ ? targetResolution.height / framePx.height
249
269
  : DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR
250
270
  : 1,
251
271
  };
252
272
  }
253
273
 
254
- function chooseSlideFrame(metrics) {
274
+ function chooseSlideFrame(metrics, slideMode = DEFAULT_SLIDE_MODE) {
275
+ const { framePx } = getSlideModeConfig(slideMode);
276
+ const targetAspectRatio = framePx.width / framePx.height;
255
277
  const viewportArea = Math.max(1, metrics.viewport.width * metrics.viewport.height);
256
278
  const bodyArea = Math.max(1, metrics.body.width * metrics.body.height);
257
279
  const bodyScrollArea = Math.max(1, metrics.body.scrollWidth * metrics.body.scrollHeight);
@@ -269,7 +291,7 @@ function chooseSlideFrame(metrics) {
269
291
  .map((candidate) => ({
270
292
  ...candidate,
271
293
  area: candidate.width * candidate.height,
272
- aspectDelta: Math.abs(candidate.width / candidate.height - TARGET_ASPECT_RATIO),
294
+ aspectDelta: Math.abs(candidate.width / candidate.height - targetAspectRatio),
273
295
  coverage: (candidate.width * candidate.height) / viewportArea,
274
296
  }))
275
297
  .sort((left, right) => right.area - left.area);
@@ -359,7 +381,7 @@ export async function waitForSlideRenderReady(page, options = {}) {
359
381
  }, { settleMs, runReadySignal: shouldRunReadySignal });
360
382
  }
361
383
 
362
- export async function detectSlideFrame(page) {
384
+ export async function detectSlideFrame(page, slideMode = DEFAULT_SLIDE_MODE) {
363
385
  const metrics = await page.evaluate(() => {
364
386
  function toBox(element) {
365
387
  const rect = element.getBoundingClientRect();
@@ -398,12 +420,13 @@ export async function detectSlideFrame(page) {
398
420
  };
399
421
  });
400
422
 
401
- const frame = chooseSlideFrame(metrics);
423
+ const frame = chooseSlideFrame(metrics, slideMode);
424
+ const fallbackSize = getSlideModeConfig(slideMode).framePx;
402
425
  return {
403
426
  x: normalizeDimension(frame.x, 0),
404
427
  y: normalizeDimension(frame.y, 0),
405
- width: normalizeDimension(frame.width, FALLBACK_SLIDE_SIZE.width),
406
- height: normalizeDimension(frame.height, FALLBACK_SLIDE_SIZE.height),
428
+ width: normalizeDimension(frame.width, fallbackSize.width),
429
+ height: normalizeDimension(frame.height, fallbackSize.height),
407
430
  candidateIndex: Number.isInteger(frame.candidateIndex) ? frame.candidateIndex : null,
408
431
  source: frame.source,
409
432
  };
@@ -641,20 +664,22 @@ export async function renderSlideToPdf(page, slideFile, slidesDir, options = {})
641
664
  const slidePath = join(slidesDir, slideFile);
642
665
  const slideUrl = pathToFileURL(slidePath).href;
643
666
  const mode = normalizeMode(options.mode ?? DEFAULT_MODE);
667
+ const slideMode = normalizeSlideMode(options.slideMode ?? DEFAULT_SLIDE_MODE, { optionName: '--slide-mode' });
644
668
  const captureResolution = mode === 'capture' ? normalizeResolutionPreset(options.resolution ?? '') : '';
645
669
 
646
670
  await page.goto(slideUrl, { waitUntil: 'load' });
647
671
  await waitForSlideRenderReady(page, options);
648
672
 
649
- const slideFrame = await detectSlideFrame(page);
673
+ const slideFrame = await detectSlideFrame(page, slideMode);
650
674
  const normalizedSlideFrame = await isolateSlideFrame(page, slideFrame);
651
675
  await normalizeBodyToSlideFrame(page, normalizedSlideFrame);
652
676
  await waitForSlideRenderReady(page, { ...options, runReadySignal: false });
653
677
 
654
678
  if (mode === 'capture') {
679
+ const fallbackSize = getSlideModeConfig(slideMode).framePx;
655
680
  const viewportSize = {
656
- width: normalizeDimension(normalizedSlideFrame.width, FALLBACK_SLIDE_SIZE.width),
657
- height: normalizeDimension(normalizedSlideFrame.height, FALLBACK_SLIDE_SIZE.height),
681
+ width: normalizeDimension(normalizedSlideFrame.width, fallbackSize.width),
682
+ height: normalizeDimension(normalizedSlideFrame.height, fallbackSize.height),
658
683
  };
659
684
  await page.setViewportSize(viewportSize);
660
685
  await waitForSlideRenderReady(page, { ...options, runReadySignal: false });
@@ -679,9 +704,10 @@ export async function renderSlideToPdf(page, slideFile, slidesDir, options = {})
679
704
  height: viewportSize.height,
680
705
  },
681
706
  });
682
- const normalizedPngBytes = await normalizeCaptureRasterSize(pngBytes, captureResolution);
707
+ const normalizedPngBytes = await normalizeCaptureRasterSize(pngBytes, captureResolution, slideMode);
683
708
  return {
684
709
  mode,
710
+ slideMode,
685
711
  width: normalizedSlideFrame.width,
686
712
  height: normalizedSlideFrame.height,
687
713
  pngBytes: normalizedPngBytes,
@@ -692,6 +718,7 @@ export async function renderSlideToPdf(page, slideFile, slidesDir, options = {})
692
718
 
693
719
  return {
694
720
  mode,
721
+ slideMode,
695
722
  width: normalizedSlideFrame.width,
696
723
  height: normalizedSlideFrame.height,
697
724
  pdfBytes: await page.pdf(buildPdfOptions(normalizedSlideFrame.width, normalizedSlideFrame.height)),
@@ -740,14 +767,14 @@ async function main() {
740
767
  }
741
768
 
742
769
  const slidesDir = resolve(process.cwd(), options.slidesDir);
743
- await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PDF export' });
770
+ await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PDF export', slideMode: options.slideMode });
744
771
  const slideFiles = await findSlideFiles(slidesDir);
745
772
  if (slideFiles.length === 0) {
746
773
  throw new Error(`No slide-*.html files found in: ${slidesDir}`);
747
774
  }
748
775
 
749
776
  const browser = await chromium.launch({ headless: true });
750
- const page = await browser.newPage(buildPageOptions(options.mode, options.resolution));
777
+ const page = await browser.newPage(buildPageOptions(options.mode, options.resolution, options.slideMode));
751
778
  const diagnostics = createSlideDiagnostics();
752
779
  diagnostics.attach(page);
753
780
  const renderedSlides = [];
@@ -758,6 +785,7 @@ async function main() {
758
785
  try {
759
786
  const slideResult = await renderSlideToPdf(page, slideFile, slidesDir, {
760
787
  mode: options.mode,
788
+ slideMode: options.slideMode,
761
789
  resolution: options.resolution,
762
790
  });
763
791
  renderedSlides.push(slideResult);
@@ -10,6 +10,7 @@ import { ensureOutputDirectory, SLIDE_FILE_PATTERN, sortFigmaSlideFiles } from '
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
12
  const html2pptx = require('../src/html2pptx.cjs');
13
+ const { DEFAULT_SLIDE_MODE, getSlideModeChoices, getSlideModeConfig, normalizeSlideMode } = require('../src/slide-mode.cjs');
13
14
 
14
15
  const DEFAULT_SLIDES_DIR = 'slides';
15
16
  const DEFAULT_OUTPUT = 'output.pptx';
@@ -22,6 +23,7 @@ function printUsage() {
22
23
  'Options:',
23
24
  ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
24
25
  ` --output <path> Output PPTX file (default: ${DEFAULT_OUTPUT})`,
26
+ ` --mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
25
27
  ' -h, --help Show this help message',
26
28
  '',
27
29
  'Experimental / unstable PPTX export. Treat output as best-effort only.',
@@ -42,6 +44,7 @@ function parseArgs(args) {
42
44
  const options = {
43
45
  slidesDir: DEFAULT_SLIDES_DIR,
44
46
  output: DEFAULT_OUTPUT,
47
+ mode: DEFAULT_SLIDE_MODE,
45
48
  help: false,
46
49
  };
47
50
 
@@ -74,6 +77,17 @@ function parseArgs(args) {
74
77
  continue;
75
78
  }
76
79
 
80
+ if (arg === '--mode') {
81
+ options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
82
+ i += 1;
83
+ continue;
84
+ }
85
+
86
+ if (arg.startsWith('--mode=')) {
87
+ options.mode = normalizeSlideMode(arg.slice('--mode='.length));
88
+ continue;
89
+ }
90
+
77
91
  throw new Error(`Unknown option: ${arg}`);
78
92
  }
79
93
 
@@ -87,6 +101,7 @@ function parseArgs(args) {
87
101
 
88
102
  options.slidesDir = options.slidesDir.trim();
89
103
  options.output = options.output.trim();
104
+ options.mode = normalizeSlideMode(options.mode);
90
105
  return options;
91
106
  }
92
107
 
@@ -118,7 +133,13 @@ async function main() {
118
133
  const files = getHtmlSlides(slidesDir);
119
134
 
120
135
  const pres = new PptxGenJS();
121
- pres.layout = 'LAYOUT_WIDE';
136
+ const { pptxSizeIn } = getSlideModeConfig(options.mode);
137
+ pres.defineLayout({
138
+ name: 'SLIDES_GRAB_HTML2PPTX',
139
+ width: pptxSizeIn.width,
140
+ height: pptxSizeIn.height,
141
+ });
142
+ pres.layout = 'SLIDES_GRAB_HTML2PPTX';
122
143
 
123
144
  for (const file of files) {
124
145
  await html2pptx(resolve(slidesDir, file), pres);
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { createRequire } from 'node:module';
5
6
  import { chromium } from 'playwright';
6
7
 
7
8
  import {
@@ -20,6 +21,9 @@ import {
20
21
  selectSlideFiles,
21
22
  } from '../src/validation/core.js';
22
23
 
24
+ const require = createRequire(import.meta.url);
25
+ const { DEFAULT_SLIDE_MODE } = require('../src/slide-mode.cjs');
26
+
23
27
  export {
24
28
  DEFAULT_SLIDES_DIR,
25
29
  ensureSlidesPassValidation,
@@ -160,7 +164,7 @@ function peekValidateFormat(args = []) {
160
164
  return DEFAULT_VALIDATE_FORMAT;
161
165
  }
162
166
 
163
- export async function validateSlides(slidesDir, { selectedSlides = [] } = {}) {
167
+ export async function validateSlides(slidesDir, { mode = DEFAULT_SLIDE_MODE, selectedSlides = [] } = {}) {
164
168
  const slideFiles = selectSlideFiles(await findSlideFiles(slidesDir), selectedSlides, slidesDir);
165
169
  if (slideFiles.length === 0) {
166
170
  throw new Error(`No slide-*.html files found in: ${slidesDir}`);
@@ -171,13 +175,26 @@ export async function validateSlides(slidesDir, { selectedSlides = [] } = {}) {
171
175
  const page = await context.newPage();
172
176
 
173
177
  try {
174
- const slides = await scanSlides(page, slidesDir, slideFiles);
175
- return createValidationResult(slides);
178
+ const slides = await scanSlides(page, slidesDir, slideFiles, mode);
179
+ return createValidationResult(slides, mode);
176
180
  } finally {
177
181
  await browser.close();
178
182
  }
179
183
  }
180
184
 
185
+ function peekValidateMode(args = []) {
186
+ for (let i = 0; i < args.length; i += 1) {
187
+ const arg = args[i];
188
+ if (arg === '--mode') {
189
+ return args[i + 1] || DEFAULT_SLIDE_MODE;
190
+ }
191
+ if (arg.startsWith('--mode=')) {
192
+ return arg.slice('--mode='.length) || DEFAULT_SLIDE_MODE;
193
+ }
194
+ }
195
+ return DEFAULT_SLIDE_MODE;
196
+ }
197
+
181
198
  export async function main(args = process.argv.slice(2)) {
182
199
  const options = parseValidateCliArgs(args);
183
200
  if (options.help) {
@@ -186,7 +203,7 @@ export async function main(args = process.argv.slice(2)) {
186
203
  }
187
204
 
188
205
  const slidesDir = resolve(process.cwd(), options.slidesDir);
189
- const result = await validateSlides(slidesDir, { selectedSlides: options.slides });
206
+ const result = await validateSlides(slidesDir, { mode: options.mode, selectedSlides: options.slides });
190
207
  process.stdout.write(formatValidationResult(result, options.format));
191
208
  if (result.summary.failedSlides > 0) {
192
209
  process.exitCode = 1;
@@ -197,7 +214,7 @@ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(imp
197
214
 
198
215
  if (isMain) {
199
216
  main().catch((error) => {
200
- const failure = createValidationFailure(error);
217
+ const failure = createValidationFailure(error, peekValidateMode(process.argv.slice(2)));
201
218
  process.stdout.write(formatValidationResult(failure, peekValidateFormat(process.argv.slice(2))));
202
219
  process.exit(1);
203
220
  });