lumina-slides 8.9.4 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LUMINA_LLM_EXAMPLES.json +234 -0
  2. package/README.md +18 -18
  3. package/dist/lumina-slides.js +13207 -12659
  4. package/dist/lumina-slides.umd.cjs +215 -215
  5. package/dist/style.css +1 -1
  6. package/package.json +5 -4
  7. package/src/App.vue +16 -0
  8. package/src/animation/index.ts +11 -0
  9. package/src/animation/registry.ts +126 -0
  10. package/src/animation/stagger.ts +95 -0
  11. package/src/animation/types.ts +53 -0
  12. package/src/components/LandingPage.vue +229 -0
  13. package/src/components/LuminaDeck.vue +224 -0
  14. package/src/components/LuminaSpeakerNotes.vue +701 -0
  15. package/src/components/base/BaseSlide.vue +122 -0
  16. package/src/components/base/LuminaElement.vue +67 -0
  17. package/src/components/base/VideoPlayer.vue +204 -0
  18. package/src/components/layouts/LayoutAuto.vue +71 -0
  19. package/src/components/layouts/LayoutChart.vue +287 -0
  20. package/src/components/layouts/LayoutCustom.vue +92 -0
  21. package/src/components/layouts/LayoutDiagram.vue +253 -0
  22. package/src/components/layouts/LayoutFeatures.vue +121 -0
  23. package/src/components/layouts/LayoutFlex.vue +172 -0
  24. package/src/components/layouts/LayoutFree.vue +62 -0
  25. package/src/components/layouts/LayoutHalf.vue +127 -0
  26. package/src/components/layouts/LayoutStatement.vue +74 -0
  27. package/src/components/layouts/LayoutSteps.vue +106 -0
  28. package/src/components/layouts/LayoutTimeline.vue +104 -0
  29. package/src/components/layouts/LayoutVideo.vue +41 -0
  30. package/src/components/parts/FlexBullets.vue +45 -0
  31. package/src/components/parts/FlexButton.vue +132 -0
  32. package/src/components/parts/FlexImage.vue +54 -0
  33. package/src/components/parts/FlexOrdered.vue +44 -0
  34. package/src/components/parts/FlexSpacer.vue +13 -0
  35. package/src/components/parts/FlexStepper.vue +59 -0
  36. package/src/components/parts/FlexText.vue +29 -0
  37. package/src/components/parts/FlexTimeline.vue +67 -0
  38. package/src/components/parts/FlexTitle.vue +39 -0
  39. package/src/components/parts/LuminaBackground.vue +100 -0
  40. package/src/components/site/LivePreview.vue +101 -0
  41. package/src/components/site/SiteApi.vue +301 -0
  42. package/src/components/site/SiteDashboard.vue +604 -0
  43. package/src/components/site/SiteDocs.vue +3267 -0
  44. package/src/components/site/SiteExamples.vue +65 -0
  45. package/src/components/site/SiteFooter.vue +6 -0
  46. package/src/components/site/SiteHome.vue +362 -0
  47. package/src/components/site/SiteNavBar.vue +122 -0
  48. package/src/components/site/SitePlayground.vue +389 -0
  49. package/src/components/site/SitePromptBuilder.vue +266 -0
  50. package/src/components/site/SiteUserMenu.vue +90 -0
  51. package/src/components/studio/ActionEditor.vue +108 -0
  52. package/src/components/studio/ArrayEditor.vue +124 -0
  53. package/src/components/studio/CollapsibleSection.vue +33 -0
  54. package/src/components/studio/ColorField.vue +22 -0
  55. package/src/components/studio/EditorCanvas.vue +326 -0
  56. package/src/components/studio/EditorLayoutFeatures.vue +18 -0
  57. package/src/components/studio/EditorLayoutFixed.vue +46 -0
  58. package/src/components/studio/EditorLayoutFlex.vue +133 -0
  59. package/src/components/studio/EditorLayoutHalf.vue +18 -0
  60. package/src/components/studio/EditorLayoutStatement.vue +18 -0
  61. package/src/components/studio/EditorLayoutSteps.vue +18 -0
  62. package/src/components/studio/EditorLayoutTimeline.vue +18 -0
  63. package/src/components/studio/EditorNode.vue +89 -0
  64. package/src/components/studio/FieldEditor.vue +133 -0
  65. package/src/components/studio/IconPicker.vue +109 -0
  66. package/src/components/studio/LayerItem.vue +117 -0
  67. package/src/components/studio/LuminaStudio.vue +30 -0
  68. package/src/components/studio/SaveSuccessModal.vue +138 -0
  69. package/src/components/studio/SlideNavigator.vue +373 -0
  70. package/src/components/studio/SliderField.vue +44 -0
  71. package/src/components/studio/StudioInspector.vue +595 -0
  72. package/src/components/studio/StudioJsonEditor.vue +191 -0
  73. package/src/components/studio/StudioLayers.vue +145 -0
  74. package/src/components/studio/StudioSettings.vue +514 -0
  75. package/src/components/studio/StudioSidebar.vue +29 -0
  76. package/src/components/studio/StudioToolbar.vue +222 -0
  77. package/src/components/studio/fieldLabels.ts +224 -0
  78. package/src/components/studio/inspectors/DiagramEdgeEditor.vue +77 -0
  79. package/src/components/studio/inspectors/DiagramNodeEditor.vue +117 -0
  80. package/src/components/studio/nodes/StudioDiagramNode.vue +138 -0
  81. package/src/composables/useAuth.ts +87 -0
  82. package/src/composables/useEditor.ts +224 -0
  83. package/src/composables/useElementState.ts +81 -0
  84. package/src/composables/useFlexLayout.ts +122 -0
  85. package/src/composables/useKeyboard.ts +45 -0
  86. package/src/composables/useLumina.ts +32 -0
  87. package/src/composables/useStudio.ts +87 -0
  88. package/src/composables/useSwipeNav.ts +53 -0
  89. package/src/composables/useTransition.ts +373 -0
  90. package/src/core/Lumina.ts +819 -0
  91. package/src/core/animationConfig.ts +251 -0
  92. package/src/core/compression.ts +34 -0
  93. package/src/core/elementController.ts +170 -0
  94. package/src/core/elementId.ts +27 -0
  95. package/src/core/elementResolver.ts +207 -0
  96. package/src/core/events.ts +53 -0
  97. package/src/core/fonts.ts +100 -0
  98. package/src/core/presets.ts +231 -0
  99. package/src/core/prompts.ts +272 -0
  100. package/src/core/schema.ts +478 -0
  101. package/src/core/speaker-channel.ts +250 -0
  102. package/src/core/store.ts +461 -0
  103. package/src/core/theme.ts +666 -0
  104. package/src/core/types.ts +1611 -0
  105. package/src/directives/vStudio.ts +45 -0
  106. package/src/index.ts +175 -0
  107. package/src/main.ts +17 -0
  108. package/src/router/index.ts +92 -0
  109. package/src/style/main.css +462 -0
  110. package/src/utils/deep.ts +127 -0
  111. package/src/utils/firebase.ts +184 -0
  112. package/src/utils/streaming.ts +134 -0
  113. package/src/views/DashboardView.vue +32 -0
  114. package/src/views/DeckView.vue +289 -0
  115. package/src/views/HomeView.vue +17 -0
  116. package/src/views/SiteLayout.vue +21 -0
  117. package/src/views/StudioView.vue +61 -0
  118. package/src/vite-env.d.ts +6 -0
  119. package/IMPLEMENTATION.md +0 -418
@@ -0,0 +1,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>