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,514 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex flex-col h-full text-xs bg-[#111]">
|
|
3
|
+
<!-- Header -->
|
|
4
|
+
<div class="px-3 py-2 font-bold text-white/50 uppercase tracking-wider border-b border-[#333]">
|
|
5
|
+
Deck Settings
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="flex-1 overflow-y-auto p-2 space-y-2 custom-scrollbar">
|
|
9
|
+
|
|
10
|
+
<!-- Metadata Section -->
|
|
11
|
+
<CollapsibleSection title="Metadata" icon="ph-thin ph-info" :defaultExpanded="true">
|
|
12
|
+
<div class="space-y-2">
|
|
13
|
+
<div class="space-y-1">
|
|
14
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">Title</label>
|
|
15
|
+
<input type="text" :value="deckMeta?.title || ''"
|
|
16
|
+
@input="updateMeta('title', ($event.target as HTMLInputElement).value)"
|
|
17
|
+
placeholder="Presentation Title"
|
|
18
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white focus:border-blue-500 focus:outline-none" />
|
|
19
|
+
</div>
|
|
20
|
+
<div class="space-y-1">
|
|
21
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">Author</label>
|
|
22
|
+
<input type="text" :value="deckMeta?.author || ''"
|
|
23
|
+
@input="updateMeta('author', ($event.target as HTMLInputElement).value)"
|
|
24
|
+
placeholder="Author Name"
|
|
25
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white focus:border-blue-500 focus:outline-none" />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</CollapsibleSection>
|
|
29
|
+
|
|
30
|
+
<!-- Presets Section -->
|
|
31
|
+
<CollapsibleSection title="Theme Preset" icon="ph-thin ph-palette">
|
|
32
|
+
<div class="grid grid-cols-2 gap-2">
|
|
33
|
+
<button v-for="preset in themePresets" :key="preset.id" @click="applyPreset(preset.id)"
|
|
34
|
+
class="p-2 rounded border text-[10px] font-bold transition flex flex-col items-center gap-1"
|
|
35
|
+
:class="currentTheme === preset.id
|
|
36
|
+
? 'border-blue-500 bg-blue-500/20 text-blue-400'
|
|
37
|
+
: 'border-[#333] bg-[#1a1a1a] text-white/70 hover:border-white/30'">
|
|
38
|
+
<div class="flex gap-1">
|
|
39
|
+
<span class="w-3 h-3 rounded-full" :style="{ backgroundColor: preset.primary }"></span>
|
|
40
|
+
<span class="w-3 h-3 rounded-full" :style="{ backgroundColor: preset.secondary }"></span>
|
|
41
|
+
<span class="w-3 h-3 rounded-full" :style="{ backgroundColor: preset.bg }"></span>
|
|
42
|
+
</div>
|
|
43
|
+
{{ preset.name }}
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
</CollapsibleSection>
|
|
47
|
+
|
|
48
|
+
<!-- Colors Section -->
|
|
49
|
+
<CollapsibleSection title="Colors" icon="ph-thin ph-drop">
|
|
50
|
+
<div class="space-y-3">
|
|
51
|
+
<div class="text-white/30 text-[10px] uppercase tracking-widest">Brand</div>
|
|
52
|
+
<ColorField label="Primary" field="primary" :value="themeColors.primary"
|
|
53
|
+
@update="updateThemeColor" />
|
|
54
|
+
<ColorField label="Secondary" field="secondary" :value="themeColors.secondary"
|
|
55
|
+
@update="updateThemeColor" />
|
|
56
|
+
<ColorField label="Accent" field="accent" :value="themeColors.accent" @update="updateThemeColor" />
|
|
57
|
+
|
|
58
|
+
<div class="text-white/30 text-[10px] uppercase tracking-widest pt-2 border-t border-[#333]">Base
|
|
59
|
+
</div>
|
|
60
|
+
<ColorField label="Background" field="background" :value="themeColors.background"
|
|
61
|
+
@update="updateThemeColor" />
|
|
62
|
+
<ColorField label="Surface" field="surface" :value="themeColors.surface"
|
|
63
|
+
@update="updateThemeColor" />
|
|
64
|
+
|
|
65
|
+
<div class="text-white/30 text-[10px] uppercase tracking-widest pt-2 border-t border-[#333]">Text
|
|
66
|
+
</div>
|
|
67
|
+
<ColorField label="Text Main" field="text" :value="themeColors.text" @update="updateThemeColor" />
|
|
68
|
+
<ColorField label="Text Muted" field="muted" :value="themeColors.muted"
|
|
69
|
+
@update="updateThemeColor" />
|
|
70
|
+
|
|
71
|
+
<div class="text-white/30 text-[10px] uppercase tracking-widest pt-2 border-t border-[#333]">
|
|
72
|
+
Semantic</div>
|
|
73
|
+
<ColorField label="Success" field="success" :value="themeColors.success"
|
|
74
|
+
@update="updateThemeColor" />
|
|
75
|
+
<ColorField label="Warning" field="warning" :value="themeColors.warning"
|
|
76
|
+
@update="updateThemeColor" />
|
|
77
|
+
<ColorField label="Danger" field="danger" :value="themeColors.danger" @update="updateThemeColor" />
|
|
78
|
+
</div>
|
|
79
|
+
</CollapsibleSection>
|
|
80
|
+
|
|
81
|
+
<!-- Typography Section -->
|
|
82
|
+
<CollapsibleSection title="Typography" icon="ph-thin ph-text-t">
|
|
83
|
+
<div class="space-y-3">
|
|
84
|
+
<div class="space-y-1">
|
|
85
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">Heading Font</label>
|
|
86
|
+
<select :value="typographySettings.heading"
|
|
87
|
+
@change="updateTypography('heading', ($event.target as HTMLSelectElement).value)"
|
|
88
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white focus:border-blue-500 focus:outline-none">
|
|
89
|
+
<optgroup v-for="(group, key) in fontGroups" :key="key" :label="group.label">
|
|
90
|
+
<option v-for="font in group.options" :key="font.value" :value="font.value">
|
|
91
|
+
{{ font.label }}
|
|
92
|
+
</option>
|
|
93
|
+
</optgroup>
|
|
94
|
+
</select>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="space-y-1">
|
|
97
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">Body Font</label>
|
|
98
|
+
<select :value="typographySettings.body"
|
|
99
|
+
@change="updateTypography('body', ($event.target as HTMLSelectElement).value)"
|
|
100
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white focus:border-blue-500 focus:outline-none">
|
|
101
|
+
<optgroup v-for="(group, key) in fontGroups" :key="key" :label="group.label">
|
|
102
|
+
<option v-for="font in group.options" :key="font.value" :value="font.value">
|
|
103
|
+
{{ font.label }}
|
|
104
|
+
</option>
|
|
105
|
+
</optgroup>
|
|
106
|
+
</select>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</CollapsibleSection>
|
|
110
|
+
|
|
111
|
+
<!-- Spacing Section -->
|
|
112
|
+
<CollapsibleSection title="Spacing Scale" icon="ph-thin ph-ruler">
|
|
113
|
+
<div class="space-y-3">
|
|
114
|
+
<div class="grid grid-cols-2 gap-2">
|
|
115
|
+
<div v-for="token in spacingTokens" :key="token" class="space-y-1">
|
|
116
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">{{ token }}</label>
|
|
117
|
+
<input type="text" :value="themeSpacing[token]"
|
|
118
|
+
@input="updateSpacing(token, ($event.target as HTMLInputElement).value)"
|
|
119
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white focus:border-blue-500 focus:outline-none font-mono text-xs" />
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</CollapsibleSection>
|
|
124
|
+
|
|
125
|
+
<!-- Border Radius Section -->
|
|
126
|
+
<CollapsibleSection title="Border Radius" icon="ph-thin ph-corners-out">
|
|
127
|
+
<div class="space-y-3">
|
|
128
|
+
<div class="grid grid-cols-2 gap-2">
|
|
129
|
+
<div v-for="token in radiusTokens" :key="token" class="space-y-1">
|
|
130
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">{{ token }}</label>
|
|
131
|
+
<input type="text" :value="themeRadius[token]"
|
|
132
|
+
@input="updateRadius(token, ($event.target as HTMLInputElement).value)"
|
|
133
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white focus:border-blue-500 focus:outline-none font-mono text-xs" />
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</CollapsibleSection>
|
|
138
|
+
|
|
139
|
+
<!-- Effects Section -->
|
|
140
|
+
<CollapsibleSection title="Effects" icon="ph-thin ph-magic-wand">
|
|
141
|
+
<div class="space-y-3">
|
|
142
|
+
<ToggleField label="Animations Enabled" :value="effectsSettings.animations"
|
|
143
|
+
@update="v => updateEffect('animationsEnabled', v)" />
|
|
144
|
+
|
|
145
|
+
<div v-if="effectsSettings.animations" class="space-y-3 pt-2 pl-2 border-l-2 border-[#333]">
|
|
146
|
+
<div class="space-y-1">
|
|
147
|
+
<label class="text-white/50 text-[10px] uppercase">Default Animation</label>
|
|
148
|
+
<select :value="effectsSettings.animationType"
|
|
149
|
+
@change="updateEffect('animationType', ($event.target as HTMLSelectElement).value)"
|
|
150
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono">
|
|
151
|
+
<option value="cascade">Cascade</option>
|
|
152
|
+
<option value="fade">Fade</option>
|
|
153
|
+
<option value="slide">Slide</option>
|
|
154
|
+
<option value="zoom">Zoom</option>
|
|
155
|
+
<option value="spring">Spring</option>
|
|
156
|
+
<option value="blur">Blur</option>
|
|
157
|
+
<option value="skew">Skew</option>
|
|
158
|
+
<option value="flip">Flip (3D)</option>
|
|
159
|
+
<option value="zoom-rotate">Zoom Rotate</option>
|
|
160
|
+
<option value="elastic-up">Elastic Up</option>
|
|
161
|
+
</select>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="grid grid-cols-2 gap-2">
|
|
164
|
+
<div class="space-y-1">
|
|
165
|
+
<label class="text-white/50 text-[10px] uppercase">Duration</label>
|
|
166
|
+
<input type="number" step="0.1" :value="effectsSettings.animationDuration"
|
|
167
|
+
@input="updateEffect('animationDuration', parseFloat(($event.target as HTMLInputElement).value))"
|
|
168
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
|
|
169
|
+
</div>
|
|
170
|
+
<div class="space-y-1">
|
|
171
|
+
<label class="text-white/50 text-[10px] uppercase">Stagger</label>
|
|
172
|
+
<input type="number" step="0.05" :value="effectsSettings.animationStagger"
|
|
173
|
+
@input="updateEffect('animationStagger', parseFloat(($event.target as HTMLInputElement).value))"
|
|
174
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div class="pt-2 border-t border-[#333]">
|
|
180
|
+
<ToggleField label="Ambient Orb" :value="effectsSettings.orb"
|
|
181
|
+
@update="v => updateEffect('useOrb', v)" />
|
|
182
|
+
<div v-if="effectsSettings.orb" class="mt-2 pl-2 border-l-2 border-[#333] space-y-2">
|
|
183
|
+
<SliderField label="Opacity" :value="effectsSettings.orbOpacity" :min="0" :max="1"
|
|
184
|
+
:step="0.05" @update="v => updateEffect('orbOpacity', v)" />
|
|
185
|
+
<SliderField label="Blur (px)" :value="parseInt(effectsSettings.orbBlur as string)" :min="0"
|
|
186
|
+
:max="200" :step="10" unit="px" @update="v => updateEffect('orbBlur', v + 'px')" />
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div class="pt-2 border-t border-[#333]">
|
|
191
|
+
<ToggleField label="Glass Effect" :value="effectsSettings.glass"
|
|
192
|
+
@update="v => updateEffect('useGlass', v)" />
|
|
193
|
+
<div v-if="effectsSettings.glass" class="mt-2 pl-2 border-l-2 border-[#333] space-y-2">
|
|
194
|
+
<SliderField label="Opacity" :value="effectsSettings.glassOpacity" :min="0" :max="1"
|
|
195
|
+
:step="0.05" @update="v => updateEffect('glassOpacity', v)" />
|
|
196
|
+
<SliderField label="Blur (px)" :value="parseInt(effectsSettings.glassBlur as string)"
|
|
197
|
+
:min="0" :max="50" :step="2" unit="px"
|
|
198
|
+
@update="v => updateEffect('glassBlur', v + 'px')" />
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="pt-2 border-t border-[#333]">
|
|
203
|
+
<ToggleField label="Shadows" :value="effectsSettings.shadows"
|
|
204
|
+
@update="v => updateEffect('useShadows', v)" />
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</CollapsibleSection>
|
|
208
|
+
|
|
209
|
+
<!-- Components Section -->
|
|
210
|
+
<CollapsibleSection title="Components" icon="ph-thin ph-cubes">
|
|
211
|
+
<div class="space-y-3">
|
|
212
|
+
<div class="text-white/30 text-[10px] uppercase tracking-widest">Buttons</div>
|
|
213
|
+
<div class="space-y-1">
|
|
214
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">Radius</label>
|
|
215
|
+
<select :value="componentsSettings.buttonRadius"
|
|
216
|
+
@change="updateComponent('buttonRadius', ($event.target as HTMLSelectElement).value)"
|
|
217
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white focus:border-blue-500 focus:outline-none">
|
|
218
|
+
<option value="0">Square</option>
|
|
219
|
+
<option value="0.25rem">Small</option>
|
|
220
|
+
<option value="0.5rem">Medium</option>
|
|
221
|
+
<option value="9999px">Pill (Full)</option>
|
|
222
|
+
</select>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div class="text-white/30 text-[10px] uppercase tracking-widest pt-2 border-t border-[#333]">Cards
|
|
226
|
+
</div>
|
|
227
|
+
<div class="space-y-1">
|
|
228
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">Radius</label>
|
|
229
|
+
<select :value="componentsSettings.cardRadius"
|
|
230
|
+
@change="updateComponent('cardRadius', ($event.target as HTMLSelectElement).value)"
|
|
231
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white focus:border-blue-500 focus:outline-none">
|
|
232
|
+
<option value="0">Square</option>
|
|
233
|
+
<option value="0.5rem">Small</option>
|
|
234
|
+
<option value="1rem">Medium</option>
|
|
235
|
+
<option value="1.5rem">Large</option>
|
|
236
|
+
<option value="2rem">X-Large</option>
|
|
237
|
+
</select>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</CollapsibleSection>
|
|
241
|
+
|
|
242
|
+
<!-- Stats -->
|
|
243
|
+
<div class="space-y-2 pt-4 border-t border-[#333] mt-2">
|
|
244
|
+
<div class="text-white/30 text-[10px] uppercase tracking-widest">Statistics</div>
|
|
245
|
+
<div class="flex justify-between text-white/50">
|
|
246
|
+
<span>Slides</span>
|
|
247
|
+
<span class="font-mono text-white">{{ slideCount }}</span>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</template>
|
|
253
|
+
|
|
254
|
+
<script setup lang="ts">
|
|
255
|
+
import { computed, h, defineComponent } from 'vue';
|
|
256
|
+
import { useEditor } from '../../composables/useEditor';
|
|
257
|
+
import { ThemeManager } from '../../core/theme';
|
|
258
|
+
import type { DeckMeta } from '../../core/types';
|
|
259
|
+
import CollapsibleSection from './CollapsibleSection.vue';
|
|
260
|
+
import SliderField from './SliderField.vue';
|
|
261
|
+
import { GOOGLE_FONTS } from '../../core/fonts';
|
|
262
|
+
|
|
263
|
+
const editor = useEditor();
|
|
264
|
+
|
|
265
|
+
const deckMeta = computed(() => editor.store.state.deck?.meta);
|
|
266
|
+
const slideCount = computed(() => editor.store.state.deck?.slides?.length || 0);
|
|
267
|
+
const currentTheme = computed(() => editor.store.state.deck?.meta?.theme || editor.store.state.deck?.theme || 'default');
|
|
268
|
+
|
|
269
|
+
// Theme presets
|
|
270
|
+
const themePresets = [
|
|
271
|
+
{ id: 'default', name: 'Default', primary: '#3b82f6', secondary: '#8b5cf6', bg: '#030303' },
|
|
272
|
+
{ id: 'dark', name: 'Dark', primary: '#6366f1', secondary: '#a855f7', bg: '#0a0a0a' },
|
|
273
|
+
{ id: 'midnight', name: 'Midnight', primary: '#3b82f6', secondary: '#06b6d4', bg: '#0f172a' },
|
|
274
|
+
{ id: 'ocean', name: 'Ocean', primary: '#0ea5e9', secondary: '#14b8a6', bg: '#0c4a6e' },
|
|
275
|
+
{ id: 'forest', name: 'Forest', primary: '#22c55e', secondary: '#10b981', bg: '#052e16' },
|
|
276
|
+
{ id: 'sunset', name: 'Sunset', primary: '#f97316', secondary: '#ec4899', bg: '#1c1917' },
|
|
277
|
+
{ id: 'cyber', name: 'Cyber', primary: '#22d3ee', secondary: '#a855f7', bg: '#020617' },
|
|
278
|
+
{ id: 'minimal', name: 'Minimal', primary: '#18181b', secondary: '#3f3f46', bg: '#fafafa' },
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
// Current theme colors
|
|
282
|
+
const themeColors = computed(() => ({
|
|
283
|
+
primary: editor.store.state.deck?.meta?.colors?.primary || '#3b82f6',
|
|
284
|
+
secondary: editor.store.state.deck?.meta?.colors?.secondary || '#8b5cf6',
|
|
285
|
+
accent: editor.store.state.deck?.meta?.colors?.accent || '#06b6d4',
|
|
286
|
+
background: editor.store.state.deck?.meta?.colors?.background || '#030303',
|
|
287
|
+
surface: editor.store.state.deck?.meta?.colors?.surface || '#0a0a0a',
|
|
288
|
+
text: editor.store.state.deck?.meta?.colors?.text || '#ffffff',
|
|
289
|
+
muted: editor.store.state.deck?.meta?.colors?.muted || '#9ca3af',
|
|
290
|
+
success: editor.store.state.deck?.meta?.colors?.success || '#10b981',
|
|
291
|
+
warning: editor.store.state.deck?.meta?.colors?.warning || '#f59e0b',
|
|
292
|
+
danger: editor.store.state.deck?.meta?.colors?.danger || '#ef4444',
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
// Typography settings
|
|
296
|
+
const typographySettings = computed(() => ({
|
|
297
|
+
heading: editor.store.state.deck?.meta?.typography?.fontFamily?.heading || 'Inter, system-ui, sans-serif',
|
|
298
|
+
body: editor.store.state.deck?.meta?.typography?.fontFamily?.body || 'Inter, system-ui, sans-serif',
|
|
299
|
+
}));
|
|
300
|
+
|
|
301
|
+
// Components settings
|
|
302
|
+
const componentsSettings = computed(() => ({
|
|
303
|
+
buttonRadius: editor.store.state.deck?.meta?.components?.buttonRadius || '9999px',
|
|
304
|
+
cardRadius: editor.store.state.deck?.meta?.components?.cardRadius || '1.5rem',
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
// Spacing settings
|
|
308
|
+
const spacingTokens = ['xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl'];
|
|
309
|
+
const themeSpacing = computed(() => {
|
|
310
|
+
const defaults = { xs: '0.25rem', sm: '0.5rem', md: '1rem', lg: '1.5rem', xl: '2rem', '2xl': '3rem', '3xl': '4rem', '4xl': '6rem' };
|
|
311
|
+
const saved = editor.store.state.deck?.meta?.spacing || {};
|
|
312
|
+
// @ts-ignore
|
|
313
|
+
return Object.fromEntries(spacingTokens.map(k => [k, saved[k] || defaults[k]]));
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Radius settings
|
|
317
|
+
const radiusTokens = ['sm', 'md', 'lg', 'xl', '2xl', '3xl', 'full'];
|
|
318
|
+
const themeRadius = computed(() => {
|
|
319
|
+
const defaults = { sm: '0.125rem', md: '0.375rem', lg: '0.5rem', xl: '0.75rem', '2xl': '1rem', '3xl': '1.5rem', full: '9999px' };
|
|
320
|
+
const saved = editor.store.state.deck?.meta?.borderRadius || {};
|
|
321
|
+
// @ts-ignore
|
|
322
|
+
return Object.fromEntries(radiusTokens.map(k => [k, saved[k] || defaults[k]]));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Effects settings
|
|
326
|
+
const effectsSettings = computed(() => ({
|
|
327
|
+
animations: editor.store.state.deck?.meta?.effects?.animationsEnabled ?? true,
|
|
328
|
+
animationType: editor.store.state.deck?.meta?.effects?.animationType || 'cascade',
|
|
329
|
+
animationDuration: editor.store.state.deck?.meta?.effects?.animationDuration ?? 0.8,
|
|
330
|
+
animationStagger: editor.store.state.deck?.meta?.effects?.animationStagger ?? 0.1,
|
|
331
|
+
animationEase: editor.store.state.deck?.meta?.effects?.animationEase || 'power3.out',
|
|
332
|
+
shadows: editor.store.state.deck?.meta?.effects?.useShadows ?? true,
|
|
333
|
+
glass: editor.store.state.deck?.meta?.effects?.useGlass ?? true,
|
|
334
|
+
glassOpacity: editor.store.state.deck?.meta?.effects?.glassOpacity ?? 0.12,
|
|
335
|
+
glassBlur: editor.store.state.deck?.meta?.effects?.glassBlur ?? '20px',
|
|
336
|
+
orb: editor.store.state.deck?.meta?.effects?.useOrb ?? true,
|
|
337
|
+
orbOpacity: editor.store.state.deck?.meta?.effects?.orbOpacity ?? 0.2,
|
|
338
|
+
orbBlur: editor.store.state.deck?.meta?.effects?.orbBlur ?? '120px',
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
// Font groupings
|
|
342
|
+
const fontGroups = computed(() => {
|
|
343
|
+
const groups = {
|
|
344
|
+
'sans-serif': { label: 'Sans Serif', options: [] as any[] },
|
|
345
|
+
'serif': { label: 'Serif', options: [] as any[] },
|
|
346
|
+
'display': { label: 'Display', options: [] as any[] },
|
|
347
|
+
'handwriting': { label: 'Handwriting', options: [] as any[] },
|
|
348
|
+
'monospace': { label: 'Monospace', options: [] as any[] }
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
GOOGLE_FONTS.forEach(font => {
|
|
352
|
+
// @ts-ignore
|
|
353
|
+
if (groups[font.category]) {
|
|
354
|
+
// @ts-ignore
|
|
355
|
+
groups[font.category].options.push({
|
|
356
|
+
value: `${font.family}, ${font.category}`,
|
|
357
|
+
label: font.label
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Add fallback system fonts
|
|
363
|
+
const systemGroup = {
|
|
364
|
+
label: 'System', options: [
|
|
365
|
+
{ value: 'Inter, system-ui, sans-serif', label: 'Inter (Default)' },
|
|
366
|
+
{ value: 'system-ui, sans-serif', label: 'System UI' }
|
|
367
|
+
]
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
return { ...groups, system: systemGroup };
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
374
|
+
|
|
375
|
+
const commitUpdate = () => {
|
|
376
|
+
// Helper to debounce updates
|
|
377
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
378
|
+
debounceTimer = setTimeout(() => {
|
|
379
|
+
editor.commit();
|
|
380
|
+
// Re-inject theme to sync changes
|
|
381
|
+
// Use the current theme preset as the base, and apply overrides on top
|
|
382
|
+
const meta: Partial<DeckMeta> = editor.store.state.deck?.meta || {};
|
|
383
|
+
const config = {
|
|
384
|
+
colors: meta.colors,
|
|
385
|
+
// Deep merge logic is handled in ThemeManager, but we pass what we have
|
|
386
|
+
typography: meta.typography,
|
|
387
|
+
spacing: meta.spacing,
|
|
388
|
+
borderRadius: meta.borderRadius,
|
|
389
|
+
effects: meta.effects,
|
|
390
|
+
components: meta.components
|
|
391
|
+
};
|
|
392
|
+
// Fix: Pass the current theme ID so we don't reset to Default logic
|
|
393
|
+
ThemeManager.inject(currentTheme.value, config);
|
|
394
|
+
}, 150);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const updateMeta = (key: string, value: any) => {
|
|
398
|
+
editor.store.updateNode(`meta.${key}`, value);
|
|
399
|
+
commitUpdate();
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const applyPreset = (presetId: string) => {
|
|
403
|
+
// 1. Set the new theme ID
|
|
404
|
+
editor.store.updateNode('meta.theme', presetId);
|
|
405
|
+
|
|
406
|
+
// 2. Clear ALL custom overrides to ensure a fresh start with the new preset
|
|
407
|
+
// We explicitly set them to undefined to remove the keys from the store object via updateNode behavior
|
|
408
|
+
// or just set empty objects if the store expects that. Assuming updateNode handles undefined/null as "unset".
|
|
409
|
+
// Based on usage, clearing the objects is safer.
|
|
410
|
+
editor.store.updateNode('meta.colors', undefined);
|
|
411
|
+
editor.store.updateNode('meta.typography', undefined);
|
|
412
|
+
editor.store.updateNode('meta.spacing', undefined);
|
|
413
|
+
editor.store.updateNode('meta.borderRadius', undefined);
|
|
414
|
+
editor.store.updateNode('meta.components', undefined);
|
|
415
|
+
// For effects, we might want to keep some global toggles, but generally presets imply a full look change.
|
|
416
|
+
// Let's reset effects too to match the preset's defaults.
|
|
417
|
+
editor.store.updateNode('meta.effects', undefined);
|
|
418
|
+
|
|
419
|
+
// 3. Inject the clean preset
|
|
420
|
+
ThemeManager.inject(presetId);
|
|
421
|
+
editor.commit();
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const updateThemeColor = (field: string, value: string) => {
|
|
425
|
+
editor.store.updateNode(`meta.colors.${field}`, value);
|
|
426
|
+
commitUpdate();
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const updateSpacing = (key: string, value: string) => {
|
|
430
|
+
editor.store.updateNode(`meta.spacing.${key}`, value);
|
|
431
|
+
commitUpdate();
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const updateRadius = (key: string, value: string) => {
|
|
435
|
+
editor.store.updateNode(`meta.borderRadius.${key}`, value);
|
|
436
|
+
commitUpdate();
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const updateTypography = (key: string, value: string) => {
|
|
440
|
+
editor.store.updateNode(`meta.typography.fontFamily.${key}`, value);
|
|
441
|
+
commitUpdate();
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const updateEffect = (key: string, value: any) => {
|
|
445
|
+
editor.store.updateNode(`meta.effects.${key}`, value);
|
|
446
|
+
commitUpdate();
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const updateComponent = (key: string, value: any) => {
|
|
450
|
+
editor.store.updateNode(`meta.components.${key}`, value);
|
|
451
|
+
commitUpdate();
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// Color Field Component
|
|
455
|
+
const ColorField = defineComponent({
|
|
456
|
+
props: ['label', 'field', 'value'],
|
|
457
|
+
emits: ['update'],
|
|
458
|
+
setup(props, { emit }) {
|
|
459
|
+
return () => h('div', { class: 'flex items-center gap-2' }, [
|
|
460
|
+
h('input', {
|
|
461
|
+
type: 'color',
|
|
462
|
+
value: props.value,
|
|
463
|
+
onInput: (e: Event) => emit('update', props.field, (e.target as HTMLInputElement).value),
|
|
464
|
+
class: 'w-6 h-6 rounded border border-[#333] cursor-pointer bg-transparent'
|
|
465
|
+
}),
|
|
466
|
+
h('span', { class: 'flex-1 text-white/70' }, props.label),
|
|
467
|
+
h('input', {
|
|
468
|
+
type: 'text',
|
|
469
|
+
value: props.value,
|
|
470
|
+
onInput: (e: Event) => emit('update', props.field, (e.target as HTMLInputElement).value),
|
|
471
|
+
class: 'w-16 bg-[#1a1a1a] border border-[#333] rounded px-1.5 py-0.5 text-[10px] font-mono text-white/70'
|
|
472
|
+
})
|
|
473
|
+
]);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Toggle Field Component
|
|
478
|
+
const ToggleField = defineComponent({
|
|
479
|
+
props: ['label', 'value'],
|
|
480
|
+
emits: ['update'],
|
|
481
|
+
setup(props, { emit }) {
|
|
482
|
+
return () => h('div', { class: 'flex items-center justify-between' }, [
|
|
483
|
+
h('span', { class: 'text-white/70' }, props.label),
|
|
484
|
+
h('button', {
|
|
485
|
+
onClick: () => emit('update', !props.value),
|
|
486
|
+
class: `w-8 h-4 rounded-full transition-colors relative ${props.value ? 'bg-blue-600' : 'bg-[#333]'}`
|
|
487
|
+
}, [
|
|
488
|
+
h('span', {
|
|
489
|
+
class: `absolute top-0.5 w-3 h-3 rounded-full bg-white transition-transform ${props.value ? 'left-4.5 translate-x-1' : 'left-0.5'}`
|
|
490
|
+
})
|
|
491
|
+
])
|
|
492
|
+
]);
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
</script>
|
|
496
|
+
|
|
497
|
+
<style scoped>
|
|
498
|
+
.custom-scrollbar::-webkit-scrollbar {
|
|
499
|
+
width: 4px;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.custom-scrollbar::-webkit-scrollbar-track {
|
|
503
|
+
background: transparent;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
507
|
+
background: #333;
|
|
508
|
+
border-radius: 2px;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
512
|
+
background: #555;
|
|
513
|
+
}
|
|
514
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-64 bg-[#111] border-r border-[#333] flex flex-col">
|
|
3
|
+
<!-- Tabs -->
|
|
4
|
+
<div class="flex border-b border-[#333]">
|
|
5
|
+
<button @click="activeTab = 'elements'" class="flex-1 px-3 py-2 text-xs font-bold uppercase transition"
|
|
6
|
+
:class="activeTab === 'elements' ? 'text-white bg-[#1a1a1a]' : 'text-white/50 hover:text-white'">
|
|
7
|
+
<i class="ph-thin ph-tree-structure mr-1"></i> Elements
|
|
8
|
+
</button>
|
|
9
|
+
<button @click="activeTab = 'settings'" class="flex-1 px-3 py-2 text-xs font-bold uppercase transition"
|
|
10
|
+
:class="activeTab === 'settings' ? 'text-white bg-[#1a1a1a]' : 'text-white/50 hover:text-white'">
|
|
11
|
+
<i class="ph-thin ph-gear mr-1"></i> Settings
|
|
12
|
+
</button>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<!-- Panel Content -->
|
|
16
|
+
<div class="flex-1 overflow-hidden">
|
|
17
|
+
<StudioLayers v-if="activeTab === 'elements'" />
|
|
18
|
+
<StudioSettings v-else />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { ref } from 'vue';
|
|
25
|
+
import StudioLayers from './StudioLayers.vue';
|
|
26
|
+
import StudioSettings from './StudioSettings.vue';
|
|
27
|
+
|
|
28
|
+
const activeTab = ref<'elements' | 'settings'>('elements');
|
|
29
|
+
</script>
|