slides-grab 1.2.2 → 1.2.4
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 +19 -13
- package/skills/slides-grab-design/references/beautiful-slide-defaults.md +45 -2
- package/skills/slides-grab-design/references/design-rules.md +5 -0
- package/skills/slides-grab-design/references/detailed-design-rules.md +18 -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.
|
|
@@ -23,20 +23,21 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
|
|
|
23
23
|
## Workflow
|
|
24
24
|
1. Read approved `slide-outline.md` and extract the `style` field from its meta section.
|
|
25
25
|
2. Load the chosen style's full spec from `src/design-styles-data.js` — colors, fonts, layout, signature elements, and things to avoid. If the meta specifies a custom direction instead of a bundled ID, use that custom direction as the design basis.
|
|
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.
|
|
26
|
+
3. Before generating slides, write a quick **visual thesis** (mood/material/energy), a **content plan** (opener → support/proof → detail/story → close/CTA), a **system declaration** (reused layout patterns, max two background colors, max two typefaces, image-led vs text-led slides, where section dividers reset tempo), and the core design tokens (background, surface, text, muted, accent + display/headline/body/caption roles). Ground these tokens in the chosen style's spec. Follow `references/beautiful-slide-defaults.md` for the full working model, content discipline, color discipline, and AI slop tropes to avoid.
|
|
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.
|
|
@@ -53,6 +55,10 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
|
|
|
53
55
|
- Treat opening slides and section dividers like posters, not dashboards.
|
|
54
56
|
- Default to cardless layouts; only add a card when it improves structure or comprehension.
|
|
55
57
|
- Use whitespace, alignment, scale, cropping, and contrast before adding decorative chrome.
|
|
58
|
+
- Do not pad slides with filler copy, dummy stats, or decorative iconography — when a slide feels empty, solve it with layout and scale, not invented content.
|
|
59
|
+
- Pull every color from the approved style spec or the user's brand tokens; extend only with harmonic `oklch()` neighbors. Do not invent fresh standalone hex colors mid-slide.
|
|
60
|
+
- Keep body copy at 14pt minimum on a 720pt × 405pt slide and never render any text below the 10pt absolute floor.
|
|
61
|
+
- Avoid AI slop tropes — aggressive gradient backgrounds, left-border accent cards, SVG-drawn imagery, generic font stacks (Inter/Roboto/Arial), and generic 3×2 icon-plus-blurb grids. See `references/beautiful-slide-defaults.md` for the full list.
|
|
56
62
|
- Prefer `tldraw` for complex diagrams instead of recreating dense node/edge diagrams directly in HTML/CSS.
|
|
57
63
|
- Use `slides-grab tldraw` plus `templates/diagram-tldraw.html` when that gives a cleaner, more export-friendly result.
|
|
58
64
|
- Do not present slides for review until `slides-grab validate --slides-dir <path>` passes.
|
|
@@ -63,5 +69,5 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
|
|
|
63
69
|
For full constraints and style system, follow:
|
|
64
70
|
- `references/design-rules.md`
|
|
65
71
|
- `references/detailed-design-rules.md`
|
|
66
|
-
- `references/beautiful-slide-defaults.md` — slide-specific art direction defaults adapted from OpenAI's frontend design guidance
|
|
72
|
+
- `references/beautiful-slide-defaults.md` — slide-specific art direction defaults adapted from OpenAI's frontend design guidance and Anthropic's Claude design system guidance (content/color discipline, system declaration, AI slop tropes)
|
|
67
73
|
- `references/design-system-full.md` — archived full design system, templates, and advanced pattern guidance
|
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
# Beautiful Slide Defaults
|
|
2
2
|
|
|
3
|
-
Slide-specific art direction guidance adapted from OpenAI's frontend design guidance for GPT-5.4. Use it to make HTML slides feel deliberate, premium, and instantly scannable without breaking `slides-grab`'s export constraints.
|
|
3
|
+
Slide-specific art direction guidance adapted from OpenAI's frontend design guidance for GPT-5.4, with additional distilled principles from Anthropic's Claude design system guidance. Use it to make HTML slides feel deliberate, premium, and instantly scannable without breaking `slides-grab`'s export constraints.
|
|
4
4
|
|
|
5
5
|
## Working Model
|
|
6
6
|
|
|
7
|
-
Before building the deck, write
|
|
7
|
+
Before building the deck, write three things:
|
|
8
8
|
|
|
9
9
|
- **visual thesis** — one sentence describing the mood, material, energy, and imagery treatment
|
|
10
10
|
- **content plan** — opener → support/proof → detail/story → close/CTA or decision
|
|
11
|
+
- **system declaration** — one short paragraph committing to the system you will reuse across the deck
|
|
11
12
|
|
|
12
13
|
If the style direction is still open, gather visual references or a mood board first. Define the core tokens early: `background`, `surface`, `primary text`, `muted text`, `accent`, plus typography roles for `display`, `headline`, `body`, and `caption`.
|
|
13
14
|
|
|
15
|
+
### Vocalize the System Before Designing
|
|
16
|
+
|
|
17
|
+
After the visual thesis and tokens are set, write the system declaration out loud so the deck stays consistent and iteration stays cheap. Name:
|
|
18
|
+
|
|
19
|
+
- the layout patterns you will reuse for titles, section headers, content, quotes, and closing slides
|
|
20
|
+
- the two background colors (max) you will use to introduce intentional rhythm between sections and content slides
|
|
21
|
+
- the two typefaces max, plus the one accent color that carries focus
|
|
22
|
+
- which slides will be image-led, which will be text-led, and where section dividers reset tempo
|
|
23
|
+
|
|
24
|
+
A deck without a declared system drifts. Committing to the system up front is the single cheapest way to make the deck feel deliberate.
|
|
25
|
+
|
|
14
26
|
## Beautiful Defaults for Slides
|
|
15
27
|
|
|
16
28
|
- Start with composition, not components.
|
|
@@ -34,6 +46,35 @@ Use a narrative rhythm that feels intentional:
|
|
|
34
46
|
|
|
35
47
|
Section dividers should reset the visual tempo. Alternate dense proof slides with simpler image-led or statement-led slides so the deck keeps breathing.
|
|
36
48
|
|
|
49
|
+
## Content Discipline
|
|
50
|
+
|
|
51
|
+
Every element must earn its place. When a slide feels empty, solve it with layout, scale, whitespace, and a stronger visual anchor — never by inventing filler content.
|
|
52
|
+
|
|
53
|
+
- Do not pad slides with placeholder copy, dummy stats, or decorative iconography just to fill space.
|
|
54
|
+
- Avoid data slop: invented numbers, vague percentages, and stat strips whose only purpose is to look informational.
|
|
55
|
+
- If you believe a slide needs an extra section, example, page, or call-out beyond the approved outline, ask the user before adding it. The user knows the audience better than you do.
|
|
56
|
+
- Say one thousand no's for every yes. Cutting is a design tool.
|
|
57
|
+
|
|
58
|
+
## Color Discipline
|
|
59
|
+
|
|
60
|
+
- Pull every color from the approved style spec in `src/design-styles-data.js` (or the user's brand tokens when they override the bundled style). Do not invent fresh standalone hex colors mid-slide.
|
|
61
|
+
- If the approved palette is too restrictive for a specific slide, extend it harmonically with `oklch()` — derive neighbors from the existing accent or surface — rather than picking a fresh hex from scratch.
|
|
62
|
+
- Keep one accent color per deck. Two background colors max across the entire deck; use them to introduce rhythm between section dividers and content slides, not to decorate individual slides.
|
|
63
|
+
- Every color must trace back to the approved palette or a documented harmonic extension of it.
|
|
64
|
+
|
|
65
|
+
## AI Slop Tropes to Avoid
|
|
66
|
+
|
|
67
|
+
Common AI-generated patterns that cheapen a deck instantly. Treat these as anti-patterns unless the brief explicitly asks for them.
|
|
68
|
+
|
|
69
|
+
- Aggressive full-slide gradient backgrounds used as the primary surface treatment.
|
|
70
|
+
- Rounded-rectangle containers with a solid left-border accent stripe (the AI "accent card" default).
|
|
71
|
+
- Drawing iconography or product imagery with inline SVG shapes — use a real asset or a `data-image-placeholder` box instead.
|
|
72
|
+
- Overused, generic font families: Inter, Roboto, Arial, Fraunces, and OS system stacks. Prefer Pretendard or the style-specified typeface.
|
|
73
|
+
- Emoji as default iconography. Prefer Lucide; emoji is only for briefs that explicitly call for a playful, native-emoji tone.
|
|
74
|
+
- "Feature card grid" 3×2 layouts of icon + heading + two-line blurb used as the generic answer to any content slide.
|
|
75
|
+
- Faux chrome: drop shadows, subtle gradients, and card borders added to decorate empty space instead of carrying meaning.
|
|
76
|
+
- Placeholder-looking real imagery: stock photos that obviously do not match the topic, or AI-generated images with visible artifacts. Prefer a well-composed `data-image-placeholder` over a bad real image.
|
|
77
|
+
|
|
37
78
|
## Review Litmus
|
|
38
79
|
|
|
39
80
|
Before showing the deck, ask:
|
|
@@ -43,3 +84,5 @@ Before showing the deck, ask:
|
|
|
43
84
|
- Is there one real visual anchor, not just decoration?
|
|
44
85
|
- Would this still feel premium without shadows, cards, or extra chrome?
|
|
45
86
|
- Can any line of copy, badge, or callout be removed without losing meaning?
|
|
87
|
+
- Does every color on the slide trace back to the approved style spec or a documented `oklch` harmonic extension of it?
|
|
88
|
+
- Does any slide lean on an AI slop trope? If so, replace it with composition, typography, or real imagery before review.
|
|
@@ -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,24 @@
|
|
|
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
|
+
## Typography Scale Rules
|
|
28
|
+
- Body copy minimum is 14pt on a 720pt × 405pt slide; prefer 16-20pt so copy reads cleanly at presentation distance and on PDF export.
|
|
29
|
+
- Absolute floor for captions, labels, footnotes, and meta text is 10pt. Never render any text below 10pt.
|
|
30
|
+
- Display and title text should scale well above body copy — prefer 36pt or larger so the slide's main takeaway reads in 3-5 seconds.
|
|
31
|
+
- If content does not fit at the minimum scale, cut content. Do not shrink type to accommodate more.
|
|
32
|
+
- Keep at most two typefaces across the deck. One display/headline face plus one body face is enough.
|
|
33
|
+
|
|
34
|
+
## Color Usage Rules
|
|
35
|
+
- Pull every color from the approved style spec in `src/design-styles-data.js` or the user-provided brand tokens. Do not invent fresh standalone hex colors mid-slide.
|
|
36
|
+
- If the approved palette cannot cover a specific slide, extend it harmonically with `oklch()` — derive the new color from the existing accent, surface, or background — rather than picking a fresh hex from scratch.
|
|
37
|
+
- Keep one accent color per deck. Two background colors max across the entire deck, used to introduce rhythm between section dividers and content slides.
|
|
38
|
+
- Every CSS color must keep the `#` prefix and survive raster export to PPTX/PDF; avoid non-sRGB values that will flatten unexpectedly.
|
|
39
|
+
|
|
40
|
+
## Icon Usage Rules
|
|
41
|
+
- Prefer Lucide as the default icon library for slide UI elements, callouts, and supporting visuals.
|
|
42
|
+
- Do not default to emoji for iconography; reserve emoji for cases where the brief explicitly wants a playful or native-emoji tone.
|
|
43
|
+
- Keep icon sizing, stroke weight, and color aligned with the deck's approved design tokens.
|
|
44
|
+
|
|
27
45
|
## Workflow (Stage 2: Design + Human Review)
|
|
28
46
|
- After slide generation or edits, run `slides-grab validate --slides-dir <path>`.
|
|
29
47
|
- After validation passes, run `slides-grab build-viewer --slides-dir <path>`.
|