@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.
@@ -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, `![](url)` 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
+ - `![alt](url)` — 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, `![](image)` 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 `![alt](url)` 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
+ ```