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.
@@ -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, FALLBACK_SLIDE_SIZE.width)}px`,
231
- height: `${normalizeDimension(heightPx, FALLBACK_SLIDE_SIZE.height)}px`,
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 targetResolution = normalizeMode(mode) === 'capture' ? getResolutionSize(resolution) : null;
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: FALLBACK_SLIDE_SIZE.width,
244
- height: FALLBACK_SLIDE_SIZE.height,
263
+ width: framePx.width,
264
+ height: framePx.height,
245
265
  },
246
266
  deviceScaleFactor: normalizeMode(mode) === 'capture'
247
267
  ? targetResolution
248
- ? targetResolution.height / FALLBACK_SLIDE_SIZE.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 - TARGET_ASPECT_RATIO),
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, FALLBACK_SLIDE_SIZE.width),
406
- height: normalizeDimension(frame.height, FALLBACK_SLIDE_SIZE.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, FALLBACK_SLIDE_SIZE.width),
657
- height: normalizeDimension(normalizedSlideFrame.height, FALLBACK_SLIDE_SIZE.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);
@@ -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
- pres.layout = 'LAYOUT_WIDE';
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 is preferred; PPTX/Figma export is experimental / unstable.
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. Export to PPTX: `slides-grab convert --slides-dir <path> --output <name>.pptx` (**experimental / unstable**)
52
- 3. Export to PDF (if requested): `slides-grab pdf --slides-dir <path> --output <name>.pdf`
53
- 4. Report results.
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 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/`.
30
- 7. 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/`.
31
- 8. 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.
32
- 9. 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.
33
- 10. Run `slides-grab validate --slides-dir <path>` after generation or edits.
34
- 11. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
35
- 12. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.
36
- 13. Launch the interactive editor for visual review: `slides-grab edit --slides-dir <path>`
37
- 14. Iterate on user feedback by editing only requested slide files, then re-run validation after each edit round.
38
- 15. 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).
39
- 16. Keep revising until user approves conversion stage.
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, then validate artifacts.
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 (`.pptx` or `.pdf`)
20
+ - Presentation artifact (`.pdf`, `.png` per slide, or `.pptx`)
21
21
 
22
22
  ## Workflow
23
23
  1. Confirm user approval for conversion.
24
- 2. Run conversion command:
25
- - `slides-grab convert --slides-dir <path> --output <name>.pptx` (**experimental / unstable**)
26
- 3. If requested, run PDF conversion:
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
- 4. If requested, run Figma export:
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
- 5. Report success/failure with actionable errors.
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 react-icons SVG to PNG images
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 { FaHome } = require('react-icons/fa');
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(FaHome, "4472c4", "256", "home-icon.png");
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