sdocs-dev 1.6.1 → 1.12.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,2706 @@
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
+ SORTING
1015
+ Hover a column letter: an arrow appears on its right showing what a click
1016
+ will do (up = sort ascending, down = descending, x = clear the sort). The
1017
+ current sort shows as a colored arrow. A text first row is kept pinned as
1018
+ a header. Sorting is a view reorder - it does not change the document;
1019
+ copy reflects what is on screen.
1020
+
1021
+ Formula cells sort by their computed value, and that value travels with
1022
+ its row: references keep meaning what the author wrote, so a sorted
1023
+ =SUM(...) row never recomputes against the shuffled view.
1024
+
1025
+ A trailing summary row - one whose formula aggregates a range of other
1026
+ rows, like a Total row with =SUM(D2:D5) - is pinned to the bottom and
1027
+ stays there through any sort, the same way a header row stays at the top.
1028
+ Per-row formulas like =B2*C2 sort with the data as normal.
1029
+
1030
+ RESIZING
1031
+ Drag a column header's right edge to set an explicit width; it persists
1032
+ through sorts. Otherwise columns size to their content.
1033
+
1034
+ SELECTING
1035
+ Click a cell to select it; its column letter and row number light up.
1036
+ Click a column letter / row number to select the whole column / row.
1037
+ Drag to select a range; Shift+Click or Shift+Arrow extends it. Arrow
1038
+ keys move the selection; Cmd/Ctrl+Arrow jumps to the far edge;
1039
+ Shift+(Cmd/Ctrl+)Arrow extends to it. Dragging to the left/right edge of
1040
+ a wide sheet auto-scrolls. Esc - or clicking anywhere outside the sheet -
1041
+ clears the selection.
1042
+
1043
+ Selecting a multi-cell range opens a stats strip below the grid with the
1044
+ range's Sum / Avg / Min / Max / Count. Formula cells count by their
1045
+ computed value. It collapses when the selection is cleared or back to a
1046
+ single cell.
1047
+
1048
+ TOOLBAR
1049
+ A white bar sits above each sheet:
1050
+ - Left: the selection address (e.g. B3 or B2:C3) and, for a sheet
1051
+ loaded from a file, the source filename - "B3 · report.csv".
1052
+ - Right: a copy icon that copies the WHOLE sheet as CSV (values only,
1053
+ no row / column labels).
1054
+ - When something is selected, a second button copies just the
1055
+ selection ("cell" for one cell, "selection" for a range).
1056
+ - A fullscreen expand button opens the sheet as a full-window,
1057
+ editable spreadsheet (see FULLSCREEN & EDITING).
1058
+
1059
+ Copy buttons copy what the sheet shows: formula cells emit their
1060
+ computed value, not the "=..." text. In fullscreen, a sheet with
1061
+ formulas swaps the copy icon for a labelled "values" button and adds a
1062
+ "formulas" button beside it that copies the raw data - formulas as
1063
+ written, plain values elsewhere.
1064
+
1065
+ LOADING FROM A CSV FILE
1066
+ Reference a file instead of typing data inline:
1067
+
1068
+ \`\`\`cells
1069
+ {{path/to/report.csv}}
1070
+ \`\`\`
1071
+
1072
+ sdoc reads that file (resolved relative to the markdown document), reads
1073
+ the whole thing, and bakes the CSV straight into the document. The result
1074
+ is self-contained: a shared link shows the data, never a "file not
1075
+ found". Only the filename is recorded as the source (shown in the bar),
1076
+ not the full path.
1077
+
1078
+ - One reference per cells block (the block's entire body is the
1079
+ reference).
1080
+ - \`sdoc report.csv\` opens a CSV file directly as a sheet.
1081
+
1082
+ CSV FILES WITH FORMULAS
1083
+ A .csv file can hold =formulas in its cells, exactly like an inline
1084
+ block:
1085
+
1086
+ Item,Qty,Total
1087
+ Laptop,12,=B2*100
1088
+ Total,=SUM(B2:B2),=SUM(C2:C2)
1089
+
1090
+ \`sdoc that-file.csv\` opens it as a working sheet - the formulas
1091
+ compute - and the Excel download carries them as live formulas. This
1092
+ is the lowest-friction way for an agent to hand over a spreadsheet:
1093
+ write a plain CSV with formulas where computed values belong, run
1094
+ sdoc on it. Quote any formula containing commas ("=ROUND(B2, 1)") -
1095
+ standard CSV quoting. The file itself opens read-only; it stays the
1096
+ clean source of truth.
1097
+
1098
+ EXCEL EXPORT SECURITY
1099
+ Only formulas this sheet itself can compute (the FORMULAS list above)
1100
+ export as live Excel formulas. Anything else - Excel functions we do
1101
+ not support, and especially the CSV-injection attack class
1102
+ (WEBSERVICE, HYPERLINK, DDE) - exports as inert text. A document
1103
+ someone shares with you can never smuggle an executable formula into
1104
+ your downloaded workbook.
1105
+
1106
+ LIMITS
1107
+ - Per-block source-size cap and a per-document block cap.
1108
+ - The inline preview bounds very large grids and notes what it clipped
1109
+ ("Showing 200 × 50 of 231 × 60 cells"); the full data still travels and
1110
+ copies.
1111
+
1112
+ FULLSCREEN & EDITING
1113
+ The expand button opens the sheet full-window: frozen headers, both-axis
1114
+ scroll, and a header bar with the selection address, its Sum / Avg /
1115
+ Min / Max / Count, and the formula bar. Formula cells count toward those
1116
+ stats by their computed value. The fullscreen sheet is editable (the
1117
+ inline one stays read-only):
1118
+ - Double-click a cell, or just start typing, to edit it.
1119
+ - Enter / Tab (and Shift+ to go back) commit and move on.
1120
+ - The formula bar edits the active cell; type a value or =formula, Enter.
1121
+ - Delete / Backspace clears the selected cell(s).
1122
+ - Cmd/Ctrl+Z undoes, Shift+Cmd/Ctrl+Z (or Ctrl+Y) redoes.
1123
+ - Paste TSV or CSV to drop a block of values in at the selection.
1124
+ - Point mode: while typing a formula, arrow keys point at cells and
1125
+ write their reference into the formula. Type =SUM( then arrow to a
1126
+ cell, hold Shift+Arrow to grow it into a range, type ) and Enter.
1127
+ Or arrow to the range start, type :, arrow to the end. Works in the
1128
+ cell editor and the formula bar; the pointed cells highlight violet.
1129
+ - Fill handle: drag the small square on the selection's corner to fill
1130
+ neighbouring cells. Formulas shift their references (=B2*C2 becomes
1131
+ =B3*C3 one row down), values repeat, and a run of numbers (1, 2)
1132
+ continues as a series (3, 4...).
1133
+ - Copy / paste formulas: Cmd/Ctrl+C copies the selection (formulas
1134
+ included); pasting back in adjusts each formula's references for
1135
+ where it lands. Copy one formula, select a range, paste - every cell
1136
+ gets the formula relative to its own row/column.
1137
+ - Formula view: when a sheet has formulas, an =fx button in the top bar
1138
+ switches every formula cell to show its source ("=B2*C2") instead of
1139
+ its value, editable in place. Click again for values.
1140
+ Editing is client-side and ephemeral: changes show in the inline grid when
1141
+ you close, but nothing is written back to the document or to any file.
1142
+
1143
+ After a fullscreen edit, the inline toolbar shows a "showing edited" pill:
1144
+ the grid displays your edits, the document is unchanged. Click the pill to
1145
+ flip to the document's original data ("showing original") and back.
1146
+ Expanding again always resumes from the edits.
1147
+
1148
+ EXPORT
1149
+ HTML / Word / PDF export emit a real table of the values (the source
1150
+ label and the row / column chrome are dropped).
1151
+
1152
+ The download button in a sheet's toolbar (inline and fullscreen) exports
1153
+ that sheet as an Excel workbook (.xlsx). Formulas export as live Excel
1154
+ formulas and recalculate when the file opens; the format: directive's
1155
+ currency / percent / comma columns carry over as Excel number formats.
1156
+ The export uses the document's row order (plus any fullscreen edits),
1157
+ never the sorted view, so formula references stay correct.
1158
+ `;
1159
+
1160
+ const SLIDES_HELP = `
1161
+ SmallDocs — Slides
1162
+ ==============
1163
+ Embed presentation slides in any markdown document using fenced
1164
+ slide blocks. Slides render as thumbnails inline; click the small
1165
+ present-mode icon in the slide's top-right corner to enter fullscreen.
1166
+ Text inside a slide thumbnail is selectable. Esc to exit, arrows to
1167
+ navigate.
1168
+
1169
+ \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
1170
+ sdoc present <file> Open file directly in fullscreen slide view
1171
+ sdoc <file> Open normally (click a slide's present icon)
1172
+ sdoc slides This help
1173
+ sdoc slides list List built-in templates + slot names
1174
+ sdoc slides icons [query] List the Lucide icons available to the
1175
+ \`icon\` shape kind. Optional substring filter.
1176
+ sdoc slides custom-shapes Long-tail notes for raw-shape custom slides
1177
+ (polygon text, composite patterns, layering).
1178
+ Most decks use \`@extends\` and never need this.
1179
+
1180
+ ── DESIGN GUIDELINES ────────────────────────────────────
1181
+ The built-in templates encode a few rules that separate professional
1182
+ slides from amateur ones. When you compose slides by hand (no
1183
+ template), keep these in mind - they're the difference between a
1184
+ deck that lands and one that doesn't.
1185
+
1186
+ Margins. Keep all content inside a 1-unit safe area on every side
1187
+ of a 16x9 grid (so x ∈ [1, 15], y ∈ [0.5, 8.5]). Nothing touches
1188
+ the slide edge except a deliberate full-bleed background (\`section\`
1189
+ uses this; nothing else should).
1190
+
1191
+ No auto-flow. Every shape claims its full (x, y, w, h) rect.
1192
+ Source order is paint order, so a later filled rectangle that
1193
+ overlaps an earlier text shape's bbox covers its content. Place
1194
+ each shape at the previous shape's bottom edge (y + h) or later.
1195
+ Common slip: putting content at y=12 right under
1196
+ \`r 6 9 88 5 text=subtitle ...\` - the subtitle reserves y=9..14;
1197
+ starting the body at y=12 puts the fill on top of the subtitle's
1198
+ bottom half. For \`text=title\` and \`text=subtitle\` add another
1199
+ ~0.5 grid unit on top to clear glyph descenders; see
1200
+ \`slides custom-shapes\` for the fine print.
1201
+
1202
+ No fill colours behind body or title text. The slide background IS
1203
+ your canvas. Saturated rectangles compete with content and read as
1204
+ "PowerPoint 2003". The only exception: section dividers, which want
1205
+ contrast against content slides - use \`grid bg=\` there, not a
1206
+ shape \`fill=\`.
1207
+
1208
+ Two or three sizes per slide, max. Stick to the role table:
1209
+ text=title (64px) for cover, quote, section, metric
1210
+ text=subtitle (40px) for in-deck content-slide titles
1211
+ text=body (24px) default; bullets, paragraphs
1212
+ text=caption (14px) ONLY for footers, eyebrows, attributions
1213
+ Caption renders as ~3px in a 240px-wide thumbnail - never put
1214
+ load-bearing content in caption role.
1215
+
1216
+ Default \`valign=center\`. \`valign=top\` reads right only when the
1217
+ body shape is sized to its content. On an oversized body shape
1218
+ top-anchoring leaves dead space underneath; centering balances it.
1219
+
1220
+ Content fills 55-65% of the safe area, no more. Empty space is a
1221
+ feature: it's what makes a deck feel confident rather than crowded.
1222
+ If a shape is mostly empty, shrink the shape - don't fill it.
1223
+
1224
+ Action titles versus topic titles. An action title states the
1225
+ claim ("Method X reduced error 40%") and reads like prose. A topic
1226
+ title labels what's below ("Methodology"). Both are fine; pick
1227
+ one. Action titles tend to wrap to two lines, which is why
1228
+ in-deck titles use subtitle role (40px) not title role (64px).
1229
+
1230
+ Body bullets should be parallel. If your bullets don't read like
1231
+ a list - if the items have different shapes, weights, or
1232
+ connective tissue (because, but, so) - write a sentence instead.
1233
+ A bulleted paragraph is hiding the fact that you haven't decided
1234
+ what you're claiming.
1235
+
1236
+ When in doubt, \`@extends\` a built-in template instead of
1237
+ composing from raw shapes. Run \`sdoc slides list\` to see the
1238
+ registry.
1239
+
1240
+ \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
1241
+ Wrap shape DSL in a ~~~slide fenced block:
1242
+
1243
+ ~~~slide
1244
+ grid 100 56.25
1245
+ r 5 5 90 15 fill=#1e40af color=#fff text=title | Q4 Review
1246
+ r 5 22 42 26 align=left |
1247
+ ## Wins
1248
+ - Shipped slides
1249
+ - Tilde fences
1250
+ ~~~
1251
+
1252
+ Tildes (not backticks) so shape content can include \`\`\` code blocks
1253
+ without closing the fence early. Triple-backtick \`\`\`slide also works
1254
+ (marked accepts either) - but prefer tildes so a nested \`\`\`python
1255
+ inside a shape doesn't end the slide block prematurely.
1256
+
1257
+ \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
1258
+ First line: \`grid W H [key=val ...]\`. Default 100 56.25 (16:9).
1259
+ All shape coordinates are in grid units.
1260
+ grid 100 56.25
1261
+ grid 100 75 (4:3)
1262
+ grid 100 100 (square)
1263
+ grid 100 56.25 bg=#0f172a (set slide background color)
1264
+
1265
+ W x H defines the aspect ratio and coordinate system, not a pixel size.
1266
+ Slides fill whatever space they're rendered into - inline thumbnail in
1267
+ a doc, small rail thumbnail in present mode, fullscreen stage, PDF page -
1268
+ and text auto-fits via container queries. Pick numbers for the aspect
1269
+ ratio you want; 100 on one axis is the convention, making the other
1270
+ axis a simple percentage.
1271
+
1272
+ \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
1273
+ Templates encode their layouts in shape DSL (\`r\` / \`p\` / \`c\` /
1274
+ \`e\` / \`l\` / \`a\` / \`chev\` / \`bub\` / \`cyl\`). As a consumer of a
1275
+ template, you don't see these - you fill slots via \`#name: value\`
1276
+ (see TEMPLATES below). If you're defining your own \`@template\`,
1277
+ or composing a custom slide from raw shapes, run:
1278
+
1279
+ sdoc slides custom-shapes Shape kinds, ids / @refs, layering,
1280
+ polygon gotchas, composite patterns
1281
+
1282
+ Three high-level shapes (\`chev\`, \`bub\`, \`cyl\`) ship with their
1283
+ text-centring rect already tuned to the visual body, not the
1284
+ bounding box. Reach for them before composing the equivalent
1285
+ polygon by hand - they save lines AND get the text positioning
1286
+ right by default.
1287
+
1288
+ \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
1289
+ The \`icon\` shape kind renders an inline outline icon from the
1290
+ bundled Lucide library (https://lucide.dev/icons/, ~1960 icons).
1291
+ Use it to mark concepts on a slide - a user, a database, a cloud,
1292
+ a lock - without resorting to emoji or raster images.
1293
+
1294
+ icon 8 30 6 6 name=user color=#0F1E3A
1295
+ icon 20 30 6 6 name=database
1296
+ icon 32 30 6 6 name=cloud-upload strokeWidth=2
1297
+
1298
+ Unknown names render a pink struck-through placeholder, so typos
1299
+ are visible rather than silent. To find an icon name, run:
1300
+
1301
+ sdoc slides icons List every available name
1302
+ sdoc slides icons cloud Filter by substring
1303
+
1304
+ Full attribute reference (color, strokeWidth, lazy-load behaviour)
1305
+ lives in \`sdoc slides custom-shapes\` under the \`icon\` shape entry.
1306
+
1307
+ \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
1308
+ Between geometry and \`|\`:
1309
+
1310
+ The text-related attributes below apply to BOTH the shapes a
1311
+ template defines and the slot content you write as a consumer.
1312
+ Visual attributes (fill, stroke, strokeWidth, radius, etc.) are
1313
+ documented in \`sdoc slides custom-shapes\`.
1314
+
1315
+ Padding:
1316
+ padding=N Inner padding in grid units (0 disables). On a
1317
+ 100-wide grid, \`padding=2\` is ~2% of slide width.
1318
+
1319
+ Alignment:
1320
+ align=<a> Horizontal: center (default), left, right
1321
+ valign=<v> Vertical: center (default), top, bottom
1322
+
1323
+ Text body box:
1324
+ textBox=x,y,w,h Override the rectangle that holds the shape's
1325
+ text content. Values are grid units, relative
1326
+ to the shape's bounding-box top-left. The shape
1327
+ itself (fill, stroke, geometry) is unchanged -
1328
+ only the text inside shifts. Use when the
1329
+ shape is asymmetric and centering text in its
1330
+ bounding box drifts the text off the visual
1331
+ mass.
1332
+
1333
+ The classic case: a right-pointing chevron
1334
+ polygon. Its bounding box includes the tip,
1335
+ so centred text sits to the right of the
1336
+ chevron body. Set textBox to the body region
1337
+ only:
1338
+
1339
+ p 6,20 19,20 22,24 19,28 6,28 9,24
1340
+ fill=#4f8aff textBox=0,0,14,8
1341
+ color=#fff text=subtitle | Tokenize
1342
+
1343
+ Bbox of the polygon is (6,20)-(22,28). The
1344
+ body (excluding the 3u tip) is 13 wide x 8
1345
+ tall starting at (6,20); textBox=0,0,14,8 puts
1346
+ text inside that region.
1347
+
1348
+ Text rotation:
1349
+ textAngle=N Rotate the text WITHIN the shape by N degrees.
1350
+ The shape's geometry (x, y, w, h) is untouched -
1351
+ only the text inside rotates. Useful for vertical
1352
+ axis labels, diagonal stamps, and skewed callouts.
1353
+ Default 0 (no rotation).
1354
+
1355
+ Cardinal angles (0, 90, -90, 180) "just work":
1356
+ - 90 / -90: text is laid out using the shape's
1357
+ SWAPPED dimensions, so "More autonomous" in a
1358
+ tall narrow shape wraps along the long axis
1359
+ and reads top-to-bottom (90) or
1360
+ bottom-to-top (-90).
1361
+ - 180: text is flipped in place.
1362
+
1363
+ Non-cardinal angles (e.g. textAngle=37) render
1364
+ but get NO dimension swap and NO autofit help.
1365
+ The text is rotated around the shape's centre;
1366
+ its visible extent will usually exceed the
1367
+ shape's (w, h) box. Size the container so the
1368
+ rotated text lands where you want:
1369
+ rotatedW = |w * cos N| + |h * sin N|
1370
+ rotatedH = |w * sin N| + |h * cos N|
1371
+ The shape's own bbox doesn't change - only the
1372
+ rendered text sticks out. Place neighbouring
1373
+ shapes accordingly, or accept the overlap as
1374
+ intentional.
1375
+
1376
+ Example (vertical y-axis label):
1377
+ r 8 16 4 32 textAngle=-90 text=caption
1378
+ align=center | More autonomous
1379
+
1380
+ Text sizing (role first, escape hatches second):
1381
+ text=<role> Pick a role from a fixed table. Roles give a
1382
+ deck consistent typography. Default is \`body\`.
1383
+ text=title 64px (slide titles)
1384
+ text=subtitle 40px (section heads, sub-titles)
1385
+ text=body 24px (default; paragraphs, bullets)
1386
+ text=caption 14px (footnotes, fine print)
1387
+ Unknown roles fall back to \`body\` silently.
1388
+
1389
+ size=Npx Literal size override; takes precedence over the
1390
+ role. Units: px | pt | em | rem (bare number = px).
1391
+ Use sparingly - the role table is what keeps the
1392
+ deck rhythm consistent.
1393
+ size=fit Opt into auto-fit: binary search for the largest
1394
+ font that fits the shape, capped at 12% of stage
1395
+ height (or the per-shape maxfont= value).
1396
+
1397
+ maxfont=Npx Caps \`size=fit\` higher or lower than the default
1398
+ stage cap. Has no effect when size= isn't \`fit\`.
1399
+
1400
+ Px values size as if the stage were 720px tall and scale proportionally
1401
+ in smaller views (rail thumbnails, inline thumbs), so \`size=18px\` reads
1402
+ as "18px on a fullscreen slide".
1403
+
1404
+ A deck that uses only roles (no \`size=\`) lands at 2-3 distinct font
1405
+ sizes across all slides, which is what makes presentations look
1406
+ professional. Reach for \`size=Npx\` only for hero numbers or other
1407
+ one-off treatments.
1408
+
1409
+ Per-element scale (applied inside the shape's shadow root):
1410
+ h1Scale=N h1 is N\u00d7 the shape's resolved font size.
1411
+ h2Scale=N h2 is N\u00d7 the shape's resolved font size.
1412
+ h3Scale=N, h4Scale=N, h5Scale=N, h6Scale=N
1413
+ pScale=N Scale paragraph text (default 1).
1414
+
1415
+ Each scale affects ONLY that element type. \`h1Scale=3\` enlarges
1416
+ h1 headings, leaves paragraphs alone. The shape's resolved font
1417
+ size (the role's px from \`text=\`, the \`size=Npx\` override, or the
1418
+ autofit output when \`size=fit\`) is the base for the multiplier.
1419
+
1420
+ Defaults without overrides: h1 1.4, h2 1.2, h3 1.05, h4-h6 1.0,
1421
+ p 1.0. Note that h4/h5/h6 render at the SAME size by default, so
1422
+ they don't give you three-step hierarchy out of the box - use
1423
+ explicit h4Scale/h5Scale/h6Scale if you need a h4>h5>h6 spread.
1424
+
1425
+ When to use: one shape holds mixed content (heading + body) and
1426
+ you want the heading BIGGER or the body SMALLER than the 1.4 / 1.0
1427
+ default. Two common patterns:
1428
+
1429
+ Hero number (giant + tiny caption, one shape):
1430
+ r 10 15 80 30 h1Scale=3 pScale=0.4 |
1431
+ # 87%
1432
+ of teams ship faster with one-page decks
1433
+
1434
+ Quote card (prominent body, small attribution):
1435
+ r 10 15 80 30 h2Scale=0.5 |
1436
+ Quote text in body size here.
1437
+ ## - Attribution
1438
+
1439
+ Invalid or \u2264 0 values are ignored (fallback to defaults).
1440
+
1441
+ Identification:
1442
+ #id Reference target for @refs
1443
+
1444
+ Stacking:
1445
+ layer=<v> top | mid | bottom. Default \`mid\` for every
1446
+ kind. Source order alone normally decides paint
1447
+ order; \`layer=\` is an escape hatch for shapes
1448
+ that must sit on top of (or below) everything
1449
+ regardless of where they were declared. See
1450
+ STACKING section below.
1451
+
1452
+ \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
1453
+ Everything after \`|\` is standard markdown. Multi-line uses
1454
+ indentation under the shape line - continuation lines MUST be
1455
+ indented at least 2 spaces, or the parser treats them as fresh
1456
+ top-level shape lines (and fails).
1457
+
1458
+ r 5 20 90 60 align=left |
1459
+ ## Heading
1460
+ Some body paragraph.
1461
+
1462
+ - list item one
1463
+ - list item two
1464
+
1465
+ \`\`\`python
1466
+ def hi():
1467
+ print("hello")
1468
+ \`\`\`
1469
+
1470
+ Prefer putting the \`|\` on its own line (empty) with all content
1471
+ indented below. Mixing "first line after |" with unindented lines
1472
+ is a common parser error.
1473
+
1474
+ \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
1475
+ Default: \`align=center valign=center\`. Good for:
1476
+ - Title shapes (one heading or phrase)
1477
+ - Subheaders, section labels
1478
+ - Standalone caption text
1479
+
1480
+ Switch to \`align=left\` when the shape holds body copy - paragraphs,
1481
+ bullet lists, numbered lists, blockquotes. Left-aligned reads better
1482
+ once you have multiple lines. Keep \`valign=center\` (the default) so
1483
+ the block floats in the middle of the shape; switch to \`valign=top\`
1484
+ only when the shape is sized exactly to the content and you want it
1485
+ anchored to the top.
1486
+
1487
+ Rule of thumb:
1488
+ ONE short phrase \u2192 leave centered
1489
+ MULTIPLE lines \u2192 align=left, keep valign=center
1490
+
1491
+ \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
1492
+ Slides pick up the host document's styles so a deck feels visually
1493
+ part of its doc. Two mechanisms, both resolved at render time against
1494
+ the active theme (so dark mode just works).
1495
+
1496
+ AUTOMATIC INHERITANCE (no DSL needed)
1497
+ - Slide background = styles.background (unless grid has bg=)
1498
+ - Body font = styles.fontFamily
1499
+ - Heading fonts = styles.headers.fontFamily
1500
+ - Code / pre / link = their respective styles.* values
1501
+ - Heading text inside a shape's markdown content adopts the doc's
1502
+ h1/h2/h3/h4 color. Example: \`# Title\` in a shape uses styles.h1.color.
1503
+ A shape's own \`color=\` always overrides this.
1504
+
1505
+ EXPLICIT REFERENCES ($path.to.prop)
1506
+ Any shape or grid attribute value can be a \`\$path.to.prop\` token,
1507
+ which resolves to the doc's live value for that style. Common cases:
1508
+ \`fill=\$h1.color\`, \`color=\$chart.accent\`, grid \`bg=\$background\`.
1509
+ Vocabulary = the YAML schema (run \`sdoc schema\`).
1510
+
1511
+ r 5 5 90 15 fill=\$h1.color color=#fff text=title | Title
1512
+ r 5 25 90 25 color=\$chart.accent | ## 40% growth
1513
+ grid 100 56.25 bg=\$blocks.background (subtle block-tinted slide)
1514
+
1515
+ Supported paths:
1516
+ \$background \$color \$fontFamily
1517
+ \$h.color \$h1.color \$h2.color \$h3.color \$h4.color
1518
+ \$headers.color \$headers.fontFamily
1519
+ \$p.color \$list.color \$link.color
1520
+ \$blocks.background \$blocks.color
1521
+ \$code.background \$code.color \$code.font
1522
+ \$blockquote.background \$blockquote.color \$blockquote.borderColor
1523
+ \$chart.accent \$chart.background \$chart.textColor
1524
+
1525
+ Unknown paths surface in the error badge with the rest of the
1526
+ diagnostics. Literal hex (#1e40af) still works when you want a
1527
+ one-off color that isn't in the doc's styles.
1528
+
1529
+ Blockquote-style card (tinted bg + left accent border): use a
1530
+ markdown \`>\` inside any shape. The quote picks up the doc's
1531
+ blockquote styling automatically - no \`stroke=\` workaround needed.
1532
+ r 50 15 42 30 padding=3 |
1533
+ > Our customers are the product
1534
+ >
1535
+ > - Jordan, CEO
1536
+
1537
+ \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
1538
+ Any shape (\`r\`, \`c\`, \`p\`) can hold a bitmap via \`image=<url>\`.
1539
+ The URL accepts whatever the browser would load for a standard
1540
+ markdown \`![]()\`:
1541
+
1542
+ - \`data:\` URIs inlined in the DSL, no network fetch
1543
+ - \`https://\` URLs fetched at render time (CORS applies)
1544
+
1545
+ SmallDocs does not host image bytes. Data URIs live in your document
1546
+ (and share URL); external URLs fetch from whatever host you pick.
1547
+
1548
+ Attributes (apply to every shape kind):
1549
+
1550
+ image=<url> the bitmap source
1551
+ imageFit=cover|contain cover (default) fills the shape box and
1552
+ crops overflow; contain preserves aspect
1553
+ and letterboxes inside the shape
1554
+ imagePos=center|top|bottom|left|right
1555
+ which edge is pinned when cover crops or
1556
+ contain letterboxes; default center
1557
+
1558
+ Stacking inside a shape (bottom to top):
1559
+ 1. \`fill=\` colour backdrop
1560
+ 2. \`image=\` bitmap (shows fill through image alpha / on load fail)
1561
+ 3. \`stroke=\` border
1562
+ 4. \`| content\` markdown text
1563
+
1564
+ Examples:
1565
+
1566
+ # Small corner logo on a rect
1567
+ r 14 0.5 1.5 1 image=https://lucide.dev/logo.light.svg imageFit=contain
1568
+
1569
+ # Full-bleed hero with a title overlay (one shape, not two)
1570
+ r 0 0 16 9 image=data:image/png;base64,iVBORw0K... align=left valign=bottom padding=0.5 |
1571
+ # Q4 review
1572
+
1573
+ # Photo clipped inside a hand-drawn polygon
1574
+ p 3,1 13,1 14,5 10,8 6,8 2,5 image=https://example.com/team.jpg
1575
+
1576
+ # Circle avatar with a gold ring (stroke paints above image)
1577
+ c 8 4.5 2.2 image=/avatar.jpg stroke=#d4af37 strokeWidth=0.12
1578
+
1579
+ Shorthand: \`i x y w h src=<url>\` is parser sugar for
1580
+ \`r x y w h image=<url>\`, a friendly keystroke for the common case.
1581
+ \`src=\` is treated as a valid alias for \`image=\` everywhere.
1582
+
1583
+ PDF export embeds PNG and JPEG natively. SVG / WebP / GIF are
1584
+ skipped silently (console warning is logged). External URL fetches
1585
+ need CORS headers on the host, same constraint as any browser fetch.
1586
+
1587
+ \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
1588
+ Source order = paint order. A shape declared later in the slide
1589
+ paints above shapes declared earlier - regardless of whether it is
1590
+ a rectangle, polygon, circle, line, or arrow. To put a connector
1591
+ above its cards, declare the arrow after the cards. To put a
1592
+ backdrop behind a card, declare the backdrop first.
1593
+
1594
+ Escape hatch: \`layer=top | mid | bottom\` (default \`mid\`) promotes
1595
+ or demotes a shape across coarse sublayers, overriding source order.
1596
+ Useful inside templates where a consumer slide adds more shapes
1597
+ whose declaration order you cannot predict. See the LAYERING
1598
+ section of \`sdoc slides custom-shapes\` for the full model.
1599
+
1600
+ \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
1601
+ Define a shape layout once, reuse across slides. Two directives,
1602
+ each must be the first non-blank line inside a slide block:
1603
+
1604
+ @template NAME Register this slide's DSL as a template.
1605
+ The slide DOES NOT RENDER - it's a definition.
1606
+ @extends NAME Inherit NAME's shapes; fill slot content below.
1607
+
1608
+ Shapes in the template carry an \`#id\` to mark slots. Consumers
1609
+ override those slots with \`#id: value\` blocks (inline for a single
1610
+ line, colon-only + following lines for multi-line content).
1611
+
1612
+ Author ordering doesn't matter - the consumer can appear before or
1613
+ after the template in the document. Templates never render, so they
1614
+ don't show up in the thumbnail flow or present mode.
1615
+
1616
+ Example:
1617
+
1618
+ ~~~slide
1619
+ @template title-body
1620
+ grid 16 9
1621
+ r 0 0 16 3 #title fill=\$h1.color color=#fff text=title | placeholder title
1622
+ r 0 3 16 6 #body align=left |
1623
+ placeholder body
1624
+ ~~~
1625
+
1626
+ ~~~slide
1627
+ @extends title-body
1628
+ #title: What is SmallDocs?
1629
+ #body:
1630
+ - Markdown in, styled docs out
1631
+ - No server, hash-only state
1632
+ - Slides from fenced blocks
1633
+ ~~~
1634
+
1635
+ ~~~slide
1636
+ @extends title-body
1637
+ #title: Why templates
1638
+ #body: Define shape once, fill slots N times. Recolor once in front
1639
+ matter and every slide that uses the template picks it up.
1640
+ ~~~
1641
+
1642
+ Partial fills: if a consumer omits an optional slot (provides
1643
+ \`#title\` but not \`#body\`), the template's placeholder content
1644
+ stays - so templates are self-documenting when first authored.
1645
+
1646
+ Required slots: mark a slot with a trailing \`!\` (e.g. \`#title!\`)
1647
+ in the template. If a consumer omits a required slot, the resolver
1648
+ surfaces an error in the slide's error badge. Optional slots have
1649
+ no marker.
1650
+
1651
+ Unknown template names, slots that don't match any shape id in the
1652
+ template, and missing required slots all surface in the error badge
1653
+ alongside any DSL errors.
1654
+
1655
+ Deliberately simple in v1: no attribute overrides (can't change
1656
+ \`fill=\` per consumer - fork the template if you need variants),
1657
+ no nested templates (a consumer can't extend another consumer).
1658
+
1659
+ ── BUILT-IN TEMPLATES ───────────────────────────────────
1660
+ SmallDocs ships a small library of opinionated templates. \`@extends\`
1661
+ any of them without declaring a user \`@template\` first. Run
1662
+ \`sdoc slides list\` for the names + slot lists; the templates all
1663
+ use a 1-unit safe margin and avoid full-bleed coloured bars (the
1664
+ one exception is \`section\`, which uses \`grid bg=\` for contrast
1665
+ between deck sections).
1666
+
1667
+ cover
1668
+ Opening slide of a deck. Once per deck. Sets the tone before
1669
+ anything else.
1670
+
1671
+ title-body
1672
+ The workhorse for content slides - 60-70% of slides in any
1673
+ real deck. Title at top in subtitle role (40px) so an action
1674
+ title can wrap to two lines without crowding the body.
1675
+ Optional footer slot for source, page number, or context.
1676
+
1677
+ two-column
1678
+ Compare / contrast (before/after, A vs B, problem / solution).
1679
+ A 1-unit gutter splits the columns; optional column headers
1680
+ above each. Bodies anchor top so matched-length content reads
1681
+ as parallel - keep both columns roughly the same length, or
1682
+ switch to title-body and explain the asymmetry in prose.
1683
+
1684
+ three-column
1685
+ Three-way compare. A/B/C variants, before/during/after, three
1686
+ perspectives on the same question. Equal columns separated by
1687
+ a small gutter; optional headers above each. Bias toward
1688
+ keeping all three columns roughly the same length - if one is
1689
+ half-empty, drop it and use two-column instead.
1690
+
1691
+ exhibit
1692
+ Chart on the left (~64% of safe area), takeaway column on the
1693
+ right (~32%), optional source caption underneath, required
1694
+ action title at the top. The chart is the evidence; the
1695
+ takeaway tells the audience what to see. Reserve for business
1696
+ decks where the audience needs a verbal handle on the chart
1697
+ under time pressure. Sibling templates: \`image-and-text\`
1698
+ when image and body should read as balanced peers (54/46,
1699
+ title optional and small); \`figure-hero\` when the chart
1700
+ should fill the slide with no right column at all.
1701
+
1702
+ image-and-text
1703
+ Image on the left (~54% of safe area), supporting body on
1704
+ the right (~43%), balanced. Optional small caption-style
1705
+ title at the top. Use when the image and the body are about
1706
+ equal in weight - "here's a thing + here's what it is".
1707
+ Sibling templates: \`exhibit\` when the chart should
1708
+ dominate and the right column is a narrow set of takeaway
1709
+ bullets; \`figure-hero\` when the image IS the argument
1710
+ (no body column). The image slot accepts markdown image
1711
+ syntax: \`#image: ![alt](url)\`.
1712
+
1713
+ figure-hero
1714
+ Image-dominant slide. The figure carries the whole slide
1715
+ and a small caption sits below; no body or takeaway column.
1716
+ The workhorse for research talks and any deck where a chart,
1717
+ screenshot, or photograph is the point. Sibling templates:
1718
+ \`image-and-text\` when you also want a body column;
1719
+ \`exhibit\` when you want a chart plus a narrow takeaway
1720
+ column for a business audience.
1721
+
1722
+ quote
1723
+ Single big idea, customer voice, or a manifesto sentence.
1724
+ Centered both axes so short content sits balanced rather
1725
+ than drifting top-left. Use for sentences, not numbers -
1726
+ reach for \`metric\` when the slide IS a number.
1727
+
1728
+ metric
1729
+ One hero number plus a line of context. \`size=fit\` lets the
1730
+ number scale to its shape; \`maxfont=300px\` is baked in so it
1731
+ can actually feel hero-sized. One short sentence in the
1732
+ \`context\` slot is the rule, not three - more text fights
1733
+ the number for attention and the slide stops feeling like a
1734
+ headline. Use sparingly: one metric slide per deck is the
1735
+ pattern, not three.
1736
+
1737
+ section
1738
+ Section divider between deck parts. The one template that
1739
+ uses a full-bleed background (via \`grid bg=\`) - the contrast
1740
+ against content slides is what signals "we're switching
1741
+ gears" to the audience. Shape fills are still avoided
1742
+ (Consultant-2 rule); only the slide bg is coloured.
1743
+
1744
+ closing
1745
+ Quiet bookend at the end of a deck. Center-aligned, minimal.
1746
+ Don't write "Thanks for listening" or "Questions?" here -
1747
+ both signal "I've run out of content" and the room tunes
1748
+ out. Pick something the audience will remember instead
1749
+ ("Start this week, not next", "Boring is the goal", the
1750
+ one number that summarises the deck, etc.). The contact
1751
+ slot is for one short line of channels, not a paragraph.
1752
+
1753
+ A user \`@template <name>\` with the same name as a built-in overrides
1754
+ it for the rest of the document, with a warning surfaced on the
1755
+ template's slide. So shipping a custom \`title-body\` is fine - the
1756
+ resolver just lets you know the stdlib version got shadowed.
1757
+
1758
+ \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
1759
+ A slide with parse/render errors shows a red badge at the bottom
1760
+ of the thumbnail listing every problem by line number. The badge
1761
+ has a "Copy" button that puts a diagnostic on the clipboard —
1762
+ errors + the full slide source — for pasting back to an agent.
1763
+
1764
+ Common errors:
1765
+ - "shape extends outside grid WxH" y+h > H (or x+w > W)
1766
+ - \`unknown id "@name"\` @-ref before the shape is declared
1767
+ - \`duplicate id "#name"\` two shapes share an id
1768
+ - \`invalid attribute key\` key must start with a letter
1769
+
1770
+ \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
1771
+ - Templates: no per-consumer attribute overrides; no nested templates.
1772
+ - Arrows draw as straight lines; no routing around other shapes.
1773
+ - No drag/resize edit mode yet; shapes are authored by typing DSL.
1774
+ `;
1775
+
1776
+ const SLIDES_CUSTOM_SHAPES_HELP = `
1777
+ SmallDocs — Slides (raw shapes)
1778
+ ===========================
1779
+ Reference + design notes for slides built from raw shapes rather than
1780
+ the stdlib templates. Most decks won't need any of this - if you can
1781
+ express the slide via \`@extends\` on a built-in (cover, title-body,
1782
+ two-column, exhibit, etc.), do that.
1783
+
1784
+ Read DESIGN PRINCIPLES first. The syntax reference below assumes you've
1785
+ made the visual choices the principles describe. Without them, raw
1786
+ shapes consistently produce decks that read as "default PowerPoint"
1787
+ rather than as designed.
1788
+
1789
+ ── DESIGN PRINCIPLES ─────────────────────────────────
1790
+ Raw shapes give geometric freedom that templates don't. That
1791
+ freedom is also rope. The notes below are what separate a deck
1792
+ that reads as designed from one that reads as amateur.
1793
+ Internalise them before reaching for the syntax.
1794
+
1795
+ Stroke.
1796
+ Default: NO stroke. Reads modern and confident. The slide
1797
+ background carries the silhouette via fill or whitespace; shapes
1798
+ don't compete with their own outlines.
1799
+
1800
+ Thin neutral stroke (\`strokeWidth\` <= 0.03, \`stroke=#94a3b8\` or
1801
+ a similar cool grey). Reads technical, detail-oriented. Reach
1802
+ for this when several shapes' contours need to be visible AND
1803
+ the fill contrast isn't doing the work on its own. Good for
1804
+ matrices, small multiples, schematic diagrams.
1805
+
1806
+ Thick coloured stroke. AVOID. The strongest tell that a deck
1807
+ wasn't designed. It almost never improves the slide. If a shape
1808
+ needs a thick coloured border to read, the geometry or fill is
1809
+ doing too little work. The only exception is a deliberately
1810
+ drawn arrow or callout where the line itself IS the message;
1811
+ even then, take the colour from the doc accent, not from a
1812
+ third hue.
1813
+
1814
+ Fill.
1815
+ Default: NO fill. Most shapes don't need one - the slide
1816
+ background reads through and the silhouette is implicit.
1817
+
1818
+ Subtle tint (\`#eef2ff\`, \`#f8fafc\`, or similar near-background
1819
+ values). For containers that hold body content - a card around
1820
+ a paragraph, a column header strip. The tint should look like a
1821
+ faint shadow, not a coloured panel.
1822
+
1823
+ Saturated fill. Reserved for the ONE focal element per slide -
1824
+ the navy cell on a segmentation matrix, the SOM tier on a
1825
+ market-sizing diagram, the focal band on a funnel. Saturated
1826
+ fill is your single "look here" gesture. Spend it once.
1827
+
1828
+ Typography-only is not a custom shape.
1829
+ A shape with neither stroke nor fill is invisible - it reads as
1830
+ floating text. If you're not encoding geometry (a position, a
1831
+ size, a relationship, a comparison), you're writing an
1832
+ annotation, and an annotation belongs in a plain text shape or
1833
+ in markdown content. The point of a custom shape is the shape;
1834
+ if it isn't visible, drop it and use a template slot.
1835
+
1836
+ One deviation per slide.
1837
+ Repetition + deviation = recognition. If five shapes share a
1838
+ treatment and the sixth doesn't, the eye lands on the sixth
1839
+ before reading a single label. The deviation IS the slide.
1840
+ Wanting two deviations is usually wanting two slides.
1841
+
1842
+ Shape vocabulary.
1843
+ Limit to two shape primitives per deck - typically a rectangle
1844
+ for cards / containers, plus one polygon (or circle / ellipse)
1845
+ as the variant. Using one primitive consistently across slides
1846
+ builds a visual language the audience learns by slide three.
1847
+ Six different shapes used once each flat-lines that recognition
1848
+ and reads as ornament rather than system.
1849
+
1850
+ Geometry as data.
1851
+ Where a shape's size, position, slope, or area corresponds to a
1852
+ number in the content, make it accurate. SOM at 4.7% of TAM
1853
+ should occupy 4.7% of TAM's area, not 30% because that's what
1854
+ fits the layout. A funnel band's width should be proportional
1855
+ to its population, not chosen for visual balance. Where shapes
1856
+ carry data, geometry IS the argument; labels confirm it.
1857
+
1858
+ When the magnitudes span more than ~50x and a linear scale
1859
+ collapses the tail to a hairline, two honest positions:
1860
+ a) Linear scale. Let the tail be a hairline. The
1861
+ disappearance IS the data (250k next to 28M looks like
1862
+ what 250k actually is next to 28M).
1863
+ b) Square-root or log scale. Readable across the range; state
1864
+ which you used in a caption so the geometry isn't lying.
1865
+ Either is fine. Pick deliberately. Don't fudge a linear scale
1866
+ into "what looks good" - that is lying with shapes.
1867
+
1868
+ Labels outside the shape when the shape is too narrow.
1869
+ A magnitude-proportional shape will sometimes be smaller than
1870
+ its label. Pull the label outside (column-aligned, or with a
1871
+ short leader line) - shrinking the label to fit a hairline
1872
+ shape destroys the only data the shape was carrying. See the
1873
+ TEXT INSIDE NON-RECT SHAPES section for the mechanics.
1874
+
1875
+ Visual rhymes across slides.
1876
+ A shared element that recurs on every custom-shape slide - a
1877
+ horizontal rule at a consistent y, a footer caption pinned to
1878
+ the same line, an accent colour reserved for one role - is
1879
+ what makes a custom-shape deck feel deliberate rather than
1880
+ improvised. Pick one or two such rhymes and hold them across
1881
+ every slide you author.
1882
+
1883
+ Restraint over ornament.
1884
+ The decision to ADD any visual element should require a reason.
1885
+ No fill, no stroke, no extra shape, no second colour is the
1886
+ default. Spend visual weight only on the one or two things the
1887
+ slide is about. Empty space is half the design.
1888
+
1889
+ ── SHAPE KINDS ───────────────────────────────────────
1890
+ r x y w h rectangle (x,y = top-left; w,h = size)
1891
+ i x y w h image rect (sugar for \`r\` with \`image=\`; see IMAGES)
1892
+ c cx cy radius circle (cx,cy = center)
1893
+ e cx cy rx ry ellipse (cx,cy = center; rx,ry = half-sizes)
1894
+ l x1 y1 x2 y2 line (decorative, no content)
1895
+ a x1 y1 x2 y2 arrow (decorative, tip lands on (x2,y2);
1896
+ accepts \`^h\` between endpoints to bow)
1897
+ p x1,y1 x2,y2 ... polygon (segment operators between points:
1898
+ ~ ^h >P * P1 P2; see below)
1899
+ chev x y w h chevron / arrow-block (x,y = top-left; w,h = bbox
1900
+ including the pointed right tip).
1901
+ Text centres on the BODY (excluding
1902
+ tip), not the bbox.
1903
+ Attrs: tip=N (tip width, default
1904
+ min(h/2, w*0.25)); notch=N (V-shaped
1905
+ left edge for interlocking chevrons,
1906
+ default 0).
1907
+ bub x y w h speech bubble / callout. Rounded rect body at
1908
+ (x,y,w,h); the tail points from the
1909
+ nearest edge to \`tail=tx,ty\` (target
1910
+ point in grid units, REQUIRED).
1911
+ Text centres in the body; the tail
1912
+ does not displace text.
1913
+ Attrs:
1914
+ tail=tx,ty (required)
1915
+ radius=N (corner radius, default 1)
1916
+ tailStyle=concave (default) | sharp | wide
1917
+ concave: sides curve toward tail
1918
+ axis (elegant horn /
1919
+ teardrop, reads as
1920
+ designed)
1921
+ sharp: straight narrow triangle
1922
+ (schematic, "needle")
1923
+ wide: straight broader triangle
1924
+ (friendly, generic
1925
+ callout)
1926
+ cyl x y w h cylinder (database / storage shape). Top and
1927
+ bottom ellipse caps each take \`lip\`
1928
+ height (default ~20% of h, capped
1929
+ by w). Text centres in the body
1930
+ between the caps.
1931
+ Attrs: lip=N (cap height).
1932
+ tab x y w h folder / step-marker tab. Body rectangle with
1933
+ a smaller rectangular tab on top-left
1934
+ joined by a slope. Text centres in
1935
+ the body (below the tab).
1936
+ Attrs: tabW=N (tab width, default
1937
+ ~38% of w); tabH=N (tab height,
1938
+ default min(0.22*h, 0.18*w)).
1939
+ doc x y w h document / page with a folded top-right corner.
1940
+ Text centres in the body; the fold
1941
+ triangle is rendered as an 18% black
1942
+ overlay so the corner reads as a 3D
1943
+ fold over any fill colour.
1944
+ Attrs: fold=N (fold size, default
1945
+ 15% of min(w, h)).
1946
+ cloud x y w h cloud shape (abstract source / sink for arch
1947
+ diagrams). Single SVG path adapted
1948
+ from a real icon library; no internal
1949
+ seams. Text centres in an inset body
1950
+ (~15% horizontal, 20% vertical) so
1951
+ it doesn't float over the bumps.
1952
+ Attrs: variant=heroicons (default,
1953
+ sharp / modern) | material (puffy /
1954
+ friendly) | bootstrap (smooth /
1955
+ balanced).
1956
+ icon x y w h inline icon from the bundled Lucide library
1957
+ (~1960 icons). Outline-only, no fill
1958
+ by default; stroke colour from
1959
+ \`color=\` attribute.
1960
+ Attrs:
1961
+ name=<icon> (required) - any name
1962
+ from the bundled Lucide
1963
+ set. Run
1964
+ \`sdoc slides icons\` to
1965
+ list everything, or
1966
+ \`sdoc slides icons cloud\`
1967
+ to filter by substring.
1968
+ Common ones: user,
1969
+ database, server, lock,
1970
+ cloud, search,
1971
+ trending-up. Unknown
1972
+ names render a pink
1973
+ struck-through rect
1974
+ placeholder so typos
1975
+ are visible.
1976
+ color=<hex> stroke colour
1977
+ (default #0F1E3A).
1978
+ strokeWidth=N outline width
1979
+ (default 2; Lucide
1980
+ convention).
1981
+ The full icon bundle (~81 KB gzipped)
1982
+ is lazy-loaded on first use, then
1983
+ cached for the session. Until the
1984
+ bundle arrives, a dashed-rect
1985
+ placeholder marks each icon position
1986
+ so the layout doesn't reflow when
1987
+ icons swap in.
1988
+
1989
+ Arrow geometry: the coordinates are the line's centerline. The head is
1990
+ symmetric around the line, extending up to 3 * strokeWidth perpendicular
1991
+ on each side. So a horizontal arrow at y=5.78 with strokeWidth=0.06
1992
+ lines up cleanly with a horizontal line at y=5.78 (same centerline) -
1993
+ no need to offset the arrow's y to "clear" the head. For arrows shorter
1994
+ than 12 * strokeWidth the renderer scales the stroke (and head) down so
1995
+ the tip stays on (x2,y2); the arrow renders thinner than declared, but
1996
+ the endpoints stay honest.
1997
+
1998
+ Polygon segment operators (between adjacent point tokens).
1999
+ All curve operators use through-point semantics: the value you write
2000
+ is a point the curve actually passes through, not a hidden SVG control
2001
+ point. "Place the dot where you want the curve to go" works.
2002
+
2003
+ (none) straight line from previous point
2004
+ ~ soft bow with default sagitta = 10% of chord length
2005
+ (shorthand for a gentle ^h; use ^h when you need a
2006
+ specific bow depth)
2007
+ ^h arc / bow by sagitta h perpendicular to the chord;
2008
+ h is the actual peak height at t=0.5. Positive h bows
2009
+ to the LEFT of direction-of-travel (for a rightward
2010
+ chord, that is upward)
2011
+ >P quadratic Bezier whose midpoint passes through P
2012
+ (P is \`x,y\` or \`@ref\`; attached: \`>5,3\` / \`>@card.top\`)
2013
+ * P1 P2 cubic Bezier passing through P1 at t=1/3 and P2 at
2014
+ t=2/3 (P1, P2 each \`x,y\` or @ref). For predictable
2015
+ results, keep P1 / P2 within ~20% of chord length
2016
+ perpendicular to the chord; the closed-form cubic
2017
+ through both points uses SVG controls that amplify
2018
+ the offset ~4x, so far-from-chord through-points
2019
+ overshoot dramatically beyond the curve itself.
2020
+
2021
+ Polygon point modifiers (attach to the next point, not to an edge):
2022
+
2023
+ (r round the corner at the next point with radius r.
2024
+ Walks each adjacent edge back by r / tan(half-angle),
2025
+ replaces the sharp vertex with a circular arc tangent
2026
+ to both edges. Only takes effect when both adjacent
2027
+ segments are straight; silently no-ops if either is
2028
+ curved (~, ^, >, *). If r would consume more than half
2029
+ of either neighbouring chord, it shrinks to fit so
2030
+ adjacent rounded corners cannot overlap.
2031
+
2032
+ Example, all four corners of a card softened:
2033
+ p (0.4 0,0 (0.4 8,0 (0.4 8,5 (0.4 0,5 fill=#dbeafe
2034
+
2035
+ The same \`^h\` operator works between an arrow's two endpoints to bow
2036
+ the arrow into a curve:
2037
+ a 2 5 ^0.8 12 5 (rightward arrow bowing upward by 0.8u)
2038
+ a @plan.right ^-0.5 @ship.left (gentle downward bow between two shapes)
2039
+
2040
+ Polygon points are written \`x,y\` (one token per point), not space-
2041
+ separated like \`r x y w h\`. The variable point count needs a delimiter,
2042
+ so a comma is required inside each point.
2043
+
2044
+ All shapes EXCEPT \`l\` and \`a\` can hold markdown after \`|\` - full
2045
+ markdown (headings, lists, bold/italic, code, blockquote, tables).
2046
+ Non-rectangle shapes use their bounding box as the text area (see
2047
+ the TEXT INSIDE NON-RECT SHAPES section).
2048
+
2049
+ No \`fill=\` on a shape -> transparent (slide background shows through).
2050
+ Color values accept any CSS colour: hex (#1e40af), named (tomato),
2051
+ rgb(...), rgba(...).
2052
+
2053
+ Polygon examples:
2054
+ p 50,10 90,50 10,50 | Triangle
2055
+ p 10,10 90,10 ~ 90,50 10,50 | Rounded right edge (the ~ before
2056
+ a point softens that segment)
2057
+ p 2,6 ^0.8 9,6 9,8 2,8 fill=#e9d4a6
2058
+ (loaf-shaped card: arched top, three
2059
+ straight sides. Sagitta 0.8 sets the
2060
+ dome height in grid units)
2061
+ p 1,5 >5,1 9,5 1,8 fill=#dbeafe
2062
+ (one quadratic control point at (5,1)
2063
+ pulls the top edge into a peak)
2064
+ p 0,5 * 4,0 8,10 12,5 fill=#fee2e2
2065
+ (cubic with two controls: classic S-curve
2066
+ signature - rises early, falls late)
2067
+ p 10,20 60,20 60,10 90,30 60,50 60,40 10,40 | Next steps
2068
+ (right-pointing arrow shape - text
2069
+ renders in the polygon's bounding box)
2070
+
2071
+ ── COMMON PITFALLS ──────────────────────────────────
2072
+
2073
+ 1. \`^h\` is perpendicular to the chord, NOT vertical.
2074
+
2075
+ The docs note that "for a rightward chord, positive bows upward"
2076
+ - true, but for any other chord direction the bow follows the
2077
+ PERPENDICULAR. To dome the top of a polygon across a slanted
2078
+ span, use ONE \`^h\` across the whole top, not two arcs meeting
2079
+ at an apex:
2080
+
2081
+ p 3,7 ^1.8 13,7 13,8 3,8 smooth dome
2082
+ p 3,7 ^1.8 8,2.5 ^-1.8 13,7 13,8 3,8 NOT a dome
2083
+
2084
+ The second form has slanted chords (3,7)->(8,2.5) and
2085
+ (8,2.5)->(13,7). Positive \`h\` bows perpendicular-left of each
2086
+ chord direction, which points AWAY from the would-be apex - the
2087
+ arcs flare outward and meet at a sharp peak, not a smooth dome.
2088
+
2089
+ 2. Polygon points use \`x,y\` (one comma-separated token per point).
2090
+
2091
+ Rectangles, lines, and arrows use space-separated coords (\`r x y
2092
+ w h\`, \`a x1 y1 x2 y2\`). Polygons need an in-token delimiter so
2093
+ the parser knows where one point ends and the next begins:
2094
+
2095
+ p 2,7 8,3 13,7 fill=... YES
2096
+ p 2 7 8 3 13 7 fill=... parse error
2097
+
2098
+ The parser flags this with a clear "polygon: points use 'x,y'"
2099
+ error pointing to the offending line.
2100
+
2101
+ 3. Shapes don't auto-flow - overlapping bboxes paint over each
2102
+ other.
2103
+
2104
+ Every shape reserves its full (x, y, w, h). Source order is
2105
+ paint order, so a later shape with a fill that overlaps an
2106
+ earlier text shape's bbox covers that text:
2107
+
2108
+ r 6 9 88 5 text=subtitle | Three nested envelopes
2109
+ r 10 12 60 40 fill=#f1f5f9 <- starts at y=12, inside the
2110
+ subtitle bbox (y=9..14);
2111
+ subtitle hidden from y=12 down
2112
+
2113
+ The fix: place the next shape at y + h or later (here, y >= 14).
2114
+ The standard pattern from the gallery is subtitle at \`y=9 h=5\`,
2115
+ so the first content shape goes at y >= 14 (15 is the round
2116
+ number most templates use).
2117
+
2118
+ For \`text=title\` and \`text=subtitle\` add another ~0.6 grid
2119
+ unit because the glyph itself extends below the baseline. A
2120
+ text shape's \`h\` is the layout rect, not a hard clip - letters
2121
+ with descenders (g, p, q, y, j) draw about 20% of the font
2122
+ size below the baseline, which would hang into a perfectly
2123
+ bbox-aligned neighbour:
2124
+
2125
+ r 6 9 88 5 text=subtitle | The team after the Q1 reorg
2126
+ r 40 14 20 8 fill=#1e40af | CEO <- the g in "reorg"
2127
+ still crosses into the
2128
+ top of this box
2129
+
2130
+ Two safe budgets:
2131
+ a) 1 full grid unit of clearance below a text=subtitle row
2132
+ (next shape at y >= 15), or
2133
+ b) at least \`0.2 * fontSize\` in grid units (\`text=subtitle\`
2134
+ is 40px on REF_H=720, so ~0.6 grid units on a 56.25-tall
2135
+ grid).
2136
+
2137
+ The gotcha is loudest under \`text=title\` and \`text=subtitle\`
2138
+ because the font is large; \`text=body\` and \`text=caption\` rarely
2139
+ trip on it.
2140
+
2141
+ Circles and ellipses paint only the inscribed disc, but their
2142
+ bbox still claims the full (cx-r, cy-r, 2r, 2r). A label rect
2143
+ that overlaps the bbox CORNERS (outside the disc) is fine; a
2144
+ label rect that crosses the disc itself gets painted over,
2145
+ even when the label's own bbox doesn't fully overlap the
2146
+ bubble's bbox. The safe rule for bubble + label compositions:
2147
+ place the label's nearest edge at distance >= r + 0.5 grid
2148
+ units from the bubble centre. Common patterns that work:
2149
+ a) label centred BELOW the bubble at y = cy + r + 0.5
2150
+ b) label to the side at x = cx + r + 0.5 (left edge of label)
2151
+ Sizing the label rect to the bubble's bbox - e.g. r 76 18 12 3
2152
+ next to c 84 18 2.4 - puts the text directly under the disc
2153
+ and the bubble fill covers the end of the label.
2154
+
2155
+ 4. Code blocks inside shapes look randomly sized across siblings.
2156
+
2157
+ A \`\`\`fenced code block\`\`\` inside a slide shape sizes its OWN
2158
+ font and padding based on its content, not on the surrounding
2159
+ shape. Three sibling shapes with identical \`r ... 14 8\` end
2160
+ up looking different because their code samples differ in
2161
+ line count and line length. The cards then read as a sloppy
2162
+ row instead of a parallel set.
2163
+
2164
+ The fix is discipline at author-time:
2165
+
2166
+ a) Give every code-bearing shape the SAME (w, h). Don't let
2167
+ the longest sample dictate one card's size and the
2168
+ shortest dictate another.
2169
+ b) Set an explicit \`size=Npx\` on every code-bearing shape
2170
+ so the font matches across the row. Auto-fit picks per
2171
+ shape, which is exactly what you don't want for sibling
2172
+ comparison.
2173
+ c) Set an explicit \`padding=N\` on every code-bearing shape
2174
+ to control the inset uniformly.
2175
+ d) Truncate examples with \`...\` rather than letting one
2176
+ card carry six lines and another two. The point of a
2177
+ sibling row is comparison; the longest item sets the
2178
+ shape height for everyone.
2179
+
2180
+ Example - three comparable code cards:
2181
+
2182
+ r 6 35 26 12 fill=#F4F6FA size=14px padding=1 |
2183
+ \`\`\`json
2184
+ ["The", " quick", "..."]
2185
+ \`\`\`
2186
+ r 36 35 26 12 fill=#F4F6FA size=14px padding=1 |
2187
+ \`\`\`json
2188
+ [464, 4391, ...]
2189
+ \`\`\`
2190
+ r 66 35 26 12 fill=#F4F6FA size=14px padding=1 |
2191
+ \`\`\`json
2192
+ [[0.12, ...], ...]
2193
+ \`\`\`
2194
+
2195
+ Same w, same h, same size, same padding, truncated to fit.
2196
+ The three cards then read as a parallel set rather than three
2197
+ improvisations.
2198
+
2199
+ ── SHAPE ATTRIBUTES ──────────────────────────────────
2200
+ Between geometry and \`|\`:
2201
+
2202
+ Visual:
2203
+ fill=<color> Shape fill
2204
+ stroke=<color> Outline colour
2205
+ strokeWidth=N Outline width (grid units)
2206
+ radius=N Corner radius (rectangles)
2207
+ color=<color> Text colour inside the shape
2208
+ image=<url> Bitmap fill (see IMAGES section)
2209
+ opacity=N 0..1; fades the whole shape (fill + stroke +
2210
+ text overlay) at once. Useful for translucent
2211
+ backdrops, ghosted "previous state" shapes,
2212
+ overlapping highlights. Out-of-range values
2213
+ clamp.
2214
+
2215
+ Numeric attributes (strokeWidth, radius) are in grid units - pick
2216
+ values relative to your grid size, no prescribed defaults. On a
2217
+ 100-wide grid, \`radius=2\` is ~2% of slide width.
2218
+
2219
+ For text sizing (text=role, size=, h*Scale=), padding, alignment,
2220
+ and slot ids inside templates, see \`sdoc slides\` - those work the
2221
+ same in raw shapes and template shapes.
2222
+
2223
+ ── IDS AND @REFERENCES ──────────────────────────────
2224
+ Declare an id with \`#name\`; reference with \`@name\` or \`@name.anchor\`
2225
+ from line / arrow endpoints. Omit the anchor to default to centre.
2226
+
2227
+ r 10 10 30 20 #title | # Main Point
2228
+ r 60 10 30 20 #detail | Supporting detail
2229
+
2230
+ a @title @detail (default: centres of both shapes)
2231
+ a @title.right @detail.left (explicit: right edge to left edge)
2232
+
2233
+ Each endpoint picks its own anchor independently, so you can connect
2234
+ the bottom of one box to the top of another:
2235
+ l @box-a.bottom @box-b.top
2236
+
2237
+ 9 anchors: center (default), top, bottom, left, right, topleft,
2238
+ topright, bottomleft, bottomright.
2239
+
2240
+ Anchors resolve against each shape's BOUNDING BOX. For circles and
2241
+ ellipses that means the circumscribing rectangle, not the perimeter -
2242
+ so @circle.right lands at the box edge, not the curve.
2243
+
2244
+ \`l\` and \`a\` endpoints can mix \`@ref\` with raw \`x y\` coords freely,
2245
+ e.g. \`l @title.bottom 50 30\`.
2246
+
2247
+ Note that \`#name\` is overloaded: in a \`@template\` declaration it
2248
+ marks a slot for consumers to fill; in a raw slide it marks an id
2249
+ for @refs. Same syntax, two purposes - context decides.
2250
+
2251
+ ── TEXT INSIDE NON-RECT SHAPES ──────────────────────
2252
+ Text inside a polygon, circle, or ellipse lays out in the shape's
2253
+ AXIS-ALIGNED BOUNDING BOX, not the visible silhouette. For a trapezoid,
2254
+ triangle, or arrow this means the text rectangle is larger than the
2255
+ filled shape - long labels can overhang the slanted edges and end up
2256
+ partly outside the visible polygon.
2257
+
2258
+ Example. A funnel band:
2259
+ p 1,1.8 15,1.8 14.2,3 1.8,3 fill=#e0e7ff align=left | 28M devs
2260
+ The bounding box is x ∈ [1, 15], y ∈ [1.8, 3]. \`align=left\` parks the
2261
+ text at x ≈ 1, which is the leftmost point of the WIDEST corner.
2262
+ At the bottom of the band the silhouette only reaches x ≈ 1.8, so a
2263
+ two-line label would clip on the second line.
2264
+
2265
+ Two ways to handle this:
2266
+
2267
+ 1. Short, centered labels.
2268
+ Use \`align=center\` and keep the label short enough that it fits
2269
+ within the silhouette's narrowest waist. Works for symmetric
2270
+ trapezoids, hexagons, ellipses.
2271
+
2272
+ p 5.4,7.4 10.6,7.4 9.4,8.3 6.6,8.3 fill=#312e81 color=#fff align=center |
2273
+ **250k**
2274
+
2275
+ 2. Shape-only + separate text-r on top.
2276
+ Draw the polygon WITHOUT content (no \`|\`), then place an \`r\` shape
2277
+ on top, sized to fit safely inside the silhouette. Lets the text
2278
+ box be a clean rectangle while the visible silhouette stays slanted.
2279
+
2280
+ p 1,1.8 15,1.8 14.2,3 1.8,3 fill=#e0e7ff
2281
+ r 2,2 12,0.8 align=center valign=center |
2282
+ **21M** use any AI tool at work
2283
+
2284
+ The \`r\` paints above the \`p\` because it is declared after the \`p\`
2285
+ in source order. Reverse the lines and the polygon would cover the
2286
+ rect instead.
2287
+
2288
+ ── POLYGON GEOMETRY ──────────────────────────────────
2289
+ Slant strength. A trapezoid where each side indents 0.8u over a
2290
+ 1.2u height (a ~6% slant) reads as a rectangle. If you want the
2291
+ viewer's eye to see "narrowing", make each side indent at least
2292
+ ~15% of the band's height. For a stack of bands (funnel, pyramid),
2293
+ pick a constant slant ratio across all bands so the silhouette
2294
+ reads as one continuous taper rather than a hinge between
2295
+ rectangles and triangles.
2296
+
2297
+ Order matters within a slide. Shapes declared later paint over
2298
+ shapes declared earlier (this applies across types — see LAYERING).
2299
+ For nested concentric shapes (TAM/SAM/SOM), declare the outermost
2300
+ first; inner shapes cover the outers' label space. Plan your label
2301
+ positions in the VISIBLE RING between each shape and its inner
2302
+ neighbour - or move labels out to an adjacent \`r\` column.
2303
+
2304
+ Curved segments. Five operators between adjacent points: no operator
2305
+ is a straight segment; \`~\` gives a soft bow at 10% of chord length;
2306
+ \`^h\` arcs / bows the segment by an explicit sagitta h; \`>P\` is a
2307
+ quadratic that passes through P at its midpoint; \`* P1 P2\` is a cubic
2308
+ that passes through P1 and P2 at the curve's third-points. Controls
2309
+ can be \`@refs\`, so the curve docks exactly onto another shape's edge:
2310
+
2311
+ r 1 2 4 3 #card
2312
+ r 11 2 4 3 #note
2313
+ a @card.right ^0.6 @note.left (curved arrow between two cards)
2314
+
2315
+ p @card.bottomleft >@card.bottom @note.bottomleft @note.bottom \\
2316
+ @note.bottomright @card.bottomright fill=#f1f5f9
2317
+ (banded shape: top edge dips between
2318
+ the two cards' bottom centers)
2319
+
2320
+ Bow direction. Positive sagitta bows to the LEFT of direction-of-
2321
+ travel. For a horizontal chord moving right, positive bow = upward.
2322
+ Negative bow flips the curve to the opposite side. The same
2323
+ convention applies to polygon \`^h\` segments and bowed arrows.
2324
+
2325
+ Useful for: rounded card corners, dome / loaf tops, speech-bubble
2326
+ tails, curved connectors, organic silhouettes (leaves, clouds, lenses),
2327
+ S-curve callouts. Bad for text-bearing shapes because the bounding
2328
+ box still treats the curve as if it were a straight chord, so labels
2329
+ may overhang the visible silhouette.
2330
+
2331
+ Concave polygons. The bounding box of a concave shape includes the
2332
+ concavity - text can sit in the notch and overlap a neighbouring
2333
+ shape. For concave shapes (arrows, callouts, chevrons), use the
2334
+ shape-only + r-overlay pattern.
2335
+
2336
+ ── COMPOSITE PATTERNS ───────────────────────────────
2337
+ Each pattern obeys the DESIGN PRINCIPLES above: at most one
2338
+ saturated fill per slide (used as the focal element), thin neutral
2339
+ strokes only when contour is doing real work, labels outside the
2340
+ shape when the shape is too narrow to hold them.
2341
+
2342
+ Process flow with a focal step.
2343
+ Two pale frames + one navy focal step. The navy IS the slide's
2344
+ one deviation - it tells the audience which step matters.
2345
+ r 1,3.5 3,1.5 stroke=#cbd5e1 strokeWidth=0.02 align=center valign=center | **Plan**
2346
+ r 4.5,3.5 3,1.5 stroke=#cbd5e1 strokeWidth=0.02 align=center valign=center | **Build**
2347
+ r 8,3.5 3,1.5 fill=#1e40af color=#ffffff align=center valign=center | **Ship**
2348
+ a @plan.right @build.left
2349
+ a @build.right @ship.left
2350
+
2351
+ TAM/SAM/SOM with magnitude-proportional rectangles.
2352
+ All three rects share their top-left corner; sides scale by
2353
+ \`sqrt(value / 32)\` so the AREAS read as the dollar values, not
2354
+ just "three nested shapes". Labels live in the right column with
2355
+ short leaders - the inner rects are too small to hold them and
2356
+ putting labels inside would hide some behind others. Only the
2357
+ focal SOM uses saturated fill.
2358
+
2359
+ # TAM 32 -> sqrt(32/32) = 1.0 (9.0 x 6.0 = 54 sq u)
2360
+ # SAM 8 -> sqrt( 8/32) = 0.5 (4.5 x 3.0 = 13.5)
2361
+ # SOM 1.5 -> sqrt(1.5/32) ~ 0.22 (2.0 x 1.3 ~ 2.6)
2362
+ r 1,2 9 6 stroke=#94a3b8 strokeWidth=0.02
2363
+ r 1,2 4.5 3 stroke=#94a3b8 strokeWidth=0.02
2364
+ r 1,2 2.0 1.3 fill=#1e40af
2365
+ r 11,2 4 6 align=left valign=top |
2366
+ **TAM** $32B - global developer tools
2367
+ **SAM** $8B - AI-coding subset
2368
+ **SOM** $1.5B - CLI-agent slice (4.7% of TAM by area)
2369
+
2370
+ Callout / speech bubble.
2371
+ Polygon for the bubble outline (thin neutral stroke; no fill),
2372
+ \`r\` for the text content positioned to avoid the bubble's tail.
2373
+ Reserved for genuine annotation - if the callout could be a
2374
+ body paragraph, make it one.
2375
+
2376
+ ── LAYERING ────────────────────────────────────────
2377
+ Source order = paint order. The shape declared later in the slide
2378
+ paints on top of shapes declared earlier. This holds across shape
2379
+ types — a polygon declared after a rectangle paints above that
2380
+ rectangle and vice versa.
2381
+
2382
+ # Rect first, polygon second - polygon paints on top.
2383
+ r 2 2 10 5 fill=#1e40af
2384
+ p 4,3 12,3 12,6 4,6 fill=#fde68a
2385
+
2386
+ # Reverse the lines, the rect is on top instead.
2387
+
2388
+ That's the whole rule for 95% of decks. If you want a connector
2389
+ arrow above the cards it joins, declare the arrow last. If you want
2390
+ a backdrop behind a card, declare the backdrop first.
2391
+
2392
+ Escape hatch: \`layer=top | mid | bottom\` (default \`mid\`)
2393
+
2394
+ When source order isn't enough — usually inside a template whose
2395
+ consumer adds more shapes — set \`layer=\` to promote or demote a
2396
+ shape across the three coarse sublayers:
2397
+
2398
+ bottom - paints before everything regardless of source position
2399
+ mid - the default
2400
+ top - paints after everything regardless of source position
2401
+
2402
+ Invalid values surface in the error badge. For hand-authored slides
2403
+ you should rarely need \`layer=\` at all; reaching for it is a hint
2404
+ that the shape order itself wants reordering.
2405
+
2406
+ ── IMAGES IN SHAPES ────────────────────────────────
2407
+ Any shape (\`r\`, \`c\`, \`p\`, etc.) can hold a bitmap via \`image=<url>\`
2408
+ or the \`i x y w h\` shape sugar (parser-equivalent to \`r\` + \`image=\`).
2409
+ See the IMAGES section in \`sdoc slides\` for the full reference -
2410
+ it works the same in raw shapes and template image slots.
2411
+
2412
+ ── WHEN TO STOP AND USE A TEMPLATE ─────────────────
2413
+ If your custom slide ends up being "title at top + body below" or
2414
+ "title + two columns" or "title + chart + takeaway", you're
2415
+ re-implementing a stdlib template. Run \`sdoc slides list\` and pick
2416
+ the closest match - the templates encode safe margins, role
2417
+ typography, and slot semantics that you'd otherwise have to re-derive.
2418
+
2419
+ Raw shapes earn their keep for: market sizing diagrams, custom
2420
+ funnels / pyramids / matrices, decision trees, process flows with
2421
+ arrows between named blocks, anything where the GEOMETRY is the
2422
+ message. For everything else, \`@extends\` first.
2423
+
2424
+ See also:
2425
+ sdoc slides Main slide DSL reference
2426
+ sdoc slides list Built-in templates + slot lists
2427
+ sdoc charts Chart fenced blocks (\`\`\`chart)
2428
+ sdoc diagrams Mermaid fenced blocks (\`\`\`mermaid)
2429
+ `;
2430
+
2431
+ const LIBRARY_HELP = `
2432
+ SmallDocs - Library
2433
+ ===============
2434
+ The library is a personal, on-machine index of every markdown file you
2435
+ open with \`sdoc <file>\`. It lets you search, browse, and re-open your
2436
+ notes without remembering where they live on disk.
2437
+
2438
+ WHAT IT IS, IN ONE PARAGRAPH
2439
+ Every \`sdoc <file>\` writes a small record into ~/.sdocs/library-
2440
+ index.json - the file path, its title, its tags, when it was last
2441
+ touched. A small loopback HTTP server reads that index when you visit
2442
+ smalldocs.org/library in a browser, so the page can show your files
2443
+ without anything leaving your machine. Click a result and SmallDocs spins
2444
+ up a live editing session against the file on disk. Nothing about
2445
+ the library ever touches a remote server.
2446
+
2447
+ COMMANDS
2448
+ Open and inspect
2449
+ sdoc library Open the library UI at
2450
+ smalldocs.org/library (or
2451
+ localhost:3000/library when
2452
+ running the dev server).
2453
+ Starts the local agent if it
2454
+ isn't already running.
2455
+ sdoc library ls List markdown indexed under
2456
+ this project (walks up to the
2457
+ nearest .git, falls back to
2458
+ cwd). Pass a path to override
2459
+ scope: \`sdoc library ls ~/notes\`.
2460
+ sdoc library ls --tags Same scope, but prints the tag
2461
+ bag (tag - count, sorted by
2462
+ frequency). Run before tagging
2463
+ a new file to stay consistent.
2464
+ sdoc library status Print enabled/disabled, entry
2465
+ count, last scan time.
2466
+
2467
+ Refresh
2468
+ sdoc library rebuild Walk \$HOME again from scratch
2469
+ and refresh every entry. Use
2470
+ after moving files around or
2471
+ when an entry seems stale.
2472
+
2473
+ On/off
2474
+ sdoc library enable Re-enable indexing-on-open if
2475
+ you had turned it off. New
2476
+ opens will be recorded again.
2477
+ sdoc library disable Stop indexing on \`sdoc <file>\`.
2478
+ The existing index is left in
2479
+ place; the library page still
2480
+ shows what's already there.
2481
+
2482
+ Autostart (macOS)
2483
+ sdoc library autostart Same as \`autostart status\`:
2484
+ print whether the LaunchAgent
2485
+ is installed.
2486
+ sdoc library autostart enable Install the LaunchAgent so the
2487
+ loopback agent comes back on
2488
+ every login. This is the
2489
+ default the first time you run
2490
+ \`sdoc library\`.
2491
+ sdoc library autostart disable Remove the LaunchAgent and
2492
+ record that you turned it off;
2493
+ the default-on logic respects
2494
+ this until you re-enable.
2495
+ sdoc library autostart status Print whether the LaunchAgent
2496
+ is installed and where its
2497
+ plist lives.
2498
+
2499
+ Help
2500
+ sdoc library --help This help.
2501
+ sdoc library help Same.
2502
+
2503
+ WHAT GETS INDEXED
2504
+ Every file you open with \`sdoc <file>\` is recorded at open time.
2505
+ \`sdoc library rebuild\` additionally walks \$HOME looking for
2506
+ markdown that fits these rules:
2507
+ - extension is .md, .mdx, or .markdown
2508
+ - size is at most 1 MB
2509
+ - not under a hidden directory (.git, .venv, .cache, ...)
2510
+ - not under a system / framework directory (node_modules, dist,
2511
+ build, target, vendor, .next, __pycache__, ...)
2512
+ - not under a sensitive directory (.ssh, .aws, .gnupg, .docker,
2513
+ .kube, .gcloud, .azure, .bitwarden, .password-store)
2514
+ - the file's basename does not match the deny list (SSH keys,
2515
+ .env files, .key/.pem/.crt/.kdbx etc., credentials.json,
2516
+ api-secret.yaml, ...). Markdown is deliberately NOT denied by
2517
+ name - \`credentials-handling.md\` is a legitimate note about
2518
+ secrets, not a secret.
2519
+ - the file is not excluded by a .sdocsignore (see below)
2520
+ - the file's front matter does not contain \`sdocs-library: false\`
2521
+ (per-file opt-out)
2522
+
2523
+ PER-FILE OPT-OUT
2524
+ Add \`sdocs-library: false\` to a file's YAML front matter to keep
2525
+ it out of the library. The file still opens with \`sdoc <file>\`,
2526
+ it just isn't indexed.
2527
+
2528
+ ---
2529
+ title: My private notes
2530
+ sdocs-library: false
2531
+ ---
2532
+
2533
+ PER-DIRECTORY EXCLUDES (.sdocsignore)
2534
+ Drop a \`.sdocsignore\` file into any directory. Subset of gitignore
2535
+ syntax:
2536
+ # comments and blank lines are skipped
2537
+ drafts/ exclude this directory
2538
+ *.local.md exclude by glob, basename match
2539
+ sub/secret.md anchored path match (relative to this file)
2540
+ **/scratch/ cross-directory globstar
2541
+ Patterns inherit downward. Negation (\`!\`) and other gitignore
2542
+ niceties are not in v1.
2543
+
2544
+ EPHEMERAL PATHS AND RESCUE
2545
+ Files opened from OS-managed temp folders are special: the OS will
2546
+ clean them on its own schedule (next reboot, days later, when you
2547
+ empty the trash). The library handles this by taking a one-shot
2548
+ RESCUE COPY at index time and pointing the entry at the copy.
2549
+
2550
+ Rescue triggers when the path sits inside any of:
2551
+ macOS /tmp, /private/tmp, /var/tmp, /private/var/tmp,
2552
+ ~/.Trash, plus Node's os.tmpdir()
2553
+ Linux /tmp, /var/tmp, /run, /dev/shm, ~/.cache, plus os.tmpdir()
2554
+ Windows %TEMP%, %TMP%, C:\\Windows\\Temp
2555
+
2556
+ The copy lives at ~/.sdocs/library/rescued/<hash>-<basename>. The
2557
+ hash prefix avoids name collisions between two /tmp/notes.md from
2558
+ different sessions. The entry's \`rescuedFrom\` field records the
2559
+ original path; the agent's security gate accepts requests for both
2560
+ the rescued copy and the original path.
2561
+
2562
+ Caveat: rescue is a SNAPSHOT, not a live mirror. Edit \`/tmp/foo.md\`
2563
+ after the first \`sdoc\` and the rescued copy is stale until you
2564
+ re-open the file (which triggers an index update). Once the OS
2565
+ deletes the original, the rescued copy is what you have.
2566
+
2567
+ TAGGING (TWO WAYS TO TAG)
2568
+ Tags live in the file's YAML front matter on disk. There are two
2569
+ ways to put them there:
2570
+
2571
+ 1. Write the front matter directly. The canonical place; survives
2572
+ every export and round-trip.
2573
+
2574
+ ---
2575
+ title: Q2 plan
2576
+ tags:
2577
+ - planning
2578
+ - q2
2579
+ - finance
2580
+ ---
2581
+
2582
+ The Tags row in the file-info card (browser editor) is also a
2583
+ write into the file's front matter, performed by the Bridge.
2584
+
2585
+ 2. CLI flag at open time. Append \`+tag\` to any \`sdoc\` invocation:
2586
+
2587
+ sdoc plan.md +planning +q2
2588
+
2589
+ SmallDocs injects those tags into the file's YAML front matter
2590
+ before the browser receives the content, so they survive future
2591
+ opens, exports, and any sync. The \`+\` prefix is shell-safe
2592
+ (\`#\` would be eaten as a comment). Multiple \`+tag\` args
2593
+ anywhere on the command line are allowed.
2594
+
2595
+ #words in prose are NOT extracted as tags. Front matter is the
2596
+ one source of truth; everything else is a way to write to it.
2597
+
2598
+ EDITING TAGS FROM THE BROWSER
2599
+ Open any indexed file (\`sdoc <file>\`) and look at the file info
2600
+ card above the document. There is a Tags row with chips. Hover a
2601
+ chip for an \`x\` to drop it; click \`+\` at the end of the row to
2602
+ add a new one.
2603
+
2604
+ Edits happen through the BRIDGE (the WebSocket connection that
2605
+ also handles live save). The library agent never writes content -
2606
+ exactly one process writes to your file at a time, which avoids
2607
+ the race condition two parallel writers would create.
2608
+
2609
+ If the Tags row is read-only with a hint, you opened the file via
2610
+ a short link or hash URL and there is no Bridge to save through.
2611
+ Open the file via \`sdoc <file>\` to get an editable Tags row.
2612
+
2613
+ CONSISTENCY HINTS FOR AGENTS
2614
+ Before tagging a new file in a project, run:
2615
+
2616
+ sdoc library ls --tags
2617
+
2618
+ This prints the project's current tag vocabulary (tag - count). Re-use
2619
+ existing tags where possible so the library stays consistent. The
2620
+ scope walks up to the nearest .git, so cd anywhere inside the repo
2621
+ works.
2622
+
2623
+ To see what's already in the project:
2624
+
2625
+ sdoc library ls
2626
+
2627
+ THE LOCAL AGENT
2628
+ The library page (smalldocs.org/library or localhost:3000/library) is
2629
+ hosted from the SmallDocs server; the data it shows comes from a tiny
2630
+ HTTP server the CLI runs at 127.0.0.1:47843. That server:
2631
+ - reads ~/.sdocs/library-index.json
2632
+ - never writes user content (writes go through the Bridge)
2633
+ - refuses any browser request whose Origin is not smalldocs.org,
2634
+ sdocs.dev, or localhost:3000 (extend with SDOCS_URL or
2635
+ SDOCS_AGENT_ALLOWED_ORIGINS)
2636
+ - refuses requests whose Host header is not 127.0.0.1 / localhost
2637
+ (DNS-rebinding guard)
2638
+ - refuses path arguments outside the library, on the deny list,
2639
+ or behind a symlink that resolves outside the library
2640
+ If port 47843 is busy, the CLI falls back to a random port and
2641
+ prints the URL with the port baked in.
2642
+
2643
+ AUTOSTART (macOS)
2644
+ The first time you run \`sdoc library\`, a LaunchAgent plist is
2645
+ written to ~/Library/LaunchAgents so the agent comes back after
2646
+ every reboot. Turn it off explicitly with:
2647
+
2648
+ sdoc library autostart disable
2649
+
2650
+ Once disabled, the default-on logic does not re-enable it without
2651
+ you running \`sdoc library autostart enable\` again. The browser
2652
+ shows a small recovery banner only when autostart was enabled but
2653
+ the agent isn't actually answering.
2654
+
2655
+ Linux and Windows do not have autostart in v1. The agent only
2656
+ runs when you start it explicitly with \`sdoc library\`.
2657
+
2658
+ CLI INTERACTION CHEAT SHEET FOR AGENTS
2659
+ Before authoring a new note inside a project:
2660
+ sdoc library ls --tags see the project's tag vocabulary
2661
+
2662
+ When opening or creating a file with tags:
2663
+ sdoc draft.md +planning +q2 tags get written into front matter
2664
+
2665
+ When you want a file kept OUT of the library:
2666
+ add \`sdocs-library: false\` to its YAML front matter
2667
+
2668
+ When the user asks "where is X?":
2669
+ sdoc library opens the search UI in their browser
2670
+
2671
+ PRIVACY MODEL
2672
+ The library is per-machine and per-user. Nothing about it is
2673
+ uploaded to smalldocs.org or any other server. The browser page on
2674
+ smalldocs.org calls the local agent over plain HTTP on 127.0.0.1; it
2675
+ cannot see file content the agent doesn't already have indexed,
2676
+ and the agent cannot read files outside the library.
2677
+
2678
+ COMMON QUESTIONS
2679
+ Q: I opened a file but I don't see it in the library.
2680
+ A: Check \`sdoc library status\` (is it enabled?), then
2681
+ \`sdoc library rebuild\`. If the file is in front matter
2682
+ \`sdocs-library: false\`, that's the opt-out. Also check for a
2683
+ .sdocsignore in the file's directory or any ancestor.
2684
+
2685
+ Q: The library page shows "agent is not running".
2686
+ A: Run \`sdoc library\` once to start it, or enable autostart with
2687
+ \`sdoc library autostart enable\`. The banner is dismissible if
2688
+ you don't want one running.
2689
+
2690
+ Q: A tag I added in the browser didn't save.
2691
+ A: The Tags row needs a connected Bridge. Open the file via
2692
+ \`sdoc <file>\` (not via a share URL or short link) so the
2693
+ editor knows the on-disk path.
2694
+
2695
+ Q: I added a tag with \`+tag\` but it didn't appear.
2696
+ A: The \`+\` is what makes it a tag arg; \`tag\` alone would be read
2697
+ as a filename. Valid form: \`+name\` where name starts with a
2698
+ letter and contains letters, digits, underscores, or hyphens
2699
+ (max 64 chars).
2700
+
2701
+ Q: I want to keep work notes out of the library.
2702
+ A: Add \`sdocs-library: false\` to the file's front matter, or
2703
+ drop a \`.sdocsignore\` into the directory with the pattern.
2704
+ `;
2705
+
2706
+ module.exports = { HELP, COMMENTS_HELP, SCHEMA, CHARTS_HELP, DIAGRAMS_HELP, CELLS_HELP, SLIDES_HELP, SLIDES_CUSTOM_SHAPES_HELP, LIBRARY_HELP };