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.
- package/dist/lumina-slides.js +9878 -9731
- package/dist/lumina-slides.umd.cjs +202 -202
- package/dist/style.css +1 -1
- package/package.json +3 -1
- package/src/components/layouts/LayoutFlex.vue +47 -7
- package/src/components/parts/FlexHtml.vue +65 -0
- package/src/components/parts/FlexImage.vue +29 -2
- package/src/components/site/SiteDocs.vue +149 -17
- package/src/composables/useFlexLayout.ts +9 -3
- package/src/core/schema.ts +40 -15
- package/src/core/types.ts +33 -3
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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>
|
|
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 (
|
|
2809
|
-
|
|
2810
|
-
|
|
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
|
-
{
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
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: '
|
|
70
|
-
justifyContent:
|
|
71
|
-
|
|
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
|
|
package/src/core/schema.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
*
|
|
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 })
|