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,326 @@
1
+ <template>
2
+ <div ref="canvasContainer"
3
+ class="flex-1 bg-[#050505] overflow-hidden flex items-center justify-center relative transition-cursor duration-150"
4
+ :class="{
5
+ 'cursor-grab': !isPanning && isSpacePressed,
6
+ 'cursor-grabbing': isPanning,
7
+ 'cursor-default': !isPanning && !isSpacePressed
8
+ }" @wheel="handleWheel" @mousedown="handleMouseDown" @mousemove="handleMouseMove" @mouseup="handleMouseUp"
9
+ @mouseleave="handleMouseUp">
10
+
11
+ <!-- Zoom Controls -->
12
+ <div
13
+ class="absolute top-4 right-4 z-50 flex items-center gap-2 bg-black/60 backdrop-blur-md rounded-lg px-3 py-2 border border-white/10">
14
+ <button @click="zoomOut"
15
+ class="w-6 h-6 flex items-center justify-center text-white/70 hover:text-white transition">
16
+ <i class="ph-thin ph-minus text-xs"></i>
17
+ </button>
18
+ <span class="text-white/70 text-xs font-mono w-12 text-center">{{ Math.round(scale * 100) }}%</span>
19
+ <button @click="zoomIn"
20
+ class="w-6 h-6 flex items-center justify-center text-white/70 hover:text-white transition">
21
+ <i class="ph-thin ph-plus text-xs"></i>
22
+ </button>
23
+ <button @click="resetZoom"
24
+ class="w-6 h-6 flex items-center justify-center text-white/70 hover:text-white transition ml-2"
25
+ title="Reset Zoom">
26
+ <i class="ph-thin ph-arrows-out-simple text-xs"></i>
27
+ </button>
28
+ </div>
29
+
30
+ <!-- Pan Indicator -->
31
+ <div v-if="isPanning" class="absolute top-4 left-4 z-50 bg-blue-500/80 text-white text-xs px-2 py-1 rounded">
32
+ Panning
33
+ </div>
34
+
35
+ <!-- Zoomable/Pannable Canvas Wrapper -->
36
+ <div class="canvas-transform-wrapper" :style="transformStyle">
37
+ <!-- Slide Container - Matches LuminaDeck exactly -->
38
+ <!-- Slide Container - Fixed Frame with Background -->
39
+ <div class="w-[1280px] h-[720px] shadow-2xl relative border border-[#333] overflow-hidden"
40
+ :style="slideContainerStyle" @click.stop="selectSlide">
41
+
42
+ <!-- DYNAMIC BACKGROUND (Fixed layer) -->
43
+ <div class="absolute inset-0 z-0 pointer-events-none">
44
+ <LuminaBackground :orb-color="slide?.meta?.orbColor" :orb-pos="orbPosition" />
45
+ </div>
46
+
47
+ <!-- SCROLLABLE CONTENT (Overlay layer) -->
48
+ <div class="absolute inset-0 z-10 overflow-y-auto overflow-x-hidden scroll-smooth">
49
+ <div class="relative min-h-full w-full">
50
+ <template v-if="slide">
51
+ <!-- Free Layouts -->
52
+ <EditorLayoutFlex v-if="slide.type === 'flex'" :data="(slide as any)"
53
+ :slide-index="index" />
54
+
55
+ <!-- Fixed Layouts with specialized editors -->
56
+ <EditorLayoutSteps v-else-if="slide.type === 'steps'" :data="(slide as any)"
57
+ :slide-index="index" />
58
+ <EditorLayoutTimeline v-else-if="slide.type === 'timeline'" :data="(slide as any)"
59
+ :slide-index="index" />
60
+ <EditorLayoutFeatures v-else-if="slide.type === 'features'" :data="(slide as any)"
61
+ :slide-index="index" />
62
+ <EditorLayoutHalf v-else-if="slide.type === 'half'" :data="(slide as any)"
63
+ :slide-index="index" />
64
+ <EditorLayoutStatement v-else-if="slide.type === 'statement'" :data="(slide as any)"
65
+ :slide-index="index" />
66
+
67
+ <!-- Fallback for other layouts -->
68
+ <EditorLayoutFixed v-else :data="(slide as any)" :slide-index="index" />
69
+ </template>
70
+
71
+ <div v-else class="flex w-full h-[720px] items-center justify-center text-white/30">
72
+ No slide selected
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </template>
81
+
82
+ <script setup lang="ts">
83
+ import { ref, computed, inject, watch, onMounted } from 'vue';
84
+ import { useLumina } from '../../composables/useLumina';
85
+ import { StoreKey } from '../../core/store';
86
+ import { ThemeManager } from '../../core/theme';
87
+
88
+ // Layout Editors
89
+ import EditorLayoutFlex from './EditorLayoutFlex.vue';
90
+ import EditorLayoutFixed from './EditorLayoutFixed.vue';
91
+ import EditorLayoutSteps from './EditorLayoutSteps.vue';
92
+ import EditorLayoutTimeline from './EditorLayoutTimeline.vue';
93
+ import EditorLayoutFeatures from './EditorLayoutFeatures.vue';
94
+ import EditorLayoutHalf from './EditorLayoutHalf.vue';
95
+ import EditorLayoutStatement from './EditorLayoutStatement.vue';
96
+
97
+ const { slide, index } = useLumina();
98
+ const store = inject(StoreKey);
99
+
100
+ // Zoom/Pan State
101
+ const scale = ref(0.6);
102
+ const panX = ref(0);
103
+ const panY = ref(0);
104
+ const isPanning = ref(false);
105
+ const lastMousePos = ref({ x: 0, y: 0 });
106
+
107
+ const transformStyle = computed(() => ({
108
+ transform: `translate(${panX.value}px, ${panY.value}px) scale(${scale.value})`,
109
+ transformOrigin: 'center center',
110
+ transition: isPanning.value ? 'none' : 'transform 0.15s ease-out'
111
+ }));
112
+
113
+ // Dynamic orb position per slide
114
+ const currentOrbPosition = ref({ top: '-20%', left: '-10%' });
115
+
116
+ // Slide container style (background from theme)
117
+ const slideContainerStyle = computed(() => ({
118
+ backgroundColor: 'var(--lumina-color-background, #030303)',
119
+ color: 'var(--lumina-color-text, #ffffff)',
120
+ fontFamily: 'var(--lumina-font-body, Inter, system-ui, sans-serif)',
121
+ }));
122
+
123
+ // Ambient orb style (matches LuminaDeck exactly)
124
+ import LuminaBackground from '../parts/LuminaBackground.vue';
125
+
126
+ const orbPosition = computed(() => ({
127
+ top: slide.value?.meta?.orbPos?.top || currentOrbPosition.value.top,
128
+ left: slide.value?.meta?.orbPos?.left || currentOrbPosition.value.left,
129
+ }));
130
+
131
+
132
+ // Apply theme when deck or theme changes
133
+ const applyTheme = () => {
134
+ const deck = store?.state.deck;
135
+ if (!deck) return;
136
+
137
+ // Get theme from deck meta or deck root
138
+ const themeName = deck.meta?.theme || deck.theme || 'default';
139
+
140
+ // Construct full config from meta to ensure overrides are applied immediately
141
+ const meta = deck.meta || {};
142
+ const overrides = {
143
+ colors: meta.colors,
144
+ typography: meta.typography,
145
+ spacing: meta.spacing,
146
+ borderRadius: meta.borderRadius,
147
+ effects: meta.effects,
148
+ components: meta.components
149
+ };
150
+
151
+
152
+ // Inject theme globally with overrides
153
+ ThemeManager.inject(themeName, overrides);
154
+ };
155
+
156
+ // Watch for deck/theme changes
157
+ watch(() => store?.state.deck?.meta?.theme, applyTheme, { immediate: true });
158
+ watch(() => store?.state.deck?.theme, applyTheme);
159
+ watch(() => store?.state.deck, applyTheme, { immediate: true, deep: false });
160
+
161
+ // Randomize orb position on slide change
162
+ watch(index, (newIndex, oldIndex) => {
163
+ if (newIndex === 0) {
164
+ currentOrbPosition.value = { top: '-20%', left: '-10%' };
165
+ } else if (oldIndex !== undefined) {
166
+ const randomRange = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1) + min);
167
+ currentOrbPosition.value = {
168
+ top: `${randomRange(-25, 65)}%`,
169
+ left: `${randomRange(-25, 65)}%`
170
+ };
171
+ }
172
+ }, { immediate: true });
173
+
174
+ onMounted(() => {
175
+ applyTheme();
176
+ });
177
+
178
+ const zoomIn = () => { scale.value = Math.min(scale.value + 0.1, 2); };
179
+ const zoomOut = () => { scale.value = Math.max(scale.value - 0.1, 0.2); };
180
+ const resetZoom = () => { scale.value = 0.6; panX.value = 0; panY.value = 0; };
181
+
182
+ const handleWheel = (e: WheelEvent) => {
183
+ // Zoom on Ctrl+Wheel or Meta+Wheel
184
+ if (e.ctrlKey || e.metaKey) {
185
+ e.preventDefault();
186
+ const delta = e.deltaY > 0 ? -0.05 : 0.05;
187
+ scale.value = Math.max(0.2, Math.min(2, scale.value + delta));
188
+ return;
189
+ }
190
+
191
+ // Smart Scrolling Logic
192
+ // Check if we are scrolling inside a scrollable container (the slide)
193
+ const target = e.target as HTMLElement;
194
+ const scrollContainer = target.closest('.overflow-y-auto') as HTMLElement;
195
+
196
+ if (scrollContainer) {
197
+ const { scrollTop, scrollHeight, clientHeight } = scrollContainer;
198
+ const isScrollable = scrollHeight > clientHeight;
199
+
200
+ if (isScrollable) {
201
+ // Check if we can scroll in the requested direction
202
+ const isAtTop = scrollTop <= 0;
203
+ const isAtBottom = Math.abs(scrollTop + clientHeight - scrollHeight) < 1;
204
+ const scrollingUp = e.deltaY < 0;
205
+ const scrollingDown = e.deltaY > 0;
206
+
207
+ if ((scrollingUp && !isAtTop) || (scrollingDown && !isAtBottom)) {
208
+ // Allow default scrolling behavior (don't prevent default)
209
+ // We stop propagation so we don't trigger other handlers if any
210
+ e.stopPropagation();
211
+ return;
212
+ }
213
+ }
214
+ }
215
+
216
+ // Fallback to Panning
217
+ // If not scrollable, or at the edge, we pan the canvas
218
+ e.preventDefault();
219
+
220
+ // Support Shift + Wheel for horizontal scrolling
221
+ if (e.shiftKey && e.deltaX === 0) {
222
+ panX.value -= e.deltaY;
223
+ } else {
224
+ panX.value -= e.deltaX;
225
+ panY.value -= e.deltaY;
226
+ }
227
+ };
228
+
229
+ const isSpacePressed = ref(false);
230
+ const canvasContainer = ref<HTMLElement | null>(null);
231
+
232
+ // Global Key Listeners for Space
233
+ const handleKeyDown = (e: KeyboardEvent) => {
234
+ if (e.code === 'Space' && !e.repeat && !(e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)) {
235
+ isSpacePressed.value = true;
236
+ }
237
+ };
238
+
239
+ const handleKeyUp = (e: KeyboardEvent) => {
240
+ if (e.code === 'Space') {
241
+ isSpacePressed.value = false;
242
+ // If we were panning with space, stop panning
243
+ if (isPanning.value && !isMouseDown.value) {
244
+ isPanning.value = false;
245
+ }
246
+ }
247
+ };
248
+
249
+ onMounted(() => {
250
+ applyTheme();
251
+ window.addEventListener('keydown', handleKeyDown);
252
+ window.addEventListener('keyup', handleKeyUp);
253
+ });
254
+
255
+ // Clean up listeners
256
+ import { onUnmounted } from 'vue';
257
+ onUnmounted(() => {
258
+ window.removeEventListener('keydown', handleKeyDown);
259
+ window.removeEventListener('keyup', handleKeyUp);
260
+ });
261
+
262
+
263
+ const isMouseDown = ref(false);
264
+
265
+ const handleMouseDown = (e: MouseEvent) => {
266
+ const isMiddleClick = e.button === 1;
267
+ const isLeftClick = e.button === 0;
268
+
269
+ // Check if clicking on background (not the slide)
270
+ // We assume the slide container or its children are the "content"
271
+ // The wrapper div is the target if we click the "background"
272
+ const clickedElement = e.target as HTMLElement;
273
+ const isClickingBackground = clickedElement.classList.contains('canvas-transform-wrapper') ||
274
+ clickedElement === canvasContainer.value;
275
+
276
+
277
+ if (isMiddleClick || (isLeftClick && (isSpacePressed.value || e.shiftKey || isClickingBackground))) {
278
+ isPanning.value = true;
279
+ isMouseDown.value = true;
280
+ lastMousePos.value = { x: e.clientX, y: e.clientY };
281
+ e.preventDefault(); // Prevent text selection
282
+ }
283
+ };
284
+
285
+ const handleMouseMove = (e: MouseEvent) => {
286
+ if (!isPanning.value) return;
287
+
288
+ // Calculate delta and apply zoom factor to make panning natural at different scales
289
+ // Actually, simple delta is usually fine for panning, but sometimes dividing by scale helps tracking.
290
+ // For simple translation, 1:1 mouse movement usually feels best.
291
+ panX.value += e.clientX - lastMousePos.value.x;
292
+ panY.value += e.clientY - lastMousePos.value.y;
293
+ lastMousePos.value = { x: e.clientX, y: e.clientY };
294
+ };
295
+
296
+ const handleMouseUp = () => {
297
+ isPanning.value = false;
298
+ isMouseDown.value = false;
299
+ };
300
+
301
+ import { useEditor } from '../../composables/useEditor';
302
+ const editorComposable = useEditor();
303
+
304
+ const selectSlide = () => {
305
+ // Only select if not panning (prevent selection after dragging)
306
+ if (!isPanning.value) {
307
+ editorComposable.select(`slides.${index.value}`);
308
+ }
309
+ };
310
+ </script>
311
+
312
+ <style scoped>
313
+ .canvas-transform-wrapper {
314
+ will-change: transform;
315
+ }
316
+
317
+ /* Ambient Orb - matches LuminaDeck styles */
318
+ .ambient-orb {
319
+ position: absolute;
320
+ border-radius: 50%;
321
+ filter: blur(var(--lumina-orb-blur, 120px));
322
+ opacity: var(--lumina-orb-opacity-effective, 0.20);
323
+ pointer-events: none;
324
+ will-change: transform, opacity;
325
+ }
326
+ </style>
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <div class="w-full h-full">
3
+ <EditorNode :path="`slides.${slideIndex}`" :label="data.type" class="w-full h-full">
4
+ <LayoutFeatures :data="data" />
5
+ </EditorNode>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import type { SlideFeatures } from '../../core/types';
11
+ import EditorNode from './EditorNode.vue';
12
+ import LayoutFeatures from '../layouts/LayoutFeatures.vue';
13
+
14
+ defineProps<{
15
+ data: SlideFeatures;
16
+ slideIndex: number;
17
+ }>();
18
+ </script>
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <div class="w-full h-full">
3
+ <!-- Render the layout with selectable wrapper -->
4
+ <EditorNode :path="`slides.${slideIndex}`" :label="data.type" class="w-full h-full">
5
+ <component :is="getLayoutComponent(data.type)" :data="data" />
6
+ </EditorNode>
7
+ </div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import type { BaseSlideData } from '../../core/types';
12
+ import EditorNode from './EditorNode.vue';
13
+
14
+ // Import all layout components for rendering preview
15
+ import LayoutHalf from '../layouts/LayoutHalf.vue';
16
+ import LayoutStatement from '../layouts/LayoutStatement.vue';
17
+ import LayoutFeatures from '../layouts/LayoutFeatures.vue';
18
+ import LayoutTimeline from '../layouts/LayoutTimeline.vue';
19
+ import LayoutSteps from '../layouts/LayoutSteps.vue';
20
+ import LayoutChart from '../layouts/LayoutChart.vue';
21
+ import LayoutVideo from '../layouts/LayoutVideo.vue';
22
+ import LayoutAuto from '../layouts/LayoutAuto.vue';
23
+ import LayoutCustom from '../layouts/LayoutCustom.vue';
24
+ import LayoutDiagram from '../layouts/LayoutDiagram.vue';
25
+
26
+ const props = defineProps<{
27
+ data: BaseSlideData;
28
+ slideIndex: number;
29
+ }>();
30
+
31
+ const getLayoutComponent = (type: string) => {
32
+ const components: Record<string, any> = {
33
+ 'half': LayoutHalf,
34
+ 'statement': LayoutStatement,
35
+ 'features': LayoutFeatures,
36
+ 'timeline': LayoutTimeline,
37
+ 'steps': LayoutSteps,
38
+ 'chart': LayoutChart,
39
+ 'video': LayoutVideo,
40
+ 'auto': LayoutAuto,
41
+ 'custom': LayoutCustom,
42
+ 'diagram': LayoutDiagram,
43
+ };
44
+ return components[type] || 'div';
45
+ };
46
+ </script>
@@ -0,0 +1,133 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import draggable from 'vuedraggable';
4
+ import type { SlideFlex, FlexElement, FlexSize } from '../../core/types';
5
+ import EditorNode from './EditorNode.vue';
6
+ import { useEditor } from '../../composables/useEditor';
7
+
8
+ // Parts
9
+ import FlexImage from '../parts/FlexImage.vue';
10
+ import FlexTitle from '../parts/FlexTitle.vue';
11
+ import FlexText from '../parts/FlexText.vue';
12
+ import FlexButton from '../parts/FlexButton.vue';
13
+ import FlexBullets from '../parts/FlexBullets.vue';
14
+ import FlexOrdered from '../parts/FlexOrdered.vue';
15
+ import FlexTimeline from '../parts/FlexTimeline.vue';
16
+ import FlexStepper from '../parts/FlexStepper.vue';
17
+ import FlexSpacer from '../parts/FlexSpacer.vue';
18
+
19
+ import {
20
+ spacingVarMap,
21
+ getSizeClass,
22
+ getFlexItemStyle,
23
+ getFlexContainerStyle,
24
+ getTopLevelStyle,
25
+ getFlexSlideMainStyle,
26
+ } from '../../composables/useFlexLayout';
27
+
28
+ const props = defineProps<{
29
+ data: SlideFlex,
30
+ slideIndex: number
31
+ }>();
32
+
33
+ const editor = useEditor();
34
+
35
+ const elements = computed(() => props.data.elements || []);
36
+
37
+ const directionClass = computed(() =>
38
+ props.data.direction === 'vertical' ? 'flex-col' : 'flex-row'
39
+ );
40
+
41
+ // Added containerStyle for dynamic padding
42
+ const containerStyle = computed(() => ({
43
+ padding: spacingVarMap[props.data.padding || 'none'],
44
+ }));
45
+
46
+ const mainFlexStyle = computed(() => ({
47
+ gap: spacingVarMap[props.data.gap || 'none'],
48
+ ...getFlexSlideMainStyle(props.data.direction, props.data.halign, props.data.valign),
49
+ }));
50
+
51
+ const getElementStyle = (element: FlexElement & { size?: FlexSize }) => {
52
+ return getFlexItemStyle(element, props.data.direction === 'vertical');
53
+ };
54
+
55
+ const getContentStyle = getFlexContainerStyle; // Alias for template usage
56
+
57
+ const getChildComponent = (type: string) => {
58
+ const components: Record<string, any> = {
59
+ 'title': FlexTitle,
60
+ 'text': FlexText,
61
+ 'image': FlexImage,
62
+ 'bullets': FlexBullets,
63
+ 'ordered': FlexOrdered,
64
+ 'button': FlexButton,
65
+ 'timeline': FlexTimeline,
66
+ 'stepper': FlexStepper,
67
+ 'spacer': FlexSpacer,
68
+ };
69
+ return components[type] || 'div';
70
+ };
71
+
72
+ const handleDragChange = (event: any) => {
73
+ if (event.moved) {
74
+ const { oldIndex, newIndex } = event.moved;
75
+ editor.store.moveNode(`slides.${props.slideIndex}.elements`, oldIndex, newIndex);
76
+ editor.commit();
77
+ }
78
+ };
79
+
80
+ const handleContentDragChange = (event: any, parentIndex: number) => {
81
+ if (event.moved) {
82
+ const { oldIndex, newIndex } = event.moved;
83
+ editor.store.moveNode(`slides.${props.slideIndex}.elements.${parentIndex}.elements`, oldIndex, newIndex);
84
+ editor.commit();
85
+ }
86
+ };
87
+ </script>
88
+
89
+ <template>
90
+ <!-- Root container with dynamic padding -->
91
+ <div class="w-full h-full flex items-center justify-center" :style="containerStyle">
92
+ <!-- Match nesting: .flex-layout > .flex ... -->
93
+ <draggable :list="elements" item-key="dragKey" :group="{ name: 'elements' }" @change="handleDragChange"
94
+ :class="['flex w-full h-full max-w-7xl mx-auto', directionClass]" :style="mainFlexStyle">
95
+ <template #item="{ element, index }">
96
+ <EditorNode :path="`slides.${slideIndex}.elements.${index}`" :label="element.type" :resizable="true"
97
+ :class="[getSizeClass(element.size), 'relative']" :style="element.type === 'content' || element.type === 'image' || element.type === 'video' ?
98
+ { ...getElementStyle(element), height: '100%' } :
99
+ { ...getTopLevelStyle(), width: '100%' }">
100
+
101
+ <!-- 1. Content Container -->
102
+ <div v-if="element.type === 'content'" class="h-full w-full">
103
+ <draggable :list="element.elements" item-key="dragKey" :group="{ name: 'elements' }"
104
+ @change="handleContentDragChange($event, index)" class="w-full h-full flex flex-col"
105
+ :style="[getContentStyle(element), { minHeight: '50px' }]">
106
+ <template #item="{ element: child, index: childIndex }">
107
+ <EditorNode :path="`slides.${slideIndex}.elements.${index}.elements.${childIndex}`"
108
+ :label="child.type" style="width: 100%">
109
+ <component :is="getChildComponent(child.type)" v-bind="child" />
110
+ </EditorNode>
111
+ </template>
112
+ </draggable>
113
+ </div>
114
+
115
+ <!-- 2. Leaf Elements - Image -->
116
+ <FlexImage v-else-if="element.type === 'image'" :src="element.src" :alt="element.alt"
117
+ :fill="element.fill" :rounded="element.rounded" :container-class="'w-full h-full'" />
118
+
119
+ <!-- 3. Leaf Elements - Video -->
120
+ <div v-else-if="element.type === 'video'"
121
+ class="w-full h-full bg-gray-900 flex items-center justify-center text-white/20 rounded-lg">
122
+ <i class="ph-thin ph-play mr-2"></i> Video: {{ element.src?.split('/').pop() || 'Unknown' }}
123
+ </div>
124
+
125
+ <!-- 4. Top Level Generic Elements (Title, Text, etc.) -->
126
+ <!-- They are already wrapped in EditorNode. EditorNode applies styles to itself. -->
127
+ <component v-else :is="getChildComponent(element.type)" v-bind="element" />
128
+
129
+ </EditorNode>
130
+ </template>
131
+ </draggable>
132
+ </div>
133
+ </template>
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <div class="w-full h-full">
3
+ <EditorNode :path="`slides.${slideIndex}`" :label="data.type" class="w-full h-full">
4
+ <LayoutHalf :data="data" />
5
+ </EditorNode>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import type { SlideHalf } from '../../core/types';
11
+ import EditorNode from './EditorNode.vue';
12
+ import LayoutHalf from '../layouts/LayoutHalf.vue';
13
+
14
+ defineProps<{
15
+ data: SlideHalf;
16
+ slideIndex: number;
17
+ }>();
18
+ </script>
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <div class="w-full h-full">
3
+ <EditorNode :path="`slides.${slideIndex}`" :label="data.type" class="w-full h-full">
4
+ <LayoutStatement :data="data" />
5
+ </EditorNode>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import type { SlideStatement } from '../../core/types';
11
+ import EditorNode from './EditorNode.vue';
12
+ import LayoutStatement from '../layouts/LayoutStatement.vue';
13
+
14
+ defineProps<{
15
+ data: SlideStatement;
16
+ slideIndex: number;
17
+ }>();
18
+ </script>
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <div class="w-full h-full">
3
+ <EditorNode :path="`slides.${slideIndex}`" :label="data.type" class="w-full h-full">
4
+ <LayoutSteps :data="data" />
5
+ </EditorNode>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import type { SlideSteps } from '../../core/types';
11
+ import EditorNode from './EditorNode.vue';
12
+ import LayoutSteps from '../layouts/LayoutSteps.vue';
13
+
14
+ defineProps<{
15
+ data: SlideSteps;
16
+ slideIndex: number;
17
+ }>();
18
+ </script>
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <div class="w-full h-full">
3
+ <EditorNode :path="`slides.${slideIndex}`" :label="data.type" class="w-full h-full">
4
+ <LayoutTimeline :data="data" />
5
+ </EditorNode>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import type { SlideTimeline } from '../../core/types';
11
+ import EditorNode from './EditorNode.vue';
12
+ import LayoutTimeline from '../layouts/LayoutTimeline.vue';
13
+
14
+ defineProps<{
15
+ data: SlideTimeline;
16
+ slideIndex: number;
17
+ }>();
18
+ </script>