lumina-slides 9.0.2 → 9.0.4

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.
@@ -1,8 +1,8 @@
1
1
  <template>
2
2
  <div class="min-h-screen pt-32 px-6 md:px-12 max-w-8xl mx-auto flex flex-col md:flex-row gap-16">
3
3
  <!-- Sidebar Navigation -->
4
- <aside class="w-full md:w-64 flex-shrink-0 hidden md:block">
5
- <div class="sticky top-32 space-y-10">
4
+ <aside class="w-full md:w-64 flex-shrink-0 block sticky top-32 self-start max-h-[calc(100vh-10rem)] overflow-y-auto">
5
+ <div class="space-y-10 py-1">
6
6
  <div v-for="(group, i) in navigation" :key="i">
7
7
  <!-- Group Title -->
8
8
  <h3 class="text-xs font-bold uppercase tracking-widest text-white/30 mb-4 pl-3">
@@ -12,14 +12,14 @@
12
12
  <!-- Links -->
13
13
  <ul class="space-y-1 relative border-l border-white/5">
14
14
  <li v-for="item in group.items" :key="item.id">
15
- <button @click="activeSection = item.id; scrollToTop()" :class="[
15
+ <a v-if="group.title === 'Layouts'" :href="'#' + item.id" @click.prevent="goToLayout(item.id)" :class="[
16
+ 'block group w-full text-left px-4 py-2 text-sm transition-all duration-300 border-l-2 -ml-[1px]',
17
+ activeSection === item.id ? 'border-blue-500 text-white font-medium bg-blue-500/5' : 'border-transparent text-white/50 hover:text-white hover:border-white/20'
18
+ ]">{{ item.label }}</a>
19
+ <button v-else @click="activeSection = item.id; scrollToTop()" :class="[
16
20
  'group w-full text-left px-4 py-2 text-sm transition-all duration-300 border-l-2 -ml-[1px]',
17
- activeSection === item.id
18
- ? 'border-blue-500 text-white font-medium bg-blue-500/5'
19
- : 'border-transparent text-white/50 hover:text-white hover:border-white/20'
20
- ]">
21
- {{ item.label }}
22
- </button>
21
+ activeSection === item.id ? 'border-blue-500 text-white font-medium bg-blue-500/5' : 'border-transparent text-white/50 hover:text-white hover:border-white/20'
22
+ ]">{{ item.label }}</button>
23
23
  </li>
24
24
  </ul>
25
25
  </div>
@@ -28,11 +28,10 @@
28
28
 
29
29
  <!-- Main Content Area -->
30
30
  <main class="flex-1 w-full min-w-0 pb-32">
31
- <Transition name="fade" mode="out-in">
32
- <div :key="activeSection" class="doc-content max-w-6xl">
31
+ <div class="doc-content max-w-6xl">
33
32
 
34
33
  <!-- MEDIA & VIDEO -->
35
- <div v-if="activeSection === 'media'">
34
+ <div v-if="activeSection === 'media'" id="media">
36
35
  <h1>Media & Video</h1>
37
36
  <p class="lead">Lumina supports video backgrounds on any slide, and video elements in Half and Flex layouts.</p>
38
37
 
@@ -61,7 +60,7 @@
61
60
  </div>
62
61
 
63
62
  <!-- INTRODUCTION -->
64
- <div v-else-if="activeSection === 'intro'">
63
+ <div v-else-if="activeSection === 'intro'" id="intro">
65
64
  <h1>Introduction</h1>
66
65
  <p class="lead text-2xl text-white font-light">
67
66
  Lumina is a high-performance, Universal Presentation Engine.
@@ -102,7 +101,7 @@
102
101
  </div>
103
102
 
104
103
  <!-- INSTALLATION -->
105
- <div v-else-if="activeSection === 'install'">
104
+ <div v-else-if="activeSection === 'install'" id="install">
106
105
  <h1>Installation</h1>
107
106
  <p>Lumina is available as a Universal NPM package. It works in any JavaScript environment
108
107
  (Vanilla, React, Vue, Svelte, etc.). Vue and GSAP are bundled; Chart.js is an optional peer only if you use <code>type: "chart"</code> slides.</p>
@@ -137,7 +136,7 @@
137
136
  </div>
138
137
 
139
138
  <!-- SETUP -->
140
- <div v-else-if="activeSection === 'setup'">
139
+ <div v-else-if="activeSection === 'setup'" id="setup">
141
140
  <h1>Quick Start</h1>
142
141
  <p>Import the stylesheet and initialize the engine in your main entry file.
143
142
  </p>
@@ -158,7 +157,7 @@ engine.load(myDeckData);</code></pre>
158
157
  </div>
159
158
 
160
159
  <!-- DECK STRUCTURE -->
161
- <div v-else-if="activeSection === 'deck'">
160
+ <div v-else-if="activeSection === 'deck'" id="deck">
162
161
  <h1>Deck Structure</h1>
163
162
  <p>A deck is a JSON object with <code>meta</code> and <code>slides</code>. Both are required.</p>
164
163
 
@@ -205,7 +204,7 @@ engine.load(myDeckData);</code></pre>
205
204
  </div>
206
205
 
207
206
  <!-- SLIDE LAYOUTS -->
208
- <div v-else-if="activeSection === 'slides'">
207
+ <div v-else-if="activeSection === 'slides'" id="slides">
209
208
  <h1>Slide Layouts</h1>
210
209
  <p>Lumina includes responsive layouts for common scenarios. Each layout has a reference section with a full property table.</p>
211
210
 
@@ -335,7 +334,7 @@ engine.load(myDeckData);</code></pre>
335
334
 
336
335
 
337
336
  <!-- SIZING / EMBEDDING -->
338
- <div v-else-if="activeSection === 'sizing'">
337
+ <div v-else-if="activeSection === 'sizing'" id="sizing">
339
338
  <h1>Embedding & Sizing</h1>
340
339
  <p>By default, Lumina slides take up the full viewport height (<code>100vh</code>) to create an
