lumina-slides 9.0.4 → 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.
@@ -0,0 +1,65 @@
1
+ <template>
2
+ <div
3
+ v-html="html"
4
+ :class="customClass"
5
+ :style="computedStyle"
6
+ ></div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import { computed } from 'vue';
11
+
12
+ const props = defineProps<{
13
+ html: string;
14
+ class?: string;
15
+ style?: Record<string, string>;
16
+ }>();
17
+
18
+ const customClass = computed(() => props.class || '');
19
+
20
+ const computedStyle = computed(() => {
21
+ return {
22
+ width: '100%',
23
+ ...props.style,
24
+ };
25
+ });
26
+ </script>
27
+
28
+ <style scoped>
29
+ /* Ensure HTML content respects container boundaries */
30
+ :deep(*) {
31
+ max-width: 100%;
32
+ }
33
+
34
+ /* Style common HTML elements to match Lumina theme */
35
+ :deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
36
+ color: var(--lumina-color-text);
37
+ font-family: var(--lumina-font-heading);
38
+ margin-top: var(--lumina-space-md);
39
+ margin-bottom: var(--lumina-space-sm);
40
+ }
41
+
42
+ :deep(p) {
43
+ color: var(--lumina-color-text);
44
+ line-height: var(--lumina-leading-relaxed);
45
+ margin-bottom: var(--lumina-space-md);
46
+ }
47
+
48
+ :deep(a) {
49
+ color: var(--lumina-color-primary);
50
+ text-decoration: none;
51
+ }
52
+
53
+ :deep(a:hover) {
54
+ text-decoration: underline;
55
+ }
56
+
57
+ :deep(ul), :deep(ol) {
58
+ margin-left: var(--lumina-space-lg);
59
+ margin-bottom: var(--lumina-space-md);
60
+ }
61
+
62
+ :deep(li) {
63
+ margin-bottom: var(--lumina-space-xs);
64
+ }
65
+ </style>
@@ -5,10 +5,10 @@
5
5
  :style="containerStyle">
6
6
  <img :src="src" :alt="alt || ''" :class="[
7
7
  'w-full h-full transition-opacity duration-700',
8
- fill !== false ? 'object-cover' : 'object-contain',
8
+ objectFitClass,
9
9
  roundedClass,
10
10
  isLoaded ? 'opacity-100' : 'opacity-0'
11
- ]" @load="onLoad" @error="onError" />
11
+ ]" :style="imageStyle" @load="onLoad" @error="onError" />
12
12
  </component>
13
13
  </template>
14
14
 
@@ -19,12 +19,15 @@ const props = defineProps<{
19
19
  src: string;
20
20
  alt?: string;
21
21
  fill?: boolean;
22
+ fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
23
+ position?: string;
22
24
  rounded?: string;
23
25
  containerClass?: any;
24
26
  containerStyle?: any;
25
27
  href?: string;
26
28
  target?: '_blank' | '_self';
27
29
  class?: any;
30
+ style?: Record<string, string>;
28
31
  }>();
29
32
 
30
33
  const isLoaded = ref(false);
@@ -39,6 +42,30 @@ const onError = (e: any) => {
39
42
  e.target.src = 'https://placehold.co/800x600/1a1a1a/666?text=Image+Not+Found';
40
43
  };
41
44
 
