slides-grab 1.2.2 → 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 +16 -0
- package/bin/ppt-agent.js +35 -5
- package/package.json +3 -3
- package/scripts/build-viewer.js +48 -21
- package/scripts/editor-server.js +45 -10
- 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 +9 -5
- package/skills/slides-grab-card-news/SKILL.md +35 -0
- package/skills/slides-grab-design/SKILL.md +13 -11
- package/skills/slides-grab-design/references/design-rules.md +5 -0
- package/skills/slides-grab-design/references/detailed-design-rules.md +5 -0
- package/skills/slides-grab-export/SKILL.md +15 -8
- package/skills/slides-grab-export/references/html2pptx.md +4 -4
- package/src/editor/codex-edit.js +45 -8
- package/src/editor/editor-codex-prompt.md +3 -1
- 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/slide-mode.cjs +72 -0
- package/src/validation/cli.js +23 -0
- package/src/validation/core.js +39 -25
package/README.md
CHANGED
|
@@ -84,6 +84,8 @@ slides-grab figma # Export an experimental / unstable Figma Slides i
|
|
|
84
84
|
slides-grab pdf # Export PDF in capture mode (default)
|
|
85
85
|
slides-grab pdf --resolution 2160p # Higher-resolution image-backed PDF export
|
|
86
86
|
slides-grab pdf --mode print # Export searchable/selectable text PDF
|
|
87
|
+
slides-grab png # Render one PNG per slide (default 2160p)
|
|
88
|
+
slides-grab png --slide-mode card-news # Render square 1:1 PNGs for Instagram
|
|
87
89
|
slides-grab image --prompt "..." # Generate a local slide image with Nano Banana Pro
|
|
88
90
|
slides-grab fetch-video --url <youtube-url> --slides-dir decks/my-deck # Download a local video asset with yt-dlp
|
|
89
91
|
slides-grab tldraw # Render a .tldr diagram into a slide-sized local SVG asset
|
|
@@ -153,12 +155,26 @@ slides-grab edit --slides-dir decks/my-deck
|
|
|
153
155
|
slides-grab validate --slides-dir decks/my-deck
|
|
154
156
|
slides-grab pdf --slides-dir decks/my-deck --output decks/my-deck.pdf
|
|
155
157
|
slides-grab pdf --slides-dir decks/my-deck --mode print --output decks/my-deck-searchable.pdf
|
|
158
|
+
slides-grab png --slides-dir decks/my-deck --output-dir decks/my-deck/out-png
|
|
156
159
|
slides-grab convert --slides-dir decks/my-deck --output decks/my-deck.pptx
|
|
157
160
|
slides-grab figma --slides-dir decks/my-deck --output decks/my-deck-figma.pptx
|
|
158
161
|
```
|
|
159
162
|
|
|
160
163
|
> **Warning:** `slides-grab convert` and `slides-grab figma` are currently **experimental / unstable**. Expect best-effort output, layout shifts, and manual cleanup in PowerPoint or Figma.
|
|
161
164
|
|
|
165
|
+
### Card News (Square 1:1) Workflow
|
|
166
|
+
|
|
167
|
+
Instagram-style card news uses a 720pt × 720pt frame end-to-end. Pass `--mode card-news` (or `--slide-mode card-news` for `pdf`/`png`) at every stage and prefer `slides-grab png` as the primary export so each card becomes an Instagram-ready PNG.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
slides-grab edit --slides-dir decks/my-cards --mode card-news
|
|
171
|
+
slides-grab validate --slides-dir decks/my-cards --mode card-news
|
|
172
|
+
slides-grab png --slides-dir decks/my-cards --slide-mode card-news --resolution 2160p
|
|
173
|
+
# Optional extras (PPTX / Figma remain experimental / unstable)
|
|
174
|
+
slides-grab pdf --slides-dir decks/my-cards --slide-mode card-news --output decks/my-cards.pdf
|
|
175
|
+
slides-grab convert --slides-dir decks/my-cards --mode card-news --output decks/my-cards.pptx
|
|
176
|
+
```
|
|
177
|
+
|
|
162
178
|
### Tldraw Diagram Assets
|
|
163
179
|
|
|
164
180
|
Use `slides-grab tldraw` when you want a newly authored `tldraw` diagram to fit an exact slide region and remain export-friendly as a local SVG asset. The command supports current-format `.tldr` files and store-snapshot JSON; legacy pre-records `.tldr` files must be reopened and resaved in a current `tldraw` build first:
|
package/bin/ppt-agent.js
CHANGED
|
@@ -87,8 +87,9 @@ program
|
|
|
87
87
|
.command('build-viewer')
|
|
88
88
|
.description('Build viewer.html from slide HTML files')
|
|
89
89
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
90
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
90
91
|
.action(async (options = {}) => {
|
|
91
|
-
const args = ['--slides-dir', options.slidesDir];
|
|
92
|
+
const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
92
93
|
await runCommand('scripts/build-viewer.js', args);
|
|
93
94
|
});
|
|
94
95
|
|
|
@@ -98,9 +99,10 @@ program
|
|
|
98
99
|
.description('Run structured validation on slide HTML files (Playwright-based)')
|
|
99
100
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
100
101
|
.option('--format <format>', 'Output format: concise, json, json-full', 'concise')
|
|
102
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
101
103
|
.option('--slide <file>', 'Validate only the named slide file (repeatable)', collectRepeatedOption, [])
|
|
102
104
|
.action(async (options = {}) => {
|
|
103
|
-
const args = ['--slides-dir', options.slidesDir, '--format', options.format];
|
|
105
|
+
const args = ['--slides-dir', options.slidesDir, '--format', options.format, '--mode', options.mode];
|
|
104
106
|
for (const slide of options.slide || []) {
|
|
105
107
|
args.push('--slide', String(slide));
|
|
106
108
|
}
|
|
@@ -112,9 +114,10 @@ program
|
|
|
112
114
|
.description('Convert slide HTML files to experimental / unstable PPTX')
|
|
113
115
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
114
116
|
.option('--output <path>', 'Output PPTX file')
|
|
117
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
115
118
|
.option('--resolution <preset>', 'Raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p)')
|
|
116
119
|
.action(async (options = {}) => {
|
|
117
|
-
const args = ['--slides-dir', options.slidesDir];
|
|
120
|
+
const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
118
121
|
if (options.output) {
|
|
119
122
|
args.push('--output', String(options.output));
|
|
120
123
|
}
|
|
@@ -130,6 +133,7 @@ program
|
|
|
130
133
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
131
134
|
.option('--output <path>', 'Output PDF file')
|
|
132
135
|
.option('--mode <mode>', 'PDF export mode: capture for visual fidelity, print for searchable text', 'capture')
|
|
136
|
+
.option('--slide-mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
133
137
|
.option('--resolution <preset>', 'Capture raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p in capture mode)')
|
|
134
138
|
.action(async (options = {}) => {
|
|
135
139
|
const args = ['--slides-dir', options.slidesDir];
|
|
@@ -139,12 +143,36 @@ program
|
|
|
139
143
|
if (options.mode) {
|
|
140
144
|
args.push('--mode', String(options.mode));
|
|
141
145
|
}
|
|
146
|
+
if (options.slideMode) {
|
|
147
|
+
args.push('--slide-mode', String(options.slideMode));
|
|
148
|
+
}
|
|
142
149
|
if (options.resolution) {
|
|
143
150
|
args.push('--resolution', String(options.resolution));
|
|
144
151
|
}
|
|
145
152
|
await runCommand('scripts/html2pdf.js', args);
|
|
146
153
|
});
|
|
147
154
|
|
|
155
|
+
program
|
|
156
|
+
.command('png')
|
|
157
|
+
.description('Render slide HTML files to one PNG per slide')
|
|
158
|
+
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
159
|
+
.option('--output-dir <path>', 'Output directory for PNG files (default: <slides-dir>/out-png)')
|
|
160
|
+
.option('--slide-mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
161
|
+
.option('--resolution <preset>', 'Raster size preset: 720p, 1080p, 1440p, 2160p, or 4k', '2160p')
|
|
162
|
+
.action(async (options = {}) => {
|
|
163
|
+
const args = ['--slides-dir', options.slidesDir];
|
|
164
|
+
if (options.outputDir) {
|
|
165
|
+
args.push('--output-dir', String(options.outputDir));
|
|
166
|
+
}
|
|
167
|
+
if (options.slideMode) {
|
|
168
|
+
args.push('--slide-mode', String(options.slideMode));
|
|
169
|
+
}
|
|
170
|
+
if (options.resolution) {
|
|
171
|
+
args.push('--resolution', String(options.resolution));
|
|
172
|
+
}
|
|
173
|
+
await runCommand('scripts/html2png.js', args);
|
|
174
|
+
});
|
|
175
|
+
|
|
148
176
|
program
|
|
149
177
|
.command('fetch-video')
|
|
150
178
|
.description('Download a video into <slides-dir>/assets via yt-dlp and print the ./assets reference')
|
|
@@ -165,9 +193,10 @@ program
|
|
|
165
193
|
.helpOption('-h, --help', 'Show this help message')
|
|
166
194
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
167
195
|
.option('--output <path>', 'Output PPTX file (default: <slides-dir>-figma.pptx)')
|
|
196
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
168
197
|
.addHelpText('after', figmaHelpText)
|
|
169
198
|
.action(async (options = {}) => {
|
|
170
|
-
const args = ['--slides-dir', options.slidesDir];
|
|
199
|
+
const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
171
200
|
if (options.output) {
|
|
172
201
|
args.push('--output', String(options.output));
|
|
173
202
|
}
|
|
@@ -222,8 +251,9 @@ program
|
|
|
222
251
|
.description('Start interactive slide editor with Codex image-based edit flow')
|
|
223
252
|
.option('--port <number>', 'Server port')
|
|
224
253
|
.option('--slides-dir <path>', 'Slide directory', 'slides')
|
|
254
|
+
.option('--mode <mode>', 'Slide mode: presentation or card-news', 'presentation')
|
|
225
255
|
.action(async (options = {}) => {
|
|
226
|
-
const args = ['--slides-dir', options.slidesDir];
|
|
256
|
+
const args = ['--slides-dir', options.slidesDir, '--mode', options.mode];
|
|
227
257
|
if (options.port) {
|
|
228
258
|
args.push('--port', String(options.port));
|
|
229
259
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slides-grab",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "Agent-first presentation framework — plan, design, and visually edit HTML slides with Claude Code or Codex, then export to PDF or experimental/unstable PPTX/Figma formats",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vkehfdl1",
|
|
@@ -53,18 +53,18 @@
|
|
|
53
53
|
"build-viewer": "node scripts/build-viewer.js",
|
|
54
54
|
"validate": "node scripts/validate-slides.js",
|
|
55
55
|
"convert": "node convert.cjs",
|
|
56
|
-
"test": "node --test --test-concurrency=1 tests/design/design-styles.test.js tests/editor/editor-codex-edit.test.js tests/editor/editor-server.test.js tests/nano-banana/nano-banana.test.js tests/pdf/html2pdf.test.js tests/pdf/html2pdf.e2e.test.js tests/figma/figma-export.test.js tests/image-contract/image-contract.test.js tests/tldraw/render-tldraw.test.js tests/validation/validate-slides.test.js tests/skills/installable-skills.test.js tests/video/download-video.test.js",
|
|
56
|
+
"test": "node --test --test-concurrency=1 tests/design/design-styles.test.js tests/editor/editor-codex-edit.test.js tests/editor/editor-server.test.js tests/nano-banana/nano-banana.test.js tests/pdf/html2pdf.test.js tests/pdf/html2pdf.e2e.test.js tests/figma/figma-export.test.js tests/image-contract/image-contract.test.js tests/tldraw/render-tldraw.test.js tests/validation/validate-slides.test.js tests/viewer/build-viewer.test.js tests/skills/installable-skills.test.js tests/video/download-video.test.js",
|
|
57
57
|
"test:e2e": "node --test tests/editor/editor-ui.e2e.test.js tests/editor/editor-concurrency.e2e.test.js"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"commander": "^12.1.0",
|
|
61
61
|
"express": "^5.2.1",
|
|
62
|
+
"lucide-react": "^1.7.0",
|
|
62
63
|
"pdf-lib": "^1.17.1",
|
|
63
64
|
"playwright": "^1.40.0",
|
|
64
65
|
"pptxgenjs": "^3.12.0",
|
|
65
66
|
"react": "^19.2.4",
|
|
66
67
|
"react-dom": "^19.2.4",
|
|
67
|
-
"react-icons": "^5.0.0",
|
|
68
68
|
"sharp": "^0.33.0",
|
|
69
69
|
"tldraw": "^4.4.1"
|
|
70
70
|
},
|
package/scripts/build-viewer.js
CHANGED
|
@@ -9,11 +9,20 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
12
|
-
import {
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
13
|
import { join, resolve } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
14
15
|
|
|
15
16
|
import { buildSlideRuntimeHtml } from '../src/image-contract.js';
|
|
16
17
|
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const {
|
|
20
|
+
DEFAULT_SLIDE_MODE,
|
|
21
|
+
getSlideModeChoices,
|
|
22
|
+
getSlideModeConfig,
|
|
23
|
+
normalizeSlideMode,
|
|
24
|
+
} = require('../src/slide-mode.cjs');
|
|
25
|
+
|
|
17
26
|
const DEFAULT_SLIDES_DIR = 'slides';
|
|
18
27
|
|
|
19
28
|
function printUsage() {
|
|
@@ -23,6 +32,7 @@ function printUsage() {
|
|
|
23
32
|
'',
|
|
24
33
|
'Options:',
|
|
25
34
|
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
35
|
+
` --mode <mode> Slide mode: ${getSlideModeChoices().join(', ')} (default: ${DEFAULT_SLIDE_MODE})`,
|
|
26
36
|
' -h, --help Show this help message',
|
|
27
37
|
].join('\n'),
|
|
28
38
|
);
|
|
@@ -40,6 +50,7 @@ function readOptionValue(args, index, optionName) {
|
|
|
40
50
|
export function parseCliArgs(args) {
|
|
41
51
|
const options = {
|
|
42
52
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
53
|
+
mode: DEFAULT_SLIDE_MODE,
|
|
43
54
|
help: false,
|
|
44
55
|
};
|
|
45
56
|
|
|
@@ -62,6 +73,17 @@ export function parseCliArgs(args) {
|
|
|
62
73
|
continue;
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
if (arg === '--mode') {
|
|
77
|
+
options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
|
|
78
|
+
i += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (arg.startsWith('--mode=')) {
|
|
83
|
+
options.mode = normalizeSlideMode(arg.slice('--mode='.length));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
throw new Error(`Unknown option: ${arg}`);
|
|
66
88
|
}
|
|
67
89
|
|
|
@@ -70,17 +92,28 @@ export function parseCliArgs(args) {
|
|
|
70
92
|
}
|
|
71
93
|
|
|
72
94
|
options.slidesDir = options.slidesDir.trim();
|
|
95
|
+
options.mode = normalizeSlideMode(options.mode);
|
|
73
96
|
return options;
|
|
74
97
|
}
|
|
75
98
|
|
|
99
|
+
function toSlideOrder(fileName) {
|
|
100
|
+
const match = fileName.match(/\d+/);
|
|
101
|
+
return match ? Number.parseInt(match[0], 10) : Number.POSITIVE_INFINITY;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sortSlideFiles(a, b) {
|
|
105
|
+
const orderA = toSlideOrder(a);
|
|
106
|
+
const orderB = toSlideOrder(b);
|
|
107
|
+
if (orderA !== orderB) {
|
|
108
|
+
return orderA - orderB;
|
|
109
|
+
}
|
|
110
|
+
return a.localeCompare(b);
|
|
111
|
+
}
|
|
112
|
+
|
|
76
113
|
export function findSlideFiles(slidesDir) {
|
|
77
114
|
return readdirSync(slidesDir)
|
|
78
|
-
.filter((file) => /^slide
|
|
79
|
-
.sort(
|
|
80
|
-
const numA = parseInt(a.match(/\d+/)[0], 10);
|
|
81
|
-
const numB = parseInt(b.match(/\d+/)[0], 10);
|
|
82
|
-
return numA - numB || a.localeCompare(b);
|
|
83
|
-
});
|
|
115
|
+
.filter((file) => /^slide-.*\.html$/i.test(file))
|
|
116
|
+
.sort(sortSlideFiles);
|
|
84
117
|
}
|
|
85
118
|
|
|
86
119
|
/**
|
|
@@ -106,7 +139,9 @@ export function loadSlides(slidesDir) {
|
|
|
106
139
|
});
|
|
107
140
|
}
|
|
108
141
|
|
|
109
|
-
export function buildViewerHtml(slides) {
|
|
142
|
+
export function buildViewerHtml(slides, { slideMode = DEFAULT_SLIDE_MODE } = {}) {
|
|
143
|
+
const { framePt } = getSlideModeConfig(slideMode);
|
|
144
|
+
|
|
110
145
|
return `<!DOCTYPE html>
|
|
111
146
|
<html lang="ko">
|
|
112
147
|
<head>
|
|
@@ -131,7 +166,6 @@ export function buildViewerHtml(slides) {
|
|
|
131
166
|
flex-direction: column;
|
|
132
167
|
}
|
|
133
168
|
|
|
134
|
-
/* Navigation bar */
|
|
135
169
|
.nav-bar {
|
|
136
170
|
height: 48px;
|
|
137
171
|
background: #1a1a1a;
|
|
@@ -182,7 +216,6 @@ export function buildViewerHtml(slides) {
|
|
|
182
216
|
padding: 6px 10px !important;
|
|
183
217
|
}
|
|
184
218
|
|
|
185
|
-
/* Slide viewport */
|
|
186
219
|
.slide-viewport {
|
|
187
220
|
flex: 1;
|
|
188
221
|
display: flex;
|
|
@@ -193,18 +226,17 @@ export function buildViewerHtml(slides) {
|
|
|
193
226
|
}
|
|
194
227
|
|
|
195
228
|
.slide-scaler {
|
|
196
|
-
width:
|
|
197
|
-
height:
|
|
229
|
+
width: ${framePt.width}pt;
|
|
230
|
+
height: ${framePt.height}pt;
|
|
198
231
|
position: relative;
|
|
199
232
|
transform-origin: center center;
|
|
200
233
|
}
|
|
201
234
|
|
|
202
|
-
/* Slide frames (iframes) */
|
|
203
235
|
.slide-frame {
|
|
204
236
|
position: absolute;
|
|
205
237
|
inset: 0;
|
|
206
|
-
width:
|
|
207
|
-
height:
|
|
238
|
+
width: ${framePt.width}pt;
|
|
239
|
+
height: ${framePt.height}pt;
|
|
208
240
|
border: none;
|
|
209
241
|
overflow: hidden;
|
|
210
242
|
opacity: 0;
|
|
@@ -220,7 +252,6 @@ export function buildViewerHtml(slides) {
|
|
|
220
252
|
</head>
|
|
221
253
|
<body>
|
|
222
254
|
<div class="viewer-container">
|
|
223
|
-
<!-- Navigation -->
|
|
224
255
|
<div class="nav-bar">
|
|
225
256
|
<button id="btn-prev" title="Previous (\\u2190)">Prev</button>
|
|
226
257
|
<span class="slide-counter" id="counter">1 / ${slides.length}</span>
|
|
@@ -228,7 +259,6 @@ export function buildViewerHtml(slides) {
|
|
|
228
259
|
<button class="btn-fullscreen" id="btn-fs" title="Fullscreen (F)">⛶</button>
|
|
229
260
|
</div>
|
|
230
261
|
|
|
231
|
-
<!-- Slide viewport -->
|
|
232
262
|
<div class="slide-viewport" id="viewport">
|
|
233
263
|
<div class="slide-scaler" id="scaler">
|
|
234
264
|
${slides.map((s, i) => ` <iframe class="slide-frame${i === 0 ? ' active' : ''}" data-slide="${i + 1}" srcdoc="${escapeForSrcdoc(s.html)}" sandbox="allow-same-origin"></iframe>`).join('\n')}
|
|
@@ -265,7 +295,6 @@ ${slides.map((s, i) => ` <iframe class="slide-frame${i === 0 ? ' active'
|
|
|
265
295
|
btnNext.addEventListener('click', next);
|
|
266
296
|
btnPrev.disabled = true;
|
|
267
297
|
|
|
268
|
-
// Keyboard navigation
|
|
269
298
|
document.addEventListener('keydown', (e) => {
|
|
270
299
|
if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); next(); }
|
|
271
300
|
else if (e.key === 'ArrowLeft') { e.preventDefault(); prev(); }
|
|
@@ -277,7 +306,6 @@ ${slides.map((s, i) => ` <iframe class="slide-frame${i === 0 ? ' active'
|
|
|
277
306
|
}
|
|
278
307
|
});
|
|
279
308
|
|
|
280
|
-
// Fullscreen
|
|
281
309
|
function toggleFullscreen() {
|
|
282
310
|
if (!document.fullscreenElement) {
|
|
283
311
|
document.documentElement.requestFullscreen().catch(() => {});
|
|
@@ -287,7 +315,6 @@ ${slides.map((s, i) => ` <iframe class="slide-frame${i === 0 ? ' active'
|
|
|
287
315
|
}
|
|
288
316
|
document.getElementById('btn-fs').addEventListener('click', toggleFullscreen);
|
|
289
317
|
|
|
290
|
-
// Auto-scale to fit viewport (95% fit)
|
|
291
318
|
function rescale() {
|
|
292
319
|
const vw = viewport.clientWidth;
|
|
293
320
|
const vh = viewport.clientHeight;
|
|
@@ -332,7 +359,7 @@ export function main(args = process.argv.slice(2)) {
|
|
|
332
359
|
}
|
|
333
360
|
|
|
334
361
|
console.log(`Found ${slides.length} slides`);
|
|
335
|
-
writeFileSync(output, buildViewerHtml(slides), 'utf-8');
|
|
362
|
+
writeFileSync(output, buildViewerHtml(slides, { slideMode: options.mode }), 'utf-8');
|
|
336
363
|
console.log(`Built viewer: ${output}`);
|
|
337
364
|
return { slidesDir, output, slides };
|
|
338
365
|
}
|
package/scripts/editor-server.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
import { readdir, readFile, writeFile, mkdtemp, rm, mkdir } from 'node:fs/promises';
|
|
4
4
|
import { watch as fsWatch } from 'node:fs';
|
|
5
5
|
import net from 'node:net';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
6
7
|
import { basename, dirname, join, resolve, relative, sep } from 'node:path';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import { tmpdir } from 'node:os';
|
|
9
10
|
|
|
10
11
|
import {
|
|
11
|
-
SLIDE_SIZE,
|
|
12
12
|
buildCodexEditPrompt,
|
|
13
13
|
buildCodexExecArgs,
|
|
14
14
|
buildClaudeExecArgs,
|
|
@@ -24,6 +24,14 @@ import {
|
|
|
24
24
|
} from '../src/editor/edit-subprocess.js';
|
|
25
25
|
import { buildSlideRuntimeHtml } from '../src/image-contract.js';
|
|
26
26
|
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const {
|
|
29
|
+
DEFAULT_SLIDE_MODE,
|
|
30
|
+
getSlideModeChoices,
|
|
31
|
+
getSlideModeConfig,
|
|
32
|
+
normalizeSlideMode,
|
|
33
|
+
} = require('../src/slide-mode.cjs');
|
|
34
|
+
|
|
27
35
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
36
|
const __dirname = dirname(__filename);
|
|
29
37
|
const PACKAGE_ROOT = process.env.PPT_AGENT_PACKAGE_ROOT || resolve(__dirname, '..');
|
|
@@ -58,6 +66,7 @@ function printUsage() {
|
|
|
58
66
|
process.stdout.write(`Options:\n`);
|
|
59
67
|
process.stdout.write(` --port <number> Server port (default: ${DEFAULT_PORT})\n`);
|
|
60
68
|
process.stdout.write(` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})\n`);
|
|
69
|
+
process.stdout.write(` --mode <mode> Slide mode: ${getSlideModeChoices().join(', ')} (default: ${DEFAULT_SLIDE_MODE})\n`);
|
|
61
70
|
process.stdout.write(` Model is selected in editor UI dropdown.\n`);
|
|
62
71
|
process.stdout.write(` -h, --help Show this help message\n`);
|
|
63
72
|
}
|
|
@@ -66,6 +75,7 @@ function parseArgs(argv) {
|
|
|
66
75
|
const opts = {
|
|
67
76
|
port: DEFAULT_PORT,
|
|
68
77
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
78
|
+
mode: DEFAULT_SLIDE_MODE,
|
|
69
79
|
help: false,
|
|
70
80
|
};
|
|
71
81
|
|
|
@@ -98,6 +108,17 @@ function parseArgs(argv) {
|
|
|
98
108
|
continue;
|
|
99
109
|
}
|
|
100
110
|
|
|
111
|
+
if (arg === '--mode') {
|
|
112
|
+
opts.mode = normalizeSlideMode(argv[i + 1]);
|
|
113
|
+
i += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (arg.startsWith('--mode=')) {
|
|
118
|
+
opts.mode = normalizeSlideMode(arg.slice('--mode='.length));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
if (arg === '--codex-model') {
|
|
102
123
|
// Backward compatibility: ignore legacy CLI option.
|
|
103
124
|
i += 1;
|
|
@@ -116,6 +137,7 @@ function parseArgs(argv) {
|
|
|
116
137
|
}
|
|
117
138
|
|
|
118
139
|
opts.slidesDir = opts.slidesDir.trim();
|
|
140
|
+
opts.mode = normalizeSlideMode(opts.mode);
|
|
119
141
|
|
|
120
142
|
return opts;
|
|
121
143
|
}
|
|
@@ -202,9 +224,9 @@ async function closeBrowser() {
|
|
|
202
224
|
}
|
|
203
225
|
}
|
|
204
226
|
|
|
205
|
-
async function withScreenshotPage(callback) {
|
|
227
|
+
async function withScreenshotPage(callback, screenshotSize) {
|
|
206
228
|
const { browser } = await getScreenshotBrowser();
|
|
207
|
-
const { context, page } = await screenshotMod.createScreenshotPage(browser);
|
|
229
|
+
const { context, page } = await screenshotMod.createScreenshotPage(browser, screenshotSize);
|
|
208
230
|
try {
|
|
209
231
|
return await callback(page);
|
|
210
232
|
} finally {
|
|
@@ -264,7 +286,7 @@ function sanitizeTargets(rawTargets) {
|
|
|
264
286
|
.filter((target) => target.xpath);
|
|
265
287
|
}
|
|
266
288
|
|
|
267
|
-
function normalizeSelections(rawSelections) {
|
|
289
|
+
function normalizeSelections(rawSelections, slideSize) {
|
|
268
290
|
if (!Array.isArray(rawSelections) || rawSelections.length === 0) {
|
|
269
291
|
throw new Error('At least one selection is required.');
|
|
270
292
|
}
|
|
@@ -274,7 +296,7 @@ function normalizeSelections(rawSelections) {
|
|
|
274
296
|
? selection.bbox
|
|
275
297
|
: selection;
|
|
276
298
|
|
|
277
|
-
const bbox = normalizeSelection(selectionSource,
|
|
299
|
+
const bbox = normalizeSelection(selectionSource, slideSize);
|
|
278
300
|
const targets = sanitizeTargets(selection?.targets);
|
|
279
301
|
|
|
280
302
|
return { bbox, targets };
|
|
@@ -562,6 +584,18 @@ async function startServer(opts) {
|
|
|
562
584
|
}
|
|
563
585
|
});
|
|
564
586
|
|
|
587
|
+
app.get('/api/config', (_req, res) => {
|
|
588
|
+
const cfg = getSlideModeConfig(opts.mode);
|
|
589
|
+
res.json({
|
|
590
|
+
slideMode: opts.mode,
|
|
591
|
+
framePx: { width: cfg.framePx.width, height: cfg.framePx.height },
|
|
592
|
+
screenshotPx: { width: cfg.screenshotPx.width, height: cfg.screenshotPx.height },
|
|
593
|
+
sizeLabel: cfg.sizeLabel,
|
|
594
|
+
aspectRatioLabel: cfg.aspectRatioLabel,
|
|
595
|
+
coordinateSpaceLabel: cfg.coordinateSpaceLabel,
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
565
599
|
app.get('/api/models', (_req, res) => {
|
|
566
600
|
res.json({
|
|
567
601
|
models: ALL_MODELS,
|
|
@@ -630,7 +664,7 @@ async function startServer(opts) {
|
|
|
630
664
|
|
|
631
665
|
let normalizedSelections;
|
|
632
666
|
try {
|
|
633
|
-
normalizedSelections = normalizeSelections(selections);
|
|
667
|
+
normalizedSelections = normalizeSelections(selections, getSlideModeConfig(opts.mode).framePx);
|
|
634
668
|
} catch (error) {
|
|
635
669
|
return res.status(400).json({ error: error.message });
|
|
636
670
|
}
|
|
@@ -665,15 +699,15 @@ async function startServer(opts) {
|
|
|
665
699
|
slide,
|
|
666
700
|
screenshotPath,
|
|
667
701
|
`http://localhost:${opts.port}/slides`,
|
|
668
|
-
{ useHttp: true },
|
|
702
|
+
{ useHttp: true, screenshotSize: getSlideModeConfig(opts.mode).screenshotPx },
|
|
669
703
|
);
|
|
670
|
-
});
|
|
704
|
+
}, getSlideModeConfig(opts.mode).screenshotPx);
|
|
671
705
|
|
|
672
706
|
const scaledBoxes = normalizedSelections.map((selection) =>
|
|
673
707
|
scaleSelectionToScreenshot(
|
|
674
708
|
selection.bbox,
|
|
675
|
-
|
|
676
|
-
|
|
709
|
+
getSlideModeConfig(opts.mode).framePx,
|
|
710
|
+
getSlideModeConfig(opts.mode).screenshotPx,
|
|
677
711
|
),
|
|
678
712
|
);
|
|
679
713
|
|
|
@@ -683,6 +717,7 @@ async function startServer(opts) {
|
|
|
683
717
|
slideFile: slide,
|
|
684
718
|
slidePath: toSlidePathLabel(slidesDirectory, slide),
|
|
685
719
|
userPrompt: prompt,
|
|
720
|
+
slideMode: opts.mode,
|
|
686
721
|
selections: normalizedSelections,
|
|
687
722
|
});
|
|
688
723
|
|
package/scripts/figma-export.js
CHANGED
|
@@ -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
|
|