slides-grab 1.3.1 → 1.4.0

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-ko.md CHANGED
@@ -92,13 +92,13 @@ slides-grab image --prompt "..." # 로컬 슬라이드 이미지 생성
92
92
  slides-grab fetch-video --url <youtube-url> --slides-dir decks/my-deck # yt-dlp로 동영상 에셋 다운로드
93
93
  slides-grab tldraw # .tldr 다이어그램을 슬라이드 크기의 로컬 SVG로 렌더링
94
94
  slides-grab list-templates # 사용 가능한 슬라이드 템플릿 표시
95
- slides-grab list-styles # 번들된 95개 디자인 스타일 표시
95
+ slides-grab list-styles # 기본으로 선택 가능한 92개 디자인 스타일 표시 (3개 소스-앨리어스를 포함해 95개 해석 가능; --all로 전체 표시)
96
96
  slides-grab preview-styles # 95개 스타일 미리보기 갤러리를 브라우저에서 열기
97
97
  ```
98
98
 
99
99
  ## 디자인 스타일 모음
100
100
 
101
- slides-grab은 [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles)에서 파생된 30개 스타일, slides-grab 고유 스타일 5개, [epoko77-ai/design-diversity](https://github.com/epoko77-ai/design-diversity)에서 파생된 PPT 팩 60개를 포함해 총 95개 디자인 스타일을 제공합니다. 에이전트에게 특정 스타일을 요청하거나 완전히 커스텀 디자인을 요청할 수 있습니다.
101
+ slides-grab은 [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles)에서 파생된 30개 스타일, slides-grab 고유 스타일 5개, [epoko77-ai/design-diversity](https://github.com/epoko77-ai/design-diversity)에서 파생된 PPT 팩 60개를 포함해 총 95개 디자인 스타일을 제공합니다. 60개 design-diversity PPT 팩은 분류됩니다 — 직접 중복은 빌트인에 앨리어스 처리(기본 숨김)되고, 유사 중복은 `relatedStyleIds`로 연결되며, 완전히 새 팩은 추가됩니다. 따라서 92개만 기본으로 선택 가능하며 95개 모두 해석 가능합니다(`list-styles --all`로 앨리어스 확인). 에이전트에게 특정 스타일을 요청하거나 완전히 커스텀 디자인을 요청할 수 있습니다.
102
102
 
103
103
  ```bash
104
104
  slides-grab list-styles
package/README.md CHANGED
@@ -100,13 +100,13 @@ slides-grab image --prompt "..." # Generate a local slide image with god-tibo
100
100
  slides-grab fetch-video --url <youtube-url> --slides-dir decks/my-deck # Download a local video asset with yt-dlp
101
101
  slides-grab tldraw # Render a .tldr diagram into a slide-sized local SVG asset
102
102
  slides-grab list-templates # Show available slide templates
103
- slides-grab list-styles # Show 95 bundled design styles (browse, preview, select)
103
+ slides-grab list-styles # Show 92 selectable design styles by default (95 resolvable incl. 3 source-aliases; --all shows all)
104
104
  slides-grab preview-styles # Open the 95-style visual gallery in browser
105
105
  ```
106
106
 
107
107
  ## Design Style Collections
108
108
 
109
- slides-grab bundles 95 design styles: 30 derived from [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles), 5 slides-grab originals, and 60 PPT packs derived from [epoko77-ai/design-diversity](https://github.com/epoko77-ai/design-diversity). Agents can also create fully custom designs beyond the bundled collection.
109
+ slides-grab bundles 95 design styles: 30 derived from [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles), 5 slides-grab originals, and 60 PPT packs derived from [epoko77-ai/design-diversity](https://github.com/epoko77-ai/design-diversity). The 60 design-diversity PPT packs are classified — direct duplicates are aliased to builtins (hidden by default), near-duplicates are linked via `relatedStyleIds`, and net-new packs are added — so 92 are selectable by default while all 95 remain resolvable (use `list-styles --all` to see aliases). Agents can also create fully custom designs beyond the bundled collection.
110
110
 
111
111
  ```bash
112
112
  slides-grab list-styles # Browse the catalog
package/bin/ppt-agent.js CHANGED
@@ -370,10 +370,12 @@ program
370
370
  program
371
371
  .command('list-styles')
372
372
  .description('List bundled design styles agents and users can reference during slide generation')
