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
@@ -9,14 +9,19 @@ const {
9
9
  getResolutionSize,
10
10
  normalizeResolutionPreset,
11
11
  } = require('./export-resolution.cjs');
12
+ const {
13
+ DEFAULT_SLIDE_MODE,
14
+ getSlideModeChoices,
15
+ getSlideModeConfig,
16
+ normalizeSlideMode,
17
+ } = require('./slide-mode.cjs');
12
18
 
13
19
  const DEFAULT_SLIDES_DIR = 'slides';
14
20
  const DEFAULT_OUTPUT = 'output.pptx';
15
21
  const DEFAULT_RESOLUTION = '2160p';
16
- const DEFAULT_CAPTURE_VIEWPORT = { width: 960, height: 540 };
17
22
  const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
18
23
  const TARGET_RASTER_DPI = 150;
19
- const TARGET_SLIDE_SIZE_IN = { width: 13.33, height: 7.5 };
24
+ const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
20
25
 
21
26
  function normalizeDimension(value, fallback) {
22
27
  if (!Number.isFinite(value) || value <= 0) {
@@ -25,31 +30,63 @@ function normalizeDimension(value, fallback) {
25
30
  return Math.max(1, Math.round(value));
26
31
  }
27
32
 
28
- function buildPageOptions(resolution = '') {
29
- const targetResolution = getResolutionSize(resolution);
33
+ function buildPageOptions(resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
34
+ const { framePx } = getSlideModeConfig(slideMode);
35
+ const targetResolution = getResolutionSize(resolution, slideMode);
30
36
  return {
31
37
  viewport: {
32
- width: DEFAULT_CAPTURE_VIEWPORT.width,
33
- height: DEFAULT_CAPTURE_VIEWPORT.height,
38
+ width: framePx.width,
39
+ height: framePx.height,
34
40
  },
35
41
  deviceScaleFactor: targetResolution
36
- ? targetResolution.height / DEFAULT_CAPTURE_VIEWPORT.height
42
+ ? targetResolution.height / framePx.height
37
43
  : DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR,
38
44
  };
39
45
  }
40
46
 
41
- function getTargetRasterSize(resolution = '') {
42
- const targetResolution = getResolutionSize(resolution);
47
+ function getTargetRasterSize(resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
48
+ const targetResolution = getResolutionSize(resolution, slideMode);
43
49
  if (targetResolution) {
44
50
  return targetResolution;
45
51
  }
46
52
 
53
+ const { pptxSizeIn } = getSlideModeConfig(slideMode);
47
54
  return {
48
- width: Math.round(TARGET_SLIDE_SIZE_IN.width * TARGET_RASTER_DPI),
49
- height: Math.round(TARGET_SLIDE_SIZE_IN.height * TARGET_RASTER_DPI),
55
+ width: Math.round(pptxSizeIn.width * TARGET_RASTER_DPI),
56
+ height: Math.round(pptxSizeIn.height * TARGET_RASTER_DPI),
50
57
  };
51
58
  }
52
59
 
60
+ function toSlideOrder(fileName) {
61
+ const match = fileName.match(/\d+/);
62
+ return match ? Number.parseInt(match[0], 10) : Number.POSITIVE_INFINITY;
63
+ }
64
+
65
+ function sortSlideFiles(a, b) {
66
+ const orderA = toSlideOrder(a);
67
+ const orderB = toSlideOrder(b);
68
+ if (orderA !== orderB) {
69
+ return orderA - orderB;
70
+ }
71
+ return a.localeCompare(b);
72
+ }
73
+
74
+ function getHtmlSlides(slidesDir) {
75
+ if (!fs.existsSync(slidesDir)) {
76
+ throw new Error(`Slides directory not found: ${slidesDir}`);
77
+ }
78
+
79
+ const files = fs.readdirSync(slidesDir)
80
+ .filter((fileName) => SLIDE_FILE_PATTERN.test(fileName))
81
+ .sort(sortSlideFiles);
82
+
83
+ if (files.length === 0) {
84
+ throw new Error(`No slide-*.html files found in ${slidesDir}`);
85
+ }
86
+
87
+ return files;
88
+ }
89
+
53
90
  function printUsage() {
54
91
  process.stdout.write(
55
92
  [
@@ -58,6 +95,7 @@ function printUsage() {
58
95
  'Options:',
59
96
  ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
60
97
  ` --output <path> Output pptx path (default: ${DEFAULT_OUTPUT})`,
98
+ ` --mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
61
99
  ` --resolution <preset> Raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_RESOLUTION})`,
62
100
  ' -h, --help Show this help message',
63
101
  ].join('\n'),
@@ -77,6 +115,7 @@ function parseArgs(args) {
77
115
  const options = {
78
116
  slidesDir: DEFAULT_SLIDES_DIR,
79
117
  output: DEFAULT_OUTPUT,
118
+ mode: DEFAULT_SLIDE_MODE,
80
119
  resolution: DEFAULT_RESOLUTION,
81
120
  help: false,
82
121
  };
@@ -110,6 +149,17 @@ function parseArgs(args) {
110
149
  continue;
111
150
  }
112
151
 
152
+ if (arg === '--mode') {
153
+ options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
154
+ i += 1;
155
+ continue;
156
+ }
157
+
158
+ if (arg.startsWith('--mode=')) {
159
+ options.mode = normalizeSlideMode(arg.slice('--mode='.length));
160
+ continue;
161
+ }
162
+
113
163
  if (arg === '--resolution') {
114
164
  options.resolution = normalizeResolutionPreset(readOptionValue(args, i, '--resolution'));
115
165
  i += 1;
@@ -134,14 +184,17 @@ function parseArgs(args) {
134
184
 
135
185
  options.slidesDir = options.slidesDir.trim();
136
186
  options.output = options.output.trim();
187
+ options.mode = normalizeSlideMode(options.mode);
137
188
  options.resolution = normalizeResolutionPreset(options.resolution);
138
189
  return options;
139
190
  }
140
191
 
141
192
  async function convertSlide(htmlFile, pres, browser, options = {}) {
142
193
  const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
194
+ const slideMode = normalizeSlideMode(options.mode || DEFAULT_SLIDE_MODE);
195
+ const fallbackSize = getSlideModeConfig(slideMode).framePx;
143
196
 
144
- const page = await browser.newPage(buildPageOptions(options.resolution));
197
+ const page = await browser.newPage(buildPageOptions(options.resolution, slideMode));
145
198
  await page.goto(`file://${filePath}`);
146
199
 
147
200
  const bodyDimensions = await page.evaluate(() => {
@@ -154,14 +207,14 @@ async function convertSlide(htmlFile, pres, browser, options = {}) {
154
207
  });
155
208
 
156
209
  await page.setViewportSize({
157
- width: normalizeDimension(bodyDimensions.width, DEFAULT_CAPTURE_VIEWPORT.width),
158
- height: normalizeDimension(bodyDimensions.height, DEFAULT_CAPTURE_VIEWPORT.height),
210
+ width: normalizeDimension(bodyDimensions.width, fallbackSize.width),
211
+ height: normalizeDimension(bodyDimensions.height, fallbackSize.height),
159
212
  });
160
213
 
161
214
  const screenshot = await page.screenshot({ type: 'png' });
162
215
  await page.close();
163
216
 
164
- const targetSize = getTargetRasterSize(options.resolution);
217
+ const targetSize = getTargetRasterSize(options.resolution, slideMode);
165
218
 
166
219
  const resized = await sharp(screenshot)
167
220
  .resize(targetSize.width, targetSize.height, { fit: 'fill' })
@@ -191,14 +244,18 @@ async function main(argv = process.argv.slice(2)) {
191
244
  }
192
245
 
193
246
  const pres = new PptxGenJS();
194
- pres.layout = 'LAYOUT_WIDE';
247
+ const { pptxSizeIn } = getSlideModeConfig(options.mode);
248
+ pres.defineLayout({
249
+ name: 'SLIDES_GRAB_DYNAMIC',
250
+ width: pptxSizeIn.width,
251
+ height: pptxSizeIn.height,
252
+ });
253
+ pres.layout = 'SLIDES_GRAB_DYNAMIC';
195
254
 
196
255
  const slidesDir = path.resolve(process.cwd(), options.slidesDir);
197
256
  const { ensureSlidesPassValidation } = await import('../scripts/validate-slides.js');
198
- await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PPTX export' });
199
- const files = fs.readdirSync(slidesDir)
200
- .filter((fileName) => fileName.endsWith('.html'))
201
- .sort();
257
+ await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PPTX export', slideMode: options.mode });
258
+ const files = getHtmlSlides(slidesDir);
202
259
 
203
260
  console.log(`Converting ${files.length} slides...`);
204
261
 
@@ -209,7 +266,7 @@ async function main(argv = process.argv.slice(2)) {
209
266
  const filePath = path.join(slidesDir, file);
210
267
  console.log(` Processing: ${file}`);
211
268
  try {
212
- const tmpPath = await convertSlide(filePath, pres, browser, { resolution: options.resolution });
269
+ const tmpPath = await convertSlide(filePath, pres, browser, { mode: options.mode, resolution: options.resolution });
213
270
  tmpFiles.push(tmpPath);
214
271
  console.log(` ✓ ${file} done`);
215
272
  } catch (error) {
@@ -232,6 +289,7 @@ async function main(argv = process.argv.slice(2)) {
232
289
 
233
290
  module.exports = {
234
291
  buildPageOptions,
292
+ getHtmlSlides,
235
293
  getTargetRasterSize,
236
294
  main,
237
295
  parseArgs,
package/src/resolve.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * 1. Local (user's CWD) — per-project overrides
6
6
  * 2. Package root — built-in defaults
7
7
  *
8
- * slides directory, slide-outline.md, style-config.md → always local (CWD)
9
- * templates/, themes/ → local first, package fallback
8
+ * slides directory, slide-outline.md → always local (CWD)
9
+ * templates/ → local first, package fallback
10
10
  * scripts/ → always package
11
11
  */
12
12
 
@@ -60,27 +60,6 @@ export function resolveTemplate(name) {
60
60
  return null;
61
61
  }
62
62
 
63
- /**
64
- * Resolve a theme file. Local first, then package fallback.
65
- * @param {string} name — theme name without extension (e.g. "modern-dark")
66
- * @returns {{ path: string, source: 'local' | 'package' } | null}
67
- */
68
- export function resolveTheme(name) {
69
- const fileName = name.endsWith('.css') ? name : `${name}.css`;
70
-
71
- const localPath = join(getCwd(), 'themes', fileName);
72
- if (existsSync(localPath)) {
73
- return { path: localPath, source: 'local' };
74
- }
75
-
76
- const packagePath = join(PACKAGE_ROOT, 'themes', fileName);
77
- if (existsSync(packagePath)) {
78
- return { path: packagePath, source: 'package' };
79
- }
80
-
81
- return null;
82
- }
83
-
84
63
  /**
85
64
  * List all available templates (local + package, deduplicated).
86
65
  * @returns {Array<{ name: string, source: 'local' | 'package' }>}
@@ -122,34 +101,6 @@ export function listTemplates() {
122
101
  return Array.from(seen.values()).sort((a, b) => a.name.localeCompare(b.name));
123
102
  }
124
103
 
125
- /**
126
- * List all available themes (local + package, deduplicated).
127
- * @returns {Array<{ name: string, source: 'local' | 'package' }>}
128
- */
129
- export function listThemes() {
130
- const seen = new Map();
131
-
132
- const localDir = join(getCwd(), 'themes');
133
- if (existsSync(localDir)) {
134
- for (const f of readdirSync(localDir)) {
135
- if (f.endsWith('.css')) {
136
- seen.set(f, { name: f.replace('.css', ''), source: 'local' });
137
- }
138
- }
139
- }
140
-
141
- const pkgDir = join(PACKAGE_ROOT, 'themes');
142
- if (existsSync(pkgDir)) {
143
- for (const f of readdirSync(pkgDir)) {
144
- if (f.endsWith('.css') && !seen.has(f)) {
145
- seen.set(f, { name: f.replace('.css', ''), source: 'package' });
146
- }
147
- }
148
- }
149
-
150
- return Array.from(seen.values()).sort((a, b) => a.name.localeCompare(b.name));
151
- }
152
-
153
104
  /**
154
105
  * Resolve a script path. Always from package.
155
106
  * @param {string} relativePath — e.g. "scripts/validate-slides.js"
@@ -0,0 +1,72 @@
1
+ const DEFAULT_SLIDE_MODE = 'presentation';
2
+ const PT_TO_PX = 96 / 72;
3
+
4
+ const SLIDE_MODES = Object.freeze({
5
+ presentation: Object.freeze({
6
+ name: 'presentation',
7
+ framePt: Object.freeze({ width: 720, height: 405 }),
8
+ framePx: Object.freeze({ width: 720 * PT_TO_PX, height: 405 * PT_TO_PX }),
9
+ screenshotPx: Object.freeze({ width: 1600, height: 900 }),
10
+ pptxSizeIn: Object.freeze({ width: 13.33, height: 7.5 }),
11
+ figmaSizeIn: Object.freeze({ width: 10, height: 5.625 }),
12
+ sizeLabel: '720pt x 405pt',
13
+ coordinateSpaceLabel: '960x540',
14
+ aspectRatioLabel: '16:9',
15
+ }),
16
+ 'card-news': Object.freeze({
17
+ name: 'card-news',
18
+ framePt: Object.freeze({ width: 720, height: 720 }),
19
+ framePx: Object.freeze({ width: 720 * PT_TO_PX, height: 720 * PT_TO_PX }),
20
+ screenshotPx: Object.freeze({ width: 1600, height: 1600 }),
21
+ pptxSizeIn: Object.freeze({ width: 10, height: 10 }),
22
+ figmaSizeIn: Object.freeze({ width: 10, height: 10 }),
23
+ sizeLabel: '720pt x 720pt',
24
+ coordinateSpaceLabel: '960x960',
25
+ aspectRatioLabel: '1:1',
26
+ }),
27
+ });
28
+
29
+ function getSlideModeChoices() {
30
+ return Object.keys(SLIDE_MODES);
31
+ }
32
+
33
+ function normalizeSlideMode(value, options = {}) {
34
+ const {
35
+ allowEmpty = false,
36
+ optionName = '--mode',
37
+ } = options;
38
+
39
+ if (typeof value !== 'string') {
40
+ if (allowEmpty) {
41
+ return '';
42
+ }
43
+ throw new Error(`${optionName} must be one of: ${getSlideModeChoices().join(', ')}`);
44
+ }
45
+
46
+ const normalized = value.trim().toLowerCase();
47
+ if (normalized === '') {
48
+ if (allowEmpty) {
49
+ return '';
50
+ }
51
+ throw new Error(`${optionName} must be one of: ${getSlideModeChoices().join(', ')}`);
52
+ }
53
+
54
+ if (!SLIDE_MODES[normalized]) {
55
+ throw new Error(`Unknown ${optionName} value: ${value}. Expected one of: ${getSlideModeChoices().join(', ')}`);
56
+ }
57
+
58
+ return normalized;
59
+ }
60
+
61
+ function getSlideModeConfig(value = DEFAULT_SLIDE_MODE) {
62
+ return SLIDE_MODES[normalizeSlideMode(value, { optionName: '--mode' })];
63
+ }
64
+
65
+ module.exports = {
66
+ DEFAULT_SLIDE_MODE,
67
+ PT_TO_PX,
68
+ SLIDE_MODES,
69
+ getSlideModeChoices,
70
+ getSlideModeConfig,
71
+ normalizeSlideMode,
72
+ };
@@ -1,3 +1,12 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const {
5
+ DEFAULT_SLIDE_MODE,
6
+ getSlideModeChoices,
7
+ normalizeSlideMode,
8
+ } = require('../slide-mode.cjs');
9
+
1
10
  export const DEFAULT_SLIDES_DIR = 'slides';
2
11
  export const DEFAULT_VALIDATE_FORMAT = 'concise';
3
12
  export const VALIDATE_FORMATS = ['concise', 'json', 'json-full'];
@@ -14,6 +23,7 @@ export function parseValidateCliArgs(args) {
14
23
  const options = {
15
24
  slidesDir: DEFAULT_SLIDES_DIR,
16
25
  format: DEFAULT_VALIDATE_FORMAT,
26
+ mode: DEFAULT_SLIDE_MODE,
17
27
  help: false,
18
28
  slides: [],
19
29
  };
@@ -48,6 +58,17 @@ export function parseValidateCliArgs(args) {
48
58
  continue;
49
59
  }
50
60
 
61
+ if (arg === '--mode') {
62
+ options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
63
+ i += 1;
64
+ continue;
65
+ }
66
+
67
+ if (arg.startsWith('--mode=')) {
68
+ options.mode = normalizeSlideMode(arg.slice('--mode='.length));
69
+ continue;
70
+ }
71
+
51
72
  if (arg === '--slide') {
52
73
  options.slides.push(readOptionValue(args, i, '--slide'));
53
74
  i += 1;
@@ -72,6 +93,7 @@ export function parseValidateCliArgs(args) {
72
93
 
73
94
  options.slidesDir = options.slidesDir.trim();
74
95
  options.format = options.format.trim();
96
+ options.mode = normalizeSlideMode(options.mode);
75
97
 
76
98
  if (!VALIDATE_FORMATS.includes(options.format)) {
77
99
  throw new Error(`Unknown --format value: ${options.format}. Expected one of: ${VALIDATE_FORMATS.join(', ')}`);
@@ -91,6 +113,7 @@ export function getValidateUsage() {
91
113
  'Options:',
92
114
  ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
93
115
  ` --format <format> Output format: ${VALIDATE_FORMATS.join(', ')} (default: ${DEFAULT_VALIDATE_FORMAT})`,
116
+ ` --mode <mode> Slide mode: ${getSlideModeChoices().join(', ')} (default: ${DEFAULT_SLIDE_MODE})`,
94
117
  ' --slide <file> Validate only the named slide file (repeatable)',
95
118
  ' -h, --help Show this help message',
96
119
  ].join('\n');
@@ -1,4 +1,5 @@
1
1
  import { access, readdir } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
2
3
  import { basename, join } from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
4
5
  import { chromium } from 'playwright';
@@ -9,12 +10,14 @@ import {
9
10
  resolveSlideSourcePath,
10
11
  } from '../image-contract.js';
11
12
 
12
- export const FRAME_PT = { width: 720, height: 405 };
13
- export const PT_TO_PX = 96 / 72;
14
- export const FRAME_PX = {
15
- width: FRAME_PT.width * PT_TO_PX,
16
- height: FRAME_PT.height * PT_TO_PX,
17
- };
13
+ const require = createRequire(import.meta.url);
14
+ const {
15
+ DEFAULT_SLIDE_MODE,
16
+ getSlideModeConfig,
17
+ } = require('../slide-mode.cjs');
18
+
19
+ export const FRAME_PT = getSlideModeConfig(DEFAULT_SLIDE_MODE).framePt;
20
+ export const FRAME_PX = getSlideModeConfig(DEFAULT_SLIDE_MODE).framePx;
18
21
  export const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
19
22
  export const TEXT_SELECTOR = 'p,h1,h2,h3,h4,h5,h6,li';
20
23
  export const TOLERANCE_PX = 0.5;
@@ -58,28 +61,30 @@ export function summarizeSlides(slides) {
58
61
  return summary;
59
62
  }
60
63
 
61
- export function createValidationResult(slides) {
64
+ export function createValidationResult(slides, slideMode = DEFAULT_SLIDE_MODE) {
65
+ const { framePt, framePx } = getSlideModeConfig(slideMode);
62
66
  return {
63
67
  generatedAt: new Date().toISOString(),
64
68
  frame: {
65
- widthPt: FRAME_PT.width,
66
- heightPt: FRAME_PT.height,
67
- widthPx: FRAME_PX.width,
68
- heightPx: FRAME_PX.height,
69
+ widthPt: framePt.width,
70
+ heightPt: framePt.height,
71
+ widthPx: framePx.width,
72
+ heightPx: framePx.height,
69
73
  },
70
74
  slides,
71
75
  summary: summarizeSlides(slides),
72
76
  };
73
77
  }
74
78
 
75
- export function createValidationFailure(error) {
79
+ export function createValidationFailure(error, slideMode = DEFAULT_SLIDE_MODE) {
80
+ const { framePt, framePx } = getSlideModeConfig(slideMode);
76
81
  return {
77
82
  generatedAt: new Date().toISOString(),
78
83
  frame: {
79
- widthPt: FRAME_PT.width,
80
- heightPt: FRAME_PT.height,
81
- widthPx: FRAME_PX.width,
82
- heightPx: FRAME_PX.height,
84
+ widthPt: framePt.width,
85
+ heightPt: framePt.height,
86
+ widthPx: framePx.width,
87
+ heightPx: framePx.height,
83
88
  },
84
89
  slides: [],
85
90
  summary: {
@@ -347,9 +352,10 @@ export function selectSlideFiles(slideFiles, selectedSlides = [], slidesDir = ''
347
352
  return slideFiles.filter((slide) => available.has(slide) && requested.includes(slide));
348
353
  }
349
354
 
350
- export async function inspectSlide(page, fileName, slidesDir) {
355
+ export async function inspectSlide(page, fileName, slidesDir, slideMode = DEFAULT_SLIDE_MODE) {
351
356
  const slidePath = join(slidesDir, fileName);
352
357
  const slideUrl = pathToFileURL(slidePath).href;
358
+ const { framePx, sizeLabel } = getSlideModeConfig(slideMode);
353
359
 
354
360
  await page.goto(slideUrl, { waitUntil: 'load' });
355
361
  await page.evaluate(async () => {
@@ -359,7 +365,7 @@ export async function inspectSlide(page, fileName, slidesDir) {
359
365
  });
360
366
 
361
367
  const inspection = await page.evaluate(
362
- ({ framePx, textSelector, tolerancePx }) => {
368
+ ({ framePx, sizeLabel, textSelector, tolerancePx }) => {
363
369
  const skipTags = new Set(['SCRIPT', 'STYLE', 'META', 'LINK', 'HEAD', 'TITLE', 'NOSCRIPT']);
364
370
  const critical = [];
365
371
  const warning = [];
@@ -552,7 +558,7 @@ export async function inspectSlide(page, fileName, slidesDir) {
552
558
  if (outsideFrame) {
553
559
  critical.push({
554
560
  code: 'overflow-outside-frame',
555
- message: 'Element exceeds the 720pt x 405pt slide frame.',
561
+ message: `Element exceeds the ${sizeLabel} slide frame.`,
556
562
  element: elementPath(element),
557
563
  bbox: normalizeRect(rect),
558
564
  frame: normalizeRect(frameRect),
@@ -663,7 +669,8 @@ export async function inspectSlide(page, fileName, slidesDir) {
663
669
  };
664
670
  },
665
671
  {
666
- framePx: FRAME_PX,
672
+ framePx,
673
+ sizeLabel,
667
674
  textSelector: TEXT_SELECTOR,
668
675
  tolerancePx: TOLERANCE_PX,
669
676
  },
@@ -696,12 +703,12 @@ export async function inspectSlide(page, fileName, slidesDir) {
696
703
  };
697
704
  }
698
705
 
699
- export async function scanSlides(page, slidesDir, slideFiles) {
706
+ export async function scanSlides(page, slidesDir, slideFiles, slideMode = DEFAULT_SLIDE_MODE) {
700
707
  const slides = [];
701
708
 
702
709
  for (const slideFile of slideFiles) {
703
710
  try {
704
- const result = await inspectSlide(page, slideFile, slidesDir);
711
+ const result = await inspectSlide(page, slideFile, slidesDir, slideMode);
705
712
  slides.push(result);
706
713
  } catch (error) {
707
714
  slides.push({
@@ -740,7 +747,10 @@ export function formatValidationFailureForExport(result, exportLabel = 'Export')
740
747
  }
741
748
 
742
749
  const suffix = findings.length > 0 ? `\n${findings.join('\n')}` : '';
743
- return `${exportLabel} blocked by slide validation. Run \`slides-grab validate --slides-dir <path>\` for full diagnostics.${suffix}`;
750
+ const modeHint = result.slideMode && result.slideMode !== DEFAULT_SLIDE_MODE
751
+ ? ` --mode ${result.slideMode}`
752
+ : '';
753
+ return `${exportLabel} blocked by slide validation. Run \`slides-grab validate --slides-dir <path>${modeHint}\` for full diagnostics.${suffix}`;
744
754
  }
745
755
 
746
756
  const EXPORT_BLOCKING_IMAGE_CONTRACT_CODES = new Set([
@@ -799,6 +809,7 @@ export async function ensureSlidesPassValidation(
799
809
  slidesDir,
800
810
  {
801
811
  exportLabel = 'Export',
812
+ slideMode = DEFAULT_SLIDE_MODE,
802
813
  shouldBlockIssue = isBlockingImageContractIssue,
803
814
  } = {},
804
815
  ) {
@@ -812,8 +823,11 @@ export async function ensureSlidesPassValidation(
812
823
  const page = await context.newPage();
813
824
 
814
825
  try {
815
- const slides = await scanSlides(page, slidesDir, slideFiles);
816
- const result = createValidationResult(slides);
826
+ const slides = await scanSlides(page, slidesDir, slideFiles, slideMode);
827
+ const result = {
828
+ ...createValidationResult(slides, slideMode),
829
+ slideMode,
830
+ };
817
831
  const blockingResult = filterExportBlockingSlides(result, shouldBlockIssue);
818
832
  if (blockingResult.summary.failedSlides > 0) {
819
833
  throw new Error(formatValidationFailureForExport(blockingResult, exportLabel));
@@ -0,0 +1,19 @@
1
+ # Design Style Collections
2
+
3
+ slides-grab bundles 35 design styles: 30 derived from [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles) (MIT) plus 5 slides-grab originals.
4
+
5
+ These styles are reference directions for slide generation, not drop-in HTML slide templates. Agents may also design fully custom visuals beyond the bundled collection.
6
+
7
+ ## Recommended workflow
8
+
9
+ 1. `slides-grab list-styles`
10
+ 2. `slides-grab preview-styles` to open the visual gallery in browser
11
+ 3. Tell the agent which style to use, or ask for something custom
12
+
13
+ The preview/select flow is intentionally simple: it keeps design approval inside the CLI and a local HTML preview page instead of adding a separate app.
14
+
15
+ ## Citation
16
+
17
+ - Upstream collection: `corazzon/pptx-design-styles`
18
+ - URL: <https://github.com/corazzon/pptx-design-styles>
19
+ - Reference used in this repo: `references/styles.md`