lumina-slides 9.0.5 → 9.0.7

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 (38) hide show
  1. package/README.md +63 -0
  2. package/dist/lumina-slides.js +21750 -19334
  3. package/dist/lumina-slides.umd.cjs +223 -223
  4. package/dist/style.css +1 -1
  5. package/package.json +1 -1
  6. package/src/components/LandingPage.vue +1 -1
  7. package/src/components/LuminaDeck.vue +237 -232
  8. package/src/components/base/LuminaElement.vue +2 -0
  9. package/src/components/layouts/LayoutFeatures.vue +125 -123
  10. package/src/components/layouts/LayoutFlex.vue +212 -212
  11. package/src/components/layouts/LayoutStatement.vue +5 -2
  12. package/src/components/layouts/LayoutSteps.vue +110 -108
  13. package/src/components/parts/FlexHtml.vue +65 -65
  14. package/src/components/parts/FlexImage.vue +81 -81
  15. package/src/components/site/SiteDocs.vue +3313 -3314
  16. package/src/components/site/SiteExamples.vue +66 -66
  17. package/src/components/studio/EditorLayoutChart.vue +18 -0
  18. package/src/components/studio/EditorLayoutCustom.vue +18 -0
  19. package/src/components/studio/EditorLayoutVideo.vue +18 -0
  20. package/src/components/studio/LuminaStudioEmbed.vue +68 -0
  21. package/src/components/studio/StudioEmbedRoot.vue +19 -0
  22. package/src/components/studio/StudioInspector.vue +1113 -7
  23. package/src/components/studio/StudioJsonEditor.vue +10 -3
  24. package/src/components/studio/StudioSettings.vue +658 -7
  25. package/src/components/studio/StudioToolbar.vue +26 -7
  26. package/src/composables/useElementState.ts +12 -1
  27. package/src/composables/useFlexLayout.ts +128 -128
  28. package/src/core/Lumina.ts +174 -113
  29. package/src/core/animationConfig.ts +10 -0
  30. package/src/core/elementController.ts +18 -0
  31. package/src/core/elementResolver.ts +4 -2
  32. package/src/core/schema.ts +503 -503
  33. package/src/core/store.ts +465 -465
  34. package/src/core/types.ts +26 -11
  35. package/src/index.ts +2 -2
  36. package/src/utils/prepareDeckForExport.ts +47 -0
  37. package/src/utils/templateInterpolation.ts +52 -52
  38. package/src/views/DeckView.vue +313 -313
@@ -39,6 +39,46 @@
39
39
  <!-- Standard Inspector -->
40
40
  <div v-else class="space-y-4">
41
41
 
42
+ <!-- Element ID: show resolved ID and allow customization -->
43
+ <CollapsibleSection v-if="elementIdInfo" title="Element ID" icon="ph-thin ph-fingerprint" :defaultExpanded="true">
44
+ <div class="space-y-3">
45
+ <div class="space-y-1">
46
+ <label class="text-white/50 text-[10px] uppercase">Resolved ID</label>
47
+ <div class="flex gap-1 items-center">
48
+ <code class="flex-1 bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-[10px] text-white font-mono break-all">{{ elementIdInfo.resolvedId }}</code>
49
+ <button type="button" @click="copyResolvedIdToClipboard"
50
+ class="shrink-0 p-1.5 rounded border border-[#333] text-white/50 hover:text-white hover:border-white/30"
51
+ title="Copy ID">
52
+ <i class="ph-thin ph-copy"></i>
53
+ </button>
54
+ </div>
55
+ <p class="text-[9px] text-white/30">ID used by the engine (engine.element(id), initialElementState)</p>
56
+ </div>
57
+ <div class="space-y-1">
58
+ <label class="text-white/50 text-[10px] uppercase">Logical path</label>
59
+ <code class="block bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-[10px] text-white/80 font-mono">{{ elementIdInfo.pathKey || 'slide' }}</code>
60
+ </div>
61
+ <div v-if="elementIdInfo.fallbackId !== elementIdInfo.resolvedId" class="space-y-1">
62
+ <label class="text-white/50 text-[10px] uppercase">Auto-generated (fallback)</label>
63
+ <code class="block bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-[10px] text-white/50 font-mono">{{ elementIdInfo.fallbackId }}</code>
64
+ </div>
65
+ <div v-if="elementIdInfo.canCustomizeViaIds" class="space-y-1 pt-2 border-t border-[#333]">
66
+ <label class="text-white/50 text-[10px] uppercase">Custom ID</label>
67
+ <input type="text" :value="elementIdInfo.customId"
68
+ @input="updateCustomElementId(($event.target as HTMLInputElement).value)"
69
+ :placeholder="elementIdInfo.fallbackId"
70
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-xs text-white font-mono placeholder:text-white/30" />
71
+ <p class="text-[9px] text-white/30">Override for this path. Leave empty to use auto-generated.</p>
72
+ </div>
73
+ <p v-else-if="elementIdInfo.hasObjectId" class="text-[9px] text-white/30 pt-1 border-t border-[#333]">
74
+ Customize by editing the <strong>id</strong> property in Properties below.
75
+ </p>
76
+ <p v-else-if="elementIdInfo.isSlideRoot" class="text-[9px] text-white/30 pt-1 border-t border-[#333]">
77
+ Set the slide ID in <strong>Slide Identity</strong> below to customize.
78
+ </p>
79
+ </div>
80
+ </CollapsibleSection>
81
+
42
82
  <!-- Diagram Palette (If Diagram Slide selected) -->
43
83
  <div v-if="selectedType === 'diagram'"
44
84
  class="p-3 bg-blue-900/10 border border-blue-500/20 rounded mb-4">
@@ -163,6 +203,87 @@
163
203
 
164
204
  <!-- Slide Specific Settings -->
165
205
  <div v-if="isSlide" class="space-y-4">
