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,595 @@
1
+ <template>
2
+ <div class="flex flex-col h-full text-xs">
3
+ <!-- Header -->
4
+ <div
5
+ class="px-3 py-2 font-bold text-white/50 uppercase tracking-wider border-b border-[#333] flex items-center justify-between">
6
+ <span>Properties</span>
7
+ <span v-if="selectedType" class="text-blue-400 font-mono">{{ selectedType }}</span>
8
+ </div>
9
+
10
+ <!-- No Selection State -->
11
+ <div v-if="!selectedData" class="flex-1 flex items-center justify-center p-4">
12
+ <div class="text-white/30 italic text-center">
13
+ <i class="ph-thin ph-cursor text-2xl mb-2 block opacity-50"></i>
14
+ Select an element to edit
15
+ </div>
16
+ </div>
17
+
18
+ <!-- Property Form -->
19
+ <div v-else class="flex-1 overflow-y-auto p-3 space-y-4">
20
+
21
+ <!-- Dedicated Diagram Node Inspector -->
22
+ <div v-if="isDiagramNode">
23
+ <div class="mb-4 flex items-center gap-2 text-[10px] text-white/50 uppercase tracking-widest cursor-pointer hover:text-white"
24
+ @click="editor.select(editor.selection.value!.split('.nodes.')[0])">
25
+ <i class="ph-thin ph-caret-left"></i> Back to Diagram
26
+ </div>
27
+ <DiagramNodeEditor :data="selectedData" @update="updateDiagramNode" @delete="deleteDiagramNode" />
28
+ </div>
29
+
30
+ <!-- Dedicated Diagram Edge Inspector -->
31
+ <div v-else-if="isDiagramEdge">
32
+ <div class="mb-4 flex items-center gap-2 text-[10px] text-white/50 uppercase tracking-widest cursor-pointer hover:text-white"
33
+ @click="editor.select(editor.selection.value!.split('.edges.')[0])">
34
+ <i class="ph-thin ph-caret-left"></i> Back to Diagram
35
+ </div>
36
+ <DiagramEdgeEditor :data="selectedData" @update="updateDiagramEdge" @delete="deleteSelected" />
37
+ </div>
38
+
39
+ <!-- Standard Inspector -->
40
+ <div v-else class="space-y-4">
41
+
42
+ <!-- Diagram Palette (If Diagram Slide selected) -->
43
+ <div v-if="selectedType === 'diagram'"
44
+ class="p-3 bg-blue-900/10 border border-blue-500/20 rounded mb-4">
45
+ <div class="text-white/30 text-[10px] uppercase tracking-widest mb-2">Diagram Palette</div>
46
+ <div class="grid grid-cols-3 gap-2">
47
+ <div draggable="true" @dragstart="onDragStart($event, 'default')"
48
+ @click="addNodeWithShape('default')"
49
+ class="cursor-grab flex flex-col items-center gap-1 p-2 bg-[#1a1a1a] hover:bg-[#252525] rounded border border-transparent hover:border-white/20 transition">
50
+ <div class="w-6 h-4 border border-white/50 bg-white/10"></div>
51
+ <span class="text-[9px] text-white/70">Rect</span>
52
+ </div>
53
+ <div draggable="true" @dragstart="onDragStart($event, 'input')"
54
+ @click="addNodeWithShape('input')"
55
+ class="cursor-grab flex flex-col items-center gap-1 p-2 bg-[#1a1a1a] hover:bg-[#252525] rounded border border-transparent hover:border-white/20 transition">
56
+ <div class="w-6 h-4 border border-white/50 bg-white/10 rounded-full"></div>
57
+ <span class="text-[9px] text-white/70">Rounded</span>
58
+ </div>
59
+ <div draggable="true" @dragstart="onDragStart($event, 'image')"
60
+ @click="addNodeWithShape('image')"
61
+ class="cursor-grab flex flex-col items-center gap-1 p-2 bg-[#1a1a1a] hover:bg-[#252525] rounded border border-transparent hover:border-white/20 transition">
62
+ <i class="ph-thin ph-image text-white/50 text-lg"></i>
63
+ <span class="text-[9px] text-white/70">Image</span>
64
+ </div>
65
+ </div>
66
+ <div class="mt-2 text-[9px] text-blue-200/50 text-center">
67
+ Drag from handle to handle on visible nodes to connect.
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Primitive Fields Section -->
72
+ <div v-if="Object.keys(primitiveFields).length > 0" class="space-y-3">
73
+
74
+ <div class="text-white/30 text-[10px] uppercase tracking-widest">Properties</div>
75
+ <template v-for="(value, key) in primitiveFields" :key="key">
76
+ <FieldEditor :fieldKey="key as string" :value="value" @update="updateField" />
77
+ </template>
78
+
79
+ <!-- Element Reveal Animation -->
80
+ <div v-if="!isSlide && selectedType !== 'content'" class="space-y-1">
81
+ <label class="text-white/50 text-[10px] uppercase">Reveal Animation</label>
82
+ <select :value="selectedData.class || ''"
83
+ @change="updateField('class', ($event.target as HTMLSelectElement).value)"
84
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
85
+ <option value="">None</option>
86
+ <option value="reveal-up">Reveal Up</option>
87
+ <option value="reveal-zoom">Reveal Zoom</option>
88
+ <option value="reveal-left">Reveal Left</option>
89
+ <option value="reveal-right">Reveal Right</option>
90
+ <option value="reveal-img">Image Cinematic</option>
91
+ </select>
92
+ </div>
93
+ </div>
94
+
95
+ <!-- Flex Layout (direction, gap, padding, halign, valign) -->
96
+ <div v-if="isSlide && selectedType === 'flex'" class="space-y-4">
97
+ <CollapsibleSection title="Layout" icon="ph-thin ph-squares-four" :defaultExpanded="true">
98
+ <div class="space-y-3">
99
+ <div class="space-y-1">
100
+ <label class="text-white/50 text-[10px] uppercase">Direction</label>
101
+ <select :value="selectedData.direction || 'horizontal'"
102
+ @change="updateField('direction', ($event.target as HTMLSelectElement).value)"
103
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
104
+ <option value="horizontal">→ Horizontal</option>
105
+ <option value="vertical">↓ Vertical</option>
106
+ </select>
107
+ </div>
108
+ <div class="space-y-1">
109
+ <label class="text-white/50 text-[10px] uppercase">Gap</label>
110
+ <select :value="selectedData.gap || 'none'"
111
+ @change="updateField('gap', ($event.target as HTMLSelectElement).value)"
112
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
113
+ <option value="none">None</option>
114
+ <option value="xs">XS</option>
115
+ <option value="sm">Sm</option>
116
+ <option value="md">Md</option>
117
+ <option value="lg">Lg</option>
118
+ <option value="xl">XL</option>
119
+ <option value="2xl">2XL</option>
120
+ <option value="3xl">3XL</option>
121
+ <option value="4xl">4XL</option>
122
+ </select>
123
+ </div>
124
+ <div class="space-y-1">
125
+ <label class="text-white/50 text-[10px] uppercase">Padding</label>
126
+ <select :value="selectedData.padding || 'none'"
127
+ @change="updateField('padding', ($event.target as HTMLSelectElement).value)"
128
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
129
+ <option value="none">None</option>
130
+ <option value="xs">XS</option>
131
+ <option value="sm">Sm</option>
132
+ <option value="md">Md</option>
133
+ <option value="lg">Lg</option>
134
+ <option value="xl">XL</option>
135
+ <option value="2xl">2XL</option>
136
+ <option value="3xl">3XL</option>
137
+ <option value="4xl">4XL</option>
138
+ </select>
139
+ </div>
140
+ <div class="space-y-1">
141
+ <label class="text-white/50 text-[10px] uppercase">Horizontal alignment</label>
142
+ <select :value="selectedData.halign || 'left'"
143
+ @change="updateField('halign', ($event.target as HTMLSelectElement).value)"
144
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
145
+ <option value="left">← Left</option>
146
+ <option value="center">↔ Center</option>
147
+ <option value="right">→ Right</option>
148
+ </select>
149
+ </div>
150
+ <div class="space-y-1">
151
+ <label class="text-white/50 text-[10px] uppercase">Vertical alignment</label>
152
+ <select :value="selectedData.valign || 'top'"
153
+ @change="updateField('valign', ($event.target as HTMLSelectElement).value)"
154
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
155
+ <option value="top">↑ Top</option>
156
+ <option value="center">↕ Center</option>
157
+ <option value="bottom">↓ Bottom</option>
158
+ </select>
159
+ </div>
160
+ </div>
161
+ </CollapsibleSection>
162
+ </div>
163
+
164
+ <!-- Slide Specific Settings -->
165
+ <div v-if="isSlide" class="space-y-4">
166
+ <!-- Appearance -->
167
+ <CollapsibleSection title="Slide Appearance" icon="ph-thin ph-paint-brush" :defaultExpanded="true">
168
+ <div class="space-y-3">
169
+ <ColorField label="Background Color" fieldKey="colors.background"
170
+ :value="selectedData.meta?.colors?.background" @update="updateMeta" />
171
+
172
+ <div class="space-y-1">
173
+ <label class="text-white/50 text-[10px] uppercase">Background Image URL</label>
174
+ <input type="text" :value="selectedData.background || ''"
175
+ @input="updateField('background', ($event.target as HTMLInputElement).value)"
176
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
177
+ </div>
178
+
179
+ <div class="grid grid-cols-2 gap-2">
180
+ <div class="space-y-1">
181
+ <label class="text-white/50 text-[10px] uppercase">Opacity</label>
182
+ <input type="number" step="0.1" min="0" max="1"
183
+ :value="selectedData.backgroundOpacity ?? 1"
184
+ @input="updateField('backgroundOpacity', parseFloat(($event.target as HTMLInputElement).value))"
185
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
186
+ </div>
187
+ <div class="space-y-1">
188
+ <label class="text-white/50 text-[10px] uppercase">Size</label>
189
+ <select :value="selectedData.meta?.backgroundSize || 'cover'"
190
+ @change="updateMeta('backgroundSize', ($event.target as HTMLSelectElement).value)"
191
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
192
+ <option value="cover">Cover</option>
193
+ <option value="contain">Contain</option>
194
+ <option value="auto">Auto</option>
195
+ </select>
196
+ </div>
197
+ </div>
198
+
199
+ <div class="pt-2 border-t border-[#333]">
200
+ <ColorField label="Orb Color" fieldKey="effects.orbColor"
201
+ :value="selectedData.meta?.orbColor" @update="updateMetaRaw" />
202
+
203
+ <div class="space-y-1 pt-2">
204
+ <div class="text-white/50 text-[10px] uppercase">Orb Position</div>
205
+ <div class="grid grid-cols-2 gap-2">
206
+ <input type="text" :value="selectedData.meta?.orbPos?.top || ''"
207
+ placeholder="Top (e.g. 10%)"
208
+ @input="updateMeta('orbPos.top', ($event.target as HTMLInputElement).value)"
209
+ class="bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
210
+ <input type="text" :value="selectedData.meta?.orbPos?.left || ''"
211
+ placeholder="Left (e.g. 50%)"
212
+ @input="updateMeta('orbPos.left', ($event.target as HTMLInputElement).value)"
213
+ class="bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
214
+ </div>
215
+ </div>
216
+ </div>
217
+ </div>
218
+ </CollapsibleSection>
219
+
220
+ <!-- Local Transitions -->
221
+ <CollapsibleSection title="Slide Transitions" icon="ph-thin ph-magic-wand" :defaultExpanded="false">
222
+ <div class="space-y-3">
223
+ <div class="space-y-1">
224
+ <label class="text-white/50 text-[10px] uppercase">Animation Preset</label>
225
+ <select :value="selectedData.meta?.effects?.animationType || ''"
226
+ @change="updateMeta('effects.animationType', ($event.target as HTMLSelectElement).value)"
227
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono">
228
+ <option value="">Global Default</option>
229
+ <option value="cascade">Cascade</option>
230
+ <option value="fade">Fade</option>
231
+ <option value="slide">Slide</option>
232
+ <option value="zoom">Zoom</option>
233
+ <option value="spring">Spring</option>
234
+ <option value="blur">Blur</option>
235
+ <option value="skew">Skew</option>
236
+ <option value="flip">Flip (3D)</option>
237
+ <option value="zoom-rotate">Zoom Rotate</option>
238
+ <option value="elastic-up">Elastic Up</option>
239
+ </select>
240
+ </div>
241
+
242
+ <div class="grid grid-cols-2 gap-2">
243
+ <div class="space-y-1">
244
+ <label class="text-white/50 text-[10px] uppercase">Duration</label>
245
+ <input type="number" step="0.1"
246
+ :value="selectedData.meta?.effects?.animationDuration || ''"
247
+ placeholder="e.g. 0.8"
248
+ @input="updateMeta('effects.animationDuration', parseFloat(($event.target as HTMLInputElement).value))"
249
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
250
+ </div>
251
+ <div class="space-y-1">
252
+ <label class="text-white/50 text-[10px] uppercase">Stagger</label>
253
+ <input type="number" step="0.05"
254
+ :value="selectedData.meta?.effects?.animationStagger || ''"
255
+ placeholder="e.g. 0.1"
256
+ @input="updateMeta('effects.animationStagger', parseFloat(($event.target as HTMLInputElement).value))"
257
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
258
+ </div>
259
+ </div>
260
+ </div>
261
+ </CollapsibleSection>
262
+ </div>
263
+
264
+ <!-- Action Editor (for Buttons & Images) -->
265
+ <div v-if="isActionable" class="pt-2 border-t border-[#333]">
266
+ <ActionEditor :actionType="selectedData.actionType" :href="selectedData.href"
267
+ :gotoSlide="selectedData.gotoSlide" :target="selectedData.target" :action="selectedData.action"
268
+ @update="updateField" />
269
+ </div>
270
+
271
+ <!-- String Array Fields (paragraphs, bullets, items) -->
272
+
273
+ <template v-for="(arr, key) in stringArrayFields" :key="key">
274
+ <div class="space-y-2">
275
+ <div class="flex items-center justify-between">
276
+ <span class="text-white/30 text-[10px] uppercase tracking-widest">{{ getFieldLabel(key as
277
+ string) }}</span>
278
+ <button @click="addStringToArray(key as string)"
279
+ class="text-[10px] px-2 py-0.5 bg-blue-600 hover:bg-blue-500 rounded text-white">
280
+ <i class="ph-thin ph-plus mr-1"></i>Add
281
+ </button>
282
+ </div>
283
+ <div v-for="(item, i) in arr" :key="i" class="flex gap-1">
284
+ <input type="text" :value="item"
285
+ @input="updateArrayItem(key as string, i, ($event.target as HTMLInputElement).value)"
286
+ class="flex-1 bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-white focus:border-blue-500 focus:outline-none" />
287
+ <button @click="removeFromArray(key as string, i)"
288
+ class="w-6 h-6 flex items-center justify-center text-red-400 hover:text-red-300">
289
+ <i class="ph-thin ph-trash text-[10px]"></i>
290
+ </button>
291
+ </div>
292
+ </div>
293
+ </template>
294
+
295
+ <!-- Object Array Fields (timeline, steps, features) -->
296
+ <template v-for="(arr, key) in objectArrayFields" :key="key">
297
+ <div class="space-y-2">
298
+ <div class="flex items-center justify-between">
299
+ <span class="text-white/30 text-[10px] uppercase tracking-widest">{{ key }} ({{ arr.length
300
+ }})</span>
301
+ <button @click="addObjectToArray(key as string)"
302
+ class="text-[10px] px-2 py-0.5 bg-blue-600 hover:bg-blue-500 rounded text-white">
303
+ <i class="ph-thin ph-plus mr-1"></i>Add
304
+ </button>
305
+ </div>
306
+ <div v-for="(item, i) in arr" :key="i"
307
+ class="bg-[#1a1a1a] border border-[#333] rounded p-2 space-y-2">
308
+ <div class="flex items-center justify-between text-[10px] text-white/50 pb-1">
309
+ <span class="font-bold uppercase truncate pr-2">{{ getItemLabel(item, i) }}</span>
310
+ <div class="flex gap-1 shrink-0">
311
+ <button @click="selectItem(key as string, i)"
312
+ class="w-6 h-6 flex items-center justify-center bg-blue-600/20 text-blue-400 hover:bg-blue-600 hover:text-white rounded transition-colors"
313
+ title="Edit Element">
314
+ <i class="ph-thin ph-pencil-simple text-[10px]"></i>
315
+ </button>
316
+ <div class="flex flex-col gap-0.5">
317
+ <button v-if="i > 0" @click="moveInArray(key as string, i, -1)"
318
+ class="h-3 flex items-center justify-center text-white/30 hover:text-white"
319
+ title="Move Up">
320
+ <i class="ph-thin ph-caret-up text-[8px]"></i>
321
+ </button>
322
+ <button v-if="i < arr.length - 1" @click="moveInArray(key as string, i, 1)"
323
+ class="h-3 flex items-center justify-center text-white/30 hover:text-white"
324
+ title="Move Down">
325
+ <i class="ph-thin ph-caret-down text-[8px]"></i>
326
+ </button>
327
+ </div>
328
+ <button @click="removeFromArray(key as string, i)"
329
+ class="w-6 h-6 flex items-center justify-center text-red-400 hover:text-red-300 hover:bg-red-900/30 rounded transition-colors"
330
+ title="Delete">
331
+ <i class="ph-thin ph-trash text-[10px]"></i>
332
+ </button>
333
+ </div>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ </template>
338
+
339
+ <!-- Diagram Special Handling -->
340
+ <div v-if="selectedType === 'diagram'" class="space-y-4 pt-4 border-t border-[#333]">
341
+ <div class="text-white/30 text-[10px] uppercase tracking-widest">Diagram Flow</div>
342
+
343
+ <!-- Hint -->
344
+ <div class="p-2 bg-blue-900/10 border border-blue-500/20 rounded text-[10px] text-blue-200">
345
+ <i class="ph-thin ph-info mr-1"></i>
346
+ Drag nodes directly on the canvas. Add new nodes here.
347
+ </div>
348
+
349
+ <!-- Node Management similar to Array Fields but specialized logic if needed -->
350
+ <!-- Or just reuse the generic object array logic which should capture 'nodes' automatically if we add 'nodes' to arrayKeys -->
351
+ </div>
352
+
353
+ <!-- Delete Button -->
354
+ <div class="pt-4 border-t border-[#333] mt-4">
355
+ <button @click="deleteSelected"
356
+ class="w-full px-3 py-2 rounded bg-red-900/30 border border-red-500/30 text-red-400 hover:bg-red-900/50 transition flex items-center justify-center gap-2">
357
+ <i class="ph-thin ph-trash text-xs"></i> Delete Element
358
+ </button>
359
+ </div>
360
+ </div> <!-- End of Standard Inspector -->
361
+ </div>
362
+ </div>
363
+ </template>
364
+
365
+ <script setup lang="ts">
366
+ import { computed } from 'vue';
367
+ import { useEditor } from '../../composables/useEditor';
368
+ import FieldEditor from './FieldEditor.vue';
369
+ import ActionEditor from './ActionEditor.vue';
370
+ import CollapsibleSection from './CollapsibleSection.vue';
371
+ import ColorField from './ColorField.vue';
372
+ // Import the new dedicated editor
373
+
374
+ import DiagramNodeEditor from './inspectors/DiagramNodeEditor.vue';
375
+ import DiagramEdgeEditor from './inspectors/DiagramEdgeEditor.vue';
376
+ import { getFieldLabel } from './fieldLabels';
377
+
378
+ const editor = useEditor();
379
+
380
+ const selectedData = computed(() => editor.getSelectedData());
381
+ const selectedType = computed(() => selectedData.value?.type);
382
+
383
+ const isSlide = computed(() => {
384
+ return /^slides\.\d+$/.test(editor.selection.value || '');
385
+ });
386
+
387
+ // Detect if a diagram node/edge is selected
388
+ // Path format: slides.X.nodes.Y or slides.X.edges.Y
389
+ const isDiagramNode = computed(() => {
390
+ return editor.selection.value && editor.selection.value.includes('.nodes.') && !editor.selection.value.endsWith('.nodes');
391
+ });
392
+
393
+ const isDiagramEdge = computed(() => {
394
+ const result = editor.selection.value && editor.selection.value.includes('.edges.') && !editor.selection.value.endsWith('.edges');
395
+ return result;
396
+ });
397
+
398
+ const isActionable = computed(() => {
399
+ const type = selectedData.value?.type;
400
+ return type === 'button' || type === 'image';
401
+ });
402
+
403
+ // Helper to update diagram node - emits event to update VueFlow directly (bypasses store -> watch cycle)
404
+ const updateDiagramNode = (key: string, value: any) => {
405
+ // Parse selection path to get slide index and node id
406
+ // Path format: slides.X.nodes.Y
407
+ const parts = editor.selection.value?.split('.') || [];
408
+ if (parts.length < 4) return;
409
+
410
+ const slideIndex = parseInt(parts[1]);
411
+ const nodeData = editor.getSelectedData();
412
+ if (!nodeData?.id) return;
413
+
414
+ // Emit event to LayoutDiagram to update VueFlow directly
415
+ import('../../core/events').then(({ bus }) => {
416
+ bus.emit('diagram-node-update', {
417
+ slideIndex,
418
+ nodeId: nodeData.id,
419
+ key,
420
+ value
421
+ });
422
+ });
423
+ };
424
+
425
+ const deleteDiagramNode = () => {
426
+ deleteSelected();
427
+ };
428
+
429
+ // Helper for edges
430
+ const updateDiagramEdge = (key: string, value: any) => {
431
+ // Similar logic, path is direct relative to edge
432
+ const path = `${editor.selection.value}.${key}`;
433
+ editor.store.updateNode(path, value);
434
+ editor.commit();
435
+ };
436
+
437
+ const addNodeWithShape = (shape: string) => {
438
+ const template = {
439
+ id: `node-${Date.now()}`,
440
+ label: 'New Node',
441
+ position: { x: 250, y: 250 },
442
+ type: shape, // 'default', 'input', 'output'
443
+ style: { backgroundColor: '#ffffff', color: '#000000', width: '150px' },
444
+ dragKey: Date.now() + Math.random()
445
+ };
446
+
447
+ // Assume selection is the slide, so append to 'nodes'
448
+ const path = `${editor.selection.value}.nodes`;
449
+ editor.store.addNode(path, template);
450
+ editor.commit();
451
+ };
452
+
453
+ // ... (Rest of existing methods: updateField, etc.)
454
+
455
+ // Separate field types
456
+ const primitiveFields = computed(() => {
457
+ if (!selectedData.value) return {};
458
+ const excluded = ['type', 'actionType', 'href', 'gotoSlide', 'target', 'action', 'class', 'dragKey', 'position', 'style', 'data', 'sourcePosition', 'targetPosition', 'source', 'target', 'animated', 'nodes', 'edges'];
459
+ const flexLayoutKeys = (isSlide.value && selectedType.value === 'flex') ? ['direction', 'gap', 'padding', 'halign', 'valign'] : [];
460
+ return Object.fromEntries(
461
+ Object.entries(selectedData.value).filter(([key, val]) =>
462
+ !excluded.includes(key) && !flexLayoutKeys.includes(key) &&
463
+ (typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number')
464
+ )
465
+ );
466
+ });
467
+
468
+ const stringArrayFields = computed(() => {
469
+ if (!selectedData.value) return {};
470
+ return Object.fromEntries(
471
+ Object.entries(selectedData.value).filter(([_key, val]) =>
472
+ Array.isArray(val) && val.length > 0 && typeof val[0] === 'string'
473
+ )
474
+ );
475
+ });
476
+
477
+ const objectArrayFields = computed<Record<string, any[]>>(() => {
478
+ if (!selectedData.value) return {};
479
+ const arrayKeys = ['timeline', 'steps', 'features', 'elements', 'datasets'];
480
+ return Object.fromEntries(
481
+ Object.entries(selectedData.value).filter(([key, val]) =>
482
+ arrayKeys.includes(key) && Array.isArray(val)
483
+ )
484
+ ) as Record<string, any[]>;
485
+ });
486
+
487
+ const getItemLabel = (item: any, index: number) => {
488
+ return item.title || item.date || item.step || item.label || `#${index + 1}`;
489
+ };
490
+
491
+
492
+ const getObjectTemplate = (key: string): any => {
493
+ const templates: Record<string, any> = {
494
+ 'timeline': { date: '2024', title: 'New Event', description: 'Description', dragKey: Date.now() + Math.random() },
495
+ 'steps': { step: '1', title: 'New Step', description: 'Description', dragKey: Date.now() + Math.random() },
496
+ 'features': { icon: 'star', title: 'New Feature', desc: 'Description', dragKey: Date.now() + Math.random() },
497
+ 'elements': { type: 'text', text: 'New text element', dragKey: Date.now() + Math.random() },
498
+ 'datasets': { label: 'Dataset', values: [], dragKey: Date.now() + Math.random() },
499
+ };
500
+ return templates[key] || { title: 'New Item', dragKey: Date.now() + Math.random() };
501
+ };
502
+
503
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
504
+
505
+ const updateField = (key: string, value: any) => {
506
+ const immediateKeys = ['actionType', 'variant', 'type', 'animationsEnabled', 'useOrb', 'useGlass', 'useShadows', 'class', 'backgroundOpacity'];
507
+ if (immediateKeys.includes(key) || typeof value === 'boolean') {
508
+ if (debounceTimer) clearTimeout(debounceTimer);
509
+ editor.updateSelected(key, value);
510
+ editor.commit();
511
+ return;
512
+ }
513
+ if (debounceTimer) clearTimeout(debounceTimer);
514
+ debounceTimer = setTimeout(() => {
515
+ editor.updateSelected(key, value);
516
+ editor.commit();
517
+ }, 150);
518
+ };
519
+
520
+ const addStringToArray = (key: string) => {
521
+ const path = `${editor.selection.value}.${key}`;
522
+ editor.store.addNode(path, 'New item');
523
+ editor.commit();
524
+ };
525
+
526
+ const addObjectToArray = (key: string) => {
527
+ const path = `${editor.selection.value}.${key}`;
528
+ const template = getObjectTemplate(key);
529
+ editor.store.addNode(path, template);
530
+ editor.commit();
531
+ const parentData = editor.getSelectedData();
532
+ const arr = (parentData ? parentData[key] : []) as any[];
533
+ if (arr && arr.length > 0) {
534
+ editor.select(`${path}.${arr.length - 1}`);
535
+ }
536
+ };
537
+
538
+ const selectItem = (key: string, index: number) => {
539
+ const path = `${editor.selection.value}.${key}.${index}`;
540
+ editor.select(path);
541
+ };
542
+
543
+ const updateArrayItem = (key: string, index: number, value: string) => {
544
+ const path = `${editor.selection.value}.${key}.${index}`;
545
+ editor.store.updateNode(path, value);
546
+ editor.commit();
547
+ };
548
+
549
+
550
+ const removeFromArray = (key: string, index: number) => {
551
+ const path = `${editor.selection.value}.${key}`;
552
+ editor.store.removeNode(path, index);
553
+ editor.commit();
554
+ };
555
+
556
+ const moveInArray = (key: string, index: number, direction: number) => {
557
+ const path = `${editor.selection.value}.${key}`;
558
+ editor.store.moveNode(path, index, index + direction);
559
+ editor.commit();
560
+ };
561
+
562
+ const deleteSelected = () => {
563
+ if (!editor.selection.value) return;
564
+ const parts = editor.selection.value.split('.');
565
+ const index = parseInt(parts.pop()!);
566
+ const arrayPath = parts.join('.');
567
+
568
+ if (!isNaN(index) && arrayPath !== 'slides') {
569
+ editor.store.removeNode(arrayPath, index);
570
+ editor.commit();
571
+ editor.select(null);
572
+ }
573
+ };
574
+
575
+ const updateMeta = (key: string, value: any) => {
576
+ // e.g. key='colors.background' -> meta.colors.background
577
+ const path = `${editor.selection.value}.meta.${key}`;
578
+ editor.store.updateNode(path, value);
579
+ editor.commit();
580
+ };
581
+
582
+ const updateMetaRaw = (key: string, value: any) => {
583
+ // Simplification for orbColor
584
+ const path = `${editor.selection.value}.meta.${key.replace('effects.', '')}`;
585
+ editor.store.updateNode(path, value);
586
+ editor.commit();
587
+ };
588
+
589
+ const onDragStart = (event: DragEvent, type: string) => {
590
+ if (event.dataTransfer) {
591
+ event.dataTransfer.setData('application/vueflow', type);
592
+ event.dataTransfer.effectAllowed = 'move';
593
+ }
594
+ };
595
+ </script>