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
@@ -0,0 +1,55 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { RAW_DESIGN_STYLES } from './design-styles-data.js';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ export const DESIGN_STYLES_SOURCE = Object.freeze({
10
+ name: 'PPT Design Collections',
11
+ repo: 'corazzon/pptx-design-styles',
12
+ url: 'https://github.com/corazzon/pptx-design-styles',
13
+ previewUrl: 'https://corazzon.github.io/pptx-design-styles/preview/modern-pptx-designs-30.html',
14
+ references: [
15
+ 'README.md',
16
+ 'preview/modern-pptx-designs-30.html',
17
+ 'references/styles.md',
18
+ ],
19
+ license: 'MIT',
20
+ citation: 'Design collections derived from corazzon/pptx-design-styles. Styles 31–35 are slides-grab originals.',
21
+ });
22
+
23
+ const DESIGN_STYLES = RAW_DESIGN_STYLES.map((style) => Object.freeze({
24
+ ...style,
25
+ source: DESIGN_STYLES_SOURCE,
26
+ }));
27
+
28
+ const DESIGN_STYLES_BY_ID = new Map(DESIGN_STYLES.map((style) => [style.id, style]));
29
+
30
+ export function listDesignStyles() {
31
+ return DESIGN_STYLES;
32
+ }
33
+
34
+ export function getDesignStyle(styleId) {
35
+ if (!styleId) {
36
+ return null;
37
+ }
38
+ return DESIGN_STYLES_BY_ID.get(styleId) ?? null;
39
+ }
40
+
41
+ export function requireDesignStyle(styleId) {
42
+ const style = getDesignStyle(styleId);
43
+ if (!style) {
44
+ throw new Error(`Unknown style "${styleId}". Run "slides-grab list-styles" to inspect the bundled collection.`);
45
+ }
46
+ return style;
47
+ }
48
+
49
+ export function getPreviewHtmlPath() {
50
+ return resolve(__dirname, '..', 'templates', 'design-styles', 'preview.html');
51
+ }
52
+
53
+ export function buildStylePreviewHtml() {
54
+ return readFileSync(getPreviewHtmlPath(), 'utf-8');
55
+ }
@@ -1,13 +1,24 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { mkdir } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
3
4
  import { dirname, join } from 'node:path';
4
5
  import sharp from 'sharp';
5
6
 
6
7
  import { getPackageRoot } from '../resolve.js';
7
8
 
9
+ const require = createRequire(import.meta.url);
10
+ const {
11
+ DEFAULT_SLIDE_MODE,
12
+ getSlideModeConfig,
13
+ } = require('../slide-mode.cjs');
14
+
8
15
  export const SLIDE_SIZE = { width: 960, height: 540 };
16
+ export function getSlideSize(slideMode = DEFAULT_SLIDE_MODE) {
17
+ return getSlideModeConfig(slideMode).framePx;
18
+ }
9
19
 
10
20
  const PPT_DESIGN_SKILL_PATH = join(getPackageRoot(), 'skills', 'slides-grab-design', 'SKILL.md');
21
+ const EDITOR_CODEX_PROMPT_PATH = join(dirname(new URL(import.meta.url).pathname), 'editor-codex-prompt.md');
11
22
  const DETAILED_DESIGN_SKILL_PATH = join(getPackageRoot(), 'skills', 'slides-grab-design', 'references', 'detailed-design-rules.md');
12
23
  const BEAUTIFUL_SLIDE_DEFAULTS_PATH = join(getPackageRoot(), 'skills', 'slides-grab-design', 'references', 'beautiful-slide-defaults.md');
