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.
- package/README.md +32 -4
- package/bin/ppt-agent.js +76 -36
- package/package.json +3 -4
- package/scripts/build-viewer.js +48 -21
- package/scripts/editor-server.js +113 -18
- package/scripts/figma-export.js +16 -1
- package/scripts/html2pdf.js +51 -23
- package/scripts/html2pptx.js +22 -1
- package/scripts/validate-slides.js +22 -5
- package/skills/slides-grab/SKILL.md +25 -20
- package/skills/slides-grab/references/presentation-workflow-reference.md +12 -11
- package/skills/slides-grab-card-news/SKILL.md +35 -0
- package/skills/slides-grab-design/SKILL.md +19 -16
- package/skills/slides-grab-design/references/design-rules.md +11 -7
- package/skills/slides-grab-design/references/design-system-full.md +7 -19
- package/skills/slides-grab-design/references/detailed-design-rules.md +6 -1
- package/skills/slides-grab-export/SKILL.md +15 -8
- package/skills/slides-grab-export/references/html2pptx.md +4 -4
- package/skills/slides-grab-plan/SKILL.md +7 -5
- package/src/design-styles-data.js +1928 -0
- package/src/design-styles.js +55 -0
- package/src/editor/codex-edit.js +57 -45
- package/src/editor/editor-codex-prompt.md +50 -0
- package/src/editor/js/editor-init.js +34 -2
- package/src/editor/js/editor-state.js +9 -2
- package/src/editor/screenshot.js +4 -3
- package/src/export-resolution.cjs +21 -11
- package/src/figma.js +11 -3
- package/src/pptx-raster-export.cjs +79 -21
- package/src/resolve.js +2 -51
- package/src/slide-mode.cjs +72 -0
- package/src/validation/cli.js +23 -0
- package/src/validation/core.js +39 -25
- package/templates/design-styles/README.md +19 -0
- package/templates/design-styles/preview.html +3356 -0
- package/themes/corporate.css +0 -8
- package/themes/executive.css +0 -10
- package/themes/modern-dark.css +0 -9
- package/themes/sage.css +0 -9
- 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
|
+
}
|
package/src/editor/codex-edit.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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) =>
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
|
386
|
-
|
|
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
|
-
...
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
export
|
|
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';
|
package/src/editor/screenshot.js
CHANGED
|
@@ -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:
|
|
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
|
-
},
|
|
68
|
+
}, screenshotSize);
|
|
68
69
|
|
|
69
70
|
await page.screenshot({
|
|
70
71
|
path: screenshotPath,
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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(
|
|
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 (!
|
|
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
|
|
50
|
-
|
|
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:
|
|
43
|
-
height:
|
|
50
|
+
width: figmaSizeIn.width,
|
|
51
|
+
height: figmaSizeIn.height,
|
|
44
52
|
});
|
|
45
53
|
pres.layout = FIGMA_EXPORT_LAYOUT_NAME;
|
|
46
54
|
return pres;
|