45
+ const objectFitClass = computed(() => {
46
+ if (props.fit) {
47
+ const fitMap: Record<string, string> = {
48
+ 'cover': 'object-cover',
49
+ 'contain': 'object-contain',
50
+ 'fill': 'object-fill',
51
+ 'none': 'object-none',
52
+ 'scale-down': 'object-scale-down',
53
+ };
54
+ return fitMap[props.fit] || 'object-cover';
55
+ }
56
+ return props.fill !== false ? 'object-cover' : 'object-contain';
57
+ });
58
+
59
+ const imageStyle = computed(() => {
60
+ const style: Record<string, string> = {
61
+ ...props.style,
62
+ };
63
+ if (props.position) {
64
+ style.objectPosition = props.position;
65
+ }
66
+ return style;
67
+ });
68
+
42
69
  const roundedClass = computed(() => {
43
70
  if (props.fill !== false && !props.rounded) return '';
44
71
  const map: Record<string, string> = {
@@ -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">
@@ -2143,8 +2143,9 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2143
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>
2144
2144
  </thead>
2145
2145
  <tbody class="divide-y divide-white/5">
2146
- <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>
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?, 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>
2148
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>
2149
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>
2150
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>
@@ -2161,8 +2162,49 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2161
2162
  </div>
2162
2163
 
2163
2164
  <div class="my-8">
2164
- <h2 class="text-xl font-bold text-white mb-4">Content container (valign, halign, gap, padding)</h2>
2165
- <p>Defaults: valign <code>center</code>, halign <code>left</code>, gap <code>md</code>, padding <code>lg</code>.</p>
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>
2185
+ </div>
2186
+
2187
+ <div class="my-8">
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>
2166
2208
  </div>
2167
2209
 
2168
2210
  <div class="my-8">
@@ -2777,10 +2819,13 @@ engine.data.set(<span class="text-green-400">"user"</span>, <span class="text-gr
2777
2819
 
2778
2820
  <script setup lang="ts">
2779
2821
  import { ref, watch, onUnmounted, onMounted, nextTick } from 'vue';
2822
+ import TurndownService from 'turndown';
2780
2823
  import { Lumina } from '../../core/Lumina';
2781
2824
  import { parsePartialJson } from '../../utils/streaming';
2782
2825
  import LivePreview from './LivePreview.vue';
2783
2826
 
2827
+ const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
2828
+
2784
2829
  const activeSection = ref('intro');
2785
2830
 
2786
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'];
@@ -2802,12 +2847,74 @@ function goToStreamingDemo() {
2802
2847
  nextTick(() => document.getElementById('streaming-demo-container')?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
2803
2848
  }
2804
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
+
2805
2909
  onMounted(() => {
2806
2910
  if (typeof window === 'undefined') return;
2807
2911
  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' }));
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);
2811
2918
  });
2812
2919
 
2813
2920
  // --- DEMO LOGIC ---
@@ -2974,13 +3081,31 @@ const EXAMPLES = {
2974
3081
  "type": "flex",
2975
3082
  "sizing": "container",
2976
3083
  "direction": "horizontal",
3084
+ "gap": "lg",
2977
3085
  "elements": [
2978
- { "type": "image", "src": "./brains.png", "size": "half", "fill": true },
2979
- { "type": "content", "size": "half", "valign": "center", "padding": "xl", "gap": "md", "elements": [
2980
- { "type": "title", "text": "Visual Story", "size": "2xl" },
2981
- { "type": "bullets", "items": ["Image fills height", "Content centered", "No coordinates"] },
2982
- { "type": "button", "label": "Learn More", "variant": "primary" }
2983
- ]}
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
+ }
2984
3109
  ]
2985
3110
  }` ,
2986
3111
  video: `{
@@ -3138,15 +3263,22 @@ const navigation = [
3138
3263
  }
3139
3264
 
3140
3265
  .doc-content h1 {
3141
- @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;
3142
3267
  }
3143
3268
 
3144
3269
  .doc-content h2 {
3145
- @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;
3146
3271
  }
3147
3272
 
3148
3273
  .doc-content h3 {
3149
- @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;
3150
3282
  }
3151
3283
 
3152
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
 
@@ -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() }),
package/src/core/types.ts CHANGED
@@ -160,7 +160,9 @@ export interface FlexElementImage {
160
160
  /** Fill entire container edge-to-edge. Default: true */
161
161
  fill?: boolean;
162
162
  /** Object-fit mode when fill is true. Default: 'cover' */
163
- fit?: 'cover' | 'contain';
163
+ fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down';
164
+ /** Object-position for image placement. Default: 'center' */
165
+ position?: string;
164
166
  /** Border radius. Default: 'none' when fill, 'lg' otherwise */
165
167
  rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
166
168
  /** Link URL when image is clicked. */
@@ -169,6 +171,8 @@ export interface FlexElementImage {
169
171
  target?: '_blank' | '_self';
170
172
  /** Custom CSS class */
171
173
  class?: string;
174
+ /** Custom CSS style object */
175
+ style?: Record<string, string>;
172
176
  }
173
177
 
174
178
  /**
@@ -248,13 +252,30 @@ export interface FlexElementSpacer {
248
252
  }
249
253
 
250
254
  /**
251
- * Content container - Groups child elements vertically with alignment control.
255
+ * HTML element - Raw HTML content.
256
+ */
257
+ export interface FlexElementHtml {
258
+ type: 'html';
259
+ /** Optional id for element control (engine.element(id)). */
260
+ id?: string;
261
+ /** Raw HTML string to render */
262
+ html: string;
263
+ /** Custom CSS class */
264
+ class?: string;
265
+ /** Custom CSS style object */
266
+ style?: Record<string, string>;
267
+ }
268
+
269
+ /**
270
+ * Content container - Groups child elements with alignment control.
252
271
  */
253
272
  export interface FlexElementContent {
254
273
  type: 'content';
255
274
  /** Optional id for element control (engine.element(id)). */
256
275
  id?: string;
257
276
  elements: FlexChildElement[];
277
+ /** Layout direction. Default: 'vertical' */
278
+ direction?: 'horizontal' | 'vertical';
258
279
  /** Vertical alignment of content. Default: 'center' */
259
280
  valign?: VAlign;
260
281
  /** Horizontal alignment of content. Default: 'left' */
@@ -263,10 +284,15 @@ export interface FlexElementContent {
263
284
  gap?: SpacingToken;
264
285
  /** Internal padding. Default: 'lg' */
265
286
  padding?: SpacingToken;
287
+ /** Custom CSS class */
288
+ class?: string;
289
+ /** Custom CSS style object */
290
+ style?: Record<string, string>;
266
291
  }
267
292
 
268
293
  /**
269
294
  * Child elements that can appear inside a content container.
295
+ * Supports nested content containers for complex layouts.
270
296
  */
271
297
  export type FlexChildElement =
272
298
  | FlexElementTitle
@@ -276,7 +302,10 @@ export type FlexChildElement =
276
302
  | FlexElementButton
277
303
  | FlexElementTimeline
278
304
  | FlexElementStepper
279
- | FlexElementSpacer;
305
+ | FlexElementSpacer
306
+ | FlexElementHtml
307
+ | FlexElementImage
308
+ | FlexElementContent;
280
309
 
281
310
  /**
282
311
  * Top-level flex elements that can have size.
@@ -285,6 +314,7 @@ export type FlexElement =
285
314
  | (FlexElementImage & { size?: FlexSize })
286
315
  | (FlexElementVideo & { size?: FlexSize })
287
316
  | (FlexElementContent & { size?: FlexSize })
317
+ | (FlexElementHtml & { size?: FlexSize })
288
318
  | (FlexElementTitle & { size?: FlexSize })
289
319
  | (FlexElementText & { size?: FlexSize })
290
320
  | (FlexElementBullets & { size?: FlexSize })