lumina-slides 9.0.3 → 9.0.5

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.
@@ -310,7 +310,7 @@ engine.load(myDeckData);</code></pre>
310
310
  5</div>
311
311
  <div>
312
312
  <h2 class="text-2xl font-bold text-white mb-1">Flex</h2>
313
- <p class="text-sm text-white/50">Flow-based layout with size tokens (<code>half</code>, <code>third</code>, …). Elements: <code>title</code>, <code>text</code>, <code>bullets</code>, <code>image</code>, <code>video</code>, <code>button</code>, <code>content</code>, <code>timeline</code>, <code>stepper</code>, <code>spacer</code>. See <em>Flex</em> in Layouts.</p>
313
+ <p class="text-sm text-white/50">Flow-based layout with size tokens (<code>half</code>, <code>third</code>, …). Elements: <code>title</code>, <code>text</code>, <code>bullets</code>, <code>image</code>, <code>html</code>, <code>video</code>, <code>button</code>, <code>content</code> (supports nesting), <code>timeline</code>, <code>stepper</code>, <code>spacer</code>. See <em>Flex</em> in Layouts.</p>
314
314
  </div>
315
315
  </div>
316
316
  <div class="prose prose-invert max-w-none prose-pre:bg-black/50 prose-pre:border-none">
@@ -454,30 +454,52 @@ engine.<span class="text-yellow-400">on</span>('navigate', ({ direction, toIndex
454
454
  <ol class="list-decimal list-inside mb-6 space-y-1 text-white/70">
455
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>
456
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>
457
- <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>
458
- <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>
459
459
  </ol>
460
460
 
461
- <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>
462
465
  <div class="overflow-x-auto mb-6">
463
466
  <table class="w-full text-sm border border-white/10 rounded-lg">
464
- <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>
465
468
  <tbody class="text-white/70">
466
- <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>
467
- <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>
468
- <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>
469
- <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>
470
- <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>
471
- <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>
472
- <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>
473
- <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>
474
- <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>
475
- <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>
476
498
  </tbody>
477
499
  </table>
478
500
  </div>
479
501
 
480
- <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>
481
503
 
482
504
  <h2>Option 1: meta.initialElementState</h2>
483
505
  <p>Define in the deck which elements start hidden. Ids follow the pattern
@@ -1903,7 +1925,7 @@ 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">
@@ -1937,7 +1959,7 @@ 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">
@@ -1982,7 +2004,7 @@ 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">
@@ -2028,7 +2050,7 @@ 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">
@@ -2074,7 +2096,7 @@ 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">
@@ -2121,8 +2143,9 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2121
2143
  <tr><th class="p-3 border-b border-white/10">type</th><th class="p-3 border-b border-white/10">Properties</th><th class="p-3 border-b border-white/10">Description</th></tr>
2122
2144
  </thead>
2123
2145
  <tbody class="divide-y divide-white/5">
2124
- <tr><td class="p-3 font-mono text-cyan-400">content</td><td class="p-3 font-mono text-xs">elements, valign?, halign?, gap?, padding?, size?</td><td class="p-3">Groups children vertically. Children: title, text, bullets, ordered, button, timeline, stepper, spacer (no image/video/nested content).</td></tr>
2125
- <tr><td class="p-3 font-mono text-cyan-400">image</td><td class="p-3 font-mono text-xs">src, alt?, fill?, fit?, rounded?, href?, target?, class?, size?, id?</td><td class="p-3">Media. fill: edge-to-edge. fit: cover|contain. href+target: link. Rounded: none|sm|md|lg|xl|full.</td></tr>
2146
+ <tr><td class="p-3 font-mono text-cyan-400">content</td><td class="p-3 font-mono text-xs">elements, direction?, valign?, halign?, gap?, padding?, size?, class?, style?, id?</td><td class="p-3">Groups children with alignment. <code>direction</code>: horizontal|vertical (default: vertical). Supports nested content, html, image, and all child elements. <code>class</code> and <code>style</code> for custom styling.</td></tr>
2147
+ <tr><td class="p-3 font-mono text-cyan-400">image</td><td class="p-3 font-mono text-xs">src, alt?, fill?, fit?, position?, rounded?, href?, target?, class?, style?, size?, id?</td><td class="p-3">Media. <code>fill</code>: edge-to-edge. <code>fit</code>: cover|contain|fill|none|scale-down. <code>position</code>: object-position (e.g., "top center"). <code>href</code>+<code>target</code>: link. <code>rounded</code>: none|sm|md|lg|xl|full. <code>style</code>: custom CSS.</td></tr>
2148
+ <tr><td class="p-3 font-mono text-cyan-400">html</td><td class="p-3 font-mono text-xs">html, class?, style?, size?, id?</td><td class="p-3">Raw HTML content. Renders any HTML string. Supports <code>class</code> and <code>style</code> for customization. Automatically styled to match Lumina theme.</td></tr>
2126
2149
  <tr><td class="p-3 font-mono text-cyan-400">video</td><td class="p-3 font-mono text-xs">src, poster?, autoplay?, loop?, muted?, controls?, fill?, fit?, rounded?, class?, size?, id?</td><td class="p-3">Video element. Same fill/fit/rounded as image.</td></tr>
2127
2150
  <tr><td class="p-3 font-mono text-cyan-400">title</td><td class="p-3 font-mono text-xs">text, size? (lg|xl|2xl|3xl), align?, id?</td><td class="p-3">Heading. align: left|center|right. Top-level only: optional FlexSize <code>size</code>.</td></tr>
2128
2151
  <tr><td class="p-3 font-mono text-cyan-400">text</td><td class="p-3 font-mono text-xs">text, align?, muted?, size?, id?</td><td class="p-3">Paragraph. muted: subtle style.</td></tr>
@@ -2135,12 +2158,53 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2135
2158
  </tbody>
2136
2159
  </table>
2137
2160
  </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>
2161
+ <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>
2162
+ </div>
2163
+
2164
+ <div class="my-8">
2165
+ <h2 class="text-xl font-bold text-white mb-4">Content container (direction, valign, halign, gap, padding)</h2>
2166
+ <p><strong>Direction:</strong> <code>horizontal</code> | <code>vertical</code> (default: <code>vertical</code>). Controls layout direction of child elements.</p>
2167
+ <p><strong>Defaults:</strong> direction <code>vertical</code>, valign <code>center</code>, halign <code>left</code>, gap <code>md</code>, padding <code>lg</code>.</p>
2168
+ <p class="mt-2"><strong>Nested content:</strong> Content containers can be nested inside other content containers for complex layouts. Supports unlimited nesting levels.</p>
2169
+ </div>
2170
+
2171
+ <div class="my-8">
2172
+ <h2 class="text-xl font-bold text-white mb-4">HTML Element</h2>
2173
+ <p>The <code>html</code> element allows you to insert raw HTML content into your flex layout. This is useful for custom formatting, embedded content, or any HTML that isn't covered by other element types.</p>
2174
+ <div class="mt-4 p-4 bg-white/5 rounded-lg border border-white/10">
2175
+ <p class="text-sm text-white/70 mb-2"><strong>Example:</strong></p>
2176
+ <pre class="text-xs"><code>{{
2177
+ `{
2178
+ "type": "html",
2179
+ "html": "<p>Insert <strong>any HTML</strong> here.</p><ul><li>List item</li></ul>",
2180
+ "class": "custom-class",
2181
+ "style": { "maxWidth": "600px" }
2182
+ }`}}</code></pre>
2183
+ </div>
2184
+ <p class="text-sm text-white/50 mt-2">The HTML is automatically styled to match Lumina's theme. Common HTML elements (h1-h6, p, a, ul, ol, li) are styled with Lumina CSS variables.</p>
2139
2185
  </div>
2140
2186
 
2141
2187
  <div class="my-8">
2142
- <h2 class="text-xl font-bold text-white mb-4">Content container (valign, halign, gap, padding)</h2>
2143
- <p>Defaults: valign <code>center</code>, halign <code>left</code>, gap <code>md</code>, padding <code>lg</code>.</p>
2188
+ <h2 class="text-xl font-bold text-white mb-4">Enhanced Image Element</h2>
2189
+ <p>The <code>image</code> element now supports advanced positioning and styling options:</p>
2190
+ <ul class="list-disc list-inside text-white/70 space-y-1 mt-2">
2191
+ <li><code>fit</code>: <code>cover</code> | <code>contain</code> | <code>fill</code> | <code>none</code> | <code>scale-down</code> - Controls how the image fits its container</li>
2192
+ <li><code>position</code>: string (e.g., <code>"top center"</code>, <code>"left bottom"</code>) - Controls object-position CSS property</li>
2193
+ <li><code>style</code>: object - Custom CSS styles as key-value pairs</li>
2194
+ <li><code>class</code>: string - Custom CSS class names</li>
2195
+ </ul>
2196
+ <div class="mt-4 p-4 bg-white/5 rounded-lg border border-white/10">
2197
+ <p class="text-sm text-white/70 mb-2"><strong>Example:</strong></p>
2198
+ <pre class="text-xs"><code>{{
2199
+ `{
2200
+ "type": "image",
2201
+ "src": "photo.jpg",
2202
+ "fit": "contain",
2203
+ "position": "top center",
2204
+ "rounded": "lg",
2205
+ "style": { "border": "2px solid var(--lumina-color-primary)" }
2206
+ }`}}</code></pre>
2207
+ </div>
2144
2208
  </div>
2145
2209
 
2146
2210
  <div class="my-8">
@@ -2195,6 +2259,7 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2195
2259
  </tbody>
2196
2260
  </table>
2197
2261
  </div>
2262
+ <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
2263
  </div>
2199
2264
 
2200
2265
  <div class="my-8">
@@ -2507,7 +2572,7 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2507
2572
  <LivePreview :initial-code="EXAMPLES.video_slide" />
2508
2573
  </div>
2509
2574
 
2510
- <p class="text-white/60 text-sm">Element control ids: <code>s{N}-video</code>, <code>s{N}-title</code>.</p>
2575
+ <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>
2511
2576
  </section>
2512
2577
 
2513
2578
  <!-- REF: CUSTOM HTML -->
@@ -2608,7 +2673,7 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2608
2673
  </tbody>
2609
2674
  </table>
2610
2675
  </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>
2676
+ <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
2677
  </div>
2613
2678
  </section>
2614
2679
 
@@ -2651,6 +2716,7 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2651
2716
  </tbody>
2652
2717
  </table>
2653
2718
  </div>
2719
+ <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
2720
 
2655
2721
  <h3 class="text-lg font-bold text-white mt-8 mb-4">Positioning and animation (timelineTracks)</h3>
2656
2722
  <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>
@@ -2753,10 +2819,13 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2753
2819
 
2754
2820
  <script setup lang="ts">
2755
2821
  import { ref, watch, onUnmounted, onMounted, nextTick } from 'vue';
2822
+ import TurndownService from 'turndown';
2756
2823
  import { Lumina } from '../../core/Lumina';
2757
2824
  import { parsePartialJson } from '../../utils/streaming';
2758
2825
  import LivePreview from './LivePreview.vue';
2759
2826
 
2827
+ const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
2828
+
2760
2829
  const activeSection = ref('intro');
2761
2830
 
2762
2831
  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'];
@@ -2778,12 +2847,74 @@ function goToStreamingDemo() {
2778
2847
  nextTick(() => document.getElementById('streaming-demo-container')?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
2779
2848
  }
2780
2849
 
2850
+ function getHeadingLevel(el: Element): number {
2851
+ const m = /^H([1-6])$/.exec(el.tagName || '');
2852
+ return m ? parseInt(m[1], 10) : 7;
2853
+ }
2854
+
2855
+ async function copySectionAsMarkdown(heading: HTMLElement | null, button?: HTMLElement) {
2856
+ if (!heading || !heading.closest('.doc-content')) return;
2857
+ const level = getHeadingLevel(heading);
2858
+ const container = document.createElement('div');
2859
+ const headingClone = heading.cloneNode(true) as HTMLElement;
2860
+ headingClone.querySelector('.doc-copy-btn')?.remove();
2861
+ container.appendChild(headingClone);
2862
+ let el: Element | null = heading.nextElementSibling;
2863
+ while (el) {
2864
+ if (/^H[1-6]$/i.test(el.tagName) && getHeadingLevel(el) <= level) break;
2865
+ container.appendChild(el.cloneNode(true));
2866
+ el = el.nextElementSibling;
2867
+ }
2868
+ try {
2869
+ const md = turndownService.turndown(container);
2870
+ await navigator.clipboard.writeText(md);
2871
+ if (button) {
2872
+ const orig = button.innerHTML;
2873
+ button.innerHTML = CHECK_ICON_SVG;
2874
+ button.classList.add('doc-copy-btn-done');
2875
+ button.setAttribute('aria-label', 'Copiado');
2876
+ setTimeout(() => {
2877
+ button.innerHTML = orig;
2878
+ button.classList.remove('doc-copy-btn-done');
2879
+ button.setAttribute('aria-label', 'Copiar sección en Markdown');
2880
+ }, 2000);
2881
+ }
2882
+ } catch (_) { /* clipboard or turndown error */ }
2883
+ }
2884
+
2885
+ const COPY_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8Zm-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z"/></svg>';
2886
+ const CHECK_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="M229.66 77.66l-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69 218.34 66.34a8 8 0 0 1 11.32 11.32Z"/></svg>';
2887
+
2888
+ function injectCopyButtons() {
2889
+ const root = document.querySelector('.doc-content');
2890
+ if (!root) return;
2891
+ root.querySelectorAll('h1, h2, h3').forEach((h) => {
2892
+ if ((h as HTMLElement).querySelector('.doc-copy-btn')) return;
2893
+ const btn = document.createElement('button');
2894
+ btn.type = 'button';
2895
+ btn.className = 'doc-copy-btn';
2896
+ btn.title = 'Copiar sección en Markdown';
2897
+ btn.setAttribute('aria-label', 'Copiar sección en Markdown');
2898
+ btn.innerHTML = COPY_ICON_SVG;
2899
+ btn.addEventListener('click', (e) => {
2900
+ e.stopPropagation();
2901
+ copySectionAsMarkdown((e.currentTarget as HTMLElement).parentElement, e.currentTarget as HTMLElement);
2902
+ });
2903
+ (h as HTMLElement).appendChild(btn);
2904
+ });
2905
+ }
2906
+
2907
+ watch(activeSection, () => nextTick(injectCopyButtons));
2908
+
2781
2909
  onMounted(() => {
2782
2910
  if (typeof window === 'undefined') return;
2783
2911
  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' }));
2912
+ if (h) {
2913
+ activeSection.value = h;
2914
+ nextTick(() => document.getElementById(h)?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
2915
+ }
2916
+ nextTick(injectCopyButtons);
2917
+ setTimeout(injectCopyButtons, 150);
2787
2918
  });
2788
2919
 
2789
2920
  // --- DEMO LOGIC ---
@@ -2950,13 +3081,31 @@ const EXAMPLES = {
2950
3081
  "type": "flex",
2951
3082
  "sizing": "container",
2952
3083
  "direction": "horizontal",
3084
+ "gap": "lg",
2953
3085
  "elements": [
2954
- { "type": "image", "src": "./brains.png", "size": "half", "fill": true },
2955
- { "type": "content", "size": "half", "valign": "center", "padding": "xl", "gap": "md", "elements": [
2956
- { "type": "title", "text": "Visual Story", "size": "2xl" },
2957
- { "type": "bullets", "items": ["Image fills height", "Content centered", "No coordinates"] },
2958
- { "type": "button", "label": "Learn More", "variant": "primary" }
2959
- ]}
3086
+ {
3087
+ "type": "image",
3088
+ "src": "./brains.png",
3089
+ "size": "half",
3090
+ "fill": true,
3091
+ "fit": "cover",
3092
+ "position": "center"
3093
+ },
3094
+ {
3095
+ "type": "content",
3096
+ "size": "half",
3097
+ "direction": "vertical",
3098
+ "valign": "center",
3099
+ "padding": "xl",
3100
+ "gap": "md",
3101
+ "elements": [
3102
+ { "type": "title", "text": "Visual Story", "size": "2xl" },
3103
+ { "type": "text", "text": "Enhanced flex layout with new features" },
3104
+ { "type": "html", "html": "<p>Insert <strong>custom HTML</strong> here</p>" },
3105
+ { "type": "bullets", "items": ["Nested content support", "HTML elements", "Enhanced images"] },
3106
+ { "type": "button", "label": "Learn More", "variant": "primary" }
3107
+ ]
3108
+ }
2960
3109
  ]
2961
3110
  }` ,
2962
3111
  video: `{
@@ -3114,15 +3263,22 @@ const navigation = [
3114
3263
  }
3115
3264
 
3116
3265
  .doc-content h1 {
3117
- @apply text-4xl md:text-5xl font-black tracking-tight text-white mb-8 leading-tight font-heading;
3266
+ @apply text-4xl md:text-5xl font-black tracking-tight text-white mb-8 leading-tight font-heading flex flex-wrap items-center gap-2;
3118
3267
  }
3119
3268
 
3120
3269
  .doc-content h2 {
3121
- @apply text-2xl md:text-3xl font-bold text-white mt-16 mb-6 tracking-tight font-heading;
3270
+ @apply text-2xl md:text-3xl font-bold text-white mt-16 mb-6 tracking-tight font-heading flex flex-wrap items-center gap-2;
3122
3271
  }
3123
3272
 
3124
3273
  .doc-content h3 {
3125
- @apply text-[20px] font-extrabold text-white mb-[15px] mt-8 font-heading;
3274
+ @apply text-[20px] font-extrabold text-white mb-[15px] mt-8 font-heading flex flex-wrap items-center gap-2;
3275
+ }
3276
+
3277
+ .doc-content :deep(.doc-copy-btn) {
3278
+ @apply inline-flex items-center justify-center w-8 h-8 rounded-lg text-white/40 hover:text-white/90 hover:bg-white/10 transition-colors flex-shrink-0;
3279
+ }
3280
+ .doc-content :deep(.doc-copy-btn-done) {
3281
+ @apply text-green-400;
3126
3282
  }
3127
3283
 
3128
3284
  .doc-content p {
@@ -63,14 +63,20 @@ export const getFlexContainerStyle = (element: FlexElementContent): CSSPropertie
63
63
  const halign = element.halign || 'left';
64
64
  const gap = element.gap || 'md';
65
65
  const padding = element.padding || 'lg';
66
+ const direction = element.direction || 'vertical';
66
67
 
67
68
  return {
68
69
  display: 'flex',
69
- flexDirection: 'column', // Content containers are always column based for children
70
- justifyContent: valignMap[valign] as CSSProperties['justifyContent'],
71
- alignItems: halignMap[halign] as CSSProperties['alignItems'],
70
+ flexDirection: direction === 'horizontal' ? 'row' : 'column',
71
+ justifyContent: direction === 'horizontal'
72
+ ? halignMap[halign] as CSSProperties['justifyContent']
73
+ : valignMap[valign] as CSSProperties['justifyContent'],
74
+ alignItems: direction === 'horizontal'
75
+ ? valignMap[valign] as CSSProperties['alignItems']
76
+ : halignMap[halign] as CSSProperties['alignItems'],
72
77
  gap: spacingVarMap[gap],
73
78
  padding: spacingVarMap[padding],
79
+ ...(element.style || {}),
74
80
  };
75
81
  };
76
82
 
@@ -113,22 +113,38 @@ export type PathGenerator = (slide: Readonly<BaseSlideData>) => ElementPath[];
113
113
 
114
114
  /** Map of slide.type → path generator. Extend to support new layouts. */
115
115
  const PATH_GENERATORS: Record<string, PathGenerator> = {
116
- statement: () => [['tag'], ['title'], ['subtitle']],
116
+ statement: (s) => {
117
+ const paths: ElementPath[] = [['title']];
118
+ if (getValueAt(s, ['tag'])) paths.unshift(['tag']);
119
+ if (getValueAt(s, ['subtitle'])) paths.push(['subtitle']);
120
+ return paths;
121
+ },
117
122
  features: (s) => {
118
123
  const arr = getValueAt(s, ['features']);
119
124
  const list = Array.isArray(arr) ? arr : [];
120
- return [['header'], ...list.map((_: unknown, i: number) => ['features', i] as ElementPath)];
125
+ const paths: ElementPath[] = [['header'], ['title']];
126
+ if (getValueAt(s, ['description'])) paths.push(['description']);
127
+ return [...paths, ...list.map((_: unknown, i: number) => ['features', i] as ElementPath)];
128
+ },
129
+ half: (s) => {
130
+ const paths: ElementPath[] = [['media'], ['title'], ['paragraphs']];
131
+ if (getValueAt(s, ['tag'])) paths.splice(1, 0, ['tag']); // after media, before title
132
+ if (getValueAt(s, ['cta'])) paths.push(['cta']);
133
+ return paths;
121
134
  },
122
- half: () => [['media'], ['tag'], ['title'], ['paragraphs'], ['cta']],
123
135
  timeline: (s) => {
124
136
  const arr = getValueAt(s, ['timeline']);
125
137
  const list = Array.isArray(arr) ? arr : [];
126
- return [['title'], ['subtitle'], ...list.map((_: unknown, i: number) => ['timeline', i] as ElementPath)];
138
+ const paths: ElementPath[] = [['title']];
139
+ if (getValueAt(s, ['subtitle'])) paths.push(['subtitle']);
140
+ return [...paths, ...list.map((_: unknown, i: number) => ['timeline', i] as ElementPath)];
127
141
  },
128
142
  steps: (s) => {
129
143
  const arr = getValueAt(s, ['steps']);
130
144
  const list = Array.isArray(arr) ? arr : [];
131
- return [['header'], ...list.map((_: unknown, i: number) => ['steps', i] as ElementPath)];
145
+ const paths: ElementPath[] = [['header'], ['title']];
146
+ if (getValueAt(s, ['subtitle'])) paths.push(['subtitle']);
147
+ return [...paths, ...list.map((_: unknown, i: number) => ['steps', i] as ElementPath)];
132
148
  },
133
149
  flex: (s) => {
134
150
  const paths: ElementPath[] = [];
@@ -143,7 +159,13 @@ const PATH_GENERATORS: Record<string, PathGenerator> = {
143
159
  });
144
160
  return paths;
145
161
  },
146
- chart: () => [['title'], ['subtitle'], ['chart']],
162
+ chart: (s) => {
163
+ const paths: ElementPath[] = [];
164
+ if (getValueAt(s, ['title'])) paths.push(['title']);
165
+ if (getValueAt(s, ['subtitle'])) paths.push(['subtitle']);
166
+ paths.push(['chart']);
167
+ return paths;
168
+ },
147
169
  diagram: (s) => {
148
170
  const p: ElementPath[] = [];
149
171
  const nodes = getValueAt(s, ['nodes']);
@@ -153,7 +175,11 @@ const PATH_GENERATORS: Record<string, PathGenerator> = {
153
175
  return p;
154
176
  },
155
177
  custom: () => [],
156
- video: () => [['video'], ['title']],
178
+ video: (s) => {
179
+ const paths: ElementPath[] = [['video']];
180
+ if (getValueAt(s, ['title'])) paths.push(['title']);
181
+ return paths;
182
+ },
157
183
  free: (s) => {
158
184
  const arr = getValueAt(s, ['elements']);
159
185
  const list = Array.isArray(arr) ? arr : [];
@@ -179,8 +179,20 @@ const FlexElementImageSchema = z.object({
179
179
  src: fuzzyString.describe("Image URL."),
180
180
  alt: fuzzyString.optional().describe("Alt text for accessibility."),
181
181
  fill: z.boolean().optional().describe("Fill container edge-to-edge. Default: true."),
182
- fit: z.enum(['cover', 'contain']).optional().describe("Object-fit when fill. Default: 'cover'."),
182
+ fit: z.enum(['cover', 'contain', 'fill', 'none', 'scale-down']).optional().describe("Object-fit mode. Default: 'cover' when fill is true."),
183
+ position: fuzzyString.optional().describe("Object-position for image placement. Default: 'center'."),
183
184
  rounded: z.enum(['none', 'sm', 'md', 'lg', 'xl', 'full']).optional().describe("Border radius."),
185
+ href: fuzzyString.optional().describe("Link URL when image is clicked."),
186
+ target: z.enum(['_blank', '_self']).optional().describe("Link target. Default: '_blank'."),
187
+ class: fuzzyString.optional().describe("Custom CSS class."),
188
+ style: z.record(z.string()).optional().describe("Custom CSS style object."),
189
+ });
190
+
191
+ const FlexElementHtmlSchema = z.object({
192
+ type: z.literal('html').describe("Raw HTML content element."),
193
+ html: fuzzyString.describe("Raw HTML string to render."),
194
+ class: fuzzyString.optional().describe("Custom CSS class."),
195
+ style: z.record(z.string()).optional().describe("Custom CSS style object."),
184
196
  });
185
197
 
186
198
  const FlexElementButtonSchema = z.object({
@@ -208,8 +220,27 @@ const FlexElementSpacerSchema = z.object({
208
220
  size: SpacingTokenSchema.optional().describe("Size of the space. Default: 'md'."),
209
221
  });
210
222
 
211
- // Child elements (inside content container)
212
- const FlexChildElementSchema = z.discriminatedUnion('type', [
223
+ // Child elements (inside content container) - supports nested content, html, and image
224
+ // We need to use lazy evaluation for recursive content support
225
+ let FlexChildElementSchema: z.ZodType<any>;
226
+ let FlexElementContentSchema: z.ZodType<any>;
227
+
228
+ // Define content schema with lazy reference to child elements (for recursion)
229
+ FlexElementContentSchema = z.lazy(() => z.object({
230
+ type: z.literal('content').describe("Groups child elements with alignment control. Supports nested content."),
231
+ elements: fuzzyArray(FlexChildElementSchema).describe("Child elements. Can include nested content containers."),
232
+ direction: z.enum(['horizontal', 'vertical']).optional().describe("Layout direction. Default: 'vertical'."),
233
+ valign: VAlignSchema.optional().describe("Vertical alignment. Default: 'center'."),
234
+ halign: HAlignSchema.optional().describe("Horizontal alignment. Default: 'left'."),
235
+ gap: SpacingTokenSchema.optional().describe("Gap between children. Default: 'md'."),
236
+ padding: SpacingTokenSchema.optional().describe("Internal padding. Default: 'lg'."),
237
+ size: FlexSizeSchema.optional().describe("Size in parent flex container."),
238
+ class: fuzzyString.optional().describe("Custom CSS class."),
239
+ style: z.record(z.string()).optional().describe("Custom CSS style object."),
240
+ }));
241
+
242
+ // Define child element schema with reference to content (for nesting)
243
+ FlexChildElementSchema = z.discriminatedUnion('type', [
213
244
  FlexElementTitleSchema,
214
245
  FlexElementTextSchema,
215
246
  FlexElementBulletsSchema,
@@ -218,23 +249,17 @@ const FlexChildElementSchema = z.discriminatedUnion('type', [
218
249
  FlexElementTimelineSchema,
219
250
  FlexElementStepperSchema,
220
251
  FlexElementSpacerSchema,
252
+ FlexElementHtmlSchema,
253
+ FlexElementImageSchema,
254
+ FlexElementContentSchema,
221
255
  ]);
222
256
 
223
- // Content container schema
224
- const FlexElementContentSchema = z.object({
225
- type: z.literal('content').describe("Groups child elements vertically with alignment control."),
226
- elements: fuzzyArray(FlexChildElementSchema).describe("Child elements."),
227
- valign: VAlignSchema.optional().describe("Vertical alignment. Default: 'center'."),
228
- halign: HAlignSchema.optional().describe("Horizontal alignment. Default: 'left'."),
229
- gap: SpacingTokenSchema.optional().describe("Gap between children. Default: 'md'."),
230
- padding: SpacingTokenSchema.optional().describe("Internal padding. Default: 'lg'."),
231
- size: FlexSizeSchema.optional().describe("Size in parent flex container."),
232
- });
233
-
234
257
  // Top-level flex element (with size property)
258
+ // Note: FlexElementContentSchema already includes size in its definition, so we don't need to extend it
235
259
  const FlexElementSchema = z.discriminatedUnion('type', [
236
260
  FlexElementImageSchema.extend({ size: FlexSizeSchema.optional() }),
237
- FlexElementContentSchema,
261
+ FlexElementContentSchema, // Already includes size, no need to extend
262
+ FlexElementHtmlSchema.extend({ size: FlexSizeSchema.optional() }),
238
263
  FlexElementTitleSchema.extend({ size: FlexSizeSchema.optional() }),
239
264
  FlexElementTextSchema.extend({ size: FlexSizeSchema.optional() }),
240
265
  FlexElementBulletsSchema.extend({ size: FlexSizeSchema.optional() }),