206
+ <!-- Slide Identity -->
207
+ <CollapsibleSection title="Slide Identity" icon="ph-thin ph-identification-card" :defaultExpanded="false">
208
+ <div class="space-y-3">
209
+ <div class="space-y-1">
210
+ <label class="text-white/50 text-[10px] uppercase">Slide ID</label>
211
+ <input type="text" :value="selectedData.id || ''"
212
+ @input="updateField('id', ($event.target as HTMLInputElement).value)"
213
+ placeholder="unique-slide-id"
214
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
215
+ <p class="text-[9px] text-white/30">Unique identifier for navigation</p>
216
+ </div>
217
+ <div class="space-y-1">
218
+ <label class="text-white/50 text-[10px] uppercase">CSS Class</label>
219
+ <input type="text" :value="selectedData.class || ''"
220
+ @input="updateField('class', ($event.target as HTMLInputElement).value)"
221
+ placeholder="custom-class"
222
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
223
+ </div>
224
+ <div class="space-y-1">
225
+ <label class="text-white/50 text-[10px] uppercase">Sizing Mode</label>
226
+ <select :value="selectedData.sizing || 'viewport'"
227
+ @change="updateField('sizing', ($event.target as HTMLSelectElement).value)"
228
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
229
+ <option value="viewport">Viewport (100vh)</option>
230
+ <option value="container">Container (100%)</option>
231
+ </select>
232
+ </div>
233
+
234
+ <!-- All element IDs for this slide -->
235
+ <div class="pt-2 border-t border-[#333]">
236
+ <div class="text-white/30 text-[10px] uppercase tracking-widest mb-2">All element IDs</div>
237
+ <p class="text-[9px] text-white/30 mb-2">Resolved IDs for this slide. Customize in &quot;Element ID&quot; when an element is selected, or add mapping below.</p>
238
+ <div class="max-h-32 overflow-y-auto space-y-1 custom-scrollbar">
239
+ <div v-for="entry in slideElementIdsList" :key="entry.pathKey"
240
+ class="flex items-center gap-2 bg-[#1a1a1a] rounded px-2 py-1">
241
+ <span class="text-white/50 font-mono text-[9px] shrink-0 w-24 truncate" :title="entry.pathKey">{{ entry.pathKey }}</span>
242
+ <span class="text-white/70 font-mono text-[9px] truncate flex-1" :title="entry.resolvedId">{{ entry.resolvedId }}</span>
243
+ </div>
244
+ </div>
245
+ </div>
246
+
247
+ <!-- Element IDs Mapping (custom overrides) -->
248
+ <div class="pt-2 border-t border-[#333]">
249
+ <div class="text-white/30 text-[10px] uppercase tracking-widest mb-2">Custom ID overrides</div>
250
+ <div v-if="!selectedData.ids || Object.keys(selectedData.ids).length === 0"
251
+ class="text-white/30 text-[10px] italic">No custom IDs defined</div>
252
+ <div v-else class="space-y-1">
253
+ <div v-for="(id, path) in selectedData.ids" :key="path"
254
+ class="flex items-center gap-2 bg-[#1a1a1a] rounded px-2 py-1">
255
+ <span class="text-white/50 font-mono text-[9px]">{{ path }}:</span>
256
+ <span class="text-white/70 font-mono text-[9px]">{{ id }}</span>
257
+ <button @click="removeElementIdMapping(path as string)"
258
+ class="ml-auto text-red-400 hover:text-red-300">
259
+ <i class="ph-thin ph-x text-[9px]"></i>
260
+ </button>
261
+ </div>
262
+ </div>
263
+ <div class="mt-2 flex gap-1">
264
+ <input type="text" v-model="newIdPath" placeholder="path (e.g. tag, features.0)"
265
+ class="flex-1 bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-[10px] text-white font-mono" />
266
+ <input type="text" v-model="newIdValue" placeholder="custom id"
267
+ class="flex-1 bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-[10px] text-white font-mono" />
268
+ <button @click="addElementIdMapping"
269
+ class="px-2 py-1 bg-blue-600 hover:bg-blue-500 rounded text-[10px] text-white">+</button>
270
+ </div>
271
+ </div>
272
+ </div>
273
+ </CollapsibleSection>
274
+
275
+ <!-- Speaker Notes -->
276
+ <CollapsibleSection title="Speaker Notes" icon="ph-thin ph-note" :defaultExpanded="false">
277
+ <div class="space-y-2">
278
+ <textarea :value="selectedData.notes || ''"
279
+ @input="updateField('notes', ($event.target as HTMLTextAreaElement).value)"
280
+ placeholder="Speaker notes (supports markdown)..."
281
+ rows="5"
282
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-xs text-white resize-none"></textarea>
283
+ <p class="text-[9px] text-white/30">Notes visible in presenter view</p>
284
+ </div>
285
+ </CollapsibleSection>
286
+
166
287
  <!-- Appearance -->
167
288
  <CollapsibleSection title="Slide Appearance" icon="ph-thin ph-paint-brush" :defaultExpanded="true">
168
289
  <div class="space-y-3">
@@ -171,11 +292,53 @@
171
292
 
172
293
  <div class="space-y-1">
173
294
  <label class="text-white/50 text-[10px] uppercase">Background Image URL</label>
174
- <input type="text" :value="selectedData.background || ''"
295
+ <input type="text" :value="getBackgroundUrl()"
175
296
  @input="updateField('background', ($event.target as HTMLInputElement).value)"
176
297
  class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
177
298
  </div>
178
299
 
300
+ <!-- Video Background Toggle -->
301
+ <div class="space-y-1">
302
+ <label class="flex items-center gap-2 text-white/50 text-[10px] uppercase cursor-pointer">
303
+ <input type="checkbox" :checked="isVideoBackground"
304
+ @change="toggleVideoBackground"
305
+ class="rounded border-[#333]" />
306
+ Use Video Background
307
+ </label>
308
+ </div>
309
+
310
+ <div v-if="isVideoBackground" class="space-y-2 pl-2 border-l-2 border-[#333]">
311
+ <div class="space-y-1">
312
+ <label class="text-white/50 text-[10px] uppercase">Video URL</label>
313
+ <input type="text" :value="selectedData.background?.src || ''"
314
+ @input="updateVideoBackground('src', ($event.target as HTMLInputElement).value)"
315
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
316
+ </div>
317
+ <div class="space-y-1">
318
+ <label class="text-white/50 text-[10px] uppercase">Poster Image</label>
319
+ <input type="text" :value="selectedData.background?.poster || ''"
320
+ @input="updateVideoBackground('poster', ($event.target as HTMLInputElement).value)"
321
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
322
+ </div>
323
+ <div class="flex gap-4">
324
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
325
+ <input type="checkbox" :checked="selectedData.background?.autoplay ?? true"
326
+ @change="updateVideoBackground('autoplay', ($event.target as HTMLInputElement).checked)" />
327
+ Autoplay
328
+ </label>
329
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
330
+ <input type="checkbox" :checked="selectedData.background?.loop ?? true"
331
+ @change="updateVideoBackground('loop', ($event.target as HTMLInputElement).checked)" />
332
+ Loop
333
+ </label>
334
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
335
+ <input type="checkbox" :checked="selectedData.background?.muted ?? true"
336
+ @change="updateVideoBackground('muted', ($event.target as HTMLInputElement).checked)" />
337
+ Muted
338
+ </label>
339
+ </div>
340
+ </div>
341
+
179
342
  <div class="grid grid-cols-2 gap-2">
180
343
  <div class="space-y-1">
181
344
  <label class="text-white/50 text-[10px] uppercase">Opacity</label>
@@ -259,6 +422,411 @@
259
422
  </div>
260
423
  </div>
261
424
  </CollapsibleSection>
