lumina-slides 9.0.2 → 9.0.3

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>
@@ -570,7 +569,7 @@ engine.load(deck);</code></pre>
570
569
  </div>
571
570
 
572
571
  <!-- ANIMATIONS: revealInSequence, presets, stagger -->
573
- <div v-else-if="activeSection === 'animations'">
572
+ <div v-else-if="activeSection === 'animations'" id="animations">
574
573
  <h1>Animations</h1>
575
574
  <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
575
 
@@ -602,7 +601,7 @@ engine.load(deck);</code></pre>
602
601
  </div>
603
602
 
604
603
  <!-- TIMELINE (Remotion-style keyframes) -->
605
- <div v-else-if="activeSection === 'timeline-keyframes'">
604
+ <div v-else-if="activeSection === 'timeline-keyframes'" id="timeline-keyframes">
606
605
  <h1>Timeline (Remotion-style keyframes)</h1>
607
606
  <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
607
 
@@ -638,7 +637,7 @@ engine.load(deck);</code></pre>
638
637
  </div>
639
638
 
640
639
  <!-- THEMING -->
641
- <div v-else-if="activeSection === 'theming'">
640
+ <div v-else-if="activeSection === 'theming'" id="theming">
642
641
  <h1>Theming</h1>
643
642
  <p class="lead">Customize colors, fonts, and visual style with built-in presets or create your
644
643
  own theme.</p>
@@ -1403,7 +1402,7 @@ engine.load(myDeck);</code></pre>
1403
1402
  </div>
1404
1403
 
1405
1404
  <!-- SPEAKER NOTES -->
1406
- <div v-else-if="activeSection === 'speaker-notes'">
1405
+ <div v-else-if="activeSection === 'speaker-notes'" id="speaker-notes">
1407
1406
  <h1>Speaker Notes</h1>
1408
1407
  <p class="lead">Open a separate presenter view with notes, timer, and bidirectional navigation
1409
1408
  controls.</p>
@@ -1487,7 +1486,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1487
1486
 
1488
1487
 
1489
1488
  <!-- AGENTS: INTRO -->
1490
- <div v-else-if="activeSection === 'agents-intro'">
1489
+ <div v-else-if="activeSection === 'agents-intro'" id="agents-intro">
1491
1490
  <h1>The Agent Protocol</h1>
1492
1491
  <p class="lead">Lumina is the first presentation engine designed for <strong>Agentic
1493
1492
  AI</strong>.</p>
@@ -1533,7 +1532,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1533
1532
  </div>
1534
1533
 
1535
1534
  <!-- AGENTS: TOKENS -->
1536
- <div v-else-if="activeSection === 'agents-tokens'">
1535
+ <div v-else-if="activeSection === 'agents-tokens'" id="agents-tokens">
1537
1536
  <h1>Token Optimization</h1>
1538
1537
  <p class="lead">Reduce output size with short property aliases. Less characters = faster
1539
1538
  rendering and lower costs.</p>
@@ -1641,7 +1640,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1641
1640
  </div>
1642
1641
 
1643
1642
  <!-- AGENTS: STREAMING -->
1644
- <div v-else-if="activeSection === 'agents-streaming'">
1643
+ <div v-else-if="activeSection === 'agents-streaming'" id="agents-streaming">
1645
1644
  <h1>Streaming & Realtime</h1>
1646
1645
  <p>Lumina enables a <strong>"Type & See"</strong> experience. Using our partial parser, you can
1647
1646
  feed raw LLM streams directly into the engine.</p>
@@ -1689,12 +1688,11 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1689
1688
  </div>
1690
1689
 
1691
1690
  <!-- AGENTS: LAYOUT GALLERY -->
1692
- <div v-else-if="activeSection === 'agents-layouts'">
1691
+ <div v-else-if="activeSection === 'agents-layouts'" id="agents-layouts">
1693
1692
  <h1>Layout Gallery</h1>
1694
1693
  <p>Lumina provides 5 core layouts optimized for different types of information. Click <strong><i
1695
1694
  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>.
1695
+ <a href="#streaming-demo-container" @click.prevent="goToStreamingDemo" class="text-blue-400 hover:underline">Streaming Demo</a>.
1698
1696
  </p>
1699
1697
 
1700
1698
  <div class="space-y-12 mt-12">
@@ -1750,7 +1748,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1750
1748
  </div>
1751
1749
  </div>
1752
1750
 
