sdocs-dev 1.6.2 → 1.13.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.
@@ -0,0 +1,2759 @@
1
+ // Long help strings printed by `sdoc help`, `sdoc schema`, `sdoc charts`,
2
+ // `sdoc diagrams`, `sdoc comments`. Kept as plain data with no logic.
3
+
4
+ const HELP = `
5
+ SmallDocs CLI
6
+ =========
7
+ Open, share, and style markdown files from the terminal.
8
+
9
+ USAGE
10
+ sdoc <file> Open file in browser (read mode, no live link)
11
+ sdoc <file> --write Open in write mode
12
+ sdoc <file> --style Open with style panel
13
+ sdoc <file> --raw Open raw markdown source
14
+ sdoc <file> --comment Open in comment mode (review/annotate)
15
+ sdoc bridge <file> Live editing session: edits autosave to disk,
16
+ external changes push to the page (see LIVE BRIDGE)
17
+ sdoc new New blank document (write mode)
18
+ sdoc share <file> Copy shareable link to clipboard
19
+ sdoc share <file> --section "X" Link with section anchor
20
+ sdoc share <file> --short Encrypted /s/<id> short link (see SHORT LINKS)
21
+ sdoc schema Print the full styles schema
22
+ sdoc charts Chart types, options, and styling guide
23
+ sdoc diagrams Mermaid diagrams reference (\`\`\`mermaid blocks)
24
+ sdoc cells Inline spreadsheet reference (\`\`\`cells blocks)
25
+ sdoc color-analysis <file> Check custom colours for readable contrast
26
+ (both themes). Run after styling a doc.
27
+ sdoc comments Comment-format reference (for agents)
28
+ sdoc feedback Interactive form DSL reference (\`\`\`form blocks)
29
+ sdoc feedback <file> Open <file> for the user to fill in; exits on first submit
30
+ sdoc feedback <file> --keep-open Stay alive across many submits; tail stdout per click
31
+ sdoc slides Slide DSL reference (grids, shapes, content)
32
+ sdoc slides list Built-in slide template registry + slot names
33
+ sdoc slides icons [query] Lucide icon names available to the \`icon\` shape kind
34
+ sdoc slides custom-shapes Raw-shape primitives + design principles reference
35
+ sdoc present <file> Open <file> and jump straight into fullscreen slides
36
+ sdoc library Open the personal markdown library at smalldocs.org/library
37
+ sdoc library ls List markdown indexed in this project (walks up to .git)
38
+ sdoc library ls --tags Tag bag (tag - count) for this project
39
+ sdoc library --help Full library reference - commands, tagging, rescue, autostart
40
+ sdoc <file> +tag1 +tag2 Inject tags into the file's front matter at open time
41
+ sdoc defaults Show ~/.sdocs/styles.yaml
42
+ sdoc defaults --reset Remove default styles
43
+ sdoc setup Wire SmallDocs into your coding agents
44
+ sdoc refresh Update the SmallDocs section in agent files to the current version
45
+ sdoc auto-update [on|off] Toggle auto-install of sdoc updates
46
+ sdoc upgrade Upgrade sdoc to the latest version now
47
+ sdoc safe Verify the SmallDocs server is running the published code
48
+ sdoc safe --json Same, machine-readable (for agents)
49
+ sdoc safe --audit Same, plus GitHub links to server-side source files
50
+ sdoc help Show this help
51
+ sdoc --version Print the installed version
52
+ cat file.md | sdoc Pipe markdown from stdin
53
+ cat file.md | sdoc share Pipe to clipboard link
54
+
55
+ MODE FLAGS
56
+ --read Clean reading view (default when file given)
57
+ --write Opens the contentEditable writer
58
+ --style Styled preview with style panel visible
59
+ --raw Shows raw markdown source
60
+ --comment Comment mode: gutter buttons appear on each block; cards
61
+ render under blocks that already have comments. Useful both
62
+ for human review and for opening files an agent has annotated.
63
+
64
+ OPTIONS
65
+ --section <heading> Scroll to heading section on load
66
+ --light Open in light theme
67
+ --dark Open in dark theme
68
+ --url <base> Custom base URL (default: https://smalldocs.org)
69
+ --mode <m> Alias for --read / --write / --style / --raw / --comment
70
+ --short Use the encrypted /s/<id> short-URL form (share
71
+ subcommand only). See SHORT LINKS below.
72
+ --json Machine-readable output (safe subcommand only).
73
+ --audit Also print GitHub links to server-side source
74
+ files (safe subcommand only).
75
+ --keep-open feedback subcommand: keep the bridge alive across
76
+ many submits instead of exiting on the first one.
77
+ --log-file <path> feedback subcommand: append one JSON line per
78
+ submit to <path> (mirror of stdout, for harnesses
79
+ that can't tail a background process).
80
+ --message <text> feedback subcommand: show <text> as a banner
81
+ above the document.
82
+
83
+ ENVIRONMENT
84
+ SDOCS_URL Fallback base URL if --url is not passed.
85
+
86
+ INTERACTIVE FEEDBACK (sdoc feedback)
87
+ An agent writes a fenced \`\`\`form block into a markdown file and runs
88
+ \`sdoc feedback file.md\`. The browser renders real form controls
89
+ (radio, checkbox, select, text, textarea, number, date). When the
90
+ user clicks a submit button:
91
+
92
+ - the bridge writes their answers into the same file
93
+ (under \`answers:\` and \`submissions:\` inside the form block)
94
+ - one JSON line lands on stdout: {event, by, at, scope, values, final}
95
+ - in single-shot mode (no --keep-open) the process exits 0
96
+ - in --keep-open mode the bridge stays alive for the next click
97
+
98
+ Run \`sdoc feedback\` (no args) for the full DSL reference: field
99
+ types, button options, the multi-round flow, and how agents on
100
+ different harnesses should consume the events.
101
+
102
+ LIVE BRIDGE (sdoc bridge)
103
+ \`sdoc <file>\` renders a snapshot: the document travels in the URL and
104
+ opens read-only-by-default. Nothing connects back to disk, so it works
105
+ everywhere with no prompts. Use this to read, share, or hand off a file.
106
+
107
+ \`sdoc bridge <file>\` opens a live session instead. The CLI starts a
108
+ local server on 127.0.0.1 and the browser connects to it, so edits in
109
+ the page autosave to the file on disk and external edits push to the
110
+ page. The terminal stays parked until you close the tab or press
111
+ Ctrl-C, so run it in the background when you want to keep working.
112
+
113
+ The first time the page connects, the browser asks permission to talk
114
+ to a local process (Chrome calls this "Apps on device" / Local Network
115
+ Access). The user has to accept, or the page stays read-only. Safari
116
+ does not allow the loopback connection at all; there the page stays on
117
+ the read-only snapshot.
118
+
119
+ FILE INFO CARD
120
+ When you \`sdoc <file>\`, the browser shows a small info card
121
+ above the document with:
122
+ file The filename — included in the share URL.
123
+ path Relative path from the cwd — local only.
124
+ fullPath Absolute path on your machine — local only.
125
+ tags YAML front-matter tags. Editable when the Bridge is
126
+ connected; read-only otherwise.
127
+
128
+ Local fields (path, fullPath) are passed to the browser via a
129
+ separate URL parameter that JS reads into memory and then strips
130
+ from the address bar on load. They never appear in any URL the
131
+ user can copy, and \`sdoc share <file>\` never includes them in
132
+ the generated link. If someone opens your shared URL, only
133
+ \`file\` is visible.
134
+
135
+ LIBRARY (personal, on-machine index)
136
+ Every \`sdoc <file>\` records the file in ~/.sdocs/library-index.json -
137
+ path, title, tags, last-touched. \`sdoc library\` opens a search UI at
138
+ smalldocs.org/library that talks to a local loopback agent the CLI runs
139
+ on 127.0.0.1:47843. Click a result to open the file for live editing.
140
+ Nothing about the library ever leaves the machine.
141
+
142
+ Top commands:
143
+ sdoc library Open the search UI.
144
+ sdoc library ls List indexed files in this project.
145
+ sdoc library ls --tags Tag bag (tag - count) for this project.
146
+ sdoc library rebuild Walk \$HOME and refresh every entry.
147
+ sdoc library status Show enabled/disabled + entry count.
148
+ sdoc library autostart Manage the macOS LaunchAgent.
149
+
150
+ Tagging works two ways, both ending up in the file's YAML front
151
+ matter on disk:
152
+ 1. Front-matter \`tags: [a, b]\` written directly (or edited via
153
+ the Tags row in the file-info card when the Bridge is live).
154
+ 2. CLI flag at open time: \`sdoc plan.md +planning +q2\` injects
155
+ those tags into the file's front matter. The \`+\` prefix is
156
+ shell-safe (\`#\` would be eaten as a comment).
157
+
158
+ Files in OS-managed temp folders (/tmp, ~/.cache, ~/.Trash, etc.)
159
+ get a RESCUE COPY at index time so the entry survives when the OS
160
+ cleans the original. Per-file opt-out: \`sdocs-library: false\` in
161
+ the file's front matter. Per-directory opt-out: drop a .sdocsignore
162
+ (subset of gitignore syntax) into any directory.
163
+
164
+ Run \`sdoc library --help\` for the full reference - commands,
165
+ tagging, rescue, autostart, security gates, and answers to common
166
+ questions ("I added a tag and it didn't save", "an entry seems
167
+ stale", "how do I keep this work folder out of the library").
168
+
169
+ SHORT LINKS (sdoc share --short)
170
+ By default, \`sdoc share <file>\` encodes the document into the URL hash:
171
+ \`https://smalldocs.org/#md=<base64url>\`. The whole document lives in the
172
+ hash, which the browser does not send to any server.
173
+
174
+ \`--short\` produces a shorter, encrypted form: \`https://smalldocs.org/s/<id>#k=<key>\`.
175
+
176
+ How it works:
177
+ 1. The CLI brotli-compresses the content, generates a 256-bit AES-GCM
178
+ key + 96-bit nonce locally, and encrypts the compressed bytes.
179
+ 2. The CLI POSTs the ciphertext (nonce + ct + auth tag, base64url) to
180
+ /api/short. The server stores it under a random short id and
181
+ returns the id. The key NEVER leaves the CLI.
182
+ 3. The CLI assembles \`https://smalldocs.org/s/<id>#k=<key>\` and copies
183
+ it to the clipboard. The key lives in the URL fragment, which the
184
+ browser does not send to the server on page load.
185
+ 4. Whoever opens the link: the browser fetches the ciphertext from
186
+ /api/short/<id>, reads \`#k=\` from window.location.hash, decrypts
187
+ in JavaScript, and renders.
188
+
189
+ What the server can see:
190
+ - That a ciphertext blob was uploaded under id <id>.
191
+ - When it was fetched and from which IP (standard server logs).
192
+ What the server cannot see:
193
+ - The plaintext document. It does not have the key.
194
+ - The key. The key never leaves the URL fragment.
195
+
196
+ Trade-offs vs the default \`#md=\` hash form:
197
+ - + Shorter URL.
198
+ - + Survives URL-length limits (some chat apps truncate at ~2k chars).
199
+ - - Requires the server to remain reachable (the default form does not).
200
+ - - Stored ciphertext can be deleted by the operator at any time.
201
+ - - Server logs reveal access patterns even though contents are encrypted.
202
+
203
+ If you don't trust an SmallDocs operator with even those metadata, use the
204
+ default \`sdoc share <file>\` (no --short) which never contacts the
205
+ server at all.
206
+
207
+ VERIFYING THE SERVER (sdoc safe)
208
+ \`sdoc safe\` asks https://smalldocs.org what commit it is running, pulls the
209
+ authoritative fingerprint list for that commit from GitHub (published by the
210
+ publish-manifest workflow on every push to main), downloads every frontend
211
+ file from the host, hashes each one with SHA-256, and compares. Bytes come
212
+ from the host; fingerprints come from GitHub. The host cannot produce a
213
+ match it did not already publish to GitHub.
214
+
215
+ It does not prove anything about server-side code (that runs on a machine
216
+ we control). \`sdoc safe --audit\` prints GitHub links to the server files
217
+ an agent or human would need to read to audit the rest.
218
+
219
+ \`sdoc safe --json\` returns structured output for scripting.
220
+
221
+ MATH
222
+ Inline $...$ and display $$...$$ are rendered as LaTeX via KaTeX.
223
+ Inline: The energy is $E = mc^2$.
224
+ Display: $$\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}$$
225
+ Supported commands: https://katex.org/docs/supported.html
226
+
227
+ STYLED MARKDOWN FORMAT
228
+ SmallDocs extends standard .md files with an optional YAML
229
+ front matter block (the same standard used by Jekyll, Hugo, Obsidian).
230
+ The \`styles\` key controls every visual aspect of the rendered document.
231
+
232
+ ---
233
+ title: "My Document"
234
+ styles:
235
+ fontFamily: Inter
236
+ baseFontSize: 16
237
+ color: "#1c1917"
238
+ h1: { fontSize: 2.2, color: "#1a3a5c", fontWeight: 700 }
239
+ p: { lineHeight: 1.85, marginBottom: 1.1 }
240
+ ---
241
+ # My Document
242
+ Content here...
243
+
244
+ Colors work in both themes automatically — dark mode versions
245
+ are generated by inverting lightness. Use \`dark:\` to override.
246
+
247
+ COMMENTS
248
+ SmallDocs files can carry reviewer comments in their YAML front matter
249
+ under a \`comments:\` key. Comments do not modify the body — they're
250
+ resolved at render time by index lookup with a text-based fallback.
251
+ A typical use:
252
+ 1. an agent generates a draft .md file
253
+ 2. a human reads it via \`sdoc <file> --comment\`, leaves comments
254
+ 3. the user copies the .md back to the agent (with comments)
255
+ 4. the agent processes the comments and regenerates
256
+
257
+ Or the inverse: an agent writes comments into the front matter to
258
+ flag uncertainty, and runs \`sdoc <file> --comment\` to surface them
259
+ for the human.
260
+
261
+ Run \`sdoc comments\` for the full format reference and authoring guide.
262
+ Run \`sdoc schema\` for the complete list of style properties.
263
+ Run \`sdoc charts\` for chart types, options, and styling.
264
+ Run \`sdoc library --help\` for the personal markdown library reference.
265
+ `;
266
+
267
+ const COMMENTS_HELP = `
268
+ SmallDocs — Comments
269
+ ================
270
+ Reviewer comments are stored in YAML front matter under \`comments:\`.
271
+ The body is never modified — anchoring happens at render time.
272
+ This makes the format safe for round-tripping through agents and
273
+ markdown tooling that doesn't understand SmallDocs-specific markers.
274
+
275
+ WHEN TO USE THIS
276
+ Two flows benefit from comments:
277
+
278
+ 1. Human reviewing agent output. The agent generates a .md file,
279
+ the human runs \`sdoc <file> --comment\`, leaves notes, and pastes
280
+ the file (with its YAML) back to the agent. The agent reads
281
+ \`comments:\` and acts on each entry.
282
+
283
+ 2. Agent flagging uncertainty for a human. The agent writes one or
284
+ more comments into the front matter, then opens the file with
285
+ \`sdoc <file> --comment\` so the user sees the annotations rendered
286
+ beside the relevant blocks.
287
+
288
+ OPENING IN COMMENT MODE
289
+ sdoc <file> --comment Open in comment mode (or --mode comment)
290
+
291
+ Comment mode shows a gutter "+" button beside every top-level block
292
+ for adding new comments, and renders existing comments as yellow
293
+ sidecar cards beneath their anchored blocks.
294
+
295
+ TWO INPUT FORMATS
296
+ SmallDocs accepts comments in two interchangeable formats. Both render
297
+ identically in comment mode. Pick whichever is more natural for the
298
+ context:
299
+
300
+ 1. Markdown footnote format (RECOMMENDED FOR AGENTS).
301
+ Standard markdown footnote syntax. The agent edits the body,
302
+ adding [^cN] markers where the comment anchors. No counting of
303
+ element indices required — anchoring is positional, computed
304
+ from the marker's position in the body.
305
+
306
+ 2. YAML front-matter format.
307
+ The canonical on-disk store. Used by the SmallDocs UI and round-trip
308
+ export. Comments live as a structured list under \`comments:\`.
309
+
310
+ At load time, SmallDocs parses both: footnote markers are lifted out of
311
+ the body and merged with the YAML list. On save (round-trip export),
312
+ comments are normalised to YAML.
313
+
314
+ AUTHORING VIA MARKDOWN FOOTNOTES
315
+ Recommended path for agents that produce text. No tag:n counting,
316
+ no block_text, just standard markdown. Two patterns:
317
+
318
+ Inline (anchor a specific phrase):
319
+ Wrap the phrase in [phrase][^cN] and add the definition at the
320
+ end of the document.
321
+
322
+ The migration was [implemented in three weeks][^c1] this quarter.
323
+
324
+ [^c1]: agent - actually slipped to five weeks
325
+
326
+ Block (anchor an entire paragraph or heading):
327
+ Place a lone [^cN] at the end of the block (after the closing
328
+ period) and add the definition at the end.
329
+
330
+ The reliability picture was equally encouraging.[^c2]
331
+
332
+ [^c2]: agent - need to specify what "incident-free" means
333
+
334
+ Definitions support optional author and a [resolved] marker:
335
+ [^c3]: priya [resolved] - already addressed
336
+ [^c4]: agent - check Q2 numbers (block p:5)
337
+
338
+ Only footnote ids matching the cN pattern (c1, c2, ...) are treated
339
+ as comments. Other footnote ids (e.g. [^citation1]) keep standard
340
+ footnote semantics.
341
+
342
+ This format renders sensibly in any markdown viewer — refs as
343
+ superscripts, definitions at the bottom — so the file is useful
344
+ outside SmallDocs too.
345
+
346
+ COMMENT KINDS
347
+ block Anchored to an entire block element (paragraph, heading,
348
+ list, code block, table, blockquote).
349
+ inline Anchored to a specific text span within a block.
350
+
351
+ THE BLOCK ID SCHEME
352
+ Both kinds carry a \`block\` field of the form "tag:n":
353
+ - tag is the lowercased HTML element name (p, h1, h2, h3, h4,
354
+ ul, ol, pre, blockquote, table, plus "chart" for chart blocks).
355
+ - n is the 0-indexed position of that element among siblings of
356
+ the same tag, in render order across the entire document.
357
+
358
+ Examples:
359
+ "h2:0" First <h2> in the document.
360
+ "p:3" Fourth <p> in render order (ignores headings/lists).
361
+ "ul:0" First unordered list.
362
+ "pre:1" Second code block.
363
+
364
+ Per-tag-type indexing is more resilient to reordering than a single
365
+ global ordinal, but indices still drift if blocks of the same type
366
+ are inserted upstream. See "Survival hints" below.
367
+
368
+ SCHEMA — A FULLY-POPULATED EXAMPLE
369
+ ---
370
+ title: "Q2 Roadmap (Draft)"
371
+ # Comments: block "tag:n" = nth (0-indexed) <tag> in render order.
372
+ # block kind may carry block_text (first ~60 chars) as a survival hint when the index drifts.
373
+ # inline kind anchors via quote (+ optional prefix/suffix). resolved: true marks addressed.
374
+ comments:
375
+ - id: c1
376
+ kind: block
377
+ block: "h2:0"
378
+ block_text: "Context"
379
+ author: priya
380
+ color: "#ffbb00"
381
+ at: "2026-04-22T09:14:00Z"
382
+ text: "rename this to 'Where Q1 left us' — sharper"
383
+ - id: c2
384
+ kind: inline
385
+ quote: "shipped on time"
386
+ prefix: "every committed feature "
387
+ suffix: " and within budget"
388
+ block: "p:0"
389
+ author: priya
390
+ color: "#ffbb00"
391
+ at: "2026-04-22T09:15:00Z"
392
+ text: "auth migration slipped 2 weeks — please correct"
393
+ - id: c3
394
+ kind: block
395
+ block: "p:5"
396
+ block_text: "Cost discipline becomes more visible in Q2"
397
+ author: priya
398
+ color: "#ffbb00"
399
+ at: "2026-04-22T09:24:00Z"
400
+ text: "align the $180k figure with finance before publishing"
401
+ resolved: true
402
+ ---
403
+
404
+ # Q2 Roadmap (Draft)
405
+ ## Context
406
+ Q1 closed strong: every committed feature shipped on time and within budget...
407
+
408
+ FIELDS
409
+ Required for both kinds:
410
+ id Stable identifier. Convention: c1, c2, c3...
411
+ kind "block" or "inline"
412
+ text The reviewer's note (the comment body).
413
+
414
+ Required for inline:
415
+ quote The exact text span in the rendered body to highlight.
416
+
417
+ Optional but recommended:
418
+ block The "tag:n" anchor. Used as a fast lookup. Optional
419
+ for inline (the quote alone is enough), required for
420
+ block (it's the only anchor).
421
+ block_text For block kind only. The first ~60 characters of
422
+ the block's plain text at the time of writing.
423
+ Survival hint: when "tag:n" no longer matches (the
424
+ document was edited and indices drifted), readers
425
+ fall back to scanning for a block whose start
426
+ matches block_text.
427
+ prefix For inline kind. Up to 60 chars of the rendered
428
+ text immediately before the quote, used to
429
+ disambiguate when the quote appears multiple times.
430
+ suffix Same as prefix but for the text immediately after.
431
+ resolved true if the comment has been addressed. Preserved
432
+ for audit; readers should skip resolved comments
433
+ when generating action lists.
434
+ author Display name on the rendered card. Default: "user".
435
+ color Card tint, hex (#rrggbb). Default: "#ffbb00" (yellow).
436
+ at ISO 8601 timestamp. Default: now (browser side).
437
+
438
+ ID GENERATION
439
+ Use c1, c2, c3... in chronological order. To pick the next id, take
440
+ the highest cN currently in the file and add 1. Don't reuse ids of
441
+ deleted comments — gaps are fine. Non-cN ids are tolerated but lose
442
+ the auto-increment guarantee.
443
+
444
+ ANCHOR RESOLUTION (HOW READERS RECOVER FROM DRIFT)
445
+ When a tool (the SmallDocs renderer or another agent) loads the file,
446
+ each comment is resolved in this order:
447
+
448
+ Block kind:
449
+ 1. Try \`block: "tag:n"\` exactly.
450
+ 2. If found, optionally verify the resolved block's leading text
451
+ matches \`block_text\`. If not, fall through.
452
+ 3. Search the document for any block whose first ~60 chars start
453
+ with \`block_text\`.
454
+ 4. Give up — comment is orphaned.
455
+
456
+ Inline kind:
457
+ 1. Find the block via \`block: "tag:n"\`.
458
+ 2. Inside that block, find \`prefix + quote + suffix\`.
459
+ 3. Fall back to \`prefix + quote + suffix\` anywhere in the body.
460
+ 4. Fall back to \`quote\` alone, anywhere in the body.
461
+ 5. Give up — comment is orphaned.
462
+
463
+ AUTHORING TIPS FOR AGENTS
464
+ - Prefer the markdown-footnote authoring path (above). It avoids
465
+ the index-counting work the YAML path requires and is the most
466
+ reliable way for an LLM to write a comment that anchors correctly.
467
+ - If you do author in YAML directly:
468
+ - Compute "tag:n" by counting same-tag elements in render order.
469
+ Headings, paragraphs, lists each have their own counters.
470
+ - Counting errors are common. The fallback tiers (block_text
471
+ for block kind, prefix/suffix or quote-only search for inline)
472
+ will rescue an off-by-one index — but only if you populate them.
473
+ - For block comments, ALWAYS populate block_text (first ~60 chars
474
+ of the block's plain text).
475
+ - For inline comments, ensure the comment is uniquely resolvable:
476
+ either pick a long unique quote, or populate prefix/suffix.
477
+ - To mark a comment addressed without losing audit trail, set
478
+ \`resolved: true\` (YAML) or add \`[resolved]\` after the author
479
+ name in the footnote definition.
480
+ - When acting on comments, skip those marked resolved — they
481
+ describe past work, not pending requests.
482
+ `;
483
+
484
+ const SCHEMA = `
485
+ SmallDocs — Styles Schema
486
+ =====================
487
+ All style values live under the \`styles:\` key in YAML front matter.
488
+ Every property is optional — omit anything you want left at its default.
489
+
490
+ ┌────────────────────────────────────────────────────────────┐
491
+ │ IMPORTANT — if you set ANY custom colours, run: │
492
+ │ sdoc color-analysis <file> │
493
+ │ It grades every text-on-background pair for BOTH the light │
494
+ │ and dark themes and flags anything unreadable. Hand-picked │
495
+ │ palettes (especially when you only looked at one theme) are │
496
+ │ the #1 source of unreadable docs. Run it before you ship. │
497
+ └────────────────────────────────────────────────────────────┘
498
+
499
+ GENERAL
500
+ fontFamily string Any of the supported fonts (see FONTS below)
501
+ Default: "Inter"
502
+ baseFontSize number Base font size in px. All rem/em values scale from this.
503
+ Default: 16
504
+ background string Page background color (hex).
505
+ Default: "#ffffff" (light) / "#2c2a26" (dark)
506
+ color string Master body text color (hex). Cascades to headings,
507
+ paragraphs, and lists unless those are overridden.
508
+ Default: "#1c1917"
509
+ lineHeight number Global line-height multiplier.
510
+ Default: 1.75
511
+
512
+ HEADINGS (general heading controls)
513
+ headers:
514
+ scale number Relative size multiplier applied across all heading levels.
515
+ Default: 1.0
516
+ marginBottom number Space below headings (em). Default: 0.4
517
+ color string Heading color — cascades to h1/h2/h3/h4 unless overridden.
518
+ Default: inherits \`color\`
519
+
520
+ PER-HEADING (each independently overrides the heading defaults above)
521
+ h1: { fontSize: number, color: string, fontWeight: number }
522
+ h2: { fontSize: number, color: string, fontWeight: number }
523
+ h3: { fontSize: number, color: string, fontWeight: number }
524
+ h4: { fontSize: number, color: string, fontWeight: number }
525
+
526
+ fontSize is in rem (relative to baseFontSize).
527
+ Sensible defaults: h1 2.2, h2 1.55, h3 1.2, h4 1.0
528
+ fontWeight: 400 (regular) · 600 (semibold) · 700 (bold)
529
+
530
+ PARAGRAPH
531
+ p:
532
+ lineHeight number Line height for body paragraphs. Default: 1.75
533
+ marginBottom number Space between paragraphs (em). Default: 1.1
534
+ color string Paragraph text color. Default: inherits \`color\`
535
+
536
+ LISTS
537
+ list:
538
+ color string Color for list items and bullet/number markers.
539
+ Default: inherits paragraph color
540
+
541
+ LINKS
542
+ link:
543
+ color string Link color. Default: "#2563eb"
544
+ decoration string "underline" | "none". Default: "underline"
545
+
546
+ CODE
547
+ code:
548
+ fontFamily string Monospace font. Default: "ui-monospace, monospace"
549
+ background string Inline/block code background color. Default: "#F1EDE8"
550
+ padding number Inline code padding (em). Default: 0.2
551
+
552
+ BLOCKQUOTE
553
+ blockquote:
554
+ borderColor string Left border accent color. Default: "#2563eb"
555
+ borderWidth number Left border thickness (px). Default: 3
556
+ background string Quote background color. Default: "#f7f5f2"
557
+ color string Quote text color. Default: "#6b6560"
558
+
559
+ BLOCKS (shared styling for code, blockquote, and chart blocks)
560
+ blocks:
561
+ background string Background for all block types. Cascades to code,
562
+ blockquote, and chart backgrounds unless overridden.
563
+ color string Text color for all block types. Cascades to code,
564
+ blockquote, and chart text unless overridden.
565
+
566
+ CHARTS
567
+ chart:
568
+ accent string Palette base color (hex). Default: "#3b82f6"
569
+ palette string Palette mode. Default: "monochrome"
570
+ Options: monochrome, complementary, analogous, triadic,
571
+ pastel, warm, cool, earth
572
+ background string Chart background. Default: inherits blocks.background
573
+ textColor string Chart labels/axes. Default: inherits blocks.color
574
+
575
+ Run \`sdoc charts\` for the full chart reference — chart types, JSON
576
+ format, axis/legend/annotation options, and per-chart styling overrides.
577
+
578
+ TABLES
579
+ table:
580
+ color string Text color for header and body cells.
581
+ Default: inherits \`color\`
582
+ headerBackground string Background for the header row.
583
+ Default: "#f4f1ed" (light) / "#2c2926" (dark)
584
+ oddBackground string Background for odd body rows.
585
+ Default: "#ffffff" (light) / "#2c2a26" (dark)
586
+ evenBackground string Background for even body rows.
587
+ Default: "#fafaf8" (light) / "#242220" (dark)
588
+ border string Cell border color.
589
+ Default: "#e2ddd6" (light) / "#3d3935" (dark)
590
+
591
+ SLIDES
592
+ Slides embedded via \`\`\`slide / ~~~slide fenced blocks inherit these
593
+ styles automatically (background, fonts, link color, code block colors,
594
+ heading colors on shape markdown). For explicit references, shape
595
+ attributes accept \$path.to.prop tokens - e.g. \`fill=\$h1.color\`,
596
+ \`bg=\$blocks.background\`. You should now run \`sdoc slides\` for the
597
+ full slide reference, so you don't guess at the DSL.
598
+
599
+ COLOR CASCADE
600
+ Colors cascade from general → specific:
601
+ color → headers.color → h1.color, h2.color, h3.color, h4.color
602
+ color → p.color → list.color
603
+ blocks.background → code.background, blockquote.background, chart.background
604
+ blocks.color → code.color, blockquote.color, chart.textColor
605
+ Set a child color only when you want it to differ from its parent.
606
+
607
+ THEME COLORS
608
+ Top-level colors are light-mode colors. Dark mode is auto-generated
609
+ by inverting lightness (same hue, flipped brightness). Light backgrounds
610
+ become dark, dark text becomes light. Colors already very dark (like a
611
+ dark code block background) are kept as-is.
612
+
613
+ This means you only need to specify colors ONCE:
614
+
615
+ ---
616
+ styles:
617
+ color: "#2d1810"
618
+ background: "#fdf6f0"
619
+ headers: { color: "#8b2500" }
620
+ blocks:
621
+ background: "#f5e6d8"
622
+ color: "#5a3e2e"
623
+ ---
624
+
625
+ Dark mode will automatically get inverted versions of all colors above.
626
+
627
+ To override specific dark-mode colors, add a \`dark:\` block:
628
+
629
+ ---
630
+ styles:
631
+ color: "#2d1810"
632
+ background: "#fdf6f0"
633
+ blocks:
634
+ background: "#f5e6d8"
635
+ dark:
636
+ background: "#1a1210"
637
+ blocks:
638
+ background: "#2a1a1a"
639
+ ---
640
+
641
+ Non-color properties (fonts, sizes, spacing, weights) remain at the
642
+ top level and are shared across both themes.
643
+
644
+ Because dark mode is auto-derived, a colour that reads fine in the
645
+ light theme can invert to something unreadable in the dark theme (a
646
+ mid-blue heading becomes dark navy, light text becomes near-black).
647
+ ALWAYS run \`sdoc color-analysis <file>\` after setting colours — it
648
+ checks both themes and tells you exactly which pair fails and why.
649
+
650
+ FONTS (24 supported, loaded lazily from Google Fonts)
651
+ Inter · Roboto · Open Sans · Lato · Montserrat · Source Sans 3
652
+ Oswald · Raleway · Poppins · Merriweather · Ubuntu · Nunito
653
+ Playfair Display · Roboto Slab · PT Sans · Lora · Mulish · Noto Sans
654
+ Rubik · Dosis · Josefin Sans · PT Serif · Libre Franklin · Crimson Text
655
+
656
+ EXAMPLE — editorial article with colored heading tiers
657
+ ---
658
+ styles:
659
+ fontFamily: Lora
660
+ baseFontSize: 17
661
+ background: "#fffaf5"
662
+ color: "#1a1a2e"
663
+ h1: { fontSize: 2.3, fontWeight: 700, color: "#c0392b" }
664
+ h2: { fontSize: 1.55, fontWeight: 600, color: "#8e44ad" }
665
+ h3: { fontSize: 1.2, fontWeight: 600, color: "#16a085" }
666
+ p: { lineHeight: 1.9, marginBottom: 1.2 }
667
+ link: { color: "#e67e22" }
668
+ blocks:
669
+ background: "#faf0eb"
670
+ blockquote: { borderColor: "#c0392b", color: "#7f8c8d" }
671
+ dark:
672
+ background: "#1a1520"
673
+ h1: { color: "#ef6f5e" }
674
+ h2: { color: "#c490e4" }
675
+ blockquote: { borderColor: "#ef6f5e" }
676
+ ---
677
+ `;
678
+
679
+ const CHARTS_HELP = `
680
+ SmallDocs — Charts
681
+ ==============
682
+ Render beautiful charts in markdown using \`\`\`chart code blocks.
683
+ Charts are powered by Chart.js, loaded lazily from CDN only when needed.
684
+
685
+ BASIC SYNTAX
686
+ Wrap a JSON object in a \`\`\`chart fenced code block:
687
+
688
+ \`\`\`chart
689
+ {
690
+ "type": "bar",
691
+ "title": "Monthly Revenue",
692
+ "labels": ["Jan", "Feb", "Mar"],
693
+ "values": [100, 150, 130]
694
+ }
695
+ \`\`\`
696
+
697
+ CHART TYPES
698
+ pie Circular segments (use "color" for monochrome shading)
699
+ doughnut Hollow-center pie (alias: donut)
700
+ bar Vertical bars
701
+ horizontal_bar Horizontal bars (alias: hbar)
702
+ stacked_bar Stacked vertical bars
703
+ line Line graph with data points
704
+ area Line with filled area beneath
705
+ stacked_area Multiple filled areas stacked (alias: stacked_line)
706
+ radar Spider/web chart for multi-axis comparison
707
+ polarArea Like pie but equal angles, varying radius
708
+ scatter X/Y point plots
709
+ bubble Like scatter with size dimension
710
+ mixed Combo chart — bar + line on same plot (alias: combo)
711
+
712
+ DATA FORMATS
713
+ Simple (single dataset):
714
+ "labels": ["A", "B", "C"],
715
+ "values": [10, 20, 15]
716
+
717
+ Multi-dataset:
718
+ "labels": ["Q1", "Q2"],
719
+ "datasets": [
720
+ { "label": "2024", "values": [10, 20] },
721
+ { "label": "2025", "values": [12, 25] }
722
+ ]
723
+
724
+ Scatter/Bubble:
725
+ "datasets": [
726
+ { "label": "Group", "data": [{"x": 1, "y": 2}, {"x": 3, "y": 5}] }
727
+ ]
728
+
729
+ CHART OPTIONS
730
+ title string Chart heading
731
+ subtitle string Smaller text below title
732
+ labels string[] Category labels
733
+ values number[] Data for a single dataset
734
+ datasets array Multiple datasets (see above)
735
+ color string Single accent color (hex)
736
+ colors string[] Per-segment/bar custom colors
737
+
738
+ AXIS OPTIONS
739
+ xAxis / xLabel string X-axis label
740
+ yAxis / yLabel string Y-axis label
741
+ y2Axis string Right y-axis label (enables dual axis)
742
+ min number Minimum value on value axis
743
+ max number Maximum value on value axis
744
+ stepSize number Tick interval
745
+ beginAtZero boolean Default true. Set false for auto-range.
746
+
747
+ NUMBER FORMATTING
748
+ format string "currency" ($), "euro" (€), "pound" (£),
749
+ "percent" (%), "comma" (1,000)
750
+ prefix string Custom value prefix (e.g. "£")
751
+ suffix string Custom value suffix (e.g. " kg", "°C")
752
+ y2Format string Format for right y-axis
753
+ y2Prefix string Prefix for right y-axis
754
+ y2Suffix string Suffix for right y-axis
755
+
756
+ DISPLAY OPTIONS
757
+ legend boolean Show/hide legend (auto by default)
758
+ legendPosition string "top", "bottom" (default), "left", "right"
759
+ dataLabels boolean Show values on chart (default true). Set false for clean look.
760
+ aspectRatio number Width/height ratio (e.g. 2 for wide, 0.8 for tall)
761
+ stacked boolean Force stacking on bar/line charts
762
+
763
+ DATASET OPTIONS (inside each dataset object)
764
+ label string Name shown in legend
765
+ values number[] Data points
766
+ data object[] For scatter: [{x, y}], for bubble: [{x, y, r}]
767
+ color string Dataset color (hex)
768
+ colors string[] Per-bar colors within dataset
769
+ type string Override type in mixed charts ("bar" or "line")
770
+ yAxisID string "y" (left) or "y2" (right) for dual-axis charts
771
+ fill boolean Fill area under line
772
+ tension number Line smoothing (0 = straight, 0.4 = smooth)
773
+ order number Draw order (lower = rendered on top)
774
+
775
+ ANNOTATIONS (reference lines)
776
+ "annotations": [
777
+ { "y": 60, "label": "Target", "color": "#ef4444" },
778
+ { "x": "Mar", "label": "Launch", "dashed": true }
779
+ ]
780
+
781
+ y / x number/string Position of the reference line
782
+ label string Text label on the line
783
+ color string Line color
784
+ width number Line thickness (default 2)
785
+ dashed boolean Dashed style (default true)
786
+ position string Label position: "start", "center", "end"
787
+
788
+ CHART STYLING (via front matter or style panel)
789
+ Charts inherit background and text colors from the block cascade:
790
+
791
+ ---
792
+ styles:
793
+ blocks:
794
+ background: "#1a1a2e" # all blocks: code, blockquote, charts
795
+ color: "#c8c3bc" # text in all blocks
796
+ chart:
797
+ accent: "#6366f1" # palette base color
798
+ palette: monochrome # palette generation mode
799
+ background: "#0e4a1a" # override blocks.background for charts only
800
+ textColor: "#c8f0d8" # override blocks.color for charts only
801
+ ---
802
+
803
+ COLOR CASCADE FOR BLOCKS
804
+ blocks.background → code.background, blockquote.background, chart.background
805
+ blocks.color → code.color, blockquote.color, chart.textColor
806
+ Set a child value only when you want it to differ from the parent.
807
+
808
+ DARK MODE
809
+ All colors auto-generate dark-mode counterparts (lightness inverted).
810
+ Add a \`dark:\` block to override specific values:
811
+ dark:
812
+ blocks:
813
+ background: "#2a1a1a"
814
+
815
+ PALETTE MODES
816
+ monochrome Same hue, varying lightness (default)
817
+ complementary Hues spread evenly around the color wheel
818
+ analogous Neighboring hues for a harmonious feel
819
+ triadic Three base hues 120° apart
820
+ pastel Soft, light colors
821
+ warm Reds, oranges, yellows
822
+ cool Blues, teals, purples
823
+ earth Browns, olives, muted greens
824
+
825
+ Per-chart override: set "accent" and/or "palette" directly in the chart JSON.
826
+ Per-chart colors: set "colors": ["#hex", ...] to override the palette entirely.
827
+ Single-color pie: set "color": "#hex" on a pie/doughnut for monochrome shading.
828
+
829
+ MIXED CHART EXAMPLE (dual y-axis)
830
+ \`\`\`chart
831
+ {
832
+ "type": "mixed",
833
+ "title": "Revenue vs Growth",
834
+ "labels": ["Q1", "Q2", "Q3", "Q4"],
835
+ "datasets": [
836
+ { "label": "Revenue", "type": "bar", "values": [50, 65, 80, 95], "yAxisID": "y" },
837
+ { "label": "Growth", "type": "line", "values": [12, 30, 23, 19], "yAxisID": "y2" }
838
+ ],
839
+ "yAxis": "Revenue ($M)",
840
+ "y2Axis": "Growth %",
841
+ "format": "currency",
842
+ "y2Format": "percent"
843
+ }
844
+ \`\`\`
845
+ `;
846
+
847
+ const DIAGRAMS_HELP = `
848
+ SmallDocs — Diagrams
849
+ ================
850
+ Render Mermaid diagrams in markdown using \`\`\`mermaid code blocks.
851
+ Mermaid is loaded lazily from CDN only when a diagram is present.
852
+
853
+ BASIC SYNTAX
854
+ \`\`\`mermaid
855
+ graph TD
856
+ A[Start] --> B{Decision}
857
+ B -- yes --> C[Do this]
858
+ B -- no --> D[Do that]
859
+ \`\`\`
860
+
861
+ STANDALONE .mmd FILES
862
+ \`sdoc graph.mmd\` works like \`sdoc file.md\` - the CLI wraps the
863
+ contents in a \`\`\`mermaid fence before opening. Same for share:
864
+ \`sdoc share graph.mmd\`. \`.mermaid\` files work the same way.
865
+
866
+ SUPPORTED DIAGRAM TYPES
867
+ flowchart / graph flowchart TD, LR, etc.
868
+ sequenceDiagram interaction sequences
869
+ classDiagram UML-style class relationships
870
+ stateDiagram-v2 state machines
871
+ erDiagram entity-relationship
872
+ gantt timelines
873
+ pie proportional breakdown
874
+ journey user-journey diagrams
875
+ gitGraph git history visualisation
876
+ mindmap mind maps
877
+ timeline chronological events
878
+ quadrantChart 2x2 matrix
879
+ sankey-beta flow diagrams
880
+ See https://mermaid.js.org for the full syntax reference.
881
+
882
+ THEMING
883
+ Diagrams inherit colors from the SmallDocs blocks cascade:
884
+
885
+ \`\`\`yaml
886
+ styles:
887
+ blocks:
888
+ background: "#f4f1ed" # diagram wrapper bg
889
+ color: "#6b6560" # node text / lines
890
+ \`\`\`
891
+
892
+ In dark mode the inverted block colors apply automatically.
893
+ For finer-grained control, set Mermaid theme variables in the
894
+ diagram source itself, but note that \`%%{init:...}%%\` directives
895
+ are stripped by SmallDocs as a security measure (they can otherwise
896
+ override sanitisation settings at parse time).
897
+
898
+ LIMITS
899
+ - Per-diagram source cap: 64 KB.
900
+ - Per-document diagram cap: 50 (excess rendered as plain code).
901
+ - Per-render timeout: 5 seconds (large or pathological graphs error out).
902
+
903
+ SECURITY
904
+ Mermaid runs with \`securityLevel: 'strict'\` and \`htmlLabels: true\`.
905
+ htmlLabels lets long node labels wrap inside a \`<foreignObject>\`,
906
+ which is otherwise a script-injection vector; SmallDocs makes that safe
907
+ by post-sanitising the SVG before render. \`<script>\`, \`<iframe>\`,
908
+ \`<form>\`, \`<input>\`, \`<use>\`, animation tags, \`on*\` event handlers
909
+ and \`javascript:\` URLs are stripped (inside foreignObject and out).
910
+ Source caps and a render timeout cover the DoS surface. Treat diagram
911
+ source as untrusted - it travels in the URL hash with the rest of
912
+ the document.
913
+
914
+ EXAMPLE
915
+ \`\`\`mermaid
916
+ sequenceDiagram
917
+ participant U as User
918
+ participant S as SmallDocs
919
+ participant C as CDN
920
+ U->>S: open page with diagram
921
+ S->>C: load mermaid.min.js (lazy, first time only)
922
+ C-->>S: script
923
+ S->>S: render() → SVG
924
+ S->>U: paint diagram
925
+ \`\`\`
926
+ `;
927
+
928
+
929
+ const CELLS_HELP = `
930
+ SmallDocs - Cells (sheets)
931
+ ======================
932
+ Render data as an inline spreadsheet using \`\`\`cells fenced code blocks.
933
+ The body is CSV; the grid shows column letters, row numbers, type-aware
934
+ alignment, click-and-drag selection, and a small copy toolbar. No CDN and
935
+ no dependency - it renders from the markdown itself.
936
+
937
+ BASIC SYNTAX
938
+ Put CSV inside a \`\`\`cells fence:
939
+
940
+ \`\`\`cells
941
+ Region,Q1,Q2,Q3
942
+ North,100,150,130
943
+ South,90,95,-7
944
+ \`\`\`
945
+
946
+ Every row is a row (numbered from 1); every column gets a letter
947
+ (A, B, C ...). A text first row over numeric data is auto-detected as a
948
+ header: shown bold and kept pinned to the top when you sort.
949
+
950
+ DATA & TYPES
951
+ Numbers are detected and align right; text aligns left; blank fields are
952
+ empty cells. Negatives and decimals are numbers. A quoted value like
953
+ "1,200" stays text (no locale guessing). Full CSV quoting is supported:
954
+ commas inside "quotes", escaped ""quotes"", and quoted fields spanning
955
+ multiple lines.
956
+
957
+ Number convention is fixed to comma-thousands / dot-decimal (1,000.50).
958
+ European-convention data (1.000,50; semicolon-delimited CSV) is not
959
+ supported - write numbers as 1000.50 in the source and the grid formats
960
+ them for display.
961
+
962
+ WRAPPING
963
+ A long value wraps once its column hits a width cap (rather than running
964
+ ever wider), growing the row. A literal <br> becomes a line break, and a
965
+ quoted cell with a real newline keeps it. Cell content is always plain
966
+ text - markup never renders.
967
+
968
+ FORMATTING
969
+ Numbers display with thousands separators and negatives in red by default.
970
+ An optional first line sets per-column formats by column letter:
971
+
972
+ \`\`\`cells
973
+ format: A=plain B=$ C=%
974
+ Year,Revenue,Margin
975
+ 2024,12000,0.23
976
+ \`\`\`
977
+
978
+ Renders 2024 (plain - no comma), $12,000.00, 23%.
979
+
980
+ Format tokens (keyed by column letter):
981
+ $ / usd currency, e.g. $12,000.00 (also £ / gbp, € / eur)
982
+ % percent - multiplies by 100, e.g. 0.23 -> 23%
983
+ , / number thousands separators (the default)
984
+ plain / text no number formatting (good for years, ids, codes)
985
+ .N suffix fixed decimals, e.g. $.0 (no cents), %.1, .2
986
+
987
+ Formatting is display only - copy and export always emit the original
988
+ values. This is what makes a cells block more than a CSV: the author
989
+ chooses how each column reads.
990
+
991
+ FORMULAS
992
+ A cell whose value starts with = is a formula, evaluated in the browser.
993
+ The cell shows the result; the formula stays underneath (copy / export and
994
+ the formula bar show the =formula, hovering shows it as a tooltip).
995
+
996
+ \`\`\`cells
997
+ Item,Qty,Price,Total
998
+ Widget,10,12,=B2*C2
999
+ Gadget,15,8,=B3*C3
1000
+ Total,=SUM(B2:B3),,=SUM(D2:D3)
1001
+ \`\`\`
1002
+
1003
+ Supported:
1004
+ arithmetic + - * / ^ % with parentheses, e.g. =(A1+B1)*2
1005
+ references A1, B12; ranges A1:B3 inside a function
1006
+ aggregates SUM AVERAGE (AVG) MIN MAX COUNT COUNTA PRODUCT
1007
+ functions ROUND(x,n) ABS(x) IF(cond, a, b)
1008
+ comparisons = <> < <= > >= (mostly for IF)
1009
+
1010
+ A failed formula shows a short red error code: #DIV/0!, #NAME? (unknown
1011
+ name), #REF! (bad range), #VALUE! (e.g. text in arithmetic), #CIRC! (a
1012
+ circular reference). Formulas recalculate live while you edit.
1013
+
1014
+ MULTIPLE TABS (SHEETS)
1015
+ A document can hold several tabs that work together. Each tab is its own
1016
+ \`\`\`cells block; name it in the fence, right after the word cells:
1017
+
1018
+ \`\`\`cells Expenses
1019
+ Category,Jan,Feb,Mar
1020
+ Rent,1200,1200,1200
1021
+ Food,350,400,380
1022
+ Total,=SUM(B2:B3),=SUM(C2:C3),=SUM(D2:D3)
1023
+ \`\`\`
1024
+
1025
+ \`\`\`cells Summary
1026
+ Metric,Value
1027
+ Grand Total,=SUM(Expenses!B4:D4)
1028
+ \`\`\`
1029
+
1030
+ A formula reads another tab with a Sheet!A1 reference: =Expenses!B4 reads
1031
+ cell B4 of the Expenses tab, =SUM(Expenses!B4:D4) sums a range on it. A
1032
+ bare reference (=B4) always means the current tab. Names are
1033
+ case-insensitive; an unnamed block is Sheet1, Sheet2... by order; if two
1034
+ tabs share a name the first one owns it for references.
1035
+
1036
+ Qualified ranges stay within one tab (Expenses!A1:C1). A range that names
1037
+ two different tabs, a reference to a tab that does not exist, and a cycle
1038
+ that runs between tabs are all reported as errors (#REF! / #CIRC!), never a
1039
+ wrong number or a hang.
1040
+
1041
+ Two ways to view the tabs:
1042
+ - Stacked (default): each tab renders where its block sits, named by a
1043
+ small caption - good for a document with prose between the tabs.
1044
+ - One pane (opt in): expanding ANY tab opens the whole workbook in one
1045
+ window with a tab strip to switch sheets. For an in-document tabbed
1046
+ pane, add cells-tabs: tabbed to the document's front matter and the
1047
+ tabs collapse into a single widget (a tab strip + one grid at a time),
1048
+ placed where the first tab sat.
1049
+
1050
+ VERIFYING (for agents)
1051
+ Check the computed values without a browser:
1052
+
1053
+ sdoc cells verify <file.md> # values of every tab as CSV
1054
+ sdoc cells verify <file.md> --json # structured, lossless
1055
+ sdoc cells verify <file.md> --sheet Summary # one tab only
1056
+
1057
+ It runs the SAME engine the page does, so the numbers it prints are the
1058
+ numbers the document will show. Formula cells print their result; an
1059
+ errored cell prints its code (#REF! etc.) in place. The exit code is 0 when
1060
+ every tab computes cleanly and 1 when any cell errors, so an agent can gate
1061
+ on it. Write formulas, run verify, read the values back, fix, repeat.
1062
+
1063
+ The default output banners each tab with "# sheet: <name>". A data row
1064
+ could itself start with that text, so for machine parsing use --json (its
1065
+ per-tab values array is unambiguous).
1066
+
1067
+ SORTING
1068
+ Hover a column letter: an arrow appears on its right showing what a click
1069
+ will do (up = sort ascending, down = descending, x = clear the sort). The
1070
+ current sort shows as a colored arrow. A text first row is kept pinned as
1071
+ a header. Sorting is a view reorder - it does not change the document;
1072
+ copy reflects what is on screen.
1073
+
1074
+ Formula cells sort by their computed value, and that value travels with
1075
+ its row: references keep meaning what the author wrote, so a sorted
1076
+ =SUM(...) row never recomputes against the shuffled view.
1077
+
1078
+ A trailing summary row - one whose formula aggregates a range of other
1079
+ rows, like a Total row with =SUM(D2:D5) - is pinned to the bottom and
1080
+ stays there through any sort, the same way a header row stays at the top.
1081
+ Per-row formulas like =B2*C2 sort with the data as normal.
1082
+
1083
+ RESIZING
1084
+ Drag a column header's right edge to set an explicit width; it persists
1085
+ through sorts. Otherwise columns size to their content.
1086
+
1087
+ SELECTING
1088
+ Click a cell to select it; its column letter and row number light up.
1089
+ Click a column letter / row number to select the whole column / row.
1090
+ Drag to select a range; Shift+Click or Shift+Arrow extends it. Arrow
1091
+ keys move the selection; Cmd/Ctrl+Arrow jumps to the far edge;
1092
+ Shift+(Cmd/Ctrl+)Arrow extends to it. Dragging to the left/right edge of
1093
+ a wide sheet auto-scrolls. Esc - or clicking anywhere outside the sheet -
1094
+ clears the selection.
1095
+
1096
+ Selecting a multi-cell range opens a stats strip below the grid with the
1097
+ range's Sum / Avg / Min / Max / Count. Formula cells count by their
1098
+ computed value. It collapses when the selection is cleared or back to a
1099
+ single cell.
1100
+
1101
+ TOOLBAR
1102
+ A white bar sits above each sheet:
1103
+ - Left: the selection address (e.g. B3 or B2:C3) and, for a sheet
1104
+ loaded from a file, the source filename - "B3 · report.csv".
1105
+ - Right: a copy icon that copies the WHOLE sheet as CSV (values only,
1106
+ no row / column labels).
1107
+ - When something is selected, a second button copies just the
1108
+ selection ("cell" for one cell, "selection" for a range).
1109
+ - A fullscreen expand button opens the sheet as a full-window,
1110
+ editable spreadsheet (see FULLSCREEN & EDITING).
1111
+
1112
+ Copy buttons copy what the sheet shows: formula cells emit their
1113
+ computed value, not the "=..." text. In fullscreen, a sheet with
1114
+ formulas swaps the copy icon for a labelled "values" button and adds a
1115
+ "formulas" button beside it that copies the raw data - formulas as
1116
+ written, plain values elsewhere.
1117
+
1118
+ LOADING FROM A CSV FILE
1119
+ Reference a file instead of typing data inline:
1120
+
1121
+ \`\`\`cells
1122
+ {{path/to/report.csv}}
1123
+ \`\`\`
1124
+
1125
+ sdoc reads that file (resolved relative to the markdown document), reads
1126
+ the whole thing, and bakes the CSV straight into the document. The result
1127
+ is self-contained: a shared link shows the data, never a "file not
1128
+ found". Only the filename is recorded as the source (shown in the bar),
1129
+ not the full path.
1130
+
1131
+ - One reference per cells block (the block's entire body is the
1132
+ reference).
1133
+ - \`sdoc report.csv\` opens a CSV file directly as a sheet.
1134
+
1135
+ CSV FILES WITH FORMULAS
1136
+ A .csv file can hold =formulas in its cells, exactly like an inline
1137
+ block:
1138
+
1139
+ Item,Qty,Total
1140
+ Laptop,12,=B2*100
1141
+ Total,=SUM(B2:B2),=SUM(C2:C2)
1142
+
1143
+ \`sdoc that-file.csv\` opens it as a working sheet - the formulas
1144
+ compute - and the Excel download carries them as live formulas. This
1145
+ is the lowest-friction way for an agent to hand over a spreadsheet:
1146
+ write a plain CSV with formulas where computed values belong, run
1147
+ sdoc on it. Quote any formula containing commas ("=ROUND(B2, 1)") -
1148
+ standard CSV quoting. The file itself opens read-only; it stays the
1149
+ clean source of truth.
1150
+
1151
+ EXCEL EXPORT SECURITY
1152
+ Only formulas this sheet itself can compute (the FORMULAS list above)
1153
+ export as live Excel formulas. Anything else - Excel functions we do
1154
+ not support, and especially the CSV-injection attack class
1155
+ (WEBSERVICE, HYPERLINK, DDE) - exports as inert text. A document
1156
+ someone shares with you can never smuggle an executable formula into
1157
+ your downloaded workbook.
1158
+
1159
+ LIMITS
1160
+ - Per-block source-size cap and a per-document block cap.
1161
+ - The inline preview bounds very large grids and notes what it clipped
1162
+ ("Showing 200 × 50 of 231 × 60 cells"); the full data still travels and
1163
+ copies.
1164
+
1165
+ FULLSCREEN & EDITING
1166
+ The expand button opens the sheet full-window: frozen headers, both-axis
1167
+ scroll, and a header bar with the selection address, its Sum / Avg /
1168
+ Min / Max / Count, and the formula bar. Formula cells count toward those
1169
+ stats by their computed value. The fullscreen sheet is editable (the
1170
+ inline one stays read-only):
1171
+ - Double-click a cell, or just start typing, to edit it.
1172
+ - Enter / Tab (and Shift+ to go back) commit and move on.
1173
+ - The formula bar edits the active cell; type a value or =formula, Enter.
1174
+ - Delete / Backspace clears the selected cell(s).
1175
+ - Cmd/Ctrl+Z undoes, Shift+Cmd/Ctrl+Z (or Ctrl+Y) redoes.
1176
+ - Paste TSV or CSV to drop a block of values in at the selection.
1177
+ - Point mode: while typing a formula, arrow keys point at cells and
1178
+ write their reference into the formula. Type =SUM( then arrow to a
1179
+ cell, hold Shift+Arrow to grow it into a range, type ) and Enter.
1180
+ Or arrow to the range start, type :, arrow to the end. Works in the
1181
+ cell editor and the formula bar; the pointed cells highlight violet.
1182
+ - Fill handle: drag the small square on the selection's corner to fill
1183
+ neighbouring cells. Formulas shift their references (=B2*C2 becomes
1184
+ =B3*C3 one row down), values repeat, and a run of numbers (1, 2)
1185
+ continues as a series (3, 4...).
1186
+ - Copy / paste formulas: Cmd/Ctrl+C copies the selection (formulas
1187
+ included); pasting back in adjusts each formula's references for
1188
+ where it lands. Copy one formula, select a range, paste - every cell
1189
+ gets the formula relative to its own row/column.
1190
+ - Formula view: when a sheet has formulas, an =fx button in the top bar
1191
+ switches every formula cell to show its source ("=B2*C2") instead of
1192
+ its value, editable in place. Click again for values.
1193
+ Editing is client-side and ephemeral: changes show in the inline grid when
1194
+ you close, but nothing is written back to the document or to any file.
1195
+
1196
+ After a fullscreen edit, the inline toolbar shows a "showing edited" pill:
1197
+ the grid displays your edits, the document is unchanged. Click the pill to
1198
+ flip to the document's original data ("showing original") and back.
1199
+ Expanding again always resumes from the edits.
1200
+
1201
+ EXPORT
1202
+ HTML / Word / PDF export emit a real table of the values (the source
1203
+ label and the row / column chrome are dropped).
1204
+
1205
+ The download button in a sheet's toolbar (inline and fullscreen) exports
1206
+ that sheet as an Excel workbook (.xlsx). Formulas export as live Excel
1207
+ formulas and recalculate when the file opens; the format: directive's
1208
+ currency / percent / comma columns carry over as Excel number formats.
1209
+ The export uses the document's row order (plus any fullscreen edits),
1210
+ never the sorted view, so formula references stay correct.
1211
+ `;
1212
+
1213
+ const SLIDES_HELP = `
1214
+ SmallDocs — Slides
1215
+ ==============
1216
+ Embed presentation slides in any markdown document using fenced
1217
+ slide blocks. Slides render as thumbnails inline; click the small
1218
+ present-mode icon in the slide's top-right corner to enter fullscreen.
1219
+ Text inside a slide thumbnail is selectable. Esc to exit, arrows to
1220
+ navigate.
1221
+
1222
+ \u2500\u2500 COMMANDS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1223
+ sdoc present <file> Open file directly in fullscreen slide view
1224
+ sdoc <file> Open normally (click a slide's present icon)
1225
+ sdoc slides This help
1226
+ sdoc slides list List built-in templates + slot names
1227
+ sdoc slides icons [query] List the Lucide icons available to the
1228
+ \`icon\` shape kind. Optional substring filter.
1229
+ sdoc slides custom-shapes Long-tail notes for raw-shape custom slides
1230
+ (polygon text, composite patterns, layering).
1231
+ Most decks use \`@extends\` and never need this.
1232
+
1233
+ ── DESIGN GUIDELINES ────────────────────────────────────
1234
+ The built-in templates encode a few rules that separate professional
1235
+ slides from amateur ones. When you compose slides by hand (no
1236
+ template), keep these in mind - they're the difference between a
1237
+ deck that lands and one that doesn't.
1238
+
1239
+ Margins. Keep all content inside a 1-unit safe area on every side
1240
+ of a 16x9 grid (so x ∈ [1, 15], y ∈ [0.5, 8.5]). Nothing touches
1241
+ the slide edge except a deliberate full-bleed background (\`section\`
1242
+ uses this; nothing else should).
1243
+
1244
+ No auto-flow. Every shape claims its full (x, y, w, h) rect.
1245
+ Source order is paint order, so a later filled rectangle that
1246
+ overlaps an earlier text shape's bbox covers its content. Place
1247
+ each shape at the previous shape's bottom edge (y + h) or later.
1248
+ Common slip: putting content at y=12 right under
1249
+ \`r 6 9 88 5 text=subtitle ...\` - the subtitle reserves y=9..14;
1250
+ starting the body at y=12 puts the fill on top of the subtitle's
1251
+ bottom half. For \`text=title\` and \`text=subtitle\` add another
1252
+ ~0.5 grid unit on top to clear glyph descenders; see
1253
+ \`slides custom-shapes\` for the fine print.
1254
+
1255
+ No fill colours behind body or title text. The slide background IS
1256
+ your canvas. Saturated rectangles compete with content and read as
1257
+ "PowerPoint 2003". The only exception: section dividers, which want
1258
+ contrast against content slides - use \`grid bg=\` there, not a
1259
+ shape \`fill=\`.
1260
+
1261
+ Two or three sizes per slide, max. Stick to the role table:
1262
+ text=title (64px) for cover, quote, section, metric
1263
+ text=subtitle (40px) for in-deck content-slide titles
1264
+ text=body (24px) default; bullets, paragraphs
1265
+ text=caption (14px) ONLY for footers, eyebrows, attributions
1266
+ Caption renders as ~3px in a 240px-wide thumbnail - never put
1267
+ load-bearing content in caption role.
1268
+
1269
+ Default \`valign=center\`. \`valign=top\` reads right only when the
1270
+ body shape is sized to its content. On an oversized body shape
1271
+ top-anchoring leaves dead space underneath; centering balances it.
1272
+
1273
+ Content fills 55-65% of the safe area, no more. Empty space is a
1274
+ feature: it's what makes a deck feel confident rather than crowded.
1275
+ If a shape is mostly empty, shrink the shape - don't fill it.
1276
+
1277
+ Action titles versus topic titles. An action title states the
1278
+ claim ("Method X reduced error 40%") and reads like prose. A topic
1279
+ title labels what's below ("Methodology"). Both are fine; pick
1280
+ one. Action titles tend to wrap to two lines, which is why
1281
+ in-deck titles use subtitle role (40px) not title role (64px).
1282
+
1283
+ Body bullets should be parallel. If your bullets don't read like
1284
+ a list - if the items have different shapes, weights, or
1285
+ connective tissue (because, but, so) - write a sentence instead.
1286
+ A bulleted paragraph is hiding the fact that you haven't decided
1287
+ what you're claiming.
1288
+
1289
+ When in doubt, \`@extends\` a built-in template instead of
1290
+ composing from raw shapes. Run \`sdoc slides list\` to see the
1291
+ registry.
1292
+
1293
+ \u2500\u2500 FENCE SYNTAX \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1294
+ Wrap shape DSL in a ~~~slide fenced block:
1295
+
1296
+ ~~~slide
1297
+ grid 100 56.25
1298
+ r 5 5 90 15 fill=#1e40af color=#fff text=title | Q4 Review
1299
+ r 5 22 42 26 align=left |
1300
+ ## Wins
1301
+ - Shipped slides
1302
+ - Tilde fences
1303
+ ~~~
1304
+
1305
+ Tildes (not backticks) so shape content can include \`\`\` code blocks
1306
+ without closing the fence early. Triple-backtick \`\`\`slide also works
1307
+ (marked accepts either) - but prefer tildes so a nested \`\`\`python
1308
+ inside a shape doesn't end the slide block prematurely.
1309
+
1310
+ \u2500\u2500 GRID \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1311
+ First line: \`grid W H [key=val ...]\`. Default 100 56.25 (16:9).
1312
+ All shape coordinates are in grid units.
1313
+ grid 100 56.25
1314
+ grid 100 75 (4:3)
1315
+ grid 100 100 (square)
1316
+ grid 100 56.25 bg=#0f172a (set slide background color)
1317
+
1318
+ W x H defines the aspect ratio and coordinate system, not a pixel size.
1319
+ Slides fill whatever space they're rendered into - inline thumbnail in
1320
+ a doc, small rail thumbnail in present mode, fullscreen stage, PDF page -
1321
+ and text auto-fits via container queries. Pick numbers for the aspect
1322
+ ratio you want; 100 on one axis is the convention, making the other
1323
+ axis a simple percentage.
1324
+
1325
+ \u2500\u2500 RAW SHAPES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1326
+ Templates encode their layouts in shape DSL (\`r\` / \`p\` / \`c\` /
1327
+ \`e\` / \`l\` / \`a\` / \`chev\` / \`bub\` / \`cyl\`). As a consumer of a
1328
+ template, you don't see these - you fill slots via \`#name: value\`
1329
+ (see TEMPLATES below). If you're defining your own \`@template\`,
1330
+ or composing a custom slide from raw shapes, run:
1331
+
1332
+ sdoc slides custom-shapes Shape kinds, ids / @refs, layering,
1333
+ polygon gotchas, composite patterns
1334
+
1335
+ Three high-level shapes (\`chev\`, \`bub\`, \`cyl\`) ship with their
1336
+ text-centring rect already tuned to the visual body, not the
1337
+ bounding box. Reach for them before composing the equivalent
1338
+ polygon by hand - they save lines AND get the text positioning
1339
+ right by default.
1340
+
1341
+ \u2500\u2500 ICONS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1342
+ The \`icon\` shape kind renders an inline outline icon from the
1343
+ bundled Lucide library (https://lucide.dev/icons/, ~1960 icons).
1344
+ Use it to mark concepts on a slide - a user, a database, a cloud,
1345
+ a lock - without resorting to emoji or raster images.
1346
+
1347
+ icon 8 30 6 6 name=user color=#0F1E3A
1348
+ icon 20 30 6 6 name=database
1349
+ icon 32 30 6 6 name=cloud-upload strokeWidth=2
1350
+
1351
+ Unknown names render a pink struck-through placeholder, so typos
1352
+ are visible rather than silent. To find an icon name, run:
1353
+
1354
+ sdoc slides icons List every available name
1355
+ sdoc slides icons cloud Filter by substring
1356
+
1357
+ Full attribute reference (color, strokeWidth, lazy-load behaviour)
1358
+ lives in \`sdoc slides custom-shapes\` under the \`icon\` shape entry.
1359
+
1360
+ \u2500\u2500 SHAPE ATTRIBUTES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1361
+ Between geometry and \`|\`:
1362
+
1363
+ The text-related attributes below apply to BOTH the shapes a
1364
+ template defines and the slot content you write as a consumer.
1365
+ Visual attributes (fill, stroke, strokeWidth, radius, etc.) are
1366
+ documented in \`sdoc slides custom-shapes\`.
1367
+
1368
+ Padding:
1369
+ padding=N Inner padding in grid units (0 disables). On a
1370
+ 100-wide grid, \`padding=2\` is ~2% of slide width.
1371
+
1372
+ Alignment:
1373
+ align=<a> Horizontal: center (default), left, right
1374
+ valign=<v> Vertical: center (default), top, bottom
1375
+
1376
+ Text body box:
1377
+ textBox=x,y,w,h Override the rectangle that holds the shape's
1378
+ text content. Values are grid units, relative
1379
+ to the shape's bounding-box top-left. The shape
1380
+ itself (fill, stroke, geometry) is unchanged -
1381
+ only the text inside shifts. Use when the
1382
+ shape is asymmetric and centering text in its
1383
+ bounding box drifts the text off the visual
1384
+ mass.
1385
+
1386
+ The classic case: a right-pointing chevron
1387
+ polygon. Its bounding box includes the tip,
1388
+ so centred text sits to the right of the
1389
+ chevron body. Set textBox to the body region
1390
+ only:
1391
+
1392
+ p 6,20 19,20 22,24 19,28 6,28 9,24
1393
+ fill=#4f8aff textBox=0,0,14,8
1394
+ color=#fff text=subtitle | Tokenize
1395
+
1396
+ Bbox of the polygon is (6,20)-(22,28). The
1397
+ body (excluding the 3u tip) is 13 wide x 8
1398
+ tall starting at (6,20); textBox=0,0,14,8 puts
1399
+ text inside that region.
1400
+
1401
+ Text rotation:
1402
+ textAngle=N Rotate the text WITHIN the shape by N degrees.
1403
+ The shape's geometry (x, y, w, h) is untouched -
1404
+ only the text inside rotates. Useful for vertical
1405
+ axis labels, diagonal stamps, and skewed callouts.
1406
+ Default 0 (no rotation).
1407
+
1408
+ Cardinal angles (0, 90, -90, 180) "just work":
1409
+ - 90 / -90: text is laid out using the shape's
1410
+ SWAPPED dimensions, so "More autonomous" in a
1411
+ tall narrow shape wraps along the long axis
1412
+ and reads top-to-bottom (90) or
1413
+ bottom-to-top (-90).
1414
+ - 180: text is flipped in place.
1415
+
1416
+ Non-cardinal angles (e.g. textAngle=37) render
1417
+ but get NO dimension swap and NO autofit help.
1418
+ The text is rotated around the shape's centre;
1419
+ its visible extent will usually exceed the
1420
+ shape's (w, h) box. Size the container so the
1421
+ rotated text lands where you want:
1422
+ rotatedW = |w * cos N| + |h * sin N|
1423
+ rotatedH = |w * sin N| + |h * cos N|
1424
+ The shape's own bbox doesn't change - only the
1425
+ rendered text sticks out. Place neighbouring
1426
+ shapes accordingly, or accept the overlap as
1427
+ intentional.
1428
+
1429
+ Example (vertical y-axis label):
1430
+ r 8 16 4 32 textAngle=-90 text=caption
1431
+ align=center | More autonomous
1432
+
1433
+ Text sizing (role first, escape hatches second):
1434
+ text=<role> Pick a role from a fixed table. Roles give a
1435
+ deck consistent typography. Default is \`body\`.
1436
+ text=title 64px (slide titles)
1437
+ text=subtitle 40px (section heads, sub-titles)
1438
+ text=body 24px (default; paragraphs, bullets)
1439
+ text=caption 14px (footnotes, fine print)
1440
+ Unknown roles fall back to \`body\` silently.
1441
+
1442
+ size=Npx Literal size override; takes precedence over the
1443
+ role. Units: px | pt | em | rem (bare number = px).
1444
+ Use sparingly - the role table is what keeps the
1445
+ deck rhythm consistent.
1446
+ size=fit Opt into auto-fit: binary search for the largest
1447
+ font that fits the shape, capped at 12% of stage
1448
+ height (or the per-shape maxfont= value).
1449
+
1450
+ maxfont=Npx Caps \`size=fit\` higher or lower than the default
1451
+ stage cap. Has no effect when size= isn't \`fit\`.
1452
+
1453
+ Px values size as if the stage were 720px tall and scale proportionally
1454
+ in smaller views (rail thumbnails, inline thumbs), so \`size=18px\` reads
1455
+ as "18px on a fullscreen slide".
1456
+
1457
+ A deck that uses only roles (no \`size=\`) lands at 2-3 distinct font
1458
+ sizes across all slides, which is what makes presentations look
1459
+ professional. Reach for \`size=Npx\` only for hero numbers or other
1460
+ one-off treatments.
1461
+
1462
+ Per-element scale (applied inside the shape's shadow root):
1463
+ h1Scale=N h1 is N\u00d7 the shape's resolved font size.
1464
+ h2Scale=N h2 is N\u00d7 the shape's resolved font size.
1465
+ h3Scale=N, h4Scale=N, h5Scale=N, h6Scale=N
1466
+ pScale=N Scale paragraph text (default 1).
1467
+
1468
+ Each scale affects ONLY that element type. \`h1Scale=3\` enlarges
1469
+ h1 headings, leaves paragraphs alone. The shape's resolved font
1470
+ size (the role's px from \`text=\`, the \`size=Npx\` override, or the
1471
+ autofit output when \`size=fit\`) is the base for the multiplier.
1472
+
1473
+ Defaults without overrides: h1 1.4, h2 1.2, h3 1.05, h4-h6 1.0,
1474
+ p 1.0. Note that h4/h5/h6 render at the SAME size by default, so
1475
+ they don't give you three-step hierarchy out of the box - use
1476
+ explicit h4Scale/h5Scale/h6Scale if you need a h4>h5>h6 spread.
1477
+
1478
+ When to use: one shape holds mixed content (heading + body) and
1479
+ you want the heading BIGGER or the body SMALLER than the 1.4 / 1.0
1480
+ default. Two common patterns:
1481
+
1482
+ Hero number (giant + tiny caption, one shape):
1483
+ r 10 15 80 30 h1Scale=3 pScale=0.4 |
1484
+ # 87%
1485
+ of teams ship faster with one-page decks
1486
+
1487
+ Quote card (prominent body, small attribution):
1488
+ r 10 15 80 30 h2Scale=0.5 |
1489
+ Quote text in body size here.
1490
+ ## - Attribution
1491
+
1492
+ Invalid or \u2264 0 values are ignored (fallback to defaults).
1493
+
1494
+ Identification:
1495
+ #id Reference target for @refs
1496
+
1497
+ Stacking:
1498
+ layer=<v> top | mid | bottom. Default \`mid\` for every
1499
+ kind. Source order alone normally decides paint
1500
+ order; \`layer=\` is an escape hatch for shapes
1501
+ that must sit on top of (or below) everything
1502
+ regardless of where they were declared. See
1503
+ STACKING section below.
1504
+
1505
+ \u2500\u2500 CONTENT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1506
+ Everything after \`|\` is standard markdown. Multi-line uses
1507
+ indentation under the shape line - continuation lines MUST be
1508
+ indented at least 2 spaces, or the parser treats them as fresh
1509
+ top-level shape lines (and fails).
1510
+
1511
+ r 5 20 90 60 align=left |
1512
+ ## Heading
1513
+ Some body paragraph.
1514
+
1515
+ - list item one
1516
+ - list item two
1517
+
1518
+ \`\`\`python
1519
+ def hi():
1520
+ print("hello")
1521
+ \`\`\`
1522
+
1523
+ Prefer putting the \`|\` on its own line (empty) with all content
1524
+ indented below. Mixing "first line after |" with unindented lines
1525
+ is a common parser error.
1526
+
1527
+ \u2500\u2500 ALIGNMENT GUIDELINES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1528
+ Default: \`align=center valign=center\`. Good for:
1529
+ - Title shapes (one heading or phrase)
1530
+ - Subheaders, section labels
1531
+ - Standalone caption text
1532
+
1533
+ Switch to \`align=left\` when the shape holds body copy - paragraphs,
1534
+ bullet lists, numbered lists, blockquotes. Left-aligned reads better
1535
+ once you have multiple lines. Keep \`valign=center\` (the default) so
1536
+ the block floats in the middle of the shape; switch to \`valign=top\`
1537
+ only when the shape is sized exactly to the content and you want it
1538
+ anchored to the top.
1539
+
1540
+ Rule of thumb:
1541
+ ONE short phrase \u2192 leave centered
1542
+ MULTIPLE lines \u2192 align=left, keep valign=center
1543
+
1544
+ \u2500\u2500 PULLING FROM DOC STYLES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1545
+ Slides pick up the host document's styles so a deck feels visually
1546
+ part of its doc. Two mechanisms, both resolved at render time against
1547
+ the active theme (so dark mode just works).
1548
+
1549
+ AUTOMATIC INHERITANCE (no DSL needed)
1550
+ - Slide background = styles.background (unless grid has bg=)
1551
+ - Body font = styles.fontFamily
1552
+ - Heading fonts = styles.headers.fontFamily
1553
+ - Code / pre / link = their respective styles.* values
1554
+ - Heading text inside a shape's markdown content adopts the doc's
1555
+ h1/h2/h3/h4 color. Example: \`# Title\` in a shape uses styles.h1.color.
1556
+ A shape's own \`color=\` always overrides this.
1557
+
1558
+ EXPLICIT REFERENCES ($path.to.prop)
1559
+ Any shape or grid attribute value can be a \`\$path.to.prop\` token,
1560
+ which resolves to the doc's live value for that style. Common cases:
1561
+ \`fill=\$h1.color\`, \`color=\$chart.accent\`, grid \`bg=\$background\`.
1562
+ Vocabulary = the YAML schema (run \`sdoc schema\`).
1563
+
1564
+ r 5 5 90 15 fill=\$h1.color color=#fff text=title | Title
1565
+ r 5 25 90 25 color=\$chart.accent | ## 40% growth
1566
+ grid 100 56.25 bg=\$blocks.background (subtle block-tinted slide)
1567
+
1568
+ Supported paths:
1569
+ \$background \$color \$fontFamily
1570
+ \$h.color \$h1.color \$h2.color \$h3.color \$h4.color
1571
+ \$headers.color \$headers.fontFamily
1572
+ \$p.color \$list.color \$link.color
1573
+ \$blocks.background \$blocks.color
1574
+ \$code.background \$code.color \$code.font
1575
+ \$blockquote.background \$blockquote.color \$blockquote.borderColor
1576
+ \$chart.accent \$chart.background \$chart.textColor
1577
+
1578
+ Unknown paths surface in the error badge with the rest of the
1579
+ diagnostics. Literal hex (#1e40af) still works when you want a
1580
+ one-off color that isn't in the doc's styles.
1581
+
1582
+ Blockquote-style card (tinted bg + left accent border): use a
1583
+ markdown \`>\` inside any shape. The quote picks up the doc's
1584
+ blockquote styling automatically - no \`stroke=\` workaround needed.
1585
+ r 50 15 42 30 padding=3 |
1586
+ > Our customers are the product
1587
+ >
1588
+ > - Jordan, CEO
1589
+
1590
+ \u2500\u2500 IMAGES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1591
+ Any shape (\`r\`, \`c\`, \`p\`) can hold a bitmap via \`image=<url>\`.
1592
+ The URL accepts whatever the browser would load for a standard
1593
+ markdown \`![]()\`:
1594
+
1595
+ - \`data:\` URIs inlined in the DSL, no network fetch
1596
+ - \`https://\` URLs fetched at render time (CORS applies)
1597
+
1598
+ SmallDocs does not host image bytes. Data URIs live in your document
1599
+ (and share URL); external URLs fetch from whatever host you pick.
1600
+
1601
+ Attributes (apply to every shape kind):
1602
+
1603
+ image=<url> the bitmap source
1604
+ imageFit=cover|contain cover (default) fills the shape box and
1605
+ crops overflow; contain preserves aspect
1606
+ and letterboxes inside the shape
1607
+ imagePos=center|top|bottom|left|right
1608
+ which edge is pinned when cover crops or
1609
+ contain letterboxes; default center
1610
+
1611
+ Stacking inside a shape (bottom to top):
1612
+ 1. \`fill=\` colour backdrop
1613
+ 2. \`image=\` bitmap (shows fill through image alpha / on load fail)
1614
+ 3. \`stroke=\` border
1615
+ 4. \`| content\` markdown text
1616
+
1617
+ Examples:
1618
+
1619
+ # Small corner logo on a rect
1620
+ r 14 0.5 1.5 1 image=https://lucide.dev/logo.light.svg imageFit=contain
1621
+
1622
+ # Full-bleed hero with a title overlay (one shape, not two)
1623
+ r 0 0 16 9 image=data:image/png;base64,iVBORw0K... align=left valign=bottom padding=0.5 |
1624
+ # Q4 review
1625
+
1626
+ # Photo clipped inside a hand-drawn polygon
1627
+ p 3,1 13,1 14,5 10,8 6,8 2,5 image=https://example.com/team.jpg
1628
+
1629
+ # Circle avatar with a gold ring (stroke paints above image)
1630
+ c 8 4.5 2.2 image=/avatar.jpg stroke=#d4af37 strokeWidth=0.12
1631
+
1632
+ Shorthand: \`i x y w h src=<url>\` is parser sugar for
1633
+ \`r x y w h image=<url>\`, a friendly keystroke for the common case.
1634
+ \`src=\` is treated as a valid alias for \`image=\` everywhere.
1635
+
1636
+ PDF export embeds PNG and JPEG natively. SVG / WebP / GIF are
1637
+ skipped silently (console warning is logged). External URL fetches
1638
+ need CORS headers on the host, same constraint as any browser fetch.
1639
+
1640
+ \u2500\u2500 STACKING \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1641
+ Source order = paint order. A shape declared later in the slide
1642
+ paints above shapes declared earlier - regardless of whether it is
1643
+ a rectangle, polygon, circle, line, or arrow. To put a connector
1644
+ above its cards, declare the arrow after the cards. To put a
1645
+ backdrop behind a card, declare the backdrop first.
1646
+
1647
+ Escape hatch: \`layer=top | mid | bottom\` (default \`mid\`) promotes
1648
+ or demotes a shape across coarse sublayers, overriding source order.
1649
+ Useful inside templates where a consumer slide adds more shapes
1650
+ whose declaration order you cannot predict. See the LAYERING
1651
+ section of \`sdoc slides custom-shapes\` for the full model.
1652
+
1653
+ \u2500\u2500 TEMPLATES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1654
+ Define a shape layout once, reuse across slides. Two directives,
1655
+ each must be the first non-blank line inside a slide block:
1656
+
1657
+ @template NAME Register this slide's DSL as a template.
1658
+ The slide DOES NOT RENDER - it's a definition.
1659
+ @extends NAME Inherit NAME's shapes; fill slot content below.
1660
+
1661
+ Shapes in the template carry an \`#id\` to mark slots. Consumers
1662
+ override those slots with \`#id: value\` blocks (inline for a single
1663
+ line, colon-only + following lines for multi-line content).
1664
+
1665
+ Author ordering doesn't matter - the consumer can appear before or
1666
+ after the template in the document. Templates never render, so they
1667
+ don't show up in the thumbnail flow or present mode.
1668
+
1669
+ Example:
1670
+
1671
+ ~~~slide
1672
+ @template title-body
1673
+ grid 16 9
1674
+ r 0 0 16 3 #title fill=\$h1.color color=#fff text=title | placeholder title
1675
+ r 0 3 16 6 #body align=left |
1676
+ placeholder body
1677
+ ~~~
1678
+
1679
+ ~~~slide
1680
+ @extends title-body
1681
+ #title: What is SmallDocs?
1682
+ #body:
1683
+ - Markdown in, styled docs out
1684
+ - No server, hash-only state
1685
+ - Slides from fenced blocks
1686
+ ~~~
1687
+
1688
+ ~~~slide
1689
+ @extends title-body
1690
+ #title: Why templates
1691
+ #body: Define shape once, fill slots N times. Recolor once in front
1692
+ matter and every slide that uses the template picks it up.
1693
+ ~~~
1694
+
1695
+ Partial fills: if a consumer omits an optional slot (provides
1696
+ \`#title\` but not \`#body\`), the template's placeholder content
1697
+ stays - so templates are self-documenting when first authored.
1698
+
1699
+ Required slots: mark a slot with a trailing \`!\` (e.g. \`#title!\`)
1700
+ in the template. If a consumer omits a required slot, the resolver
1701
+ surfaces an error in the slide's error badge. Optional slots have
1702
+ no marker.
1703
+
1704
+ Unknown template names, slots that don't match any shape id in the
1705
+ template, and missing required slots all surface in the error badge
1706
+ alongside any DSL errors.
1707
+
1708
+ Deliberately simple in v1: no attribute overrides (can't change
1709
+ \`fill=\` per consumer - fork the template if you need variants),
1710
+ no nested templates (a consumer can't extend another consumer).
1711
+
1712
+ ── BUILT-IN TEMPLATES ───────────────────────────────────
1713
+ SmallDocs ships a small library of opinionated templates. \`@extends\`
1714
+ any of them without declaring a user \`@template\` first. Run
1715
+ \`sdoc slides list\` for the names + slot lists; the templates all
1716
+ use a 1-unit safe margin and avoid full-bleed coloured bars (the
1717
+ one exception is \`section\`, which uses \`grid bg=\` for contrast
1718
+ between deck sections).
1719
+
1720
+ cover
1721
+ Opening slide of a deck. Once per deck. Sets the tone before
1722
+ anything else.
1723
+
1724
+ title-body
1725
+ The workhorse for content slides - 60-70% of slides in any
1726
+ real deck. Title at top in subtitle role (40px) so an action
1727
+ title can wrap to two lines without crowding the body.
1728
+ Optional footer slot for source, page number, or context.
1729
+
1730
+ two-column
1731
+ Compare / contrast (before/after, A vs B, problem / solution).
1732
+ A 1-unit gutter splits the columns; optional column headers
1733
+ above each. Bodies anchor top so matched-length content reads
1734
+ as parallel - keep both columns roughly the same length, or
1735
+ switch to title-body and explain the asymmetry in prose.
1736
+
1737
+ three-column
1738
+ Three-way compare. A/B/C variants, before/during/after, three
1739
+ perspectives on the same question. Equal columns separated by
1740
+ a small gutter; optional headers above each. Bias toward
1741
+ keeping all three columns roughly the same length - if one is
1742
+ half-empty, drop it and use two-column instead.
1743
+
1744
+ exhibit
1745
+ Chart on the left (~64% of safe area), takeaway column on the
1746
+ right (~32%), optional source caption underneath, required
1747
+ action title at the top. The chart is the evidence; the
1748
+ takeaway tells the audience what to see. Reserve for business
1749
+ decks where the audience needs a verbal handle on the chart
1750
+ under time pressure. Sibling templates: \`image-and-text\`
1751
+ when image and body should read as balanced peers (54/46,
1752
+ title optional and small); \`figure-hero\` when the chart
1753
+ should fill the slide with no right column at all.
1754
+
1755
+ image-and-text
1756
+ Image on the left (~54% of safe area), supporting body on
1757
+ the right (~43%), balanced. Optional small caption-style
1758
+ title at the top. Use when the image and the body are about
1759
+ equal in weight - "here's a thing + here's what it is".
1760
+ Sibling templates: \`exhibit\` when the chart should
1761
+ dominate and the right column is a narrow set of takeaway
1762
+ bullets; \`figure-hero\` when the image IS the argument
1763
+ (no body column). The image slot accepts markdown image
1764
+ syntax: \`#image: ![alt](url)\`.
1765
+
1766
+ figure-hero
1767
+ Image-dominant slide. The figure carries the whole slide
1768
+ and a small caption sits below; no body or takeaway column.
1769
+ The workhorse for research talks and any deck where a chart,
1770
+ screenshot, or photograph is the point. Sibling templates:
1771
+ \`image-and-text\` when you also want a body column;
1772
+ \`exhibit\` when you want a chart plus a narrow takeaway
1773
+ column for a business audience.
1774
+
1775
+ quote
1776
+ Single big idea, customer voice, or a manifesto sentence.
1777
+ Centered both axes so short content sits balanced rather
1778
+ than drifting top-left. Use for sentences, not numbers -
1779
+ reach for \`metric\` when the slide IS a number.
1780
+
1781
+ metric
1782
+ One hero number plus a line of context. \`size=fit\` lets the
1783
+ number scale to its shape; \`maxfont=300px\` is baked in so it
1784
+ can actually feel hero-sized. One short sentence in the
1785
+ \`context\` slot is the rule, not three - more text fights
1786
+ the number for attention and the slide stops feeling like a
1787
+ headline. Use sparingly: one metric slide per deck is the
1788
+ pattern, not three.
1789
+
1790
+ section
1791
+ Section divider between deck parts. The one template that
1792
+ uses a full-bleed background (via \`grid bg=\`) - the contrast
1793
+ against content slides is what signals "we're switching
1794
+ gears" to the audience. Shape fills are still avoided
1795
+ (Consultant-2 rule); only the slide bg is coloured.
1796
+
1797
+ closing
1798
+ Quiet bookend at the end of a deck. Center-aligned, minimal.
1799
+ Don't write "Thanks for listening" or "Questions?" here -
1800
+ both signal "I've run out of content" and the room tunes
1801
+ out. Pick something the audience will remember instead
1802
+ ("Start this week, not next", "Boring is the goal", the
1803
+ one number that summarises the deck, etc.). The contact
1804
+ slot is for one short line of channels, not a paragraph.
1805
+
1806
+ A user \`@template <name>\` with the same name as a built-in overrides
1807
+ it for the rest of the document, with a warning surfaced on the
1808
+ template's slide. So shipping a custom \`title-body\` is fine - the
1809
+ resolver just lets you know the stdlib version got shadowed.
1810
+
1811
+ \u2500\u2500 ERRORS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1812
+ A slide with parse/render errors shows a red badge at the bottom
1813
+ of the thumbnail listing every problem by line number. The badge
1814
+ has a "Copy" button that puts a diagnostic on the clipboard —
1815
+ errors + the full slide source — for pasting back to an agent.
1816
+
1817
+ Common errors:
1818
+ - "shape extends outside grid WxH" y+h > H (or x+w > W)
1819
+ - \`unknown id "@name"\` @-ref before the shape is declared
1820
+ - \`duplicate id "#name"\` two shapes share an id
1821
+ - \`invalid attribute key\` key must start with a letter
1822
+
1823
+ \u2500\u2500 LIMITATIONS TODAY \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1824
+ - Templates: no per-consumer attribute overrides; no nested templates.
1825
+ - Arrows draw as straight lines; no routing around other shapes.
1826
+ - No drag/resize edit mode yet; shapes are authored by typing DSL.
1827
+ `;
1828
+
1829
+ const SLIDES_CUSTOM_SHAPES_HELP = `
1830
+ SmallDocs — Slides (raw shapes)
1831
+ ===========================
1832
+ Reference + design notes for slides built from raw shapes rather than
1833
+ the stdlib templates. Most decks won't need any of this - if you can
1834
+ express the slide via \`@extends\` on a built-in (cover, title-body,
1835
+ two-column, exhibit, etc.), do that.
1836
+
1837
+ Read DESIGN PRINCIPLES first. The syntax reference below assumes you've
1838
+ made the visual choices the principles describe. Without them, raw
1839
+ shapes consistently produce decks that read as "default PowerPoint"
1840
+ rather than as designed.
1841
+
1842
+ ── DESIGN PRINCIPLES ─────────────────────────────────
1843
+ Raw shapes give geometric freedom that templates don't. That
1844
+ freedom is also rope. The notes below are what separate a deck
1845
+ that reads as designed from one that reads as amateur.
1846
+ Internalise them before reaching for the syntax.
1847
+
1848
+ Stroke.
1849
+ Default: NO stroke. Reads modern and confident. The slide
1850
+ background carries the silhouette via fill or whitespace; shapes
1851
+ don't compete with their own outlines.
1852
+
1853
+ Thin neutral stroke (\`strokeWidth\` <= 0.03, \`stroke=#94a3b8\` or
1854
+ a similar cool grey). Reads technical, detail-oriented. Reach
1855
+ for this when several shapes' contours need to be visible AND
1856
+ the fill contrast isn't doing the work on its own. Good for
1857
+ matrices, small multiples, schematic diagrams.
1858
+
1859
+ Thick coloured stroke. AVOID. The strongest tell that a deck
1860
+ wasn't designed. It almost never improves the slide. If a shape
1861
+ needs a thick coloured border to read, the geometry or fill is
1862
+ doing too little work. The only exception is a deliberately
1863
+ drawn arrow or callout where the line itself IS the message;
1864
+ even then, take the colour from the doc accent, not from a
1865
+ third hue.
1866
+
1867
+ Fill.
1868
+ Default: NO fill. Most shapes don't need one - the slide
1869
+ background reads through and the silhouette is implicit.
1870
+
1871
+ Subtle tint (\`#eef2ff\`, \`#f8fafc\`, or similar near-background
1872
+ values). For containers that hold body content - a card around
1873
+ a paragraph, a column header strip. The tint should look like a
1874
+ faint shadow, not a coloured panel.
1875
+
1876
+ Saturated fill. Reserved for the ONE focal element per slide -
1877
+ the navy cell on a segmentation matrix, the SOM tier on a
1878
+ market-sizing diagram, the focal band on a funnel. Saturated
1879
+ fill is your single "look here" gesture. Spend it once.
1880
+
1881
+ Typography-only is not a custom shape.
1882
+ A shape with neither stroke nor fill is invisible - it reads as
1883
+ floating text. If you're not encoding geometry (a position, a
1884
+ size, a relationship, a comparison), you're writing an
1885
+ annotation, and an annotation belongs in a plain text shape or
1886
+ in markdown content. The point of a custom shape is the shape;
1887
+ if it isn't visible, drop it and use a template slot.
1888
+
1889
+ One deviation per slide.
1890
+ Repetition + deviation = recognition. If five shapes share a
1891
+ treatment and the sixth doesn't, the eye lands on the sixth
1892
+ before reading a single label. The deviation IS the slide.
1893
+ Wanting two deviations is usually wanting two slides.
1894
+
1895
+ Shape vocabulary.
1896
+ Limit to two shape primitives per deck - typically a rectangle
1897
+ for cards / containers, plus one polygon (or circle / ellipse)
1898
+ as the variant. Using one primitive consistently across slides
1899
+ builds a visual language the audience learns by slide three.
1900
+ Six different shapes used once each flat-lines that recognition
1901
+ and reads as ornament rather than system.
1902
+
1903
+ Geometry as data.
1904
+ Where a shape's size, position, slope, or area corresponds to a
1905
+ number in the content, make it accurate. SOM at 4.7% of TAM
1906
+ should occupy 4.7% of TAM's area, not 30% because that's what
1907
+ fits the layout. A funnel band's width should be proportional
1908
+ to its population, not chosen for visual balance. Where shapes
1909
+ carry data, geometry IS the argument; labels confirm it.
1910
+
1911
+ When the magnitudes span more than ~50x and a linear scale
1912
+ collapses the tail to a hairline, two honest positions:
1913
+ a) Linear scale. Let the tail be a hairline. The
1914
+ disappearance IS the data (250k next to 28M looks like
1915
+ what 250k actually is next to 28M).
1916
+ b) Square-root or log scale. Readable across the range; state
1917
+ which you used in a caption so the geometry isn't lying.
1918
+ Either is fine. Pick deliberately. Don't fudge a linear scale
1919
+ into "what looks good" - that is lying with shapes.
1920
+
1921
+ Labels outside the shape when the shape is too narrow.
1922
+ A magnitude-proportional shape will sometimes be smaller than
1923
+ its label. Pull the label outside (column-aligned, or with a
1924
+ short leader line) - shrinking the label to fit a hairline
1925
+ shape destroys the only data the shape was carrying. See the
1926
+ TEXT INSIDE NON-RECT SHAPES section for the mechanics.
1927
+
1928
+ Visual rhymes across slides.
1929
+ A shared element that recurs on every custom-shape slide - a
1930
+ horizontal rule at a consistent y, a footer caption pinned to
1931
+ the same line, an accent colour reserved for one role - is
1932
+ what makes a custom-shape deck feel deliberate rather than
1933
+ improvised. Pick one or two such rhymes and hold them across
1934
+ every slide you author.
1935
+
1936
+ Restraint over ornament.
1937
+ The decision to ADD any visual element should require a reason.
1938
+ No fill, no stroke, no extra shape, no second colour is the
1939
+ default. Spend visual weight only on the one or two things the
1940
+ slide is about. Empty space is half the design.
1941
+
1942
+ ── SHAPE KINDS ───────────────────────────────────────
1943
+ r x y w h rectangle (x,y = top-left; w,h = size)
1944
+ i x y w h image rect (sugar for \`r\` with \`image=\`; see IMAGES)
1945
+ c cx cy radius circle (cx,cy = center)
1946
+ e cx cy rx ry ellipse (cx,cy = center; rx,ry = half-sizes)
1947
+ l x1 y1 x2 y2 line (decorative, no content)
1948
+ a x1 y1 x2 y2 arrow (decorative, tip lands on (x2,y2);
1949
+ accepts \`^h\` between endpoints to bow)
1950
+ p x1,y1 x2,y2 ... polygon (segment operators between points:
1951
+ ~ ^h >P * P1 P2; see below)
1952
+ chev x y w h chevron / arrow-block (x,y = top-left; w,h = bbox
1953
+ including the pointed right tip).
1954
+ Text centres on the BODY (excluding
1955
+ tip), not the bbox.
1956
+ Attrs: tip=N (tip width, default
1957
+ min(h/2, w*0.25)); notch=N (V-shaped
1958
+ left edge for interlocking chevrons,
1959
+ default 0).
1960
+ bub x y w h speech bubble / callout. Rounded rect body at
1961
+ (x,y,w,h); the tail points from the
1962
+ nearest edge to \`tail=tx,ty\` (target
1963
+ point in grid units, REQUIRED).
1964
+ Text centres in the body; the tail
1965
+ does not displace text.
1966
+ Attrs:
1967
+ tail=tx,ty (required)
1968
+ radius=N (corner radius, default 1)
1969
+ tailStyle=concave (default) | sharp | wide
1970
+ concave: sides curve toward tail
1971
+ axis (elegant horn /
1972
+ teardrop, reads as
1973
+ designed)
1974
+ sharp: straight narrow triangle
1975
+ (schematic, "needle")
1976
+ wide: straight broader triangle
1977
+ (friendly, generic
1978
+ callout)
1979
+ cyl x y w h cylinder (database / storage shape). Top and
1980
+ bottom ellipse caps each take \`lip\`
1981
+ height (default ~20% of h, capped
1982
+ by w). Text centres in the body
1983
+ between the caps.
1984
+ Attrs: lip=N (cap height).
1985
+ tab x y w h folder / step-marker tab. Body rectangle with
1986
+ a smaller rectangular tab on top-left
1987
+ joined by a slope. Text centres in
1988
+ the body (below the tab).
1989
+ Attrs: tabW=N (tab width, default
1990
+ ~38% of w); tabH=N (tab height,
1991
+ default min(0.22*h, 0.18*w)).
1992
+ doc x y w h document / page with a folded top-right corner.
1993
+ Text centres in the body; the fold
1994
+ triangle is rendered as an 18% black
1995
+ overlay so the corner reads as a 3D
1996
+ fold over any fill colour.
1997
+ Attrs: fold=N (fold size, default
1998
+ 15% of min(w, h)).
1999
+ cloud x y w h cloud shape (abstract source / sink for arch
2000
+ diagrams). Single SVG path adapted
2001
+ from a real icon library; no internal
2002
+ seams. Text centres in an inset body
2003
+ (~15% horizontal, 20% vertical) so
2004
+ it doesn't float over the bumps.
2005
+ Attrs: variant=heroicons (default,
2006
+ sharp / modern) | material (puffy /
2007
+ friendly) | bootstrap (smooth /
2008
+ balanced).
2009
+ icon x y w h inline icon from the bundled Lucide library
2010
+ (~1960 icons). Outline-only, no fill
2011
+ by default; stroke colour from
2012
+ \`color=\` attribute.
2013
+ Attrs:
2014
+ name=<icon> (required) - any name
2015
+ from the bundled Lucide
2016
+ set. Run
2017
+ \`sdoc slides icons\` to
2018
+ list everything, or
2019
+ \`sdoc slides icons cloud\`
2020
+ to filter by substring.
2021
+ Common ones: user,
2022
+ database, server, lock,
2023
+ cloud, search,
2024
+ trending-up. Unknown
2025
+ names render a pink
2026
+ struck-through rect
2027
+ placeholder so typos
2028
+ are visible.
2029
+ color=<hex> stroke colour
2030
+ (default #0F1E3A).
2031
+ strokeWidth=N outline width
2032
+ (default 2; Lucide
2033
+ convention).
2034
+ The full icon bundle (~81 KB gzipped)
2035
+ is lazy-loaded on first use, then
2036
+ cached for the session. Until the
2037
+ bundle arrives, a dashed-rect
2038
+ placeholder marks each icon position
2039
+ so the layout doesn't reflow when
2040
+ icons swap in.
2041
+
2042
+ Arrow geometry: the coordinates are the line's centerline. The head is
2043
+ symmetric around the line, extending up to 3 * strokeWidth perpendicular
2044
+ on each side. So a horizontal arrow at y=5.78 with strokeWidth=0.06
2045
+ lines up cleanly with a horizontal line at y=5.78 (same centerline) -
2046
+ no need to offset the arrow's y to "clear" the head. For arrows shorter
2047
+ than 12 * strokeWidth the renderer scales the stroke (and head) down so
2048
+ the tip stays on (x2,y2); the arrow renders thinner than declared, but
2049
+ the endpoints stay honest.
2050
+
2051
+ Polygon segment operators (between adjacent point tokens).
2052
+ All curve operators use through-point semantics: the value you write
2053
+ is a point the curve actually passes through, not a hidden SVG control
2054
+ point. "Place the dot where you want the curve to go" works.
2055
+
2056
+ (none) straight line from previous point
2057
+ ~ soft bow with default sagitta = 10% of chord length
2058
+ (shorthand for a gentle ^h; use ^h when you need a
2059
+ specific bow depth)
2060
+ ^h arc / bow by sagitta h perpendicular to the chord;
2061
+ h is the actual peak height at t=0.5. Positive h bows
2062
+ to the LEFT of direction-of-travel (for a rightward
2063
+ chord, that is upward)
2064
+ >P quadratic Bezier whose midpoint passes through P
2065
+ (P is \`x,y\` or \`@ref\`; attached: \`>5,3\` / \`>@card.top\`)
2066
+ * P1 P2 cubic Bezier passing through P1 at t=1/3 and P2 at
2067
+ t=2/3 (P1, P2 each \`x,y\` or @ref). For predictable
2068
+ results, keep P1 / P2 within ~20% of chord length
2069
+ perpendicular to the chord; the closed-form cubic
2070
+ through both points uses SVG controls that amplify
2071
+ the offset ~4x, so far-from-chord through-points
2072
+ overshoot dramatically beyond the curve itself.
2073
+
2074
+ Polygon point modifiers (attach to the next point, not to an edge):
2075
+
2076
+ (r round the corner at the next point with radius r.
2077
+ Walks each adjacent edge back by r / tan(half-angle),
2078
+ replaces the sharp vertex with a circular arc tangent
2079
+ to both edges. Only takes effect when both adjacent
2080
+ segments are straight; silently no-ops if either is
2081
+ curved (~, ^, >, *). If r would consume more than half
2082
+ of either neighbouring chord, it shrinks to fit so
2083
+ adjacent rounded corners cannot overlap.
2084
+
2085
+ Example, all four corners of a card softened:
2086
+ p (0.4 0,0 (0.4 8,0 (0.4 8,5 (0.4 0,5 fill=#dbeafe
2087
+
2088
+ The same \`^h\` operator works between an arrow's two endpoints to bow
2089
+ the arrow into a curve:
2090
+ a 2 5 ^0.8 12 5 (rightward arrow bowing upward by 0.8u)
2091
+ a @plan.right ^-0.5 @ship.left (gentle downward bow between two shapes)
2092
+
2093
+ Polygon points are written \`x,y\` (one token per point), not space-
2094
+ separated like \`r x y w h\`. The variable point count needs a delimiter,
2095
+ so a comma is required inside each point.
2096
+
2097
+ All shapes EXCEPT \`l\` and \`a\` can hold markdown after \`|\` - full
2098
+ markdown (headings, lists, bold/italic, code, blockquote, tables).
2099
+ Non-rectangle shapes use their bounding box as the text area (see
2100
+ the TEXT INSIDE NON-RECT SHAPES section).
2101
+
2102
+ No \`fill=\` on a shape -> transparent (slide background shows through).
2103
+ Color values accept any CSS colour: hex (#1e40af), named (tomato),
2104
+ rgb(...), rgba(...).
2105
+
2106
+ Polygon examples:
2107
+ p 50,10 90,50 10,50 | Triangle
2108
+ p 10,10 90,10 ~ 90,50 10,50 | Rounded right edge (the ~ before
2109
+ a point softens that segment)
2110
+ p 2,6 ^0.8 9,6 9,8 2,8 fill=#e9d4a6
2111
+ (loaf-shaped card: arched top, three
2112
+ straight sides. Sagitta 0.8 sets the
2113
+ dome height in grid units)
2114
+ p 1,5 >5,1 9,5 1,8 fill=#dbeafe
2115
+ (one quadratic control point at (5,1)
2116
+ pulls the top edge into a peak)
2117
+ p 0,5 * 4,0 8,10 12,5 fill=#fee2e2
2118
+ (cubic with two controls: classic S-curve
2119
+ signature - rises early, falls late)
2120
+ p 10,20 60,20 60,10 90,30 60,50 60,40 10,40 | Next steps
2121
+ (right-pointing arrow shape - text
2122
+ renders in the polygon's bounding box)
2123
+
2124
+ ── COMMON PITFALLS ──────────────────────────────────
2125
+
2126
+ 1. \`^h\` is perpendicular to the chord, NOT vertical.
2127
+
2128
+ The docs note that "for a rightward chord, positive bows upward"
2129
+ - true, but for any other chord direction the bow follows the
2130
+ PERPENDICULAR. To dome the top of a polygon across a slanted
2131
+ span, use ONE \`^h\` across the whole top, not two arcs meeting
2132
+ at an apex:
2133
+
2134
+ p 3,7 ^1.8 13,7 13,8 3,8 smooth dome
2135
+ p 3,7 ^1.8 8,2.5 ^-1.8 13,7 13,8 3,8 NOT a dome
2136
+
2137
+ The second form has slanted chords (3,7)->(8,2.5) and
2138
+ (8,2.5)->(13,7). Positive \`h\` bows perpendicular-left of each
2139
+ chord direction, which points AWAY from the would-be apex - the
2140
+ arcs flare outward and meet at a sharp peak, not a smooth dome.
2141
+
2142
+ 2. Polygon points use \`x,y\` (one comma-separated token per point).
2143
+
2144
+ Rectangles, lines, and arrows use space-separated coords (\`r x y
2145
+ w h\`, \`a x1 y1 x2 y2\`). Polygons need an in-token delimiter so
2146
+ the parser knows where one point ends and the next begins:
2147
+
2148
+ p 2,7 8,3 13,7 fill=... YES
2149
+ p 2 7 8 3 13 7 fill=... parse error
2150
+
2151
+ The parser flags this with a clear "polygon: points use 'x,y'"
2152
+ error pointing to the offending line.
2153
+
2154
+ 3. Shapes don't auto-flow - overlapping bboxes paint over each
2155
+ other.
2156
+
2157
+ Every shape reserves its full (x, y, w, h). Source order is
2158
+ paint order, so a later shape with a fill that overlaps an
2159
+ earlier text shape's bbox covers that text:
2160
+
2161
+ r 6 9 88 5 text=subtitle | Three nested envelopes
2162
+ r 10 12 60 40 fill=#f1f5f9 <- starts at y=12, inside the
2163
+ subtitle bbox (y=9..14);
2164
+ subtitle hidden from y=12 down
2165
+
2166
+ The fix: place the next shape at y + h or later (here, y >= 14).
2167
+ The standard pattern from the gallery is subtitle at \`y=9 h=5\`,
2168
+ so the first content shape goes at y >= 14 (15 is the round
2169
+ number most templates use).
2170
+
2171
+ For \`text=title\` and \`text=subtitle\` add another ~0.6 grid
2172
+ unit because the glyph itself extends below the baseline. A
2173
+ text shape's \`h\` is the layout rect, not a hard clip - letters
2174
+ with descenders (g, p, q, y, j) draw about 20% of the font
2175
+ size below the baseline, which would hang into a perfectly
2176
+ bbox-aligned neighbour:
2177
+
2178
+ r 6 9 88 5 text=subtitle | The team after the Q1 reorg
2179
+ r 40 14 20 8 fill=#1e40af | CEO <- the g in "reorg"
2180
+ still crosses into the
2181
+ top of this box
2182
+
2183
+ Two safe budgets:
2184
+ a) 1 full grid unit of clearance below a text=subtitle row
2185
+ (next shape at y >= 15), or
2186
+ b) at least \`0.2 * fontSize\` in grid units (\`text=subtitle\`
2187
+ is 40px on REF_H=720, so ~0.6 grid units on a 56.25-tall
2188
+ grid).
2189
+
2190
+ The gotcha is loudest under \`text=title\` and \`text=subtitle\`
2191
+ because the font is large; \`text=body\` and \`text=caption\` rarely
2192
+ trip on it.
2193
+
2194
+ Circles and ellipses paint only the inscribed disc, but their
2195
+ bbox still claims the full (cx-r, cy-r, 2r, 2r). A label rect
2196
+ that overlaps the bbox CORNERS (outside the disc) is fine; a
2197
+ label rect that crosses the disc itself gets painted over,
2198
+ even when the label's own bbox doesn't fully overlap the
2199
+ bubble's bbox. The safe rule for bubble + label compositions:
2200
+ place the label's nearest edge at distance >= r + 0.5 grid
2201
+ units from the bubble centre. Common patterns that work:
2202
+ a) label centred BELOW the bubble at y = cy + r + 0.5
2203
+ b) label to the side at x = cx + r + 0.5 (left edge of label)
2204
+ Sizing the label rect to the bubble's bbox - e.g. r 76 18 12 3
2205
+ next to c 84 18 2.4 - puts the text directly under the disc
2206
+ and the bubble fill covers the end of the label.
2207
+
2208
+ 4. Code blocks inside shapes look randomly sized across siblings.
2209
+
2210
+ A \`\`\`fenced code block\`\`\` inside a slide shape sizes its OWN
2211
+ font and padding based on its content, not on the surrounding
2212
+ shape. Three sibling shapes with identical \`r ... 14 8\` end
2213
+ up looking different because their code samples differ in
2214
+ line count and line length. The cards then read as a sloppy
2215
+ row instead of a parallel set.
2216
+
2217
+ The fix is discipline at author-time:
2218
+
2219
+ a) Give every code-bearing shape the SAME (w, h). Don't let
2220
+ the longest sample dictate one card's size and the
2221
+ shortest dictate another.
2222
+ b) Set an explicit \`size=Npx\` on every code-bearing shape
2223
+ so the font matches across the row. Auto-fit picks per
2224
+ shape, which is exactly what you don't want for sibling
2225
+ comparison.
2226
+ c) Set an explicit \`padding=N\` on every code-bearing shape
2227
+ to control the inset uniformly.
2228
+ d) Truncate examples with \`...\` rather than letting one
2229
+ card carry six lines and another two. The point of a
2230
+ sibling row is comparison; the longest item sets the
2231
+ shape height for everyone.
2232
+
2233
+ Example - three comparable code cards:
2234
+
2235
+ r 6 35 26 12 fill=#F4F6FA size=14px padding=1 |
2236
+ \`\`\`json
2237
+ ["The", " quick", "..."]
2238
+ \`\`\`
2239
+ r 36 35 26 12 fill=#F4F6FA size=14px padding=1 |
2240
+ \`\`\`json
2241
+ [464, 4391, ...]
2242
+ \`\`\`
2243
+ r 66 35 26 12 fill=#F4F6FA size=14px padding=1 |
2244
+ \`\`\`json
2245
+ [[0.12, ...], ...]
2246
+ \`\`\`
2247
+
2248
+ Same w, same h, same size, same padding, truncated to fit.
2249
+ The three cards then read as a parallel set rather than three
2250
+ improvisations.
2251
+
2252
+ ── SHAPE ATTRIBUTES ──────────────────────────────────
2253
+ Between geometry and \`|\`:
2254
+
2255
+ Visual:
2256
+ fill=<color> Shape fill
2257
+ stroke=<color> Outline colour
2258
+ strokeWidth=N Outline width (grid units)
2259
+ radius=N Corner radius (rectangles)
2260
+ color=<color> Text colour inside the shape
2261
+ image=<url> Bitmap fill (see IMAGES section)
2262
+ opacity=N 0..1; fades the whole shape (fill + stroke +
2263
+ text overlay) at once. Useful for translucent
2264
+ backdrops, ghosted "previous state" shapes,
2265
+ overlapping highlights. Out-of-range values
2266
+ clamp.
2267
+
2268
+ Numeric attributes (strokeWidth, radius) are in grid units - pick
2269
+ values relative to your grid size, no prescribed defaults. On a
2270
+ 100-wide grid, \`radius=2\` is ~2% of slide width.
2271
+
2272
+ For text sizing (text=role, size=, h*Scale=), padding, alignment,
2273
+ and slot ids inside templates, see \`sdoc slides\` - those work the
2274
+ same in raw shapes and template shapes.
2275
+
2276
+ ── IDS AND @REFERENCES ──────────────────────────────
2277
+ Declare an id with \`#name\`; reference with \`@name\` or \`@name.anchor\`
2278
+ from line / arrow endpoints. Omit the anchor to default to centre.
2279
+
2280
+ r 10 10 30 20 #title | # Main Point
2281
+ r 60 10 30 20 #detail | Supporting detail
2282
+
2283
+ a @title @detail (default: centres of both shapes)
2284
+ a @title.right @detail.left (explicit: right edge to left edge)
2285
+
2286
+ Each endpoint picks its own anchor independently, so you can connect
2287
+ the bottom of one box to the top of another:
2288
+ l @box-a.bottom @box-b.top
2289
+
2290
+ 9 anchors: center (default), top, bottom, left, right, topleft,
2291
+ topright, bottomleft, bottomright.
2292
+
2293
+ Anchors resolve against each shape's BOUNDING BOX. For circles and
2294
+ ellipses that means the circumscribing rectangle, not the perimeter -
2295
+ so @circle.right lands at the box edge, not the curve.
2296
+
2297
+ \`l\` and \`a\` endpoints can mix \`@ref\` with raw \`x y\` coords freely,
2298
+ e.g. \`l @title.bottom 50 30\`.
2299
+
2300
+ Note that \`#name\` is overloaded: in a \`@template\` declaration it
2301
+ marks a slot for consumers to fill; in a raw slide it marks an id
2302
+ for @refs. Same syntax, two purposes - context decides.
2303
+
2304
+ ── TEXT INSIDE NON-RECT SHAPES ──────────────────────
2305
+ Text inside a polygon, circle, or ellipse lays out in the shape's
2306
+ AXIS-ALIGNED BOUNDING BOX, not the visible silhouette. For a trapezoid,
2307
+ triangle, or arrow this means the text rectangle is larger than the
2308
+ filled shape - long labels can overhang the slanted edges and end up
2309
+ partly outside the visible polygon.
2310
+
2311
+ Example. A funnel band:
2312
+ p 1,1.8 15,1.8 14.2,3 1.8,3 fill=#e0e7ff align=left | 28M devs
2313
+ The bounding box is x ∈ [1, 15], y ∈ [1.8, 3]. \`align=left\` parks the
2314
+ text at x ≈ 1, which is the leftmost point of the WIDEST corner.
2315
+ At the bottom of the band the silhouette only reaches x ≈ 1.8, so a
2316
+ two-line label would clip on the second line.
2317
+
2318
+ Two ways to handle this:
2319
+
2320
+ 1. Short, centered labels.
2321
+ Use \`align=center\` and keep the label short enough that it fits
2322
+ within the silhouette's narrowest waist. Works for symmetric
2323
+ trapezoids, hexagons, ellipses.
2324
+
2325
+ p 5.4,7.4 10.6,7.4 9.4,8.3 6.6,8.3 fill=#312e81 color=#fff align=center |
2326
+ **250k**
2327
+
2328
+ 2. Shape-only + separate text-r on top.
2329
+ Draw the polygon WITHOUT content (no \`|\`), then place an \`r\` shape
2330
+ on top, sized to fit safely inside the silhouette. Lets the text
2331
+ box be a clean rectangle while the visible silhouette stays slanted.
2332
+
2333
+ p 1,1.8 15,1.8 14.2,3 1.8,3 fill=#e0e7ff
2334
+ r 2,2 12,0.8 align=center valign=center |
2335
+ **21M** use any AI tool at work
2336
+
2337
+ The \`r\` paints above the \`p\` because it is declared after the \`p\`
2338
+ in source order. Reverse the lines and the polygon would cover the
2339
+ rect instead.
2340
+
2341
+ ── POLYGON GEOMETRY ──────────────────────────────────
2342
+ Slant strength. A trapezoid where each side indents 0.8u over a
2343
+ 1.2u height (a ~6% slant) reads as a rectangle. If you want the
2344
+ viewer's eye to see "narrowing", make each side indent at least
2345
+ ~15% of the band's height. For a stack of bands (funnel, pyramid),
2346
+ pick a constant slant ratio across all bands so the silhouette
2347
+ reads as one continuous taper rather than a hinge between
2348
+ rectangles and triangles.
2349
+
2350
+ Order matters within a slide. Shapes declared later paint over
2351
+ shapes declared earlier (this applies across types — see LAYERING).
2352
+ For nested concentric shapes (TAM/SAM/SOM), declare the outermost
2353
+ first; inner shapes cover the outers' label space. Plan your label
2354
+ positions in the VISIBLE RING between each shape and its inner
2355
+ neighbour - or move labels out to an adjacent \`r\` column.
2356
+
2357
+ Curved segments. Five operators between adjacent points: no operator
2358
+ is a straight segment; \`~\` gives a soft bow at 10% of chord length;
2359
+ \`^h\` arcs / bows the segment by an explicit sagitta h; \`>P\` is a
2360
+ quadratic that passes through P at its midpoint; \`* P1 P2\` is a cubic
2361
+ that passes through P1 and P2 at the curve's third-points. Controls
2362
+ can be \`@refs\`, so the curve docks exactly onto another shape's edge:
2363
+
2364
+ r 1 2 4 3 #card
2365
+ r 11 2 4 3 #note
2366
+ a @card.right ^0.6 @note.left (curved arrow between two cards)
2367
+
2368
+ p @card.bottomleft >@card.bottom @note.bottomleft @note.bottom \\
2369
+ @note.bottomright @card.bottomright fill=#f1f5f9
2370
+ (banded shape: top edge dips between
2371
+ the two cards' bottom centers)
2372
+
2373
+ Bow direction. Positive sagitta bows to the LEFT of direction-of-
2374
+ travel. For a horizontal chord moving right, positive bow = upward.
2375
+ Negative bow flips the curve to the opposite side. The same
2376
+ convention applies to polygon \`^h\` segments and bowed arrows.
2377
+
2378
+ Useful for: rounded card corners, dome / loaf tops, speech-bubble
2379
+ tails, curved connectors, organic silhouettes (leaves, clouds, lenses),
2380
+ S-curve callouts. Bad for text-bearing shapes because the bounding
2381
+ box still treats the curve as if it were a straight chord, so labels
2382
+ may overhang the visible silhouette.
2383
+
2384
+ Concave polygons. The bounding box of a concave shape includes the
2385
+ concavity - text can sit in the notch and overlap a neighbouring
2386
+ shape. For concave shapes (arrows, callouts, chevrons), use the
2387
+ shape-only + r-overlay pattern.
2388
+
2389
+ ── COMPOSITE PATTERNS ───────────────────────────────
2390
+ Each pattern obeys the DESIGN PRINCIPLES above: at most one
2391
+ saturated fill per slide (used as the focal element), thin neutral
2392
+ strokes only when contour is doing real work, labels outside the
2393
+ shape when the shape is too narrow to hold them.
2394
+
2395
+ Process flow with a focal step.
2396
+ Two pale frames + one navy focal step. The navy IS the slide's
2397
+ one deviation - it tells the audience which step matters.
2398
+ r 1,3.5 3,1.5 stroke=#cbd5e1 strokeWidth=0.02 align=center valign=center | **Plan**
2399
+ r 4.5,3.5 3,1.5 stroke=#cbd5e1 strokeWidth=0.02 align=center valign=center | **Build**
2400
+ r 8,3.5 3,1.5 fill=#1e40af color=#ffffff align=center valign=center | **Ship**
2401
+ a @plan.right @build.left
2402
+ a @build.right @ship.left
2403
+
2404
+ TAM/SAM/SOM with magnitude-proportional rectangles.
2405
+ All three rects share their top-left corner; sides scale by
2406
+ \`sqrt(value / 32)\` so the AREAS read as the dollar values, not
2407
+ just "three nested shapes". Labels live in the right column with
2408
+ short leaders - the inner rects are too small to hold them and
2409
+ putting labels inside would hide some behind others. Only the
2410
+ focal SOM uses saturated fill.
2411
+
2412
+ # TAM 32 -> sqrt(32/32) = 1.0 (9.0 x 6.0 = 54 sq u)
2413
+ # SAM 8 -> sqrt( 8/32) = 0.5 (4.5 x 3.0 = 13.5)
2414
+ # SOM 1.5 -> sqrt(1.5/32) ~ 0.22 (2.0 x 1.3 ~ 2.6)
2415
+ r 1,2 9 6 stroke=#94a3b8 strokeWidth=0.02
2416
+ r 1,2 4.5 3 stroke=#94a3b8 strokeWidth=0.02
2417
+ r 1,2 2.0 1.3 fill=#1e40af
2418
+ r 11,2 4 6 align=left valign=top |
2419
+ **TAM** $32B - global developer tools
2420
+ **SAM** $8B - AI-coding subset
2421
+ **SOM** $1.5B - CLI-agent slice (4.7% of TAM by area)
2422
+
2423
+ Callout / speech bubble.
2424
+ Polygon for the bubble outline (thin neutral stroke; no fill),
2425
+ \`r\` for the text content positioned to avoid the bubble's tail.
2426
+ Reserved for genuine annotation - if the callout could be a
2427
+ body paragraph, make it one.
2428
+
2429
+ ── LAYERING ────────────────────────────────────────
2430
+ Source order = paint order. The shape declared later in the slide
2431
+ paints on top of shapes declared earlier. This holds across shape
2432
+ types — a polygon declared after a rectangle paints above that
2433
+ rectangle and vice versa.
2434
+
2435
+ # Rect first, polygon second - polygon paints on top.
2436
+ r 2 2 10 5 fill=#1e40af
2437
+ p 4,3 12,3 12,6 4,6 fill=#fde68a
2438
+
2439
+ # Reverse the lines, the rect is on top instead.
2440
+
2441
+ That's the whole rule for 95% of decks. If you want a connector
2442
+ arrow above the cards it joins, declare the arrow last. If you want
2443
+ a backdrop behind a card, declare the backdrop first.
2444
+
2445
+ Escape hatch: \`layer=top | mid | bottom\` (default \`mid\`)
2446
+
2447
+ When source order isn't enough — usually inside a template whose
2448
+ consumer adds more shapes — set \`layer=\` to promote or demote a
2449
+ shape across the three coarse sublayers:
2450
+
2451
+ bottom - paints before everything regardless of source position
2452
+ mid - the default
2453
+ top - paints after everything regardless of source position
2454
+
2455
+ Invalid values surface in the error badge. For hand-authored slides
2456
+ you should rarely need \`layer=\` at all; reaching for it is a hint
2457
+ that the shape order itself wants reordering.
2458
+
2459
+ ── IMAGES IN SHAPES ────────────────────────────────
2460
+ Any shape (\`r\`, \`c\`, \`p\`, etc.) can hold a bitmap via \`image=<url>\`
2461
+ or the \`i x y w h\` shape sugar (parser-equivalent to \`r\` + \`image=\`).
2462
+ See the IMAGES section in \`sdoc slides\` for the full reference -
2463
+ it works the same in raw shapes and template image slots.
2464
+
2465
+ ── WHEN TO STOP AND USE A TEMPLATE ─────────────────
2466
+ If your custom slide ends up being "title at top + body below" or
2467
+ "title + two columns" or "title + chart + takeaway", you're
2468
+ re-implementing a stdlib template. Run \`sdoc slides list\` and pick
2469
+ the closest match - the templates encode safe margins, role
2470
+ typography, and slot semantics that you'd otherwise have to re-derive.
2471
+
2472
+ Raw shapes earn their keep for: market sizing diagrams, custom
2473
+ funnels / pyramids / matrices, decision trees, process flows with
2474
+ arrows between named blocks, anything where the GEOMETRY is the
2475
+ message. For everything else, \`@extends\` first.
2476
+
2477
+ See also:
2478
+ sdoc slides Main slide DSL reference
2479
+ sdoc slides list Built-in templates + slot lists
2480
+ sdoc charts Chart fenced blocks (\`\`\`chart)
2481
+ sdoc diagrams Mermaid fenced blocks (\`\`\`mermaid)
2482
+ `;
2483
+
2484
+ const LIBRARY_HELP = `
2485
+ SmallDocs - Library
2486
+ ===============
2487
+ The library is a personal, on-machine index of every markdown file you
2488
+ open with \`sdoc <file>\`. It lets you search, browse, and re-open your
2489
+ notes without remembering where they live on disk.
2490
+
2491
+ WHAT IT IS, IN ONE PARAGRAPH
2492
+ Every \`sdoc <file>\` writes a small record into ~/.sdocs/library-
2493
+ index.json - the file path, its title, its tags, when it was last
2494
+ touched. A small loopback HTTP server reads that index when you visit
2495
+ smalldocs.org/library in a browser, so the page can show your files
2496
+ without anything leaving your machine. Click a result and SmallDocs spins
2497
+ up a live editing session against the file on disk. Nothing about
2498
+ the library ever touches a remote server.
2499
+
2500
+ COMMANDS
2501
+ Open and inspect
2502
+ sdoc library Open the library UI at
2503
+ smalldocs.org/library (or
2504
+ localhost:3000/library when
2505
+ running the dev server).
2506
+ Starts the local agent if it
2507
+ isn't already running.
2508
+ sdoc library ls List markdown indexed under
2509
+ this project (walks up to the
2510
+ nearest .git, falls back to
2511
+ cwd). Pass a path to override
2512
+ scope: \`sdoc library ls ~/notes\`.
2513
+ sdoc library ls --tags Same scope, but prints the tag
2514
+ bag (tag - count, sorted by
2515
+ frequency). Run before tagging
2516
+ a new file to stay consistent.
2517
+ sdoc library status Print enabled/disabled, entry
2518
+ count, last scan time.
2519
+
2520
+ Refresh
2521
+ sdoc library rebuild Walk \$HOME again from scratch
2522
+ and refresh every entry. Use
2523
+ after moving files around or
2524
+ when an entry seems stale.
2525
+
2526
+ On/off
2527
+ sdoc library enable Re-enable indexing-on-open if
2528
+ you had turned it off. New
2529
+ opens will be recorded again.
2530
+ sdoc library disable Stop indexing on \`sdoc <file>\`.
2531
+ The existing index is left in
2532
+ place; the library page still
2533
+ shows what's already there.
2534
+
2535
+ Autostart (macOS)
2536
+ sdoc library autostart Same as \`autostart status\`:
2537
+ print whether the LaunchAgent
2538
+ is installed.
2539
+ sdoc library autostart enable Install the LaunchAgent so the
2540
+ loopback agent comes back on
2541
+ every login. This is the
2542
+ default the first time you run
2543
+ \`sdoc library\`.
2544
+ sdoc library autostart disable Remove the LaunchAgent and
2545
+ record that you turned it off;
2546
+ the default-on logic respects
2547
+ this until you re-enable.
2548
+ sdoc library autostart status Print whether the LaunchAgent
2549
+ is installed and where its
2550
+ plist lives.
2551
+
2552
+ Help
2553
+ sdoc library --help This help.
2554
+ sdoc library help Same.
2555
+
2556
+ WHAT GETS INDEXED
2557
+ Every file you open with \`sdoc <file>\` is recorded at open time.
2558
+ \`sdoc library rebuild\` additionally walks \$HOME looking for
2559
+ markdown that fits these rules:
2560
+ - extension is .md, .mdx, or .markdown
2561
+ - size is at most 1 MB
2562
+ - not under a hidden directory (.git, .venv, .cache, ...)
2563
+ - not under a system / framework directory (node_modules, dist,
2564
+ build, target, vendor, .next, __pycache__, ...)
2565
+ - not under a sensitive directory (.ssh, .aws, .gnupg, .docker,
2566
+ .kube, .gcloud, .azure, .bitwarden, .password-store)
2567
+ - the file's basename does not match the deny list (SSH keys,
2568
+ .env files, .key/.pem/.crt/.kdbx etc., credentials.json,
2569
+ api-secret.yaml, ...). Markdown is deliberately NOT denied by
2570
+ name - \`credentials-handling.md\` is a legitimate note about
2571
+ secrets, not a secret.
2572
+ - the file is not excluded by a .sdocsignore (see below)
2573
+ - the file's front matter does not contain \`sdocs-library: false\`
2574
+ (per-file opt-out)
2575
+
2576
+ PER-FILE OPT-OUT
2577
+ Add \`sdocs-library: false\` to a file's YAML front matter to keep
2578
+ it out of the library. The file still opens with \`sdoc <file>\`,
2579
+ it just isn't indexed.
2580
+
2581
+ ---
2582
+ title: My private notes
2583
+ sdocs-library: false
2584
+ ---
2585
+
2586
+ PER-DIRECTORY EXCLUDES (.sdocsignore)
2587
+ Drop a \`.sdocsignore\` file into any directory. Subset of gitignore
2588
+ syntax:
2589
+ # comments and blank lines are skipped
2590
+ drafts/ exclude this directory
2591
+ *.local.md exclude by glob, basename match
2592
+ sub/secret.md anchored path match (relative to this file)
2593
+ **/scratch/ cross-directory globstar
2594
+ Patterns inherit downward. Negation (\`!\`) and other gitignore
2595
+ niceties are not in v1.
2596
+
2597
+ EPHEMERAL PATHS AND RESCUE
2598
+ Files opened from OS-managed temp folders are special: the OS will
2599
+ clean them on its own schedule (next reboot, days later, when you
2600
+ empty the trash). The library handles this by taking a one-shot
2601
+ RESCUE COPY at index time and pointing the entry at the copy.
2602
+
2603
+ Rescue triggers when the path sits inside any of:
2604
+ macOS /tmp, /private/tmp, /var/tmp, /private/var/tmp,
2605
+ ~/.Trash, plus Node's os.tmpdir()
2606
+ Linux /tmp, /var/tmp, /run, /dev/shm, ~/.cache, plus os.tmpdir()
2607
+ Windows %TEMP%, %TMP%, C:\\Windows\\Temp
2608
+
2609
+ The copy lives at ~/.sdocs/library/rescued/<hash>-<basename>. The
2610
+ hash prefix avoids name collisions between two /tmp/notes.md from
2611
+ different sessions. The entry's \`rescuedFrom\` field records the
2612
+ original path; the agent's security gate accepts requests for both
2613
+ the rescued copy and the original path.
2614
+
2615
+ Caveat: rescue is a SNAPSHOT, not a live mirror. Edit \`/tmp/foo.md\`
2616
+ after the first \`sdoc\` and the rescued copy is stale until you
2617
+ re-open the file (which triggers an index update). Once the OS
2618
+ deletes the original, the rescued copy is what you have.
2619
+
2620
+ TAGGING (TWO WAYS TO TAG)
2621
+ Tags live in the file's YAML front matter on disk. There are two
2622
+ ways to put them there:
2623
+
2624
+ 1. Write the front matter directly. The canonical place; survives
2625
+ every export and round-trip.
2626
+
2627
+ ---
2628
+ title: Q2 plan
2629
+ tags:
2630
+ - planning
2631
+ - q2
2632
+ - finance
2633
+ ---
2634
+
2635
+ The Tags row in the file-info card (browser editor) is also a
2636
+ write into the file's front matter, performed by the Bridge.
2637
+
2638
+ 2. CLI flag at open time. Append \`+tag\` to any \`sdoc\` invocation:
2639
+
2640
+ sdoc plan.md +planning +q2
2641
+
2642
+ SmallDocs injects those tags into the file's YAML front matter
2643
+ before the browser receives the content, so they survive future
2644
+ opens, exports, and any sync. The \`+\` prefix is shell-safe
2645
+ (\`#\` would be eaten as a comment). Multiple \`+tag\` args
2646
+ anywhere on the command line are allowed.
2647
+
2648
+ #words in prose are NOT extracted as tags. Front matter is the
2649
+ one source of truth; everything else is a way to write to it.
2650
+
2651
+ EDITING TAGS FROM THE BROWSER
2652
+ Open any indexed file (\`sdoc <file>\`) and look at the file info
2653
+ card above the document. There is a Tags row with chips. Hover a
2654
+ chip for an \`x\` to drop it; click \`+\` at the end of the row to
2655
+ add a new one.
2656
+
2657
+ Edits happen through the BRIDGE (the WebSocket connection that
2658
+ also handles live save). The library agent never writes content -
2659
+ exactly one process writes to your file at a time, which avoids
2660
+ the race condition two parallel writers would create.
2661
+
2662
+ If the Tags row is read-only with a hint, you opened the file via
2663
+ a short link or hash URL and there is no Bridge to save through.
2664
+ Open the file via \`sdoc <file>\` to get an editable Tags row.
2665
+
2666
+ CONSISTENCY HINTS FOR AGENTS
2667
+ Before tagging a new file in a project, run:
2668
+
2669
+ sdoc library ls --tags
2670
+
2671
+ This prints the project's current tag vocabulary (tag - count). Re-use
2672
+ existing tags where possible so the library stays consistent. The
2673
+ scope walks up to the nearest .git, so cd anywhere inside the repo
2674
+ works.
2675
+
2676
+ To see what's already in the project:
2677
+
2678
+ sdoc library ls
2679
+
2680
+ THE LOCAL AGENT
2681
+ The library page (smalldocs.org/library or localhost:3000/library) is
2682
+ hosted from the SmallDocs server; the data it shows comes from a tiny
2683
+ HTTP server the CLI runs at 127.0.0.1:47843. That server:
2684
+ - reads ~/.sdocs/library-index.json
2685
+ - never writes user content (writes go through the Bridge)
2686
+ - refuses any browser request whose Origin is not smalldocs.org,
2687
+ sdocs.dev, or localhost:3000 (extend with SDOCS_URL or
2688
+ SDOCS_AGENT_ALLOWED_ORIGINS)
2689
+ - refuses requests whose Host header is not 127.0.0.1 / localhost
2690
+ (DNS-rebinding guard)
2691
+ - refuses path arguments outside the library, on the deny list,
2692
+ or behind a symlink that resolves outside the library
2693
+ If port 47843 is busy, the CLI falls back to a random port and
2694
+ prints the URL with the port baked in.
2695
+
2696
+ AUTOSTART (macOS)
2697
+ The first time you run \`sdoc library\`, a LaunchAgent plist is
2698
+ written to ~/Library/LaunchAgents so the agent comes back after
2699
+ every reboot. Turn it off explicitly with:
2700
+
2701
+ sdoc library autostart disable
2702
+
2703
+ Once disabled, the default-on logic does not re-enable it without
2704
+ you running \`sdoc library autostart enable\` again. The browser
2705
+ shows a small recovery banner only when autostart was enabled but
2706
+ the agent isn't actually answering.
2707
+
2708
+ Linux and Windows do not have autostart in v1. The agent only
2709
+ runs when you start it explicitly with \`sdoc library\`.
2710
+
2711
+ CLI INTERACTION CHEAT SHEET FOR AGENTS
2712
+ Before authoring a new note inside a project:
2713
+ sdoc library ls --tags see the project's tag vocabulary
2714
+
2715
+ When opening or creating a file with tags:
2716
+ sdoc draft.md +planning +q2 tags get written into front matter
2717
+
2718
+ When you want a file kept OUT of the library:
2719
+ add \`sdocs-library: false\` to its YAML front matter
2720
+
2721
+ When the user asks "where is X?":
2722
+ sdoc library opens the search UI in their browser
2723
+
2724
+ PRIVACY MODEL
2725
+ The library is per-machine and per-user. Nothing about it is
2726
+ uploaded to smalldocs.org or any other server. The browser page on
2727
+ smalldocs.org calls the local agent over plain HTTP on 127.0.0.1; it
2728
+ cannot see file content the agent doesn't already have indexed,
2729
+ and the agent cannot read files outside the library.
2730
+
2731
+ COMMON QUESTIONS
2732
+ Q: I opened a file but I don't see it in the library.
2733
+ A: Check \`sdoc library status\` (is it enabled?), then
2734
+ \`sdoc library rebuild\`. If the file is in front matter
2735
+ \`sdocs-library: false\`, that's the opt-out. Also check for a
2736
+ .sdocsignore in the file's directory or any ancestor.
2737
+
2738
+ Q: The library page shows "agent is not running".
2739
+ A: Run \`sdoc library\` once to start it, or enable autostart with
2740
+ \`sdoc library autostart enable\`. The banner is dismissible if
2741
+ you don't want one running.
2742
+
2743
+ Q: A tag I added in the browser didn't save.
2744
+ A: The Tags row needs a connected Bridge. Open the file via
2745
+ \`sdoc <file>\` (not via a share URL or short link) so the
2746
+ editor knows the on-disk path.
2747
+
2748
+ Q: I added a tag with \`+tag\` but it didn't appear.
2749
+ A: The \`+\` is what makes it a tag arg; \`tag\` alone would be read
2750
+ as a filename. Valid form: \`+name\` where name starts with a
2751
+ letter and contains letters, digits, underscores, or hyphens
2752
+ (max 64 chars).
2753
+
2754
+ Q: I want to keep work notes out of the library.
2755
+ A: Add \`sdocs-library: false\` to the file's front matter, or
2756
+ drop a \`.sdocsignore\` into the directory with the pattern.
2757
+ `;
2758
+
2759
+ module.exports = { HELP, COMMENTS_HELP, SCHEMA, CHARTS_HELP, DIAGRAMS_HELP, CELLS_HELP, SLIDES_HELP, SLIDES_CUSTOM_SHAPES_HELP, LIBRARY_HELP };