425
+
426
+ <!-- Slide Reveal Options -->
427
+ <CollapsibleSection title="Reveal Options" icon="ph-thin ph-sparkle" :defaultExpanded="false">
428
+ <div class="space-y-3">
429
+ <p class="text-[9px] text-white/30">Override global reveal settings for this slide</p>
430
+ <div class="space-y-1">
431
+ <label class="text-white/50 text-[10px] uppercase">Delay (ms)</label>
432
+ <input type="number" :value="selectedData.reveal?.delayMs || ''"
433
+ @input="updateSlideReveal('delayMs', parseInt(($event.target as HTMLInputElement).value) || undefined)"
434
+ placeholder="Global default"
435
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
436
+ </div>
437
+ <div class="space-y-1">
438
+ <label class="text-white/50 text-[10px] uppercase">Stagger Mode</label>
439
+ <select :value="selectedData.reveal?.staggerMode || ''"
440
+ @change="updateSlideReveal('staggerMode', ($event.target as HTMLSelectElement).value || undefined)"
441
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
442
+ <option value="">Global default</option>
443
+ <option value="sequential">Sequential</option>
444
+ <option value="center-out">Center Out</option>
445
+ <option value="ends-in">Ends In</option>
446
+ <option value="wave">Wave</option>
447
+ <option value="random">Random</option>
448
+ </select>
449
+ </div>
450
+ <div class="space-y-1">
451
+ <label class="text-white/50 text-[10px] uppercase">Preset</label>
452
+ <select :value="selectedData.reveal?.preset || ''"
453
+ @change="updateSlideReveal('preset', ($event.target as HTMLSelectElement).value || undefined)"
454
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
455
+ <option value="">Global default</option>
456
+ <option value="fadeUp">Fade Up</option>
457
+ <option value="fadeDown">Fade Down</option>
458
+ <option value="scaleIn">Scale In</option>
459
+ <option value="slideLeft">Slide Left</option>
460
+ <option value="slideRight">Slide Right</option>
461
+ </select>
462
+ </div>
463
+ <div class="grid grid-cols-2 gap-2">
464
+ <div class="space-y-1">
465
+ <label class="text-white/50 text-[10px] uppercase">Duration</label>
466
+ <input type="number" step="0.05" :value="selectedData.reveal?.duration || ''"
467
+ @input="updateSlideReveal('duration', parseFloat(($event.target as HTMLInputElement).value) || undefined)"
468
+ placeholder="0.45"
469
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
470
+ </div>
471
+ <div class="space-y-1">
472
+ <label class="text-white/50 text-[10px] uppercase">Ease</label>
473
+ <input type="text" :value="selectedData.reveal?.ease || ''"
474
+ @input="updateSlideReveal('ease', ($event.target as HTMLInputElement).value || undefined)"
475
+ placeholder="power2.out"
476
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
477
+ </div>
478
+ </div>
479
+ </div>
480
+ </CollapsibleSection>
481
+ </div>
482
+
483
+ <!-- Flex Content Container Special Handling -->
484
+ <div v-if="selectedType === 'content' && !isSlide" class="space-y-4">
485
+ <CollapsibleSection title="Container Layout" icon="ph-thin ph-layout" :defaultExpanded="true">
486
+ <div class="space-y-3">
487
+ <div class="space-y-1">
488
+ <label class="text-white/50 text-[10px] uppercase">Direction</label>
489
+ <select :value="selectedData.direction || 'vertical'"
490
+ @change="updateField('direction', ($event.target as HTMLSelectElement).value)"
491
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
492
+ <option value="horizontal">→ Horizontal</option>
493
+ <option value="vertical">↓ Vertical</option>
494
+ </select>
495
+ </div>
496
+ <div class="space-y-1">
497
+ <label class="text-white/50 text-[10px] uppercase">Gap</label>
498
+ <select :value="selectedData.gap || 'md'"
499
+ @change="updateField('gap', ($event.target as HTMLSelectElement).value)"
500
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
501
+ <option value="none">None</option>
502
+ <option value="xs">XS</option>
503
+ <option value="sm">Sm</option>
504
+ <option value="md">Md</option>
505
+ <option value="lg">Lg</option>
506
+ <option value="xl">XL</option>
507
+ <option value="2xl">2XL</option>
508
+ </select>
509
+ </div>
510
+ <div class="space-y-1">
511
+ <label class="text-white/50 text-[10px] uppercase">Padding</label>
512
+ <select :value="selectedData.padding || 'lg'"
513
+ @change="updateField('padding', ($event.target as HTMLSelectElement).value)"
514
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
515
+ <option value="none">None</option>
516
+ <option value="xs">XS</option>
517
+ <option value="sm">Sm</option>
518
+ <option value="md">Md</option>
519
+ <option value="lg">Lg</option>
520
+ <option value="xl">XL</option>
521
+ <option value="2xl">2XL</option>
522
+ </select>
523
+ </div>
524
+ <div class="space-y-1">
525
+ <label class="text-white/50 text-[10px] uppercase">H. Align</label>
526
+ <select :value="selectedData.halign || 'left'"
527
+ @change="updateField('halign', ($event.target as HTMLSelectElement).value)"
528
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
529
+ <option value="left">← Left</option>
530
+ <option value="center">↔ Center</option>
531
+ <option value="right">→ Right</option>
532
+ </select>
533
+ </div>
534
+ <div class="space-y-1">
535
+ <label class="text-white/50 text-[10px] uppercase">V. Align</label>
536
+ <select :value="selectedData.valign || 'center'"
537
+ @change="updateField('valign', ($event.target as HTMLSelectElement).value)"
538
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
539
+ <option value="top">↑ Top</option>
540
+ <option value="center">↕ Center</option>
541
+ <option value="bottom">↓ Bottom</option>
542
+ </select>
543
+ </div>
544
+ </div>
545
+ </CollapsibleSection>
546
+ </div>
547
+
548
+ <!-- Flex Image Element Special Handling -->
549
+ <div v-if="selectedType === 'image' && !isSlide" class="space-y-4">
550
+ <CollapsibleSection title="Image Options" icon="ph-thin ph-image" :defaultExpanded="true">
551
+ <div class="space-y-3">
552
+ <div class="space-y-1">
553
+ <label class="text-white/50 text-[10px] uppercase">Element ID</label>
554
+ <input type="text" :value="selectedData.id || ''"
555
+ @input="updateField('id', ($event.target as HTMLInputElement).value)"
556
+ placeholder="unique-element-id"
557
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
558
+ </div>
559
+ <div class="flex gap-4">
560
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
561
+ <input type="checkbox" :checked="selectedData.fill ?? true"
562
+ @change="updateField('fill', ($event.target as HTMLInputElement).checked)" />
563
+ Fill Container
564
+ </label>
565
+ </div>
566
+ <div class="space-y-1">
567
+ <label class="text-white/50 text-[10px] uppercase">Object Fit</label>
568
+ <select :value="selectedData.fit || 'cover'"
569
+ @change="updateField('fit', ($event.target as HTMLSelectElement).value)"
570
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
571
+ <option value="cover">Cover</option>
572
+ <option value="contain">Contain</option>
573
+ <option value="fill">Fill</option>
574
+ <option value="none">None</option>
575
+ <option value="scale-down">Scale Down</option>
576
+ </select>
577
+ </div>
578
+ <div class="space-y-1">
579
+ <label class="text-white/50 text-[10px] uppercase">Position</label>
580
+ <input type="text" :value="selectedData.position || 'center'"
581
+ @input="updateField('position', ($event.target as HTMLInputElement).value)"
582
+ placeholder="center, top, left, 50% 25%"
583
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
584
+ </div>
585
+ <div class="space-y-1">
586
+ <label class="text-white/50 text-[10px] uppercase">Rounded</label>
587
+ <select :value="selectedData.rounded || 'none'"
588
+ @change="updateField('rounded', ($event.target as HTMLSelectElement).value)"
589
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
590
+ <option value="none">None</option>
591
+ <option value="sm">Small</option>
592
+ <option value="md">Medium</option>
593
+ <option value="lg">Large</option>
594
+ <option value="xl">X-Large</option>
595
+ <option value="full">Full (Circle)</option>
596
+ </select>
597
+ </div>
598
+ </div>
599
+ </CollapsibleSection>
600
+ </div>
601
+
602
+ <!-- Flex Video Element Special Handling -->
603
+ <div v-if="selectedType === 'video' && !isSlide && !isVideoSlide" class="space-y-4">
604
+ <CollapsibleSection title="Video Options" icon="ph-thin ph-video-camera" :defaultExpanded="true">
605
+ <div class="space-y-3">
606
+ <div class="space-y-1">
607
+ <label class="text-white/50 text-[10px] uppercase">Element ID</label>
608
+ <input type="text" :value="selectedData.id || ''"
609
+ @input="updateField('id', ($event.target as HTMLInputElement).value)"
610
+ placeholder="unique-element-id"
611
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
612
+ </div>
613
+ <div class="space-y-1">
614
+ <label class="text-white/50 text-[10px] uppercase">Video URL</label>
615
+ <input type="text" :value="selectedData.src || ''"
616
+ @input="updateField('src', ($event.target as HTMLInputElement).value)"
617
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
618
+ </div>
619
+ <div class="space-y-1">
620
+ <label class="text-white/50 text-[10px] uppercase">Poster</label>
621
+ <input type="text" :value="selectedData.poster || ''"
622
+ @input="updateField('poster', ($event.target as HTMLInputElement).value)"
623
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
624
+ </div>
625
+ <div class="flex flex-wrap gap-4">
626
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
627
+ <input type="checkbox" :checked="selectedData.autoplay ?? false"
628
+ @change="updateField('autoplay', ($event.target as HTMLInputElement).checked)" />
629
+ Autoplay
630
+ </label>
631
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
632
+ <input type="checkbox" :checked="selectedData.loop ?? false"
633
+ @change="updateField('loop', ($event.target as HTMLInputElement).checked)" />
634
+ Loop
635
+ </label>
636
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
637
+ <input type="checkbox" :checked="selectedData.muted ?? false"
638
+ @change="updateField('muted', ($event.target as HTMLInputElement).checked)" />
639
+ Muted
640
+ </label>
641
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
642
+ <input type="checkbox" :checked="selectedData.controls ?? false"
643
+ @change="updateField('controls', ($event.target as HTMLInputElement).checked)" />
644
+ Controls
645
+ </label>
646
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
647
+ <input type="checkbox" :checked="selectedData.fill ?? true"
648
+ @change="updateField('fill', ($event.target as HTMLInputElement).checked)" />
649
+ Fill
650
+ </label>
651
+ </div>
652
+ <div class="grid grid-cols-2 gap-2">
653
+ <div class="space-y-1">
654
+ <label class="text-white/50 text-[10px] uppercase">Fit</label>
655
+ <select :value="selectedData.fit || 'cover'"
656
+ @change="updateField('fit', ($event.target as HTMLSelectElement).value)"
657
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
658
+ <option value="cover">Cover</option>
659
+ <option value="contain">Contain</option>
660
+ </select>
661
+ </div>
662
+ <div class="space-y-1">
663
+ <label class="text-white/50 text-[10px] uppercase">Rounded</label>
664
+ <select :value="selectedData.rounded || 'none'"
665
+ @change="updateField('rounded', ($event.target as HTMLSelectElement).value)"
666
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
667
+ <option value="none">None</option>
668
+ <option value="sm">Small</option>
669
+ <option value="md">Medium</option>
670
+ <option value="lg">Large</option>
671
+ <option value="xl">X-Large</option>
672
+ <option value="full">Full</option>
673
+ </select>
674
+ </div>
675
+ </div>
676
+ </div>
677
+ </CollapsibleSection>
678
+ </div>
679
+
680
+ <!-- Flex Button Element Special Handling -->
681
+ <div v-if="selectedType === 'button' && !isSlide" class="space-y-4">
682
+ <CollapsibleSection title="Button Options" icon="ph-thin ph-rectangle" :defaultExpanded="true">
683
+ <div class="space-y-3">
684
+ <div class="space-y-1">
685
+ <label class="text-white/50 text-[10px] uppercase">Element ID</label>
686
+ <input type="text" :value="selectedData.id || ''"
687
+ @input="updateField('id', ($event.target as HTMLInputElement).value)"
688
+ placeholder="unique-element-id"
689
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
690
+ </div>
691
+ <div class="space-y-1">
692
+ <label class="text-white/50 text-[10px] uppercase">Variant</label>
693
+ <select :value="selectedData.variant || 'primary'"
694
+ @change="updateField('variant', ($event.target as HTMLSelectElement).value)"
695
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
696
+ <option value="primary">Primary</option>
697
+ <option value="secondary">Secondary</option>
698
+ <option value="outline">Outline</option>
699
+ <option value="ghost">Ghost</option>
700
+ </select>
701
+ </div>
702
+ <div class="flex gap-4">
703
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
704
+ <input type="checkbox" :checked="selectedData.fullWidth ?? false"
705
+ @change="updateField('fullWidth', ($event.target as HTMLInputElement).checked)" />
706
+ Full Width
707
+ </label>
708
+ </div>
709
+ </div>
710
+ </CollapsibleSection>
711
+ </div>
712
+
713
+ <!-- Flex Title Element Special Handling -->
714
+ <div v-if="selectedType === 'title' && !isSlide" class="space-y-4">
715
+ <CollapsibleSection title="Title Options" icon="ph-thin ph-text-h" :defaultExpanded="true">
716
+ <div class="space-y-3">
717
+ <div class="space-y-1">
718
+ <label class="text-white/50 text-[10px] uppercase">Element ID</label>
719
+ <input type="text" :value="selectedData.id || ''"
720
+ @input="updateField('id', ($event.target as HTMLInputElement).value)"
721
+ placeholder="unique-element-id"
722
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
723
+ </div>
724
+ <div class="space-y-1">
725
+ <label class="text-white/50 text-[10px] uppercase">Size</label>
726
+ <select :value="selectedData.size || 'xl'"
727
+ @change="updateField('size', ($event.target as HTMLSelectElement).value)"
728
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
729
+ <option value="lg">Large</option>
730
+ <option value="xl">X-Large</option>
731
+ <option value="2xl">2X-Large</option>
732
+ <option value="3xl">3X-Large</option>
733
+ </select>
734
+ </div>
735
+ <div class="space-y-1">
736
+ <label class="text-white/50 text-[10px] uppercase">Align</label>
737
+ <select :value="selectedData.align || 'left'"
738
+ @change="updateField('align', ($event.target as HTMLSelectElement).value)"
739
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
740
+ <option value="left">Left</option>
741
+ <option value="center">Center</option>
742
+ <option value="right">Right</option>
743
+ </select>
744
+ </div>
745
+ </div>
746
+ </CollapsibleSection>
747
+ </div>
748
+
749
+ <!-- Flex Text Element Special Handling -->
750
+ <div v-if="selectedType === 'text' && !isSlide" class="space-y-4">
751
+ <CollapsibleSection title="Text Options" icon="ph-thin ph-text-align-left" :defaultExpanded="true">
752
+ <div class="space-y-3">
753
+ <div class="space-y-1">
754
+ <label class="text-white/50 text-[10px] uppercase">Element ID</label>
755
+ <input type="text" :value="selectedData.id || ''"
756
+ @input="updateField('id', ($event.target as HTMLInputElement).value)"
757
+ placeholder="unique-element-id"
758
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
759
+ </div>
760
+ <div class="space-y-1">
761
+ <label class="text-white/50 text-[10px] uppercase">Align</label>
762
+ <select :value="selectedData.align || 'left'"
763
+ @change="updateField('align', ($event.target as HTMLSelectElement).value)"
764
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
765
+ <option value="left">Left</option>
766
+ <option value="center">Center</option>
767
+ <option value="right">Right</option>
768
+ </select>
769
+ </div>
770
+ <div class="flex gap-4">
771
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
772
+ <input type="checkbox" :checked="selectedData.muted ?? false"
773
+ @change="updateField('muted', ($event.target as HTMLInputElement).checked)" />
774
+ Muted/Subtle
775
+ </label>
776
+ </div>
777
+ </div>
778
+ </CollapsibleSection>
779
+ </div>
780
+
781
+ <!-- Flex HTML Element Special Handling -->
782
+ <div v-if="selectedType === 'html' && !isSlide" class="space-y-4">
783
+ <CollapsibleSection title="HTML Options" icon="ph-thin ph-code" :defaultExpanded="true">
784
+ <div class="space-y-3">
785
+ <div class="space-y-1">
786
+ <label class="text-white/50 text-[10px] uppercase">Element ID</label>
787
+ <input type="text" :value="selectedData.id || ''"
788
+ @input="updateField('id', ($event.target as HTMLInputElement).value)"
789
+ placeholder="unique-element-id"
790
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
791
+ </div>
792
+ <div class="space-y-1">
793
+ <label class="text-white/50 text-[10px] uppercase">CSS Class</label>
794
+ <input type="text" :value="selectedData.class || ''"
795
+ @input="updateField('class', ($event.target as HTMLInputElement).value)"
796
+ placeholder="custom-class"
797
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
798
+ </div>
799
+ </div>
800
+ </CollapsibleSection>
801
+ </div>
802
+
803
+ <!-- Flex Spacer Element Special Handling -->
804
+ <div v-if="selectedType === 'spacer' && !isSlide" class="space-y-4">
805
+ <CollapsibleSection title="Spacer Options" icon="ph-thin ph-arrows-out-line-vertical" :defaultExpanded="true">
806
+ <div class="space-y-3">
807
+ <div class="space-y-1">
808
+ <label class="text-white/50 text-[10px] uppercase">Element ID</label>
809
+ <input type="text" :value="selectedData.id || ''"
810
+ @input="updateField('id', ($event.target as HTMLInputElement).value)"
811
+ placeholder="unique-element-id"
812
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
813
+ </div>
814
+ <div class="space-y-1">
815
+ <label class="text-white/50 text-[10px] uppercase">Size</label>
816
+ <select :value="selectedData.size || 'md'"
817
+ @change="updateField('size', ($event.target as HTMLSelectElement).value)"
818
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
819
+ <option value="none">None</option>
820
+ <option value="xs">XS</option>
821
+ <option value="sm">Small</option>
822
+ <option value="md">Medium</option>
823
+ <option value="lg">Large</option>
824
+ <option value="xl">X-Large</option>
825
+ <option value="2xl">2X-Large</option>
826
+ </select>
827
+ </div>
828
+ </div>
829
+ </CollapsibleSection>
262
830
  </div>
