slide-cli 1.0.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.
Files changed (45) hide show
  1. package/README.md +184 -0
  2. package/TEMPLATE_GUIDE.md +661 -0
  3. package/dist/TEMPLATE_GUIDE.md +661 -0
  4. package/dist/index.js +277058 -0
  5. package/dist/scripts/build.js +41 -0
  6. package/dist/scripts/download-fonts.js +123 -0
  7. package/dist/templates/bold-title/sample.json +57 -0
  8. package/dist/templates/bold-title/template.html +212 -0
  9. package/dist/templates/bold-title/template.json +76 -0
  10. package/dist/templates/bold-title-wide/sample.json +58 -0
  11. package/dist/templates/bold-title-wide/template.html +224 -0
  12. package/dist/templates/bold-title-wide/template.json +76 -0
  13. package/dist/templates/minimal/sample.json +53 -0
  14. package/dist/templates/minimal/template.html +183 -0
  15. package/dist/templates/minimal/template.json +76 -0
  16. package/dist/templates/minimal-wide/sample.json +53 -0
  17. package/dist/templates/minimal-wide/template.html +208 -0
  18. package/dist/templates/minimal-wide/template.json +76 -0
  19. package/dist/templates/quote-card/sample.json +57 -0
  20. package/dist/templates/quote-card/template.html +203 -0
  21. package/dist/templates/quote-card/template.json +76 -0
  22. package/dist/templates/quote-card-wide/sample.json +58 -0
  23. package/dist/templates/quote-card-wide/template.html +215 -0
  24. package/dist/templates/quote-card-wide/template.json +76 -0
  25. package/package.json +66 -0
  26. package/scripts/build.js +41 -0
  27. package/scripts/download-fonts.js +123 -0
  28. package/templates/bold-title/sample.json +57 -0
  29. package/templates/bold-title/template.html +212 -0
  30. package/templates/bold-title/template.json +76 -0
  31. package/templates/bold-title-wide/sample.json +58 -0
  32. package/templates/bold-title-wide/template.html +224 -0
  33. package/templates/bold-title-wide/template.json +76 -0
  34. package/templates/minimal/sample.json +53 -0
  35. package/templates/minimal/template.html +183 -0
  36. package/templates/minimal/template.json +76 -0
  37. package/templates/minimal-wide/sample.json +53 -0
  38. package/templates/minimal-wide/template.html +208 -0
  39. package/templates/minimal-wide/template.json +76 -0
  40. package/templates/quote-card/sample.json +57 -0
  41. package/templates/quote-card/template.html +203 -0
  42. package/templates/quote-card/template.json +76 -0
  43. package/templates/quote-card-wide/sample.json +58 -0
  44. package/templates/quote-card-wide/template.html +215 -0
  45. package/templates/quote-card-wide/template.json +76 -0
