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.
- package/README.md +63 -0
- package/dist/lumina-slides.js +21750 -19334
- package/dist/lumina-slides.umd.cjs +223 -223
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/LandingPage.vue +1 -1
- package/src/components/LuminaDeck.vue +237 -232
- package/src/components/base/LuminaElement.vue +2 -0
- package/src/components/layouts/LayoutFeatures.vue +125 -123
- package/src/components/layouts/LayoutFlex.vue +212 -212
- package/src/components/layouts/LayoutStatement.vue +5 -2
- package/src/components/layouts/LayoutSteps.vue +110 -108
- package/src/components/parts/FlexHtml.vue +65 -65
- package/src/components/parts/FlexImage.vue +81 -81
- package/src/components/site/SiteDocs.vue +3313 -3314
- package/src/components/site/SiteExamples.vue +66 -66
- package/src/components/studio/EditorLayoutChart.vue +18 -0
- package/src/components/studio/EditorLayoutCustom.vue +18 -0
- package/src/components/studio/EditorLayoutVideo.vue +18 -0
- package/src/components/studio/LuminaStudioEmbed.vue +68 -0
- package/src/components/studio/StudioEmbedRoot.vue +19 -0
- package/src/components/studio/StudioInspector.vue +1113 -7
- package/src/components/studio/StudioJsonEditor.vue +10 -3
- package/src/components/studio/StudioSettings.vue +658 -7
- package/src/components/studio/StudioToolbar.vue +26 -7
- package/src/composables/useElementState.ts +12 -1
- package/src/composables/useFlexLayout.ts +128 -128
- package/src/core/Lumina.ts +174 -113
- package/src/core/animationConfig.ts +10 -0
- package/src/core/elementController.ts +18 -0
- package/src/core/elementResolver.ts +4 -2
- package/src/core/schema.ts +503 -503
- package/src/core/store.ts +465 -465
- package/src/core/types.ts +26 -11
- package/src/index.ts +2 -2
- package/src/utils/prepareDeckForExport.ts +47 -0
- package/src/utils/templateInterpolation.ts +52 -52
- 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 "Element ID" 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="
|
|
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': {
|
|
495
|
-
|
|
496
|
-
|
|
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
|
};
|