263
831
 
264
832
  <!-- Action Editor (for Buttons & Images) -->
@@ -336,6 +904,262 @@
336
904
  </div>
337
905
  </template>
338
906
 
907
+ <!-- Half Slide Special Handling -->
908
+ <div v-if="selectedType === 'half'" class="space-y-4 pt-4 border-t border-[#333]">
909
+ <div class="text-white/30 text-[10px] uppercase tracking-widest">Half Layout Options</div>
910
+
911
+ <div class="space-y-1">
912
+ <label class="text-white/50 text-[10px] uppercase">Image Side</label>
913
+ <select :value="selectedData.imageSide || 'left'"
914
+ @change="updateField('imageSide', ($event.target as HTMLSelectElement).value)"
915
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
916
+ <option value="left">Left</option>
917
+ <option value="right">Right</option>
918
+ </select>
919
+ </div>
920
+
921
+ <div class="space-y-1">
922
+ <label class="text-white/50 text-[10px] uppercase">Image CSS Class</label>
923
+ <input type="text" :value="selectedData.imageClass || ''"
924
+ @input="updateField('imageClass', ($event.target as HTMLInputElement).value)"
925
+ placeholder="custom-image-class"
926
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
927
+ </div>
928
+
929
+ <!-- Toggle between image and video -->
930
+ <div class="space-y-1">
931
+ <label class="flex items-center gap-2 text-white/50 text-[10px] uppercase cursor-pointer">
932
+ <input type="checkbox" :checked="!!selectedData.video"
933
+ @change="toggleHalfVideo"
934
+ class="rounded border-[#333]" />
935
+ Use Video Instead of Image
936
+ </label>
937
+ </div>
938
+
939
+ <div v-if="!selectedData.video" class="space-y-1">
940
+ <label class="text-white/50 text-[10px] uppercase">Image URL</label>
941
+ <input type="text" :value="selectedData.image || ''"
942
+ @input="updateField('image', ($event.target as HTMLInputElement).value)"
943
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
944
+ </div>
945
+
946
+ <div v-else class="space-y-2 pl-2 border-l-2 border-[#333]">
947
+ <div class="space-y-1">
948
+ <label class="text-white/50 text-[10px] uppercase">Video URL</label>
949
+ <input type="text" :value="selectedData.video?.src || ''"
950
+ @input="updateHalfVideo('src', ($event.target as HTMLInputElement).value)"
951
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
952
+ </div>
953
+ <div class="space-y-1">
954
+ <label class="text-white/50 text-[10px] uppercase">Poster Image</label>
955
+ <input type="text" :value="selectedData.video?.poster || ''"
956
+ @input="updateHalfVideo('poster', ($event.target as HTMLInputElement).value)"
957
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
958
+ </div>
959
+ <div class="flex flex-wrap gap-4">
960
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
961
+ <input type="checkbox" :checked="selectedData.video?.autoplay ?? true"
962
+ @change="updateHalfVideo('autoplay', ($event.target as HTMLInputElement).checked)" />
963
+ Autoplay
964
+ </label>
965
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
966
+ <input type="checkbox" :checked="selectedData.video?.loop ?? true"
967
+ @change="updateHalfVideo('loop', ($event.target as HTMLInputElement).checked)" />
968
+ Loop
969
+ </label>
970
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
971
+ <input type="checkbox" :checked="selectedData.video?.muted ?? true"
972
+ @change="updateHalfVideo('muted', ($event.target as HTMLInputElement).checked)" />
973
+ Muted
974
+ </label>
975
+ </div>
976
+ </div>
977
+
978
+ <div class="space-y-1">
979
+ <label class="text-white/50 text-[10px] uppercase">CTA Button Text</label>
980
+ <input type="text" :value="selectedData.cta || ''"
981
+ @input="updateField('cta', ($event.target as HTMLInputElement).value)"
982
+ placeholder="Call to Action"
983
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
984
+ </div>
985
+ </div>
986
+
987
+ <!-- Video Slide Special Handling -->
988
+ <div v-if="selectedType === 'video'" class="space-y-4 pt-4 border-t border-[#333]">
989
+ <div class="text-white/30 text-[10px] uppercase tracking-widest">Video Configuration</div>
990
+
991
+ <div class="space-y-1">
992
+ <label class="text-white/50 text-[10px] uppercase">Video URL</label>
993
+ <input type="text" :value="selectedData.video?.src || ''"
994
+ @input="updateVideoSlide('src', ($event.target as HTMLInputElement).value)"
995
+ placeholder="https://example.com/video.mp4"
996
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
997
+ </div>
998
+
999
+ <div class="space-y-1">
1000
+ <label class="text-white/50 text-[10px] uppercase">Poster Image</label>
1001
+ <input type="text" :value="selectedData.video?.poster || ''"
1002
+ @input="updateVideoSlide('poster', ($event.target as HTMLInputElement).value)"
1003
+ placeholder="https://example.com/poster.jpg"
1004
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
1005
+ </div>
1006
+
1007
+ <div class="space-y-1">
1008
+ <label class="text-white/50 text-[10px] uppercase">Title Overlay</label>
1009
+ <input type="text" :value="selectedData.title || ''"
1010
+ @input="updateField('title', ($event.target as HTMLInputElement).value)"
1011
+ placeholder="Optional title overlay"
1012
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
1013
+ </div>
1014
+
1015
+ <div class="flex flex-wrap gap-4">
1016
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
1017
+ <input type="checkbox" :checked="selectedData.video?.autoplay ?? true"
1018
+ @change="updateVideoSlide('autoplay', ($event.target as HTMLInputElement).checked)" />
1019
+ Autoplay
1020
+ </label>
1021
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
1022
+ <input type="checkbox" :checked="selectedData.video?.loop ?? true"
1023
+ @change="updateVideoSlide('loop', ($event.target as HTMLInputElement).checked)" />
1024
+ Loop
1025
+ </label>
1026
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
1027
+ <input type="checkbox" :checked="selectedData.video?.muted ?? true"
1028
+ @change="updateVideoSlide('muted', ($event.target as HTMLInputElement).checked)" />
1029
+ Muted
1030
+ </label>
1031
+ <label class="flex items-center gap-1 text-white/50 text-[10px]">
1032
+ <input type="checkbox" :checked="selectedData.video?.controls ?? false"
1033
+ @change="updateVideoSlide('controls', ($event.target as HTMLInputElement).checked)" />
1034
+ Controls
1035
+ </label>
1036
+ </div>
1037
+ </div>
1038
+
1039
+ <!-- Custom HTML Special Handling -->
1040
+ <div v-if="selectedType === 'custom'" class="space-y-4 pt-4 border-t border-[#333]">
1041
+ <div class="text-white/30 text-[10px] uppercase tracking-widest">Custom HTML</div>
1042
+
1043
+ <div class="space-y-1">
1044
+ <label class="text-white/50 text-[10px] uppercase">HTML Content</label>
1045
+ <textarea :value="selectedData.html || ''"
1046
+ @input="updateField('html', ($event.target as HTMLTextAreaElement).value)"
1047
+ rows="10"
1048
+ placeholder="<div class='my-custom'>Your HTML here</div>"
1049
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-xs text-white font-mono resize-none"></textarea>
1050
+ </div>
1051
+
1052
+ <div class="space-y-1">
1053
+ <label class="text-white/50 text-[10px] uppercase">CSS Styles</label>
1054
+ <textarea :value="selectedData.css || ''"
1055
+ @input="updateField('css', ($event.target as HTMLTextAreaElement).value)"
1056
+ rows="8"
1057
+ placeholder=".my-custom { color: white; }"
1058
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-xs text-white font-mono resize-none"></textarea>
1059
+ </div>
1060
+
1061
+ <div class="p-2 bg-yellow-900/10 border border-yellow-500/20 rounded text-[10px] text-yellow-200">
1062
+ <i class="ph-thin ph-warning mr-1"></i>
1063
+ HTML is rendered directly. Use with caution.
1064
+ </div>
1065
+ </div>
1066
+
1067
+ <!-- Chart Special Handling -->
1068
+ <div v-if="selectedType === 'chart'" class="space-y-4 pt-4 border-t border-[#333]">
1069
+ <div class="text-white/30 text-[10px] uppercase tracking-widest">Chart Configuration</div>
1070
+
1071
+ <div class="space-y-1">
1072
+ <label class="text-white/50 text-[10px] uppercase">Chart Type</label>
1073
+ <select :value="selectedData.chartType || 'bar'"
1074
+ @change="updateField('chartType', ($event.target as HTMLSelectElement).value)"
1075
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white">
1076
+ <option value="bar">Bar</option>
1077
+ <option value="line">Line</option>
1078
+ <option value="pie">Pie</option>
1079
+ <option value="doughnut">Doughnut</option>
1080
+ </select>
1081
+ </div>
1082
+
1083
+ <div class="space-y-1">
1084
+ <label class="text-white/50 text-[10px] uppercase">Chart Title</label>
1085
+ <input type="text" :value="selectedData.title || ''"
1086
+ @input="updateField('title', ($event.target as HTMLInputElement).value)"
1087
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
1088
+ </div>
1089
+
1090
+ <div class="space-y-1">
1091
+ <label class="text-white/50 text-[10px] uppercase">Subtitle</label>
1092
+ <input type="text" :value="selectedData.subtitle || ''"
1093
+ @input="updateField('subtitle', ($event.target as HTMLInputElement).value)"
1094
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
1095
+ </div>
1096
+
1097
+ <!-- Labels -->
1098
+ <div class="space-y-2">
1099
+ <div class="flex items-center justify-between">
1100
+ <span class="text-white/30 text-[10px] uppercase tracking-widest">Labels</span>
1101
+ <button @click="addChartLabel"
1102
+ class="text-[10px] px-2 py-0.5 bg-blue-600 hover:bg-blue-500 rounded text-white">
1103
+ <i class="ph-thin ph-plus mr-1"></i>Add
1104
+ </button>
1105
+ </div>
1106
+ <div v-for="(label, i) in (selectedData.data?.labels || [])" :key="i" class="flex gap-1">
1107
+ <input type="text" :value="label"
1108
+ @input="updateChartLabel(Number(i), ($event.target as HTMLInputElement).value)"
1109
+ class="flex-1 bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-white text-xs" />
1110
+ <button @click="removeChartLabel(Number(i))"
1111
+ class="w-6 h-6 flex items-center justify-center text-red-400 hover:text-red-300">
1112
+ <i class="ph-thin ph-trash text-[10px]"></i>
1113
+ </button>
1114
+ </div>
1115
+ </div>
1116
+
1117
+ <!-- Datasets -->
1118
+ <div class="space-y-2">
1119
+ <div class="flex items-center justify-between">
1120
+ <span class="text-white/30 text-[10px] uppercase tracking-widest">Datasets</span>
1121
+ <button @click="addChartDataset"
1122
+ class="text-[10px] px-2 py-0.5 bg-blue-600 hover:bg-blue-500 rounded text-white">
1123
+ <i class="ph-thin ph-plus mr-1"></i>Add
1124
+ </button>
1125
+ </div>
1126
+ <div v-for="(dataset, i) in (selectedData.data?.datasets || [])" :key="i"
1127
+ class="bg-[#1a1a1a] border border-[#333] rounded p-2 space-y-2">
1128
+ <div class="flex items-center justify-between">
1129
+ <span class="text-white/70 text-[10px] font-bold">{{ dataset.label || `Dataset ${Number(i) + 1}` }}</span>
1130
+ <button @click="removeChartDataset(Number(i))"
1131
+ class="text-red-400 hover:text-red-300 text-[10px]">
1132
+ <i class="ph-thin ph-trash"></i>
1133
+ </button>
1134
+ </div>
1135
+ <div class="space-y-1">
1136
+ <label class="text-white/50 text-[9px] uppercase">Label</label>
1137
+ <input type="text" :value="dataset.label"
1138
+ @input="updateChartDataset(Number(i), 'label', ($event.target as HTMLInputElement).value)"
1139
+ class="w-full bg-[#252525] border border-[#444] rounded px-2 py-1 text-[10px] text-white" />
1140
+ </div>
1141
+ <div class="space-y-1">
1142
+ <label class="text-white/50 text-[9px] uppercase">Values (comma-separated)</label>
1143
+ <input type="text" :value="(dataset.values || []).join(', ')"
1144
+ @input="updateChartDataset(Number(i), 'values', ($event.target as HTMLInputElement).value.split(',').map((v: string) => parseFloat(v.trim())).filter((v: number) => !isNaN(v)))"
1145
+ class="w-full bg-[#252525] border border-[#444] rounded px-2 py-1 text-[10px] text-white font-mono" />
1146
+ </div>
1147
+ <div class="space-y-1">
1148
+ <label class="text-white/50 text-[9px] uppercase">Color</label>
1149
+ <div class="flex gap-2">
1150
+ <input type="color" :value="dataset.color || '#3b82f6'"
1151
+ @input="updateChartDataset(Number(i), 'color', ($event.target as HTMLInputElement).value)"
1152
+ class="w-8 h-6 rounded border border-[#444] cursor-pointer bg-transparent" />
1153
+ <input type="text" :value="dataset.color || ''"
1154
+ @input="updateChartDataset(Number(i), 'color', ($event.target as HTMLInputElement).value)"
1155
+ placeholder="c:p, #hex, or color name"
1156
+ class="flex-1 bg-[#252525] border border-[#444] rounded px-2 py-1 text-[10px] text-white font-mono" />
1157
+ </div>
1158
+ </div>
1159
+ </div>
1160
+ </div>
1161
+ </div>
1162
+
339
1163
  <!-- Diagram Special Handling -->