@@ -0,0 +1,661 @@
1
+ # Template Authoring Guide
2
+
3
+ This guide contains everything needed to create a slide-cli template from scratch —
4
+ whether you are a human developer or an LLM agent generating one on demand.
5
+
6
+ ---
7
+
8
+ ## What a template is
9
+
10
+ A template is a **folder** containing exactly three files:
11
+
12
+ ```
13
+ my-template/
14
+ ├── template.json ← manifest: id, slots, dimensions
15
+ ├── template.html ← Handlebars HTML: the visual design
16
+ └── sample.json ← working example + slot documentation
17
+ ```
18
+
19
+ Install it with:
20
+
21
+ ```bash
22
+ slide add-template ./my-template/
23
+ slide create --template my-template --data data.json --out ./output
24
+ ```
25
+
26
+ ---
27
+
28
+ ## 1. template.json — the manifest
29
+
30
+ Defines the template identity and every data slot the HTML can use.
31
+
32
+ ```json
33
+ {
34
+ "name": "My Template",
35
+ "id": "my-template",
36
+ "version": "1.0.0",
37
+ "description": "One sentence describing the visual style and use case.",
38
+ "author": "your-name",
39
+ "aspectRatio": "9:16",
40
+ "width": 1080,
41
+ "height": 1920,
42
+ "slots": [
43
+ {
44
+ "id": "headline",
45
+ "type": "text",
46
+ "label": "Headline",
47
+ "required": true,
48
+ "description": "Main heading text. 1–6 words work best at large font sizes."
49
+ },
50
+ {
51
+ "id": "body",
52
+ "type": "text",
53
+ "label": "Body text",
54
+ "required": false,
55
+ "default": "",
56
+ "description": "Supporting paragraph. 1–3 sentences."
57
+ },
58
+ {
59
+ "id": "bg",
60
+ "type": "color",
61
+ "label": "Background color",
62
+ "required": false,
63
+ "default": "#0f0e0c",
64
+ "description": "Slide background. Any valid CSS hex color."
65
+ }
66
+ ]
67
+ }
68
+ ```
69
+
70
+ ### Slot types
71
+
72
+ | type | What it holds | Example value |
73
+ |---|---|---|
74
+ | `text` | Any string | `"Less is more"` |
75
+ | `color` | CSS hex color | `"#ff6b35"` |
76
+ | `number` | Integer or float | `42` |
77
+ | `url` | A URL string | `"https://example.com"` |
78
+ | `image` | Local file path or https:// URL | `"./photo.jpg"` or `"https://…"` |
79
+
80
+ > **How image slots work:** The renderer automatically converts any `image` slot value to a base64 data URI before injecting it into the HTML. This means Puppeteer can render the image without any network access, and relative paths (e.g. `"./assets/photo.jpg"`) are resolved relative to the directory of the source data JSON file. Template authors just use `{{slotId}}` as a normal `src` attribute — no special handling needed.
81
+ >
82
+ > ```html
83
+ > <!-- This just works — the renderer handles the conversion -->
84
+ > <img src="{{image}}" alt="">
85
+ > ```
86
+ >
87
+ > Accepted values in the data JSON:
88
+ > - Local path relative to the data JSON: `"./photo.jpg"`, `"assets/hero.png"`
89
+ > - Absolute local path: `"/Users/me/photos/hero.jpg"`
90
+ > - Remote URL: `"https://images.unsplash.com/photo-xxx?w=1080"`
91
+ > - Already a data URI: passed through unchanged
92
+
93
+ ### Rules
94
+ - `id` must be kebab-case (`my-slot`, not `mySlot` or `my_slot`)
95
+ - `version` must be semver (`1.0.0`)
96
+ - `id` at the top level must be kebab-case and unique across all installed templates
97
+ - Every slot referenced with `{{slotId}}` in `template.html` must be declared here
98
+ - Required slots with no default will cause `slide create` to error if missing from data
99
+
100
+ ### Standard dimensions
101
+
102
+ | Ratio | width | height | Use case |
103
+ |---|---|---|---|
104
+ | 9:16 | 1080 | 1920 | Instagram Stories, TikTok, Reels |
105
+ | 1:1 | 1080 | 1080 | Instagram feed, Twitter/X |
106
+ | 16:9 | 1920 | 1080 | YouTube thumbnails, presentations, Google Slides export |
107
+
108
+ Set `aspectRatio`, `width`, and `height` in `template.json` to match. The renderer crops the Puppeteer viewport to exactly these dimensions — the HTML must declare the same pixel size in CSS.
109
+
110
+ ---
111
+
112
+ ## 2. template.html — the visual design
113
+
114
+ A **full HTML document** rendered by Puppeteer at exactly `width × height` pixels.
115
+ Slot values are injected via [Handlebars](https://handlebarsjs.com/) before rendering.
116
+
117
+ ### Minimum valid template.html
118
+
119
+ ```html
120
+ <!DOCTYPE html>
121
+ <html lang="en">
122
+ <head>
123
+ <meta charset="UTF-8">
124
+ <style>
125
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
126
+ html, body {
127
+ width: 1080px;
128
+ height: 1920px;
129
+ overflow: hidden;
130
+ background: {{bg}};
131
+ font-family: Georgia, serif;
132
+ }
133
+ body {
134
+ display: flex;
135
+ flex-direction: column;
136
+ justify-content: center;
137
+ padding: 96px;
138
+ }
139
+ h1 { font-size: 120px; color: #f0ece4; line-height: 1.1; }
140
+ p { font-size: 40px; color: #7a756e; margin-top: 48px; }
141
+ </style>
142
+ </head>
143
+ <body>
144
+ <h1>{{headline}}</h1>
145
+ {{#if body}}<p>{{body}}</p>{{/if}}
146
+ <footer style="position:absolute;bottom:60px;right:80px;font-size:28px;color:#333">
147
+ {{slideIndex}} / {{totalSlides}}
148
+ </footer>
149
+ </body>
150
+ </html>
151
+ ```
152
+
153
+ ### Handlebars syntax reference
154
+
155
+ ```handlebars
156
+ {{slotId}} Output a slot value (HTML-escaped)
157
+ {{{slotId}}} Output raw HTML (unescaped) — use with care
158
+
159
+ {{#if slotId}}…{{/if}} Render block only if slot is truthy (non-empty)
160
+ {{#if slotId}}…{{else}}…{{/if}} With fallback
161
+
162
+ {{#unless slotId}}…{{/unless}} Render block if slot is falsy
163
+
164
+ {{upper slotId}} Uppercase the value
165
+ {{lower slotId}} Lowercase the value
166
+ {{default slotId "fallback"}} Use fallback if slot is empty
167
+ {{add slotId 1}} Add a number to the slot value
168
+ ```
169
+
170
+ ### Always-available variables (no need to declare in slots)
171
+
172
+ | Variable | Value |
173
+ |---|---|
174
+ | `{{slideIndex}}` | 1-based index of the current slide (1, 2, 3…) |
175
+ | `{{totalSlides}}` | Total number of slides in the deck |
176
+ | `{{title}}` | Top-level `"title"` from the data JSON |
177
+
178
+ ### Critical HTML constraints
179
+
180
+ 1. **Fixed pixel dimensions** — `html` and `body` must be exactly `width × height` pixels
181
+ from the manifest. Do not use `%`, `vw`, `vh`, or `auto` for the root size.
182
+ Match your manifest values precisely:
183
+ ```css
184
+ /* 9:16 Stories/Reels */
185
+ html, body { width: 1080px; height: 1920px; overflow: hidden; }
186
+
187
+ /* 16:9 Presentations/YouTube */
188
+ html, body { width: 1920px; height: 1080px; overflow: hidden; }
189
+
190
+ /* 1:1 Feed/Square */
191
+ html, body { width: 1080px; height: 1080px; overflow: hidden; }
192
+ ```
193
+
194
+ 2. **No external network requests in production** — Google Fonts and CDN links
195
+ will fail in headless Puppeteer unless the machine has internet access.
196
+ Always provide system font fallbacks:
197
+ ```css
198
+ font-family: 'Bebas Neue', Impact, 'Arial Narrow', sans-serif;
199
+ font-family: 'Playfair Display', Georgia, 'Times New Roman', serif;
200
+ font-family: 'DM Mono', 'Courier New', monospace;
201
+ ```
202
+
203
+ 3. **Unicode support (CJK, French, accented Latin)** — Latin display fonts like Bebas Neue
204
+ have no CJK glyphs. If a user supplies Japanese, Korean, Chinese, or French-accented text,
205
+ the primary font silently falls back to a system sans-serif, which may not be bold or
206
+ match the design. Solve this by adding a Noto CJK font as the second entry in the stack:
207
+ ```css
208
+ /* Display title — Latin gets Bebas Neue, CJK/accented Latin gets Noto Sans Black */
209
+ @font-face {
210
+ font-family: 'Noto Sans CJK';
211
+ font-weight: 900;
212
+ src: local('Noto Sans CJK JP Black'),
213
+ url('file:///usr/share/fonts/opentype/noto/NotoSansCJK-Black.ttc') format('truetype');
214
+ }
215
+ @font-face {
216
+ font-family: 'Noto Sans CJK';
217
+ font-weight: 700;
218
+ src: local('Noto Sans CJK JP'),
219
+ url('file:///usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc') format('truetype');
220
+ }
221
+ @font-face {
222
+ font-family: 'Noto Serif CJK';
223
+ font-weight: 700;
224
+ src: local('Noto Serif CJK JP Bold'),
225
+ url('file:///usr/share/fonts/opentype/noto/NotoSerifCJK-Bold.ttc') format('truetype');
226
+ }
227
+
228
+ /* Font stacks with CJK fallback */
229
+ .title { font-family: 'Bebas Neue', 'Noto Sans CJK', Impact, sans-serif; font-weight: 900; }
230
+ .subtitle { font-family: 'Outfit', 'Noto Sans CJK', Arial, sans-serif; font-weight: 700; }
231
+ .heading { font-family: 'Fraunces', 'Noto Serif CJK', Georgia, serif; font-weight: 200; }
232
+ .quote { font-family: 'Playfair Display', 'Noto Serif CJK', Georgia, serif; font-weight: 700; }
233
+ ```
234
+ The `local()` hint resolves on the user's system if Noto is installed. The `url()` path
235
+ resolves in the slide-cli Puppeteer renderer on machines where Noto is at the standard
236
+ Linux path. On macOS/Windows, remove the `url()` and rely on `local()` or bundle the font.
237
+
238
+ **Noto CJK font weights available on Linux:**
239
+ | File | Font weight | Use for |
240
+ |---|---|---|
241
+ | `NotoSansCJK-Black.ttc` | 900 | Display titles |
242
+ | `NotoSansCJK-Bold.ttc` | 700 | Subtitles, body, labels |
243
+ | `NotoSerifCJK-Bold.ttc` | 700 | Serif quotes, headings |
244
+
245
+ 4. **Use bold weights for legibility** — at the 1080px→mobile scale factor (~2.8×),
246
+ light and thin font weights lose legibility faster than colour does. Apply these minimums:
247
+ - Display titles (`font-size ≥ 140px`): `font-weight: 700–900`
248
+ - Subtitles and subheadings (`font-size 50–100px`): `font-weight: 700`
249
+ - Body text (`font-size 42–50px`): `font-weight: 400` minimum
250
+ - Labels and eyebrows (`font-size 30–40px`): `font-weight: 700`
251
+ - Footers and counters (`font-size 28–32px`): `font-weight: 400`, never lighter
252
+
253
+ 5. **No `<script>` side effects that block rendering** — Puppeteer waits for
254
+ `networkidle0` then `document.fonts.ready`. Avoid long JS loops or timers
255
+ that prevent the page from settling.
256
+
257
+ 4. **Colors must be hardcoded or from slots** — CSS variables like `var(--accent)`
258
+ will not be set unless you define them yourself in `<style>`.
259
+
260
+ 5. **Slot values in CSS** — you CAN use slot values directly inside `<style>` blocks:
261
+ ```css
262
+ body { background: {{bg}}; }
263
+ h1 { color: {{accent}}; }
264
+ ```
265
+ This is the recommended way to apply per-slide color themes.
266
+
267
+ ---
268
+
269
+ ## 3. sample.json — documentation + working example
270
+
271
+ `sample.json` serves two purposes simultaneously:
272
+ - **Working example** — the `slides` array is valid input for `slide create`
273
+ - **Agent documentation** — `_slots` explains every slot in plain language
274
+
275
+ ```json
276
+ {
277
+ "_template": "my-template",
278
+ "_description": "One paragraph describing when and why to use this template.",
279
+ "_slots": {
280
+ "headline": "REQUIRED — main heading. 1–6 words. Displayed at ~120px so short phrases work best.",
281
+ "body": "optional — supporting paragraph. 1–3 sentences. Shown below the heading.",
282
+ "bg": "optional — hex background color. Dark backgrounds (#0f0e0c) feel editorial; light (#fafaf8) feel clean. Default: #0f0e0c"
283
+ },
284
+ "title": "My Deck Title",
285
+ "slides": [
286
+ {
287
+ "layout": "my-template",
288
+ "headline": "Less is more",
289
+ "body": "Remove everything that does not serve the message.",
290
+ "bg": "#0f0e0c"
291
+ },
292
+ {
293
+ "layout": "my-template",
294
+ "headline": "Ship fast",
295
+ "body": "A good product today beats a perfect product never.",
296
+ "bg": "#0a1628"
297
+ }
298
+ ]
299
+ }
300
+ ```
301
+
302
+ ### Rules for sample.json
303
+ - `_*` keys are stripped before any output is shown to users — use them freely for notes
304
+ - `_slots` should have an entry for **every** slot, starting with `"REQUIRED — "` or `"optional — "`
305
+ - The `slides` array must be valid input — it must pass `slide create` without errors
306
+ - Include 2–4 slides showing variety: different `bg` colors, different content lengths, optional slots both present and absent
307
+ - The `layout` field in each slide object is informational only — `slide create` ignores it
308
+
309
+ ---
310
+
311
+ ## Design guidelines
312
+
313
+ ### Choosing your aspect ratio
314
+
315
+ | Format | Ratio | Canvas | Primary display context |
316
+ |---|---|---|---|
317
+ | Stories, Reels, TikTok | 9:16 | 1080×1920 | Mobile, full-screen, held vertically |
318
+ | Presentations, YouTube | 16:9 | 1920×1080 | Desktop/TV, projected, landscape |
319
+ | Feed, Square posts | 1:1 | 1080×1080 | Mixed — mobile and desktop feeds |
320
+
321
+ The aspect ratio shapes every design decision: how much vertical space you have, how text wraps, and whether a two-column layout makes sense. Choose before writing any CSS.
322
+
323
+ ---
324
+
325
+ ## Design guidelines for 9:16 cards (1080×1920px)
326
+
327
+ ### Typography scale
328
+
329
+ These slides render at 1080×1920px but display on a mobile screen at roughly 375–430px wide. The browser scales the image down by ~2.8×. A font that is 36px at render time appears as ~13px on screen — barely readable. Design for the final display size, not the render size.
330
+
331
+ | Role | Min render size | Approx on mobile | Notes |
332
+ |---|---|---|---|
333
+ | Display / hero title | 140px | ~50px | 1–3 words |
334
+ | Large heading | 100px | ~36px | 4–8 words |
335
+ | Subheading / subtitle | 50px | ~18px | 1–2 lines |
336
+ | Body text | 42px | ~15px | 3–5 lines max |
337
+ | Label / eyebrow | 32px | ~11px | Uppercase + letter-spacing |
338
+ | Footer / counter | 28px | ~10px | Absolute minimum — keep short |
339
+
340
+ **Never go below 28px render size** for any text a user needs to read.
341
+
342
+ ### Layout zones (for 1080×1920)
343
+
344
+ ```
345
+ ┌─────────────────────────────┐ ← y=0
346
+ │ │
347
+ │ TOP MARGIN: 80–120px │ Branding, eyebrow label, logo
348
+ │ │
349
+ ├─────────────────────────────┤ ← y≈140
350
+ │ │
351
+ │ │
352
+ │ CONTENT ZONE │ Main heading, visual, quote
353
+ │ (flex-grow: 1) │
354
+ │ │
355
+ │ │
356
+ ├─────────────────────────────┤ ← y≈1780
357
+ │ │
358
+ │ BOTTOM MARGIN: 60–100px │ Slide counter, CTA, footer
359
+ │ │
360
+ └─────────────────────────────┘ ← y=1920
361
+ ```
362
+
363
+ **Horizontal padding:** 80–120px left/right. Cards are viewed on mobile — tight edges feel cramped.
364
+
365
+ **Layout direction:** `flex-direction: column`. 9:16 is a tall, narrow canvas — vertical stacking is natural. Two-column layouts can work for specific use cases (image beside text) but require careful width management.
366
+
367
+ ---
368
+
369
+ ## Design guidelines for 16:9 cards (1920×1080px)
370
+
371
+ ### Typography scale
372
+
373
+ 16:9 cards are typically viewed at full screen on a desktop (1920px native) or projected. Scale factors vary widely — a 1920px canvas on a 1280px laptop screen is ~0.67×; projected on a wall it may be 1× or larger. Design for comfortable legibility at native size: text that reads well at 1920px will scale gracefully.
374
+
375
+ | Role | Recommended render size | Notes |
376
+ |---|---|---|
377
+ | Display / hero title | 120–160px | 1–4 words |
378
+ | Large heading | 80–110px | 4–8 words |
379
+ | Subheading / subtitle | 32–48px | 1–3 lines |
380
+ | Body text | 28–36px | 4–8 lines max |
381
+ | Label / eyebrow | 22–28px | Uppercase + letter-spacing |
382
+ | Footer / counter | 20–24px | Minimum — keep very short |
383
+
384
+ **Minimum:** 20px render size. Below that, text becomes illegible when projected or on a laptop.
385
+
386
+ ### Layout zones (for 1920×1080)
387
+
388
+ ```
389
+ ┌──────────────────────────────────────────────┐ ← y=0
390
+ │ TOP BAR: 40–60px · branding / counter │
391
+ ├──────────┬───────────────────────────────────┤ ← y≈80
392
+ │ │ │
393
+ │ LEFT │ RIGHT COLUMN │
394
+ │ COLUMN │ (image, subtitle, attribution) │
395
+ │ (title, │ │
396
+ │ heading,│ │
397
+ │ quote) │ │
398
+ │ │ │
399
+ ├──────────┴───────────────────────────────────┤ ← y≈1020
400
+ │ BOTTOM BAR: 40–60px · CTA / footer / rule │
401
+ └──────────────────────────────────────────────┘ ← y=1080
402
+ ```
403
+
404
+ **Layout direction:** `flex-direction: row` is often the right choice. A 16:9 canvas is wide and shallow — two-column layouts (title left, subtitle right; quote left, attribution right) use the space naturally and give each element room to breathe.
405
+
406
+ **Horizontal padding:** 80–96px left/right.
407
+ **Vertical padding:** 44–64px top/bottom.
408
+
409
+ ### Two-column layout patterns
410
+
411
+ ```css
412
+ /* Split: content left + image/aside right */
413
+ .main {
414
+ display: flex;
415
+ flex-direction: row;
416
+ align-items: center;
417
+ flex: 1;
418
+ padding: 0 80px;
419
+ gap: 80px;
420
+ }
421
+ .main-left { flex: 1 1 0; } /* grows to fill */
422
+ .main-right { flex: 0 0 540px; } /* fixed right column */
423
+
424
+ /* Equal split */
425
+ .main-left { flex: 1 1 0; }
426
+ .main-right { flex: 1 1 0; }
427
+
428
+ /* Image takes right half */
429
+ .text-col { flex: 0 0 900px; }
430
+ .image-col { flex: 1 1 0; }
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Shared design principles (all aspect ratios)
436
+
437
+ ### Mobile/display contrast rules
438
+
439
+ - **Primary text** (headings, quotes): near-white on dark (`#f0ece4` or brighter), near-black on light (`#1a1714` or darker). No opacity reduction.
440
+ - **Secondary text** (body, subtitle): minimum `opacity: 0.7` or equivalent solid colour.
441
+ - **Tertiary text** (labels, roles, counters): minimum `opacity: 0.35`.
442
+ - **Ghost text** (decorative numbers/watermarks): `opacity: 0.03–0.08`.
443
+ - **Font weight matters as much as colour**: use `font-weight: 400` minimum for body text, `700` for labels and eyebrows at small sizes.
444
+
445
+ ### Opacity pitfalls
446
+
447
+ ```css
448
+ /* ✗ Risky — opacity affects background too if element has one */
449
+ .secondary { color: #ffffff; opacity: 0.45; }
450
+
451
+ /* ✓ Better — only the text colour is affected */
452
+ .secondary { color: rgba(255,255,255,0.6); }
453
+
454
+ /* ✓ Best for dark-on-light — use a concrete muted colour */
455
+ .secondary { color: #6b6560; }
456
+ ```
457
+
458
+ ### Color palette strategies
459
+
460
+ **Dark editorial** (popular for thought-leadership content):
461
+ ```css
462
+ background: #0f0e0c; /* near-black warm */
463
+ color: #f0ece4; /* warm white */
464
+ accent: #c8b89a; /* aged gold */
465
+ muted: #3d3a36; /* dark gray */
466
+ ```
467
+
468
+ **Light clean** (popular for quotes, tips):
469
+ ```css
470
+ background: #faf7f2; /* warm off-white */
471
+ color: #1a1714; /* near-black */
472
+ accent: #c0392b; /* crimson */
473
+ muted: #8a857e; /* warm gray */
474
+ ```
475
+
476
+ **Gradient dark** (popular for bold impact):
477
+ ```css
478
+ background: linear-gradient(160deg, #0d0221 0%, #1a0f2e 100%);
479
+ color: #ffffff;
480
+ accent: #ff6b35; /* any vivid color */
481
+ ```
482
+
483
+ ### Decorative elements that work well
484
+
485
+ ```css
486
+ /* Thin ruled line */
487
+ .rule { width: 60px; height: 2px; background: var(--accent); }
488
+
489
+ /* Corner brackets (no border-radius) */
490
+ .corner-tl { position: absolute; top: 80px; left: 80px;
491
+ width: 50px; height: 50px;
492
+ border-left: 2px solid currentColor;
493
+ border-top: 2px solid currentColor; }
494
+
495
+ /* Noise texture overlay */
496
+ body::before {
497
+ content: ''; position: absolute; inset: 0;
498
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
499
+ pointer-events: none; opacity: 0.35; }
500
+
501
+ /* Subtle grid overlay */
502
+ body::before {
503
+ content: ''; position: absolute; inset: 0;
504
+ background-image:
505
+ linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
506
+ linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
507
+ background-size: 80px 80px; }
508
+
509
+ /* SVG quotation marks (font-independent) */
510
+ <svg viewBox="0 0 200 160" xmlns="http://www.w3.org/2000/svg" style="width:200px;opacity:0.12">
511
+ <path d="M20 130 C20 90 45 55 80 30 L70 15 C25 42 0 82 0 130 C0 148 12 160 28 160
512
+ C44 160 55 148 55 132 C55 116 44 104 28 104 C24 104 22 104 20 105 Z" fill="currentColor"/>
513
+ <path d="M110 130 C110 90 135 55 170 30 L160 15 C115 42 90 82 90 130 C90 148 102 160 118 160
514
+ C134 160 145 148 145 132 C145 116 134 104 118 104 C114 104 112 104 110 105 Z" fill="currentColor"/>
515
+ </svg>
516
+ ```
517
+
518
+ ### Balancing headings and titles with `text-wrap: pretty`
519
+
520
+ Apply `text-wrap: pretty` to any heading, title, or short text that may wrap across multiple lines. The browser distributes words across lines as evenly as possible — so a two-line title has roughly equal line lengths rather than a long first line with a dangling short word at the bottom.
521
+
522
+ ```css
523
+ /* Apply to all display text that might wrap */
524
+ h1, h2, h3, .title, .quote-text, .card-title {
525
+ text-wrap: pretty;
526
+ }
527
+ ```
528
+
529
+ **When to use it:** Short text blocks — slide titles, card headings, pull-quotes, eyebrow labels. Not needed on long body paragraphs (browsers cap it at ~6 lines for performance, so it has no effect on prose anyway).
530
+
531
+ **`text-wrap: balance`** is the stronger alternative — it fully balances line lengths across all lines, not just the last one. Use `pretty` when you want clean, natural wrapping without orphans (the default for these templates), and `balance` when you specifically want every line to be the same visual weight.
532
+
533
+ **Browser support:** Chrome 114+, Firefox 121+, Safari 17.4+. All headless Chromium versions used by this CLI support it — no JS fallback needed.
534
+
535
+ **Do not use** `white-space: nowrap` combined with JS font-scaling to force single-line titles. That approach requires JavaScript, adds timing dependencies with Puppeteer, and prevents natural wrapping when a title genuinely needs two lines. `text-wrap: pretty` handles it correctly with one CSS property.
536
+
537
+ ### Per-slide color variation
538
+ Pass `bg`, `accent`, and `ink` as slots so each slide in a deck can have its own
539
+ personality while sharing the same layout:
540
+
541
+ ```json
542
+ { "layout": "my-template", "heading": "Slide 1", "bg": "#0f0e0c", "accent": "#c8b89a" },
543
+ { "layout": "my-template", "heading": "Slide 2", "bg": "#080f0c", "accent": "#8eb8a0" },
544
+ { "layout": "my-template", "heading": "Slide 3", "bg": "#090c14", "accent": "#a8b8d0" }
545
+ ```
546
+
547
+ ---
548
+
549
+ ## Complete worked example
550
+
551
+ This is a fully working minimal 16:9 template. The same structure applies to any ratio — just change `aspectRatio`, `width`, `height`, and the matching CSS dimensions.
552
+
553
+ ### template.json
554
+ ```json
555
+ {
556
+ "name": "Statement",
557
+ "id": "statement",
558
+ "version": "1.0.0",
559
+ "description": "Single bold statement card with a colored rule and optional caption.",
560
+ "aspectRatio": "16:9",
561
+ "width": 1920,
562
+ "height": 1080,
563
+ "slots": [
564
+ { "id": "statement", "type": "text", "label": "Statement", "required": true, "description": "The main statement. 3–10 words." },
565
+ { "id": "caption", "type": "text", "label": "Caption", "required": false, "default": "", "description": "Small text below. Attribution, context, or source." },
566
+ { "id": "label", "type": "text", "label": "Top label", "required": false, "default": "", "description": "Uppercase eyebrow tag at the top." },
567
+ { "id": "bg", "type": "color", "label": "Background", "required": false, "default": "#0f0e0c" },
568
+ { "id": "accent", "type": "color", "label": "Accent color", "required": false, "default": "#e8b86d" },
569
+ { "id": "ink", "type": "color", "label": "Text color", "required": false, "default": "#f0ece4" }
570
+ ]
571
+ }
572
+ ```
573
+
574
+ ### template.html
575
+ ```html
576
+ <!DOCTYPE html>
577
+ <html lang="en">
578
+ <head>
579
+ <meta charset="UTF-8">
580
+ <style>
581
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
582
+ html, body { width: 1920px; height: 1080px; overflow: hidden; background: {{bg}}; }
583
+ body {
584
+ display: flex; flex-direction: column; justify-content: center;
585
+ padding: 80px 120px;
586
+ font-family: Georgia, 'Times New Roman', serif;
587
+ color: {{ink}};
588
+ position: relative;
589
+ }
590
+ .label {
591
+ font-family: 'Courier New', monospace;
592
+ font-size: 22px; font-weight: 400;
593
+ letter-spacing: 0.2em; text-transform: uppercase;
594
+ color: {{accent}}; margin-bottom: 36px;
595
+ }
596
+ .rule { width: 64px; height: 3px; background: {{accent}}; margin-bottom: 48px; }
597
+ .statement {
598
+ font-size: 96px; font-weight: 400;
599
+ line-height: 1.1; letter-spacing: -0.02em;
600
+ color: {{ink}}; max-width: 1400px;
601
+ text-wrap: pretty;
602
+ }
603
+ .caption {
604
+ margin-top: 48px;
605
+ font-family: 'Courier New', monospace;
606
+ font-size: 28px; font-weight: 400;
607
+ line-height: 1.6; color: {{accent}};
608
+ opacity: 0.8;
609
+ }
610
+ .counter {
611
+ position: absolute; bottom: 52px; right: 96px;
612
+ font-family: 'Courier New', monospace;
613
+ font-size: 20px; opacity: 0.3; letter-spacing: 0.1em;
614
+ }
615
+ </style>
616
+ </head>
617
+ <body>
618
+ {{#if label}}<div class="label">{{label}}</div>{{/if}}
619
+ <div class="rule"></div>
620
+ <p class="statement">{{statement}}</p>
621
+ {{#if caption}}<p class="caption">{{caption}}</p>{{/if}}
622
+ <div class="counter">{{slideIndex}} / {{totalSlides}}</div>
623
+ </body>
624
+ </html>
625
+ ```
626
+
627
+ ### sample.json
628
+ ```json
629
+ {
630
+ "_template": "statement",
631
+ "_description": "Single bold statement card. Best for impactful one-liners, principles, or rules. Each slide should have its own 'bg' and 'accent' colors for visual variety across the deck.",
632
+ "_slots": {
633
+ "statement": "REQUIRED — the main statement. 3–10 words works best at 96px font size.",
634
+ "caption": "optional — smaller text below: attribution, source, or elaboration.",
635
+ "label": "optional — short uppercase eyebrow tag above the rule (e.g. 'Rule 01').",
636
+ "bg": "optional — background hex color. Default #0f0e0c. Try dark near-blacks for editorial feel.",
637
+ "accent": "optional — accent hex color for the rule, label, and caption. Default #e8b86d.",
638
+ "ink": "optional — main text hex color. Default #f0ece4 (warm white)."
639
+ },
640
+ "title": "Design Rules",
641
+ "slides": [
642
+ { "layout": "statement", "label": "Rule 01", "statement": "Constraints breed creativity.", "caption": "— limit your tools, not your thinking", "bg": "#0f0e0c", "accent": "#e8b86d", "ink": "#f0ece4" },
643
+ { "layout": "statement", "label": "Rule 02", "statement": "Ship, then refine.", "caption": "— perfection is the enemy of done", "bg": "#0a1220", "accent": "#7eb8d4", "ink": "#e8f0f4" },
644
+ { "layout": "statement", "label": "Rule 03", "statement": "Clarity over cleverness.", "bg": "#120a0a", "accent": "#d47e7e", "ink": "#f4e8e8" }
645
+ ]
646
+ }
647
+ ```
648
+
649
+ ---
650
+
651
+ ## Checklist before running `slide add-template`
652
+
653
+ - [ ] `template.json` has a unique kebab-case `id`
654
+ - [ ] `aspectRatio`, `width`, and `height` are set correctly for your format (9:16, 16:9, or 1:1)
655
+ - [ ] All slots used as `{{slotId}}` in the HTML are declared in `slots[]`
656
+ - [ ] `html` and `body` CSS dimensions exactly match `width × height` in the manifest, with `overflow: hidden`
657
+ - [ ] Font stacks include system fallbacks (no network-only fonts)
658
+ - [ ] `{{slideIndex}}` and `{{totalSlides}}` used for slide counter (optional but recommended)
659
+ - [ ] `sample.json` has `_slots` with a description for every slot
660
+ - [ ] `sample.json` `slides` array is valid and would pass `slide create`
661
+ - [ ] At least 2 slides in the sample showing different content and color combos