@ztffn/presentation-generator-plugin 1.2.0 → 1.3.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/.claude/settings.local.json +9 -0
- package/.claude-plugin/plugin.json +1 -1
- package/CONTEXT.md +110 -0
- package/HANDOFF.md +49 -0
- package/PLAN-consolidate-design-skills.md +238 -0
- package/README.md +15 -17
- package/agents/presentation-design.md +55 -8
- package/package.json +1 -1
- package/skills/graph-json-spec/SKILL.md +812 -0
- package/skills/presentation-generator/SKILL.md +1 -1
- package/skills/slide-content/SKILL.md +45 -0
- package/skills/edge-conventions/SKILL.md +0 -97
- package/skills/layout-templates/SKILL.md +0 -315
- package/skills/node-schema/SKILL.md +0 -135
- package/skills/pitch-reference/SKILL.md +0 -164
- package/skills/positioning/SKILL.md +0 -89
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graph-json-spec
|
|
3
|
+
description: >
|
|
4
|
+
Complete specification for generating valid graph-based presentation JSON —
|
|
5
|
+
node schema, edge wiring, positioning grid, layout decisions, and slide recipes.
|
|
6
|
+
Used by the presentation-design agent.
|
|
7
|
+
user-invocable: false
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Graph JSON Specification
|
|
11
|
+
|
|
12
|
+
Complete reference for producing valid presentation graph JSON.
|
|
13
|
+
Derived from the renderer at `src/types/presentation.ts` and validated demo JSON.
|
|
14
|
+
|
|
15
|
+
## Quick Reference
|
|
16
|
+
|
|
17
|
+
The most common errors and their fixes — read this first.
|
|
18
|
+
|
|
19
|
+
| Rule | Correct | Wrong |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Slide title field | `data.label` | ~~title~~, ~~headline~~, ~~heading~~ |
|
|
22
|
+
| Body content field | `data.content` | ~~body~~, ~~text~~, ~~bullets~~ |
|
|
23
|
+
| Speaker notes field | `data.notes` | ~~speakerNotes~~, ~~speakerNote~~ |
|
|
24
|
+
| Node type | `"huma"` | ~~"slide"~~, ~~"default"~~ |
|
|
25
|
+
| Slide type values | `"content"` \| `"r3f"` \| `"chart"` \| `"custom"` | ~~"titleAndBullets"~~, ~~"image"~~ |
|
|
26
|
+
| Layout values | `"single"` \| `"two-column"` | ~~"top"~~, ~~"split"~~, ~~"left-right"~~ |
|
|
27
|
+
| Source handles | `s-right`, `s-left`, `s-top`, `s-bottom` | ~~right~~, ~~left~~ (bare) |
|
|
28
|
+
| Target handles | `t-right`, `t-left`, `t-top`, `t-bottom` | ~~right~~, ~~left~~ (bare) |
|
|
29
|
+
| Horizontal spacing | 240px | ~~250px~~, ~~200px~~ |
|
|
30
|
+
| Vertical spacing | 150px | ~~100px~~, ~~200px~~ |
|
|
31
|
+
| Node size | `{ width: 180, height: 70 }` in both `style` and `measured` | any other size |
|
|
32
|
+
| Edge pairs | Every forward edge needs a return edge | one-way edges |
|
|
33
|
+
|
|
34
|
+
**Banned data field names** — these are silently ignored by the renderer:
|
|
35
|
+
`headline`, `subheadline`, `bullets`, `speakerNote`, `speakerNotes`, `visualHint`, `theme`, `title`, `body`, `text`, `background`, `showLogo`, `keyMessage`, `claim`, `hook`, `description`, `summary`
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Node Structure
|
|
40
|
+
|
|
41
|
+
Every node in the graph has this wrapper:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"id": "unique-slug",
|
|
46
|
+
"type": "huma",
|
|
47
|
+
"position": { "x": 0, "y": 0 },
|
|
48
|
+
"data": { /* SlideNodeData fields */ },
|
|
49
|
+
"style": { "width": 180, "height": 70 },
|
|
50
|
+
"measured": { "width": 180, "height": 70 }
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- `id`: Kebab-case slug (e.g. `"cover"`, `"problem-detail"`, `"feat-3d"`)
|
|
55
|
+
- `type`: Always `"huma"` — the custom node type with directional handles
|
|
56
|
+
- `position`: `{ x, y }` in graph canvas coordinates (see Positioning Grid below)
|
|
57
|
+
- `style.width`: Always `180`. `style.height`: Always `70`.
|
|
58
|
+
- `measured`: Must mirror `style` — `{ width: 180, height: 70 }`
|
|
59
|
+
- Optional: `style.backgroundColor` — hex string for node background in the graph editor (also applied as slide background)
|
|
60
|
+
|
|
61
|
+
### SlideNodeData Fields
|
|
62
|
+
|
|
63
|
+
#### Content Fields
|
|
64
|
+
|
|
65
|
+
| Field | Type | Default | Description |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| `label` | `string?` | — | Slide title displayed in graph editor and as slide heading |
|
|
68
|
+
| `topic` | `string?` | — | Section badge on the slide (e.g. `"01 / Problem"`, `"Solution"`) |
|
|
69
|
+
| `content` | `string?` | — | Markdown body. Supports headings, bullets, bold, code, tables, `[chart:name]` embeds, `` images/videos, `[text](#nodeId)` navigation links |
|
|
70
|
+
| `notes` | `string?` | — | Speaker notes, shown only in the presenter panel |
|
|
71
|
+
|
|
72
|
+
#### Slide Type
|
|
73
|
+
|
|
74
|
+
| Field | Type | Default | Description |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| `type` | `"content" \| "r3f" \| "chart" \| "custom"` | `"content"` | Slide renderer. `"content"` for standard text, `"r3f"` for 3D scene, `"chart"` for full-viewport chart |
|
|
77
|
+
|
|
78
|
+
#### Layout & Display
|
|
79
|
+
|
|
80
|
+
| Field | Type | Default | Description |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `centered` | `boolean?` | `true` | Center content vertically and horizontally |
|
|
83
|
+
| `layout` | `"single" \| "two-column"` | `"single"` | Content layout. `"two-column"` splits on `---` delimiter in content |
|
|
84
|
+
| `lightText` | `boolean?` | `false` | Force white text for dark backgrounds |
|
|
85
|
+
| `brandFont` | `boolean?` | `false` | Use HumaDisplay display font for the title |
|
|
86
|
+
| `showBranding` | `boolean?` | `true` | Show branding overlay |
|
|
87
|
+
| `brandingText` | `string?` | — | Bottom-left branding label (e.g. `"huma.energy"`) |
|
|
88
|
+
|
|
89
|
+
#### Background Media
|
|
90
|
+
|
|
91
|
+
| Field | Type | Default | Description |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| `backgroundImage` | `string?` | — | URL to background image |
|
|
94
|
+
| `backgroundImageFit` | `"cover" \| "contain"` | `"cover"` | How image fills the slide |
|
|
95
|
+
| `backgroundImageOverlay` | `boolean?` | `false` | Dark scrim over background image for text readability |
|
|
96
|
+
| `backgroundVideo` | `string?` | — | URL to background video |
|
|
97
|
+
| `backgroundVideoFit` | `"cover" \| "contain"` | `"cover"` | How video fills the slide |
|
|
98
|
+
| `backgroundVideoLoop` | `boolean?` | `true` | Loop background video |
|
|
99
|
+
|
|
100
|
+
#### Inline Video
|
|
101
|
+
|
|
102
|
+
| Field | Type | Default | Description |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| `inlineVideoControls` | `boolean?` | `true` | Show controls on inline videos in `content` |
|
|
105
|
+
| `inlineVideoAutoplay` | `boolean?` | `true` | Autoplay inline videos |
|
|
106
|
+
| `inlineVideoLoop` | `boolean?` | `true` | Loop inline videos |
|
|
107
|
+
|
|
108
|
+
#### R3F Scene (when `type: "r3f"`)
|
|
109
|
+
|
|
110
|
+
| Field | Type | Default | Description |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `scene.component` | `string` | — | Registry key (e.g. `"rotating-cube"`, `"particle-field"`) |
|
|
113
|
+
| `scene.props` | `Record<string, unknown>?` | — | Props passed to the scene component |
|
|
114
|
+
| `scene.controls` | `boolean?` | — | Enable OrbitControls for user interaction |
|
|
115
|
+
| `scene.background` | `string?` | — | Scene background hex color |
|
|
116
|
+
|
|
117
|
+
#### Charts
|
|
118
|
+
|
|
119
|
+
**Full-viewport chart** (when `type: "chart"`):
|
|
120
|
+
|
|
121
|
+
| Field | Type | Default | Description |
|
|
122
|
+
|---|---|---|---|
|
|
123
|
+
| `chart.chartType` | `"bar" \| "line" \| "area" \| "pie" \| "radar"` | — | Chart renderer |
|
|
124
|
+
| `chart.data` | `Array<Record<string, unknown>>` | — | Data array |
|
|
125
|
+
| `chart.config.xKey` | `string?` | — | X-axis data key |
|
|
126
|
+
| `chart.config.yKeys` | `string[]?` | — | Y-axis data keys |
|
|
127
|
+
| `chart.config.colors` | `string[]?` | — | Series colors |
|
|
128
|
+
| `chart.config.showGrid` | `boolean?` | — | Show grid lines |
|
|
129
|
+
| `chart.config.showLegend` | `boolean?` | — | Show legend |
|
|
130
|
+
|
|
131
|
+
**Inline charts** (referenced via `[chart:name]` in content):
|
|
132
|
+
|
|
133
|
+
| Field | Type | Default | Description |
|
|
134
|
+
|---|---|---|---|
|
|
135
|
+
| `charts` | `Record<string, ChartConfig>` | — | Named chart configurations. Key is referenced in content as `[chart:keyname]` |
|
|
136
|
+
|
|
137
|
+
Each `ChartConfig` has the same shape as `chart` above (`chartType`, `data`, `config`).
|
|
138
|
+
|
|
139
|
+
#### Other
|
|
140
|
+
|
|
141
|
+
| Field | Type | Default | Description |
|
|
142
|
+
|---|---|---|---|
|
|
143
|
+
| `sceneGroup` | — | — | Scene group reference |
|
|
144
|
+
| `focus` | — | — | Focus state |
|
|
145
|
+
|
|
146
|
+
### Content Markdown Features
|
|
147
|
+
|
|
148
|
+
The `content` field supports:
|
|
149
|
+
|
|
150
|
+
- `## Heading` — headings (ATX style only, no setext)
|
|
151
|
+
- `- bullet` or `* bullet` — unordered lists
|
|
152
|
+
- `1. item` — ordered lists
|
|
153
|
+
- `` ```language ``` `` — syntax-highlighted code blocks
|
|
154
|
+
- `> blockquote` — styled blockquotes
|
|
155
|
+
- `**bold**` and `*italic*` — inline formatting
|
|
156
|
+
- `| col | col |` — GFM tables
|
|
157
|
+
- `` — images; `.mp4/.webm/.mov` URLs render as inline video
|
|
158
|
+
- `[text](url)` — external links (open in new tab)
|
|
159
|
+
- `[text](#nodeId)` — navigation links to other slides
|
|
160
|
+
- `[chart:name]` — inline chart embed (must be on its own line)
|
|
161
|
+
- `---` — column delimiter when `layout: "two-column"`
|
|
162
|
+
- Single newline = visible line break (not collapsed)
|
|
163
|
+
- Multiple blank lines = visible vertical spacing
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Edge Wiring
|
|
168
|
+
|
|
169
|
+
Edges define valid navigation paths — without correct edges, arrow keys won't work.
|
|
170
|
+
|
|
171
|
+
### Handle IDs
|
|
172
|
+
|
|
173
|
+
Eight handle IDs exist, four source and four target:
|
|
174
|
+
|
|
175
|
+
| Handle ID | Type | Position | Navigation |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| `s-right` | source | Right side | Pressing **right arrow** follows this edge |
|
|
178
|
+
| `s-left` | source | Left side | Pressing **left arrow** follows this edge |
|
|
179
|
+
| `s-bottom` | source | Bottom | Pressing **down arrow** follows this edge |
|
|
180
|
+
| `s-top` | source | Top | Pressing **up arrow** follows this edge |
|
|
181
|
+
| `t-right` | target | Right side | Arrived via **left arrow** from source |
|
|
182
|
+
| `t-left` | target | Left side | Arrived via **right arrow** from source |
|
|
183
|
+
| `t-bottom` | target | Bottom | Arrived via **up arrow** from source |
|
|
184
|
+
| `t-top` | target | Top | Arrived via **down arrow** from source |
|
|
185
|
+
|
|
186
|
+
### Bidirectional Pair Rule
|
|
187
|
+
|
|
188
|
+
**Every navigation edge must have a return edge** with swapped source/target and swapped handles.
|
|
189
|
+
|
|
190
|
+
If the user can press right to go from A to B, they must be able to press left to go from B back to A.
|
|
191
|
+
|
|
192
|
+
### Standard Edge Pairs
|
|
193
|
+
|
|
194
|
+
**Horizontal Forward/Back (Spine Navigation):**
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{ "id": "e-a-b", "source": "a", "target": "b", "sourceHandle": "s-right", "targetHandle": "t-left" }
|
|
198
|
+
{ "id": "e-b-a", "source": "b", "target": "a", "sourceHandle": "s-left", "targetHandle": "t-right" }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Drill-Down / Return-to-Parent:**
|
|
202
|
+
|
|
203
|
+
```json
|
|
204
|
+
{ "id": "e-parent-child", "source": "parent", "target": "child", "sourceHandle": "s-bottom", "targetHandle": "t-top" }
|
|
205
|
+
{ "id": "e-child-parent", "source": "child", "target": "parent", "sourceHandle": "s-top", "targetHandle": "t-bottom" }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Horizontal Within a Drill-Down Branch (Siblings):**
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{ "id": "e-child1-child2", "source": "child1", "target": "child2", "sourceHandle": "s-right", "targetHandle": "t-left" }
|
|
212
|
+
{ "id": "e-child2-child1", "source": "child2", "target": "child1", "sourceHandle": "s-left", "targetHandle": "t-right" }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Edge ID Convention
|
|
216
|
+
|
|
217
|
+
Pattern: `e-{source}-{target}` using the node IDs or abbreviated forms.
|
|
218
|
+
|
|
219
|
+
Examples: `e-cover-problem`, `e-problem-cover`, `e-problem-detail`, `e-detail-problem`
|
|
220
|
+
|
|
221
|
+
### Edge Object Structure
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"id": "e-cover-problem",
|
|
226
|
+
"source": "cover",
|
|
227
|
+
"target": "problem",
|
|
228
|
+
"sourceHandle": "s-right",
|
|
229
|
+
"targetHandle": "t-left"
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
All four fields (`source`, `target`, `sourceHandle`, `targetHandle`) are required. Always set handles explicitly.
|
|
234
|
+
|
|
235
|
+
### Validation Checklist
|
|
236
|
+
|
|
237
|
+
1. Every node has at least one outgoing edge — no dead ends
|
|
238
|
+
2. Every forward edge has a paired return edge — source/target swapped, handles swapped
|
|
239
|
+
3. All handle IDs are from the valid set — only the 8 IDs listed above
|
|
240
|
+
4. Source handles start with `s-` and target handles start with `t-`
|
|
241
|
+
5. The first spine node has no incoming `s-right` edge — it's the leftmost entry point
|
|
242
|
+
6. The last spine node has no outgoing `s-right` edge (or loops back to cover)
|
|
243
|
+
7. Drill-down children always have `s-top`/`t-bottom` return edge to parent
|
|
244
|
+
8. No duplicate edge IDs
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Positioning Grid
|
|
249
|
+
|
|
250
|
+
### Grid Constants
|
|
251
|
+
|
|
252
|
+
| Parameter | Value |
|
|
253
|
+
|---|---|
|
|
254
|
+
| Horizontal spacing | 240px |
|
|
255
|
+
| Vertical spacing | 150px |
|
|
256
|
+
| Node width | 180px (in `style` and `measured`) |
|
|
257
|
+
| Node height | 70px (in `style` and `measured`) |
|
|
258
|
+
|
|
259
|
+
### Spine Row
|
|
260
|
+
|
|
261
|
+
All spine nodes sit at `y: 0`, starting at `x: 0`, incrementing by 240 per node.
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
x: 0 240 480 720 960 1200
|
|
265
|
+
y: 0 [Cover] [Problem] [Solution] [Value] [Features] [CTA]
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Drill-Down Rows
|
|
269
|
+
|
|
270
|
+
Drill-down children are placed directly below their parent:
|
|
271
|
+
|
|
272
|
+
- First level: `y: 150`
|
|
273
|
+
- Second level: `y: 300`
|
|
274
|
+
|
|
275
|
+
The first child inherits the parent's `x` position. Additional siblings at the same depth increment `x` by 240.
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
x: 720 960
|
|
279
|
+
y:0 [Value]
|
|
280
|
+
|
|
|
281
|
+
y:150 [Case A] [Case B]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
- First child: same `x` as parent (720)
|
|
285
|
+
- Second child: parent `x` + 240 (960)
|
|
286
|
+
- Third child: parent `x` + 480 (1200)
|
|
287
|
+
|
|
288
|
+
### Position Quick Reference
|
|
289
|
+
|
|
290
|
+
| Node | x | y | Role |
|
|
291
|
+
|---|---|---|---|
|
|
292
|
+
| cover | 0 | 0 | Spine 1 |
|
|
293
|
+
| problem | 240 | 0 | Spine 2 |
|
|
294
|
+
| problem-detail | 240 | 150 | Drill-down under problem |
|
|
295
|
+
| solution | 480 | 0 | Spine 3 |
|
|
296
|
+
| solution-how | 480 | 150 | Drill-down under solution |
|
|
297
|
+
| value | 720 | 0 | Spine 4 |
|
|
298
|
+
| value-case | 720 | 150 | Drill-down under value |
|
|
299
|
+
| cta | 960 | 0 | Spine 5 |
|
|
300
|
+
|
|
301
|
+
### Rules
|
|
302
|
+
|
|
303
|
+
1. Position reflects visual layout in the editor — it has no effect on navigation
|
|
304
|
+
2. Navigation is determined solely by edges and their handle assignments
|
|
305
|
+
3. Spine nodes must be at `y: 0` for visual clarity
|
|
306
|
+
4. Drill-downs must be below their parent
|
|
307
|
+
5. No two nodes should overlap (maintain at least 240px horizontal, 150px vertical separation)
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Design Decisions
|
|
312
|
+
|
|
313
|
+
### Slide Type Selection
|
|
314
|
+
|
|
315
|
+
| Content Signal | `type` | Key Fields |
|
|
316
|
+
|---|---|---|
|
|
317
|
+
| Standard text, bullets, headings | `"content"` (default) | `content`, `layout` |
|
|
318
|
+
| Full-viewport data visualization | `"chart"` | `chart` (ChartConfig) |
|
|
319
|
+
| Interactive 3D scene | `"r3f"` | `scene` (R3FSceneConfig) |
|
|
320
|
+
|
|
321
|
+
Most slides are `"content"`. Use `"chart"` or `"r3f"` only when the content is primarily a visualization.
|
|
322
|
+
|
|
323
|
+
### Layout Decisions
|
|
324
|
+
|
|
325
|
+
**When to use `two-column`** — set `layout: "two-column"` and split content on `---`:
|
|
326
|
+
|
|
327
|
+
- Comparison: before/after, old/new, us/them
|
|
328
|
+
- Pros/cons: advantages on left, considerations on right
|
|
329
|
+
- Text + chart: explanation on left, `[chart:name]` on right
|
|
330
|
+
- Text + media: bullets on left, `` on right
|
|
331
|
+
- Dual evidence: two independent supporting points side by side
|
|
332
|
+
|
|
333
|
+
Do not force two-column when content is naturally sequential.
|
|
334
|
+
|
|
335
|
+
**When to center** — set `centered: true` for:
|
|
336
|
+
|
|
337
|
+
- Cover slides: Title + subtitle, branded
|
|
338
|
+
- Call-to-action slides: Single message, end of presentation
|
|
339
|
+
- Single-message impact slides: One powerful statement or quote
|
|
340
|
+
- Transition slides: Brief pause between major sections
|
|
341
|
+
|
|
342
|
+
Set `centered: false` for content-heavy slides, data slides, comparison slides.
|
|
343
|
+
|
|
344
|
+
**When to use brand font** — set `brandFont: true` for:
|
|
345
|
+
|
|
346
|
+
- Cover slide (first slide)
|
|
347
|
+
- Closing/CTA slide (last slide)
|
|
348
|
+
- High-impact single-message slides
|
|
349
|
+
|
|
350
|
+
Do not use on content-heavy or data slides — brand fonts are display fonts, not body fonts.
|
|
351
|
+
|
|
352
|
+
**When to show branding** — set `showBranding: true` and `brandingText` for:
|
|
353
|
+
|
|
354
|
+
- Cover slide, closing slide, slides likely to be screenshotted
|
|
355
|
+
|
|
356
|
+
Set `showBranding: false` for immersive slides (R3F, full-bleed video) where the overlay is distracting.
|
|
357
|
+
|
|
358
|
+
### Background Treatment
|
|
359
|
+
|
|
360
|
+
**Background image** — use when the outline indicates mood, atmosphere, visual evidence, or impact:
|
|
361
|
+
|
|
362
|
+
- Set `backgroundImageOverlay: true` and `lightText: true` when text appears over the image
|
|
363
|
+
- Image URLs: use Unsplash with query params `?w=1920&q=80`
|
|
364
|
+
|
|
365
|
+
**Background video** — use for cinematic section openers or demo context:
|
|
366
|
+
|
|
367
|
+
- Set `backgroundVideo` to a placeholder: `"PLACEHOLDER: [description of needed video]"`
|
|
368
|
+
- Set `backgroundVideoFit: "cover"` and `backgroundVideoLoop: true`
|
|
369
|
+
- Add to delivery summary for manual upload
|
|
370
|
+
|
|
371
|
+
**Inline video** — for videos embedded via `` in content:
|
|
372
|
+
|
|
373
|
+
- Set URL to placeholder, configure `inlineVideoControls`, `inlineVideoAutoplay`, `inlineVideoLoop`
|
|
374
|
+
- Add to delivery summary
|
|
375
|
+
|
|
376
|
+
**Text contrast** — set `lightText: true` whenever background is dark:
|
|
377
|
+
|
|
378
|
+
- Dark `style.backgroundColor`
|
|
379
|
+
- `backgroundImage` with `backgroundImageOverlay: true`
|
|
380
|
+
- `backgroundVideo` slides
|
|
381
|
+
- `type: "r3f"` with dark scene background
|
|
382
|
+
|
|
383
|
+
### Chart Decisions
|
|
384
|
+
|
|
385
|
+
**Full-viewport chart** (`type: "chart"`) — when the data IS the slide:
|
|
386
|
+
|
|
387
|
+
```json
|
|
388
|
+
{
|
|
389
|
+
"type": "chart",
|
|
390
|
+
"chart": {
|
|
391
|
+
"chartType": "bar",
|
|
392
|
+
"data": [...],
|
|
393
|
+
"config": { "xKey": "quarter", "yKeys": ["revenue", "cost"], "showGrid": true, "showLegend": true }
|
|
394
|
+
},
|
|
395
|
+
"content": "Optional caption below the chart"
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
**Inline chart** (`[chart:name]` in content) — when data supports a text argument:
|
|
400
|
+
|
|
401
|
+
```json
|
|
402
|
+
{
|
|
403
|
+
"content": "## Why this approach wins\n\n[chart:comparison]\n\nClosing statement.",
|
|
404
|
+
"charts": {
|
|
405
|
+
"comparison": {
|
|
406
|
+
"chartType": "radar",
|
|
407
|
+
"data": [...],
|
|
408
|
+
"config": { "xKey": "axis", "yKeys": ["ours", "theirs"], "showLegend": true }
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Chart title rule** — `label` states the insight, not the data category:
|
|
415
|
+
|
|
416
|
+
| Weak | Strong |
|
|
417
|
+
|---|---|
|
|
418
|
+
| "Revenue Data" | "Revenue grew 40% QoQ after the pricing change" |
|
|
419
|
+
| "Response Time" | "Response time dropped 60% after caching rollout" |
|
|
420
|
+
|
|
421
|
+
**Chart type selection:**
|
|
422
|
+
|
|
423
|
+
| Data Pattern | Chart Type |
|
|
424
|
+
|---|---|
|
|
425
|
+
| Trend over time | `"line"` or `"area"` |
|
|
426
|
+
| Category comparison | `"bar"` |
|
|
427
|
+
| Multi-axis comparison | `"radar"` |
|
|
428
|
+
| Part-of-whole | `"pie"` |
|
|
429
|
+
| Volume trend | `"area"` |
|
|
430
|
+
|
|
431
|
+
### R3F Scene Decisions
|
|
432
|
+
|
|
433
|
+
Available scenes:
|
|
434
|
+
- `"rotating-cube"` — interactive cube, good for tech demos. Set `scene.controls: true`.
|
|
435
|
+
- `"particle-field"` — atmospheric particle cloud. Set `scene.controls: false`.
|
|
436
|
+
|
|
437
|
+
Always set `lightText: true` for R3F slides (dark backgrounds).
|
|
438
|
+
|
|
439
|
+
### Topic Badge Conventions
|
|
440
|
+
|
|
441
|
+
Format: `"NN / Section Name"` for numbered sections:
|
|
442
|
+
- `"01 / Problem"`, `"02 / Solution"`, `"03 / Value"`, `"04 / Evidence"`
|
|
443
|
+
|
|
444
|
+
Or plain text: `"Huma Showcase"`, `"Technical Deep Dive"`.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Visual Intent Mapping
|
|
449
|
+
|
|
450
|
+
The narrative agent annotates each slide with a `**Visual intent:**` label. Map them to JSON treatments:
|
|
451
|
+
|
|
452
|
+
| Visual Intent | type | centered | layout | brandFont | Background | lightText | Notes |
|
|
453
|
+
|---|---|---|---|---|---|---|---|
|
|
454
|
+
| `bookend` | content | true | single | true | optional image/color | if dark bg | Cover and CTA slides. showBranding: true |
|
|
455
|
+
| `chapter-opener` | content | false or true | single | false | backgroundImage + overlay | true | Full-bleed image, sets section mood. Max 3 bullet points. |
|
|
456
|
+
| `impact` | content | true | single | false | optional dark color | if dark bg | Single statement, no bullets. Let whitespace do the work. |
|
|
457
|
+
| `workhorse` | content | false | single or two-column | false | none | false | Standard bullets/content. The default treatment. |
|
|
458
|
+
| `evidence` | content or chart | false | single or two-column | false | none | false | Data-forward: chart, table, or comparison. |
|
|
459
|
+
| `breathing-room` | content | true | single | false | backgroundImage + overlay OR style.backgroundColor | true | Minimal text. Visual pause. Resets audience attention. |
|
|
460
|
+
|
|
461
|
+
When the outline lacks visual intent annotations, infer them:
|
|
462
|
+
- First slide → bookend
|
|
463
|
+
- Last slide → bookend
|
|
464
|
+
- Slide after 2+ consecutive bullet-heavy slides → breathing-room or impact
|
|
465
|
+
- Slide introducing a new topic section → chapter-opener
|
|
466
|
+
- Slide with chart data or comparison → evidence
|
|
467
|
+
- Everything else → workhorse
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## Slide Recipes
|
|
472
|
+
|
|
473
|
+
Complete `data` field patterns for common slide types.
|
|
474
|
+
|
|
475
|
+
### Cover / Call to Action
|
|
476
|
+
|
|
477
|
+
```json
|
|
478
|
+
{
|
|
479
|
+
"label": "[Title or Ask]",
|
|
480
|
+
"topic": "[Company or Meeting Context]",
|
|
481
|
+
"content": "One sentence: what this is and for whom. (Cover) — or — what happens next and when. (CTA)",
|
|
482
|
+
"centered": true,
|
|
483
|
+
"brandFont": true,
|
|
484
|
+
"showBranding": true,
|
|
485
|
+
"brandingText": "[company.domain]"
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Standard Bullets
|
|
490
|
+
|
|
491
|
+
```json
|
|
492
|
+
{
|
|
493
|
+
"label": "Headline claim that makes the point",
|
|
494
|
+
"topic": "01 / Problem",
|
|
495
|
+
"content": "## Headline claim in one sentence\n\n- Specific point with a number or named entity\n- Second point — consequence or contrast\n- Third point — the implication\n\nClosing sentence bridging to the next slide.",
|
|
496
|
+
"notes": "Talking point not on screen. Objection signal: 'If they ask X, navigate to [node-id].' Time: 2 minutes max.",
|
|
497
|
+
"centered": false
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### Impact Statement
|
|
502
|
+
|
|
503
|
+
```json
|
|
504
|
+
{
|
|
505
|
+
"label": "The Shift",
|
|
506
|
+
"topic": "01 / Problem",
|
|
507
|
+
"content": "The market has permanently changed.\n\n**Batch scheduling is structurally incompatible with same-day delivery expectations.**",
|
|
508
|
+
"notes": "Pause. Do not advance immediately.",
|
|
509
|
+
"centered": true
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Two-Column Comparison
|
|
514
|
+
|
|
515
|
+
```json
|
|
516
|
+
{
|
|
517
|
+
"label": "Old Way vs. New Way",
|
|
518
|
+
"topic": "02 / Solution",
|
|
519
|
+
"content": "## Today\n- 3 systems, no shared state\n- 11 hours/week reconciling exports\n\n---\n\n## With [Product]\n- Single live record across all systems\n- Reports update in real time",
|
|
520
|
+
"layout": "two-column",
|
|
521
|
+
"centered": false
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Text + Inline Chart
|
|
526
|
+
|
|
527
|
+
```json
|
|
528
|
+
{
|
|
529
|
+
"label": "The Data Confirms It",
|
|
530
|
+
"topic": "03 / Evidence",
|
|
531
|
+
"content": "## Continuous monitoring outperforms batch on every metric\n\nThree field studies. Same result each time.\n\n---\n\n[chart:comparison]",
|
|
532
|
+
"charts": {
|
|
533
|
+
"comparison": {
|
|
534
|
+
"chartType": "bar",
|
|
535
|
+
"data": [{ "metric": "Uptime %", "ours": 98.2, "baseline": 91.5 }],
|
|
536
|
+
"config": { "xKey": "metric", "yKeys": ["ours", "baseline"], "showGrid": true, "showLegend": true }
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
"layout": "two-column",
|
|
540
|
+
"centered": false
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Proof Point / Customer Quote
|
|
545
|
+
|
|
546
|
+
```json
|
|
547
|
+
{
|
|
548
|
+
"label": "Summit Health: Year One",
|
|
549
|
+
"topic": "04 / Proof",
|
|
550
|
+
"content": "## 2.3 hours of admin time eliminated per nurse per shift\n\n> \"The conflicts surfaced automatically.\"\n> — VP Operations, Summit Health (42 facilities)\n\n- 38% reduction in shift coverage failures\n- Full rollout across 6 units in 11 weeks",
|
|
551
|
+
"notes": "If they ask about rollout timeline, the drill-down below has the full breakdown.",
|
|
552
|
+
"centered": false
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### Section Opener with Background Image
|
|
557
|
+
|
|
558
|
+
```json
|
|
559
|
+
{
|
|
560
|
+
"label": "The Opportunity",
|
|
561
|
+
"topic": "03 / Opportunity",
|
|
562
|
+
"content": "## A $40B market with no dominant platform\n\nEvery competitor is solving scheduling.\nNobody is solving the coordination layer.",
|
|
563
|
+
"backgroundImage": "https://images.unsplash.com/photo-XXXXX?w=1920&q=80",
|
|
564
|
+
"backgroundImageFit": "cover",
|
|
565
|
+
"backgroundImageOverlay": true,
|
|
566
|
+
"lightText": true,
|
|
567
|
+
"centered": false
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Styled Background Color Slide
|
|
572
|
+
|
|
573
|
+
```json
|
|
574
|
+
{
|
|
575
|
+
"label": "Styling & Branding",
|
|
576
|
+
"topic": "04 / Features",
|
|
577
|
+
"content": "## Per-slide styling\n\n- Custom background colors\n- Light text mode for dark backgrounds\n- Brand font for titles",
|
|
578
|
+
"lightText": true,
|
|
579
|
+
"brandFont": true,
|
|
580
|
+
"centered": true,
|
|
581
|
+
"showBranding": true,
|
|
582
|
+
"brandingText": "huma.energy"
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
With `style.backgroundColor` set at the node level:
|
|
587
|
+
|
|
588
|
+
```json
|
|
589
|
+
"style": { "width": 180, "height": 70, "backgroundColor": "#1a1a2e" }
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## Complete Reference Example
|
|
595
|
+
|
|
596
|
+
A 7-node presentation demonstrating key slide types. This is the minimum viable structure the design agent should know how to produce.
|
|
597
|
+
|
|
598
|
+
```json
|
|
599
|
+
{
|
|
600
|
+
"meta": {
|
|
601
|
+
"name": "Graph-Based Presentations"
|
|
602
|
+
},
|
|
603
|
+
"nodes": [
|
|
604
|
+
{
|
|
605
|
+
"id": "cover",
|
|
606
|
+
"type": "huma",
|
|
607
|
+
"position": { "x": 0, "y": 0 },
|
|
608
|
+
"data": {
|
|
609
|
+
"label": "Graph-Based Presentations",
|
|
610
|
+
"topic": "Huma Showcase",
|
|
611
|
+
"content": "Non-linear storytelling for technical teams.\n\nNavigate with arrow keys. Press **down** to drill into any topic.",
|
|
612
|
+
"centered": true,
|
|
613
|
+
"brandFont": true,
|
|
614
|
+
"showBranding": true,
|
|
615
|
+
"brandingText": "huma.energy"
|
|
616
|
+
},
|
|
617
|
+
"style": { "width": 180, "height": 70 },
|
|
618
|
+
"measured": { "width": 180, "height": 70 }
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
"id": "problem",
|
|
622
|
+
"type": "huma",
|
|
623
|
+
"position": { "x": 240, "y": 0 },
|
|
624
|
+
"data": {
|
|
625
|
+
"label": "The Problem",
|
|
626
|
+
"topic": "01 / Problem",
|
|
627
|
+
"content": "## Linear tools break complex stories\n\n- Cannot branch based on audience questions\n- Skipping sections breaks the flow\n- Presenters get lost navigating dense topics\n- No private view for speaker notes during screen share\n\nEvery meeting is different. The presentation should adapt.",
|
|
628
|
+
"notes": "Pause here and ask: 'Does this match what you experience with your current tooling?' Key talking point: the fourth bullet resonates most with technical presenters.",
|
|
629
|
+
"centered": false
|
|
630
|
+
},
|
|
631
|
+
"style": { "width": 180, "height": 70 },
|
|
632
|
+
"measured": { "width": 180, "height": 70 }
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
"id": "problem-detail",
|
|
636
|
+
"type": "huma",
|
|
637
|
+
"position": { "x": 240, "y": 150 },
|
|
638
|
+
"data": {
|
|
639
|
+
"label": "Value Breakdown",
|
|
640
|
+
"topic": "03 / Value",
|
|
641
|
+
"content": "## Flexible narratives\nAdapt flow to audience type and questions in real time\n\n---\n\n## Consistent style\nReuse front-end web styling and components directly\n\n## Professional delivery\nSpeaker notes and timing without the audience seeing them",
|
|
642
|
+
"layout": "two-column",
|
|
643
|
+
"centered": false
|
|
644
|
+
},
|
|
645
|
+
"style": { "width": 180, "height": 70 },
|
|
646
|
+
"measured": { "width": 180, "height": 70 }
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
"id": "value",
|
|
650
|
+
"type": "huma",
|
|
651
|
+
"position": { "x": 480, "y": 0 },
|
|
652
|
+
"data": {
|
|
653
|
+
"label": "The Value",
|
|
654
|
+
"topic": "03 / Value",
|
|
655
|
+
"content": "## Why this approach wins\n\n[chart:value]\n\nAdapt to the audience. Reuse web components. Deliver with confidence.",
|
|
656
|
+
"charts": {
|
|
657
|
+
"value": {
|
|
658
|
+
"chartType": "radar",
|
|
659
|
+
"data": [
|
|
660
|
+
{ "axis": "Flexibility", "graph": 95, "traditional": 30 },
|
|
661
|
+
{ "axis": "Visual Quality", "graph": 90, "traditional": 50 },
|
|
662
|
+
{ "axis": "Interactivity", "graph": 85, "traditional": 20 },
|
|
663
|
+
{ "axis": "Ease of Use", "graph": 60, "traditional": 90 },
|
|
664
|
+
{ "axis": "Consistency", "graph": 85, "traditional": 40 },
|
|
665
|
+
{ "axis": "Tech Signal", "graph": 95, "traditional": 15 }
|
|
666
|
+
],
|
|
667
|
+
"config": {
|
|
668
|
+
"xKey": "axis",
|
|
669
|
+
"yKeys": ["graph", "traditional"],
|
|
670
|
+
"showGrid": true,
|
|
671
|
+
"showLegend": true
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
"centered": false
|
|
676
|
+
},
|
|
677
|
+
"style": { "width": 180, "height": 70 },
|
|
678
|
+
"measured": { "width": 180, "height": 70 }
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
"id": "feat-3d",
|
|
682
|
+
"type": "huma",
|
|
683
|
+
"position": { "x": 720, "y": 0 },
|
|
684
|
+
"data": {
|
|
685
|
+
"label": "3D Scenes",
|
|
686
|
+
"topic": "04 / Features",
|
|
687
|
+
"type": "r3f",
|
|
688
|
+
"scene": {
|
|
689
|
+
"component": "rotating-cube",
|
|
690
|
+
"controls": true,
|
|
691
|
+
"background": "#1a1a2e"
|
|
692
|
+
},
|
|
693
|
+
"content": "Interactive React Three Fiber scene.\n\nDrag to rotate. Scroll to zoom. Scenes load from a component registry.",
|
|
694
|
+
"lightText": true
|
|
695
|
+
},
|
|
696
|
+
"style": { "width": 180, "height": 70 },
|
|
697
|
+
"measured": { "width": 180, "height": 70 }
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
"id": "feat-bgimage",
|
|
701
|
+
"type": "huma",
|
|
702
|
+
"position": { "x": 960, "y": 0 },
|
|
703
|
+
"data": {
|
|
704
|
+
"label": "Background Image",
|
|
705
|
+
"topic": "04 / Features",
|
|
706
|
+
"content": "## Full-bleed imagery\n\nUpload any image as a slide background. Toggle overlay for text readability.\n\n- Cover/contain fit modes\n- Dark overlay toggle\n- Light text auto-detection",
|
|
707
|
+
"backgroundImage": "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1920&q=80",
|
|
708
|
+
"backgroundImageFit": "cover",
|
|
709
|
+
"backgroundImageOverlay": true,
|
|
710
|
+
"lightText": true,
|
|
711
|
+
"centered": false
|
|
712
|
+
},
|
|
713
|
+
"style": { "width": 180, "height": 70 },
|
|
714
|
+
"measured": { "width": 180, "height": 70 }
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
"id": "end",
|
|
718
|
+
"type": "huma",
|
|
719
|
+
"position": { "x": 1200, "y": 0 },
|
|
720
|
+
"data": {
|
|
721
|
+
"label": "Get Started",
|
|
722
|
+
"content": "Fork this template to build your own.\n\nEvery slide, chart, and scene is editable in the graph editor.",
|
|
723
|
+
"centered": true,
|
|
724
|
+
"brandFont": true,
|
|
725
|
+
"showBranding": true,
|
|
726
|
+
"brandingText": "huma.energy"
|
|
727
|
+
},
|
|
728
|
+
"style": { "width": 180, "height": 70 },
|
|
729
|
+
"measured": { "width": 180, "height": 70 }
|
|
730
|
+
}
|
|
731
|
+
],
|
|
732
|
+
"edges": [
|
|
733
|
+
{ "id": "e-cover-problem", "source": "cover", "target": "problem", "sourceHandle": "s-right", "targetHandle": "t-left" },
|
|
734
|
+
{ "id": "e-problem-cover", "source": "problem", "target": "cover", "sourceHandle": "s-left", "targetHandle": "t-right" },
|
|
735
|
+
{ "id": "e-problem-value", "source": "problem", "target": "value", "sourceHandle": "s-right", "targetHandle": "t-left" },
|
|
736
|
+
{ "id": "e-value-problem", "source": "value", "target": "problem", "sourceHandle": "s-left", "targetHandle": "t-right" },
|
|
737
|
+
{ "id": "e-problem-detail", "source": "problem", "target": "problem-detail", "sourceHandle": "s-bottom", "targetHandle": "t-top" },
|
|
738
|
+
{ "id": "e-detail-problem", "source": "problem-detail", "target": "problem", "sourceHandle": "s-top", "targetHandle": "t-bottom" },
|
|
739
|
+
{ "id": "e-value-3d", "source": "value", "target": "feat-3d", "sourceHandle": "s-right", "targetHandle": "t-left" },
|
|
740
|
+
{ "id": "e-3d-value", "source": "feat-3d", "target": "value", "sourceHandle": "s-left", "targetHandle": "t-right" },
|
|
741
|
+
{ "id": "e-3d-bgimage", "source": "feat-3d", "target": "feat-bgimage", "sourceHandle": "s-right", "targetHandle": "t-left" },
|
|
742
|
+
{ "id": "e-bgimage-3d", "source": "feat-bgimage", "target": "feat-3d", "sourceHandle": "s-left", "targetHandle": "t-right" },
|
|
743
|
+
{ "id": "e-bgimage-end", "source": "feat-bgimage", "target": "end", "sourceHandle": "s-right", "targetHandle": "t-left" },
|
|
744
|
+
{ "id": "e-end-bgimage", "source": "end", "target": "feat-bgimage", "sourceHandle": "s-left", "targetHandle": "t-right" }
|
|
745
|
+
]
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Additional Patterns from the Demo
|
|
750
|
+
|
|
751
|
+
**Styled background color node** (node-level `style.backgroundColor`):
|
|
752
|
+
|
|
753
|
+
```json
|
|
754
|
+
{
|
|
755
|
+
"id": "feat-style",
|
|
756
|
+
"type": "huma",
|
|
757
|
+
"position": { "x": 1920, "y": 150 },
|
|
758
|
+
"data": {
|
|
759
|
+
"label": "Styling & Branding",
|
|
760
|
+
"topic": "04 / Features",
|
|
761
|
+
"content": "## Per-slide styling\n\n- Custom background colors\n- Light text mode for dark backgrounds\n- Brand font for titles",
|
|
762
|
+
"lightText": true,
|
|
763
|
+
"brandFont": true,
|
|
764
|
+
"centered": true,
|
|
765
|
+
"showBranding": true,
|
|
766
|
+
"brandingText": "huma.energy"
|
|
767
|
+
},
|
|
768
|
+
"style": { "width": 180, "height": 70, "backgroundColor": "#1a1a2e" },
|
|
769
|
+
"measured": { "width": 180, "height": 70 }
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
**Second-level drill-down** (y:300, child of a y:150 node):
|
|
774
|
+
|
|
775
|
+
```json
|
|
776
|
+
{
|
|
777
|
+
"id": "feat-tables",
|
|
778
|
+
"type": "huma",
|
|
779
|
+
"position": { "x": 960, "y": 300 },
|
|
780
|
+
"data": { "label": "Tables", "topic": "04 / Features", "content": "...", "centered": false },
|
|
781
|
+
"style": { "width": 180, "height": 70 },
|
|
782
|
+
"measured": { "width": 180, "height": 70 }
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
Edges: `feat-markdown` (y:150) → `feat-tables` (y:300) via `s-bottom`/`t-top`.
|
|
787
|
+
|
|
788
|
+
**Sibling drill-downs connected horizontally** (same y-level, `s-right`/`t-left`):
|
|
789
|
+
|
|
790
|
+
```json
|
|
791
|
+
{ "id": "e-md-charts", "source": "feat-markdown", "target": "feat-charts", "sourceHandle": "s-right", "targetHandle": "t-left" }
|
|
792
|
+
{ "id": "e-charts-md", "source": "feat-charts", "target": "feat-markdown", "sourceHandle": "s-left", "targetHandle": "t-right" }
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
Both at y:150, connected horizontally within the drill-down branch.
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
## Media Delivery Summary Format
|
|
800
|
+
|
|
801
|
+
After generating the JSON, list any slides with placeholder media:
|
|
802
|
+
|
|
803
|
+
```
|
|
804
|
+
Slides requiring manual media upload:
|
|
805
|
+
- "Cover" — background video: [description]
|
|
806
|
+
- "Demo" — inline video: [description]
|
|
807
|
+
|
|
808
|
+
Upload via POST /api/slide-images/upload (multipart/form-data, field: "file")
|
|
809
|
+
Returns { ok: true, key, url } — replace placeholder with returned url
|
|
810
|
+
Accepted: all image/*, video/mp4, video/webm, video/quicktime
|
|
811
|
+
Max size: 50MB
|
|
812
|
+
```
|