340
1164
  <div v-if="selectedType === 'diagram'" class="space-y-4 pt-4 border-t border-[#333]">
341
1165
  <div class="text-white/30 text-[10px] uppercase tracking-widest">Diagram Flow</div>
@@ -363,7 +1187,7 @@
363
1187
  </template>
364
1188
 
365
1189
  <script setup lang="ts">
366
- import { computed } from 'vue';
1190
+ import { computed, ref } from 'vue';
367
1191
  import { useEditor } from '../../composables/useEditor';
368
1192
  import FieldEditor from './FieldEditor.vue';
369
1193
  import ActionEditor from './ActionEditor.vue';
@@ -374,9 +1198,30 @@ import ColorField from './ColorField.vue';
374
1198
  import DiagramNodeEditor from './inspectors/DiagramNodeEditor.vue';
375
1199
  import DiagramEdgeEditor from './inspectors/DiagramEdgeEditor.vue';
376
1200
  import { getFieldLabel } from './fieldLabels';
1201
+ import { resolveId, pathToKey, getElementPaths } from '../../core/elementResolver';
1202
+ import { elemId } from '../../core/elementId';
1203
+ import type { ElementPath } from '../../core/elementResolver';
377
1204
 
378
1205
  const editor = useEditor();
379
1206
 
