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,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>
|