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,373 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Floating Slide Thumbnails Bar -->
|
|
3
|
+
<div
|
|
4
|
+
class="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 flex items-center gap-3 bg-[#0a0a0a]/90 backdrop-blur-2xl px-4 py-3 rounded-2xl border border-white/10 shadow-[0_20px_50px_rgba(0,0,0,0.5)] transition-all duration-300">
|
|
5
|
+
|
|
6
|
+
<!-- Left: Add Slide Button Container -->
|
|
7
|
+
<div class="relative shrink-0">
|
|
8
|
+
<button @click.stop="toggleAddMenu"
|
|
9
|
+
class="w-10 h-10 rounded-xl bg-blue-600 hover:bg-blue-500 text-white flex items-center justify-center transition-all duration-200 active:scale-95 shadow-lg shadow-blue-600/20 group"
|
|
10
|
+
title="Add New Slide">
|
|
11
|
+
<i class="ph-thin ph-plus transition-transform duration-300" :class="{ 'rotate-45': showAddMenu }"></i>
|
|
12
|
+
</button>
|
|
13
|
+
|
|
14
|
+
<!-- Add Slide Menu (Premium Design) -->
|
|
15
|
+
<Transition name="menu">
|
|
16
|
+
<div v-if="showAddMenu"
|
|
17
|
+
class="absolute bottom-full left-0 mb-4 bg-[#111] border border-white/10 rounded-2xl p-2 min-w-[180px] shadow-2xl backdrop-blur-xl z-[60]"
|
|
18
|
+
@click.stop>
|
|
19
|
+
<div
|
|
20
|
+
class="px-3 py-2 text-[10px] font-bold text-white/30 uppercase tracking-widest border-b border-white/5 mb-1">
|
|
21
|
+
Select Layout
|
|
22
|
+
</div>
|
|
23
|
+
<div class="flex flex-col gap-1">
|
|
24
|
+
<button v-for="layout in layoutOptions" :key="layout.type" @click="addSlide(layout.type)"
|
|
25
|
+
class="w-full px-3 py-2.5 text-left text-xs text-white/70 hover:text-white hover:bg-white/5 rounded-xl transition-all flex items-center gap-3 group">
|
|
26
|
+
<div
|
|
27
|
+
class="w-8 h-8 rounded-lg bg-white/5 flex items-center justify-center group-hover:bg-blue-500/20 group-hover:text-blue-400 transition-colors">
|
|
28
|
+
<i :class="layout.icon"></i>
|
|
29
|
+
</div>
|
|
30
|
+
<span class="font-medium">{{ layout.label }}</span>
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</Transition>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Slide Thumbnails with visible scrollbar -->
|
|
38
|
+
<draggable v-model="draggableSlides" item-key="id"
|
|
39
|
+
class="flex items-center gap-2 max-w-[650px] overflow-x-auto slide-scroll px-1 py-1 scroll-smooth"
|
|
40
|
+
:animation="200" ghost-class="opacity-50" drag-class="scale-105" @start="drag = true" @end="drag = false">
|
|
41
|
+
<template #item="{ element: slide, index: i }">
|
|
42
|
+
<button @click="goToSlide(i)"
|
|
43
|
+
class="shrink-0 w-16 h-10 rounded-xl overflow-hidden border-2 transition-all duration-200 relative group active:scale-95 cursor-grab active:cursor-grabbing"
|
|
44
|
+
:class="currentIndex === i
|
|
45
|
+
? 'border-blue-500 ring-4 ring-blue-500/20 scale-105 z-10'
|
|
46
|
+
: 'border-white/5 shadow-inner hover:border-white/20'">
|
|
47
|
+
|
|
48
|
+
<!-- Thumbnail Preview (simplified) -->
|
|
49
|
+
<div class="w-full h-full flex items-center justify-center text-[8px] text-white/30"
|
|
50
|
+
:style="{ backgroundColor: 'var(--lumina-color-background, #0a0a0a)' }">
|
|
51
|
+
<span class="font-bold text-white/50">{{ i + 1 }}</span>
|
|
52
|
+
<!-- Type indicator -->
|
|
53
|
+
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Slide Type Badge -->
|
|
57
|
+
<div
|
|
58
|
+
class="absolute bottom-1 right-1 px-1.5 py-0.5 bg-white/10 backdrop-blur-md rounded-md text-[6px] text-white/80 font-bold uppercase tracking-tighter">
|
|
59
|
+
{{ getSlideTypeLabel(slide.type) }}
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<!-- Delete Button (on hover) -->
|
|
63
|
+
<button v-if="slides.length > 1" @click.stop="confirmDeleteIndex = i"
|
|
64
|
+
class="absolute top-1 right-1 w-5 h-5 bg-red-500/80 hover:bg-red-500 rounded-lg flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all duration-200 transform hover:scale-110">
|
|
65
|
+
<i class="ph-thin ph-x text-[10px] text-white"></i>
|
|
66
|
+
</button>
|
|
67
|
+
</button>
|
|
68
|
+
</template>
|
|
69
|
+
</draggable>
|
|
70
|
+
|
|
71
|
+
<!-- Right: Slide Counter -->
|
|
72
|
+
<div
|
|
73
|
+
class="flex flex-col items-center justify-center px-4 py-1.5 rounded-xl bg-white/5 border border-white/5 shrink-0">
|
|
74
|
+
<div class="text-[10px] font-bold text-white uppercase tracking-tighter leading-tight">Slide</div>
|
|
75
|
+
<div class="text-[10px] font-mono text-white/50 leading-tight">
|
|
76
|
+
{{ currentIndex + 1 }} <span class="text-white/20">/</span> {{ slides.length }}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<!-- Confirmation Modal -->
|
|
82
|
+
<Transition name="modal">
|
|
83
|
+
<div v-if="confirmDeleteIndex !== null"
|
|
84
|
+
class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-md"
|
|
85
|
+
@click="confirmDeleteIndex = null">
|
|
86
|
+
<div class="bg-[#111111] border border-white/10 rounded-3xl p-8 shadow-2xl max-w-sm w-full mx-4 transform transition-all"
|
|
87
|
+
@click.stop>
|
|
88
|
+
<div class="flex flex-col items-center text-center mb-8">
|
|
89
|
+
<div
|
|
90
|
+
class="w-16 h-16 rounded-2xl bg-red-500/10 flex items-center justify-center text-red-500 mb-6 border border-red-500/20">
|
|
91
|
+
<i class="ph-thin ph-trash text-2xl"></i>
|
|
92
|
+
</div>
|
|
93
|
+
<h3 class="text-xl font-bold text-white mb-2">Delete slide?</h3>
|
|
94
|
+
<p class="text-sm text-white/40">This will permanently remove slide #{{ confirmDeleteIndex + 1 }}
|
|
95
|
+
and all its content.</p>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="flex gap-4">
|
|
98
|
+
<button @click="confirmDeleteIndex = null"
|
|
99
|
+
class="flex-1 px-6 py-3 rounded-2xl bg-white/5 hover:bg-white/10 text-white transition text-sm font-semibold border border-white/5">
|
|
100
|
+
Cancel
|
|
101
|
+
</button>
|
|
102
|
+
<button @click="deleteSlide"
|
|
103
|
+
class="flex-1 px-6 py-3 rounded-2xl bg-red-600 hover:bg-red-500 text-white transition text-sm font-semibold shadow-xl shadow-red-600/30">
|
|
104
|
+
Delete
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</Transition>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<script setup lang="ts">
|
|
113
|
+
import { ref, computed, inject, onMounted, onUnmounted } from 'vue';
|
|
114
|
+
import { StoreKey } from '../../core/store';
|
|
115
|
+
import { useEditor } from '../../composables/useEditor';
|
|
116
|
+
|
|
117
|
+
import draggable from 'vuedraggable';
|
|
118
|
+
|
|
119
|
+
const store = inject(StoreKey)!;
|
|
120
|
+
const editor = useEditor();
|
|
121
|
+
|
|
122
|
+
const showAddMenu = ref(false);
|
|
123
|
+
const confirmDeleteIndex = ref<number | null>(null);
|
|
124
|
+
const drag = ref(false);
|
|
125
|
+
|
|
126
|
+
const slides = computed(() => store.state.deck?.slides || []);
|
|
127
|
+
const currentIndex = computed(() => store.state.currentIndex);
|
|
128
|
+
|
|
129
|
+
const draggableSlides = computed({
|
|
130
|
+
get() {
|
|
131
|
+
return store.state.deck?.slides || [];
|
|
132
|
+
},
|
|
133
|
+
set(value: any[]) {
|
|
134
|
+
store.patchDeck({ slides: value });
|
|
135
|
+
editor.commit();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const layoutOptions = [
|
|
140
|
+
{ type: 'flex', label: 'Flex Layout', icon: 'ph-thin ph-squares-four' },
|
|
141
|
+
{ type: 'statement', label: 'Statement', icon: 'ph-thin ph-quotes' },
|
|
142
|
+
{ type: 'half', label: 'Half Layout', icon: 'ph-thin ph-columns' },
|
|
143
|
+
{ type: 'features', label: 'Features', icon: 'ph-thin ph-grid-four' },
|
|
144
|
+
{ type: 'timeline', label: 'Timeline', icon: 'ph-thin ph-clock' },
|
|
145
|
+
{ type: 'steps', label: 'Steps', icon: 'ph-thin ph-list-numbers' },
|
|
146
|
+
{ type: 'diagram', label: 'Diagram', icon: 'ph-thin ph-tree-structure' },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const slideTemplates: Record<string, any> = {
|
|
150
|
+
'flex': {
|
|
151
|
+
type: 'flex',
|
|
152
|
+
direction: 'horizontal',
|
|
153
|
+
gap: 'md',
|
|
154
|
+
padding: 'lg',
|
|
155
|
+
elements: [
|
|
156
|
+
{
|
|
157
|
+
type: 'content',
|
|
158
|
+
size: 'half',
|
|
159
|
+
halign: 'left',
|
|
160
|
+
valign: 'center',
|
|
161
|
+
elements: [
|
|
162
|
+
{ type: 'title', text: 'New Slide', size: 'xl' },
|
|
163
|
+
{ type: 'text', text: 'Start editing your content here.' }
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'image',
|
|
168
|
+
size: 'half',
|
|
169
|
+
src: 'https://images.unsplash.com/photo-1557683316-973673baf926?w=800',
|
|
170
|
+
alt: 'Placeholder image',
|
|
171
|
+
fill: true,
|
|
172
|
+
rounded: 'lg'
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
},
|
|
176
|
+
'statement': {
|
|
177
|
+
type: 'statement',
|
|
178
|
+
tag: 'Tagline',
|
|
179
|
+
title: 'Big Statement Here',
|
|
180
|
+
subtitle: 'Supporting description text goes below the main statement.'
|
|
181
|
+
},
|
|
182
|
+
'half': {
|
|
183
|
+
type: 'half',
|
|
184
|
+
imageSide: 'right',
|
|
185
|
+
image: 'https://images.unsplash.com/photo-1557683316-973673baf926?w=800',
|
|
186
|
+
tag: 'Category',
|
|
187
|
+
title: 'Split Layout Title',
|
|
188
|
+
paragraphs: [
|
|
189
|
+
'First paragraph of content.',
|
|
190
|
+
'Second paragraph with more details.'
|
|
191
|
+
],
|
|
192
|
+
cta: 'Learn More'
|
|
193
|
+
},
|
|
194
|
+
'features': {
|
|
195
|
+
type: 'features',
|
|
196
|
+
title: 'Key Features',
|
|
197
|
+
description: 'Discover what makes us different.',
|
|
198
|
+
features: [
|
|
199
|
+
{ icon: 'star', title: 'Feature One', desc: 'Description of the first feature.' },
|
|
200
|
+
{ icon: 'rocket', title: 'Feature Two', desc: 'Description of the second feature.' },
|
|
201
|
+
{ icon: 'lightning', title: 'Feature Three', desc: 'Description of the third feature.' }
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
'timeline': {
|
|
205
|
+
type: 'timeline',
|
|
206
|
+
title: 'Our Journey',
|
|
207
|
+
subtitle: 'Key milestones in our history.',
|
|
208
|
+
timeline: [
|
|
209
|
+
{ date: '2020', title: 'Founded', description: 'Company was established.' },
|
|
210
|
+
{ date: '2022', title: 'Growth', description: 'Expanded to new markets.' },
|
|
211
|
+
{ date: '2024', title: 'Today', description: 'Serving customers worldwide.' }
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
'steps': {
|
|
215
|
+
type: 'steps',
|
|
216
|
+
title: 'How It Works',
|
|
217
|
+
subtitle: 'Simple steps to get started.',
|
|
218
|
+
steps: [
|
|
219
|
+
{ step: '1', title: 'Step One', description: 'First thing to do.' },
|
|
220
|
+
{ step: '2', title: 'Step Two', description: 'Second thing to do.' },
|
|
221
|
+
{ step: '3', title: 'Step Three', description: 'Final step.' }
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
'diagram': {
|
|
225
|
+
type: 'diagram',
|
|
226
|
+
title: 'New Diagram',
|
|
227
|
+
nodes: [
|
|
228
|
+
{ id: '1', type: 'input', label: 'Start', position: { x: 250, y: 5 } },
|
|
229
|
+
{ id: '2', label: 'Process', position: { x: 100, y: 100 } },
|
|
230
|
+
{ id: '3', label: 'End', position: { x: 400, y: 100 } }
|
|
231
|
+
],
|
|
232
|
+
edges: [
|
|
233
|
+
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
|
234
|
+
{ id: 'e1-3', source: '1', target: '3' }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const getSlideTypeLabel = (type: string) => {
|
|
240
|
+
const labels: Record<string, string> = {
|
|
241
|
+
'flex': 'Flex',
|
|
242
|
+
'statement': 'Stmt',
|
|
243
|
+
'half': 'Half',
|
|
244
|
+
'features': 'Feat',
|
|
245
|
+
'timeline': 'Time',
|
|
246
|
+
'steps': 'Step',
|
|
247
|
+
'diagram': 'Diag',
|
|
248
|
+
};
|
|
249
|
+
return labels[type] || type.slice(0, 4);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const goToSlide = (index: number) => {
|
|
253
|
+
store.goto(index);
|
|
254
|
+
editor.select(null); // Clear selection when changing slides
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const toggleAddMenu = () => {
|
|
258
|
+
showAddMenu.value = !showAddMenu.value;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const addSlide = (type: string) => {
|
|
262
|
+
const template = slideTemplates[type] || slideTemplates['flex'];
|
|
263
|
+
const newSlide = JSON.parse(JSON.stringify(template));
|
|
264
|
+
|
|
265
|
+
// Ensure elements have dragKeys for Flex layouts
|
|
266
|
+
if (newSlide.type === 'flex' && Array.isArray(newSlide.elements)) {
|
|
267
|
+
newSlide.elements.forEach((el: any) => {
|
|
268
|
+
el.dragKey = Date.now() + Math.random();
|
|
269
|
+
if (el.type === 'content' && Array.isArray(el.elements)) {
|
|
270
|
+
el.elements.forEach((child: any) => {
|
|
271
|
+
child.dragKey = Date.now() + Math.random() + 1;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const currentSlides = [...slides.value];
|
|
278
|
+
const insertAt = currentIndex.value + 1;
|
|
279
|
+
currentSlides.splice(insertAt, 0, newSlide);
|
|
280
|
+
|
|
281
|
+
store.patchDeck({ slides: currentSlides });
|
|
282
|
+
editor.commit();
|
|
283
|
+
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
store.goto(insertAt);
|
|
286
|
+
showAddMenu.value = false;
|
|
287
|
+
}, 50);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const deleteSlide = () => {
|
|
291
|
+
const index = confirmDeleteIndex.value;
|
|
292
|
+
if (index === null || slides.value.length <= 1) {
|
|
293
|
+
confirmDeleteIndex.value = null;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const currentSlides = [...slides.value];
|
|
298
|
+
currentSlides.splice(index, 1);
|
|
299
|
+
|
|
300
|
+
store.patchDeck({ slides: currentSlides });
|
|
301
|
+
editor.commit();
|
|
302
|
+
|
|
303
|
+
// Adjust current index if needed
|
|
304
|
+
if (currentIndex.value >= currentSlides.length) {
|
|
305
|
+
store.goto(currentSlides.length - 1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
confirmDeleteIndex.value = null;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
// Close menu when clicking outside
|
|
313
|
+
const handleClickOutside = () => {
|
|
314
|
+
showAddMenu.value = false;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
onMounted(() => {
|
|
318
|
+
document.addEventListener('click', handleClickOutside);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
onUnmounted(() => {
|
|
322
|
+
document.removeEventListener('click', handleClickOutside);
|
|
323
|
+
});
|
|
324
|
+
</script>
|
|
325
|
+
|
|
326
|
+
<style scoped>
|
|
327
|
+
/* Modal Transition */
|
|
328
|
+
.modal-enter-active,
|
|
329
|
+
.modal-leave-active {
|
|
330
|
+
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.modal-enter-from,
|
|
334
|
+
.modal-leave-to {
|
|
335
|
+
opacity: 0;
|
|
336
|
+
transform: scale(0.9) translateY(10px);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* Menu Transition */
|
|
340
|
+
.menu-enter-active,
|
|
341
|
+
.menu-leave-active {
|
|
342
|
+
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.menu-enter-from,
|
|
346
|
+
.menu-leave-to {
|
|
347
|
+
opacity: 0;
|
|
348
|
+
transform: translateY(10px) scale(0.95);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* Custom styled scrollbar for slide container */
|
|
352
|
+
.slide-scroll::-webkit-scrollbar {
|
|
353
|
+
height: 4px;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.slide-scroll::-webkit-scrollbar-track {
|
|
357
|
+
background: transparent;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.slide-scroll::-webkit-scrollbar-thumb {
|
|
361
|
+
background: rgba(255, 255, 255, 0.1);
|
|
362
|
+
border-radius: 2px;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.slide-scroll::-webkit-scrollbar-thumb:hover {
|
|
366
|
+
background: rgba(255, 255, 255, 0.2);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.slide-scroll {
|
|
370
|
+
scrollbar-width: thin;
|
|
371
|
+
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
|
|
372
|
+
}
|
|
373
|
+
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="space-y-1">
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">{{ label }}</label>
|
|
5
|
+
<span class="text-white/70 text-[10px] font-mono">{{ displayValue }}</span>
|
|
6
|
+
</div>
|
|
7
|
+
<input type="range" :min="min" :max="max" :step="step" :value="numericValue"
|
|
8
|
+
@input="updateValue(($event.target as HTMLInputElement).value)"
|
|
9
|
+
class="w-full h-1 bg-[#333] rounded-lg appearance-none cursor-pointer accent-blue-500 hover:accent-blue-400" />
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { computed } from 'vue';
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
label: string;
|
|
18
|
+
value: number | string | undefined;
|
|
19
|
+
min?: number;
|
|
20
|
+
max?: number;
|
|
21
|
+
step?: number;
|
|
22
|
+
unit?: string;
|
|
23
|
+
multiplier?: number; // Logic for mapping 0-1 to 0-100 etc if needed
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{
|
|
27
|
+
(e: 'update', value: number | string): void;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const numericValue = computed(() => {
|
|
31
|
+
if (typeof props.value === 'number') return props.value;
|
|
32
|
+
if (typeof props.value === 'string') return parseFloat(props.value) || 0;
|
|
33
|
+
return 0;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const displayValue = computed(() => {
|
|
37
|
+
return `${numericValue.value}${props.unit || ''}`;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const updateValue = (val: string) => {
|
|
41
|
+
const num = parseFloat(val);
|
|
42
|
+
emit('update', num);
|
|
43
|
+
};
|
|
44
|
+
</script>
|