1207
+ /** Convert editor selection path (e.g. "slides.0.features.1") to slideIndex and element path. */
1208
+ function selectionToPath(selection: string | null): { slideIndex: number; path: ElementPath; slidePath: string } | null {
1209
+ if (!selection || !selection.startsWith('slides.')) return null;
1210
+ const parts = selection.split('.');
1211
+ if (parts.length < 2) return null;
1212
+ const slideIndex = parseInt(parts[1], 10);
1213
+ if (isNaN(slideIndex)) return null;
1214
+ const slidePath = `slides.${slideIndex}`;
1215
+ if (parts.length === 2) {
1216
+ return { slideIndex, path: ['slide'], slidePath };
1217
+ }
1218
+ const pathSegments: ElementPath = [];
1219
+ for (let i = 2; i < parts.length; i++) {
1220
+ pathSegments.push(/^\d+$/.test(parts[i]) ? parseInt(parts[i], 10) : parts[i]);
1221
+ }
1222
+ return { slideIndex, path: pathSegments, slidePath };
1223
+ }
1224
+
380
1225
  const selectedData = computed(() => editor.getSelectedData());
381
1226
  const selectedType = computed(() => selectedData.value?.type);
382
1227
 
@@ -384,6 +1229,227 @@ const isSlide = computed(() => {
384
1229
  return /^slides\.\d+$/.test(editor.selection.value || '');
385
1230
  });
