lumina-slides 8.9.4 → 9.0.0

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.
Files changed (119) hide show
  1. package/LUMINA_LLM_EXAMPLES.json +234 -0
  2. package/README.md +18 -18
  3. package/dist/lumina-slides.js +13207 -12659
  4. package/dist/lumina-slides.umd.cjs +215 -215
  5. package/dist/style.css +1 -1
  6. package/package.json +5 -4
  7. package/src/App.vue +16 -0
  8. package/src/animation/index.ts +11 -0
  9. package/src/animation/registry.ts +126 -0
  10. package/src/animation/stagger.ts +95 -0
  11. package/src/animation/types.ts +53 -0
  12. package/src/components/LandingPage.vue +229 -0
  13. package/src/components/LuminaDeck.vue +224 -0
  14. package/src/components/LuminaSpeakerNotes.vue +701 -0
  15. package/src/components/base/BaseSlide.vue +122 -0
  16. package/src/components/base/LuminaElement.vue +67 -0
  17. package/src/components/base/VideoPlayer.vue +204 -0
  18. package/src/components/layouts/LayoutAuto.vue +71 -0
  19. package/src/components/layouts/LayoutChart.vue +287 -0
  20. package/src/components/layouts/LayoutCustom.vue +92 -0
  21. package/src/components/layouts/LayoutDiagram.vue +253 -0
  22. package/src/components/layouts/LayoutFeatures.vue +121 -0
  23. package/src/components/layouts/LayoutFlex.vue +172 -0
  24. package/src/components/layouts/LayoutFree.vue +62 -0
  25. package/src/components/layouts/LayoutHalf.vue +127 -0
  26. package/src/components/layouts/LayoutStatement.vue +74 -0
  27. package/src/components/layouts/LayoutSteps.vue +106 -0
  28. package/src/components/layouts/LayoutTimeline.vue +104 -0
  29. package/src/components/layouts/LayoutVideo.vue +41 -0
  30. package/src/components/parts/FlexBullets.vue +45 -0
  31. package/src/components/parts/FlexButton.vue +132 -0
  32. package/src/components/parts/FlexImage.vue +54 -0
  33. package/src/components/parts/FlexOrdered.vue +44 -0
  34. package/src/components/parts/FlexSpacer.vue +13 -0
  35. package/src/components/parts/FlexStepper.vue +59 -0
  36. package/src/components/parts/FlexText.vue +29 -0
  37. package/src/components/parts/FlexTimeline.vue +67 -0
  38. package/src/components/parts/FlexTitle.vue +39 -0
  39. package/src/components/parts/LuminaBackground.vue +100 -0
  40. package/src/components/site/LivePreview.vue +101 -0
  41. package/src/components/site/SiteApi.vue +301 -0
  42. package/src/components/site/SiteDashboard.vue +604 -0
  43. package/src/components/site/SiteDocs.vue +3267 -0
  44. package/src/components/site/SiteExamples.vue +65 -0
  45. package/src/components/site/SiteFooter.vue +6 -0
  46. package/src/components/site/SiteHome.vue +362 -0
  47. package/src/components/site/SiteNavBar.vue +122 -0
  48. package/src/components/site/SitePlayground.vue +389 -0
  49. package/src/components/site/SitePromptBuilder.vue +266 -0
  50. package/src/components/site/SiteUserMenu.vue +90 -0
  51. package/src/components/studio/ActionEditor.vue +108 -0
  52. package/src/components/studio/ArrayEditor.vue +124 -0
  53. package/src/components/studio/CollapsibleSection.vue +33 -0
  54. package/src/components/studio/ColorField.vue +22 -0
  55. package/src/components/studio/EditorCanvas.vue +326 -0
  56. package/src/components/studio/EditorLayoutFeatures.vue +18 -0
  57. package/src/components/studio/EditorLayoutFixed.vue +46 -0
  58. package/src/components/studio/EditorLayoutFlex.vue +133 -0
  59. package/src/components/studio/EditorLayoutHalf.vue +18 -0
  60. package/src/components/studio/EditorLayoutStatement.vue +18 -0
  61. package/src/components/studio/EditorLayoutSteps.vue +18 -0
  62. package/src/components/studio/EditorLayoutTimeline.vue +18 -0
  63. package/src/components/studio/EditorNode.vue +89 -0
  64. package/src/components/studio/FieldEditor.vue +133 -0
  65. package/src/components/studio/IconPicker.vue +109 -0
  66. package/src/components/studio/LayerItem.vue +117 -0
  67. package/src/components/studio/LuminaStudio.vue +30 -0
  68. package/src/components/studio/SaveSuccessModal.vue +138 -0
  69. package/src/components/studio/SlideNavigator.vue +373 -0
  70. package/src/components/studio/SliderField.vue +44 -0
  71. package/src/components/studio/StudioInspector.vue +595 -0
  72. package/src/components/studio/StudioJsonEditor.vue +191 -0
  73. package/src/components/studio/StudioLayers.vue +145 -0
  74. package/src/components/studio/StudioSettings.vue +514 -0
  75. package/src/components/studio/StudioSidebar.vue +29 -0
  76. package/src/components/studio/StudioToolbar.vue +222 -0
  77. package/src/components/studio/fieldLabels.ts +224 -0
  78. package/src/components/studio/inspectors/DiagramEdgeEditor.vue +77 -0
  79. package/src/components/studio/inspectors/DiagramNodeEditor.vue +117 -0
  80. package/src/components/studio/nodes/StudioDiagramNode.vue +138 -0
  81. package/src/composables/useAuth.ts +87 -0
  82. package/src/composables/useEditor.ts +224 -0
  83. package/src/composables/useElementState.ts +81 -0
  84. package/src/composables/useFlexLayout.ts +122 -0
  85. package/src/composables/useKeyboard.ts +45 -0
  86. package/src/composables/useLumina.ts +32 -0
  87. package/src/composables/useStudio.ts +87 -0
  88. package/src/composables/useSwipeNav.ts +53 -0
  89. package/src/composables/useTransition.ts +373 -0
  90. package/src/core/Lumina.ts +819 -0
  91. package/src/core/animationConfig.ts +251 -0
  92. package/src/core/compression.ts +34 -0
  93. package/src/core/elementController.ts +170 -0
  94. package/src/core/elementId.ts +27 -0
  95. package/src/core/elementResolver.ts +207 -0
  96. package/src/core/events.ts +53 -0
  97. package/src/core/fonts.ts +100 -0
  98. package/src/core/presets.ts +231 -0
  99. package/src/core/prompts.ts +272 -0
  100. package/src/core/schema.ts +478 -0
  101. package/src/core/speaker-channel.ts +250 -0
  102. package/src/core/store.ts +461 -0
  103. package/src/core/theme.ts +666 -0
  104. package/src/core/types.ts +1611 -0
  105. package/src/directives/vStudio.ts +45 -0
  106. package/src/index.ts +175 -0
  107. package/src/main.ts +17 -0
  108. package/src/router/index.ts +92 -0
  109. package/src/style/main.css +462 -0
  110. package/src/utils/deep.ts +127 -0
  111. package/src/utils/firebase.ts +184 -0
  112. package/src/utils/streaming.ts +134 -0
  113. package/src/views/DashboardView.vue +32 -0
  114. package/src/views/DeckView.vue +289 -0
  115. package/src/views/HomeView.vue +17 -0
  116. package/src/views/SiteLayout.vue +21 -0
  117. package/src/views/StudioView.vue +61 -0
  118. package/src/vite-env.d.ts +6 -0
  119. package/IMPLEMENTATION.md +0 -418
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <BaseSlide :data="data" :slide-index="slideIndex">
3
+ <div :class="[
4
+ 'features-root w-full max-w-full flex flex-col justify-center overflow-x-hidden',
5
+ data.sizing === 'container' ? 'min-h-full' : 'min-h-screen'
6
+ ]">
7
+ <!-- Header -->
8
+ <LuminaElement :id="headerId" :class="[data.class || '', 'text-center lg:text-left max-w-6xl min-w-0']"
9
+ :style="{ marginBottom: 'var(--lumina-space-2xl)' }">
10
+ <h2 class="font-bold break-words" :style="{
11
+ fontFamily: 'var(--lumina-font-heading)',
12
+ fontSize: data.sizing === 'container' ? 'var(--lumina-text-3xl)' : 'var(--lumina-text-5xl)',
13
+ marginBottom: 'var(--lumina-space-md)',
14
+ letterSpacing: 'var(--lumina-tracking-tighter)',
15
+ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))',
16
+ textShadow: '0 2px 10px rgba(0,0,0,0.1)'
17
+ }">
18
+ {{ data.title }}
19
+ </h2>
20
+ <p v-if="data.description" class="break-words" :style="{
21
+ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))',
22
+ opacity: 0.9,
23
+ fontSize: data.sizing === 'container' ? 'var(--lumina-text-sm)' : 'var(--lumina-text-xl)'
24
+ }">
25
+ {{ data.description }}
26
+ </p>
27
+ </LuminaElement>
28
+
29
+ <!-- Grid -->
30
+ <div :class="['features-grid', data.sizing !== 'container' ? 'features-grid--multi' : '']">
31
+ <LuminaElement v-for="(feature, i) in data.features" :key="i" :id="featureId(i)"
32
+ :class="['glass-panel group min-w-0', feature.class || '']" :style="{
33
+ borderRadius: 'var(--lumina-card-radius)',
34
+ padding: data.sizing === 'container' ? 'var(--lumina-space-lg)' : 'var(--lumina-card-padding)',
35
+ borderTop: '1px solid var(--lumina-color-border-subtle)',
36
+ transition: 'all var(--lumina-transition-duration) var(--lumina-transition-easing)'
37
+ }">
38
+ <!-- Icon -->
39
+ <div class="flex items-center justify-center group-hover:scale-110 shrink-0" :style="{
40
+ color: 'white',
41
+ background: 'linear-gradient(135deg, var(--lumina-color-primary) 0%, var(--lumina-color-secondary) 100%)',
42
+ width: data.sizing === 'container' ? 'var(--lumina-space-xl)' : 'var(--lumina-space-2xl)',
43
+ height: data.sizing === 'container' ? 'var(--lumina-space-xl)' : 'var(--lumina-space-2xl)',
44
+ borderRadius: 'var(--lumina-radius-xl)',
45
+ marginBottom: data.sizing === 'container' ? 'var(--lumina-space-md)' : 'var(--lumina-space-lg)',
46
+ fontSize: 'var(--lumina-text-xl)',
47
+ boxShadow: '0 4px 12px rgba(var(--lumina-color-primary-rgb), 0.3)',
48
+ transition: 'all var(--lumina-transition-duration) var(--lumina-transition-easing)'
49
+ }">
50
+ <i
51
+ :class="['ph-thin', (feature.icon || 'star').startsWith('ph-') ? feature.icon : `ph-${feature.icon || 'star'}`]"></i>
52
+ </div>
53
+ <!-- Title -->
54
+ <h3 class="font-bold break-words" :style="{
55
+ color: 'var(--lumina-surface-text, var(--lumina-color-text))',
56
+ fontSize: data.sizing === 'container' ? 'var(--lumina-text-lg)' : 'var(--lumina-text-2xl)',
57
+ marginBottom: 'var(--lumina-space-sm)'
58
+ }">
59
+ {{ feature.title }}
60
+ </h3>
61
+ <!-- Description (support both desc and description) -->
62
+ <p class="break-words" :style="{
63
+ color: 'var(--lumina-color-text-secondary)',
64
+ fontSize: data.sizing === 'container' ? 'var(--lumina-text-sm)' : 'var(--lumina-text-base)',
65
+ lineHeight: 'var(--lumina-leading-relaxed)',
66
+ opacity: 0.8
67
+ }">
68
+ {{ feature.desc || feature.description || '' }}
69
+ </p>
70
+ </LuminaElement>
71
+ </div>
72
+ </div>
73
+ </BaseSlide>
74
+ </template>
75
+
76
+ <script setup lang="ts">
77
+ import { computed, inject } from 'vue';
78
+ import BaseSlide from '../base/BaseSlide.vue';
79
+ import { resolveId } from '../../core/elementResolver';
80
+ import { StoreKey } from '../../core/store';
81
+ import type { SlideFeatures } from '../../core/types';
82
+
83
+ const props = defineProps<{
84
+ data: SlideFeatures;
85
+ slideIndex?: number;
86
+ }>();
87
+
88
+ const store = inject(StoreKey);
89
+ const slideIndex = computed(() => props.slideIndex ?? store?.state.currentIndex ?? 0);
90
+
91
+ const headerId = computed(() => resolveId(props.data, slideIndex.value, ['header']));
92
+ const featureId = (i: number) => resolveId(props.data, slideIndex.value, ['features', i]);
93
+ </script>
94
+
95
+ <style scoped>
96
+ .features-root {
97
+ padding: var(--lumina-space-xl) var(--lumina-space-3xl);
98
+ }
99
+ @media (max-width: 1023px) {
100
+ .features-root { padding-left: var(--lumina-space-xl); padding-right: var(--lumina-space-xl); }
101
+ }
102
+ @media (max-width: 639px) {
103
+ .features-root { padding: var(--lumina-space-lg) var(--lumina-space-md); }
104
+ }
105
+
106
+ .features-grid {
107
+ display: grid;
108
+ grid-template-columns: 1fr;
109
+ gap: var(--lumina-space-md);
110
+ }
111
+ .features-grid--multi {
112
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
113
+ gap: var(--lumina-space-lg);
114
+ }
115
+ @media (max-width: 639px) {
116
+ .features-grid--multi {
117
+ grid-template-columns: 1fr;
118
+ gap: var(--lumina-space-md);
119
+ }
120
+ }
121
+ </style>
@@ -0,0 +1,172 @@
1
+ <template>
2
+ <BaseSlide :data="data" :slide-index="slideIndex">
3
+ <div class="flex-layout w-full h-screen flex flex-col overflow-y-auto lg:overflow-hidden"
4
+ :style="containerStyle">
5
+ <div :class="['flex-1 flex w-full min-h-full lg:h-full', directionClass]"
6
+ :style="mainFlexStyle">
7
+ <template v-for="(element, i) in data.elements" :key="i">
8
+ <!-- Image Element -->
9
+ <LuminaElement v-if="element.type === 'image'" :id="elemId(i)" :class="['flex-item', getSizeClass(element.size)]" :style="getElementStyle(element)">
10
+ <FlexImage :src="element.src" :alt="element.alt"
11
+ :fill="element.fill" :rounded="element.rounded"
12
+ container-class="w-full h-full min-h-0" :container-style="{}" :class="element.class" />
13
+ </LuminaElement>
14
+
15
+ <!-- Video Element -->
16
+ <LuminaElement v-else-if="element.type === 'video'" :id="elemId(i)"
17
+ :class="['flex-item', getSizeClass(element.size), element.class]"
18
+ :style="getElementStyle(element)">
19
+ <VideoPlayer :class="getRoundedClass(element.rounded, element.fill)" :src="element.src"
20
+ :poster="element.poster" :autoplay="element.autoplay ?? false" :loop="element.loop ?? false"
21
+ :muted="element.muted ?? false" :controls="element.controls ?? true"
22
+ :object-fit="element.fill !== false ? 'cover' : 'contain'" />
23
+ </LuminaElement>
24
+
25
+ <!-- Content Container -->
26
+ <LuminaElement v-else-if="element.type === 'content'" :id="elemId(i)"
27
+ :class="['flex-item flex flex-col h-full', getSizeClass(element.size)]"
28
+ :style="getContentStyle(element)">
29
+ <template v-for="(child, j) in element.elements" :key="j">
30
+ <LuminaElement :id="childId(i, j)">
31
+ <component :is="getChildComponent(child.type)" v-bind="child" @action="handleAction" />
32
+ </LuminaElement>
33
+ </template>
34
+ </LuminaElement>
35
+
36
+ <!-- Top-level elements (title, text, bullets, etc.) -->
37
+ <LuminaElement v-else :id="elemId(i)" :class="['flex-item', getSizeClass(element.size)]"
38
+ :style="{ ...getTopLevelStyle(), width: data.direction === 'vertical' ? '100%' : 'auto' }">
39
+ <component :is="getChildComponent(element.type)" v-bind="element" @action="handleAction" />
40
+ </LuminaElement>
41
+ </template>
42
+ </div>
43
+ </div>
44
+ </BaseSlide>
45
+ </template>
46
+
47
+ <script setup lang="ts">
48
+ import { computed, inject } from 'vue';
49
+ import BaseSlide from '../base/BaseSlide.vue';
50
+ import VideoPlayer from '../base/VideoPlayer.vue';
51
+ import { resolveId } from '../../core/elementResolver';
52
+ import { StoreKey } from '../../core/store';
53
+ import type { SlideFlex, FlexElement, FlexSize, FlexElementContent } from '../../core/types';
54
+ import FlexImage from '../parts/FlexImage.vue';
55
+ import FlexTitle from '../parts/FlexTitle.vue';
56
+ import FlexText from '../parts/FlexText.vue';
57
+ import FlexButton from '../parts/FlexButton.vue';
58
+ import FlexBullets from '../parts/FlexBullets.vue';
59
+ import FlexOrdered from '../parts/FlexOrdered.vue';
60
+ import FlexTimeline from '../parts/FlexTimeline.vue';
61
+ import FlexStepper from '../parts/FlexStepper.vue';
62
+ import FlexSpacer from '../parts/FlexSpacer.vue';
63
+ import { bus } from '../../core/events';
64
+ import {
65
+ spacingVarMap,
66
+ getSizeClass,
67
+ getFlexItemStyle,
68
+ getFlexContainerStyle,
69
+ getTopLevelStyle,
70
+ getFlexSlideMainStyle,
71
+ getRoundedClass
72
+ } from '../../composables/useFlexLayout';
73
+
74
+ const props = defineProps<{
75
+ data: SlideFlex;
76
+ slideIndex?: number;
77
+ }>();
78
+
79
+ const emit = defineEmits(['action']);
80
+ const store = inject(StoreKey);
81
+ const slideIndex = computed(() => props.slideIndex ?? store?.state.currentIndex ?? 0);
82
+ const elemId = (i: number) => resolveId(props.data, slideIndex.value, ['elements', i]);
83
+ const childId = (i: number, j: number) => resolveId(props.data, slideIndex.value, ['elements', i, 'elements', j]);
84
+
85
+ const containerStyle = computed(() => ({
86
+ padding: spacingVarMap[props.data.padding || 'none'],
87
+ }));
88
+
89
+ const directionClass = computed(() =>
90
+ props.data.direction === 'vertical' ? 'flex-col' : 'flex-col lg:flex-row'
91
+ );
92
+
93
+ const mainFlexStyle = computed(() => ({
94
+ gap: spacingVarMap[props.data.gap || 'none'],
95
+ ...getFlexSlideMainStyle(props.data.direction, props.data.halign, props.data.valign),
96
+ }));
97
+
98
+ // Wrapper for shared logic requiring vertical flag
99
+ const getElementStyle = (element: FlexElement & { size?: FlexSize }) => {
100
+ return getFlexItemStyle(element, props.data.direction === 'vertical');
101
+ };
102
+
103
+ const getContentStyle = (element: FlexElementContent & { size?: FlexSize }) => {
104
+ const isVertical = props.data.direction === 'vertical';
105
+ return {
106
+ ...getFlexItemStyle(element, isVertical),
107
+ ...getFlexContainerStyle(element),
108
+ // Force full height/width for alignment space
109
+ height: isVertical ? 'auto' : '100%',
110
+ width: isVertical ? '100%' : 'auto',
111
+ minHeight: isVertical ? 'auto' : '100%'
112
+ };
113
+ };
114
+
115
+ const handleAction = (payload: any) => {
116
+ emit('action', payload);
117
+ bus.emit('action', payload);
118
+ };
119
+
120
+ // ============================================================================
121
+ // CHILD ELEMENT COMPONENTS - All using CSS variables
122
+ // ============================================================================
123
+
124
+
125
+ // Child components extracted to ../parts/
126
+
127
+ const getChildComponent = (type: string) => {
128
+ const components: Record<string, any> = {
129
+ 'title': FlexTitle,
130
+ 'text': FlexText,
131
+ 'image': FlexImage,
132
+ 'video': VideoPlayer,
133
+ 'bullets': FlexBullets,
134
+ 'ordered': FlexOrdered,
135
+ 'button': FlexButton,
136
+ 'timeline': FlexTimeline,
137
+ 'stepper': FlexStepper,
138
+ 'spacer': FlexSpacer,
139
+ };
140
+ return components[type] || 'div';
141
+ };
142
+ </script>
143
+
144
+ <style scoped>
145
+ .flex-layout {
146
+ min-height: 100vh;
147
+ }
148
+
149
+ @media (max-width: 1023px) {
150
+ .flex-item {
151
+ flex: 0 0 auto !important;
152
+ width: 100% !important;
153
+ max-width: 100% !important;
154
+ max-height: none !important;
155
+ min-height: auto !important;
156
+ height: auto !important;
157
+ }
158
+
159
+ /* Images in stacked flex should have a reasonable height */
160
+ .flex-item:has(img),
161
+ .flex-item img {
162
+ height: 40vh !important;
163
+ min-height: 300px !important;
164
+ }
165
+
166
+ .flex-layout {
167
+ overflow-y: auto !important;
168
+ height: auto !important;
169
+ min-height: 100vh;
170
+ }
171
+ }
172
+ </style>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <BaseSlide :data="data" :slide-index="slideIndex">
3
+ <div class="layout-free-stage absolute inset-0 min-h-[100vh] overflow-hidden">
4
+ <template v-for="(el, i) in (data.elements || [])" :key="i">
5
+ <LuminaElement
6
+ :id="elementId(i)"
7
+ :tag="el.type === 'image' ? 'div' : 'div'"
8
+ :style="baseStyle(el)"
9
+ class="layout-free-element">
10
+ <template v-if="el.type === 'text'">{{ el.text }}</template>
11
+ <img v-else-if="el.type === 'image' && el.src" :src="el.src" :alt="''" class="max-w-full max-h-full object-contain" :style="imgStyle(el)" />
12
+ <span v-else-if="el.type === 'box'">&nbsp;</span>
13
+ </LuminaElement>
14
+ </template>
15
+ </div>
16
+ </BaseSlide>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { computed, inject } from 'vue';
21
+ import BaseSlide from '../base/BaseSlide.vue';
22
+ import LuminaElement from '../base/LuminaElement.vue';
23
+ import { resolveId } from '../../core/elementResolver';
24
+ import { StoreKey } from '../../core/store';
25
+ import type { SlideFree, FreeElement } from '../../core/types';
26
+
27
+ const props = defineProps<{
28
+ data: SlideFree;
29
+ slideIndex?: number;
30
+ }>();
31
+
32
+ const store = inject(StoreKey);
33
+ const slideIndex = computed(() => props.slideIndex ?? store?.state.currentIndex ?? 0);
34
+
35
+ function elementId(i: number) {
36
+ return resolveId(props.data, slideIndex.value, ['elements', i]);
37
+ }
38
+
39
+ function baseStyle(el: FreeElement): Record<string, string | number> {
40
+ const s: Record<string, string | number> = {
41
+ position: 'absolute',
42
+ left: 0,
43
+ top: 0,
44
+ fontFamily: 'var(--lumina-font-heading, inherit)',
45
+ color: 'var(--lumina-color-text-safe, #fff)',
46
+ };
47
+ if (el.width != null) s.width = typeof el.width === 'number' ? `${el.width}px` : el.width;
48
+ if (el.height != null) s.height = typeof el.height === 'number' ? `${el.height}px` : el.height;
49
+ if (el.fontSize != null) s.fontSize = typeof el.fontSize === 'number' ? `${el.fontSize}px` : el.fontSize;
50
+ if (el.color) s.color = el.color;
51
+ if (el.fontWeight) s.fontWeight = el.fontWeight;
52
+ if (el.type === 'box' && el.backgroundColor) s.backgroundColor = el.backgroundColor;
53
+ return s;
54
+ }
55
+
56
+ function imgStyle(el: FreeElement): Record<string, string | number> {
57
+ const s: Record<string, string | number> = { display: 'block' };
58
+ if (el.width != null) s.width = typeof el.width === 'number' ? `${el.width}px` : el.width;
59
+ if (el.height != null) s.height = typeof el.height === 'number' ? `${el.height}px` : el.height;
60
+ return s;
61
+ }
62
+ </script>
@@ -0,0 +1,127 @@
1
+ <template>
2
+ <BaseSlide :data="data" :slide-index="si">
3
+ <div
4
+ :class="['w-full flex flex-col lg:block relative', data.sizing === 'container' ? 'min-h-full' : 'min-h-screen']">
5
+ <!-- Image Section -->
6
+ <LuminaElement :id="mediaId"
7
+ :class="['relative h-[40vh] lg:h-full lg:w-1/2 overflow-hidden z-0', data.sizing === 'container' ? 'lg:absolute lg:top-0' : 'lg:fixed lg:top-0', data.imageSide === 'right' ? 'lg:right-0 order-1' : 'lg:left-0 order-1']">
8
+ <div class="absolute inset-0 z-10" :style="{ backgroundColor: 'var(--lumina-color-overlay, rgba(0,0,0,0.2))' }">
9
+ </div>
10
+
11
+ <!-- Video Handling -->
12
+ <div v-if="data.video" class="w-full h-full relative">
13
+ <VideoPlayer class="w-full h-full" :src="data.video.src" :poster="data.video.poster"
14
+ :autoplay="data.video.autoplay ?? true" :loop="data.video.loop ?? true" :muted="data.video.muted ?? true"
15
+ :controls="data.video.controls" object-fit="cover" />
16
+ </div>
17
+
18
+ <!-- Image Handling -->
19
+ <img v-else-if="data.image" :src="data.image"
20
+ :class="['w-full h-full object-cover scale-110 origin-center', data.imageClass || '', isLoaded ? 'opacity-100' : 'opacity-0']"
21
+ :style="{ transition: 'opacity var(--lumina-transition-duration) var(--lumina-transition-easing)' }"
22
+ alt="Slide Image" @load="isLoaded = true"
23
+ @error="(e: any) => { isLoaded = true; e.target.src = 'https://placehold.co/1920x1080/1a1a1a/666?text=Image+Not+Found'; }">
24
+ </LuminaElement>
25
+
26
+ <!-- Content Section - Uses background color for proper theme integration -->
27
+ <div
28
+ :class="['relative z-10 w-full lg:w-1/2 min-h-[60vh] flex flex-col justify-center order-2', data.imageSide === 'right' ? 'lg:mr-[50%]' : 'lg:ml-[50%]', data.sizing === 'container' ? 'lg:min-h-full' : 'lg:min-h-screen']"
29
+ :style="contentPanelStyle">
30
+ <div :class="data.class || ''" :style="{ maxWidth: '36rem' }">
31
+ <!-- Tag -->
32
+ <LuminaElement v-if="data.tag" :id="tagId" tag="span" class="font-bold uppercase block" :style="{
33
+ color: 'var(--lumina-color-primary)',
34
+ letterSpacing: 'var(--lumina-tracking-widest)',
35
+ fontSize: 'var(--lumina-text-xs)',
36
+ marginBottom: 'var(--lumina-space-lg)'
37
+ }">
38
+ {{ data.tag }}
39
+ </LuminaElement>
40
+
41
+ <!-- Title -->
42
+ <LuminaElement :id="titleId" tag="h1" class="font-bold" :style="{
43
+ fontFamily: 'var(--lumina-font-heading)',
44
+ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))',
45
+ fontSize: 'var(--lumina-text-6xl)',
46
+ letterSpacing: 'var(--lumina-tracking-tighter)',
47
+ lineHeight: 'var(--lumina-leading-tight)',
48
+ marginBottom: 'var(--lumina-space-xl)',
49
+ textShadow: '0 2px 10px rgba(0,0,0,0.1)'
50
+ }">
51
+ {{ data.title }}
52
+ </LuminaElement>
53
+
54
+ <!-- Paragraphs -->
55
+ <LuminaElement :id="paragraphsId" :style="{
56
+ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))',
57
+ opacity: 0.9,
58
+ fontSize: 'var(--lumina-text-lg)',
59
+ lineHeight: 'var(--lumina-leading-relaxed)',
60
+ marginBottom: 'var(--lumina-space-xl)',
61
+ display: 'flex',
62
+ flexDirection: 'column',
63
+ gap: 'var(--lumina-space-lg)'
64
+ }">
65
+ <p v-for="(p, i) in data.paragraphs" :key="i">{{ p }}</p>
66
+ </LuminaElement>
67
+
68
+ <!-- CTA -->
69
+ <LuminaElement v-if="data.cta" :id="ctaId" :style="{
70
+ paddingTop: 'var(--lumina-space-lg)',
71
+ borderTop: '1px solid var(--lumina-color-border)'
72
+ }">
73
+ <button @click="$emit('action', { type: 'cta', label: data.cta })" class="font-bold flex items-center"
74
+ :style="{
75
+ backgroundColor: 'var(--lumina-color-primary)',
76
+ color: 'var(--lumina-color-background)',
77
+ borderRadius: 'var(--lumina-button-radius)',
78
+ padding: 'var(--lumina-button-padding)',
79
+ fontWeight: 'var(--lumina-button-font-weight)',
80
+ gap: 'var(--lumina-space-sm)',
81
+ transition: 'all var(--lumina-transition-duration) var(--lumina-transition-easing)'
82
+ }">
83
+ {{ data.cta }} <i class="ph-thin ph-arrow-right"></i>
84
+ </button>
85
+ </LuminaElement>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </BaseSlide>
90
+ </template>
91
+
92
+ <script setup lang="ts">
93
+ import { ref, computed, inject } from 'vue';
94
+ import BaseSlide from '../base/BaseSlide.vue';
95
+ import VideoPlayer from '../base/VideoPlayer.vue';
96
+ import { resolveId } from '../../core/elementResolver';
97
+ import { StoreKey } from '../../core/store';
98
+ import type { SlideHalf } from '../../core/types';
99
+
100
+ const props = defineProps<{
101
+ data: SlideHalf;
102
+ slideIndex?: number;
103
+ }>();
104
+
105
+ const store = inject(StoreKey);
106
+ const isLoaded = ref(false);
107
+ const si = computed(() => props.slideIndex ?? store?.state.currentIndex ?? 0);
108
+ const mediaId = computed(() => resolveId(props.data, si.value, ['media']));
109
+ const tagId = computed(() => resolveId(props.data, si.value, ['tag']));
110
+ const titleId = computed(() => resolveId(props.data, si.value, ['title']));
111
+ const paragraphsId = computed(() => resolveId(props.data, si.value, ['paragraphs']));
112
+ const ctaId = computed(() => resolveId(props.data, si.value, ['cta']));
113
+
114
+ // Content panel style - uses background color for proper theme integration
115
+ // This ensures text is always readable against the background
116
+ const contentPanelStyle = computed(() => ({
117
+ backgroundColor: 'var(--lumina-color-background)',
118
+ paddingTop: 'var(--lumina-space-xl)',
119
+ paddingLeft: 'var(--lumina-space-3xl)',
120
+ paddingRight: 'var(--lumina-space-3xl)',
121
+ paddingBottom: 'calc(var(--lumina-space-xl) + var(--lumina-safe-area-bottom))'
122
+ }));
123
+
124
+ defineEmits<{
125
+ (e: 'action', payload: any): void
126
+ }>();
127
+ </script>
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <BaseSlide :data="data" :slide-index="slideIndex">
3
+ <div
4
+ :class="['flex flex-col justify-center items-center text-center relative', data.sizing === 'container' ? 'h-full' : 'min-h-screen']"
5
+ :style="{ paddingTop: 'var(--lumina-space-xl)', paddingLeft: 'var(--lumina-space-xl)', paddingRight: 'var(--lumina-space-xl)', paddingBottom: 'calc(var(--lumina-space-xl) + var(--lumina-safe-area-bottom))' }">
6
+ <div :class="[data.class || '', 'max-w-5xl z-10']">
7
+ <!-- Tag -->
8
+ <LuminaElement v-if="data.tag" :id="tagId" tag="span" class="inline-block rounded-full font-medium uppercase" :style="{
9
+ color: 'var(--lumina-color-primary)',
10
+ border: '1px solid var(--lumina-color-border)',
11
+ backgroundColor: 'rgba(var(--lumina-color-text-rgb), 0.05)',
12
+ padding: 'var(--lumina-tag-padding)',
13
+ fontSize: 'var(--lumina-tag-font-size)',
14
+ letterSpacing: 'var(--lumina-tracking-widest)',
15
+ marginBottom: 'var(--lumina-space-xl)'
16
+ }">
17
+ {{ data.tag }}
18
+ </LuminaElement>
19
+
20
+ <!-- Title -->
21
+ <LuminaElement :id="titleId" tag="h1" :style="titleStyle">
22
+ {{ data.title }}
23
+ </LuminaElement>
24
+
25
+ <!-- Subtitle -->
26
+ <LuminaElement v-if="data.subtitle" :id="subtitleId" tag="p" class="max-w-3xl mx-auto" :style="{
27
+ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))',
28
+ opacity: 0.8,
29
+ fontWeight: 'normal',
30
+ fontSize: 'clamp(var(--lumina-text-xl), 3vw, var(--lumina-text-3xl))',
31
+ lineHeight: 'var(--lumina-leading-normal)'
32
+ }">
33
+ {{ data.subtitle }}
34
+ </LuminaElement>
35
+ </div>
36
+ </div>
37
+ </BaseSlide>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import { computed, inject } from 'vue';
42
+ import BaseSlide from '../base/BaseSlide.vue';
43
+ import { resolveId } from '../../core/elementResolver';
44
+ import { StoreKey } from '../../core/store';
45
+ import type { SlideStatement } from '../../core/types';
46
+
47
+ const props = defineProps<{
48
+ data: SlideStatement;
49
+ slideIndex?: number;
50
+ }>();
51
+
52
+ const store = inject(StoreKey);
53
+ const slideIndex = computed(() => props.slideIndex ?? store?.state.currentIndex ?? 0);
54
+
55
+ const tagId = computed(() => resolveId(props.data, slideIndex.value, ['tag']));
56
+ const titleId = computed(() => resolveId(props.data, slideIndex.value, ['title']));
57
+ const subtitleId = computed(() => resolveId(props.data, slideIndex.value, ['subtitle']));
58
+
59
+ const titleStyle = computed(() => ({
60
+ fontSize: props.data.sizing === 'container'
61
+ ? 'var(--lumina-text-5xl)'
62
+ : 'clamp(var(--lumina-text-4xl), 10vw, var(--lumina-text-7xl))',
63
+ fontFamily: 'var(--lumina-font-heading)',
64
+ fontWeight: 'var(--lumina-font-weight-bold)',
65
+ lineHeight: 'var(--lumina-leading-tight)',
66
+ letterSpacing: 'var(--lumina-tracking-tighter)',
67
+ marginBottom: 'var(--lumina-space-xl)',
68
+ backgroundImage: 'linear-gradient(135deg, var(--lumina-color-text-safe, var(--lumina-color-text)) 30%, #525252 100%)',
69
+ WebkitBackgroundClip: 'text',
70
+ WebkitTextFillColor: 'transparent',
71
+ backgroundClip: 'text',
72
+ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))', // Fallback
73
+ }));
74
+ </script>
@@ -0,0 +1,106 @@
1
+ <template>
2
+ <BaseSlide :data="data" :slide-index="slideIndex">
3
+ <div :class="['w-full flex flex-col justify-center', data.sizing === 'container' ? 'min-h-full py-12 lg:py-16' : 'min-h-screen']"
4
+ :style="{ padding: 'var(--lumina-space-xl) var(--lumina-space-3xl)' }">
5
+ <!-- Header -->
6
+ <LuminaElement :id="headerId" :class="['text-center max-w-4xl mx-auto', data.class || '']"
7
+ :style="{ marginBottom: 'var(--lumina-space-2xl)' }">
8
+ <h2 class="font-bold" :style="{
9
+ fontFamily: 'var(--lumina-font-heading)',
10
+ fontSize: 'var(--lumina-text-5xl)',
11
+ marginBottom: 'var(--lumina-space-md)',
12
+ letterSpacing: 'var(--lumina-tracking-tighter)',
13
+ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))'
14
+ }">
15
+ {{ data.title }}
16
+ </h2>
17
+ <p v-if="data.subtitle" :style="{
18
+ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))',
19
+ opacity: 0.9,
20
+ fontSize: 'var(--lumina-text-xl)'
21
+ }">
22
+ {{ data.subtitle }}
23
+ </p>
24
+ </LuminaElement>
25
+
26
+ <!-- Steps Grid -->
27
+ <div :style="{
28
+ display: 'grid',
29
+ gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
30
+ gap: 'var(--lumina-space-xl)',
31
+ maxWidth: '72rem',
32
+ margin: '0 auto',
33
+ width: '100%'
34
+ }">
35
+ <LuminaElement v-for="(step, i) in data.steps" :key="i" :id="stepId(i)"
36
+ :class="['glass-panel group relative overflow-hidden', step.class || '']" :style="{
37
+ borderRadius: 'var(--lumina-card-radius)',
38
+ padding: 'var(--lumina-card-padding)'
39
+ }">
40
+ <!-- Step Number -->
41
+ <div :style="{
42
+ display: 'flex',
43
+ alignItems: 'center',
44
+ gap: 'var(--lumina-space-md)',
45
+ marginBottom: 'var(--lumina-space-md)'
46
+ }">
47
+ <div class="flex items-center justify-center font-bold" :style="{
48
+ width: 'var(--lumina-step-badge-size, 2.5rem)',
49
+ height: 'var(--lumina-step-badge-size, 2.5rem)',
50
+ fontSize: 'var(--lumina-step-font-size, 1rem)',
51
+ background: 'linear-gradient(135deg, var(--lumina-color-primary), var(--lumina-color-secondary))',
52
+ color: 'white',
53
+ borderRadius: 'var(--lumina-radius-xl)',
54
+ boxShadow: '0 4px 12px rgba(var(--lumina-color-primary-rgb), 0.4)'
55
+ }">
56
+ {{ step.step }}
57
+ </div>
58
+ <h3 class="font-bold" :style="{
59
+ fontSize: 'var(--lumina-text-xl)',
60
+ color: 'var(--lumina-surface-text, var(--lumina-color-text))'
61
+ }">
62
+ {{ step.title }}
63
+ </h3>
64
+ </div>
65
+
66
+ <!-- Description -->
67
+ <p v-if="step.description" :style="{
68
+ color: 'var(--lumina-color-text-secondary)',
69
+ lineHeight: 'var(--lumina-leading-relaxed)',
70
+ fontSize: 'var(--lumina-text-base)',
71
+ opacity: 0.8
72
+ }">
73
+ {{ step.description }}
74
+ </p>
75
+
76
+ <!-- Icon (optional) -->
77
+ <div v-if="step.icon" class="absolute top-4 right-4" :style="{
78
+ color: 'var(--lumina-color-primary)',
79
+ opacity: 0.1,
80
+ fontSize: 'var(--lumina-text-4xl)'
81
+ }">
82
+ <i :class="['ph-thin', step.icon.startsWith('ph-') ? step.icon : `ph-${step.icon}`]"></i>
83
+ </div>
84
+ </LuminaElement>
85
+ </div>
86
+ </div>
87
+ </BaseSlide>
88
+ </template>
89
+
90
+ <script setup lang="ts">
91
+ import { computed, inject } from 'vue';
92
+ import BaseSlide from '../base/BaseSlide.vue';
93
+ import { resolveId } from '../../core/elementResolver';
94
+ import { StoreKey } from '../../core/store';
95
+ import type { SlideSteps } from '../../core/types';
96
+
97
+ const props = defineProps<{
98
+ data: SlideSteps;
99
+ slideIndex?: number;
100
+ }>();
101
+
102
+ const store = inject(StoreKey);
103
+ const slideIndex = computed(() => props.slideIndex ?? store?.state.currentIndex ?? 0);
104
+ const headerId = computed(() => resolveId(props.data, slideIndex.value, ['header']));
105
+ const stepId = (i: number) => resolveId(props.data, slideIndex.value, ['steps', i]);
106
+ </script>