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,604 @@
1
+ <template>
2
+ <!-- Ambient Background Glow -->
3
+ <div class="fixed inset-0 pointer-events-none overflow-hidden z-0">
4
+ <div
5
+ class="absolute -top-[20%] -left-[10%] w-[60%] h-[60%] bg-blue-600/5 blur-[120px] rounded-full animate-pulse-soft">
6
+ </div>
7
+ <div class="absolute -bottom-[20%] -right-[10%] w-[50%] h-[50%] bg-indigo-600/5 blur-[120px] rounded-full animate-pulse-soft"
8
+ style="animation-delay: 1.5s"></div>
9
+ </div>
10
+
11
+ <div class="min-h-screen pt-32 px-8 max-w-7xl mx-auto relative z-10">
12
+ <!-- Header Section (Clean Premium) -->
13
+ <div class="flex flex-col md:flex-row md:items-end justify-between mb-20 gap-8 relative">
14
+ <div>
15
+ <div class="flex items-center gap-3 mb-6">
16
+ <div
17
+ class="w-10 h-10 rounded-2xl bg-blue-600/20 flex items-center justify-center text-blue-500 border border-blue-500/10">
18
+ <i class="ph-thin ph-stack text-sm"></i>
19
+ </div>
20
+ <span
21
+ class="text-[11px] font-black text-blue-500 uppercase tracking-[0.4em] leading-none">Workspace</span>
22
+ </div>
23
+ <h2 class="text-6xl font-[800] tracking-[-0.04em] text-white mb-6 leading-[0.9]">
24
+ My Presentations
25
+ </h2>
26
+ <p class="text-white/40 text-xl font-medium max-w-xl leading-relaxed tracking-tight">Design, present,
27
+ and collaborate
28
+ with Lumina Engine.</p>
29
+ </div>
30
+
31
+ <div class="relative">
32
+ <!-- Create Trigger (High End) -->
33
+ <button v-if="!isCreating" @click="isCreating = true"
34
+ class="group relative px-10 py-5 rounded-[2rem] bg-white text-black font-black hover:scale-105 active:scale-95 transition-all duration-500 flex items-center gap-4 shadow-[0_30px_60px_-15px_rgba(255,255,255,0.3)] overflow-hidden">
35
+ <div
36
+ class="absolute inset-0 bg-gradient-to-tr from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000">
37
+ </div>
38
+ <i class="ph-thin ph-plus-circle text-xl"></i>
39
+ <span class="tracking-tight text-lg">New Presentation</span>
40
+ </button>
41
+
42
+ <!-- Premium Popover Form -->
43
+ <Transition name="spring-pop">
44
+ <div v-if="isCreating"
45
+ class="absolute right-0 top-0 w-[420px] bg-[#0d0d0d]/90 border border-white/10 rounded-[3rem] p-10 shadow-[0_50px_100px_rgba(0,0,0,0.9)] z-[100] backdrop-blur-3xl">
46
+
47
+ <!-- Header -->
48
+ <div class="flex items-center justify-between mb-10">
49
+ <div>
50
+ <h4 class="text-2xl font-black tracking-tight uppercase mb-1">New Presentation</h4>
51
+ <div class="h-1 w-12 bg-blue-600 rounded-full"></div>
52
+ </div>
53
+ <button @click="isCreating = false"
54
+ class="w-10 h-10 rounded-full bg-white/5 flex items-center justify-center text-white/20 hover:text-white hover:bg-white/10 transition">
55
+ <i class="ph-thin ph-x"></i>
56
+ </button>
57
+ </div>
58
+
59
+ <div class="space-y-8">
60
+ <div class="group/field">
61
+ <div class="flex justify-between items-end mb-3 px-1">
62
+ <label
63
+ class="text-[10px] font-black text-white/20 uppercase tracking-[0.3em] group-focus-within/field:text-blue-500 transition">Display
64
+ Title</label>
65
+ <span class="text-[9px] text-white/10 font-bold uppercase">Required</span>
66
+ </div>
67
+ <div class="relative">
68
+ <input v-model="newDeckForm.title" type="text"
69
+ class="w-full bg-white/[0.03] border border-white/5 rounded-2xl px-6 py-5 text-lg font-bold focus:border-blue-500 focus:bg-white/5 outline-none transition-all duration-500 shadow-inner"
70
+ @keyup.enter="createNewDeck" />
71
+ <Transition name="placeholder-slide">
72
+ <span v-if="!newDeckForm.title" :key="currentTitlePlaceholder"
73
+ class="absolute left-6 top-1/2 -translate-y-1/2 text-lg font-bold text-white/10 pointer-events-none select-none">
74
+ {{ currentTitlePlaceholder }}
75
+ </span>
76
+ </Transition>
77
+ </div>
78
+ </div>
79
+
80
+ <div class="group/field">
81
+ <label
82
+ class="block text-[10px] font-black text-white/20 uppercase tracking-[0.3em] mb-3 px-1">Summary</label>
83
+ <div class="relative">
84
+ <textarea v-model="newDeckForm.description" rows="3"
85
+ class="w-full bg-white/[0.03] border border-white/5 rounded-2xl px-6 py-5 text-sm font-medium focus:border-blue-500 focus:bg-white/5 outline-none transition-all duration-500 resize-none shadow-inner"></textarea>
86
+ <Transition name="placeholder-slide">
87
+ <span v-if="!newDeckForm.description" :key="currentDescPlaceholder"
88
+ class="absolute left-6 top-5 text-sm font-medium text-white/10 pointer-events-none select-none max-w-[calc(100%-3rem)]">
89
+ {{ currentDescPlaceholder }}
90
+ </span>
91
+ </Transition>
92
+ </div>
93
+ </div>
94
+
95
+ <button @click="createNewDeck" :disabled="!newDeckForm.title || creating"
96
+ class="w-full py-6 rounded-[2rem] bg-blue-600 hover:bg-blue-500 disabled:opacity-20 disabled:grayscale text-white font-black text-lg transition-all duration-500 shadow-[0_20px_40px_-10px_rgba(37,99,235,0.4)] active:scale-[0.98] flex items-center justify-center gap-3">
97
+ <i v-if="!creating" class="ph-thin ph-plus-circle"></i>
98
+ <span v-if="!creating">Create Presentation</span>
99
+ <span v-else class="flex items-center justify-center gap-2">
100
+ <i class="ph-thin ph-spinner ph-spin"></i> Finalizing...
101
+ </span>
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </Transition>
106
+ </div>
107
+ </div>
108
+
109
+ <!-- States -->
110
+ <div v-if="loading" class="flex flex-col items-center justify-center py-32 space-y-6">
111
+ <div class="relative w-16 h-16">
112
+ <div class="absolute inset-0 rounded-2xl border-2 border-white/5"></div>
113
+ <div class="absolute inset-0 rounded-2xl border-t-2 border-blue-500 animate-spin"></div>
114
+ </div>
115
+ <p class="text-white/30 font-medium tracking-wide">Syncing Workspace...</p>
116
+ </div>
117
+
118
+ <div v-else-if="error"
119
+ class="max-w-md mx-auto bg-red-500/5 border border-red-500/20 rounded-3xl p-10 text-center">
120
+ <div class="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center mx-auto mb-6">
121
+ <i class="ph-thin ph-warning text-2xl text-red-500"></i>
122
+ </div>
123
+ <h3 class="text-xl font-bold mb-2">Sync Error</h3>
124
+ <p class="text-white/40 mb-8 text-sm leading-relaxed">{{ error }}</p>
125
+ <button @click="fetchDecks"
126
+ class="px-8 py-3 bg-white/10 hover:bg-white/20 rounded-xl text-xs font-black uppercase tracking-widest transition-all">
127
+ Reconnect
128
+ </button>
129
+ </div>
130
+
131
+ <div v-else-if="decks.length === 0"
132
+ class="flex flex-col items-center py-32 bg-white/[0.02] rounded-[3rem] border border-dashed border-white/10">
133
+ <div
134
+ class="w-24 h-24 rounded-[2rem] bg-gradient-to-br from-white/10 to-transparent flex items-center justify-center mb-8 text-4xl shadow-2xl">
135
+
136
+ </div>
137
+ <h3 class="text-2xl font-bold mb-3">Your canvas is empty</h3>
138
+ <p class="text-white/30 mb-10 max-w-sm text-center leading-relaxed">Start your next visual journey. Create a
139
+ deck to begin designing with Lumina Engine.</p>
140
+ <button @click="isCreating = true"
141
+ class="text-blue-400 font-extrabold hover:text-blue-300 transition-all flex items-center gap-2 group">
142
+ Create First Presentation <i
143
+ class="ph-thin ph-arrow-right group-hover:translate-x-2 transition-transform"></i>
144
+ </button>
145
+ </div>
146
+
147
+ <!-- Deck Grid (Premium Stability) -->
148
+ <div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12 pb-20">
149
+ <div v-for="deck in decks" :key="deck.meta?.id"
150
+ class="group relative bg-[#0a0a0a] rounded-[2.5rem] border border-white/5 hover:border-white/10 transition-all duration-500 shadow-[0_30px_60px_-15px_rgba(0,0,0,0.5)] hover:shadow-[0_50px_100px_-20px_rgba(0,0,0,0.7)] flex flex-col overflow-hidden"
151
+ :style="getCardStyle(deck)">
152
+
153
+ <!-- Physical Inner Border -->
154
+ <div class="absolute inset-0 pointer-events-none rounded-[2.5rem] border border-white/[0.03] z-50">
155
+ </div>
156
+
157
+ <!-- Thumbnail Area (Abstract Identity) -->
158
+ <div class="relative aspect-[16/10] overflow-hidden">
159
+ <!-- Dynamic Mesh Orb System -->
160
+ <div class="absolute inset-0 opacity-40 mix-blend-screen group-hover:scale-110 transition-transform duration-1000"
161
+ :style="getMeshStyle(deck)"></div>
162
+ <div
163
+ class="absolute inset-0 bg-gradient-to-br from-black/0 via-black/20 to-black/60 pointer-events-none">
164
+ </div>
165
+
166
+ <!-- Ambient Logo Watermark -->
167
+ <div
168
+ class="absolute inset-0 flex items-center justify-center z-10 opacity-10 group-hover:opacity-20 transition-opacity">
169
+ <i class="ph-thin ph-shapes text-8xl text-white"></i>
170
+ </div>
171
+
172
+ <!-- Centered Actions on Hover -->
173
+ <div
174
+ class="absolute inset-0 z-30 flex items-center justify-center gap-6 bg-black/80 backdrop-blur-xl opacity-0 group-hover:opacity-100 transition-all duration-500">
175
+ <button @click.stop="router.push({ name: 'studio', params: { id: deck.meta?.id } })"
176
+ class="w-20 h-20 rounded-3xl bg-white text-black hover:scale-110 active:scale-90 transition-all flex flex-col items-center justify-center shadow-2xl group/btn"
177
+ title="Open Studio">
178
+ <i class="ph-thin ph-pencil-simple text-2xl mb-1.5"></i>
179
+ <span class="text-[9px] font-black uppercase tracking-widest">Studio</span>
180
+ </button>
181
+ <button @click.stop="openDeckInNewTab(deck.meta?.id)"
182
+ class="w-20 h-20 rounded-3xl text-white hover:scale-110 active:scale-95 transition-all flex flex-col items-center justify-center shadow-2xl group/play"
183
+ :style="`background: ${getThemeColor(deck)}`" title="Start Presentation">
184
+ <i class="ph-thin ph-play text-2xl mb-1.5 ml-1"></i>
185
+ <span class="text-[9px] font-black uppercase tracking-widest">Play</span>
186
+ </button>
187
+ </div>
188
+
189
+ <!-- Slide Count Badge -->
190
+ <div class="absolute top-6 left-6 z-20">
191
+ <div
192
+ class="px-3 py-1.5 rounded-xl bg-black/60 backdrop-blur-md border border-white/10 flex items-center gap-2">
193
+ <div class="w-1.5 h-1.5 rounded-full" :style="`background: ${getThemeColor(deck)}`"></div>
194
+ <span class="text-[10px] font-black uppercase tracking-widest text-white/50">
195
+ {{ deck.slides?.length || 0 }} {{ (deck.slides?.length === 1) ? 'Slide' : 'Slides' }}
196
+ </span>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ <!-- Info Section (Accessible Actions) -->
202
+ <div class="p-10 flex-1 flex flex-col relative z-20">
203
+ <div class="flex items-start justify-between mb-5">
204
+ <div class="flex-1 min-w-0 pr-6"
205
+ @click="router.push({ name: 'studio', params: { id: deck.meta?.id } })">
206
+ <h3
207
+ class="text-2xl font-extrabold text-white truncate hover:text-blue-500 transition-colors cursor-pointer tracking-tight leading-tight">
208
+ {{ deck.meta?.title || 'Untitled' }}
209
+ </h3>
210
+ <p
211
+ class="text-sm text-white/30 line-clamp-2 mt-3 leading-relaxed h-11 font-medium tracking-tight">
212
+ {{ deck.meta?.description || 'No summary provided for this project.' }}
213
+ </p>
214
+ </div>
215
+
216
+ <!-- Visible Delete Button -->
217
+ <button @click.stop="confirmDelete(deck.meta?.id)"
218
+ class="w-10 h-10 rounded-2xl flex items-center justify-center text-white/10 hover:text-red-500 hover:bg-red-500/10 transition-all duration-300 bg-white/5 border border-white/5"
219
+ title="Delete Presentation">
220
+ <i class="ph-thin ph-trash text-sm"></i>
221
+ </button>
222
+ </div>
223
+
224
+ <div class="mt-auto pt-8 border-t border-white/[0.03] flex items-center justify-between">
225
+ <div class="flex items-center gap-4">
226
+ <div
227
+ class="w-10 h-10 rounded-2xl bg-white/5 border border-white/5 flex items-center justify-center text-[11px] font-black text-white/30 uppercase tracking-tighter">
228
+ {{ deck.meta?.authorName ? deck.meta.authorName.charAt(0) : 'L' }}
229
+ </div>
230
+ <div class="flex flex-col">
231
+ <span
232
+ class="text-[9px] text-white/10 font-black uppercase tracking-[0.3em] mb-0.5">Updated</span>
233
+ <span class="text-xs text-white/40 font-bold tracking-tight">{{
234
+ formatDate(deck.meta?.updatedAt || deck.meta?.savedAt) }}</span>
235
+ </div>
236
+ </div>
237
+
238
+ <div class="flex items-center gap-2.5">
239
+ <div class="w-2 h-2 rounded-full"
240
+ :class="deck.meta?.isPublic ? 'bg-indigo-500 shadow-[0_0_15px_rgba(99,102,241,0.5)]' : 'bg-white/5'">
241
+ </div>
242
+ <div class="flex flex-col items-end">
243
+ <span class="text-[8px] text-white/10 font-bold uppercase tracking-widest">{{
244
+ getReadingTime(deck) }} min read</span>
245
+ <span class="text-[10px] font-black text-white/20 uppercase tracking-widest">v{{
246
+ deck.meta.version || '1.0' }}</span>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ </div>
251
+
252
+ <!-- Delete Overlay (Premium & Authoritative) -->
253
+ <Transition name="fade-scale">
254
+ <div v-if="deletingDeckId === deck.meta?.id"
255
+ class="absolute inset-0 bg-[#0d0d0d] z-[60] flex flex-col items-center justify-center p-12 text-center"
256
+ @click.stop>
257
+ <div
258
+ class="w-20 h-20 rounded-[2.5rem] bg-red-600/10 flex items-center justify-center mb-10 border border-red-600/10 text-red-500">
259
+ <i class="ph-thin ph-trash text-3xl"></i>
260
+ </div>
261
+ <h4 class="text-2xl font-extrabold text-white/95 mb-3 tracking-tight leading-none">Permanently
262
+ Delete?</h4>
263
+ <p class="text-sm text-white/20 mb-12 max-w-[280px] leading-relaxed font-medium tracking-tight">
264
+ This action cannot be undone. All data will be wiped from your workspace.</p>
265
+
266
+ <div class="flex flex-col gap-4 w-full max-w-[260px]">
267
+ <button @click.stop="handleDeleteDeck(deck.meta?.id)"
268
+ class="w-full py-5 rounded-[1.5rem] bg-red-600 text-white font-extrabold text-[13px] uppercase tracking-widest hover:bg-red-500 transition-all shadow-[0_20px_40px_-5px_rgba(220,38,38,0.3)] active:scale-95">
269
+ {{ deleting ? 'Processing...' : 'Delete Permanently' }}
270
+ </button>
271
+ <button @click.stop="deletingDeckId = null"
272
+ class="w-full py-5 rounded-[1.5rem] bg-white/5 text-white font-extrabold text-[13px] uppercase tracking-widest hover:bg-white/10 transition-all border border-white/5 active:scale-95">
273
+ Keep Presentation
274
+ </button>
275
+ </div>
276
+ </div>
277
+ </Transition>
278
+ </div>
279
+ </div>
280
+ </div>
281
+ </template>
282
+
283
+ <script setup lang="ts">
284
+ import { ref, onMounted, watch } from 'vue';
285
+ import { useRouter } from 'vue-router';
286
+ import { useAuth } from '../../composables/useAuth';
287
+ import { getUserDecks, saveDeck, softDeleteDeck } from '../../utils/firebase';
288
+ import type { Deck } from '../../core/types';
289
+
290
+ // const emit = defineEmits(['select-deck']); // No longer used
291
+ const { user } = useAuth();
292
+ const router = useRouter();
293
+
294
+ const decks = ref<Deck[]>([]);
295
+ const loading = ref(true);
296
+ const creating = ref(false); // Action loading state
297
+ const deleting = ref(false); // Global deleting state
298
+ const deletingDeckId = ref<string | null>(null); // Specific deck being confirmed
299
+ const isCreating = ref(false); // UI toggle state
300
+ const error = ref<string | null>(null);
301
+
302
+ const newDeckForm = ref({
303
+ title: '',
304
+ description: ''
305
+ });
306
+
307
+ // Dynamic Placeholder Logic
308
+ const titlePlaceholders = [
309
+ "Quantum Leap into Visual Intelligence",
310
+ "Synchronizing Intuition with Silicon Logic",
311
+ "The Narrative Architecture of Tomorrow",
312
+ "Orchestrating the Singularity of Design",
313
+ "Ethereal Landscapes of Digital Thought",
314
+ "Transcending the Boundaries of Content",
315
+ "A Manifesto on Future Experiences"
316
+ ];
317
+
318
+ const descPlaceholders = [
319
+ "Synthesizing multi-dimensional data into a seamless cognitive flow...",
320
+ "A deep dive into the next generation of visual storytelling.",
321
+ "Mapping the trajectory of decentralized intelligence.",
322
+ "The algorithmic pursuit of aesthetic perfection and clarity.",
323
+ "Redefining the relationship between information and focus.",
324
+ "An exploration of generative systems in the era of liquidity."
325
+ ];
326
+
327
+ const currentTitleIndex = ref(0);
328
+ const currentDescIndex = ref(0);
329
+ const currentTitlePlaceholder = ref(titlePlaceholders[0]);
330
+ const currentDescPlaceholder = ref(descPlaceholders[0]);
331
+ let placeholderInterval: any = null;
332
+
333
+ const rotatePlaceholders = () => {
334
+ currentTitleIndex.value = (currentTitleIndex.value + 1) % titlePlaceholders.length;
335
+ currentDescIndex.value = (currentDescIndex.value + 1) % descPlaceholders.length;
336
+ currentTitlePlaceholder.value = titlePlaceholders[currentTitleIndex.value];
337
+ currentDescPlaceholder.value = descPlaceholders[currentDescIndex.value];
338
+ };
339
+
340
+ watch(isCreating, (val) => {
341
+ if (val) {
342
+ placeholderInterval = setInterval(rotatePlaceholders, 3500);
343
+ } else {
344
+ if (placeholderInterval) clearInterval(placeholderInterval);
345
+ }
346
+ });
347
+
348
+ const confirmDelete = (id?: string) => {
349
+ if (!id) return;
350
+ deletingDeckId.value = id;
351
+ };
352
+
353
+ const handleDeleteDeck = async (id?: string) => {
354
+ if (!id || !user.value) return;
355
+
356
+ deleting.value = true;
357
+ try {
358
+ await softDeleteDeck(id);
359
+ // Refresh local list
360
+ decks.value = decks.value.filter(d => d.meta.id !== id);
361
+ deletingDeckId.value = null;
362
+ } catch (e) {
363
+ console.error("Failed to delete deck:", e);
364
+ error.value = "Failed to delete presentation.";
365
+ } finally {
366
+ deleting.value = false;
367
+ }
368
+ };
369
+
370
+ const fetchDecks = async () => {
371
+ if (!user.value) return;
372
+
373
+ loading.value = true;
374
+ error.value = null;
375
+
376
+ try {
377
+ decks.value = await getUserDecks(user.value.uid);
378
+ } catch (e: any) {
379
+ console.error("Failed to fetch decks:", e);
380
+ error.value = "Failed to load your decks. Please try again.";
381
+ } finally {
382
+ loading.value = false;
383
+ }
384
+ };
385
+
386
+ // Handle automatic fetching when auth is ready
387
+ watch(user, (newUser) => {
388
+ if (newUser) {
389
+ fetchDecks();
390
+ } else {
391
+ // If auth finishes and there's no user, stop loading
392
+ loading.value = false;
393
+ }
394
+ }, { immediate: true });
395
+
396
+ const createNewDeck = async () => {
397
+ if (!user.value || !newDeckForm.value.title) return;
398
+
399
+ creating.value = true;
400
+ error.value = null;
401
+
402
+ // Create a scaffold deck with user values
403
+ const newDeck: Deck = {
404
+ meta: {
405
+ title: newDeckForm.value.title,
406
+ description: newDeckForm.value.description,
407
+ authorId: user.value.uid,
408
+ authorName: user.value.displayName || 'Anonymous',
409
+ createdAt: new Date().toISOString(),
410
+ updatedAt: new Date().toISOString(),
411
+ isPublic: false,
412
+ version: '1.0'
413
+ },
414
+ slides: [
415
+ {
416
+ type: 'statement',
417
+ title: 'Hello World',
418
+ subtitle: 'Welcome to your new presentation.'
419
+ }
420
+ ]
421
+ };
422
+
423
+ try {
424
+ const id = await saveDeck(newDeck, user.value.uid, user.value.displayName || 'User');
425
+ // Reset form
426
+ newDeckForm.value = { title: '', description: '' };
427
+ isCreating.value = false;
428
+
429
+ // Navigate to the new deck studio
430
+ router.push({ name: 'studio', params: { id } });
431
+ } catch (e) {
432
+ console.error("Failed to create deck:", e);
433
+ error.value = "Failed to create new deck.";
434
+ } finally {
435
+ creating.value = false;
436
+ }
437
+ };
438
+
439
+ const formatDate = (dateString?: string) => {
440
+ if (!dateString) return 'Unknown date';
441
+ return new Date(dateString).toLocaleDateString(undefined, {
442
+ year: 'numeric',
443
+ month: 'short',
444
+ day: 'numeric'
445
+ });
446
+ };
447
+
448
+ // --- Theme Extraction Logic ---
449
+ const getThemeColor = (deck: Deck) => {
450
+ // Attempt to extract primary from custom theme object or use defaults
451
+ const theme = deck.theme;
452
+ if (typeof theme === 'object' && theme?.colors?.primary) {
453
+ return theme.colors.primary;
454
+ }
455
+ // Fallback based on deck title hash for consistent randomness if no theme
456
+ const colors = ['#6366f1', '#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981'];
457
+ const title = deck.meta?.title || 'Lumina';
458
+ let hash = 0;
459
+ for (let i = 0; i < title.length; i++) hash = title.charCodeAt(i) + ((hash << 5) - hash);
460
+ return colors[Math.abs(hash) % colors.length];
461
+ };
462
+
463
+ const getMeshStyle = (deck: Deck) => {
464
+ const color = getThemeColor(deck);
465
+ return {
466
+ backgroundImage: `
467
+ radial-gradient(circle at 20% 30%, ${color}66, transparent 70%),
468
+ radial-gradient(circle at 80% 70%, ${color}33, transparent 70%),
469
+ linear-gradient(135deg, #0f172a 0%, #000000 100%)
470
+ `
471
+ };
472
+ };
473
+
474
+ const getCardStyle = (deck: Deck) => {
475
+ const color = getThemeColor(deck);
476
+ return {
477
+ '--accent-glow': `${color}15`
478
+ };
479
+ };
480
+
481
+ // --- Intelligence Logic ---
482
+
483
+ const getSlideIcon = (type?: string) => {
484
+ const types: Record<string, string> = {
485
+ 'statement': 'ph-thin ph-quotes',
486
+ 'hero': 'ph-thin ph-mountains',
487
+ 'grid': 'ph-thin ph-grid-four',
488
+ 'list': 'ph-thin ph-list-bullets',
489
+ 'chart': 'ph-thin ph-chart-line',
490
+ 'image': 'ph-thin ph-image',
491
+ 'quote': 'ph-thin ph-quotes',
492
+ 'code': 'ph-thin ph-code',
493
+ 'feature': 'ph-thin ph-magic-wand'
494
+ };
495
+ return types[type || ''] || 'ph-thin ph-copy';
496
+ };
497
+
498
+ const getFirstSlideLabel = (deck: Deck) => {
499
+ const slide = deck.slides?.[0];
500
+ if (slide?.title) return slide.title.substring(0, 16) + (slide.title.length > 16 ? '...' : '');
501
+ return 'Cover Slide';
502
+ };
503
+
504
+ const getReadingTime = (deck: Deck) => {
505
+ // Highly scientific estimate: 30s per slide
506
+ const count = deck.slides?.length || 0;
507
+ return Math.max(1, Math.ceil(count * 0.5));
508
+ };
509
+
510
+ const openDeckInNewTab = (deckId?: string) => {
511
+ if (!deckId) return;
512
+ const routeData = router.resolve({ name: 'deck', params: { id: deckId } });
513
+ window.open(routeData.href, '_blank');
514
+ };
515
+
516
+ onMounted(() => {
517
+ // Initial fetch if user already available
518
+ if (user.value) fetchDecks();
519
+ });
520
+ </script>
521
+
522
+ <style scoped>
523
+ .spring-pop-enter-active,
524
+ .spring-pop-leave-active {
525
+ transition: all 0.7s cubic-bezier(0.34, 1.56, 0.64, 1);
526
+ }
527
+
528
+ .spring-pop-enter-from {
529
+ opacity: 0;
530
+ transform: translateY(30px) scale(0.9) rotate3d(1, 0, 0, 10deg);
531
+ }
532
+
533
+ .spring-pop-leave-to {
534
+ opacity: 0;
535
+ transform: translateY(20px) scale(0.95);
536
+ }
537
+
538
+ .placeholder-slide-enter-active,
539
+ .placeholder-slide-leave-active {
540
+ transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1);
541
+ }
542
+
543
+ .placeholder-slide-enter-from {
544
+ opacity: 0;
545
+ transform: translateY(10px);
546
+ filter: blur(4px);
547
+ }
548
+
549
+ .placeholder-slide-leave-to {
550
+ opacity: 0;
551
+ transform: translateY(-10px);
552
+ filter: blur(4px);
553
+ }
554
+
555
+ .fade-scale-enter-active,
556
+ .fade-scale-leave-active {
557
+ transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1);
558
+ }
559
+
560
+ .fade-scale-enter-from {
561
+ opacity: 0;
562
+ transform: scale(1.1);
563
+ }
564
+
565
+ .fade-scale-leave-to {
566
+ opacity: 0;
567
+ transform: scale(0.95);
568
+ }
569
+
570
+ .line-clamp-2 {
571
+ display: -webkit-box;
572
+ -webkit-line-clamp: 2;
573
+ -webkit-box-orient: vertical;
574
+ overflow: hidden;
575
+ }
576
+
577
+ /* Glassmorphism utility if not globally available */
578
+ .glass {
579
+ background: rgba(255, 255, 255, 0.03);
580
+ backdrop-filter: blur(25px);
581
+ border: 1px solid rgba(255, 255, 255, 0.05);
582
+ }
583
+
584
+ @keyframes pulse-soft {
585
+
586
+ 0%,
587
+ 100% {
588
+ opacity: 1;
589
+ transform: scale(1);
590
+ }
591
+
592
+ 50% {
593
+ opacity: 0.5;
594
+ transform: scale(0.8);
595
+ }
596
+ }
597
+
598
+
599
+ /* Accent Glow Hover */
600
+ .group:hover {
601
+ box-shadow:
602
+ 0 0 80px -20px var(--accent-glow);
603
+ }
604
+ </style>