386
1231
 
1232
+ /** Resolved element ID info for the current selection (for display and customization). */
1233
+ const elementIdInfo = computed(() => {
1234
+ const sel = editor.selection.value;
1235
+ const parsed = selectionToPath(sel);
1236
+ if (!parsed) return null;
1237
+ const { slideIndex, path, slidePath } = parsed;
1238
+ const deck = editor.store.state.deck;
1239
+ const slide = deck?.slides?.[slideIndex];
1240
+ if (!slide) return null;
1241
+ const pathKey = pathToKey(path);
1242
+ const resolvedId = resolveId(slide, slideIndex, path);
1243
+ const fallbackId = path.length === 1 && path[0] === 'slide'
1244
+ ? elemId(slideIndex, 'slide')
1245
+ : elemId(slideIndex, ...path);
1246
+ const isSlideRoot = path.length === 1 && path[0] === 'slide';
1247
+ const slideAny = slide as { ids?: Record<string, string> };
1248
+ const customId = slideAny?.ids?.[pathKey] ?? '';
1249
+ return {
1250
+ resolvedId,
1251
+ pathKey,
1252
+ fallbackId,
1253
+ slideIndex,
1254
+ slide,
1255
+ slidePath,
1256
+ isSlideRoot,
1257
+ customId,
1258
+ canCustomizeViaIds: !isSlideRoot && pathKey !== '',
1259
+ hasObjectId: path.length >= 2 && selectedData.value && typeof (selectedData.value as { id?: string }).id === 'string',
1260
+ };
1261
+ });
1262
+
1263
+ /** List of all element paths and resolved IDs for the currently selected slide (when isSlide). */
1264
+ const slideElementIdsList = computed(() => {
1265
+ if (!isSlide.value || !selectedData.value) return [];
1266
+ const sel = editor.selection.value;
1267
+ const parsed = selectionToPath(sel);
1268
+ if (!parsed) return [];
1269
+ const { slideIndex, path: _ } = parsed;
1270
+ const slide = selectedData.value as import('../../core/types').BaseSlideData;
1271
+ const paths = getElementPaths(slide);
1272
+ return paths.map((path) => ({
1273
+ pathKey: pathToKey(path),
1274
+ resolvedId: resolveId(slide, slideIndex, path),
1275
+ }));
1276
+ });
1277
+
1278
+ // Element ID mapping refs
1279
+ const newIdPath = ref('');
1280
+ const newIdValue = ref('');
1281
+
1282
+ // Check if background is a video object
1283
+ const isVideoBackground = computed(() => {
1284
+ const bg = selectedData.value?.background;
1285
+ return bg && typeof bg === 'object' && bg.type === 'video';
1286
+ });
1287
+
1288
+ // Get background URL (works for both string and video object)
1289
+ const getBackgroundUrl = () => {
1290
+ const bg = selectedData.value?.background;
1291
+ if (!bg) return '';
1292
+ if (typeof bg === 'string') return bg;
1293
+ if (typeof bg === 'object' && bg.src) return bg.src;
1294
+ return '';
1295
+ };
1296
+
1297
+ // Toggle between string background and video background
1298
+ const toggleVideoBackground = (event: Event) => {
1299
+ const checked = (event.target as HTMLInputElement).checked;
1300
+ if (checked) {
1301
+ // Convert to video object
1302
+ const currentUrl = getBackgroundUrl();
1303
+ updateField('background', {
1304
+ type: 'video',
1305
+ src: currentUrl,
1306
+ autoplay: true,
1307
+ loop: true,
1308
+ muted: true
1309
+ });
1310
+ } else {
1311
+ // Convert back to string
1312
+ const currentUrl = getBackgroundUrl();
1313
+ updateField('background', currentUrl);
1314
+ }
1315
+ };
1316
+
1317
+ // Update video background properties
1318
+ const updateVideoBackground = (key: string, value: any) => {
1319
+ const current = selectedData.value?.background || { type: 'video' };
1320
+ const updated = { ...current, [key]: value };
1321
+ updateField('background', updated);
1322
+ };
1323
+
1324
+ // Element ID mapping methods
1325
+ const addElementIdMapping = () => {
1326
+ if (!newIdPath.value.trim() || !newIdValue.value.trim()) return;
1327
+ const path = `${editor.selection.value}.ids.${newIdPath.value}`;
1328
+ editor.store.updateNode(path, newIdValue.value);
1329
+ editor.commit();
1330
+ newIdPath.value = '';
1331
+ newIdValue.value = '';
1332
+ };
1333
+
1334
+ const removeElementIdMapping = (pathKey: string) => {
1335
+ const currentIds = { ...selectedData.value?.ids };
1336
+ delete currentIds[pathKey];
1337
+ const path = `${editor.selection.value}.ids`;
1338
+ editor.store.updateNode(path, Object.keys(currentIds).length > 0 ? currentIds : undefined);
1339
+ editor.commit();
1340
+ };
1341
+
1342
+ /** Update custom element ID via slide.ids (for current selection path). */
1343
+ const updateCustomElementId = (value: string) => {
1344
+ const info = elementIdInfo.value;
1345
+ if (!info || !info.canCustomizeViaIds) return;
1346
+ const currentIds = { ...(info.slide as { ids?: Record<string, string> }).ids };
1347
+ const trimmed = value.trim();
1348
+ if (trimmed) {
1349
+ currentIds[info.pathKey] = trimmed;
1350
+ } else {
1351
+ delete currentIds[info.pathKey];
1352
+ }
1353
+ editor.store.updateNode(`${info.slidePath}.ids`, Object.keys(currentIds).length > 0 ? currentIds : undefined);
1354
+ editor.commit();
1355
+ };
1356
+
1357
+ /** Copy resolved ID to clipboard. */
1358
+ const copyResolvedIdToClipboard = async () => {
1359
+ const info = elementIdInfo.value;
1360
+ if (!info?.resolvedId) return;
1361
+ try {
1362
+ await navigator.clipboard.writeText(info.resolvedId);
1363
+ } catch (_) {}
1364
+ };
1365
+
1366
+ // Slide reveal options
1367
+ const updateSlideReveal = (key: string, value: any) => {
1368
+ const path = `${editor.selection.value}.reveal.${key}`;
1369
+ editor.store.updateNode(path, value);
1370
+ editor.commit();
1371
+ };
1372
+
1373
+ // Chart editing methods
1374
+ const addChartLabel = () => {
1375
+ const labels = [...(selectedData.value?.data?.labels || []), 'New Label'];
1376
+ editor.store.updateNode(`${editor.selection.value}.data.labels`, labels);
1377
+ editor.commit();
1378
+ };
1379
+
1380
+ const updateChartLabel = (index: number, value: string) => {
1381
+ editor.store.updateNode(`${editor.selection.value}.data.labels.${index}`, value);
1382
+ editor.commit();
1383
+ };
1384
+
1385
+ const removeChartLabel = (index: number) => {
1386
+ const labels = [...(selectedData.value?.data?.labels || [])];
1387
+ labels.splice(index, 1);
1388
+ editor.store.updateNode(`${editor.selection.value}.data.labels`, labels);
1389
+ editor.commit();
1390
+ };
1391
+
1392
+ const addChartDataset = () => {
1393
+ const datasets = [...(selectedData.value?.data?.datasets || []), {
1394
+ label: 'New Dataset',
1395
+ values: [],
1396
+ color: '#3b82f6'
1397
+ }];
1398
+ editor.store.updateNode(`${editor.selection.value}.data.datasets`, datasets);
1399
+ editor.commit();
1400
+ };
1401
+
1402
+ const updateChartDataset = (index: number, key: string, value: any) => {
1403
+ editor.store.updateNode(`${editor.selection.value}.data.datasets.${index}.${key}`, value);
1404
+ editor.commit();
1405
+ };
1406
+
1407
+ const removeChartDataset = (index: number) => {
1408
+ const datasets = [...(selectedData.value?.data?.datasets || [])];
1409
+ datasets.splice(index, 1);
1410
+ editor.store.updateNode(`${editor.selection.value}.data.datasets`, datasets);
1411
+ editor.commit();
1412
+ };
1413
+
1414
+ // Video slide methods
1415
+ const updateVideoSlide = (key: string, value: any) => {
1416
+ // Ensure video object exists with type
1417
+ const current = selectedData.value?.video || { type: 'video' };
1418
+ const updated = { ...current, type: 'video', [key]: value };
1419
+ editor.store.updateNode(`${editor.selection.value}.video`, updated);
1420
+ editor.commit();
1421
+ };
1422
+
1423
+ // Half slide video methods
1424
+ const toggleHalfVideo = (event: Event) => {
1425
+ const useVideo = (event.target as HTMLInputElement).checked;
1426
+ if (useVideo) {
1427
+ // Set up video object
1428
+ editor.store.updateNode(`${editor.selection.value}.video`, {
1429
+ type: 'video',
1430
+ src: selectedData.value?.image || '',
1431
+ autoplay: true,
1432
+ loop: true,
1433
+ muted: true
1434
+ });
1435
+ // Clear image
1436
+ editor.store.updateNode(`${editor.selection.value}.image`, undefined);
1437
+ } else {
1438
+ // Get video src to use as image
1439
+ const videoSrc = selectedData.value?.video?.src || '';
1440
+ editor.store.updateNode(`${editor.selection.value}.image`, videoSrc);
1441
+ editor.store.updateNode(`${editor.selection.value}.video`, undefined);
1442
+ }
1443
+ editor.commit();
1444
+ };
1445
+
1446
+ const updateHalfVideo = (key: string, value: any) => {
1447
+ const current = selectedData.value?.video || { type: 'video' };
1448
+ const updated = { ...current, type: 'video', [key]: value };
1449
+ editor.store.updateNode(`${editor.selection.value}.video`, updated);
1450
+ editor.commit();
1451
+ };
1452
+
387
1453
  // Detect if a diagram node/edge is selected