341
340
  immersive experience.</p>
@@ -364,7 +363,7 @@ engine.load(myDeckData);</code></pre>
364
363
  </div>
365
364
 
366
365
  <!-- CONFIGURATION -->
367
- <div v-else-if="activeSection === 'config'">
366
+ <div v-else-if="activeSection === 'config'" id="config">
368
367
  <h1>Configuration</h1>
369
368
  <p>Pass options to <code>new Lumina(selector, options)</code>. All properties are optional.</p>
370
369
 
@@ -408,7 +407,7 @@ engine.load(myDeckData);</code></pre>
408
407
  </div>
409
408
 
410
409
  <!-- EVENTS -->
411
- <div v-else-if="activeSection === 'events'">
410
+ <div v-else-if="activeSection === 'events'" id="events">
412
411
  <h1>Events & API</h1>
413
412
  <p>Subscribe with <code>engine.on(event, handler)</code>; unsubscribe with <code>engine.off(event, handler)</code> using the same function reference.</p>
414
413
 
@@ -443,7 +442,7 @@ engine.<span class="text-yellow-400">on</span>('navigate', ({ direction, toIndex
443
442
  </div>
444
443
 
445
444
  <!-- ELEMENT CONTROL -->
446
- <div v-else-if="activeSection === 'element-control'">
445
+ <div v-else-if="activeSection === 'element-control'" id="element-control">
447
446
  <h1>Element Control (Reveal on Demand)</h1>
448
447
  <p class="lead">Start with elements hidden and reveal them in sequence: ideal for blank slides that
449
448
  fill in as you speak, or for effects driven by <code>engine.element(id)</code>.</p>
@@ -455,30 +454,52 @@ engine.<span class="text-yellow-400">on</span>('navigate', ({ direction, toIndex
455
454
  <ol class="list-decimal list-inside mb-6 space-y-1 text-white/70">
456
455
  <li><strong>Slide root</strong> (path <code>[]</code> or <code>['slide']</code>): <code>slide.id</code> if present, otherwise <code>s{N}-slide</code>.</li>
457
456
  <li><strong>Object with <code>id</code></strong>: if the value at that path is an object with <code>id: string</code> (e.g. <code>features[0].id</code>, a diagram node), that <code>id</code> is used.</li>
458
- <li><strong><code>slide.ids</code></strong>: if the path has a single segment (e.g. <code>tag</code>) and <code>slide.ids.tag</code> exists, that value is used.</li>
459
- <li><strong>Fallback</strong>: <code>elemId(slideIndex, ...path)</code> → format <code>s{N}-{path0}-{path1}-...</code> (e.g. <code>s0-tag</code>, <code>s1-features-2</code>, <code>s2-elements-0-elements-1</code>).</li>
457
+ <li><strong><code>slide.ids[key]</code></strong>: e.g. <code>slide.ids["tag"]</code>, <code>slide.ids["features.0"]</code>. Overrides the fallback when the object has no <code>id</code>.</li>
458
+ <li><strong>Fallback</strong>: if <code>slide.id</code> is set → <code>{slide.id}-{path}</code> (e.g. <code>intro-tag</code>, <code>intro-title</code>); these IDs stay stable when you insert, remove or reorder slides. Otherwise → <code>s{N}-{path}</code> (e.g. <code>s0-tag</code>, <code>s1-features-0</code>).</li>
460
459
  </ol>
461
460
 
462
- <p><strong>Ids by slide type</strong> (when there is no <code>id</code> or <code>slide.ids</code>):</p>
461
+ <p><strong>Stable IDs with <code>slide.id</code></strong>: set <code>id: "intro"</code> on a slide so the fallback becomes <code>intro-tag</code>, <code>intro-title</code>, etc. Recommended when using <code>meta.initialElementState</code> or reorderable decks.</p>
462
+
463
+ <h3>Element IDs per layout</h3>
464
+ <p>Each row is one controllable element. <strong>Path</strong> is used in <code>engine.element(slideIndex, "tag")</code> or <code>engine.elementInCurrent("title")</code>. <strong>Fallback ID</strong> when no <code>slide.id</code>, <code>slide.ids</code> or <code>object.id</code>; with <code>slide.id: "x"</code> it becomes <code>x-tag</code>, <code>x-title</code>, etc. <strong>When</strong> = always, or only when that field exists in the slide JSON.</p>
463
465
  <div class="overflow-x-auto mb-6">
464
466
  <table class="w-full text-sm border border-white/10 rounded-lg">
465
- <thead><tr class="border-b border-white/10"><th class="text-left py-2 px-3 text-white/90">Type</th><th class="text-left py-2 px-3 text-white/90">Paths (path id fallback)</th></tr></thead>
467
+ <thead><tr class="border-b border-white/10"><th class="text-left py-2 px-3 text-white/90">Layout</th><th class="text-left py-2 px-3 text-white/90">Path</th><th class="text-left py-2 px-3 text-white/90">Fallback ID</th><th class="text-left py-2 px-3 text-white/90">When</th></tr></thead>
466
468
  <tbody class="text-white/70">
467
- <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">statement</td><td class="py-2 px-3"><code>tag</code>→s{N}-tag, <code>title</code>→s{N}-title, <code>subtitle</code>→s{N}-subtitle</td></tr>
468
- <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">features</td><td class="py-2 px-3"><code>header</code>→s{N}-header, <code>features.i</code>→s{N}-features-{i}</td></tr>
469
- <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">half</td><td class="py-2 px-3"><code>media</code>, <code>tag</code>, <code>title</code>, <code>paragraphs</code>, <code>cta</code> s{N}-media, s{N}-tag, etc.</td></tr>
470
- <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">timeline</td><td class="py-2 px-3"><code>title</code>, <code>subtitle</code>, <code>timeline.i</code> → s{N}-title, s{N}-subtitle, s{N}-timeline-{i}</td></tr>
471
- <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">steps</td><td class="py-2 px-3"><code>header</code>, <code>steps.i</code> → s{N}-header, s{N}-steps-{i}</td></tr>
472
- <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">flex</td><td class="py-2 px-3"><code>elements.i</code> s{N}-elements-{i}; nested <code>elements.i.elements.j</code> s{N}-elements-{i}-elements-{j}</td></tr>
473
- <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">chart</td><td class="py-2 px-3"><code>title</code>, <code>subtitle</code>, <code>chart</code> → s{N}-title, s{N}-subtitle, s{N}-chart</td></tr>
474
- <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">video</td><td class="py-2 px-3"><code>video</code>, <code>title</code> → s{N}-video, s{N}-title</td></tr>
475
- <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">diagram</td><td class="py-2 px-3"><code>nodes.i</code>, <code>edges.i</code>; if the node has <code>id</code>, it is used</td></tr>
476
- <tr><td class="py-2 px-3 font-mono">free</td><td class="py-2 px-3"><code>elements.i</code> s{N}-elements-0, s{N}-elements-1, …</td></tr>
469
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">statement</td><td class="py-2 px-3"><code>tag</code></td><td class="py-2 px-3"><code>s{N}-tag</code></td><td class="py-2 px-3">only when <code>tag</code> is set</td></tr>
470
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">statement</td><td class="py-2 px-3"><code>title</code></td><td class="py-2 px-3"><code>s{N}-title</code></td><td class="py-2 px-3">always</td></tr>
471
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">statement</td><td class="py-2 px-3"><code>subtitle</code></td><td class="py-2 px-3"><code>s{N}-subtitle</code></td><td class="py-2 px-3">only when <code>subtitle</code> is set</td></tr>
472
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">features</td><td class="py-2 px-3"><code>header</code></td><td class="py-2 px-3"><code>s{N}-header</code></td><td class="py-2 px-3">always (wrapper for title + description)</td></tr>
473
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">features</td><td class="py-2 px-3"><code>title</code></td><td class="py-2 px-3"><code>s{N}-title</code></td><td class="py-2 px-3">always</td></tr>
474
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">features</td><td class="py-2 px-3"><code>description</code></td><td class="py-2 px-3"><code>s{N}-description</code></td><td class="py-2 px-3">only when <code>description</code> is set</td></tr>
475
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">features</td><td class="py-2 px-3"><code>features.0</code>, <code>features.1</code>, …</td><td class="py-2 px-3"><code>s{N}-features-{i}</code> or <code>features[i].id</code></td><td class="py-2 px-3">one per feature</td></tr>
476
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">half</td><td class="py-2 px-3"><code>media</code></td><td class="py-2 px-3"><code>s{N}-media</code></td><td class="py-2 px-3">always</td></tr>
477
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">half</td><td class="py-2 px-3"><code>tag</code></td><td class="py-2 px-3"><code>s{N}-tag</code></td><td class="py-2 px-3">only when <code>tag</code> is set</td></tr>
478
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">half</td><td class="py-2 px-3"><code>title</code></td><td class="py-2 px-3"><code>s{N}-title</code></td><td class="py-2 px-3">always</td></tr>
479
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">half</td><td class="py-2 px-3"><code>paragraphs</code></td><td class="py-2 px-3"><code>s{N}-paragraphs</code></td><td class="py-2 px-3">always</td></tr>
480
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">half</td><td class="py-2 px-3"><code>cta</code></td><td class="py-2 px-3"><code>s{N}-cta</code></td><td class="py-2 px-3">only when <code>cta</code> is set</td></tr>
481
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">timeline</td><td class="py-2 px-3"><code>title</code></td><td class="py-2 px-3"><code>s{N}-title</code></td><td class="py-2 px-3">always</td></tr>
482
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">timeline</td><td class="py-2 px-3"><code>subtitle</code></td><td class="py-2 px-3"><code>s{N}-subtitle</code></td><td class="py-2 px-3">only when <code>subtitle</code> is set</td></tr>
483
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">timeline</td><td class="py-2 px-3"><code>timeline.0</code>, <code>timeline.1</code>, …</td><td class="py-2 px-3"><code>s{N}-timeline-{i}</code> or <code>timeline[i].id</code></td><td class="py-2 px-3">one per item</td></tr>
484
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">steps</td><td class="py-2 px-3"><code>header</code></td><td class="py-2 px-3"><code>s{N}-header</code></td><td class="py-2 px-3">always (wrapper for title + subtitle)</td></tr>
485
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">steps</td><td class="py-2 px-3"><code>title</code></td><td class="py-2 px-3"><code>s{N}-title</code></td><td class="py-2 px-3">always</td></tr>
486
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">steps</td><td class="py-2 px-3"><code>subtitle</code></td><td class="py-2 px-3"><code>s{N}-subtitle</code></td><td class="py-2 px-3">only when <code>subtitle</code> is set</td></tr>
487
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">steps</td><td class="py-2 px-3"><code>steps.0</code>, <code>steps.1</code>, …</td><td class="py-2 px-3"><code>s{N}-steps-{i}</code> or <code>steps[i].id</code></td><td class="py-2 px-3">one per step</td></tr>
488
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">flex</td><td class="py-2 px-3"><code>elements.0</code>, <code>elements.1</code>, …</td><td class="py-2 px-3"><code>s{N}-elements-{i}</code> or <code>elements[i].id</code></td><td class="py-2 px-3">one per element</td></tr>
489
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">flex</td><td class="py-2 px-3"><code>elements.i.elements.j</code></td><td class="py-2 px-3"><code>s{N}-elements-{i}-elements-{j}</code></td><td class="py-2 px-3">content children only</td></tr>
490
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">chart</td><td class="py-2 px-3"><code>title</code></td><td class="py-2 px-3"><code>s{N}-title</code></td><td class="py-2 px-3">only when <code>title</code> is set</td></tr>
491
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">chart</td><td class="py-2 px-3"><code>subtitle</code></td><td class="py-2 px-3"><code>s{N}-subtitle</code></td><td class="py-2 px-3">only when <code>subtitle</code> is set</td></tr>
492
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">chart</td><td class="py-2 px-3"><code>chart</code></td><td class="py-2 px-3"><code>s{N}-chart</code></td><td class="py-2 px-3">always (may be absent in DOM if Chart.js fails)</td></tr>
493
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">video</td><td class="py-2 px-3"><code>video</code></td><td class="py-2 px-3"><code>s{N}-video</code></td><td class="py-2 px-3">always</td></tr>
494
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">video</td><td class="py-2 px-3"><code>title</code></td><td class="py-2 px-3"><code>s{N}-title</code></td><td class="py-2 px-3">only when <code>title</code> is set</td></tr>
495
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">diagram</td><td class="py-2 px-3"><code>nodes.0</code>, <code>nodes.1</code>, …</td><td class="py-2 px-3"><code>nodes[i].id</code> or <code>s{N}-nodes-{i}</code></td><td class="py-2 px-3">set <code>nodes[i].id</code> in JSON for control</td></tr>
496
+ <tr class="border-b border-white/5"><td class="py-2 px-3 font-mono">diagram</td><td class="py-2 px-3"><code>edges.0</code>, <code>edges.1</code>, …</td><td class="py-2 px-3"><code>edges[i].id</code> or <code>s{N}-edges-{i}</code></td><td class="py-2 px-3">set <code>edges[i].id</code> in JSON for control</td></tr>
497
+ <tr><td class="py-2 px-3 font-mono">free</td><td class="py-2 px-3"><code>elements.0</code>, <code>elements.1</code>, …</td><td class="py-2 px-3"><code>s{N}-elements-{i}</code></td><td class="py-2 px-3">one per element</td></tr>
477
498
  </tbody>
478
499
  </table>
479
500
  </div>
480
501
 
481
- <p>To see a slide's ids at runtime: <code>engine.elements(slideIndex)</code>. To use by path: <code>engine.element(slideIndex, "tag")</code> or <code>engine.element(slideIndex, "features.0")</code>; the id is resolved with the same rules.</p>
502
+ <p><strong>Discover and target</strong>: <code>engine.elements()</code> or <code>engine.elements(slideIndex)</code> lists all ids for a slide (omit <code>slideIndex</code> for the current slide). <code>engine.element(id)</code>, <code>engine.element(slideIndex, "tag")</code>, or <code>engine.elementInCurrent("title")</code> for the current slide by path. Override any path via <code>slide.ids</code> (e.g. <code>ids: { "title": "hero-title" }</code>) or <code>object.id</code> on items (e.g. <code>features[0].id: "hero"</code>).</p>
482
503
 
483
504
  <h2>Option 1: meta.initialElementState</h2>
484
505
  <p>Define in the deck which elements start hidden. Ids follow the pattern
@@ -570,7 +591,7 @@ engine.load(deck);</code></pre>
570
591
  </div>
571
592
 
572
593
  <!-- ANIMATIONS: revealInSequence, presets, stagger -->
573
- <div v-else-if="activeSection === 'animations'">
594
+ <div v-else-if="activeSection === 'animations'" id="animations">
574
595
  <h1>Animations</h1>
575
596
  <p class="lead">Control entrances with <code>revealInSequence</code>, built-in presets, and stagger modes. Use with <code>meta.initialElementState</code> or <code>elementControl.defaultVisible: false</code> to start hidden.</p>
576
597
 
@@ -602,7 +623,7 @@ engine.load(deck);</code></pre>
602
623
  </div>
603
624
 
604
625
  <!-- TIMELINE (Remotion-style keyframes) -->
605
- <div v-else-if="activeSection === 'timeline-keyframes'">
626
+ <div v-else-if="activeSection === 'timeline-keyframes'" id="timeline-keyframes">
606
627
  <h1>Timeline (Remotion-style keyframes)</h1>
607
628
  <p class="lead">Drive element state from a progress 0–1 with <code>slide.timelineTracks</code>. Use <code>engine.seekTo(progress)</code> to scrub or <code>engine.playTimeline(duration)</code> to play. Works with any layout; the <code>free</code> layout is for absolutely positioned storytelling.</p>
608
629
 
@@ -638,7 +659,7 @@ engine.load(deck);</code></pre>
638
659
  </div>
639
660
 
640
661
  <!-- THEMING -->
641
- <div v-else-if="activeSection === 'theming'">
662
+ <div v-else-if="activeSection === 'theming'" id="theming">
642
663
  <h1>Theming</h1>
643
664
  <p class="lead">Customize colors, fonts, and visual style with built-in presets or create your
644
665
  own theme.</p>
@@ -1403,7 +1424,7 @@ engine.load(myDeck);</code></pre>
1403
1424
  </div>
1404
1425
 
1405
1426
  <!-- SPEAKER NOTES -->
1406
- <div v-else-if="activeSection === 'speaker-notes'">
1427
+ <div v-else-if="activeSection === 'speaker-notes'" id="speaker-notes">
1407
1428
  <h1>Speaker Notes</h1>
1408
1429
  <p class="lead">Open a separate presenter view with notes, timer, and bidirectional navigation
1409
1430
  controls.</p>
@@ -1487,7 +1508,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1487
1508
 
1488
1509
 
1489
1510
  <!-- AGENTS: INTRO -->
1490
- <div v-else-if="activeSection === 'agents-intro'">
1511
+ <div v-else-if="activeSection === 'agents-intro'" id="agents-intro">
1491
1512
  <h1>The Agent Protocol</h1>
1492
1513
  <p class="lead">Lumina is the first presentation engine designed for <strong>Agentic
1493
1514
  AI</strong>.</p>
@@ -1533,7 +1554,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1533
1554
  </div>
1534
1555
 
1535
1556
  <!-- AGENTS: TOKENS -->
1536
- <div v-else-if="activeSection === 'agents-tokens'">
1557
+ <div v-else-if="activeSection === 'agents-tokens'" id="agents-tokens">
1537
1558
  <h1>Token Optimization</h1>
1538
1559
  <p class="lead">Reduce output size with short property aliases. Less characters = faster
1539
1560
  rendering and lower costs.</p>
@@ -1641,7 +1662,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1641
1662
  </div>
1642
1663
 
1643
1664
  <!-- AGENTS: STREAMING -->
1644
- <div v-else-if="activeSection === 'agents-streaming'">
1665
+ <div v-else-if="activeSection === 'agents-streaming'" id="agents-streaming">
1645
1666
  <h1>Streaming & Realtime</h1>
1646
1667
  <p>Lumina enables a <strong>"Type & See"</strong> experience. Using our partial parser, you can
1647
1668
  feed raw LLM streams directly into the engine.</p>
@@ -1689,12 +1710,11 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1689
1710
  </div>
1690
1711
 
1691
1712
  <!-- AGENTS: LAYOUT GALLERY -->
1692
- <div v-else-if="activeSection === 'agents-layouts'">
1713
+ <div v-else-if="activeSection === 'agents-layouts'" id="agents-layouts">
1693
1714
  <h1>Layout Gallery</h1>
1694
1715
  <p>Lumina provides 5 core layouts optimized for different types of information. Click <strong><i
1695
1716
  class="fa-solid fa-play text-blue-400"></i> Visualize</strong> to load them into the
1696
- <a @click.prevent="activeSection = 'agents-streaming'; scroll('streaming-demo-container')"
1697
- href="#" class="text-blue-400 hover:underline">Streaming Demo</a>.
1717
+ <a href="#streaming-demo-container" @click.prevent="goToStreamingDemo" class="text-blue-400 hover:underline">Streaming Demo</a>.
1698
1718
  </p>
1699
1719
 
1700
1720
  <div class="space-y-12 mt-12">
@@ -1750,7 +1770,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1750
1770
  </div>
1751
1771
  </div>
1752
1772
 
1753
- <div v-else-if="activeSection === 'agents-auto'">
1773
+ <div v-else-if="activeSection === 'agents-auto'" id="agents-auto">
1754
1774
  <h1>Auto-Layouts</h1>
1755
1775
  <p>Sometimes the Agent doesn't know which layout is best. Just use <code>type: 'auto'</code>.
1756
1776
  </p>
@@ -1828,7 +1848,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1828
1848
  </div>
1829
1849
 
1830
1850
  <!-- AGENTS: STATE -->
1831
- <div v-else-if="activeSection === 'agents-state'">
1851
+ <div v-else-if="activeSection === 'agents-state'" id="agents-state">
1832
1852
  <h1>State & Feedback Loop</h1>
1833
1853
  <p>Use <code>engine.exportState()</code> to get the current state for your LLM or agent. It returns a frozen object with <code>status</code>, <code>currentSlide</code> (<code>{ index, id, type, title }</code>), <code>narrative</code> (e.g. "User is on slide 3. Session: clicked 'Learn More' → next."), <code>engagementLevel</code> ("High" or "Low"), and <code>history</code> (recorded actions). Add this to your prompt for context-aware replies.</p>
1834
1854
  <pre><code><span class="text-blue-400">const</span> state = engine.exportState();
@@ -1836,7 +1856,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1836
1856
  </div>
1837
1857
 
1838
1858
  <!-- TEMPLATE TAGS & engine.data -->
1839
- <div v-else-if="activeSection === 'template-tags'">
1859
+ <div v-else-if="activeSection === 'template-tags'" id="template-tags">
1840
1860
  <h1>Template Tags & Key-Value Store</h1>
1841
1861
  <p class="lead">Use <code>engine.data</code> as a key-value store and <code v-pre>{{key}}</code> placeholders in slide strings. Values resolve at render time and update reactively when <code>engine.data</code> changes.</p>
1842
1862
 
@@ -1879,8 +1899,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
1879
1899
  </p>
1880
1900
  </div>
1881
1901
 
1902
+ <!-- LAYOUTS (single page with anchors) -->
1903
+ <div v-else-if="LAYOUT_IDS.includes(activeSection)" class="space-y-24">
1882
1904
  <!-- REF: STATEMENT -->
1883
- <div v-else-if="activeSection === 'ref-statement'">
1905
+ <section id="ref-statement">
1884
1906
  <h1>Statement Slide</h1>
1885
1907
  <p class="lead">High-impact titles, opening covers, or emphatic quotes. Punchy and minimal. Supports <code>sizing</code>, <code>background</code>, <code>notes</code>, <code>class</code>, <code>timelineTracks</code>, <code>reveal</code>, <code>ids</code>, <code>meta</code> (SlideBase).</p>
1886
1908
 
@@ -1903,17 +1925,17 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
1903
1925
  </tbody>
1904
1926
  </table>
1905
1927
  </div>
1906
- <p class="text-white/50 text-sm mt-2">Element ids: <code>s{N}-tag</code>, <code>s{N}-title</code>, <code>s{N}-subtitle</code>. Override via <code>ids</code>.</p>
1928
+ <p class="text-white/50 text-sm mt-2">Element ids: <code>tag</code> (if set)→<code>s{N}-tag</code>, <code>title</code>→<code>s{N}-title</code>, <code>subtitle</code> (if set)→<code>s{N}-subtitle</code>. With <code>slide.id</code>: <code>{id}-tag</code>, etc. Override via <code>slide.ids</code>. <a href="#element-control" class="text-blue-400 hover:underline">Element Control</a> has the full table.</p>
1907
1929
  </div>
1908
1930
 
1909
1931
  <div class="my-8">
1910
1932
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
1911
1933
  <LivePreview :initial-code="EXAMPLES.statement" />
1912
1934
  </div>
1913
- </div>
1935
+ </section>
1914
1936
 
1915
1937
  <!-- REF: HALF -->
1916
- <div v-else-if="activeSection === 'ref-half'">
1938
+ <section id="ref-half">
1917
1939
  <h1>Half / Split Slide</h1>
1918
1940
  <p class="lead">Image or video on one side, text on the other. Product showcases, "about me", explainers. <strong>Provide <code>image</code> or <code>video</code> (one required).</strong> Plus SlideBase (sizing, background, notes, class, timelineTracks, reveal, ids, meta).</p>
1919
1941
 
@@ -1937,17 +1959,17 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
1937
1959
  </tbody>
1938
1960
  </table>
1939
1961
  </div>
1940
- <p class="text-white/50 text-sm mt-2">Element ids: <code>s{N}-media</code>, <code>s{N}-tag</code>, <code>s{N}-title</code>, <code>s{N}-paragraphs</code>, <code>s{N}-cta</code>.</p>
1962
+ <p class="text-white/50 text-sm mt-2">Element ids: <code>media</code>, <code>tag</code> (if set), <code>title</code>, <code>paragraphs</code>, <code>cta</code> (if set) → <code>s{N}-media</code>, <code>s{N}-tag</code>, etc. <a href="#element-control" class="text-blue-400 hover:underline">Element Control</a> has the full table.</p>
1941
1963
  </div>
1942
1964
 
1943
1965
  <div class="my-8">
1944
1966
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
1945
1967
  <LivePreview :initial-code="EXAMPLES.half" />
1946
1968
  </div>
1947
- </div>
1969
+ </section>
1948
1970
 
1949
1971
  <!-- REF: FEATURES -->
1950
- <div v-else-if="activeSection === 'ref-features'">
1972
+ <section id="ref-features">
1951
1973
  <h1>Features Slide</h1>
1952
1974
  <p class="lead">Grid of cards for benefits, stats, or services. Responsive: 1 column on mobile, auto-fit on desktop. SlideBase (sizing, background, notes, class, timelineTracks, reveal, ids, meta) applies.</p>
1953
1975
 
@@ -1982,17 +2004,17 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
1982
2004
  </tbody>
1983
2005
  </table>
1984
2006
  </div>
1985
- <p class="text-white/50 text-sm mt-2">Element ids: <code>s{N}-header</code>, <code>s{N}-features-0</code>, <code>s{N}-features-1</code>, …</p>
2007
+ <p class="text-white/50 text-sm mt-2">Element ids: <code>header</code>, <code>title</code>, <code>description</code> (if set), <code>features.0</code>, <code>features.1</code>, … → <code>s{N}-header</code>, <code>s{N}-title</code>, <code>s{N}-features-0</code>, etc. <a href="#element-control" class="text-blue-400 hover:underline">Element Control</a> has the full table.</p>
1986
2008
  </div>
1987
2009
 
1988
2010
  <div class="my-8">
1989
2011
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
1990
2012
  <LivePreview :initial-code="EXAMPLES.features" />
1991
2013
  </div>
1992
- </div>
2014
+ </section>
1993
2015
 
1994
2016
  <!-- REF: TIMELINE -->
1995
- <div v-else-if="activeSection === 'ref-timeline'">
2017
+ <section id="ref-timeline">
1996
2018
  <h1>Timeline Slide</h1>
1997
2019
  <p class="lead">Vertical chronological list for events, roadmaps, or history. Alternating left/right on desktop. <em>Not</em> the same as <code>slide.timelineTracks</code> (keyframe animation). SlideBase applies.</p>
1998
2020
 
@@ -2028,17 +2050,17 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2028
2050
  </tbody>
2029
2051
  </table>
2030
2052
  </div>
2031
- <p class="text-white/50 text-sm mt-2">Element ids: <code>s{N}-title</code>, <code>s{N}-subtitle</code>, <code>s{N}-timeline-0</code>, …</p>
2053
+ <p class="text-white/50 text-sm mt-2">Element ids: <code>title</code>, <code>subtitle</code> (if set), <code>timeline.0</code>, <code>timeline.1</code>, … → <code>s{N}-title</code>, <code>s{N}-subtitle</code>, <code>s{N}-timeline-{i}</code>. <a href="#element-control" class="text-blue-400 hover:underline">Element Control</a> has the full table.</p>
2032
2054
  </div>
2033
2055
 
2034
2056
  <div class="my-8">
2035
2057
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
2036
2058
  <LivePreview :initial-code="EXAMPLES.timeline" />
2037
2059
  </div>
2038
- </div>
2060
+ </section>
2039
2061
 
2040
2062
  <!-- REF: STEPS -->
2041
- <div v-else-if="activeSection === 'ref-steps'">
2063
+ <section id="ref-steps">
2042
2064
  <h1>Steps Slide</h1>
2043
2065
  <p class="lead">Numbered steps for tutorials, how-to guides, or process flows. Responsive grid. SlideBase applies.</p>
2044
2066
 
@@ -2074,17 +2096,17 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2074
2096
  </tbody>
2075
2097
  </table>
2076
2098
  </div>
2077
- <p class="text-white/50 text-sm mt-2">Element ids: <code>s{N}-header</code>, <code>s{N}-steps-0</code>, <code>s{N}-steps-1</code>, …</p>
2099
+ <p class="text-white/50 text-sm mt-2">Element ids: <code>header</code>, <code>title</code>, <code>subtitle</code> (if set), <code>steps.0</code>, <code>steps.1</code>, … → <code>s{N}-header</code>, <code>s{N}-title</code>, <code>s{N}-steps-{i}</code>. <a href="#element-control" class="text-blue-400 hover:underline">Element Control</a> has the full table.</p>
2078
2100
  </div>
2079
2101
 
2080
2102
  <div class="my-8">
2081
2103
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
2082
2104
  <LivePreview :initial-code="EXAMPLES.steps" />
2083
2105
  </div>
2084
- </div>
2106
+ </section>
2085
2107
 
2086
2108
  <!-- REF: FLEX -->
2087
- <div v-else-if="activeSection === 'ref-flex'">
2109
+ <section id="ref-flex">
2088
2110
  <h1>Flex Layout</h1>
2089
2111
  <p class="lead">Flow-based layout with semantic sizing (quarter, half, full, etc.). No coordinates—ideal for LLMs. SlideBase applies. Top-level and content children can have <code>size?: FlexSize</code>.</p>
2090
2112
 
@@ -2135,7 +2157,7 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2135
2157
  </tbody>
2136
2158
  </table>
2137
2159
  </div>
2138
- <p class="text-white/50 text-sm mt-2">SpacingToken: <code>none | xs | sm | md | lg | xl | 2xl</code>. Element ids: <code>s{N}-elements-{i}</code>, <code>s{N}-elements-{i}-elements-{j}</code> for content children.</p>
2160
+ <p class="text-white/50 text-sm mt-2">SpacingToken: <code>none | xs | sm | md | lg | xl | 2xl</code>. Element ids: <code>elements.0</code>, <code>elements.1</code>, …, <code>elements.i.elements.j</code> (content) → <code>s{N}-elements-{i}</code>, <code>s{N}-elements-{i}-elements-{j}</code>. <a href="#element-control" class="text-blue-400 hover:underline">Element Control</a> has the full table.</p>
2139
2161
  </div>
2140
2162
 
2141
2163
  <div class="my-8">
@@ -2147,10 +2169,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2147
2169
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
2148
2170
  <LivePreview :initial-code="EXAMPLES.flex" />
2149
2171
  </div>
2150
- </div>
2172
+ </section>
2151
2173
 
2152
2174
  <!-- REF: CHART -->
2153
- <div v-else-if="activeSection === 'ref-chart'">
2175
+ <section id="ref-chart">
2154
2176
  <h1>Chart Slide</h1>
2155
2177
  <p class="lead">Data visualization with Chart.js. Types: <code>bar</code>, <code>line</code>, <code>pie</code>, <code>doughnut</code>. Requires <code>chart.js</code> as an optional peer. SlideBase applies.</p>
2156
2178
 
@@ -2195,6 +2217,7 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2195
2217
  </tbody>
2196
2218
  </table>
2197
2219
  </div>
2220
+ <p class="text-white/50 text-sm mt-2">Element ids: <code>title</code> (if set), <code>subtitle</code> (if set), <code>chart</code> → <code>s{N}-title</code>, <code>s{N}-subtitle</code>, <code>s{N}-chart</code>. <a href="#element-control" class="text-blue-400 hover:underline">Element Control</a> has the full table.</p>
2198
2221
  </div>
2199
2222
 
2200
2223
  <div class="my-8">
@@ -2439,10 +2462,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2439
2462
  charts automatically</li>
2440
2463
  </ul>
2441
2464
  </div>
2442
- </div>
2465
+ </section>
2443
2466
 
2444
2467
  <!-- REF: VIDEO -->
2445
- <div v-else-if="activeSection === 'ref-video'">
2468
+ <section id="ref-video">
2446
2469
  <h1>Video Slide</h1>
2447
2470
  <p class="lead">Full-screen video. <code>video</code> (VideoProperties) is required. Optional <code>title</code> overlay at bottom-left (on hover). SlideBase applies.</p>
2448
2471
 
@@ -2507,11 +2530,11 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2507
2530
  <LivePreview :initial-code="EXAMPLES.video_slide" />
2508
2531
  </div>
2509
2532
 
2510
- <p class="text-white/60 text-sm">Element control ids: <code>s{N}-video</code>, <code>s{N}-title</code>.</p>
2511
- </div>
2533
+ <p class="text-white/50 text-sm mt-2">Element ids: <code>video</code>, <code>title</code> (if set) → <code>s{N}-video</code>, <code>s{N}-title</code>. <a href="#element-control" class="text-blue-400 hover:underline">Element Control</a> has the full table.</p>
2534
+ </section>
2512
2535
 
2513
2536
  <!-- REF: CUSTOM HTML -->
2514
- <div v-else-if="activeSection === 'ref-custom'">
2537
+ <section id="ref-custom">
2515
2538
  <h1>Custom HTML Slide</h1>
2516
2539
  <p class="lead">Raw <code>html</code> and optional <code>css</code>. For iframes, D3, or brand-specific layouts. SlideBase applies. Sanitized: <code>&lt;script&gt;</code> and <code>on*</code> handlers removed.</p>
2517
2540
 
@@ -2585,10 +2608,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2585
2608
  creative freedom</li>
2586
2609
  </ul>
2587
2610
  </div>
2588
- </div>
2611
+ </section>
2589
2612
 
2590
2613
  <!-- REF: DIAGRAM -->
2591
- <div v-else-if="activeSection === 'ref-diagram'">
2614
+ <section id="ref-diagram">
2592
2615
  <h1>Diagram Slide</h1>
2593
2616
  <p class="lead">Node-based diagram (Vue Flow). <code>nodes</code> and <code>edges</code> required. In Studio: drag-and-drop, connect, resize. SlideBase applies.</p>
2594
2617
 
@@ -2608,12 +2631,12 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2608
2631
  </tbody>
2609
2632
  </table>
2610
2633
  </div>
2611
- <p class="text-white/50 text-sm mt-2">Element control: <code>s{N}-nodes-{i}</code>, <code>s{N}-edges-{i}</code>. Studio: palette, drag, connect, resize.</p>
2634
+ <p class="text-white/50 text-sm mt-2">Element ids: <code>nodes.0</code>, <code>nodes.1</code>, … and <code>edges.0</code>, … Use <code>nodes[i].id</code> and <code>edges[i].id</code> in JSON so <code>engine.elements()</code> aligns with the DOM. Fallback: <code>s{N}-nodes-{i}</code>, <code>s{N}-edges-{i}</code>. <a href="#element-control" class="text-blue-400 hover:underline">Element Control</a> has the full table. Studio: palette, drag, connect, resize.</p>
2612
2635
  </div>
2613
- </div>
2636
+ </section>
2614
2637
 
2615
2638
  <!-- REF: FREE -->
2616
- <div v-else-if="activeSection === 'ref-free'">
2639
+ <section id="ref-free">
2617
2640
  <h1>Free / Composition Slide</h1>
2618
2641
  <p class="lead">Absolutely positioned elements for Remotion-style storytelling. Three types: <code>text</code>, <code>image</code>, <code>box</code>. Position and animation are driven by <code>slide.timelineTracks</code> (x, y as translate). SlideBase applies.</p>
2619
2642
 
@@ -2651,6 +2674,7 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2651
2674
  </tbody>
2652
2675
  </table>
2653
2676
  </div>
2677
+ <p class="text-white/50 text-sm mt-2">Element ids: <code>elements.0</code>, <code>elements.1</code>, … → <code>s{N}-elements-{i}</code>. Override with <code>elements[i].id</code>. <a href="#element-control" class="text-blue-400 hover:underline">Element Control</a> has the full table.</p>
2654
2678
 
2655
2679
  <h3 class="text-lg font-bold text-white mt-8 mb-4">Positioning and animation (timelineTracks)</h3>
2656
2680
  <p>Elements are placed at <code>left: 0; top: 0</code> by default. To move or animate them, set <code>slide.timelineTracks</code> on the slide (SlideBase). Keys are element ids: <code>s{N}-elements-0</code>, <code>s{N}-elements-1</code>, or the element’s <code>id</code>. Value: keyframes from progress string to state:</p>
@@ -2675,10 +2699,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2675
2699
  }
2676
2700
  }</code></pre>
2677
2701
  </div>
2678
- </div>
2702
+ </section>
2679
2703
 
2680
2704
  <!-- REF: AUTO -->
2681
- <div v-else-if="activeSection === 'ref-auto'">
2705
+ <section id="ref-auto">
2682
2706
  <h1>Auto Strategy</h1>
2683
2707
  <p class="lead">Picks a layout from the data shape. Use <code>type: "auto"</code>; the engine resolves to one of: Chart, Timeline, Steps, Features, Half, Statement. All SlideBase and layout-specific props are passed through to the chosen layout. Best for LLMs and rapid prototyping.</p>
2684
2708
 
@@ -2742,22 +2766,50 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2742
2766
  ]
2743
2767
  }' />
2744
2768
  </div>
2769
+ </section>
2770
+
2745
2771
  </div>
2746
2772
 
2747
2773
  </div>
2748
- </Transition>
2749
2774
  </main>
2750
2775
  </div>
2751
2776
  </template>
2752
2777
 
2753
2778
  <script setup lang="ts">
2754
- import { ref, watch, onUnmounted, nextTick } from 'vue';
2779
+ import { ref, watch, onUnmounted, onMounted, nextTick } from 'vue';
2755
2780
  import { Lumina } from '../../core/Lumina';
2756
2781
  import { parsePartialJson } from '../../utils/streaming';
2757
2782
  import LivePreview from './LivePreview.vue';
2758
2783
 
2759
2784
  const activeSection = ref('intro');
2760
2785
 
2786
+ const LAYOUT_IDS = ['ref-statement', 'ref-half', 'ref-features', 'ref-timeline', 'ref-steps', 'ref-flex', 'ref-chart', 'ref-video', 'ref-diagram', 'ref-free', 'ref-custom', 'ref-auto'];
2787
+
2788
+ function scrollToTop() {
2789
+ window.scrollTo({ top: 0, behavior: 'smooth' });
2790
+ }
2791
+
2792
+ function goToLayout(id: string) {
2793
+ activeSection.value = id;
2794
+ nextTick(() => {
2795
+ document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
2796
+ if (typeof location !== 'undefined') location.hash = id;
2797
+ });
2798
+ }
2799
+
2800
+ function goToStreamingDemo() {
2801
+ activeSection.value = 'agents-streaming';
2802
+ nextTick(() => document.getElementById('streaming-demo-container')?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
2803
+ }
2804
+
2805
+ onMounted(() => {
2806
+ if (typeof window === 'undefined') return;
2807
+ const h = window.location.hash.slice(1) || '';
2808
+ if (!h) return;
2809
+ activeSection.value = h;
2810
+ nextTick(() => document.getElementById(h)?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
2811
+ });
2812
+
2761
2813
  // --- DEMO LOGIC ---
2762
2814
  const demoInput = ref('');
2763
2815
  const demoStarted = ref(false);
@@ -2847,7 +2899,7 @@ async function runDemo() {
2847
2899
 
2848
2900
  function loadIntoDemo(json: string) {
2849
2901
  activeSection.value = 'agents-streaming';
2850
- window.scrollTo({ top: 0, behavior: 'smooth' });
2902
+ nextTick(() => document.getElementById('streaming-demo-container')?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
2851
2903
  demoInput.value = '';
2852
2904
  demoStarted.value = true;
2853
2905
 
@@ -3057,6 +3109,7 @@ const navigation = [
3057
3109
  title: 'AI & LLM Integration',
3058
3110
  items: [
3059
3111
  { id: 'agents-streaming', label: 'Streaming & Realtime' },
3112
+ { id: 'agents-layouts', label: 'Layout Gallery' },
3060
3113
  { id: 'agents-intro', label: 'Agent Protocol' },
3061
3114
  { id: 'agents-tokens', label: 'Token Optimization' }
3062
3115
  ]
@@ -3077,21 +3130,13 @@ const navigation = [
3077
3130
  ];
3078
3131
 
3079
3132
 
3080
- function scroll(id: string) {
3081
- nextTick(() => {
3082
- const el = document.getElementById(id);
3083
- if (el) {
3084
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
3085
- }
3086
- });
3087
- }
3088
-
3089
- function scrollToTop() {
3090
- window.scrollTo({ top: 0, behavior: 'smooth' });
3091
- }
3092
3133
  </script>
3093
3134
 
3094
3135
  <style scoped>
3136
+ .doc-content [id] {
3137
+ scroll-margin-top: 8.5rem;
3138
+ }
3139
+
3095
3140
  .doc-content h1 {
3096
3141
  @apply text-4xl md:text-5xl font-black tracking-tight text-white mb-8 leading-tight font-heading;
3097
3142
  }