13
24
  const EDITOR_PPT_DESIGN_SECTION_HEADINGS = [
@@ -17,9 +28,13 @@ const EDITOR_PPT_DESIGN_SECTION_HEADINGS = [
17
28
  const DETAILED_DESIGN_SECTION_HEADINGS = [
18
29
  '## Base Settings',
19
30
  '## Text Usage Rules',
31
+ '## Icon Usage Rules',
20
32
  '## Workflow (Stage 2: Design + Human Review)',
21
33
  '## Important Notes',
22
34
  ];
35
+ const DETAILED_DESIGN_REQUIRED_SECTION_HEADINGS = [
36
+ '## Icon Usage Rules',
37
+ ];
23
38
  const BEAUTIFUL_SLIDE_DEFAULTS_SECTION_HEADINGS = [
24
39
  '## Working Model',
25
40
  '## Beautiful Defaults for Slides',
@@ -48,6 +63,8 @@ const EDITOR_PPT_DESIGN_SKILL_FALLBACK = [
48
63
  '## Rules',
49
64
  '- Keep slide size 720pt x 405pt.',
50
65
  '- Keep semantic text tags (`p`, `h1-h6`, `ul`, `ol`, `li`).',
66
+ '- Prefer Lucide as the default icon library for slide UI elements, callouts, and supporting visuals.',
67
+ '- Do not default to emoji for iconography unless the brief explicitly asks for a playful or native-emoji tone.',
51
68
  '- Put local images and videos under `<slides-dir>/assets/` and reference them as `./assets/<file>`.',
52
69
  '- Allow `data:` URLs when the slide must be fully self-contained.',
53
70
  '- Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.',
@@ -86,15 +103,21 @@ const DETAILED_DESIGN_SKILL_FALLBACK = [
86
103
  '- All text must be inside <p>, <h1>-<h6>, <ul>, <ol>, or <li>.',
87
104
  '- Never place text directly in <div> or <span>.',
88
105
  '',
106
+ '## Icon Usage Rules',
107
+ '- Prefer Lucide as the default icon library for slide UI elements, callouts, and supporting visuals.',
108
+ '- Do not default to emoji for iconography; reserve emoji for cases where the brief explicitly wants a playful or native-emoji tone.',
109
+ '- Keep icon sizing, stroke weight, and color aligned with the deck\'s approved design tokens.',
110
+ '',
89
111
  '## Workflow (Stage 2: Design + Human Review)',
90
- '- After slide generation or edits, run slides-grab build-viewer --slides-dir <path>.',
112
+ '- After slide generation or edits, run slides-grab validate --slides-dir <path>.',
113
+ '- Only after validation passes, run slides-grab build-viewer --slides-dir <path>.',
91
114
  '- Edit only the relevant HTML file during revision loops.',
92
115
  '- Prefer slides-grab image before remote image sourcing when a slide explicitly needs bespoke imagery.',
93
116
  '- Never start PPTX conversion without explicit approval.',
94
117
  '- Never forget to rebuild the viewer after slide changes.',
95
118
  '',
96
119
  '## Important Notes',
97
- '- CSS gradients are not supported in PowerPoint conversion; replace them with background images.',
120
+ '- CSS gradients may not export cleanly to all formats; prefer solid colors or background images when possible.',
98
121
  '- Always include the Pretendard CDN link.',
99
122
  '- Use ./assets/<file> from each slide-XX.html for local images and videos, and avoid absolute filesystem paths.',
100
123
  '- Always include # prefix in CSS colors.',
@@ -225,16 +248,11 @@ function getEditorPptDesignSkillPrompt() {
225
248
  return cachedEditorPptDesignSkillPrompt;
226
249
  }
227
250
 
228
- const prompt = loadMarkdownSections(
229
- PPT_DESIGN_SKILL_PATH,
230
- EDITOR_PPT_DESIGN_SECTION_HEADINGS,
231
- EDITOR_PPT_DESIGN_SKILL_FALLBACK,
232
- );
233
-
234
- cachedEditorPptDesignSkillPrompt = pruneDuplicateLines(
235
- prompt,
236
- EDITOR_PPT_DESIGN_DUPLICATE_PATTERNS,
237
- );
251
+ try {
252
+ cachedEditorPptDesignSkillPrompt = readFileSync(EDITOR_CODEX_PROMPT_PATH, 'utf8').trim();
253
+ } catch {
254
+ cachedEditorPptDesignSkillPrompt = EDITOR_PPT_DESIGN_SKILL_FALLBACK;
255
+ }
238
256
 
239
257
  return cachedEditorPptDesignSkillPrompt;
240
258
  }
@@ -285,14 +303,20 @@ function pruneDuplicateLines(markdown, patterns) {
285
303
  return filtered.join('\n').trim();
286
304
  }
287
305
 
288
- function loadMarkdownSections(markdownPath, headings, fallback) {
306
+ function loadMarkdownSections(markdownPath, headings, fallback, options = {}) {
289
307
  try {
290
308
  const markdown = readFileSync(markdownPath, 'utf8');
309
+ const { requiredHeadings = [] } = options;
310
+ const sectionsByHeading = new Map(headings.map((heading) => [
311
+ heading,
312
+ extractMarkdownSection(markdown, heading),
313
+ ]));
291
314
  const sections = headings
292
- .map((heading) => extractMarkdownSection(markdown, heading))
315
+ .map((heading) => sectionsByHeading.get(heading))
293
316
  .filter(Boolean);
317
+ const isMissingRequiredSection = requiredHeadings.some((requiredHeading) => !sectionsByHeading.get(requiredHeading));
294
318
 
295
- return sections.length > 0
319
+ return sections.length > 0 && !isMissingRequiredSection
296
320
  ? sections.join('\n\n')
297
321
  : fallback;
298
322
  } catch {
@@ -309,6 +333,9 @@ function getStructuralDesignSkillPrompt() {
309
333
  DETAILED_DESIGN_SKILL_PATH,
310
334
  DETAILED_DESIGN_SECTION_HEADINGS,
311
335
  DETAILED_DESIGN_SKILL_FALLBACK,
336
+ {
337
+ requiredHeadings: DETAILED_DESIGN_REQUIRED_SECTION_HEADINGS,
338
+ },
312
339
  );
313
340
 
314
341
  return cachedStructuralDesignSkillPrompt;
@@ -335,11 +362,12 @@ export function getDetailedDesignSkillPrompt() {
335
362
  ].filter(Boolean).join('\n\n');
336
363
  }
337
364
 
338
- export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selections = [] }) {
365
+ export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, slideMode = DEFAULT_SLIDE_MODE, selections = [] }) {
339
366
  const sanitizedPrompt = typeof userPrompt === 'string' ? userPrompt.trim() : '';
340
367
  if (!sanitizedPrompt) {
341
368
  throw new Error('Prompt must be a non-empty string.');
342
369
  }
370
+ const { coordinateSpaceLabel, sizeLabel } = getSlideModeConfig(slideMode);
343
371
 
344
372
  const normalizedSlidePath = typeof slidePath === 'string' && slidePath.trim() !== ''
345
373
  ? slidePath.trim()
@@ -361,30 +389,16 @@ export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selecti
361
389
  ];
362
390
  });
363
391
 
364
- const pptDesignSkillPrompt = getEditorPptDesignSkillPrompt();
365
- const skillLines = pptDesignSkillPrompt
366
- ? [
367
- 'Project skill guidance (follow strictly):',
368
- `Source: ${PPT_DESIGN_SKILL_PATH}`,
369
- pptDesignSkillPrompt,
370
- '',
371
- ]
372
- : [];
373
- const detailedDesignSkillPrompt = getStructuralDesignSkillPrompt();
374
- const detailedSkillLines = detailedDesignSkillPrompt
375
- ? [
376
- 'Detailed design/export guardrails (selected from the full design system):',
377
- `Primary source: ${DETAILED_DESIGN_SKILL_PATH}`,
378
- detailedDesignSkillPrompt,
379
- '',
380
- ]
381
- : [];
382
- const slideArtDirectionPrompt = getSlideArtDirectionPrompt();
383
- const slideArtDirectionLines = slideArtDirectionPrompt
392
+ const editorPrompt = getEditorPptDesignSkillPrompt()
393
+ .replaceAll('720pt x 405pt', sizeLabel)
394
+ .replace(
395
+ 'Run `slides-grab validate --slides-dir <path>` after editing.',
396
+ `Run \`slides-grab validate --slides-dir <path>${slideMode === DEFAULT_SLIDE_MODE ? '' : ` --mode ${slideMode}`}\` after editing.`,
397
+ );
398
+ const editorPromptLines = editorPrompt
384
399
  ? [
385
- 'Slide art direction defaults (packaged guidance for beautiful HTML slides):',
386
- `Primary source: ${BEAUTIFUL_SLIDE_DEFAULTS_PATH}`,
387
- slideArtDirectionPrompt,
400
+ 'Slide edit rules (follow strictly):',
401
+ editorPrompt,
388
402
  '',
389
403
  ]
390
404
  : [];
@@ -392,19 +406,17 @@ export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selecti
392
406
  return [
393
407
  `Edit ${normalizedSlidePath} only.`,
394
408
  '',
395
- ...skillLines,
396
- ...detailedSkillLines,
397
- ...slideArtDirectionLines,
398
- 'User edit request:',
409
+ ...editorPromptLines,
410
+ 'User edit request (this is the primary objective — follow it faithfully):',
399
411
  sanitizedPrompt,
400
412
  '',
401
- 'Selected regions on slide (960x540 coordinate space):',
413
+ `Selected regions on slide (${coordinateSpaceLabel} coordinate space):`,
402
414
  ...selectionLines,
403
415
  'Rules:',
404
416
  '- Edit only the requested slide HTML file among slide-*.html files.',
405
417
  '- Do not modify any other slide HTML files unless explicitly requested.',
406
418
  '- Keep existing structure/content unless the request requires a change.',
407
- '- Keep slide dimensions at 720pt x 405pt.',
419
+ `- Keep slide dimensions at ${sizeLabel}.`,
408
420
  '- Keep text in semantic tags (<p>, <h1>-<h6>, <ul>, <ol>, <li>).',
409
421
  '- You may add or update supporting files required for the requested slide, including local images and videos under <slides-dir>/assets/ and tldraw source/export files used to generate those assets.',
410
422
  '- When the request needs bespoke imagery, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>` so Nano Banana Pro saves the asset under <slides-dir>/assets/.',
@@ -0,0 +1,50 @@
1
+ # Editor Codex Prompt — Slide Edit Rules
2
+
3
+ This prompt is sent to Codex when the editor requests a single-slide edit.
4
+ It is intentionally separate from the full design skill (SKILL.md) because
5
+ the editor context assumes the deck design is already established.
6
+
7
+ ## Primary Objective
8
+ The user's edit request is the primary objective. All rules below exist to support it, not override it. When a rule conflicts with the user's intent, follow the user.
9
+
10
+ ## Edit Workflow
11
+ 1. Read the target slide HTML file.
12
+ 2. Apply the user's edit request to the selected region.
13
+ 3. Run `slides-grab validate --slides-dir <path>` after editing.
14
+ 4. If validation fails, fix the HTML/CSS and re-run until it passes.
15
+ 5. Return after applying the change.
16
+
17
+ ## Slide Rules
18
+ - Keep slide size appropriate for the current mode (`720pt x 405pt` for presentation, `720pt x 720pt` for card-news).
19
+ - Keep semantic text tags (`p`, `h1-h6`, `ul`, `ol`, `li`).
20
+ - Never place text directly in `<div>` or `<span>`.
21
+ - Always include `#` prefix in CSS colors.
22
+ - Always include the Pretendard webfont CDN link.
23
+
24
+ ## Asset Rules
25
+ - Put local images and videos under `<slides-dir>/assets/` and reference as `./assets/<file>`.
26
+ - Always include `alt` on `<img>` tags.
27
+ - Allow `data:` URLs only when the slide must be fully self-contained.
28
+ - Do not leave remote `http(s)://` image URLs in saved slide HTML; download into `./assets/`.
29
+ - Do not use absolute filesystem paths in slide HTML.
30
+ - Do not use non-body `background-image` for content imagery; use `<img>` instead.
31
+ - Use `data-image-placeholder` to reserve space when no image is available yet.
32
+ - When the request needs bespoke imagery, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>` so Nano Banana Pro saves the asset under `<slides-dir>/assets/`.
33
+ - If `GOOGLE_API_KEY` / `GEMINI_API_KEY` is unavailable or the Nano Banana API fails, ask the user for a key or fall back to web search + download into `./assets/`.
34
+ - For local videos, use `<video src="./assets/<file>">` with `poster="./assets/<file>"`.
35
+ - If a video starts on YouTube or a supported page, use `slides-grab fetch-video --url <url> --slides-dir <path>` to download first.
36
+
37
+ ## Art Direction Defaults
38
+ - Give each slide one job, one dominant visual anchor, one primary takeaway.
39
+ - Keep copy short enough to scan in seconds.
40
+ - Use whitespace, alignment, scale, cropping, and contrast before adding decorative chrome.
41
+ - Prefer Lucide as the default icon library for slide UI elements, callouts, and supporting visuals.
42
+ - Do not default to emoji for iconography unless the brief explicitly asks for a playful or native-emoji tone.
43
+ - Default to cardless layouts unless a card improves structure.
44
+ - Limit to two typefaces max and one accent color.
45
+
46
+ ## Do NOT
47
+ - Re-open style selection or run `slides-grab preview-styles`.
48
+ - Modify other slide HTML files unless explicitly requested.
49
+ - Persist runtime-only editor/viewer injections (`<base>`, debug scripts, viewer wrappers).
50
+ - Start PPTX/PDF conversion.
@@ -1,8 +1,8 @@
1
1
  // editor-init.js — Entry point: imports, event bindings, init()
2
2
 
3
- import { state, TOOL_MODE_DRAW, TOOL_MODE_SELECT } from './editor-state.js';
3
+ import { state, TOOL_MODE_DRAW, TOOL_MODE_SELECT, setSlideFrame } from './editor-state.js';
4
4
  import {
5
- btnPrev, btnNext, slideIframe, drawLayer, promptInput, modelSelect,
5
+ btnPrev, btnNext, slideIframe, slideWrapper, drawLayer, promptInput, modelSelect,
6
6
  btnSend, btnClearBboxes, slideCounter,
7
7
  toggleBold, toggleItalic, toggleUnderline, toggleStrike,
8
8
  alignLeft, alignCenter, alignRight,
@@ -256,10 +256,42 @@ slideIframe.addEventListener('load', () => {
256
256
  updateSendState();
257
257
  });
258
258
 
259
+ function applySlideFrameCss(width, height) {
260
+ if (slideWrapper) {
261
+ slideWrapper.style.width = `${width}px`;
262
+ slideWrapper.style.height = `${height}px`;
263
+ }
264
+ if (slideIframe) {
265
+ slideIframe.style.width = `${width}px`;
266
+ slideIframe.style.height = `${height}px`;
267
+ }
268
+ }
269
+
270
+ async function loadEditorConfig() {
271
+ try {
272
+ const res = await fetch('/api/config');
273
+ if (!res.ok) return;
274
+ const cfg = await res.json();
275
+ const w = cfg?.framePx?.width;
276
+ const h = cfg?.framePx?.height;
277
+ if (w && h) {
278
+ setSlideFrame(w, h);
279
+ applySlideFrameCss(w, h);
280
+ }
281
+ if (cfg?.slideMode && document?.body) {
282
+ document.body.dataset.slideMode = cfg.slideMode;
283
+ }
284
+ } catch {
285
+ // Defaults (960x540) stay in effect.
286
+ }
287
+ }
288
+
259
289
  // Init
260
290
  async function init() {
261
291
  setStatus('Loading slide list...');
262
292
 
293
+ await loadEditorConfig();
294
+
263
295
  try {
264
296
  const res = await fetch('/api/slides');
265
297
  if (!res.ok) {
@@ -1,7 +1,14 @@
1
1
  // editor-state.js — State variables, constants, Maps/Sets
2
2
 
3
- export const SLIDE_W = 960;
4
- export const SLIDE_H = 540;
3
+ export let SLIDE_W = 960;
4
+ export let SLIDE_H = 540;
5
+
6
+ export function setSlideFrame(width, height) {
7
+ const w = Number(width);
8
+ const h = Number(height);
9
+ if (Number.isFinite(w) && w > 0) SLIDE_W = Math.round(w);
10
+ if (Number.isFinite(h) && h > 0) SLIDE_H = Math.round(h);
11
+ }
5
12
  export const TOOL_MODE_DRAW = 'draw';
6
13
  export const TOOL_MODE_SELECT = 'select';
7
14
  export const POPOVER_TEXT = 'text';
@@ -17,8 +17,8 @@ export async function createScreenshotBrowser() {
17
17
  * Create a fresh screenshot page/context from an existing browser.
18
18
  * Caller must close the returned context.
19
19
  */
20
- export async function createScreenshotPage(browser) {
21
- const context = await browser.newContext({ viewport: SCREENSHOT_SIZE });
20
+ export async function createScreenshotPage(browser, screenshotSize = SCREENSHOT_SIZE) {
21
+ const context = await browser.newContext({ viewport: screenshotSize });
22
22
  const page = await context.newPage();
23
23
  return { context, page };
24
24
  }
@@ -34,6 +34,7 @@ export async function createScreenshotPage(browser) {
34
34
  * @param {boolean} [options.useHttp] – if true, slidesDir is treated as a base URL
35
35
  */
36
36
  export async function captureSlideScreenshot(page, slideFile, screenshotPath, slidesDir, options = {}) {
37
+ const screenshotSize = options.screenshotSize || SCREENSHOT_SIZE;
37
38
  const slideUrl = options.useHttp
38
39
  ? `${slidesDir}/${slideFile}`
39
40
  : pathToFileURL(join(slidesDir, slideFile)).href;
@@ -64,7 +65,7 @@ export async function captureSlideScreenshot(page, slideFile, screenshotPath, sl
64
65
  const scale = Math.min(width / sourceWidth, height / sourceHeight);
65
66
 
66
67
  bodyStyle.transform = `scale(${scale})`;
67
- }, SCREENSHOT_SIZE);
68
+ }, screenshotSize);
68
69
 
69
70
  await page.screenshot({
70
71
  path: screenshotPath,
@@ -1,8 +1,13 @@
1
- const RESOLUTION_PRESETS = Object.freeze({
2
- '720p': { width: 1280, height: 720 },
3
- '1080p': { width: 1920, height: 1080 },
4
- '1440p': { width: 2560, height: 1440 },
5
- '2160p': { width: 3840, height: 2160 },
1
+ const {
2
+ DEFAULT_SLIDE_MODE,
3
+ getSlideModeConfig,
4
+ } = require('./slide-mode.cjs');
5
+
6
+ const RESOLUTION_HEIGHTS = Object.freeze({
7
+ '720p': 720,
8
+ '1080p': 1080,
9
+ '1440p': 1440,
10
+ '2160p': 2160,
6
11
  });
7
12
 
8
13
  const RESOLUTION_ALIASES = Object.freeze({
@@ -11,7 +16,7 @@ const RESOLUTION_ALIASES = Object.freeze({
11
16
  });
12
17
 
13
18
  function getResolutionChoices() {
14
- return Object.keys(RESOLUTION_PRESETS);
19
+ return Object.keys(RESOLUTION_HEIGHTS);
15
20
  }
16
21
 
17
22
  function normalizeResolutionPreset(value, options = {}) {
@@ -33,25 +38,30 @@ function normalizeResolutionPreset(value, options = {}) {
33
38
  }
34
39
 
35
40
  const normalized = RESOLUTION_ALIASES[trimmed] || trimmed;
36
- if (!RESOLUTION_PRESETS[normalized]) {
41
+ if (!RESOLUTION_HEIGHTS[normalized]) {
37
42
  throw new Error(`Unknown resolution "${value}". Expected one of: ${getResolutionChoices().join(', ')}, 4k`);
38
43
  }
39
44
 
40
45
  return normalized;
41
46
  }
42
47
 
43
- function getResolutionSize(value) {
48
+ function getResolutionSize(value, slideMode = DEFAULT_SLIDE_MODE) {
44
49
  const normalized = normalizeResolutionPreset(value);
45
50
  if (!normalized) {
46
51
  return null;
47
52
  }
48
53
 
49
- const preset = RESOLUTION_PRESETS[normalized];
50
- return { width: preset.width, height: preset.height };
54
+ const height = RESOLUTION_HEIGHTS[normalized];
55
+ const { framePx } = getSlideModeConfig(slideMode);
56
+ const aspectRatio = framePx.width / framePx.height;
57
+ return {
58
+ width: Math.round(height * aspectRatio),
59
+ height,
60
+ };
51
61
  }
52
62
 
53
63
  module.exports = {
54
- RESOLUTION_PRESETS,
64
+ RESOLUTION_PRESETS: RESOLUTION_HEIGHTS,
55
65
  getResolutionChoices,
56
66
  getResolutionSize,
57
67
  normalizeResolutionPreset,
package/src/figma.js CHANGED
@@ -1,6 +1,13 @@
1
+ import { createRequire } from 'node:module';
1
2
  import { mkdir } from 'node:fs/promises';
2
3
  import { basename, dirname, extname, join, resolve } from 'node:path';
3
4
 
5
+ const require = createRequire(import.meta.url);
6
+ const {
7
+ DEFAULT_SLIDE_MODE,
8
+ getSlideModeConfig,
9
+ } = require('./slide-mode.cjs');
10
+
4
11
  export const DEFAULT_FIGMA_SUFFIX = '-figma.pptx';
5
12
  export const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
6
13
  export const FIGMA_EXPORT_LAYOUT_NAME = 'SLIDES_GRAB_STANDARD';
@@ -36,11 +43,12 @@ export function getFigmaManualImportInstructions() {
36
43
  return 'Figma Slides -> Import -> select the generated .pptx file.';
37
44
  }
38
45
 
39
- export function configureFigmaExportPresentation(pres) {
46
+ export function configureFigmaExportPresentation(pres, slideMode = DEFAULT_SLIDE_MODE) {
47
+ const { figmaSizeIn } = getSlideModeConfig(slideMode);
40
48
  pres.defineLayout({
41
49
  name: FIGMA_EXPORT_LAYOUT_NAME,
42
- width: SLIDE_WIDTH_INCHES,
43
- height: SLIDE_HEIGHT_INCHES,
50
+ width: figmaSizeIn.width,
51
+ height: figmaSizeIn.height,
44
52
  });
45
53
  pres.layout = FIGMA_EXPORT_LAYOUT_NAME;
46
54
  return pres;