388
1454
  // Path format: slides.X.nodes.Y or slides.X.edges.Y
389
1455
  const isDiagramNode = computed(() => {
@@ -400,6 +1466,11 @@ const isActionable = computed(() => {
400
1466
  return type === 'button' || type === 'image';
401
1467
  });
402
1468
 
1469
+ // Detect if this is a video slide (not a video element inside a flex slide)
1470
+ const isVideoSlide = computed(() => {
1471
+ return isSlide.value && selectedType.value === 'video';
1472
+ });
1473
+
403
1474
  // Helper to update diagram node - emits event to update VueFlow directly (bypasses store -> watch cycle)
404
1475
  const updateDiagramNode = (key: string, value: any) => {
405
1476
  // Parse selection path to get slide index and node id
@@ -476,7 +1547,7 @@ const stringArrayFields = computed(() => {
476
1547
 
477
1548
  const objectArrayFields = computed<Record<string, any[]>>(() => {
478
1549
  if (!selectedData.value) return {};
479
- const arrayKeys = ['timeline', 'steps', 'features', 'elements', 'datasets'];
1550
+ const arrayKeys = ['timeline', 'steps', 'features', 'elements', 'datasets', 'nodes', 'edges'];
480
1551
  return Object.fromEntries(
481
1552
  Object.entries(selectedData.value).filter(([key, val]) =>
482
1553
  arrayKeys.includes(key) && Array.isArray(val)
@@ -491,11 +1562,46 @@ const getItemLabel = (item: any, index: number) => {
491
1562
 
492
1563
  const getObjectTemplate = (key: string): any => {
493
1564
  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() },
1565
+ 'timeline': {
1566
+ id: '',
1567
+ date: '2024',
1568
+ title: 'New Event',
1569
+ description: 'Description',
1570
+ icon: '',
1571
+ dragKey: Date.now() + Math.random()
1572
+ },
1573
+ 'steps': {
1574
+ id: '',
1575
+ step: '1',
1576
+ title: 'New Step',
1577
+ description: 'Description',
1578
+ icon: '',
1579
+ dragKey: Date.now() + Math.random()
1580
+ },
1581
+ 'features': {
1582
+ id: '',
1583
+ icon: 'star',
1584
+ title: 'New Feature',
1585
+ desc: 'Description',
1586
+ class: '',
1587
+ dragKey: Date.now() + Math.random()
1588
+ },
497
1589
  'elements': { type: 'text', text: 'New text element', dragKey: Date.now() + Math.random() },
498
- 'datasets': { label: 'Dataset', values: [], dragKey: Date.now() + Math.random() },
1590
+ 'datasets': { label: 'Dataset', values: [], color: '#3b82f6', dragKey: Date.now() + Math.random() },
1591
+ 'nodes': {
1592
+ id: `node-${Date.now()}`,
1593
+ label: 'New Node',
1594
+ position: { x: 200, y: 200 },
1595
+ type: 'default',
1596
+ dragKey: Date.now() + Math.random()
1597
+ },
1598
+ 'edges': {
1599
+ id: `edge-${Date.now()}`,
1600
+ source: '',
1601
+ target: '',
1602
+ animated: false,
1603
+ dragKey: Date.now() + Math.random()
1604
+ }
499
1605
  };
500
1606
  return templates[key] || { title: 'New Item', dragKey: Date.now() + Math.random() };
501
1607
  };