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.
- package/README.md +184 -0
- package/TEMPLATE_GUIDE.md +661 -0
- package/dist/TEMPLATE_GUIDE.md +661 -0
- package/dist/index.js +277058 -0
- package/dist/scripts/build.js +41 -0
- package/dist/scripts/download-fonts.js +123 -0
- package/dist/templates/bold-title/sample.json +57 -0
- package/dist/templates/bold-title/template.html +212 -0
- package/dist/templates/bold-title/template.json +76 -0
- package/dist/templates/bold-title-wide/sample.json +58 -0
- package/dist/templates/bold-title-wide/template.html +224 -0
- package/dist/templates/bold-title-wide/template.json +76 -0
- package/dist/templates/minimal/sample.json +53 -0
- package/dist/templates/minimal/template.html +183 -0
- package/dist/templates/minimal/template.json +76 -0
- package/dist/templates/minimal-wide/sample.json +53 -0
- package/dist/templates/minimal-wide/template.html +208 -0
- package/dist/templates/minimal-wide/template.json +76 -0
- package/dist/templates/quote-card/sample.json +57 -0
- package/dist/templates/quote-card/template.html +203 -0
- package/dist/templates/quote-card/template.json +76 -0
- package/dist/templates/quote-card-wide/sample.json +58 -0
- package/dist/templates/quote-card-wide/template.html +215 -0
- package/dist/templates/quote-card-wide/template.json +76 -0
- package/package.json +66 -0
- package/scripts/build.js +41 -0
- package/scripts/download-fonts.js +123 -0
- package/templates/bold-title/sample.json +57 -0
- package/templates/bold-title/template.html +212 -0
- package/templates/bold-title/template.json +76 -0
- package/templates/bold-title-wide/sample.json +58 -0
- package/templates/bold-title-wide/template.html +224 -0
- package/templates/bold-title-wide/template.json +76 -0
- package/templates/minimal/sample.json +53 -0
- package/templates/minimal/template.html +183 -0
- package/templates/minimal/template.json +76 -0
- package/templates/minimal-wide/sample.json +53 -0
- package/templates/minimal-wide/template.html +208 -0
- package/templates/minimal-wide/template.json +76 -0
- package/templates/quote-card/sample.json +57 -0
- package/templates/quote-card/template.html +203 -0
- package/templates/quote-card/template.json +76 -0
- package/templates/quote-card-wide/sample.json +58 -0
- package/templates/quote-card-wide/template.html +215 -0
- 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
|