1753
- <div v-else-if="activeSection === 'agents-auto'">
1751
+ <div v-else-if="activeSection === 'agents-auto'" id="agents-auto">
1754
1752
  <h1>Auto-Layouts</h1>
1755
1753
  <p>Sometimes the Agent doesn't know which layout is best. Just use <code>type: 'auto'</code>.
1756
1754
  </p>
@@ -1828,7 +1826,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1828
1826
  </div>
1829
1827
 
1830
1828
  <!-- AGENTS: STATE -->
1831
- <div v-else-if="activeSection === 'agents-state'">
1829
+ <div v-else-if="activeSection === 'agents-state'" id="agents-state">
1832
1830
  <h1>State & Feedback Loop</h1>
1833
1831
  <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
1832
  <pre><code><span class="text-blue-400">const</span> state = engine.exportState();
@@ -1836,7 +1834,7 @@ engine.<span class="text-yellow-400">closeSpeakerNotes</span>();</code></pre>
1836
1834
  </div>
1837
1835
 
1838
1836
  <!-- TEMPLATE TAGS & engine.data -->
1839
- <div v-else-if="activeSection === 'template-tags'">
1837
+ <div v-else-if="activeSection === 'template-tags'" id="template-tags">
1840
1838
  <h1>Template Tags & Key-Value Store</h1>
1841
1839
  <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
1840
 
@@ -1879,8 +1877,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
1879
1877
  </p>
1880
1878
  </div>
1881
1879
 
1880
+ <!-- LAYOUTS (single page with anchors) -->
1881
+ <div v-else-if="LAYOUT_IDS.includes(activeSection)" class="space-y-24">
1882
1882
  <!-- REF: STATEMENT -->
1883
- <div v-else-if="activeSection === 'ref-statement'">
1883
+ <section id="ref-statement">
1884
1884
  <h1>Statement Slide</h1>
1885
1885
  <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
1886
 
@@ -1910,10 +1910,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
1910
1910
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
1911
1911
  <LivePreview :initial-code="EXAMPLES.statement" />
1912
1912
  </div>
1913
- </div>
1913
+ </section>
1914
1914
 
1915
1915
  <!-- REF: HALF -->
1916
- <div v-else-if="activeSection === 'ref-half'">
1916
+ <section id="ref-half">
1917
1917
  <h1>Half / Split Slide</h1>
1918
1918
  <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
1919
 
@@ -1944,10 +1944,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
1944
1944
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
1945
1945
  <LivePreview :initial-code="EXAMPLES.half" />
1946
1946
  </div>
1947
- </div>
1947
+ </section>
1948
1948
 
1949
1949
  <!-- REF: FEATURES -->
1950
- <div v-else-if="activeSection === 'ref-features'">
1950
+ <section id="ref-features">
1951
1951
  <h1>Features Slide</h1>
1952
1952
  <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
1953
 
@@ -1989,10 +1989,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
1989
1989
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
1990
1990
  <LivePreview :initial-code="EXAMPLES.features" />
1991
1991
  </div>
1992
- </div>
1992
+ </section>
1993
1993
 
1994
1994
  <!-- REF: TIMELINE -->
1995
- <div v-else-if="activeSection === 'ref-timeline'">
1995
+ <section id="ref-timeline">
1996
1996
  <h1>Timeline Slide</h1>
1997
1997
  <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
1998
 
@@ -2035,10 +2035,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2035
2035
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
2036
2036
  <LivePreview :initial-code="EXAMPLES.timeline" />
2037
2037
  </div>
2038
- </div>
2038
+ </section>
2039
2039
 
2040
2040
  <!-- REF: STEPS -->
2041
- <div v-else-if="activeSection === 'ref-steps'">
2041
+ <section id="ref-steps">
2042
2042
  <h1>Steps Slide</h1>
2043
2043
  <p class="lead">Numbered steps for tutorials, how-to guides, or process flows. Responsive grid. SlideBase applies.</p>
2044
2044
 
@@ -2081,10 +2081,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2081
2081
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
2082
2082
  <LivePreview :initial-code="EXAMPLES.steps" />
2083
2083
  </div>
2084
- </div>
2084
+ </section>
2085
2085
 
2086
2086
  <!-- REF: FLEX -->
2087
- <div v-else-if="activeSection === 'ref-flex'">
2087
+ <section id="ref-flex">
2088
2088
  <h1>Flex Layout</h1>
2089
2089
  <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
2090
 
