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/scripts/html2pdf.js
CHANGED
|
@@ -16,6 +16,12 @@ const {
|
|
|
16
16
|
getResolutionSize,
|
|
17
17
|
normalizeResolutionPreset,
|
|
18
18
|
} = require('../src/export-resolution.cjs');
|
|
19
|
+
const {
|
|
20
|
+
DEFAULT_SLIDE_MODE,
|
|
21
|
+
getSlideModeChoices,
|
|
22
|
+
getSlideModeConfig,
|
|
23
|
+
normalizeSlideMode,
|
|
24
|
+
} = require('../src/slide-mode.cjs');
|
|
19
25
|
|
|
20
26
|
const DEFAULT_OUTPUT = 'slides.pdf';
|
|
21
27
|
const DEFAULT_SLIDES_DIR = 'slides';
|
|
@@ -23,9 +29,7 @@ const DEFAULT_MODE = 'capture';
|
|
|
23
29
|
const DEFAULT_CAPTURE_RESOLUTION = '2160p';
|
|
24
30
|
const PDF_MODES = new Set(['capture', 'print']);
|
|
25
31
|
const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
|
|
26
|
-
const FALLBACK_SLIDE_SIZE = { width: 960, height: 540 };
|
|
27
32
|
const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
|
|
28
|
-
const TARGET_ASPECT_RATIO = 16 / 9;
|
|
29
33
|
const RENDER_SETTLE_MS = 120;
|
|
30
34
|
const CSS_PIXELS_PER_INCH = 96;
|
|
31
35
|
const PDF_POINTS_PER_INCH = 72;
|
|
@@ -40,6 +44,7 @@ function printUsage() {
|
|
|
40
44
|
` --output <path> Output PDF path (default: ${DEFAULT_OUTPUT})`,
|
|
41
45
|
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
42
46
|
` --mode <mode> PDF export mode: capture|print (default: ${DEFAULT_MODE})`,
|
|
47
|
+
` --slide-mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
|
|
43
48
|
` --resolution <preset> Capture raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_CAPTURE_RESOLUTION}; ignored in print mode)`,
|
|
44
49
|
' -h, --help Show this help message',
|
|
45
50
|
'',
|
|
@@ -89,8 +94,8 @@ function cssPixelsToPdfPoints(value) {
|
|
|
89
94
|
return Math.round((normalizeDimension(value, 0) * PDF_POINTS_PER_INCH) / CSS_PIXELS_PER_INCH);
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
async function normalizeCaptureRasterSize(pngBytes, resolution = '') {
|
|
93
|
-
const targetSize = getResolutionSize(resolution);
|
|
97
|
+
async function normalizeCaptureRasterSize(pngBytes, resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
|
|
98
|
+
const targetSize = getResolutionSize(resolution, slideMode);
|
|
94
99
|
if (!targetSize) {
|
|
95
100
|
return pngBytes;
|
|
96
101
|
}
|
|
@@ -140,6 +145,7 @@ export function parseCliArgs(args) {
|
|
|
140
145
|
output: DEFAULT_OUTPUT,
|
|
141
146
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
142
147
|
mode: DEFAULT_MODE,
|
|
148
|
+
slideMode: DEFAULT_SLIDE_MODE,
|
|
143
149
|
resolution: DEFAULT_CAPTURE_RESOLUTION,
|
|
144
150
|
help: false,
|
|
145
151
|
};
|
|
@@ -185,6 +191,17 @@ export function parseCliArgs(args) {
|
|
|
185
191
|
continue;
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
if (arg === '--slide-mode') {
|
|
195
|
+
options.slideMode = normalizeSlideMode(readOptionValue(args, i, '--slide-mode'), { optionName: '--slide-mode' });
|
|
196
|
+
i += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (arg.startsWith('--slide-mode=')) {
|
|
201
|
+
options.slideMode = normalizeSlideMode(arg.slice('--slide-mode='.length), { optionName: '--slide-mode' });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
188
205
|
if (arg === '--resolution') {
|
|
189
206
|
options.resolution = normalizeResolutionPreset(readOptionValue(args, i, '--resolution'));
|
|
190
207
|
i += 1;
|
|
@@ -209,6 +226,7 @@ export function parseCliArgs(args) {
|
|
|
209
226
|
options.output = options.output.trim();
|
|
210
227
|
options.slidesDir = options.slidesDir.trim();
|
|
211
228
|
options.mode = normalizeMode(options.mode);
|
|
229
|
+
options.slideMode = normalizeSlideMode(options.slideMode, { optionName: '--slide-mode' });
|
|
212
230
|
options.resolution = normalizeResolutionPreset(options.resolution);
|
|
213
231
|
if (options.mode === 'print') {
|
|
214
232
|
options.resolution = '';
|
|
@@ -226,9 +244,10 @@ export async function findSlideFiles(slidesDir = resolve(process.cwd(), DEFAULT_
|
|
|
226
244
|
}
|
|
227
245
|
|
|
228
246
|
export function buildPdfOptions(widthPx, heightPx) {
|
|
247
|
+
const fallbackSize = getSlideModeConfig(DEFAULT_SLIDE_MODE).framePx;
|
|
229
248
|
return {
|
|
230
|
-
width: `${normalizeDimension(widthPx,
|
|
231
|
-
height: `${normalizeDimension(heightPx,
|
|
249
|
+
width: `${normalizeDimension(widthPx, fallbackSize.width)}px`,
|
|
250
|
+
height: `${normalizeDimension(heightPx, fallbackSize.height)}px`,
|
|
232
251
|
printBackground: true,
|
|
233
252
|
pageRanges: '1',
|
|
234
253
|
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' },
|
|
@@ -236,22 +255,25 @@ export function buildPdfOptions(widthPx, heightPx) {
|
|
|
236
255
|
};
|
|
237
256
|
}
|
|
238
257
|
|
|
239
|
-
export function buildPageOptions(mode = DEFAULT_MODE, resolution = '') {
|
|
240
|
-
const
|
|
258
|
+
export function buildPageOptions(mode = DEFAULT_MODE, resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
|
|
259
|
+
const { framePx } = getSlideModeConfig(slideMode);
|
|
260
|
+
const targetResolution = normalizeMode(mode) === 'capture' ? getResolutionSize(resolution, slideMode) : null;
|
|
241
261
|
return {
|
|
242
262
|
viewport: {
|
|
243
|
-
width:
|
|
244
|
-
height:
|
|
263
|
+
width: framePx.width,
|
|
264
|
+
height: framePx.height,
|
|
245
265
|
},
|
|
246
266
|
deviceScaleFactor: normalizeMode(mode) === 'capture'
|
|
247
267
|
? targetResolution
|
|
248
|
-
? targetResolution.height /
|
|
268
|
+
? targetResolution.height / framePx.height
|
|
249
269
|
: DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR
|
|
250
270
|
: 1,
|
|
251
271
|
};
|
|
252
272
|
}
|
|
253
273
|
|
|
254
|
-
function chooseSlideFrame(metrics) {
|
|
274
|
+
function chooseSlideFrame(metrics, slideMode = DEFAULT_SLIDE_MODE) {
|
|
275
|
+
const { framePx } = getSlideModeConfig(slideMode);
|
|
276
|
+
const targetAspectRatio = framePx.width / framePx.height;
|
|
255
277
|
const viewportArea = Math.max(1, metrics.viewport.width * metrics.viewport.height);
|
|
256
278
|
const bodyArea = Math.max(1, metrics.body.width * metrics.body.height);
|
|
257
279
|
const bodyScrollArea = Math.max(1, metrics.body.scrollWidth * metrics.body.scrollHeight);
|
|
@@ -269,7 +291,7 @@ function chooseSlideFrame(metrics) {
|
|
|
269
291
|
.map((candidate) => ({
|
|
270
292
|
...candidate,
|
|
271
293
|
area: candidate.width * candidate.height,
|
|
272
|
-
aspectDelta: Math.abs(candidate.width / candidate.height -
|
|
294
|
+
aspectDelta: Math.abs(candidate.width / candidate.height - targetAspectRatio),
|
|
273
295
|
coverage: (candidate.width * candidate.height) / viewportArea,
|
|
274
296
|
}))
|
|
275
297
|
.sort((left, right) => right.area - left.area);
|
|
@@ -359,7 +381,7 @@ export async function waitForSlideRenderReady(page, options = {}) {
|
|
|
359
381
|
}, { settleMs, runReadySignal: shouldRunReadySignal });
|
|
360
382
|
}
|
|
361
383
|
|
|
362
|
-
export async function detectSlideFrame(page) {
|
|
384
|
+
export async function detectSlideFrame(page, slideMode = DEFAULT_SLIDE_MODE) {
|
|
363
385
|
const metrics = await page.evaluate(() => {
|
|
364
386
|
function toBox(element) {
|
|
365
387
|
const rect = element.getBoundingClientRect();
|
|
@@ -398,12 +420,13 @@ export async function detectSlideFrame(page) {
|
|
|
398
420
|
};
|
|
399
421
|
});
|
|
400
422
|
|
|
401
|
-
const frame = chooseSlideFrame(metrics);
|
|
423
|
+
const frame = chooseSlideFrame(metrics, slideMode);
|
|
424
|
+
const fallbackSize = getSlideModeConfig(slideMode).framePx;
|
|
402
425
|
return {
|
|
403
426
|
x: normalizeDimension(frame.x, 0),
|
|
404
427
|
y: normalizeDimension(frame.y, 0),
|
|
405
|
-
width: normalizeDimension(frame.width,
|
|
406
|
-
height: normalizeDimension(frame.height,
|
|
428
|
+
width: normalizeDimension(frame.width, fallbackSize.width),
|
|
429
|
+
height: normalizeDimension(frame.height, fallbackSize.height),
|
|
407
430
|
candidateIndex: Number.isInteger(frame.candidateIndex) ? frame.candidateIndex : null,
|
|
408
431
|
source: frame.source,
|
|
409
432
|
};
|
|
@@ -641,20 +664,22 @@ export async function renderSlideToPdf(page, slideFile, slidesDir, options = {})
|
|
|
641
664
|
const slidePath = join(slidesDir, slideFile);
|
|
642
665
|
const slideUrl = pathToFileURL(slidePath).href;
|
|
643
666
|
const mode = normalizeMode(options.mode ?? DEFAULT_MODE);
|
|
667
|
+
const slideMode = normalizeSlideMode(options.slideMode ?? DEFAULT_SLIDE_MODE, { optionName: '--slide-mode' });
|
|
644
668
|
const captureResolution = mode === 'capture' ? normalizeResolutionPreset(options.resolution ?? '') : '';
|
|
645
669
|
|
|
646
670
|
await page.goto(slideUrl, { waitUntil: 'load' });
|
|
647
671
|
await waitForSlideRenderReady(page, options);
|
|
648
672
|
|
|
649
|
-
const slideFrame = await detectSlideFrame(page);
|
|
673
|
+
const slideFrame = await detectSlideFrame(page, slideMode);
|
|
650
674
|
const normalizedSlideFrame = await isolateSlideFrame(page, slideFrame);
|
|
651
675
|
await normalizeBodyToSlideFrame(page, normalizedSlideFrame);
|
|
652
676
|
await waitForSlideRenderReady(page, { ...options, runReadySignal: false });
|
|
653
677
|
|
|
654
678
|
if (mode === 'capture') {
|
|
679
|
+
const fallbackSize = getSlideModeConfig(slideMode).framePx;
|
|
655
680
|
const viewportSize = {
|
|
656
|
-
width: normalizeDimension(normalizedSlideFrame.width,
|
|
657
|
-
height: normalizeDimension(normalizedSlideFrame.height,
|
|
681
|
+
width: normalizeDimension(normalizedSlideFrame.width, fallbackSize.width),
|
|
682
|
+
height: normalizeDimension(normalizedSlideFrame.height, fallbackSize.height),
|
|
658
683
|
};
|
|
659
684
|
await page.setViewportSize(viewportSize);
|
|
660
685
|
await waitForSlideRenderReady(page, { ...options, runReadySignal: false });
|
|
@@ -679,9 +704,10 @@ export async function renderSlideToPdf(page, slideFile, slidesDir, options = {})
|
|
|
679
704
|
height: viewportSize.height,
|
|
680
705
|
},
|
|
681
706
|
});
|
|
682
|
-
const normalizedPngBytes = await normalizeCaptureRasterSize(pngBytes, captureResolution);
|
|
707
|
+
const normalizedPngBytes = await normalizeCaptureRasterSize(pngBytes, captureResolution, slideMode);
|
|
683
708
|
return {
|
|
684
709
|
mode,
|
|
710
|
+
slideMode,
|
|
685
711
|
width: normalizedSlideFrame.width,
|
|
686
712
|
height: normalizedSlideFrame.height,
|
|
687
713
|
pngBytes: normalizedPngBytes,
|
|
@@ -692,6 +718,7 @@ export async function renderSlideToPdf(page, slideFile, slidesDir, options = {})
|
|
|
692
718
|
|
|
693
719
|
return {
|
|
694
720
|
mode,
|
|
721
|
+
slideMode,
|
|
695
722
|
width: normalizedSlideFrame.width,
|
|
696
723
|
height: normalizedSlideFrame.height,
|
|
697
724
|
pdfBytes: await page.pdf(buildPdfOptions(normalizedSlideFrame.width, normalizedSlideFrame.height)),
|
|
@@ -740,14 +767,14 @@ async function main() {
|
|
|
740
767
|
}
|
|
741
768
|
|
|
742
769
|
const slidesDir = resolve(process.cwd(), options.slidesDir);
|
|
743
|
-
await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PDF export' });
|
|
770
|
+
await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PDF export', slideMode: options.slideMode });
|
|
744
771
|
const slideFiles = await findSlideFiles(slidesDir);
|
|
745
772
|
if (slideFiles.length === 0) {
|
|
746
773
|
throw new Error(`No slide-*.html files found in: ${slidesDir}`);
|
|
747
774
|
}
|
|
748
775
|
|
|
749
776
|
const browser = await chromium.launch({ headless: true });
|
|
750
|
-
const page = await browser.newPage(buildPageOptions(options.mode, options.resolution));
|
|
777
|
+
const page = await browser.newPage(buildPageOptions(options.mode, options.resolution, options.slideMode));
|
|
751
778
|
const diagnostics = createSlideDiagnostics();
|
|
752
779
|
diagnostics.attach(page);
|
|
753
780
|
const renderedSlides = [];
|
|
@@ -758,6 +785,7 @@ async function main() {
|
|
|
758
785
|
try {
|
|
759
786
|
const slideResult = await renderSlideToPdf(page, slideFile, slidesDir, {
|
|
760
787
|
mode: options.mode,
|
|
788
|
+
slideMode: options.slideMode,
|
|
761
789
|
resolution: options.resolution,
|
|
762
790
|
});
|
|
763
791
|
renderedSlides.push(slideResult);
|
package/scripts/html2pptx.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ensureOutputDirectory, SLIDE_FILE_PATTERN, sortFigmaSlideFiles } from '
|
|
|
10
10
|
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
const html2pptx = require('../src/html2pptx.cjs');
|
|
13
|
+
const { DEFAULT_SLIDE_MODE, getSlideModeChoices, getSlideModeConfig, normalizeSlideMode } = require('../src/slide-mode.cjs');
|
|
13
14
|
|
|
14
15
|
const DEFAULT_SLIDES_DIR = 'slides';
|
|
15
16
|
const DEFAULT_OUTPUT = 'output.pptx';
|
|
@@ -22,6 +23,7 @@ function printUsage() {
|
|
|
22
23
|
'Options:',
|
|
23
24
|
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
24
25
|
` --output <path> Output PPTX file (default: ${DEFAULT_OUTPUT})`,
|
|
26
|
+
` --mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
|
|
25
27
|
' -h, --help Show this help message',
|
|
26
28
|
'',
|
|
27
29
|
'Experimental / unstable PPTX export. Treat output as best-effort only.',
|
|
@@ -42,6 +44,7 @@ function parseArgs(args) {
|
|
|
42
44
|
const options = {
|
|
43
45
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
44
46
|
output: DEFAULT_OUTPUT,
|
|
47
|
+
mode: DEFAULT_SLIDE_MODE,
|
|
45
48
|
help: false,
|
|
46
49
|
};
|
|
47
50
|
|
|
@@ -74,6 +77,17 @@ function parseArgs(args) {
|
|
|
74
77
|
continue;
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
if (arg === '--mode') {
|
|
81
|
+
options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
|
|
82
|
+
i += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (arg.startsWith('--mode=')) {
|
|
87
|
+
options.mode = normalizeSlideMode(arg.slice('--mode='.length));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
77
91
|
throw new Error(`Unknown option: ${arg}`);
|
|
78
92
|
}
|
|
79
93
|
|
|
@@ -87,6 +101,7 @@ function parseArgs(args) {
|
|
|
87
101
|
|
|
88
102
|
options.slidesDir = options.slidesDir.trim();
|
|
89
103
|
options.output = options.output.trim();
|
|
104
|
+
options.mode = normalizeSlideMode(options.mode);
|
|
90
105
|
return options;
|
|
91
106
|
}
|
|
92
107
|
|
|
@@ -118,7 +133,13 @@ async function main() {
|
|
|
118
133
|
const files = getHtmlSlides(slidesDir);
|
|
119
134
|
|
|
120
135
|
const pres = new PptxGenJS();
|
|
121
|
-
|
|
136
|
+
const { pptxSizeIn } = getSlideModeConfig(options.mode);
|
|
137
|
+
pres.defineLayout({
|
|
138
|
+
name: 'SLIDES_GRAB_HTML2PPTX',
|
|
139
|
+
width: pptxSizeIn.width,
|
|
140
|
+
height: pptxSizeIn.height,
|
|
141
|
+
});
|
|
142
|
+
pres.layout = 'SLIDES_GRAB_HTML2PPTX';
|
|
122
143
|
|
|
123
144
|
for (const file of files) {
|
|
124
145
|
await html2pptx(resolve(slidesDir, file), pres);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
import { chromium } from 'playwright';
|
|
6
7
|
|
|
7
8
|
import {
|
|
@@ -20,6 +21,9 @@ import {
|
|
|
20
21
|
selectSlideFiles,
|
|
21
22
|
} from '../src/validation/core.js';
|
|
22
23
|
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const { DEFAULT_SLIDE_MODE } = require('../src/slide-mode.cjs');
|
|
26
|
+
|
|
23
27
|
export {
|
|
24
28
|
DEFAULT_SLIDES_DIR,
|
|
25
29
|
ensureSlidesPassValidation,
|
|
@@ -160,7 +164,7 @@ function peekValidateFormat(args = []) {
|
|
|
160
164
|
return DEFAULT_VALIDATE_FORMAT;
|
|
161
165
|
}
|
|
162
166
|
|
|
163
|
-
export async function validateSlides(slidesDir, { selectedSlides = [] } = {}) {
|
|
167
|
+
export async function validateSlides(slidesDir, { mode = DEFAULT_SLIDE_MODE, selectedSlides = [] } = {}) {
|
|
164
168
|
const slideFiles = selectSlideFiles(await findSlideFiles(slidesDir), selectedSlides, slidesDir);
|
|
165
169
|
if (slideFiles.length === 0) {
|
|
166
170
|
throw new Error(`No slide-*.html files found in: ${slidesDir}`);
|
|
@@ -171,13 +175,26 @@ export async function validateSlides(slidesDir, { selectedSlides = [] } = {}) {
|
|
|
171
175
|
const page = await context.newPage();
|
|
172
176
|
|
|
173
177
|
try {
|
|
174
|
-
const slides = await scanSlides(page, slidesDir, slideFiles);
|
|
175
|
-
return createValidationResult(slides);
|
|
178
|
+
const slides = await scanSlides(page, slidesDir, slideFiles, mode);
|
|
179
|
+
return createValidationResult(slides, mode);
|
|
176
180
|
} finally {
|
|
177
181
|
await browser.close();
|
|
178
182
|
}
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
function peekValidateMode(args = []) {
|
|
186
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
187
|
+
const arg = args[i];
|
|
188
|
+
if (arg === '--mode') {
|
|
189
|
+
return args[i + 1] || DEFAULT_SLIDE_MODE;
|
|
190
|
+
}
|
|
191
|
+
if (arg.startsWith('--mode=')) {
|
|
192
|
+
return arg.slice('--mode='.length) || DEFAULT_SLIDE_MODE;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return DEFAULT_SLIDE_MODE;
|
|
196
|
+
}
|
|
197
|
+
|
|
181
198
|
export async function main(args = process.argv.slice(2)) {
|
|
182
199
|
const options = parseValidateCliArgs(args);
|
|
183
200
|
if (options.help) {
|
|
@@ -186,7 +203,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
const slidesDir = resolve(process.cwd(), options.slidesDir);
|
|
189
|
-
const result = await validateSlides(slidesDir, { selectedSlides: options.slides });
|
|
206
|
+
const result = await validateSlides(slidesDir, { mode: options.mode, selectedSlides: options.slides });
|
|
190
207
|
process.stdout.write(formatValidationResult(result, options.format));
|
|
191
208
|
if (result.summary.failedSlides > 0) {
|
|
192
209
|
process.exitCode = 1;
|
|
@@ -197,7 +214,7 @@ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(imp
|
|
|
197
214
|
|
|
198
215
|
if (isMain) {
|
|
199
216
|
main().catch((error) => {
|
|
200
|
-
const failure = createValidationFailure(error);
|
|
217
|
+
const failure = createValidationFailure(error, peekValidateMode(process.argv.slice(2)));
|
|
201
218
|
process.stdout.write(formatValidationResult(failure, peekValidateFormat(process.argv.slice(2))));
|
|
202
219
|
process.exit(1);
|
|
203
220
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slides-grab
|
|
3
|
-
description: End-to-end presentation workflow for Codex. Use when making a full presentation from scratch — planning, designing slides, editing, and exporting. PDF
|
|
3
|
+
description: End-to-end presentation workflow for Codex. Use when making a full presentation from scratch — planning, designing slides, editing, and exporting. PDF and per-slide PNG are preferred; PPTX/Figma export is experimental / unstable.
|
|
4
4
|
metadata:
|
|
5
|
-
short-description: Full pipeline from topic to PDF + experimental / unstable PPTX/Figma export
|
|
5
|
+
short-description: Full pipeline from topic to PDF/PNG + experimental / unstable PPTX/Figma export
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# slides-grab Skill (Codex) - Full Workflow Orchestrator
|
|
@@ -48,9 +48,13 @@ Use the installed **slides-grab-design** skill.
|
|
|
48
48
|
Use the installed **slides-grab-export** skill.
|
|
49
49
|
|
|
50
50
|
1. Confirm user wants conversion.
|
|
51
|
-
2.
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
2. Pick the primary target:
|
|
52
|
+
- Card-news / Instagram-style decks → `slides-grab png --slides-dir <path> --slide-mode card-news --resolution 2160p` (see `slides-grab-card-news`).
|
|
53
|
+
- Widescreen decks → `slides-grab pdf --slides-dir <path> --output <name>.pdf`.
|
|
54
|
+
3. Per-slide PNG (any mode): `slides-grab png --slides-dir <path> --output-dir <path>/out-png --resolution 2160p`.
|
|
55
|
+
4. PPTX (optional, **experimental / unstable**): `slides-grab convert --slides-dir <path> --output <name>.pptx`.
|
|
56
|
+
5. Figma-importable PPTX (optional, **experimental / unstable**): `slides-grab figma --slides-dir <path> --output <name>-figma.pptx`.
|
|
57
|
+
6. Report results.
|
|
54
58
|
|
|
55
59
|
---
|
|
56
60
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: slides-grab-card-news
|
|
3
|
+
description: Generate square Instagram-style card news by reusing the slides-grab workflow with card-news mode enabled. Defaults to per-slide PNG export.
|
|
4
|
+
metadata:
|
|
5
|
+
short-description: Square card-news workflow on top of slides-grab (PNG by default)
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# slides-grab Card News Skill (Codex)
|
|
9
|
+
|
|
10
|
+
Use this when the user wants card news instead of a widescreen presentation.
|
|
11
|
+
|
|
12
|
+
## Goal
|
|
13
|
+
Reuse the existing slides-grab plan/design/export workflow, but generate **square** outputs optimized for Instagram posts. Per-slide PNG is the default export since Instagram and most card-news distribution channels consume images, not PDFs.
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
1. Reuse the normal outline process from `slides-grab-plan`.
|
|
17
|
+
2. During design and review, keep every card at **720pt x 720pt** and run:
|
|
18
|
+
- `slides-grab validate --slides-dir <path> --mode card-news`
|
|
19
|
+
- `slides-grab build-viewer --slides-dir <path> --mode card-news`
|
|
20
|
+
- `slides-grab edit --slides-dir <path> --mode card-news`
|
|
21
|
+
3. During export, **default to per-slide PNG** for Instagram-ready output:
|
|
22
|
+
- `slides-grab png --slides-dir <path> --slide-mode card-news --resolution 2160p`
|
|
23
|
+
- Optional `--output-dir <path>/out-png` to choose the output folder (defaults to `<slides-dir>/out-png`).
|
|
24
|
+
4. Only produce PDF/PPTX/Figma when the user explicitly asks for it:
|
|
25
|
+
- `slides-grab pdf --slides-dir <path> --slide-mode card-news --output <name>.pdf`
|
|
26
|
+
- `slides-grab convert --slides-dir <path> --mode card-news --output <name>.pptx` (**experimental / unstable**)
|
|
27
|
+
- `slides-grab figma --slides-dir <path> --mode card-news --output <name>-figma.pptx` (**experimental / unstable**)
|
|
28
|
+
5. Remind the user that PPTX/Figma exports remain experimental / unstable.
|
|
29
|
+
|
|
30
|
+
## Rules
|
|
31
|
+
- Optimize layouts for square Instagram-style card news, not 16:9 slides.
|
|
32
|
+
- Default the export to `slides-grab png --slide-mode card-news`; only switch to PDF/PPTX/Figma when the user explicitly requests it.
|
|
33
|
+
- Reuse existing design, viewer, editor, and export policy wherever possible.
|
|
34
|
+
- Do **not** implement SNS/Instagram publishing automation.
|
|
35
|
+
- Use the packaged CLI and bundled skills only.
|
|
@@ -26,17 +26,18 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
|
|
|
26
26
|
3. Before generating slides, write a quick **visual thesis** (mood/material/energy), a **content plan** (opener → support/proof → detail/story → close/CTA), and the core design tokens (background, surface, text, muted, accent + display/headline/body/caption roles). Ground these tokens in the chosen style's spec.
|
|
27
27
|
4. If you need to confirm or revisit the approved bundled style before designing, re-run `slides-grab list-styles` and open the gallery from `slides-grab preview-styles` so the Stage 2 deck stays aligned with the Stage 1 direction.
|
|
28
28
|
5. Generate slide HTML files with 2-digit numbering in selected `--slides-dir`.
|
|
29
|
-
6. When a slide
|
|
30
|
-
7.
|
|
31
|
-
8. If the
|
|
32
|
-
9. If the
|
|
33
|
-
10.
|
|
34
|
-
11.
|
|
35
|
-
12.
|
|
36
|
-
13.
|
|
37
|
-
14.
|
|
38
|
-
15.
|
|
39
|
-
16.
|
|
29
|
+
6. When a slide needs iconography, prefer Lucide as the default icon library. Use clean Lucide icons before falling back to emoji, and only use emoji when the brief explicitly calls for them.
|
|
30
|
+
7. When a slide explicitly needs bespoke imagery, when the user asks for an image, or when stronger imagery would materially improve the slide, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>` to generate a local asset with Nano Banana Pro and save it under `<slides-dir>/assets/`.
|
|
31
|
+
8. If the deck needs a complex diagram (architecture, workflows, relationship maps, multi-node concepts), create the diagram in `tldraw`, export it with `slides-grab tldraw`, and treat the result as a local slide asset under `<slides-dir>/assets/`.
|
|
32
|
+
9. If the slide needs a local video, store the video under `<slides-dir>/assets/`, reference it as `./assets/<file>`, and prefer a `poster="./assets/<file>"` thumbnail so PDF export uses a stable still image.
|
|
33
|
+
10. If the source video starts on YouTube or another supported page, use `slides-grab fetch-video --url <youtube-url> --slides-dir <path>` (or `yt-dlp` directly if needed) to download it into `<slides-dir>/assets/` before saving the slide HTML.
|
|
34
|
+
11. Run `slides-grab validate --slides-dir <path>` after generation or edits.
|
|
35
|
+
12. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
|
|
36
|
+
13. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.
|
|
37
|
+
14. Launch the interactive editor for visual review: `slides-grab edit --slides-dir <path>`
|
|
38
|
+
15. Iterate on user feedback by editing only requested slide files, then re-run validation after each edit round.
|
|
39
|
+
16. When the user confirms editing is complete, suggest: build the viewer (`slides-grab build-viewer --slides-dir <path>`) for a final read-only preview, or proceed to export (PDF/PPTX).
|
|
40
|
+
17. Keep revising until user approves conversion stage.
|
|
40
41
|
|
|
41
42
|
## Rules
|
|
42
43
|
- Keep slide size 720pt x 405pt.
|
|
@@ -44,6 +45,7 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
|
|
|
44
45
|
- Put local images and videos under `<slides-dir>/assets/` and reference them as `./assets/<file>`.
|
|
45
46
|
- Allow `data:` URLs when the slide must be fully self-contained.
|
|
46
47
|
- 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>`.
|
|
48
|
+
- Prefer Lucide for default slide iconography. Avoid emoji as the default icon treatment unless the brief explicitly asks for emoji.
|
|
47
49
|
- Prefer `slides-grab image` with Nano Banana Pro for bespoke slide imagery before reaching for remote URLs.
|
|
48
50
|
- If `GOOGLE_API_KEY` (or `GEMINI_API_KEY`) is unavailable or the Nano Banana API fails, ask the user for a Google API key or fall back to web search + download into `<slides-dir>/assets/`.
|
|
49
51
|
- Prefer local videos with a `poster="./assets/<file>"` thumbnail so PDF export uses the still image.
|
|
@@ -19,6 +19,11 @@ These are the packaged design rules for installable `slides-grab` skills.
|
|
|
19
19
|
- CSS colors must include `#`
|
|
20
20
|
- Avoid CSS gradients for PPTX-targeted decks
|
|
21
21
|
|
|
22
|
+
## Icon guidance
|
|
23
|
+
- Prefer Lucide as the default icon library when a slide needs iconography.
|
|
24
|
+
- Avoid emoji as the default icon treatment; only use emoji when the brief explicitly calls for them.
|
|
25
|
+
- Keep icons visually consistent within a deck (stroke weight, size, and color should follow the slide's design tokens).
|
|
26
|
+
|
|
22
27
|
## Asset rules
|
|
23
28
|
- Store deck-local assets in `<slides-dir>/assets/`
|
|
24
29
|
- Reference deck-local assets as `./assets/<file>`
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
- All text must be inside `<p>`, `<h1>`-`<h6>`, `<ul>`, `<ol>`, or `<li>`.
|
|
25
25
|
- Never place text directly in `<div>` or `<span>`.
|
|
26
26
|
|
|
27
|
+
## Icon Usage Rules
|
|
28
|
+
- Prefer Lucide as the default icon library for slide UI elements, callouts, and supporting visuals.
|
|
29
|
+
- Do not default to emoji for iconography; reserve emoji for cases where the brief explicitly wants a playful or native-emoji tone.
|
|
30
|
+
- Keep icon sizing, stroke weight, and color aligned with the deck's approved design tokens.
|
|
31
|
+
|
|
27
32
|
## Workflow (Stage 2: Design + Human Review)
|
|
28
33
|
- After slide generation or edits, run `slides-grab validate --slides-dir <path>`.
|
|
29
34
|
- After validation passes, run `slides-grab build-viewer --slides-dir <path>`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: slides-grab-export
|
|
3
|
-
description: Stage 3 conversion skill for Codex. Convert approved HTML slides to PDF and to experimental / unstable PPTX/Figma outputs
|
|
3
|
+
description: Stage 3 conversion skill for Codex. Convert approved HTML slides to PDF or per-slide PNG reliably, and to experimental / unstable PPTX/Figma outputs on a best-effort basis.
|
|
4
4
|
metadata:
|
|
5
5
|
short-description: Convert slides and run conversion checks
|
|
6
6
|
---
|
|
@@ -10,24 +10,31 @@ metadata:
|
|
|
10
10
|
Use this only after the user approves design output.
|
|
11
11
|
|
|
12
12
|
## Goal
|
|
13
|
-
Convert reviewed slide HTML into PDF reliably, and into experimental / unstable PPTX/Figma outputs on a best-effort basis.
|
|
13
|
+
Convert reviewed slide HTML into PDF or per-slide PNG reliably, and into experimental / unstable PPTX/Figma outputs on a best-effort basis.
|
|
14
14
|
|
|
15
15
|
## Inputs
|
|
16
16
|
- Approved `<slides-dir>/slide-*.html`
|
|
17
17
|
- Optional output path settings
|
|
18
18
|
|
|
19
19
|
## Outputs
|
|
20
|
-
- Presentation artifact (`.
|
|
20
|
+
- Presentation artifact (`.pdf`, `.png` per slide, or `.pptx`)
|
|
21
21
|
|
|
22
22
|
## Workflow
|
|
23
23
|
1. Confirm user approval for conversion.
|
|
24
|
-
2.
|
|
25
|
-
- `slides-grab
|
|
26
|
-
|
|
24
|
+
2. Pick the right primary target:
|
|
25
|
+
- Card-news / Instagram-style decks → `slides-grab png --slides-dir <path> --slide-mode card-news --resolution 2160p` (see `slides-grab-card-news`).
|
|
26
|
+
- Widescreen slide decks → `slides-grab pdf --slides-dir <path> --output <name>.pdf`.
|
|
27
|
+
3. When per-slide raster output is needed (card news, social posts, thumbnails):
|
|
28
|
+
- `slides-grab png --slides-dir <path> --output-dir <path>/out-png --resolution 2160p`
|
|
29
|
+
- Add `--slide-mode card-news` for 1:1 cards.
|
|
30
|
+
4. If the user also wants a PDF deck:
|
|
27
31
|
- `slides-grab pdf --slides-dir <path> --output <name>.pdf`
|
|
28
|
-
|
|
32
|
+
- Add `--slide-mode card-news` when the deck is square.
|
|
33
|
+
5. If the user wants PPTX (experimental / unstable):
|
|
34
|
+
- `slides-grab convert --slides-dir <path> --output <name>.pptx`
|
|
35
|
+
6. If the user wants Figma-importable PPTX (experimental / unstable):
|
|
29
36
|
- `slides-grab figma --slides-dir <path> --output <name>-figma.pptx`
|
|
30
|
-
|
|
37
|
+
7. Report success/failure with actionable errors.
|
|
31
38
|
|
|
32
39
|
## Rules
|
|
33
40
|
- Do not modify slide content during conversion stage unless explicitly requested.
|
|
@@ -84,7 +84,7 @@ Every HTML slide must include proper body dimensions:
|
|
|
84
84
|
- **CRITICAL: Never use CSS gradients (`linear-gradient`, `radial-gradient`)** - They don't convert to PowerPoint
|
|
85
85
|
- **ALWAYS create gradient/icon PNGs FIRST using Sharp, then reference in HTML**
|
|
86
86
|
- For gradients: Rasterize SVG to PNG background images
|
|
87
|
-
- For icons: Rasterize
|
|
87
|
+
- For icons: Rasterize Lucide SVG to PNG images
|
|
88
88
|
- All visual effects must be pre-rendered as raster images before HTML rendering
|
|
89
89
|
|
|
90
90
|
**Rasterizing Icons with Sharp:**
|
|
@@ -93,11 +93,11 @@ Every HTML slide must include proper body dimensions:
|
|
|
93
93
|
const React = require('react');
|
|
94
94
|
const ReactDOMServer = require('react-dom/server');
|
|
95
95
|
const sharp = require('sharp');
|
|
96
|
-
const {
|
|
96
|
+
const { House } = require('lucide-react');
|
|
97
97
|
|
|
98
98
|
async function rasterizeIconPng(IconComponent, color, size = "256", filename) {
|
|
99
99
|
const svgString = ReactDOMServer.renderToStaticMarkup(
|
|
100
|
-
React.createElement(IconComponent, { color: `#${color}`, size: size })
|
|
100
|
+
React.createElement(IconComponent, { color: `#${color}`, size: size, strokeWidth: 1.75 })
|
|
101
101
|
);
|
|
102
102
|
|
|
103
103
|
// Convert SVG to PNG using Sharp
|
|
@@ -109,7 +109,7 @@ async function rasterizeIconPng(IconComponent, color, size = "256", filename) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Usage: Rasterize icon before using in HTML
|
|
112
|
-
const iconPath = await rasterizeIconPng(
|
|
112
|
+
const iconPath = await rasterizeIconPng(House, "4472c4", "256", "home-icon.png");
|
|
113
113
|
// Then reference in HTML: <img src="home-icon.png" style="width: 40pt; height: 40pt;">
|
|
114
114
|
```
|
|
115
115
|
|