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,287 @@
1
+ <template>
2
+ <BaseSlide :data="data" :slide-index="slideIndex">
3
+ <div
4
+ :class="['w-full flex flex-col justify-center p-8 lg:p-16', data.sizing === 'container' ? 'min-h-full py-8' : 'min-h-screen']">
5
+ <!-- Header -->
6
+ <div v-if="data.title || data.subtitle" :class="['text-center mb-8', data.class || '']">
7
+ <LuminaElement v-if="data.title" :id="titleId" tag="h2" class="font-heading font-bold mb-3" :style="{
8
+ fontSize: 'var(--lumina-text-5xl)',
9
+ letterSpacing: 'var(--lumina-tracking-tighter)',
10
+ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))',
11
+ textShadow: '0 2px 10px rgba(0,0,0,0.1)'
12
+ }">
13
+ {{ data.title }}
14
+ </LuminaElement>
15
+ <LuminaElement v-if="data.subtitle" :id="subtitleId" tag="p" class="text-lg lg:text-xl"
16
+ :style="{ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))', opacity: 0.8 }">
17
+ {{ data.subtitle }}
18
+ </LuminaElement>
19
+ </div>
20
+
21
+ <!-- Error State -->
22
+ <div v-if="chartError" class="flex-1 flex items-center justify-center">
23
+ <div class="text-center p-8 bg-red-500/10 border border-red-500/30 rounded-xl max-w-lg">
24
+ <svg class="w-12 h-12 mx-auto mb-4 text-red-400" fill="none" stroke="currentColor"
25
+ viewBox="0 0 24 24">
26
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
27
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
28
+ </svg>
29
+ <p class="text-red-300 font-medium mb-2">Chart.js Required</p>
30
+ <code class="text-sm bg-black/30 px-3 py-1 rounded text-red-200">npm install chart.js</code>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- Loading State -->
35
+ <div v-else-if="chartLoading" class="flex-1 flex items-center justify-center">
36
+ <div class="animate-pulse text-white/40">Loading chart...</div>
37
+ </div>
38
+
39
+ <!-- Chart Container -->
40
+ <LuminaElement v-else :id="chartId"
41
+ :class="['flex-1 flex items-center justify-center max-w-5xl mx-auto w-full', data.chartClass || '']">
42
+ <div class="w-full h-full min-h-[300px] lg:min-h-[400px] relative">
43
+ <canvas ref="chartCanvas"></canvas>
44
+ </div>
45
+ </LuminaElement>
46
+ </div>
47
+ </BaseSlide>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { ref, onMounted, onUnmounted, watch, computed, inject } from 'vue';
52
+ import BaseSlide from '../base/BaseSlide.vue';
53
+ import { resolveId } from '../../core/elementResolver';
54
+ import { StoreKey } from '../../core/store';
55
+ import type { SlideChart, ChartDataset } from '../../core/types';
56
+
57
+ // Chart.js is loaded dynamically - it's an optional dependency
58
+ let ChartJS: typeof import('chart.js').Chart | null = null;
59
+ const chartError = ref<string | null>(null);
60
+ const chartLoading = ref(true);
61
+
62
+ const props = defineProps<{
63
+ data: SlideChart;
64
+ slideIndex?: number;
65
+ }>();
66
+
67
+ const store = inject(StoreKey);
68
+ const slideIndex = computed(() => props.slideIndex ?? store?.state.currentIndex ?? 0);
69
+ const titleId = computed(() => resolveId(props.data, slideIndex.value, ['title']));
70
+ const subtitleId = computed(() => resolveId(props.data, slideIndex.value, ['subtitle']));
71
+ const chartId = computed(() => resolveId(props.data, slideIndex.value, ['chart']));
72
+
73
+ const chartCanvas = ref<HTMLCanvasElement | null>(null);
74
+ let chartInstance: any = null;
75
+
76
+ // Try to load Chart.js dynamically
77
+ const loadChartJS = async () => {
78
+ try {
79
+ const chartModule = await import('chart.js');
80
+ ChartJS = chartModule.Chart;
81
+ ChartJS.register(...chartModule.registerables);
82
+ chartLoading.value = false;
83
+ return true;
84
+ } catch (e) {
85
+ chartError.value = 'Chart.js is required for chart slides. Install it with: npm install chart.js';
86
+ chartLoading.value = false;
87
+ console.warn('[Lumina] chart.js not found. Install with: npm install chart.js');
88
+ return false;
89
+ }
90
+ };
91
+
92
+ // Color token resolver
93
+ const resolveColor = (colorToken: string | undefined, index: number): string => {
94
+ if (!colorToken) {
95
+ // Default color palette
96
+ const defaultColors = [
97
+ 'rgba(59, 130, 246, 0.8)', // Blue
98
+ 'rgba(16, 185, 129, 0.8)', // Green
99
+ 'rgba(245, 158, 11, 0.8)', // Amber
100
+ 'rgba(239, 68, 68, 0.8)', // Red
101
+ 'rgba(139, 92, 246, 0.8)', // Purple
102
+ 'rgba(236, 72, 153, 0.8)', // Pink
103
+ ];
104
+ return defaultColors[index % defaultColors.length];
105
+ }
106
+
107
+ // Resolve Lumina color tokens with strict fallbacks
108
+ switch (colorToken) {
109
+ case 'c:p':
110
+ return getComputedStyle(document.documentElement).getPropertyValue('--lumina-colors-primary').trim() || '#3b82f6';
111
+ case 'c:s':
112
+ return getComputedStyle(document.documentElement).getPropertyValue('--lumina-colors-secondary').trim() || '#10b981';
113
+ case 'c:m':
114
+ return getComputedStyle(document.documentElement).getPropertyValue('--lumina-colors-muted').trim() || '#9ca3af';
115
+ default:
116
+ // If valid definition, return it
117
+ if (colorToken && (colorToken.startsWith('#') || colorToken.startsWith('rgb') || colorToken.startsWith('hsl'))) return colorToken;
118
+ // If known color name (red, blue), return it
119
+ if (colorToken && /^[a-z]+$/i.test(colorToken)) return colorToken;
120
+
121
+ // Fallback for empty/undefined/invalid tokens
122
+ return resolveColor(undefined, index);
123
+ }
124
+ };
125
+
126
+ // Generate smart palette derived from theme identity
127
+ const generateSegmentColors = (datasets: ChartDataset[], labelsCount: number): string[] => {
128
+ const colors: string[] = [];
129
+
130
+ // Get theme colors from CSS variables
131
+ const style = getComputedStyle(document.body);
132
+ const primary = style.getPropertyValue('--lumina-color-primary').trim() || '#3b82f6';
133
+ const secondary = style.getPropertyValue('--lumina-color-secondary').trim() || '#8b5cf6';
134
+ const accent = style.getPropertyValue('--lumina-color-accent').trim() || '#06b6d4';
135
+
136
+ // Create an intelligent palette based on brand identity
137
+ // We alternate between brand colors and variations
138
+ const basePalette = [
139
+ primary,
140
+ secondary,
141
+ accent,
142
+ // Generate variations (simulated by opacity or mixing - hard to do perfectly in JS without lib,
143
+ // but we can assume these are distinct enough for 3 categories)
144
+ ];
145
+
146
+ // If we need more colors, we loop or use fallbacks that match the vibe
147
+ const extendedPalette = [
148
+ ...basePalette,
149
+ '#f59e0b', // construction/warning (often useful in business/engine contexts)
150
+ '#10b981', // success
151
+ '#ef4444' // danger
152
+ ];
153
+
154
+ // For Pie/Doughnut charts, if we only have one dataset and one color,
155
+ // it results in a mono-colored chart which is often not desired.
156
+ // We force using the extended palette to differentiate segments.
157
+ for (let i = 0; i < labelsCount; i++) {
158
+ // If it's a multi-dataset chart (e.g. bar), we might want to respect the dataset color.
159
+ // But for single-dataset Pie charts, we ignore the single color to show variety.
160
+ if (datasets.length === 1 && datasets[0].color && labelsCount > 1) {
161
+ colors.push(extendedPalette[i % extendedPalette.length]);
162
+ }
163
+ else if (datasets[0]?.color) {
164
+ colors.push(resolveColor(datasets[0].color, i));
165
+ } else {
166
+ colors.push(extendedPalette[i % extendedPalette.length]);
167
+ }
168
+ }
169
+ return colors;
170
+ };
171
+
172
+ const chartConfig = computed(() => {
173
+ const { chartType, data } = props.data;
174
+ const isPieType = chartType === 'pie' || chartType === 'doughnut';
175
+
176
+ return {
177
+ type: chartType,
178
+ data: {
179
+ labels: data.labels,
180
+ datasets: data.datasets.map((dataset, index) => ({
181
+ label: dataset.label,
182
+ data: dataset.values,
183
+ backgroundColor: isPieType
184
+ ? generateSegmentColors(data.datasets, data.labels.length)
185
+ : resolveColor(dataset.color, index),
186
+ borderColor: isPieType
187
+ ? 'rgba(0, 0, 0, 0.2)'
188
+ : resolveColor(dataset.color, index),
189
+ borderWidth: isPieType ? 2 : 2,
190
+ tension: chartType === 'line' ? 0.4 : 0,
191
+ fill: chartType === 'line' ? false : undefined,
192
+ }))
193
+ },
194
+ options: {
195
+ responsive: true,
196
+ maintainAspectRatio: false,
197
+ plugins: {
198
+ legend: {
199
+ display: true,
200
+ position: 'bottom' as const,
201
+ labels: {
202
+ color: getComputedStyle(document.body).getPropertyValue('--lumina-color-text').trim() || '#ffffff',
203
+ padding: 20,
204
+ font: {
205
+ family: 'Inter, system-ui, sans-serif',
206
+ size: 12
207
+ }
208
+ }
209
+ },
210
+ tooltip: {
211
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
212
+ titleColor: '#fff',
213
+ bodyColor: 'rgba(255, 255, 255, 0.8)',
214
+ padding: 12,
215
+ cornerRadius: 8,
216
+ titleFont: {
217
+ family: 'Inter, system-ui, sans-serif',
218
+ weight: 'bold' as const
219
+ },
220
+ bodyFont: {
221
+ family: 'Inter, system-ui, sans-serif'
222
+ }
223
+ }
224
+ },
225
+ scales: isPieType ? {} : {
226
+ x: {
227
+ grid: {
228
+ color: 'rgba(128, 128, 128, 0.1)'
229
+ },
230
+ ticks: {
231
+ color: getComputedStyle(document.body).getPropertyValue('--lumina-color-text-secondary').trim() || '#e5e7eb',
232
+ font: {
233
+ family: 'Inter, system-ui, sans-serif'
234
+ }
235
+ }
236
+ },
237
+ y: {
238
+ grid: {
239
+ color: 'rgba(128, 128, 128, 0.1)'
240
+ },
241
+ ticks: {
242
+ color: getComputedStyle(document.body).getPropertyValue('--lumina-color-text-secondary').trim() || '#e5e7eb',
243
+ font: {
244
+ family: 'Inter, system-ui, sans-serif'
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+ };
251
+ });
252
+
253
+ const createChart = () => {
254
+ if (!chartCanvas.value || !ChartJS) return;
255
+
256
+ // Destroy existing chart
257
+ if (chartInstance) {
258
+ chartInstance.destroy();
259
+ }
260
+
261
+ const ctx = chartCanvas.value.getContext('2d');
262
+ if (!ctx) return;
263
+
264
+ chartInstance = new ChartJS(ctx, chartConfig.value as any);
265
+ };
266
+
267
+ onMounted(async () => {
268
+ // Load Chart.js dynamically
269
+ const loaded = await loadChartJS();
270
+ if (loaded) {
271
+ // Small delay to ensure canvas is ready
272
+ setTimeout(createChart, 50);
273
+ }
274
+ });
275
+
276
+ onUnmounted(() => {
277
+ if (chartInstance) {
278
+ chartInstance.destroy();
279
+ chartInstance = null;
280
+ }
281
+ });
282
+
283
+ // Watch for data changes
284
+ watch(() => props.data, () => {
285
+ createChart();
286
+ }, { deep: true });
287
+ </script>
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <BaseSlide :data="data" :slide-index="slideIndex">
3
+ <div :class="[
4
+ 'custom-slide-wrapper relative w-full overflow-hidden',
5
+ data.sizing === 'container' ? 'h-full' : 'min-h-screen'
6
+ ]">
7
+ <!-- HTML Content -->
8
+ <div class="custom-html-content w-full h-full" v-html="sanitizedHtml"></div>
9
+ </div>
10
+ </BaseSlide>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { computed, inject, onMounted, onUnmounted, watch } from 'vue';
15
+ import BaseSlide from '../base/BaseSlide.vue';
16
+ import { StoreKey } from '../../core/store';
17
+ import type { SlideCustom } from '../../core/types';
18
+
19
+ const props = defineProps<{
20
+ data: SlideCustom;
21
+ slideIndex?: number;
22
+ }>();
23
+
24
+ const store = inject(StoreKey);
25
+ const slideIndex = computed(() => props.slideIndex ?? store?.state.currentIndex ?? 0);
26
+
27
+ // Generate unique ID for this instance
28
+ const styleId = `custom-slide-${Math.random().toString(36).slice(2, 10)}`;
29
+
30
+ /**
31
+ * Inject CSS into <head> as a style element
32
+ */
33
+ function injectStyles() {
34
+ // Remove existing style if any
35
+ removeStyles();
36
+
37
+ if (!props.data.css) return;
38
+
39
+ const styleEl = document.createElement('style');
40
+ styleEl.id = styleId;
41
+ styleEl.textContent = props.data.css;
42
+ document.head.appendChild(styleEl);
43
+ }
44
+
45
+ function removeStyles() {
46
+ const existing = document.getElementById(styleId);
47
+ if (existing) {
48
+ existing.remove();
49
+ }
50
+ }
51
+
52
+ // Inject on mount, update on data change
53
+ onMounted(() => {
54
+ injectStyles();
55
+ });
56
+
57
+ // Watch for css changes
58
+ watch(() => props.data.css, () => {
59
+ injectStyles();
60
+ });
61
+
62
+ onUnmounted(() => {
63
+ removeStyles();
64
+ });
65
+
66
+ /**
67
+ * Basic HTML sanitization - removes script tags and event handlers
68
+ */
69
+ const sanitizedHtml = computed(() => {
70
+ if (!props.data.html) return '';
71
+
72
+ return props.data.html
73
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
74
+ .replace(/on\w+\s*=\s*["'][^"']*["']/gi, '')
75
+ .replace(/javascript:/gi, '');
76
+ });
77
+ </script>
78
+
79
+ <style scoped>
80
+ .custom-slide-wrapper {
81
+ /* Wrapper styles */
82
+ }
83
+
84
+ .custom-html-content {
85
+ /* Allow custom content to fill the space */
86
+ display: block;
87
+ }
88
+
89
+ .custom-html-content :deep(*) {
90
+ box-sizing: border-box;
91
+ }
92
+ </style>
@@ -0,0 +1,253 @@
1
+ <template>
2
+ <BaseSlide :data="data" :slide-index="index" class="layout-diagram">
3
+ <!-- Stop propagation to prevent EditorNode from overriding edge/node selection -->
4
+ <div :class="['w-full relative', data.sizing === 'container' ? 'h-full min-h-[400px]' : 'h-screen']"
5
+ @click.stop>
6
+ <VueFlow v-model:nodes="nodes" v-model:edges="edges" class="h-full w-full" :fit-view-on-init="true"
7
+ :default-viewport="{ zoom: 1 }" :min-zoom="0.1" :max-zoom="4" @dragover="onDragOver" @drop="onDrop">
8
+
9
+ <Controls v-if="isEditable" />
10
+
11
+ <template #node-default="props">
12
+ <StudioDiagramNode v-bind="props" :editable="isEditable" :element-id="props.node?.id" />
13
+ </template>
14
+ <template #node-input="props">
15
+ <StudioDiagramNode v-bind="props" :editable="isEditable" :element-id="props.node?.id" />
16
+ </template>
17
+ </VueFlow>
18
+ </div>
19
+ </BaseSlide>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import { ref, watch, computed, onMounted } from 'vue';
24
+ import { VueFlow, useVueFlow } from '@vue-flow/core';
25
+ import { Controls } from '@vue-flow/controls';
26
+ import BaseSlide from '../base/BaseSlide.vue';
27
+ import type { SlideDiagram } from '../../core/types';
28
+ import { useLumina } from '../../composables/useLumina';
29
+
30
+ // CSS imports
31
+ import '@vue-flow/core/dist/style.css';
32
+ import '@vue-flow/core/dist/theme-default.css';
33
+ import '@vue-flow/controls/dist/style.css';
34
+
35
+ const props = defineProps<{
36
+ data: SlideDiagram
37
+ }>();
38
+
39
+ const { options, index } = useLumina();
40
+
41
+ const isEditable = computed(() => {
42
+ // If studio option is enabled, allow editing.
43
+ return !!options.studio;
44
+ });
45
+
46
+ const nodes = ref(props.data.nodes || []);
47
+ const edges = ref(props.data.edges || []);
48
+
49
+ // NO WATCH FOR NODES - VueFlow is the source of truth
50
+ // We only sync FROM VueFlow TO store (one-way), not the other way
51
+ // Watch only for edges (which don't have the same position/dimensions issue)
52
+ watch(() => props.data.edges, (newEdges) => {
53
+ if (newEdges && JSON.stringify(newEdges) !== JSON.stringify(edges.value)) {
54
+ edges.value = [...newEdges];
55
+ }
56
+ }, { deep: true });
57
+
58
+ // Initial setup from props
59
+ onMounted(() => {
60
+ if (props.data.nodes) nodes.value = [...props.data.nodes];
61
+ if (props.data.edges) edges.value = [...props.data.edges];
62
+ });
63
+
64
+ const { onNodeDragStop, onConnect } = useVueFlow();
65
+
66
+ // Helper to sync all nodes to store
67
+ const syncNodesToStore = () => {
68
+ const plainNodes = nodes.value.map(n => JSON.parse(JSON.stringify(n)));
69
+ import('../../core/events').then(({ bus }) => {
70
+ bus.emit('action', {
71
+ type: 'slide-update',
72
+ slideIndex: index.value,
73
+ patch: { nodes: plainNodes }
74
+ });
75
+ });
76
+ };
77
+
78
+ // Listen for direct node updates from inspector (bypasses store -> watch cycle)
79
+ import('../../core/events').then(({ bus }) => {
80
+ bus.on('diagram-node-update', (payload) => {
81
+ if (payload.slideIndex !== index.value) {
82
+ return;
83
+ }
84
+
85
+ // Find and update the node in VueFlow's state directly
86
+ const nodeIndex = nodes.value.findIndex((n: any) => n.id === payload.nodeId);
87
+ if (nodeIndex !== -1) {
88
+ const node = nodes.value[nodeIndex];
89
+ // Handle nested paths like 'style.backgroundColor' or 'data.shape'
90
+ const keys = payload.key.split('.');
91
+ let target: any = node;
92
+ for (let i = 0; i < keys.length - 1; i++) {
93
+ if (!target[keys[i]]) target[keys[i]] = {};
94
+ target = target[keys[i]];
95
+ }
96
+ target[keys[keys.length - 1]] = payload.value;
97
+
98
+ // Trigger reactivity
99
+ nodes.value = [...nodes.value];
100
+
101
+ // Sync to store
102
+ syncNodesToStore();
103
+ }
104
+ });
105
+ });
106
+
107
+ // Handle Node Dragging (Persistence)
108
+ onNodeDragStop((e) => {
109
+ if (!isEditable.value) return;
110
+
111
+ // Create plain objects to avoid proxy issues
112
+ const plainNodes = nodes.value.map(n => JSON.parse(JSON.stringify(n)));
113
+
114
+ import('../../core/events').then(({ bus }) => {
115
+ bus.emit('action', {
116
+ type: 'slide-update',
117
+ slideIndex: index.value,
118
+ patch: {
119
+ nodes: plainNodes
120
+ }
121
+ });
122
+ });
123
+ });
124
+
125
+ // Also handle connection changes
126
+ onConnect((params) => {
127
+ if (!isEditable.value) return;
128
+
129
+ // VueFlow does NOT automatically add the edge. We must do it ourselves.
130
+ const newEdge = {
131
+ id: `edge-${params.source}-${params.target}-${Date.now()}`,
132
+ source: params.source,
133
+ target: params.target,
134
+ sourceHandle: params.sourceHandle,
135
+ targetHandle: params.targetHandle,
136
+ type: 'default',
137
+ animated: false,
138
+ };
139
+
140
+ // Add to local state first for instant feedback
141
+ edges.value = [...edges.value, newEdge];
142
+
143
+ // Persist to store
144
+ const plainEdges = edges.value.map(e => JSON.parse(JSON.stringify(e)));
145
+ import('../../core/events').then(({ bus }) => {
146
+ bus.emit('action', {
147
+ type: 'slide-update',
148
+ slideIndex: index.value,
149
+ patch: {
150
+ edges: plainEdges
151
+ }
152
+ });
153
+ });
154
+ });
155
+
156
+ // Selection Sync
157
+ import { inject } from 'vue';
158
+ import { EditorKey } from '../../composables/useEditor';
159
+ import StudioDiagramNode from '../studio/nodes/StudioDiagramNode.vue';
160
+
161
+ // Optional injection (will be null if not in Studio scope)
162
+ const editor = inject(EditorKey, null);
163
+
164
+ const { onNodeClick, onPaneClick, onEdgeClick, addNodes, project, findNode } = useVueFlow();
165
+
166
+ // Register custom node types
167
+ // We map 'default', 'input', 'output' all to our custom node to ensure resizing works for everything
168
+ // Or we can introduce a new type 'custom' and migrate data.
169
+ // For now, let's map 'default' -> StudioDiagramNode via the template slot or node-types prop
170
+ // Using template slots is easier for dynamic resolving but defineProps is cleaner.
171
+ // Let's use template slots in VueFlow component above.
172
+
173
+ onNodeClick(({ node }) => {
174
+ if (!isEditable.value || !editor) return;
175
+ // Use props.data.nodes (store data) for index lookup to match getByPath
176
+ const storeNodes = props.data.nodes || [];
177
+ const nodeIndex = storeNodes.findIndex((n: any) => n.id === node.id);
178
+ if (nodeIndex !== -1) {
179
+ editor.select(`slides.${index.value}.nodes.${nodeIndex}`);
180
+ }
181
+ });
182
+
183
+ onEdgeClick(({ edge }) => {
184
+ if (!isEditable.value || !editor) return;
185
+ // Use props.data.edges (store data) for index lookup to match getByPath
186
+ const storeEdges = props.data.edges || [];
187
+ const edgeIndex = storeEdges.findIndex((e: any) => e.id === edge.id);
188
+ if (edgeIndex !== -1) {
189
+ const path = `slides.${index.value}.edges.${edgeIndex}`;
190
+ editor.select(path);
191
+ }
192
+ });
193
+
194
+ // NOTE: Do NOT use onPaneClick for selection as it fires AFTER edge/node clicks and overrides them.
195
+ // Users can deselect by clicking on the slide in the left panel.
196
+
197
+ // Drag and Drop Logic
198
+ const onDragOver = (event: DragEvent) => {
199
+ event.preventDefault();
200
+ if (event.dataTransfer) {
201
+ event.dataTransfer.dropEffect = 'move';
202
+ }
203
+ };
204
+
205
+ const onDrop = (event: DragEvent) => {
206
+ if (!isEditable.value || !editor) return;
207
+
208
+ const type = event.dataTransfer?.getData('application/vueflow');
209
+ if (!type) return;
210
+
211
+ // Calculate position
212
+ const { left, top } = (event.currentTarget as HTMLElement).getBoundingClientRect();
213
+ const position = project({
214
+ x: event.clientX - left,
215
+ y: event.clientY - top,
216
+ });
217
+
218
+ const newNode = {
219
+ id: `node-${Date.now()}`,
220
+ type: 'default', // Using 'default' which we will map to our custom component
221
+ position,
222
+ label: `${type} Node`,
223
+ data: {
224
+ type: type, // Store actual shape info in data
225
+ style: { backgroundColor: '#ffffff', color: '#000000', width: '150px', height: '50px' }
226
+ },
227
+ };
228
+
229
+ // Add to store
230
+ // We can use editor.store.addNode, but we need the correct path
231
+ // Since we are in LayoutDiagram, we know the path is `slides.${index.value}.nodes`
232
+ // Use setTimeout to allow UI update
233
+
234
+ // First add locally to via VueFlow for instant feedback?
235
+ // No, single source of truth.
236
+
237
+ editor.store.addNode(`slides.${index.value}.nodes`, newNode);
238
+ editor.commit();
239
+ };
240
+ </script>
241
+
242
+ <style>
243
+ /* Override VueFlow default node styles so our custom component controls appearance */
244
+ .vue-flow__node-default,
245
+ .vue-flow__node-input,
246
+ .vue-flow__node-output {
247
+ padding: 0 !important;
248
+ border-radius: 0 !important;
249
+ border: none !important;
250
+ background-color: transparent !important;
251
+ /* NOTE: Do NOT set width here - let NodeResizer control it */
252
+ }
253
+ </style>