@@ -2147,10 +2147,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2147
2147
  <h2 class="text-xl font-bold text-white mb-4">Example</h2>
2148
2148
  <LivePreview :initial-code="EXAMPLES.flex" />
2149
2149
  </div>
2150
- </div>
2150
+ </section>
2151
2151
 
2152
2152
  <!-- REF: CHART -->
2153
- <div v-else-if="activeSection === 'ref-chart'">
2153
+ <section id="ref-chart">
2154
2154
  <h1>Chart Slide</h1>
2155
2155
  <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
2156
 
@@ -2439,10 +2439,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2439
2439
  charts automatically</li>
2440
2440
  </ul>
2441
2441
  </div>
2442
- </div>
2442
+ </section>
2443
2443
 
2444
2444
  <!-- REF: VIDEO -->
2445
- <div v-else-if="activeSection === 'ref-video'">
2445
+ <section id="ref-video">
2446
2446
  <h1>Video Slide</h1>
2447
2447
  <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
2448
 
@@ -2508,10 +2508,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2508
2508
  </div>
2509
2509
 
2510
2510
  <p class="text-white/60 text-sm">Element control ids: <code>s{N}-video</code>, <code>s{N}-title</code>.</p>
2511
- </div>
2511
+ </section>
2512
2512
 
2513
2513
  <!-- REF: CUSTOM HTML -->
2514
- <div v-else-if="activeSection === 'ref-custom'">
2514
+ <section id="ref-custom">
2515
2515
  <h1>Custom HTML Slide</h1>
2516
2516
  <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
2517
 
@@ -2585,10 +2585,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2585
2585
  creative freedom</li>
2586
2586
  </ul>
2587
2587
  </div>
2588
- </div>
2588
+ </section>
2589
2589
 
2590
2590
  <!-- REF: DIAGRAM -->
2591
- <div v-else-if="activeSection === 'ref-diagram'">
2591
+ <section id="ref-diagram">
2592
2592
  <h1>Diagram Slide</h1>
2593
2593
  <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
2594
 
@@ -2610,10 +2610,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2610
2610
  </div>
2611
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>
2612
2612
  </div>
2613
- </div>
2613
+ </section>
2614
2614
 
2615
2615
  <!-- REF: FREE -->
2616
- <div v-else-if="activeSection === 'ref-free'">
2616
+ <section id="ref-free">
2617
2617
  <h1>Free / Composition Slide</h1>
2618
2618
  <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
2619
 
@@ -2675,10 +2675,10 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2675
2675
  }
2676
2676
  }</code></pre>
2677
2677
  </div>
2678
- </div>
2678
+ </section>
2679
2679
 
2680
2680
  <!-- REF: AUTO -->
2681
- <div v-else-if="activeSection === 'ref-auto'">
2681
+ <section id="ref-auto">
2682
2682
  <h1>Auto Strategy</h1>
2683
2683
  <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
2684
 
@@ -2742,22 +2742,50 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2742
2742
  ]
2743
2743
  }' />
2744
2744
  </div>
2745
+ </section>
2746
+
2745
2747
  </div>
2746
2748
 
2747
2749
  </div>
2748
- </Transition>
2749
2750
  </main>
2750
2751
  </div>
2751
2752
  </template>
2752
2753
 
2753
2754
  <script setup lang="ts">
2754
- import { ref, watch, onUnmounted, nextTick } from 'vue';
2755
+ import { ref, watch, onUnmounted, onMounted, nextTick } from 'vue';
2755
2756
  import { Lumina } from '../../core/Lumina';
2756
2757
  import { parsePartialJson } from '../../utils/streaming';
2757
2758
  import LivePreview from './LivePreview.vue';
2758
2759
 
2759
2760
  const activeSection = ref('intro');
2760
2761
 
