slides-grab 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,6 +82,7 @@ slides-grab figma # Export an experimental / unstable Figma Slides i
82
82
  slides-grab pdf # Export PDF in capture mode (default)
83
83
  slides-grab pdf --resolution 2160p # Higher-resolution image-backed PDF export
84
84
  slides-grab pdf --mode print # Export searchable/selectable text PDF
85
+ slides-grab tldraw # Render a .tldr diagram into a slide-sized local SVG asset
85
86
  slides-grab list-templates # Show available slide templates
86
87
  slides-grab list-themes # Show available color themes
87
88
  ```
@@ -116,6 +117,27 @@ slides-grab figma --slides-dir decks/my-deck --output decks/my-deck-figma.p
116
117
 
117
118
  > **Warning:** `slides-grab convert` and `slides-grab figma` are currently **experimental / unstable**. Expect best-effort output, layout shifts, and manual cleanup in PowerPoint or Figma.
118
119
 
120
+ ### Tldraw Diagram Assets
121
+
122
+ Use `slides-grab tldraw` when you want a newly authored `tldraw` diagram to fit an exact slide region and remain export-friendly as a local SVG asset. The command supports current-format `.tldr` files and store-snapshot JSON; legacy pre-records `.tldr` files must be reopened and resaved in a current `tldraw` build first:
123
+
124
+ ```bash
125
+ slides-grab tldraw \
126
+ --input decks/my-deck/assets/system.tldr \
127
+ --output decks/my-deck/assets/system.svg \
128
+ --width 640 \
129
+ --height 320 \
130
+ --padding 16
131
+ ```
132
+
133
+ Then reference the generated SVG from your slide HTML with a normal local image:
134
+
135
+ ```html
136
+ <img src="./assets/system.svg" alt="System architecture diagram">
137
+ ```
138
+
139
+ The built-in `diagram-tldraw` template is a simple starting point for this workflow.
140
+
119
141
  ### Figma Workflow
120
142
 
121
143
  ```bash
package/bin/ppt-agent.js CHANGED
@@ -154,6 +154,28 @@ program
154
154
  await runCommand('scripts/figma-export.js', args);
155
155
  });
156
156
 
157
+ program
158
+ .command('tldraw')
159
+ .description('Render a current-format .tldr or store-snapshot JSON file to an exact-size SVG asset for slides')
160
+ .option('--input <path>', 'Input current-format .tldr or snapshot JSON file')
161
+ .option('--output <path>', 'Output SVG asset path')
162
+ .option('--width <number>', 'Exact output width in CSS pixels')
163
+ .option('--height <number>', 'Exact output height in CSS pixels')
164
+ .option('--padding <number>', 'Inner fit padding in CSS pixels')
165
+ .option('--background <css>', 'Optional wrapper background fill')
166
+ .option('--page-id <id>', 'Optional tldraw page id to export')
167
+ .action(async (options = {}) => {
168
+ const args = [];
169
+ if (options.input) args.push('--input', String(options.input));
170
+ if (options.output) args.push('--output', String(options.output));
171
+ if (options.width) args.push('--width', String(options.width));
172
+ if (options.height) args.push('--height', String(options.height));
173
+ if (options.padding) args.push('--padding', String(options.padding));
174
+ if (options.background) args.push('--background', String(options.background));
175
+ if (options.pageId) args.push('--page-id', String(options.pageId));
176
+ await runCommand('scripts/render-tldraw.js', args);
177
+ });
178
+
157
179
  program
158
180
  .command('edit')
159
181
  .description('Start interactive slide editor with Codex image-based edit flow')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slides-grab",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
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",
@@ -38,6 +38,7 @@
38
38
  "scripts/figma-export.js",
39
39
  "scripts/html2pdf.js",
40
40
  "scripts/html2pptx.js",
41
+ "scripts/render-tldraw.js",
41
42
  "scripts/validate-slides.js",
42
43
  "skills/",
43
44
  "src/",
@@ -51,7 +52,7 @@
51
52
  "build-viewer": "node scripts/build-viewer.js",
52
53
  "validate": "node scripts/validate-slides.js",
53
54
  "convert": "node convert.cjs",
54
- "test": "node --test tests/editor/editor-codex-edit.test.js tests/pdf/html2pdf.test.js tests/pdf/html2pdf.e2e.test.js tests/figma/figma-export.test.js tests/image-contract/image-contract.test.js tests/validation/validate-slides.test.js tests/skills/installable-skills.test.js",
55
+ "test": "node --test tests/editor/editor-codex-edit.test.js tests/pdf/html2pdf.test.js tests/pdf/html2pdf.e2e.test.js tests/figma/figma-export.test.js tests/image-contract/image-contract.test.js tests/tldraw/render-tldraw.test.js tests/validation/validate-slides.test.js tests/skills/installable-skills.test.js",
55
56
  "test:e2e": "node --test tests/editor/editor-ui.e2e.test.js tests/editor/editor-concurrency.e2e.test.js"
56
57
  },
57
58
  "dependencies": {
@@ -60,7 +61,10 @@
60
61
  "pdf-lib": "^1.17.1",
61
62
  "playwright": "^1.40.0",
62
63
  "pptxgenjs": "^3.12.0",
64
+ "react": "^19.2.4",
65
+ "react-dom": "^19.2.4",
63
66
  "react-icons": "^5.0.0",
64
- "sharp": "^0.33.0"
67
+ "sharp": "^0.33.0",
68
+ "tldraw": "^4.4.1"
65
69
  }
66
70
  }
@@ -403,7 +403,6 @@ export async function normalizeBodyToSlideFrame(page, slideFrame) {
403
403
  const documentElement = document.documentElement;
404
404
 
405
405
  body.style.margin = '0';
406
- body.style.padding = '0';
407
406
  body.style.width = `${width}px`;
408
407
  body.style.height = `${height}px`;
409
408
  body.style.minWidth = `${width}px`;
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import {
7
+ buildFixedSizeSvg,
8
+ buildTldrawImportUrl,
9
+ DEFAULT_TLDRAW_HEIGHT,
10
+ DEFAULT_TLDRAW_OUTPUT,
11
+ DEFAULT_TLDRAW_PADDING,
12
+ DEFAULT_TLDRAW_WIDTH,
13
+ getTldrawUsage,
14
+ loadTldrawInput,
15
+ main,
16
+ normalizeTldrawSnapshot,
17
+ parseTldrawCliArgs,
18
+ renderTldrawFile,
19
+ renderTldrawSnapshot,
20
+ } from '../src/tldraw/render.js';
21
+
22
+ export {
23
+ buildFixedSizeSvg,
24
+ buildTldrawImportUrl,
25
+ DEFAULT_TLDRAW_HEIGHT,
26
+ DEFAULT_TLDRAW_OUTPUT,
27
+ DEFAULT_TLDRAW_PADDING,
28
+ DEFAULT_TLDRAW_WIDTH,
29
+ getTldrawUsage,
30
+ loadTldrawInput,
31
+ normalizeTldrawSnapshot,
32
+ parseTldrawCliArgs,
33
+ renderTldrawFile,
34
+ renderTldrawSnapshot,
35
+ };
36
+
37
+ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
38
+
39
+ if (isMain) {
40
+ main().catch((error) => {
41
+ console.error(`[slides-grab] ${error.message}`);
42
+ process.exit(1);
43
+ });
44
+ }
@@ -33,9 +33,10 @@ Use the installed **slides-grab-design** skill.
33
33
  3. Run validation: `slides-grab validate --slides-dir <path>`
34
34
  4. If validation fails, automatically fix the slide HTML/CSS until validation passes.
35
35
  5. Build the viewer: `slides-grab build-viewer --slides-dir <path>`
36
- 6. Present viewer to user for review.
37
- 7. Revise individual slides based on feedback, then re-run validation and rebuild the viewer.
38
- 8. Optionally launch the visual editor: `slides-grab edit --slides-dir <path>`
36
+ 6. 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>`.
37
+ 7. Present viewer to user for review.
38
+ 8. Revise individual slides based on feedback, then re-run validation and rebuild the viewer.
39
+ 9. Optionally launch the visual editor: `slides-grab edit --slides-dir <path>`
39
40
 
40
41
  **Do not proceed to Stage 3 without approval.**
41
42
 
@@ -58,6 +59,7 @@ Use the installed **slides-grab-export** skill.
58
59
  4. **Use `decks/<deck-name>/`** as the slides workspace for multi-deck projects.
59
60
  5. **Call out export risk clearly**: PPTX and Figma export are experimental / unstable and must be described as best-effort output.
60
61
  6. Use the stage skills as the source of truth for plan, design, and export rules.
62
+ 7. When a slide needs a complex diagram, default to a `tldraw`-generated asset unless the user explicitly asks for a different approach.
61
63
 
62
64
  ## Reference
63
65
  - `references/presentation-workflow-reference.md` — archived end-to-end workflow guidance from the legacy skill set
@@ -26,9 +26,10 @@ Use **design-skill** (`.claude/skills/design-skill/SKILL.md`).
26
26
  3. Run validation: `slides-grab validate --slides-dir <path>`
27
27
  4. If validation fails, automatically fix the slide HTML/CSS until validation passes.
28
28
  5. Build the viewer: `node scripts/build-viewer.js --slides-dir <path>`
29
- 6. Present viewer to user for review.
30
- 7. Revise individual slides based on feedback, then re-run validation and rebuild the viewer.
31
- 8. Optionally launch the visual editor: `slides-grab edit --slides-dir <path>`
29
+ 6. 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>`.
30
+ 7. Present viewer to user for review.
31
+ 8. Revise individual slides based on feedback, then re-run validation and rebuild the viewer.
32
+ 9. Optionally launch the visual editor: `slides-grab edit --slides-dir <path>`
32
33
 
33
34
  **Do not proceed to Stage 3 without approval.**
34
35
 
@@ -50,3 +51,4 @@ Use **pptx-skill** (`.claude/skills/pptx-skill/SKILL.md`).
50
51
  3. **Read each stage's SKILL.md** for detailed rules — this skill only orchestrates.
51
52
  4. **Use `decks/<deck-name>/`** as the slides workspace for multi-deck projects.
52
53
  5. **Call out export risk clearly**: PPTX and Figma export are experimental / unstable and should be described as best-effort output.
54
+ 6. **Prefer tldraw for complex diagrams**: Use `slides-grab tldraw` for diagram-heavy slides unless the user explicitly wants another rendering path.
@@ -24,11 +24,12 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
24
24
  ## Workflow
25
25
  1. Read approved `slide-outline.md`.
26
26
  2. Generate slide HTML files with 2-digit numbering in selected `--slides-dir`.
27
- 3. Run `slides-grab validate --slides-dir <path>` after generation or edits.
28
- 4. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
29
- 5. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.
30
- 6. Iterate on user feedback by editing only requested slide files, then re-run validation and rebuild the viewer.
31
- 7. Keep revising until user approves conversion stage.
27
+ 3. 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/`.
28
+ 4. Run `slides-grab validate --slides-dir <path>` after generation or edits.
29
+ 5. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
30
+ 6. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.
31
+ 7. Iterate on user feedback by editing only requested slide files, then re-run validation and rebuild the viewer.
32
+ 8. Keep revising until user approves conversion stage.
32
33
 
33
34
  ## Rules
34
35
  - Keep slide size 720pt x 405pt.
@@ -37,6 +38,8 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
37
38
  - Allow `data:` URLs when the slide must be fully self-contained.
38
39
  - Treat remote `https://` images as best-effort only, and never use absolute filesystem paths.
39
40
  - Prefer `<img>` for slide imagery and `data-image-placeholder` when no final asset exists.
41
+ - Prefer `tldraw` for complex diagrams instead of recreating dense node/edge diagrams directly in HTML/CSS.
42
+ - Use `slides-grab tldraw` plus `templates/diagram-tldraw.html` when that gives a cleaner, more export-friendly result.
40
43
  - Do not present slides for review until `slides-grab validate --slides-dir <path>` passes.
41
44
  - Do not start conversion before approval.
42
45
  - Use the packaged CLI and bundled references only; do not depend on unpublished agent-specific files.
@@ -6,6 +6,7 @@ These are the packaged design rules for installable `slides-grab` skills.
6
6
  - Validate slides: `slides-grab validate --slides-dir <path>`
7
7
  - Build review viewer: `slides-grab build-viewer --slides-dir <path>`
8
8
  - Launch editor: `slides-grab edit --slides-dir <path>`
9
+ - Render `tldraw` diagrams: `slides-grab tldraw --input <path> --output <path>`
9
10
 
10
11
  ## Slide spec
11
12
  - Slide size: `720pt x 405pt` (16:9)
@@ -17,6 +18,7 @@ These are the packaged design rules for installable `slides-grab` skills.
17
18
  ## Asset rules
18
19
  - Store deck-local assets in `<slides-dir>/assets/`
19
20
  - Reference deck-local assets as `./assets/<file>`
21
+ - Use `tldraw`-generated local assets for complex diagrams when possible
20
22
  - Allow `data:` URLs only when the slide must be fully self-contained
21
23
  - Never use absolute filesystem paths
22
24
 
@@ -40,10 +42,12 @@ These are the packaged design rules for installable `slides-grab` skills.
40
42
  - `templates/closing.html`
41
43
  - `templates/chart.html`
42
44
  - `templates/diagram.html`
45
+ - `templates/diagram-tldraw.html`
43
46
  - `templates/custom/`
44
47
 
45
48
  ## Review loop
46
49
  - Generate or edit only the needed slide files.
50
+ - Prefer `tldraw` for complex diagrams instead of hand-building dense diagram geometry in HTML/CSS.
47
51
  - Re-run validation after every generation/edit pass.
48
52
  - Rebuild the viewer only after validation passes.
49
53
  - Do not move to export until the user approves the reviewed deck.
@@ -282,6 +282,10 @@ grid-template-columns: 1fr 2.3fr;
282
282
  ### 12. Diagram Slide
283
283
  - Template file: `templates/diagram.html`
284
284
 
285
+ ### 13. Tldraw Diagram Slide
286
+ - Template file: `templates/diagram-tldraw.html`
287
+ - Use this when the slide needs a complex diagram that will be easier to author in `tldraw` and safer to export as a local image asset.
288
+
285
289
  ### Custom Templates
286
290
  - Custom template directory: `templates/custom/`
287
291
  - Users can add template files as drop-in for reuse.
@@ -536,28 +540,30 @@ This skill is **Stage 2**. It works from the `slide-outline.md` approved by the
536
540
  ### Steps
537
541
 
538
542
  1. **Analyze + Design**: Read `slide-outline.md`, decide theme/layout, generate HTML slides
539
- 2. **Validate slides**: After slide generation or edits, automatically run:
543
+ 2. **Diagram choice**: If a slide needs a complex diagram (architecture, workflows, relationship maps, multi-node concepts), prefer `tldraw`. Export the diagram with `slides-grab tldraw` and reference the generated local asset from the slide HTML.
544
+ 3. **Validate slides**: After slide generation or edits, automatically run:
540
545
  ```bash
541
546
  slides-grab validate --slides-dir <path>
542
547
  ```
543
- 3. **Auto-fix validation issues**: If validation fails, fix the source HTML/CSS and re-run validation until it passes
544
- 4. **Auto-build viewer**: After validation passes, automatically run:
548
+ 4. **Auto-fix validation issues**: If validation fails, fix the source HTML/CSS and re-run validation until it passes
549
+ 5. **Auto-build viewer**: After validation passes, automatically run:
545
550
  ```bash
546
551
  node scripts/build-viewer.js --slides-dir <path>
547
552
  ```
548
- 5. **Guide user to review**: Tell the user to check slides in the browser:
553
+ 6. **Guide user to review**: Tell the user to check slides in the browser:
549
554
  ```
550
555
  open <slides-dir>/viewer.html
551
556
  ```
552
- 6. **Revision loop**: When the user requests changes to specific slides:
557
+ 7. **Revision loop**: When the user requests changes to specific slides:
553
558
  - Edit only the relevant HTML file
554
559
  - Re-run `slides-grab validate --slides-dir <path>` and fix any failures
555
560
  - Re-run `node scripts/build-viewer.js --slides-dir <path>` to rebuild the viewer
556
561
  - Guide user to review again
557
- 7. **Completion**: Repeat the revision loop until the user signals approval for PPTX conversion
562
+ 8. **Completion**: Repeat the revision loop until the user signals approval for PPTX conversion
558
563
 
559
564
  ### Absolute Rules
560
565
  - **Never start PPTX conversion without approval** — PPTX conversion is the responsibility of `pptx-skill` and requires explicit user approval.
566
+ - **Prefer tldraw for complex diagrams** — Use `slides-grab tldraw` when the slide needs a non-trivial diagram instead of forcing dense diagram geometry into HTML/CSS.
561
567
  - **Never skip validation** — Run `slides-grab validate --slides-dir <path>` after generation or edits and fix failures before review.
562
568
  - **Never forget to build the viewer** — Run `node scripts/build-viewer.js --slides-dir <path>` every time slides are generated or modified.
563
569
 
@@ -9,6 +9,7 @@
9
9
  - Always include alt on img tags.
10
10
  - Use `./assets/<file>` as the default image contract for slide HTML.
11
11
  - Keep slide assets in `<slides-dir>/assets/`.
12
+ - Use `tldraw`-generated assets for complex diagrams whenever possible.
12
13
  - `data:` URLs are allowed for fully self-contained slides.
13
14
  - Remote `https://` URLs are allowed but non-deterministic and fallback only.
14
15
  - Do not use absolute filesystem paths in slide HTML.
@@ -23,6 +24,7 @@
23
24
  - After slide generation or edits, run `slides-grab validate --slides-dir <path>`.
24
25
  - After validation passes, run `slides-grab build-viewer --slides-dir <path>`.
25
26
  - Edit only the relevant HTML file during revision loops.
27
+ - Prefer `slides-grab tldraw` + local exported assets for architecture, workflow, relationship, and other complex diagrams.
26
28
  - Never start PPTX conversion without explicit approval.
27
29
  - Never forget to build the viewer after slide changes.
28
30
  - Do not persist runtime-only editor/viewer injections in saved slide HTML.
@@ -0,0 +1,470 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { chromium } from 'playwright';
6
+
7
+ const require = createRequire(import.meta.url);
8
+
9
+ export const DEFAULT_TLDRAW_WIDTH = 960;
10
+ export const DEFAULT_TLDRAW_HEIGHT = 540;
11
+ export const DEFAULT_TLDRAW_PADDING = 24;
12
+ export const DEFAULT_TLDRAW_OUTPUT = 'diagram.svg';
13
+
14
+ function readOptionValue(args, index, optionName) {
15
+ const next = args[index + 1];
16
+ if (!next || next.startsWith('-')) {
17
+ throw new Error(`Missing value for ${optionName}.`);
18
+ }
19
+ return next;
20
+ }
21
+
22
+ function parsePositiveInteger(value, optionName) {
23
+ const parsed = Number.parseInt(String(value), 10);
24
+ if (!Number.isFinite(parsed) || parsed <= 0) {
25
+ throw new Error(`${optionName} must be a positive integer.`);
26
+ }
27
+ return parsed;
28
+ }
29
+
30
+ function parseNonNegativeInteger(value, optionName) {
31
+ const parsed = Number.parseInt(String(value), 10);
32
+ if (!Number.isFinite(parsed) || parsed < 0) {
33
+ throw new Error(`${optionName} must be zero or a positive integer.`);
34
+ }
35
+ return parsed;
36
+ }
37
+
38
+ function formatNumber(value) {
39
+ return Number.parseFloat(Number(value).toFixed(4)).toString();
40
+ }
41
+
42
+ function escapeAttribute(value) {
43
+ return String(value)
44
+ .replaceAll('&', '&amp;')
45
+ .replaceAll('"', '&quot;')
46
+ .replaceAll('<', '&lt;')
47
+ .replaceAll('>', '&gt;');
48
+ }
49
+
50
+ function extractSvgMarkup(svg) {
51
+ const match = String(svg).match(/<svg\b[^>]*>([\s\S]*?)<\/svg>\s*$/i);
52
+ if (!match) {
53
+ throw new Error('Rendered tldraw output did not contain a root <svg> element.');
54
+ }
55
+ return match[1];
56
+ }
57
+
58
+ function findPackageJsonPath(moduleName) {
59
+ const resolvedEntry = require.resolve(moduleName);
60
+ let currentDir = dirname(resolvedEntry);
61
+
62
+ while (true) {
63
+ const packageJsonPath = join(currentDir, 'package.json');
64
+ if (existsSync(packageJsonPath)) {
65
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
66
+ if (packageJson.name === moduleName) {
67
+ return packageJsonPath;
68
+ }
69
+ }
70
+
71
+ const parentDir = dirname(currentDir);
72
+ if (parentDir === currentDir) {
73
+ break;
74
+ }
75
+ currentDir = parentDir;
76
+ }
77
+
78
+ throw new Error(`Unable to locate package.json for ${moduleName}.`);
79
+ }
80
+
81
+ function getInstalledPackageVersion(moduleName) {
82
+ const packageJsonPath = findPackageJsonPath(moduleName);
83
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
84
+ if (!packageJson.version) {
85
+ throw new Error(`Package ${moduleName} does not declare a version.`);
86
+ }
87
+ return packageJson.version;
88
+ }
89
+
90
+ function isLegacyTldrawPayload(payload) {
91
+ return Boolean(payload && typeof payload === 'object' && payload.document && payload.document.version);
92
+ }
93
+
94
+ export function buildTldrawImportUrl() {
95
+ const tldrawVersion = getInstalledPackageVersion('tldraw');
96
+ const reactVersion = getInstalledPackageVersion('react');
97
+ const reactDomVersion = getInstalledPackageVersion('react-dom');
98
+
99
+ return `https://esm.sh/tldraw@${encodeURIComponent(tldrawVersion)}?deps=react@${encodeURIComponent(reactVersion)},react-dom@${encodeURIComponent(reactDomVersion)}`;
100
+ }
101
+
102
+ export function parseTldrawCliArgs(args = []) {
103
+ const options = {
104
+ input: '',
105
+ output: DEFAULT_TLDRAW_OUTPUT,
106
+ width: DEFAULT_TLDRAW_WIDTH,
107
+ height: DEFAULT_TLDRAW_HEIGHT,
108
+ padding: DEFAULT_TLDRAW_PADDING,
109
+ background: 'transparent',
110
+ pageId: '',
111
+ help: false,
112
+ };
113
+
114
+ for (let index = 0; index < args.length; index += 1) {
115
+ const arg = args[index];
116
+
117
+ if (arg === '-h' || arg === '--help') {
118
+ options.help = true;
119
+ continue;
120
+ }
121
+
122
+ if (arg === '--input') {
123
+ options.input = readOptionValue(args, index, '--input');
124
+ index += 1;
125
+ continue;
126
+ }
127
+
128
+ if (arg.startsWith('--input=')) {
129
+ options.input = arg.slice('--input='.length);
130
+ continue;
131
+ }
132
+
133
+ if (arg === '--output') {
134
+ options.output = readOptionValue(args, index, '--output');
135
+ index += 1;
136
+ continue;
137
+ }
138
+
139
+ if (arg.startsWith('--output=')) {
140
+ options.output = arg.slice('--output='.length);
141
+ continue;
142
+ }
143
+
144
+ if (arg === '--width') {
145
+ options.width = parsePositiveInteger(readOptionValue(args, index, '--width'), '--width');
146
+ index += 1;
147
+ continue;
148
+ }
149
+
150
+ if (arg.startsWith('--width=')) {
151
+ options.width = parsePositiveInteger(arg.slice('--width='.length), '--width');
152
+ continue;
153
+ }
154
+
155
+ if (arg === '--height') {
156
+ options.height = parsePositiveInteger(readOptionValue(args, index, '--height'), '--height');
157
+ index += 1;
158
+ continue;
159
+ }
160
+
161
+ if (arg.startsWith('--height=')) {
162
+ options.height = parsePositiveInteger(arg.slice('--height='.length), '--height');
163
+ continue;
164
+ }
165
+
166
+ if (arg === '--padding') {
167
+ options.padding = parseNonNegativeInteger(readOptionValue(args, index, '--padding'), '--padding');
168
+ index += 1;
169
+ continue;
170
+ }
171
+
172
+ if (arg.startsWith('--padding=')) {
173
+ options.padding = parseNonNegativeInteger(arg.slice('--padding='.length), '--padding');
174
+ continue;
175
+ }
176
+
177
+ if (arg === '--background') {
178
+ options.background = readOptionValue(args, index, '--background').trim() || 'transparent';
179
+ index += 1;
180
+ continue;
181
+ }
182
+
183
+ if (arg.startsWith('--background=')) {
184
+ options.background = arg.slice('--background='.length).trim() || 'transparent';
185
+ continue;
186
+ }
187
+
188
+ if (arg === '--page-id') {
189
+ options.pageId = readOptionValue(args, index, '--page-id').trim();
190
+ index += 1;
191
+ continue;
192
+ }
193
+
194
+ if (arg.startsWith('--page-id=')) {
195
+ options.pageId = arg.slice('--page-id='.length).trim();
196
+ continue;
197
+ }
198
+
199
+ throw new Error(`Unknown option: ${arg}`);
200
+ }
201
+
202
+ if (!options.help && (!options.input || !options.input.trim())) {
203
+ throw new Error('--input must be a non-empty string.');
204
+ }
205
+
206
+ options.input = options.input.trim();
207
+ options.output = String(options.output).trim();
208
+ options.background = options.background || 'transparent';
209
+
210
+ if (!options.help && !options.output) {
211
+ throw new Error('--output must be a non-empty string.');
212
+ }
213
+
214
+ return options;
215
+ }
216
+
217
+ export function getTldrawUsage() {
218
+ return [
219
+ 'Usage: node scripts/render-tldraw.js [options]',
220
+ '',
221
+ 'Options:',
222
+ ' --input <path> Input current-format .tldr or store-snapshot JSON file',
223
+ ` --output <path> Output SVG asset path (default: ${DEFAULT_TLDRAW_OUTPUT})`,
224
+ ` --width <px> Exact output width in CSS pixels (default: ${DEFAULT_TLDRAW_WIDTH})`,
225
+ ` --height <px> Exact output height in CSS pixels (default: ${DEFAULT_TLDRAW_HEIGHT})`,
226
+ ` --padding <px> Inner fit padding in CSS pixels (default: ${DEFAULT_TLDRAW_PADDING})`,
227
+ ' --background <css> Optional wrapper background fill (default: transparent)',
228
+ ' --page-id <id> Optional tldraw page id to export',
229
+ ' -h, --help Show this help message',
230
+ '',
231
+ 'Notes:',
232
+ ' - Legacy pre-records .tldr files are not supported. Open and resave them in a current tldraw build first.',
233
+ '',
234
+ 'Examples:',
235
+ ' node scripts/render-tldraw.js --input slides/assets/diagram.tldr --output slides/assets/diagram.svg',
236
+ ' node scripts/render-tldraw.js --input deck/assets/system.json --output deck/assets/system.svg --width 640 --height 320',
237
+ ].join('\n');
238
+ }
239
+
240
+ export function normalizeTldrawSnapshot(payload) {
241
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
242
+ throw new Error('Expected the tldraw input file to contain a JSON object.');
243
+ }
244
+
245
+ if (isLegacyTldrawPayload(payload)) {
246
+ throw new Error('Legacy pre-records .tldr files are not supported yet. Open the diagram in a current tldraw build and save it again before exporting.');
247
+ }
248
+
249
+ if (payload.store && payload.schema) {
250
+ return payload;
251
+ }
252
+
253
+ if (Array.isArray(payload.records) && payload.schema) {
254
+ const store = Object.fromEntries(
255
+ payload.records.map((record) => {
256
+ if (!record || typeof record !== 'object' || typeof record.id !== 'string') {
257
+ throw new Error('Each tldraw record must be an object with a string id.');
258
+ }
259
+ return [record.id, record];
260
+ }),
261
+ );
262
+
263
+ return {
264
+ store,
265
+ schema: payload.schema,
266
+ };
267
+ }
268
+
269
+ throw new Error('Input JSON must contain either { store, schema } or a current-format { records, schema } tldraw file.');
270
+ }
271
+
272
+ export function buildFixedSizeSvg(
273
+ { svg, width, height },
274
+ {
275
+ targetWidth = DEFAULT_TLDRAW_WIDTH,
276
+ targetHeight = DEFAULT_TLDRAW_HEIGHT,
277
+ padding = DEFAULT_TLDRAW_PADDING,
278
+ background = 'transparent',
279
+ } = {},
280
+ ) {
281
+ const sourceWidth = Number(width);
282
+ const sourceHeight = Number(height);
283
+
284
+ if (!Number.isFinite(sourceWidth) || sourceWidth <= 0 || !Number.isFinite(sourceHeight) || sourceHeight <= 0) {
285
+ throw new Error('Rendered tldraw output must include positive width and height values.');
286
+ }
287
+
288
+ const safePadding = Math.max(0, padding);
289
+ const availableWidth = Math.max(1, targetWidth - safePadding * 2);
290
+ const availableHeight = Math.max(1, targetHeight - safePadding * 2);
291
+ const scale = Math.min(availableWidth / sourceWidth, availableHeight / sourceHeight);
292
+ const fittedWidth = sourceWidth * scale;
293
+ const fittedHeight = sourceHeight * scale;
294
+ const offsetX = (targetWidth - fittedWidth) / 2;
295
+ const offsetY = (targetHeight - fittedHeight) / 2;
296
+ const markup = extractSvgMarkup(svg);
297
+ const backgroundMarkup = background && background !== 'transparent'
298
+ ? `\n <rect x="0" y="0" width="${targetWidth}" height="${targetHeight}" fill="${escapeAttribute(background)}" />`
299
+ : '';
300
+
301
+ return [
302
+ '<?xml version="1.0" encoding="UTF-8"?>',
303
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${targetWidth}" height="${targetHeight}" viewBox="0 0 ${targetWidth} ${targetHeight}" role="img" aria-label="tldraw diagram export">`,
304
+ backgroundMarkup,
305
+ ` <g transform="translate(${formatNumber(offsetX)} ${formatNumber(offsetY)}) scale(${formatNumber(scale)})">`,
306
+ markup
307
+ .split('\n')
308
+ .map((line) => ` ${line}`)
309
+ .join('\n'),
310
+ ' </g>',
311
+ '</svg>',
312
+ '',
313
+ ].join('\n');
314
+ }
315
+
316
+ export async function loadTldrawInput(inputPath) {
317
+ const raw = await readFile(inputPath, 'utf8');
318
+ let parsed;
319
+ try {
320
+ parsed = JSON.parse(raw);
321
+ } catch (error) {
322
+ throw new Error(`Unable to parse tldraw JSON from ${inputPath}: ${error.message}`);
323
+ }
324
+ return normalizeTldrawSnapshot(parsed);
325
+ }
326
+
327
+ export async function renderTldrawSnapshot(snapshot, { pageId = '' } = {}) {
328
+ const browser = await chromium.launch({ headless: true });
329
+ const page = await browser.newPage({ viewport: { width: DEFAULT_TLDRAW_WIDTH, height: DEFAULT_TLDRAW_HEIGHT } });
330
+
331
+ try {
332
+ await page.setContent('<!DOCTYPE html><html><head><meta charset="utf-8"></head><body></body></html>', {
333
+ waitUntil: 'load',
334
+ });
335
+
336
+ const moduleUrl = buildTldrawImportUrl();
337
+
338
+ await page.evaluate(
339
+ async ({ snapshot: browserSnapshot, requestedPageId, browserModuleUrl }) => {
340
+ window.__TLDRAW_RENDER_RESULT__ = null;
341
+ window.__TLDRAW_RENDER_ERROR__ = null;
342
+
343
+ try {
344
+ const {
345
+ Editor,
346
+ createTLStore,
347
+ defaultAddFontsFromNode,
348
+ defaultBindingUtils,
349
+ defaultShapeUtils,
350
+ tipTapDefaultExtensions,
351
+ } = await import(browserModuleUrl);
352
+ const container = document.createElement('div');
353
+ container.style.position = 'fixed';
354
+ container.style.inset = '0';
355
+ container.classList.add('tl-container', 'tl-theme__light');
356
+ document.body.appendChild(container);
357
+
358
+ const tempElm = document.createElement('div');
359
+ container.appendChild(tempElm);
360
+
361
+ const store = createTLStore({
362
+ snapshot: browserSnapshot,
363
+ shapeUtils: defaultShapeUtils,
364
+ });
365
+
366
+ const editor = new Editor({
367
+ store,
368
+ shapeUtils: defaultShapeUtils,
369
+ bindingUtils: defaultBindingUtils,
370
+ tools: [],
371
+ getContainer: () => tempElm,
372
+ options: {
373
+ text: {
374
+ tipTapConfig: {
375
+ extensions: tipTapDefaultExtensions,
376
+ },
377
+ addFontsFromNode: defaultAddFontsFromNode,
378
+ },
379
+ },
380
+ });
381
+
382
+ if (requestedPageId) {
383
+ editor.setCurrentPage(requestedPageId);
384
+ }
385
+
386
+ await editor.fonts.loadRequiredFontsForCurrentPage(editor.options.maxFontsToLoadBeforeRender);
387
+ await new Promise((resolveAnimation) => requestAnimationFrame(() => requestAnimationFrame(resolveAnimation)));
388
+
389
+ const shapeIds = Array.from(editor.getCurrentPageShapeIds());
390
+ if (shapeIds.length === 0) {
391
+ throw new Error('The selected tldraw page does not contain any shapes to export.');
392
+ }
393
+
394
+ const result = await editor.getSvgString(shapeIds, {
395
+ background: false,
396
+ padding: 0,
397
+ });
398
+
399
+ editor.dispose();
400
+
401
+ if (!result) {
402
+ throw new Error('tldraw did not return an SVG export.');
403
+ }
404
+
405
+ window.__TLDRAW_RENDER_RESULT__ = result;
406
+ } catch (error) {
407
+ window.__TLDRAW_RENDER_ERROR__ = error instanceof Error ? error.message : String(error);
408
+ }
409
+ },
410
+ {
411
+ snapshot,
412
+ requestedPageId: pageId,
413
+ browserModuleUrl: moduleUrl,
414
+ },
415
+ );
416
+
417
+ await page.waitForFunction(
418
+ () => window.__TLDRAW_RENDER_RESULT__ !== null || window.__TLDRAW_RENDER_ERROR__ !== null,
419
+ null,
420
+ { timeout: 30000 },
421
+ );
422
+
423
+ const errorMessage = await page.evaluate(() => window.__TLDRAW_RENDER_ERROR__);
424
+ if (errorMessage) {
425
+ throw new Error(errorMessage);
426
+ }
427
+
428
+ const result = await page.evaluate(() => window.__TLDRAW_RENDER_RESULT__);
429
+ if (!result) {
430
+ throw new Error('tldraw render completed without producing an SVG export.');
431
+ }
432
+
433
+ return result;
434
+ } finally {
435
+ await browser.close();
436
+ }
437
+ }
438
+
439
+ export async function renderTldrawFile(inputPath, outputPath, options = {}) {
440
+ const snapshot = await loadTldrawInput(inputPath);
441
+ const rendered = await renderTldrawSnapshot(snapshot, { pageId: options.pageId });
442
+ const fittedSvg = buildFixedSizeSvg(rendered, {
443
+ targetWidth: options.width,
444
+ targetHeight: options.height,
445
+ padding: options.padding,
446
+ background: options.background,
447
+ });
448
+
449
+ await mkdir(dirname(outputPath), { recursive: true });
450
+ await writeFile(outputPath, fittedSvg, 'utf8');
451
+ return {
452
+ inputPath,
453
+ outputPath,
454
+ width: options.width,
455
+ height: options.height,
456
+ };
457
+ }
458
+
459
+ export async function main(args = process.argv.slice(2)) {
460
+ const options = parseTldrawCliArgs(args);
461
+ if (options.help) {
462
+ process.stdout.write(`${getTldrawUsage()}\n`);
463
+ return;
464
+ }
465
+
466
+ const inputPath = resolve(process.cwd(), options.input);
467
+ const outputPath = resolve(process.cwd(), options.output);
468
+ const result = await renderTldrawFile(inputPath, outputPath, options);
469
+ process.stdout.write(`Generated tldraw SVG: ${result.outputPath} (${result.width}x${result.height})\n`);
470
+ }
@@ -0,0 +1,56 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">
5
+ <style>
6
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7
+ body {
8
+ width: 720pt;
9
+ height: 405pt;
10
+ font-family: 'Pretendard', sans-serif;
11
+ background: #ffffff;
12
+ color: #111827;
13
+ padding: 34pt 40pt 28pt;
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 12pt;
17
+ }
18
+ .diagram-frame {
19
+ flex: 1;
20
+ min-height: 0;
21
+ border: 1pt solid #e5e7eb;
22
+ border-radius: 14pt;
23
+ background: #f8fafc;
24
+ padding: 12pt;
25
+ display: flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ overflow: hidden;
29
+ }
30
+ .diagram-frame img {
31
+ display: block;
32
+ width: 100%;
33
+ height: 100%;
34
+ object-fit: contain;
35
+ }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ <div style="display: flex; justify-content: space-between; align-items: baseline;">
40
+ <h2 style="font-size: 28pt; font-weight: 700; letter-spacing: -0.01em;">System Diagram</h2>
41
+ <p style="font-size: 10pt; color: #6b7280;">DIAGRAM / TLDRAW</p>
42
+ </div>
43
+
44
+ <p style="font-size: 12pt; color: #4b5563;">
45
+ Render a current-format local .tldr file with slides-grab tldraw, then reference the generated SVG asset below.
46
+ </p>
47
+
48
+ <div class="diagram-frame">
49
+ <img src="./assets/diagram.svg" alt="System architecture diagram" />
50
+ </div>
51
+
52
+ <p style="font-size: 9pt; color: #9ca3af;">
53
+ Example asset path: ./assets/diagram.svg
54
+ </p>
55
+ </body>
56
+ </html>