373
- .action(async () => {
373
+ .option('--all', 'Include source-alias styles that resolve to a builtin (hidden by default)')
374
+ .action(async (options = {}) => {
374
375
  try {
375
- const { listDesignStyles } = await import('../src/design-styles.js');
376
- const styles = listDesignStyles();
376
+ const { listDesignStyles, listSelectableDesignStyles } = await import('../src/design-styles.js');
377
+ const showAll = Boolean(options.all);
378
+ const styles = showAll ? listDesignStyles() : listSelectableDesignStyles();
377
379
 
378
380
  if (styles.length === 0) {
379
381
  console.log('No bundled design styles found.');
@@ -382,11 +384,33 @@ program
382
384
 
383
385
  console.log('Available design styles:\n');
384
386
  for (const style of styles) {
385
- console.log(` ${style.id.padEnd(22)} ${style.title}`);
387
+ let tag;
388
+ if (style.classification === 'builtin') {
389
+ tag = '[builtin]';
390
+ } else if (style.classification === 'source-variant') {
391
+ tag = '[variant]';
392
+ } else if (style.classification === 'source-alias') {
393
+ tag = `[alias -> ${style.aliasOf}]`;
394
+ } else {
395
+ tag = '[new]';
396
+ }
397
+ console.log(` ${style.id.padEnd(38)} ${tag} ${style.title}`);
386
398
  console.log(` ${style.mood} · ${style.bestFor}`);
399
+ if (style.classification === 'source-variant' && Array.isArray(style.relatedStyleIds) && style.relatedStyleIds.length) {
400
+ console.log(` related: ${style.relatedStyleIds.join(', ')}`);
401
+ }
387
402
  }
388
403
 
389
- console.log(`\nTotal: ${styles.length} styles`);
404
+ const totalResolvable = listDesignStyles().length;
405
+ if (!showAll) {
406
+ const hiddenCount = totalResolvable - styles.length;
407
+ console.log(`\nTotal: ${styles.length} selectable styles`);
408
+ console.log(`${hiddenCount} source-alias slug(s) hidden (resolve to a builtin); use --all to show all ${totalResolvable} resolvable styles`);
409
+ } else {
410
+ const selectableCount = listSelectableDesignStyles().length;
411
+ const aliasCount = totalResolvable - selectableCount;
412
+ console.log(`\nTotal: ${styles.length} resolvable styles (${selectableCount} selectable, ${aliasCount} aliases)`);
413
+ }
390
414
  console.log('Preview: slides-grab preview-styles [--style <id>]');
391
415
  } catch (error) {
392
416
  reportCliError(error);
@@ -495,6 +519,10 @@ program
495
519
  console.log(`Best for: ${style.bestFor}`);
496
520
  if (style.source?.repo) console.log(`Source: ${style.source.repo}`);
497
521
  if (style.source?.url) console.log(`Source URL: ${style.source.url}`);
522
+ if (style.classification) console.log(`Classification: ${style.classification}`);
523
+ if (style.aliasOf) console.log(`Alias of: ${style.aliasOf}`);
524
+ if (Array.isArray(style.aliases) && style.aliases.length) console.log(`Aliases: ${style.aliases.join(', ')}`);
525
+ if (Array.isArray(style.relatedStyleIds) && style.relatedStyleIds.length) console.log(`Related styles: ${style.relatedStyleIds.join(', ')}`);
498
526
  if (Array.isArray(style.background)) {
499
527
  console.log('\n## Background');
500
528
  for (const b of style.background) console.log(`- ${b}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slides-grab",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Agent-first presentation framework — plan, design, and visually edit HTML slides with Claude Code or Codex, then export to PDF or experimental/unstable PPTX/Figma formats",
5
5
  "license": "MIT",
6
6
  "author": "vkehfdl1",
@@ -10,8 +10,8 @@
10
10
 
11
11
  import { readFileSync, writeFileSync, readdirSync } from 'fs';
12
12
  import { createRequire } from 'node:module';
13
- import { join, resolve } from 'path';
14
- import { fileURLToPath } from 'url';
13
+ import { dirname, extname, join, resolve } from 'path';
14
+ import { fileURLToPath, pathToFileURL } from 'url';
15
15
 
16
16
  import { buildSlideRuntimeHtml } from '../src/image-contract.js';
17
17
 
@@ -24,6 +24,25 @@ const {
24
24
  } = require('../src/slide-mode.cjs');
25
25
 
26
26
  const DEFAULT_SLIDES_DIR = 'slides';
27
+ const LOCAL_ASSET_PREFIX = './assets/';
28
+
29
+ // Deliberate sandbox decision: allow-scripts (so Chart.js and other in-slide JS
30
+ // render in the viewer) WITHOUT allow-same-origin. Omitting allow-same-origin gives
31
+ // each iframe an opaque origin so first-party slide content cannot reach the parent
32
+ // viewer document. Because relative URLs break under an opaque origin, local
33
+ // ./assets/ references are inlined as data: URLs in loadSlides()/inlineLocalAssetsForSrcdoc.
34
+ const SLIDE_FRAME_SANDBOX = 'allow-scripts';
35
+
36
+ const MIME_BY_EXTENSION = new Map([
37
+ ['.avif', 'image/avif'],
38
+ ['.gif', 'image/gif'],
39
+ ['.jpg', 'image/jpeg'],
40
+ ['.jpeg', 'image/jpeg'],
41
+ ['.mp4', 'video/mp4'],
42
+ ['.png', 'image/png'],
43
+ ['.svg', 'image/svg+xml'],
44
+ ['.webp', 'image/webp'],
45
+ ]);
27
46
 
28
47
  function printUsage() {
29
48
  process.stdout.write(
@@ -127,18 +146,61 @@ export function escapeForSrcdoc(html) {
127
146
  }
128
147
 
129
148
  export function loadSlides(slidesDir) {
149
+ const slideBaseHref = pathToFileURL(`${resolve(slidesDir)}/`).href;
130
150
  return findSlideFiles(slidesDir).map((file) => {
131
- const html = readFileSync(join(slidesDir, file), 'utf-8');
151
+ const slidePath = join(slidesDir, file);
152
+ const html = inlineLocalAssetsForSrcdoc(readFileSync(slidePath, 'utf-8'), slidePath);
132
153
  return {
133
154
  file,
134
155
  html: buildSlideRuntimeHtml(html, {
135
- baseHref: './',
156
+ baseHref: slideBaseHref,
136
157
  slideFile: file,
137
158
  }),
138
159
  };
139
160
  });
140
161
  }
141
162
 
163
+ function getMimeType(filePath) {
164
+ return MIME_BY_EXTENSION.get(extname(filePath).toLowerCase()) || 'application/octet-stream';
165
+ }
166
+
167
+ function toDataUrl(filePath) {
168
+ const bytes = readFileSync(filePath);
169
+ return `data:${getMimeType(filePath)};base64,${bytes.toString('base64')}`;
170
+ }
171
+
172
+ export function inlineLocalAssetsForSrcdoc(html, slidePath) {
173
+ const resolveAsset = (source) => {
174
+ if (!source.startsWith(LOCAL_ASSET_PREFIX)) {
175
+ return source;
176
+ }
177
+
178
+ try {
179
+ return toDataUrl(resolve(dirname(slidePath), source));
180
+ } catch {
181
+ return source;
182
+ }
183
+ };
184
+
185
+ return html
186
+ .replace(/\b(src|poster)=("([^"]*)"|'([^']*)')/gi, (match, attribute, quoted, doubleValue, singleValue) => {
187
+ const value = doubleValue ?? singleValue ?? '';
188
+ const nextValue = resolveAsset(value);
189
+ if (nextValue === value) {
190
+ return match;
191
+ }
192
+ const quote = quoted.startsWith("'") ? "'" : '"';
193
+ return `${attribute}=${quote}${nextValue}${quote}`;
194
+ })
195
+ .replace(/url\(\s*(['"]?)(\.\/assets\/[^'")]+)\1\s*\)/gi, (match, quote, source) => {
196
+ const nextValue = resolveAsset(source);
197
+ if (nextValue === source) {
198
+ return match;
199
+ }
200
+ return `url("${nextValue}")`;
201
+ });
202
+ }
203
+
142
204
  export function buildViewerHtml(slides, { slideMode = DEFAULT_SLIDE_MODE } = {}) {
143
205
  const { framePt } = getSlideModeConfig(slideMode);
144
206
 
@@ -261,7 +323,7 @@ export function buildViewerHtml(slides, { slideMode = DEFAULT_SLIDE_MODE } = {})
261
323
 
262
324
  <div class="slide-viewport" id="viewport">
263
325
  <div class="slide-scaler" id="scaler">
264
- ${slides.map((s, i) => ` <iframe class="slide-frame${i === 0 ? ' active' : ''}" data-slide="${i + 1}" srcdoc="${escapeForSrcdoc(s.html)}" sandbox="allow-same-origin"></iframe>`).join('\n')}
326
+ ${slides.map((s, i) => ` <iframe class="slide-frame${i === 0 ? ' active' : ''}" data-slide="${i + 1}" srcdoc="${escapeForSrcdoc(s.html)}" sandbox="${SLIDE_FRAME_SANDBOX}"></iframe>`).join('\n')}
265
327
  </div>
266
328
  </div>
267
329
  </div>
@@ -35,12 +35,14 @@ Use the installed **slides-grab-design** skill.
35
35
  5. If validation fails, automatically fix the slide HTML/CSS until validation passes.
36
36
  6. For bespoke slide imagery, use `slides-grab image --prompt "<prompt>" --slides-dir <path>` so the default god-tibo-imagen provider (reuses local Codex ChatGPT login — no API key required) saves a local asset under `<slides-dir>/assets/`.
37
37
  7. For complex diagrams (architecture, workflows, relationship maps, multi-node concepts), prefer `tldraw` over hand-built HTML/CSS diagrams. Render the asset with `slides-grab tldraw`, store it under `<slides-dir>/assets/`, and place it in the slide with a normal `<img>`.
38
- 8. Keep local videos under `<slides-dir>/assets/`, prefer `poster="./assets/<file>"` thumbnails, and use `slides-grab fetch-video --url <youtube-url> --slides-dir <path>` (or `yt-dlp` directly) when the source starts on a supported web page.
39
- 9. The default provider, god-tibo-imagen, reuses the local Codex ChatGPT login (`~/.codex/auth.json`) — run `codex login` once; no API key required. ⚠️ god-tibo-imagen uses an unsupported private Codex backend that may break without notice. Optional alternatives: `--provider codex` (Codex/OpenAI gpt-image-2 via `OPENAI_API_KEY`; maps `--aspect-ratio` to the nearest supported OpenAI image size; `--image-size 2K|4K` is Nano Banana-only) or `--provider nano-banana` (Google `gemini-3-pro-image-preview` via `GOOGLE_API_KEY` or `GEMINI_API_KEY`; supports `--image-size 2K|4K`). If credentials are unavailable, fall back to web search/download into `<slides-dir>/assets/`.
40
- 10. **Run the design gate as an adversarial quality loop** before presenting the deck (`skills/slides-grab-design/references/design-gate.md`): capture render evidence (`slides-grab png … --output-dir <path>/gate-preview`), dispatch two parallel read-only reviewer passes when the current runtime supports subagents/tasks (A: System Contract / Constraint Integrity, B: Audience Impact / Expressive Readability), and synthesize contract coherence against audience impact into one Design Gate Report with a verdict. If subagents are unavailable, run the same two passes sequentially from the bundled reference. Review Litmus is the shared audience-success tie-breaker, not a third pass. **Resolve every Critical finding** (unreadable text, palette violation, AI slop trope as a slide's primary treatment, key slide with no visual anchor, invented data), re-run validate, capture fresh PNGs, and re-run both adversarial passes until the latest rendered state reaches `Proceed` or requires `Rethink approach`. For `Proceed`, the Pass A/B reports must follow the CLI-enforced structure in `slides-grab-design/references/design-gate.md` (role title, `VERDICT: PASS`, confidence, rendered PNG evidence filenames, current `slide-*.html: <sha256>` fingerprints, `Unresolved Critical: 0`, `Blocking findings: None`, findings table, and required checks marked `PASS`). When the verdict is `Proceed`, record the export-unlocking receipt with `slides-grab design-gate --slides-dir <path> --verdict proceed --pass-a-report <pass-a.md> --pass-b-report <pass-b.md>`. If the CLI rejects the reports, continue the fix validate → render → A/B review loop. Track deferred Minor/Note in `<slides-dir>/design-debt.md`.
41
- 11. Launch the interactive editor for review: `slides-grab edit --slides-dir <path>`
42
- 12. Revise slides based on user feedback via the editor, then re-run validation and the design gate after each edit round that changes layout, color, typography, imagery, or content density.
43
- 13. When the user confirms editing is complete, suggest next steps: build the viewer (`slides-grab build-viewer --slides-dir <path>`) for a final preview, or proceed directly to Stage 3 for PDF/PPTX export.
38
+ 8. For quantitative slides, use Chart.js as the default charting library. Start from `templates/chart.html` when useful, keep chart scripts inside each `slide-*.html`, set `animation: false`, `responsive: true`, and `maintainAspectRatio: false`, and size every `<canvas>` with a stable wrapper so validation can inspect real painted pixels.
39
+ 9. Keep local videos under `<slides-dir>/assets/`, prefer `poster="./assets/<file>"` thumbnails, and use `slides-grab fetch-video --url <youtube-url> --slides-dir <path>` (or `yt-dlp` directly) when the source starts on a supported web page.
40
+ 10. The default provider, god-tibo-imagen, reuses the local Codex ChatGPT login (`~/.codex/auth.json`) run `codex login` once; no API key required. ⚠️ god-tibo-imagen uses an unsupported private Codex backend that may break without notice. Optional alternatives: `--provider codex` (Codex/OpenAI gpt-image-2 via `OPENAI_API_KEY`; maps `--aspect-ratio` to the nearest supported OpenAI image size; `--image-size 2K|4K` is Nano Banana-only) or `--provider nano-banana` (Google `gemini-3-pro-image-preview` via `GOOGLE_API_KEY` or `GEMINI_API_KEY`; supports `--image-size 2K|4K`). If credentials are unavailable, fall back to web search/download into `<slides-dir>/assets/`.
41
+ 11. **Run the design gate as an adversarial quality loop** before presenting the deck (`skills/slides-grab-design/references/design-gate.md`): capture render evidence (`slides-grab png --output-dir <path>/gate-preview`), dispatch two parallel read-only reviewer passes when the current runtime supports subagents/tasks (A: System Contract / Constraint Integrity, B: Audience Impact / Expressive Readability), and synthesize contract coherence against audience impact into one Design Gate Report with a verdict. If subagents are unavailable, run the same two passes sequentially from the bundled reference. Review Litmus is the shared audience-success tie-breaker, not a third pass. **Resolve every Critical finding** (unreadable text, palette violation, AI slop trope as a slide's primary treatment, key slide with no visual anchor, invented data), re-run validate, capture fresh PNGs, and re-run both adversarial passes until the latest rendered state reaches `Proceed` or requires `Rethink approach`. For `Proceed`, the Pass A/B reports must follow the CLI-enforced structure in `slides-grab-design/references/design-gate.md` (role title, `VERDICT: PASS`, confidence, rendered PNG evidence filenames, current `slide-*.html: <sha256>` fingerprints, `Unresolved Critical: 0`, `Blocking findings: None`, findings table, and required checks marked `PASS`). When the verdict is `Proceed`, record the export-unlocking receipt with `slides-grab design-gate --slides-dir <path> --verdict proceed --pass-a-report <pass-a.md> --pass-b-report <pass-b.md>`. If the CLI rejects the reports, continue the fix → validate → render → A/B review loop. Track deferred Minor/Note in `<slides-dir>/design-debt.md`.
42
+ 12. Launch the interactive editor for review: `slides-grab edit --slides-dir <path>`
43
+ 13. For decks with Chart.js or other `<canvas>` charts, also build the viewer (`slides-grab build-viewer --slides-dir <path>`) and visually confirm the charts render in `viewer.html`; validation reports `empty-canvas` when a visible canvas has no painted pixels.
44
+ 14. Revise slides based on user feedback via the editor, then re-run validation and the design gate after each edit round that changes layout, color, typography, imagery, or content density.
45
+ 15. When the user confirms editing is complete, suggest next steps: build the viewer (`slides-grab build-viewer --slides-dir <path>`) for a final preview, or proceed directly to Stage 3 for PDF/PPTX export.
44
46
 
45
47
  **Do not proceed to Stage 3 without approval, and never while any Critical design-gate finding is unresolved.**
46
48
 
@@ -70,6 +72,7 @@ Use the installed **slides-grab-export** skill.
70
72
  7. Use the stage skills as the source of truth for plan, design, and export rules.
71
73
  8. When a slide needs a complex diagram, default to a `tldraw`-generated asset unless the user explicitly asks for a different approach.
72
74
  9. When a slide needs bespoke imagery, prefer the default god-tibo-imagen provider via `slides-grab image` (reuses local Codex ChatGPT login — no API key required) and keep the saved asset local under `<slides-dir>/assets/`.
75
+ 10. When a slide needs a chart, default to Chart.js in-slide canvas rendering and require `slides-grab validate` plus `slides-grab build-viewer` review so blank canvases are caught before export.
73
76
 
74
77
  ## Reference
75
78
  - `references/presentation-workflow-reference.md` — archived end-to-end workflow guidance from the legacy skill set
@@ -29,11 +29,12 @@ Use the installed **slides-grab-design** skill.
29
29
  6. Build the viewer: `slides-grab build-viewer --slides-dir <path>`
30
30
  7. When a slide calls for bespoke imagery, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>` so the default god-tibo-imagen provider (reuses local Codex ChatGPT login — no API key required) saves a local asset under `<slides-dir>/assets/`.
31
31
  8. For complex diagrams (architecture, workflows, relationship maps, multi-node concepts), prefer `tldraw`. Render a local diagram asset with `slides-grab tldraw`, store it under `<slides-dir>/assets/`, and place it into the slide with a normal `<img>`.
32
- 9. Keep local videos under `<slides-dir>/assets/`, prefer `poster="./assets/<file>"` thumbnails, and use `slides-grab fetch-video --url <youtube-url> --slides-dir <path>` (or `yt-dlp` directly) when the source starts on a supported web page.
33
- 10. The default provider, god-tibo-imagen, reuses the local Codex ChatGPT login (`~/.codex/auth.json`) — run `codex login` once; no API key required. ⚠️ god-tibo-imagen uses an unsupported private Codex backend that may break without notice. Optional alternatives: `--provider codex` (Codex/OpenAI gpt-image-2 via `OPENAI_API_KEY`; maps `--aspect-ratio` to the nearest supported OpenAI image size; `--image-size 2K|4K` is Nano Banana-only) or `--provider nano-banana` (Google `gemini-3-pro-image-preview` via `GOOGLE_API_KEY` or `GEMINI_API_KEY`; supports `--image-size 2K|4K`). If credentials are unavailable, fall back to web search + download into `<slides-dir>/assets/`.
34
- 11. Present viewer to user for review.
35
- 12. Revise individual slides based on feedback, then re-run validation and rebuild the viewer.
36
- 13. Optionally launch the visual editor: `slides-grab edit --slides-dir <path>`
32
+ 9. For quantitative slides, use Chart.js as the default charting library, preferably starting from `templates/chart.html`. Keep scripts in `slide-*.html`, disable animation, use stable canvas wrappers, and fix any `empty-canvas` validation error before review.
33
+ 10. Keep local videos under `<slides-dir>/assets/`, prefer `poster="./assets/<file>"` thumbnails, and use `slides-grab fetch-video --url <youtube-url> --slides-dir <path>` (or `yt-dlp` directly) when the source starts on a supported web page.
34
+ 11. The default provider, god-tibo-imagen, reuses the local Codex ChatGPT login (`~/.codex/auth.json`) — run `codex login` once; no API key required. ⚠️ god-tibo-imagen uses an unsupported private Codex backend that may break without notice. Optional alternatives: `--provider codex` (Codex/OpenAI gpt-image-2 via `OPENAI_API_KEY`; maps `--aspect-ratio` to the nearest supported OpenAI image size; `--image-size 2K|4K` is Nano Banana-only) or `--provider nano-banana` (Google `gemini-3-pro-image-preview` via `GOOGLE_API_KEY` or `GEMINI_API_KEY`; supports `--image-size 2K|4K`). If credentials are unavailable, fall back to web search + download into `<slides-dir>/assets/`.
35
+ 12. Present viewer to user for review. For Chart.js decks, specifically confirm charts render inside `viewer.html`.
36
+ 13. Revise individual slides based on feedback, then re-run validation and rebuild the viewer.
37
+ 14. Optionally launch the visual editor: `slides-grab edit --slides-dir <path>`
37
38
 
38
39
  **Do not proceed to Stage 3 without approval.**
39
40
 
@@ -57,3 +58,4 @@ Use the installed **slides-grab-export** skill.
57
58
  5. **Call out export risk clearly**: PPTX and Figma export are experimental / unstable and should be described as best-effort output.
58
59
  6. **Prefer tldraw for complex diagrams**: Use `slides-grab tldraw` for diagram-heavy slides unless the user explicitly wants another rendering path.
59
60
  7. **Prefer Codex/OpenAI for bespoke imagery**: Use `slides-grab image` when a slide benefits from generated imagery, and keep the result as a local asset under `<slides-dir>/assets/`.
61
+ 8. **Prefer Chart.js for charts**: Use real canvas charts for quantitative claims and require validation plus viewer review before export.
@@ -33,17 +33,19 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
33
33
  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.
34
34
  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 the default god-tibo-imagen provider (which reuses the local Codex ChatGPT login — no API key required) and save it under `<slides-dir>/assets/`.
35
35
  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/`.
36
- 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.
37
- 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.
38
- 11. Run `slides-grab validate --slides-dir <path>` after generation or edits.
39
- 12. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
40
- 13. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.
41
- 14. **Run the design gate as an adversarial quality loop** (`references/design-gate.md`) before showing the deck: (a) capture render evidence with `slides-grab png --slides-dir <path> --output-dir <path>/gate-preview --resolution 1080p`; (b) run two read-only reviewer passes — Pass A (System Contract / Constraint Integrity) and Pass B (Audience Impact / Expressive Readability) — that open the rendered PNGs directly, using runtime-native subagents/tasks in parallel when available or sequential passes when not; (c) synthesize contract coherence against audience impact into a single Design Gate Report ending in a verdict (`Proceed` / `Revise and re-review` / `Rethink approach`). Review Litmus is the shared audience-success tie-breaker, not a third pass. Keep the two reviewer passes distinct from the slide-building pass. The render evidence aims the reviewers; it is not the verdict. For `Proceed`, each pass report must satisfy the CLI-enforced structure in `references/design-gate.md`: role title, `VERDICT: PASS`, confidence, rendered PNG evidence filenames, current `slide-*.html: <sha256>` fingerprints, `Unresolved Critical: 0`, `Blocking findings: None`, a findings table, and all required checks marked `PASS`.
42
- 15. **Repeat until the latest rendered state survives the gate.** Critical findings (unreadable text, palette violation, an AI slop trope used as a slide's primary treatment, a key slide with no real visual anchor, invented data shown as real) hard-block progress. Fix the source HTML/CSS, re-run `slides-grab validate`, capture fresh PNGs, then re-run both adversarial passes until the verdict is `Proceed` (zero unresolved Critical) or `Rethink approach` requires redesigning the visual thesis/system. When the verdict is `Proceed`, write the export-unlocking receipt with `slides-grab design-gate --slides-dir <path> --verdict proceed --pass-a-report <pass-a.md> --pass-b-report <pass-b.md>`. If the CLI rejects the reports, treat that as the loop still failing: fix the missing evidence/checks or unresolved findings, re-render, re-review, and retry. Record deferred Minor/Note findings in `<slides-dir>/design-debt.md`; never silently drop a finding.
43
- 16. Launch the interactive editor for visual review: `slides-grab edit --slides-dir <path>`
44
- 17. Iterate on user feedback by editing only requested slide files, then re-run validation and the design gate after each edit round that changes layout, color, typography, imagery, or content density.
45
- 18. 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).
46
- 19. Keep revising until user approves conversion stage.
36
+ 9. If a slide needs a chart, default to Chart.js (`<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>`) and start from `templates/chart.html` when helpful. Keep the script in the slide HTML, use real data from the outline/research, set `animation: false`, `responsive: true`, and `maintainAspectRatio: false`, wrap each `<canvas>` in a fixed-size or flex-stable container, and use direct labels or concise legends that match the approved style tokens.
37
+ 10. 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.
38
+ 11. 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.
39
+ 12. Run `slides-grab validate --slides-dir <path>` after generation or edits. Treat `empty-canvas` as a chart-rendering failure: fix the Chart.js target id, script loading, canvas sizing, or chart initialization until the canvas paints.
40
+ 13. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
41
+ 14. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.
42
+ 15. **Run the design gate as an adversarial quality loop** (`references/design-gate.md`) before showing the deck: (a) capture render evidence with `slides-grab png --slides-dir <path> --output-dir <path>/gate-preview --resolution 1080p`; (b) run two read-only reviewer passes Pass A (System Contract / Constraint Integrity) and Pass B (Audience Impact / Expressive Readability) that open the rendered PNGs directly, using runtime-native subagents/tasks in parallel when available or sequential passes when not; (c) synthesize contract coherence against audience impact into a single Design Gate Report ending in a verdict (`Proceed` / `Revise and re-review` / `Rethink approach`). Review Litmus is the shared audience-success tie-breaker, not a third pass. Keep the two reviewer passes distinct from the slide-building pass. The render evidence aims the reviewers; it is not the verdict. For `Proceed`, each pass report must satisfy the CLI-enforced structure in `references/design-gate.md`: role title, `VERDICT: PASS`, confidence, rendered PNG evidence filenames, current `slide-*.html: <sha256>` fingerprints, `Unresolved Critical: 0`, `Blocking findings: None`, a findings table, and all required checks marked `PASS`.
43
+ 16. **Repeat until the latest rendered state survives the gate.** Critical findings (unreadable text, palette violation, an AI slop trope used as a slide's primary treatment, a key slide with no real visual anchor, invented data shown as real) hard-block progress. Fix the source HTML/CSS, re-run `slides-grab validate`, capture fresh PNGs, then re-run both adversarial passes until the verdict is `Proceed` (zero unresolved Critical) or `Rethink approach` requires redesigning the visual thesis/system. When the verdict is `Proceed`, write the export-unlocking receipt with `slides-grab design-gate --slides-dir <path> --verdict proceed --pass-a-report <pass-a.md> --pass-b-report <pass-b.md>`. If the CLI rejects the reports, treat that as the loop still failing: fix the missing evidence/checks or unresolved findings, re-render, re-review, and retry. Record deferred Minor/Note findings in `<slides-dir>/design-debt.md`; never silently drop a finding.
44
+ 17. Launch the interactive editor for visual review: `slides-grab edit --slides-dir <path>`
45
+ 18. For decks with Chart.js or other canvas charts, also run `slides-grab build-viewer --slides-dir <path>` and open `viewer.html` to confirm charts render inside the generated iframe viewer, not only as standalone `slide-*.html` files.
46
+ 19. Iterate on user feedback by editing only requested slide files, then re-run validation and the design gate after each edit round that changes layout, color, typography, imagery, or content density.
47
+ 20. 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).
48
+ 21. Keep revising until user approves conversion stage.
47
49
 
48
50
  ## Rules
49
51
  - Keep slide size 720pt x 405pt.
@@ -67,6 +69,8 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
67
69
  - 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.
68
70
  - Prefer `tldraw` for complex diagrams instead of recreating dense node/edge diagrams directly in HTML/CSS.
69
71
  - Use `slides-grab tldraw` plus `templates/diagram-tldraw.html` when that gives a cleaner, more export-friendly result.
72
+ - Prefer Chart.js for charts. Do not fake charts with decorative div bars when the slide is communicating data; real `<canvas>` charts are validated for painted pixels and blank canvases fail as `empty-canvas`.
73
+ - Keep Chart.js chart animation disabled for deterministic validation/export, and keep canvas dimensions stable with CSS so the drawing buffer and layout box are both non-zero.
70
74
  - Do not present slides for review until `slides-grab validate --slides-dir <path>` passes.
71
75
  - Do not present slides for review, and do not advance toward export, while any **Critical** design-gate finding is unresolved (`references/design-gate.md`). Critical hard-blocks; Major findings are listed for user acceptance; Minor/Note findings may be tracked. `slides-grab pdf`, `slides-grab convert`, and `slides-grab figma` require a fresh `slides-grab design-gate` Proceed receipt.
72
76
  - Do not start conversion before approval.
@@ -24,6 +24,14 @@ These are the packaged design rules for installable `slides-grab` skills.
24
24
  - Avoid emoji as the default icon treatment; only use emoji when the brief explicitly calls for them.
25
25
  - Keep icons visually consistent within a deck (stroke weight, size, and color should follow the slide's design tokens).
26
26
 
27
+ ## Chart guidance
28
+ - Use Chart.js as the default charting library for quantitative slides; `templates/chart.html` is the packaged starting point.
29
+ - Keep chart scripts inside the relevant `slide-*.html` file and initialize charts after the target `<canvas>` exists.
30
+ - Use `animation: false`, `responsive: true`, and `maintainAspectRatio: false` for deterministic validation, viewer preview, and export.
31
+ - Put each `<canvas>` inside a stable fixed-size or flex-stable wrapper so the layout box and drawing buffer are non-zero.
32
+ - Use real data from the outline/research notes, label sample data as sample/synthetic, and avoid decorative pseudo-charts for data claims.
33
+ - Run `slides-grab validate --slides-dir <path>` and fix any `empty-canvas` error before building the viewer or exporting.
34
+
27
35
  ## Asset rules
28
36
  - Store deck-local assets in `<slides-dir>/assets/`
29
37
  - Reference deck-local assets as `./assets/<file>`
@@ -61,6 +69,7 @@ These are the packaged design rules for installable `slides-grab` skills.
61
69
  - Generate or edit only the needed slide files.
62
70
  - Prefer `slides-grab image` before remote image sourcing when the slide needs bespoke imagery.
63
71
  - Prefer `tldraw` for complex diagrams instead of hand-building dense diagram geometry in HTML/CSS.
72
+ - Prefer Chart.js for charts and verify the generated `viewer.html` when a deck contains `<canvas>` charts.
64
73
  - Re-run validation after every generation/edit pass.
65
74
  - Rebuild the viewer only after validation passes.
66
75
  - Do not move to export until the user approves the reviewed deck.
@@ -42,12 +42,21 @@
42
42
  - Do not default to emoji for iconography; reserve emoji for cases where the brief explicitly wants a playful or native-emoji tone.
43
43
  - Keep icon sizing, stroke weight, and color aligned with the deck's approved design tokens.
44
44
 
45
+ ## Chart Usage Rules
46
+ - Prefer Chart.js for bar, line, pie, doughnut, and mixed quantitative slides.
47
+ - Start from `templates/chart.html` when a chart slide needs a proven structure.
48
+ - Keep the `<canvas>` in a stable wrapper and set Chart.js options to `animation: false`, `responsive: true`, and `maintainAspectRatio: false`.
49
+ - Match chart colors to the approved style tokens; avoid rainbow palettes unless the data categories require distinct hues.
50
+ - Direct-label the key data point or keep legends short. Do not let chart legends compete with the slide headline.
51
+ - `slides-grab validate` treats a visible unpainted canvas as `empty-canvas`; fix script loading, target ids, sizing, or data before review/export.
52
+
45
53
  ## Workflow (Stage 2: Design + Human Review)
46
54
  - After slide generation or edits, run `slides-grab validate --slides-dir <path>`.
47
55
  - After validation passes, run `slides-grab build-viewer --slides-dir <path>`.
48
56
  - Edit only the relevant HTML file during revision loops.
49
57
  - When the brief explicitly calls for an image, the user requests one, or the slide clearly benefits from it, prefer `slides-grab image` before falling back to remote image sourcing.
50
58
  - Prefer `slides-grab tldraw` + local exported assets for architecture, workflow, relationship, and other complex diagrams.
59
+ - For Chart.js decks, open the generated viewer and confirm charts render inside `viewer.html`, not only as standalone slide files.
51
60
  - Keep local videos and their poster thumbnails together under `<slides-dir>/assets/`.
52
61
  - Never start PPTX conversion without explicit approval.
53
62
  - Never forget to build the viewer after slide changes.
@@ -30,16 +30,18 @@ Convert reviewed slide HTML into PDF or per-slide PNG reliably, and into experim
30
30
  4. If the user also wants a PDF deck:
31
31
  - `slides-grab pdf --slides-dir <path> --output <name>.pdf`
32
32
  - Add `--slide-mode card-news` when the deck is square.
33
- 5. If the user wants PPTX (experimental / unstable):
33
+ 5. For decks with Chart.js or other `<canvas>` charts, confirm `slides-grab validate --slides-dir <path>` passes without `empty-canvas`, then build/open `viewer.html` once before export. Chart.js charts should use disabled animation so PDF/PNG capture sees the final painted state.
34
+ 6. If the user wants PPTX (experimental / unstable):
34
35
  - `slides-grab convert --slides-dir <path> --output <name>.pptx`
35
- 6. If the user wants Figma-importable PPTX (experimental / unstable):
36
+ 7. If the user wants Figma-importable PPTX (experimental / unstable):
36
37
  - `slides-grab figma --slides-dir <path> --output <name>-figma.pptx`
37
- 7. Report success/failure with actionable errors.
38
+ 8. Report success/failure with actionable errors.
38
39
 
39
40
  ## Rules
40
41
  - Do not export while any **Critical** design-gate finding is unresolved (`../slides-grab-design/references/design-gate.md`). The design gate is a hard precondition for this stage, and `slides-grab pdf`, `slides-grab convert`, and `slides-grab figma` block if the latest receipt is missing or stale.
41
42
  - Do not modify slide content during conversion stage unless explicitly requested.
42
43
  - If conversion fails, diagnose and fix root causes in source HTML/CSS.
44
+ - For chart-heavy decks, treat a blank exported chart as a source rendering bug first: re-run validation, inspect `empty-canvas`, and verify the same slide in `viewer.html` before retrying PDF/PNG/PPTX export.
43
45
  - Always tell the user that PPTX and Figma export are experimental / unstable and may require manual cleanup.
44
46
  - Use the packaged CLI and bundled references only; do not depend on unpublished agent-specific files.
45
47
 
@@ -11,6 +11,9 @@ These are the packaged export rules for installable `slides-grab` skills.
11
11
  - Only export after the user approves the reviewed HTML slides.
12
12
  - Do not modify slide content during export unless explicitly requested.
13
13
  - If export fails, fix the root cause in the source HTML/CSS or packaged runtime path.
14
+ - For decks with Chart.js or other `<canvas>` charts, run `slides-grab validate --slides-dir <path>` first and fix any `empty-canvas` error before exporting.
15
+ - Build and open `viewer.html` once for chart-heavy decks so the same charts are confirmed in the generated review surface.
16
+ - Keep Chart.js animation disabled in source slides so PDF/PNG capture sees the final painted chart.
14
17
 
15
18
  ## User-facing caveats
16
19
  - PPTX export is experimental / unstable.
@@ -20,6 +20,7 @@ Produce an approved `slide-outline.md` before any slide HTML generation.
20
20
 
21
21
  ## Output
22
22
  - `slide-outline.md` (must include `style: <id>` in meta section)
23
+ - For chart-heavy decks, explicit chart slide notes: chart type, data source, key comparison, and whether the design stage should use the default Chart.js canvas template.
23
24
 
24
25
  ## Workflow
25
26
  1. Analyze user goal and audience.
@@ -37,6 +38,7 @@ Produce an approved `slide-outline.md` before any slide HTML generation.
37
38
  - bundled style → `style: <id>`
38
39
  - converted DESIGN.slides.md → `style: ./DESIGN.slides.md`
39
40
  - free-form custom direction → leave a one-paragraph `style:` block describing it
41
+ - for chart slides, name the intended chart type and data payload in the slide notes so Stage 2 can build a real Chart.js canvas instead of placeholder bars or decorative pseudo-charts
40
42
  5. Present a concise summary to user.
41
43
  6. Repeat revisions until explicit approval.
42
44
 
@@ -44,6 +46,7 @@ Produce an approved `slide-outline.md` before any slide HTML generation.
44
46
  - **Do not write the outline before the user approves a style.** Style selection comes first.
45
47
  - Do not generate slide HTML (`<slides-dir>/slide-*.html`) in this stage.
46
48
  - Keep scope to structure, narrative, and style selection.
49
+ - For quantitative stories, plan charts as evidence: identify the source values, comparison axis, baseline, and intended takeaway. Do not invent filler metrics just to fill a chart.
47
50
  - Ask for approval before moving to design.
48
51
  - Assume later stages run through the packaged `slides-grab` CLI.
49
52
  - Use the packaged CLI and bundled references only; do not depend on unpublished agent-specific files.
@@ -38,9 +38,68 @@ function getStyleSource(style) {
38
38
  return DESIGN_STYLES_SOURCE;
39
39
  }
40
40
 
41
+ const DESIGN_DIVERSITY_ALIAS_MAP = {
42
+ 'ppt-glassmorphism': 'glassmorphism',
43
+ 'ppt-neo-brutalism': 'neo-brutalism',
44
+ 'ppt-editorial-magazine': 'editorial-magazine',
45
+ };
46
+
47
+ const DESIGN_DIVERSITY_RELATED_MAP = {
48
+ 'ppt-consulting-precision-grid': ['swiss-international-style', 'executive-minimal', 'corporate-blue'],
49
+ 'ppt-mckinsey-ghost-deck': ['swiss-international-style', 'executive-minimal'],
50
+ 'ppt-bcg-exhibit-deck': ['swiss-international-style', 'corporate-blue'],
51
+ 'ppt-bain-results-deck': ['executive-minimal', 'corporate-blue'],
52
+ 'ppt-keynote-minimal-fullbleed': ['monochrome-minimal', 'executive-minimal'],
53
+ 'ppt-minimal-mono-note': ['monochrome-minimal'],
54
+ 'ppt-monochrome-risk': ['monochrome-minimal', 'executive-minimal'],
55
+ 'ppt-monochrome-infrastructure-deck': ['monochrome-minimal', 'executive-minimal'],
56
+ 'ppt-dark-tech': ['modern-dark', 'cyberpunk-outline'],
57
+ 'ppt-engineered-dark-deck': ['modern-dark'],
58
+ 'ppt-prismatic-dark-deck': ['modern-dark', 'scifi-holographic-data'],
59
+ 'ppt-dark-luxury-keynote': ['modern-dark', 'dark-neon-miami'],
60
+ 'ppt-vivid-gradient-future': ['gradient-mesh', 'aurora-neon-glow'],
61
+ 'ppt-vivid-gradient-infographic-deck': ['gradient-mesh', 'aurora-neon-glow'],
62
+ };
63
+
64
+ // reverse map: builtin id -> [dd slug] for enrichment
65
+ const BUILTIN_ALIAS_SLUGS = Object.entries(DESIGN_DIVERSITY_ALIAS_MAP).reduce((acc, [slug, builtinId]) => {
66
+ (acc[builtinId] = acc[builtinId] || []).push(slug);
67
+ return acc;
68
+ }, {});
69
+
70
+ function classifyStyle(style) {
71
+ if (style.collection === 'design-diversity') {
72
+ const slug = style.id;
73
+ if (DESIGN_DIVERSITY_ALIAS_MAP[slug]) {
74
+ return {
75
+ classification: 'source-alias',
76
+ sourceSlug: slug,
77
+ aliasOf: DESIGN_DIVERSITY_ALIAS_MAP[slug],
78
+ relatedStyleIds: [],
79
+ };
80
+ }
81
+ if (DESIGN_DIVERSITY_RELATED_MAP[slug]) {
82
+ return {
83
+ classification: 'source-variant',
84
+ sourceSlug: slug,
85
+ relatedStyleIds: [...DESIGN_DIVERSITY_RELATED_MAP[slug]],
86
+ };
87
+ }
88
+ return {
89
+ classification: 'source-new',
90
+ sourceSlug: slug,
91
+ relatedStyleIds: [],
92
+ };
93
+ }
94
+ const extra = { classification: 'builtin', relatedStyleIds: [] };
95
+ if (BUILTIN_ALIAS_SLUGS[style.id]) extra.aliases = [...BUILTIN_ALIAS_SLUGS[style.id]];
96
+ return extra;
97
+ }
98
+
41
99
  const DESIGN_STYLES = [...RAW_DESIGN_STYLES, ...RAW_DESIGN_DIVERSITY_STYLES].map((style) => Object.freeze({
42
100
  ...style,
43
101
  source: getStyleSource(style),
102
+ ...classifyStyle(style),
44
103
  }));
45
104
 
46
105
  const DESIGN_STYLES_BY_ID = new Map(DESIGN_STYLES.map((style) => [style.id, style]));
@@ -49,6 +108,10 @@ export function listDesignStyles() {
49
108
  return DESIGN_STYLES;
50
109
  }
51
110
 
111
+ export function listSelectableDesignStyles() {
112
+ return DESIGN_STYLES.filter((style) => style.classification !== 'source-alias');
113
+ }
114
+
52
115
  export function getDesignStyle(styleId) {
53
116
  if (!styleId) {
54
117
  return null;
@@ -362,6 +362,10 @@ export async function inspectSlide(page, fileName, slidesDir, slideMode = DEFAUL
362
362
  if (document.fonts?.ready) {
363
363
  await document.fonts.ready;
364
364
  }
365
+
366
+ await new Promise((resolve) => {
367
+ requestAnimationFrame(() => requestAnimationFrame(resolve));
368
+ });
365
369
  });
366
370
 
367
371
  const inspection = await page.evaluate(
@@ -406,6 +410,61 @@ export async function inspectSlide(page, fileName, slidesDir, slideMode = DEFAUL
406
410
  };
407
411
  };
408
412
 
413
+ const inspectCanvasPaint = (canvas) => {
414
+ const rect = canvas.getBoundingClientRect();
415
+ const context = canvas.getContext('2d');
416
+ const metrics = {
417
+ layoutWidth: round(rect.width),
418
+ layoutHeight: round(rect.height),
419
+ bufferWidth: canvas.width,
420
+ bufferHeight: canvas.height,
421
+ sampledPixels: 0,
422
+ paintedSamples: 0,
423
+ };
424
+
425
+ if (!context || canvas.width <= 0 || canvas.height <= 0) {
426
+ return {
427
+ empty: true,
428
+ reason: !context ? '2d-context-unavailable' : 'zero-size-drawing-buffer',
429
+ metrics,
430
+ };
431
+ }
432
+
433
+ try {
434
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height).data;
435
+ const totalPixels = canvas.width * canvas.height;
436
+ const stride = Math.max(1, Math.ceil(totalPixels / 50000));
437
+ let sampledPixels = 0;
438
+ let paintedSamples = 0;
439
+
440
+ for (let pixel = 0; pixel < totalPixels; pixel += stride) {
441
+ sampledPixels += 1;
442
+ if (imageData[(pixel * 4) + 3] !== 0) {
443
+ paintedSamples += 1;
444
+ break;
445
+ }
446
+ }
447
+
448
+ metrics.sampledPixels = sampledPixels;
449
+ metrics.paintedSamples = paintedSamples;
450
+
451
+ return {
452
+ empty: paintedSamples === 0,
453
+ reason: paintedSamples === 0 ? 'transparent-drawing-buffer' : '',
454
+ metrics,
455
+ };
456
+ } catch (error) {
457
+ return {
458
+ empty: true,
459
+ reason: 'drawing-buffer-unreadable',
460
+ metrics: {
461
+ ...metrics,
462
+ detail: error instanceof Error ? error.message : String(error),
463
+ },
464
+ };
465
+ }
466
+ };
467
+
409
468
  const elementPath = (element) => {
410
469
  if (!element || element.nodeType !== Node.ELEMENT_NODE) return '';
411
470
  if (element === document.body) return 'body';
@@ -587,6 +646,23 @@ export async function inspectSlide(page, fileName, slidesDir, slideMode = DEFAUL
587
646
  });
588
647
  }
589
648
 
649
+ const canvases = Array.from(document.querySelectorAll('canvas'));
650
+ for (const canvas of canvases) {
651
+ if (!isVisible(canvas)) continue;
652
+
653
+ const result = inspectCanvasPaint(canvas);
654
+ if (!result.empty) continue;
655
+
656
+ critical.push({
657
+ code: 'empty-canvas',
658
+ message: 'Canvas has visible layout size but no painted pixels. Chart.js and other canvas charts must render before validation.',
659
+ element: elementPath(canvas),
660
+ detail: result.reason,
661
+ metrics: result.metrics,
662
+ bbox: normalizeRect(canvas.getBoundingClientRect()),
663
+ });
664
+ }
665
+
590
666
  const parents = [document.body, ...allVisibleElements];
591
667
  for (const parent of parents) {
592
668
  const children = Array.from(parent.children).filter((child) => visibleSet.has(child));