2762
+ 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'];
2763
+
2764
+ function scrollToTop() {
2765
+ window.scrollTo({ top: 0, behavior: 'smooth' });
2766
+ }
2767
+
2768
+ function goToLayout(id: string) {
2769
+ activeSection.value = id;
2770
+ nextTick(() => {
2771
+ document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
2772
+ if (typeof location !== 'undefined') location.hash = id;
2773
+ });
2774
+ }
2775
+
2776
+ function goToStreamingDemo() {
2777
+ activeSection.value = 'agents-streaming';
2778
+ nextTick(() => document.getElementById('streaming-demo-container')?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
2779
+ }
2780
+
2781
+ onMounted(() => {
2782
+ if (typeof window === 'undefined') return;
2783
+ const h = window.location.hash.slice(1) || '';
2784
+ if (!h) return;
2785
+ activeSection.value = h;
2786
+ nextTick(() => document.getElementById(h)?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
2787
+ });
2788
+
2761
2789
  // --- DEMO LOGIC ---
2762
2790
  const demoInput = ref('');
2763
2791
  const demoStarted = ref(false);
@@ -2847,7 +2875,7 @@ async function runDemo() {
2847
2875
 
2848
2876
  function loadIntoDemo(json: string) {
2849
2877
  activeSection.value = 'agents-streaming';
2850
- window.scrollTo({ top: 0, behavior: 'smooth' });
2878
+ nextTick(() => document.getElementById('streaming-demo-container')?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
2851
2879
  demoInput.value = '';
2852
2880
  demoStarted.value = true;
2853
2881
 
@@ -3057,6 +3085,7 @@ const navigation = [
3057
3085
  title: 'AI & LLM Integration',
3058
3086
  items: [
3059
3087
  { id: 'agents-streaming', label: 'Streaming & Realtime' },
3088
+ { id: 'agents-layouts', label: 'Layout Gallery' },
3060
3089
  { id: 'agents-intro', label: 'Agent Protocol' },
3061
3090
  { id: 'agents-tokens', label: 'Token Optimization' }
3062
3091
  ]
@@ -3077,21 +3106,13 @@ const navigation = [
3077
3106
  ];
3078
3107
 
3079
3108
 
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
3109
  </script>
3093
3110
 
3094
3111
  <style scoped>
3112
+ .doc-content [id] {
3113
+ scroll-margin-top: 8.5rem;
3114
+ }
3115
+
3095
3116
  .doc-content h1 {
3096
3117
  @apply text-4xl md:text-5xl font-black tracking-tight text-white mb-8 leading-tight font-heading;
3097
3118
  }
@@ -433,28 +433,50 @@ export class Lumina {
433
433
  return createElementController(this.store, this, idOrSlide);
434
434
  }
435
435
 
436
+ /**
437
+ * Returns the element at the given path on the current slide. Shorthand for
438
+ * engine.element(engine.currentSlideIndex, path). Use when you don't need the slide index.
439
+ *
440
+ * @param path - Logical path (e.g. "title", "features.0", ["elements", 0, "elements", 1]).
441
+ * @returns ElementController for that element.
442
+ *
443
+ * @example
444
+ * engine.elementInCurrent("title").show();
445
+ * engine.elementInCurrent("features.0").animate({ from: { opacity: 0 }, to: { opacity: 1 }, duration: 0.5 });
446
+ *
447
+ * @see element
448
+ * @see elements
449
+ */
450
+ public elementInCurrent(path: string | ElementPath): ElementController {
451
+ const i = this.store.state.currentIndex ?? 0;
452
+ return this.element(i, path);
453
+ }
454
+
436
455
  /**
437
456
  * Returns all element ids for a slide. Use to discover which ids exist (e.g. for
438
457
  * meta.initialElementState, or to iterate and control elements). Each id can be passed to
439
458
  * engine.element(id). Uses the same path→id logic as element(slideIndex, path); ids follow
440
- * resolveId (explicit id, slide.ids, or elemId(slideIndex, ...path)).
459
+ * resolveId (explicit id, slide.ids, or slide.id / elemId(slideIndex, ...path) as fallback).
441
460
  *
442
- * @param slideIndex - Zero-based slide index. If out of range or deck not loaded, returns [].
443
- * @returns Array of id strings (e.g. ["s0-slide","s0-tag","s0-title","s0-subtitle"] for a statement slide).
461
+ * @param slideIndex - Zero-based slide index. Omit to use the current slide. If out of range or deck not loaded, returns [].
462
+ * @returns Array of id strings (e.g. ["s0-slide","s0-tag","s0-title","s0-subtitle"] or ["intro-slide","intro-tag",...] when slide.id is set).
444
463
  *
445
464
  * @example
446
- * const ids = engine.elements(0);
447
- * ids.forEach(id => { if (id.endsWith('-title')) engine.element(id).hide(); });
465
+ * engine.elements(); // current slide
466
+ * engine.elements(0);
467
+ * engine.elements().forEach(id => { if (id.endsWith('-title')) engine.element(id).hide(); });
448
468
  *
449
469
  * @see element
470
+ * @see elementInCurrent
450
471
  * @see getElementIds
451
472
  * @see resolveId
452
473
  * @see DeckMeta.initialElementState
453
474
  */
454
- public elements(slideIndex: number): string[] {
455
- const slide = this.store.state.deck?.slides[slideIndex];
475
+ public elements(slideIndex?: number): string[] {
476
+ const i = slideIndex ?? this.store.state.currentIndex ?? 0;
477
+ const slide = this.store.state.deck?.slides[i];
456
478
  if (!slide) return [];
457
- return getElementIds(slide, slideIndex);
479
+ return getElementIds(slide, i);
458
480
  }
459
481
 
460
482
  /**
@@ -71,7 +71,8 @@ export function parsePath(input: string | ElementPath): ElementPath {
71
71
  * 1. Path [] or ['slide'] → slide.id or elemId(slideIndex, 'slide').
72
72
  * 2. If the value at path is an object with string `id` → use it (e.g. feature.id, node.id).
73
73
  * 3. If slide.ids[pathToKey(path)] exists → use it (supports compound keys: 'tag', 'features.0', 'elements.0.elements.1').
74
- * 4. Otherwise → elemId(slideIndex, ...path) (e.g. "s0-tag", "s1-elements-0-elements-1").
74
+ * 4. Otherwise → if slide.id is set, "{slide.id}-{path}"; else elemId(slideIndex, ...path)
75
+ * (e.g. "intro-tag" with slide.id "intro", or "s0-tag" when no slide.id).
75
76
  *
76
77
  * @param slide - The slide data.
77
78
  * @param slideIndex - Zero-based slide index (for elemId fallback).
@@ -99,6 +100,11 @@ export function resolveId(
99
100
  if (key && slideAny?.ids?.[key]) {
100
101
  return slideAny.ids[key];
101
102
  }
103
+ // Fallback: use slide.id as namespace when present, so IDs stay stable when slides are
104
+ // inserted, removed or reordered. Otherwise s{N}-{path} (same as before).
105
+ if (slideAny?.id != null && slideAny.id !== '') {
106
+ return `${slideAny.id}-${path.map(String).join('-')}`;
107
+ }
102
108
  return elemId(slideIndex, ...path);
103
109
  }
104
110
 
package/src/core/types.ts CHANGED
@@ -448,8 +448,8 @@ export type TimelineKeyframes = Record<string, TimelineKeyframeState>;
448
448
  export type TimelineTracks = Record<string, TimelineKeyframes>;
449
449
 
450
450
  /**
451
- * Fluent API to control one slide element in real time. Returned by `engine.element(id)`
452
- * and `engine.element(slideIndex, path)`. All methods return `this` for chaining.
451
+ * Fluent API to control one slide element in real time. Returned by `engine.element(id)`,
452
+ * `engine.element(slideIndex, path)`, and `engine.elementInCurrent(path)`. All methods return `this` for chaining.
453
453
  *
454
454
  * Use for: starting with an empty slide and revealing items in order, hiding highlights,
455
455
  * animating entrances on demand, or syncing with voice/agent.
@@ -462,6 +462,7 @@ export type TimelineTracks = Record<string, TimelineKeyframes>;
462
462
  * engine.element(0, 'subtitle').show();
463
463
  *
464
464
  * @see Lumina.element
465
+ * @see Lumina.elementInCurrent
465
466
  * @see Lumina.elements
466
467
  * @see ElementState
467
468
  * @see AnimateOptions
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * **Deck:** `{ meta: { title, initialElementState?, elementControl?, effects? }, slides: [...] }`
11
11
  * Slide types: statement, features, half, timeline, steps, flex, chart, diagram, custom, video.
12
12
  *
13
- * **Element control:** `engine.element(id)` or `engine.element(slideIndex, path)` → ElementController:
13
+ * **Element control:** `engine.element(id)`, `engine.element(slideIndex, path)`, or `engine.elementInCurrent(path)` → ElementController:
14
14
  * `.show()`, `.hide()`, `.toggle()`, `.opacity(n)`, `.transform(s)`, `.animate({ preset?, from?, to?, duration?, ease? })`.
15
15
  * Presets: fadeUp, fadeIn, scaleIn, slideLeft, slideRight, zoomIn, blurIn, spring, drop, fadeOut. `to` optional when using preset.
16
16
  * Ids: `engine.elements(slideIndex)` or `s{N}-{path}` (e.g. s0-tag, s1-features-0). `meta.initialElementState`: