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.
- package/LUMINA_LLM_EXAMPLES.json +234 -0
- package/README.md +18 -18
- package/dist/lumina-slides.js +13207 -12659
- package/dist/lumina-slides.umd.cjs +215 -215
- package/dist/style.css +1 -1
- package/package.json +5 -4
- package/src/App.vue +16 -0
- package/src/animation/index.ts +11 -0
- package/src/animation/registry.ts +126 -0
- package/src/animation/stagger.ts +95 -0
- package/src/animation/types.ts +53 -0
- package/src/components/LandingPage.vue +229 -0
- package/src/components/LuminaDeck.vue +224 -0
- package/src/components/LuminaSpeakerNotes.vue +701 -0
- package/src/components/base/BaseSlide.vue +122 -0
- package/src/components/base/LuminaElement.vue +67 -0
- package/src/components/base/VideoPlayer.vue +204 -0
- package/src/components/layouts/LayoutAuto.vue +71 -0
- package/src/components/layouts/LayoutChart.vue +287 -0
- package/src/components/layouts/LayoutCustom.vue +92 -0
- package/src/components/layouts/LayoutDiagram.vue +253 -0
- package/src/components/layouts/LayoutFeatures.vue +121 -0
- package/src/components/layouts/LayoutFlex.vue +172 -0
- package/src/components/layouts/LayoutFree.vue +62 -0
- package/src/components/layouts/LayoutHalf.vue +127 -0
- package/src/components/layouts/LayoutStatement.vue +74 -0
- package/src/components/layouts/LayoutSteps.vue +106 -0
- package/src/components/layouts/LayoutTimeline.vue +104 -0
- package/src/components/layouts/LayoutVideo.vue +41 -0
- package/src/components/parts/FlexBullets.vue +45 -0
- package/src/components/parts/FlexButton.vue +132 -0
- package/src/components/parts/FlexImage.vue +54 -0
- package/src/components/parts/FlexOrdered.vue +44 -0
- package/src/components/parts/FlexSpacer.vue +13 -0
- package/src/components/parts/FlexStepper.vue +59 -0
- package/src/components/parts/FlexText.vue +29 -0
- package/src/components/parts/FlexTimeline.vue +67 -0
- package/src/components/parts/FlexTitle.vue +39 -0
- package/src/components/parts/LuminaBackground.vue +100 -0
- package/src/components/site/LivePreview.vue +101 -0
- package/src/components/site/SiteApi.vue +301 -0
- package/src/components/site/SiteDashboard.vue +604 -0
- package/src/components/site/SiteDocs.vue +3267 -0
- package/src/components/site/SiteExamples.vue +65 -0
- package/src/components/site/SiteFooter.vue +6 -0
- package/src/components/site/SiteHome.vue +362 -0
- package/src/components/site/SiteNavBar.vue +122 -0
- package/src/components/site/SitePlayground.vue +389 -0
- package/src/components/site/SitePromptBuilder.vue +266 -0
- package/src/components/site/SiteUserMenu.vue +90 -0
- package/src/components/studio/ActionEditor.vue +108 -0
- package/src/components/studio/ArrayEditor.vue +124 -0
- package/src/components/studio/CollapsibleSection.vue +33 -0
- package/src/components/studio/ColorField.vue +22 -0
- package/src/components/studio/EditorCanvas.vue +326 -0
- package/src/components/studio/EditorLayoutFeatures.vue +18 -0
- package/src/components/studio/EditorLayoutFixed.vue +46 -0
- package/src/components/studio/EditorLayoutFlex.vue +133 -0
- package/src/components/studio/EditorLayoutHalf.vue +18 -0
- package/src/components/studio/EditorLayoutStatement.vue +18 -0
- package/src/components/studio/EditorLayoutSteps.vue +18 -0
- package/src/components/studio/EditorLayoutTimeline.vue +18 -0
- package/src/components/studio/EditorNode.vue +89 -0
- package/src/components/studio/FieldEditor.vue +133 -0
- package/src/components/studio/IconPicker.vue +109 -0
- package/src/components/studio/LayerItem.vue +117 -0
- package/src/components/studio/LuminaStudio.vue +30 -0
- package/src/components/studio/SaveSuccessModal.vue +138 -0
- package/src/components/studio/SlideNavigator.vue +373 -0
- package/src/components/studio/SliderField.vue +44 -0
- package/src/components/studio/StudioInspector.vue +595 -0
- package/src/components/studio/StudioJsonEditor.vue +191 -0
- package/src/components/studio/StudioLayers.vue +145 -0
- package/src/components/studio/StudioSettings.vue +514 -0
- package/src/components/studio/StudioSidebar.vue +29 -0
- package/src/components/studio/StudioToolbar.vue +222 -0
- package/src/components/studio/fieldLabels.ts +224 -0
- package/src/components/studio/inspectors/DiagramEdgeEditor.vue +77 -0
- package/src/components/studio/inspectors/DiagramNodeEditor.vue +117 -0
- package/src/components/studio/nodes/StudioDiagramNode.vue +138 -0
- package/src/composables/useAuth.ts +87 -0
- package/src/composables/useEditor.ts +224 -0
- package/src/composables/useElementState.ts +81 -0
- package/src/composables/useFlexLayout.ts +122 -0
- package/src/composables/useKeyboard.ts +45 -0
- package/src/composables/useLumina.ts +32 -0
- package/src/composables/useStudio.ts +87 -0
- package/src/composables/useSwipeNav.ts +53 -0
- package/src/composables/useTransition.ts +373 -0
- package/src/core/Lumina.ts +819 -0
- package/src/core/animationConfig.ts +251 -0
- package/src/core/compression.ts +34 -0
- package/src/core/elementController.ts +170 -0
- package/src/core/elementId.ts +27 -0
- package/src/core/elementResolver.ts +207 -0
- package/src/core/events.ts +53 -0
- package/src/core/fonts.ts +100 -0
- package/src/core/presets.ts +231 -0
- package/src/core/prompts.ts +272 -0
- package/src/core/schema.ts +478 -0
- package/src/core/speaker-channel.ts +250 -0
- package/src/core/store.ts +461 -0
- package/src/core/theme.ts +666 -0
- package/src/core/types.ts +1611 -0
- package/src/directives/vStudio.ts +45 -0
- package/src/index.ts +175 -0
- package/src/main.ts +17 -0
- package/src/router/index.ts +92 -0
- package/src/style/main.css +462 -0
- package/src/utils/deep.ts +127 -0
- package/src/utils/firebase.ts +184 -0
- package/src/utils/streaming.ts +134 -0
- package/src/views/DashboardView.vue +32 -0
- package/src/views/DeckView.vue +289 -0
- package/src/views/HomeView.vue +17 -0
- package/src/views/SiteLayout.vue +21 -0
- package/src/views/StudioView.vue +61 -0
- package/src/vite-env.d.ts +6 -0
- 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>
|