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.
@@ -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.
@@ -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 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.
@@ -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 two things:
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>`.