lumina-slides 8.9.5 → 9.0.1
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/LUMINA_LLM_EXAMPLES.json +234 -0
- package/README.md +18 -18
- package/dist/lumina-slides.js +13153 -12619
- package/dist/lumina-slides.umd.cjs +217 -217
- package/dist/style.css +1 -1
- package/package.json +5 -4
- package/src/App.vue +16 -0
- package/src/animation/index.ts +11 -0
- package/src/animation/registry.ts +126 -0
- package/src/animation/stagger.ts +95 -0
- package/src/animation/types.ts +53 -0
- package/src/components/LandingPage.vue +229 -0
- package/src/components/LuminaDeck.vue +224 -0
- package/src/components/LuminaSpeakerNotes.vue +701 -0
- package/src/components/base/BaseSlide.vue +122 -0
- package/src/components/base/LuminaElement.vue +67 -0
- package/src/components/base/VideoPlayer.vue +204 -0
- package/src/components/layouts/LayoutAuto.vue +71 -0
- package/src/components/layouts/LayoutChart.vue +287 -0
- package/src/components/layouts/LayoutCustom.vue +92 -0
- package/src/components/layouts/LayoutDiagram.vue +253 -0
- package/src/components/layouts/LayoutFeatures.vue +121 -0
- package/src/components/layouts/LayoutFlex.vue +172 -0
- package/src/components/layouts/LayoutFree.vue +62 -0
- package/src/components/layouts/LayoutHalf.vue +127 -0
- package/src/components/layouts/LayoutStatement.vue +74 -0
- package/src/components/layouts/LayoutSteps.vue +106 -0
- package/src/components/layouts/LayoutTimeline.vue +104 -0
- package/src/components/layouts/LayoutVideo.vue +41 -0
- package/src/components/parts/FlexBullets.vue +45 -0
- package/src/components/parts/FlexButton.vue +132 -0
- package/src/components/parts/FlexImage.vue +54 -0
- package/src/components/parts/FlexOrdered.vue +44 -0
- package/src/components/parts/FlexSpacer.vue +13 -0
- package/src/components/parts/FlexStepper.vue +59 -0
- package/src/components/parts/FlexText.vue +29 -0
- package/src/components/parts/FlexTimeline.vue +67 -0
- package/src/components/parts/FlexTitle.vue +39 -0
- package/src/components/parts/LuminaBackground.vue +100 -0
- package/src/components/site/LivePreview.vue +101 -0
- package/src/components/site/SiteApi.vue +301 -0
- package/src/components/site/SiteDashboard.vue +604 -0
- package/src/components/site/SiteDocs.vue +3273 -0
- package/src/components/site/SiteExamples.vue +65 -0
- package/src/components/site/SiteFooter.vue +6 -0
- package/src/components/site/SiteHome.vue +362 -0
- package/src/components/site/SiteNavBar.vue +122 -0
- package/src/components/site/SitePlayground.vue +389 -0
- package/src/components/site/SitePromptBuilder.vue +266 -0
- package/src/components/site/SiteUserMenu.vue +90 -0
- package/src/components/studio/ActionEditor.vue +108 -0
- package/src/components/studio/ArrayEditor.vue +124 -0
- package/src/components/studio/CollapsibleSection.vue +33 -0
- package/src/components/studio/ColorField.vue +22 -0
- package/src/components/studio/EditorCanvas.vue +326 -0
- package/src/components/studio/EditorLayoutFeatures.vue +18 -0
- package/src/components/studio/EditorLayoutFixed.vue +46 -0
- package/src/components/studio/EditorLayoutFlex.vue +133 -0
- package/src/components/studio/EditorLayoutHalf.vue +18 -0
- package/src/components/studio/EditorLayoutStatement.vue +18 -0
- package/src/components/studio/EditorLayoutSteps.vue +18 -0
- package/src/components/studio/EditorLayoutTimeline.vue +18 -0
- package/src/components/studio/EditorNode.vue +89 -0
- package/src/components/studio/FieldEditor.vue +133 -0
- package/src/components/studio/IconPicker.vue +109 -0
- package/src/components/studio/LayerItem.vue +117 -0
- package/src/components/studio/LuminaStudio.vue +30 -0
- package/src/components/studio/SaveSuccessModal.vue +138 -0
- package/src/components/studio/SlideNavigator.vue +373 -0
- package/src/components/studio/SliderField.vue +44 -0
- package/src/components/studio/StudioInspector.vue +595 -0
- package/src/components/studio/StudioJsonEditor.vue +191 -0
- package/src/components/studio/StudioLayers.vue +145 -0
- package/src/components/studio/StudioSettings.vue +514 -0
- package/src/components/studio/StudioSidebar.vue +29 -0
- package/src/components/studio/StudioToolbar.vue +222 -0
- package/src/components/studio/fieldLabels.ts +224 -0
- package/src/components/studio/inspectors/DiagramEdgeEditor.vue +77 -0
- package/src/components/studio/inspectors/DiagramNodeEditor.vue +117 -0
- package/src/components/studio/nodes/StudioDiagramNode.vue +138 -0
- package/src/composables/useAuth.ts +87 -0
- package/src/composables/useEditor.ts +224 -0
- package/src/composables/useElementState.ts +81 -0
- package/src/composables/useFlexLayout.ts +122 -0
- package/src/composables/useKeyboard.ts +45 -0
- package/src/composables/useLumina.ts +32 -0
- package/src/composables/useStudio.ts +87 -0
- package/src/composables/useSwipeNav.ts +53 -0
- package/src/composables/useTransition.ts +373 -0
- package/src/core/Lumina.ts +819 -0
- package/src/core/animationConfig.ts +251 -0
- package/src/core/compression.ts +34 -0
- package/src/core/elementController.ts +170 -0
- package/src/core/elementId.ts +27 -0
- package/src/core/elementResolver.ts +207 -0
- package/src/core/events.ts +53 -0
- package/src/core/fonts.ts +100 -0
- package/src/core/presets.ts +231 -0
- package/src/core/prompts.ts +272 -0
- package/src/core/schema.ts +478 -0
- package/src/core/speaker-channel.ts +250 -0
- package/src/core/store.ts +465 -0
- package/src/core/theme.ts +666 -0
- package/src/core/types.ts +1615 -0
- package/src/directives/vStudio.ts +45 -0
- package/src/index.ts +175 -0
- package/src/main.ts +17 -0
- package/src/router/index.ts +92 -0
- package/src/style/main.css +462 -0
- package/src/utils/deep.ts +127 -0
- package/src/utils/firebase.ts +184 -0
- package/src/utils/streaming.ts +134 -0
- package/src/views/DashboardView.vue +32 -0
- package/src/views/DeckView.vue +289 -0
- package/src/views/HomeView.vue +17 -0
- package/src/views/SiteLayout.vue +21 -0
- package/src/views/StudioView.vue +61 -0
- package/src/vite-env.d.ts +6 -0
- package/IMPLEMENTATION.md +0 -418
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative" ref="menuRef">
|
|
3
|
+
<!-- Avatar Trigger -->
|
|
4
|
+
<button @click="isOpen = !isOpen"
|
|
5
|
+
class="flex items-center gap-2 rounded-full hover:bg-white/10 p-1 pr-3 transition border border-transparent hover:border-white/10">
|
|
6
|
+
|
|
7
|
+
<img v-if="avatarSrc" :src="avatarSrc" @error="handleImageError"
|
|
8
|
+
class="w-8 h-8 rounded-full border border-white/20 object-cover bg-neutral-800" alt="Avatar">
|
|
9
|
+
|
|
10
|
+
<div v-else
|
|
11
|
+
class="w-8 h-8 rounded-full border border-white/20 bg-gradient-to-br from-blue-600 to-purple-600 flex items-center justify-center text-xs font-bold text-white shadow-inner">
|
|
12
|
+
{{ initials }}
|
|
13
|
+
</div>
|
|
14
|
+
<span class="text-sm font-medium text-white/90 hidden md:block max-w-[100px] truncate">
|
|
15
|
+
{{ user?.displayName?.split(' ')[0] }}
|
|
16
|
+
</span>
|
|
17
|
+
<i class="ph-thin ph-caret-down text-xs text-white/50 transition-transform duration-200"
|
|
18
|
+
:class="{ 'rotate-180': isOpen }"></i>
|
|
19
|
+
</button>
|
|
20
|
+
|
|
21
|
+
<!-- Dropdown -->
|
|
22
|
+
<Transition enter-active-class="transition ease-out duration-200"
|
|
23
|
+
enter-from-class="opacity-0 translate-y-2 scale-95" enter-to-class="opacity-100 translate-y-0 scale-100"
|
|
24
|
+
leave-active-class="transition ease-in duration-150" leave-from-class="opacity-100 translate-y-0 scale-100"
|
|
25
|
+
leave-to-class="opacity-0 translate-y-2 scale-95">
|
|
26
|
+
<div v-if="isOpen"
|
|
27
|
+
class="absolute right-0 top-full mt-2 w-56 bg-[#111] border border-white/10 rounded-xl shadow-2xl overflow-hidden z-[100]">
|
|
28
|
+
|
|
29
|
+
<!-- Header -->
|
|
30
|
+
<div class="p-4 border-b border-white/5 bg-white/5">
|
|
31
|
+
<p class="text-sm font-bold text-white truncate">{{ user?.displayName }}</p>
|
|
32
|
+
<p class="text-xs text-white/50 truncate">{{ user?.email }}</p>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- Menu Items -->
|
|
36
|
+
<div class="p-1">
|
|
37
|
+
<button @click="navigate('dashboard')"
|
|
38
|
+
class="w-full text-left px-3 py-2 rounded-lg text-sm text-white/80 hover:text-white hover:bg-white/10 transition flex items-center gap-3">
|
|
39
|
+
<i class="ph-thin ph-stack text-blue-400 w-4"></i>
|
|
40
|
+
My Decks
|
|
41
|
+
</button>
|
|
42
|
+
<button @click="navigate('examples')"
|
|
43
|
+
class="w-full text-left px-3 py-2 rounded-lg text-sm text-white/80 hover:text-white hover:bg-white/10 transition flex items-center gap-3">
|
|
44
|
+
<i class="ph-thin ph-compass text-purple-400 w-4"></i>
|
|
45
|
+
Explore
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="h-px bg-white/5 mx-2 my-1"></div>
|
|
50
|
+
|
|
51
|
+
<div class="p-1">
|
|
52
|
+
<button @click="handleLogout"
|
|
53
|
+
class="w-full text-left px-3 py-2 rounded-lg text-sm text-red-400 hover:text-red-300 hover:bg-red-500/10 transition flex items-center gap-3">
|
|
54
|
+
<i class="ph-thin ph-sign-out w-4"></i>
|
|
55
|
+
Sign Out
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</Transition>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<script setup lang="ts">
|
|
64
|
+
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
|
65
|
+
import { useRouter } from 'vue-router';
|
|
66
|
+
import { useAuth } from '../../composables/useAuth';
|
|
67
|
+
|
|
68
|
+
// const emit = defineEmits(['navigate']); // Deprecated
|
|
69
|
+
const router = useRouter();
|
|
70
|
+
const { user, logout } = useAuth();
|
|
71
|
+
const isOpen = ref(false);
|
|
72
|
+
const menuRef = ref<HTMLElement | null>(null);
|
|
73
|
+
const imageError = ref(false);
|
|
74
|
+
|
|
75
|
+
// ... (watch and computed stay same)
|
|
76
|
+
|
|
77
|
+
const navigate = (page: string) => {
|
|
78
|
+
isOpen.value = false;
|
|
79
|
+
router.push({ name: page });
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handleLogout = async () => {
|
|
83
|
+
isOpen.value = false;
|
|
84
|
+
await logout();
|
|
85
|
+
router.push({ name: 'home' });
|
|
86
|
+
};
|
|
87
|
+
// ... rest of script stays same but we need to re-copy it because we replace the whole script block in previous steps if I am not careful.
|
|
88
|
+
// Wait, I should just replace the specific lines or the whole block carefully.
|
|
89
|
+
// Let's replace the top imports and the navigate function.
|
|
90
|
+
</script>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="space-y-2">
|
|
3
|
+
<div class="text-white/30 text-[10px] uppercase tracking-widest">Action</div>
|
|
4
|
+
|
|
5
|
+
<!-- Action Type Selector -->
|
|
6
|
+
<div class="space-y-1">
|
|
7
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">Type</label>
|
|
8
|
+
<select :value="actionType || 'none'" @change="updateActionType"
|
|
9
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white focus:border-blue-500 focus:outline-none">
|
|
10
|
+
<option value="none">None</option>
|
|
11
|
+
<option value="url">Open URL</option>
|
|
12
|
+
<option value="slide">Go to Slide</option>
|
|
13
|
+
<option value="download">Download File</option>
|
|
14
|
+
<option value="event">Custom Event</option>
|
|
15
|
+
</select>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- URL Field (for url/download) -->
|
|
19
|
+
<div v-if="actionType === 'url' || actionType === 'download'" class="space-y-1">
|
|
20
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">
|
|
21
|
+
{{ actionType === 'download' ? 'File URL' : 'Link URL' }}
|
|
22
|
+
</label>
|
|
23
|
+
<input type="url" :value="href || ''" @input="updateHref" placeholder="https://example.com"
|
|
24
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white focus:border-blue-500 focus:outline-none" />
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- Target Selector (for url) -->
|
|
28
|
+
<div v-if="actionType === 'url'" class="space-y-1">
|
|
29
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">Open In</label>
|
|
30
|
+
<div class="flex gap-2">
|
|
31
|
+
<button @click="emit('update', 'target', '_blank')"
|
|
32
|
+
class="flex-1 px-2 py-1 text-[10px] rounded border transition"
|
|
33
|
+
:class="target !== '_self' ? 'bg-blue-600 border-blue-500 text-white' : 'bg-[#1a1a1a] border-[#333] text-white/50'">
|
|
34
|
+
New Tab
|
|
35
|
+
</button>
|
|
36
|
+
<button @click="emit('update', 'target', '_self')"
|
|
37
|
+
class="flex-1 px-2 py-1 text-[10px] rounded border transition"
|
|
38
|
+
:class="target === '_self' ? 'bg-blue-600 border-blue-500 text-white' : 'bg-[#1a1a1a] border-[#333] text-white/50'">
|
|
39
|
+
Same Tab
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<!-- Slide Selector -->
|
|
45
|
+
<div v-if="actionType === 'slide'" class="space-y-1">
|
|
46
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">Go to Slide</label>
|
|
47
|
+
<select :value="gotoSlide ?? ''" @change="updateGotoSlide"
|
|
48
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white focus:border-blue-500 focus:outline-none">
|
|
49
|
+
<option value="">Select slide...</option>
|
|
50
|
+
<option v-for="(_, i) in slideCount" :key="i" :value="i">
|
|
51
|
+
Slide {{ i + 1 }}
|
|
52
|
+
</option>
|
|
53
|
+
</select>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Custom Action (for event) -->
|
|
57
|
+
<div v-if="actionType === 'event'" class="space-y-1">
|
|
58
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">Action ID</label>
|
|
59
|
+
<input type="text" :value="action || ''" @input="updateAction" placeholder="custom-action-id"
|
|
60
|
+
class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1.5 text-white font-mono text-xs focus:border-blue-500 focus:outline-none" />
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<script setup lang="ts">
|
|
66
|
+
import { computed } from 'vue';
|
|
67
|
+
import { useEditor } from '../../composables/useEditor';
|
|
68
|
+
|
|
69
|
+
const props = defineProps<{
|
|
70
|
+
actionType?: string;
|
|
71
|
+
href?: string;
|
|
72
|
+
gotoSlide?: number;
|
|
73
|
+
target?: string;
|
|
74
|
+
action?: string;
|
|
75
|
+
}>();
|
|
76
|
+
|
|
77
|
+
const emit = defineEmits<{
|
|
78
|
+
(e: 'update', key: string, value: any): void;
|
|
79
|
+
}>();
|
|
80
|
+
|
|
81
|
+
const editor = useEditor();
|
|
82
|
+
const slideCount = computed(() => editor.store.state.deck?.slides?.length || 0);
|
|
83
|
+
|
|
84
|
+
const updateActionType = (e: Event) => {
|
|
85
|
+
const value = (e.target as HTMLSelectElement).value;
|
|
86
|
+
emit('update', 'actionType', value === 'none' ? undefined : value);
|
|
87
|
+
// Clear related fields when type changes
|
|
88
|
+
if (value !== 'url' && value !== 'download') {
|
|
89
|
+
emit('update', 'href', undefined);
|
|
90
|
+
}
|
|
91
|
+
if (value !== 'slide') {
|
|
92
|
+
emit('update', 'gotoSlide', undefined);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const updateHref = (e: Event) => {
|
|
97
|
+
emit('update', 'href', (e.target as HTMLInputElement).value || undefined);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const updateGotoSlide = (e: Event) => {
|
|
101
|
+
const value = (e.target as HTMLSelectElement).value;
|
|
102
|
+
emit('update', 'gotoSlide', value ? parseInt(value) : undefined);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const updateAction = (e: Event) => {
|
|
106
|
+
emit('update', 'action', (e.target as HTMLInputElement).value || undefined);
|
|
107
|
+
};
|
|
108
|
+
</script>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="space-y-2">
|
|
3
|
+
<!-- Array Header -->
|
|
4
|
+
<div class="flex items-center justify-between">
|
|
5
|
+
<label class="text-white/50 uppercase tracking-wider text-[10px]">{{ label }} ({{ items.length }})</label>
|
|
6
|
+
<button @click="addItem"
|
|
7
|
+
class="text-[10px] px-2 py-0.5 bg-blue-600 hover:bg-blue-500 rounded text-white transition">
|
|
8
|
+
<i class="ph-thin ph-plus mr-1"></i>Add
|
|
9
|
+
</button>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<!-- Items List -->
|
|
13
|
+
<div class="space-y-2">
|
|
14
|
+
<div v-for="(item, i) in items" :key="i" class="bg-[#1a1a1a] border border-[#333] rounded p-2">
|
|
15
|
+
|
|
16
|
+
<!-- Item Header -->
|
|
17
|
+
<div class="flex items-center justify-between mb-2 pb-2 border-b border-[#333]">
|
|
18
|
+
<span class="text-white/70 text-[10px] font-bold uppercase">{{ getItemLabel(item, i) }}</span>
|
|
19
|
+
<div class="flex gap-1">
|
|
20
|
+
<button v-if="i > 0" @click="moveItem(i, i - 1)"
|
|
21
|
+
class="w-5 h-5 flex items-center justify-center text-white/50 hover:text-white text-[10px]">
|
|
22
|
+
<i class="ph-thin ph-caret-up"></i>
|
|
23
|
+
</button>
|
|
24
|
+
<button v-if="i < items.length - 1" @click="moveItem(i, i + 1)"
|
|
25
|
+
class="w-5 h-5 flex items-center justify-center text-white/50 hover:text-white text-[10px]">
|
|
26
|
+
<i class="ph-thin ph-caret-down"></i>
|
|
27
|
+
</button>
|
|
28
|
+
<button @click="removeItem(i)"
|
|
29
|
+
class="w-5 h-5 flex items-center justify-center text-red-400 hover:text-red-300 text-[10px]">
|
|
30
|
+
<i class="ph-thin ph-trash"></i>
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- Item Fields -->
|
|
36
|
+
<div class="space-y-2">
|
|
37
|
+
<template v-for="(value, key) in getEditableItemFields(item)" :key="key">
|
|
38
|
+
<div class="space-y-1">
|
|
39
|
+
<label class="text-white/40 text-[9px] uppercase">{{ key }}</label>
|
|
40
|
+
|
|
41
|
+
<!-- Enum Select -->
|
|
42
|
+
<select v-if="getEnumOptions(key as string)" :value="value"
|
|
43
|
+
@change="updateItemField(i, key as string, ($event.target as HTMLSelectElement).value)"
|
|
44
|
+
class="w-full bg-[#111] border border-[#333] rounded px-2 py-1 text-white text-xs focus:border-blue-500 focus:outline-none">
|
|
45
|
+
<option v-for="opt in getEnumOptions(key as string)" :key="opt" :value="opt">{{ opt }}
|
|
46
|
+
</option>
|
|
47
|
+
</select>
|
|
48
|
+
|
|
49
|
+
<!-- Textarea for long text -->
|
|
50
|
+
<textarea v-else-if="isLongTextField(key as string, value)"
|
|
51
|
+
:value="value as string | number"
|
|
52
|
+
@input="updateItemField(i, key as string, ($event.target as HTMLTextAreaElement).value)"
|
|
53
|
+
rows="2"
|
|
54
|
+
class="w-full bg-[#111] border border-[#333] rounded px-2 py-1 text-white text-xs focus:border-blue-500 focus:outline-none resize-none"></textarea>
|
|
55
|
+
|
|
56
|
+
<!-- Default Input -->
|
|
57
|
+
<input v-else type="text" :value="value"
|
|
58
|
+
@input="updateItemField(i, key as string, ($event.target as HTMLInputElement).value)"
|
|
59
|
+
class="w-full bg-[#111] border border-[#333] rounded px-2 py-1 text-white text-xs focus:border-blue-500 focus:outline-none" />
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
const props = defineProps<{
|
|
70
|
+
label: string;
|
|
71
|
+
items: any[];
|
|
72
|
+
basePath: string;
|
|
73
|
+
itemTemplate: Record<string, any>;
|
|
74
|
+
}>();
|
|
75
|
+
|
|
76
|
+
const emit = defineEmits<{
|
|
77
|
+
(e: 'update', path: string, value: any): void;
|
|
78
|
+
(e: 'add', item: any): void;
|
|
79
|
+
(e: 'remove', index: number): void;
|
|
80
|
+
(e: 'move', payload: { from: number, to: number }): void;
|
|
81
|
+
}>();
|
|
82
|
+
|
|
83
|
+
const getItemLabel = (item: any, index: number) => {
|
|
84
|
+
return item.title || item.date || item.step || item.label || item.name || `#${index + 1}`;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getEditableItemFields = (item: any) => {
|
|
88
|
+
return Object.fromEntries(
|
|
89
|
+
Object.entries(item).filter(([_, val]) =>
|
|
90
|
+
typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean'
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const getEnumOptions = (key: string): string[] | null => {
|
|
96
|
+
const enums: Record<string, string[]> = {
|
|
97
|
+
'variant': ['primary', 'secondary', 'outline', 'ghost'],
|
|
98
|
+
'size': ['sm', 'md', 'lg'],
|
|
99
|
+
'imageSide': ['left', 'right'],
|
|
100
|
+
};
|
|
101
|
+
return enums[key] || null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const isLongTextField = (key: string, value: any): boolean => {
|
|
105
|
+
const longFields = ['description', 'desc', 'text', 'content'];
|
|
106
|
+
return longFields.includes(key) || (typeof value === 'string' && value.length > 40);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const updateItemField = (index: number, key: string, value: any) => {
|
|
110
|
+
emit('update', `${props.basePath}.${index}.${key}`, value);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const addItem = () => {
|
|
114
|
+
emit('add', JSON.parse(JSON.stringify(props.itemTemplate)));
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const removeItem = (index: number) => {
|
|
118
|
+
emit('remove', index);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const moveItem = (from: number, to: number) => {
|
|
122
|
+
emit('move', { from, to });
|
|
123
|
+
};
|
|
124
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="border border-[#333] rounded bg-[#151515] overflow-hidden">
|
|
3
|
+
<button @click="isExpanded = !isExpanded"
|
|
4
|
+
class="w-full flex items-center justify-between px-3 py-2 bg-[#1a1a1a] hover:bg-[#222] transition text-left group">
|
|
5
|
+
<div class="flex items-center gap-2">
|
|
6
|
+
<i v-if="icon"
|
|
7
|
+
:class="[icon, 'text-white/40 group-hover:text-white/70 transition w-4 text-center']"></i>
|
|
8
|
+
<span
|
|
9
|
+
class="text-[10px] uppercase tracking-widest font-bold text-white/50 group-hover:text-white/70 transition">
|
|
10
|
+
{{ title }}
|
|
11
|
+
</span>
|
|
12
|
+
</div>
|
|
13
|
+
<i class="ph-thin ph-caret-down text-[10px] text-white/30 transition-transform duration-200"
|
|
14
|
+
:class="{ 'rotate-180': isExpanded }"></i>
|
|
15
|
+
</button>
|
|
16
|
+
|
|
17
|
+
<div v-show="isExpanded" class="p-3 border-t border-[#333] space-y-3">
|
|
18
|
+
<slot></slot>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { ref } from 'vue';
|
|
25
|
+
|
|
26
|
+
const props = defineProps<{
|
|
27
|
+
title: string;
|
|
28
|
+
icon?: string;
|
|
29
|
+
defaultExpanded?: boolean;
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
const isExpanded = ref(props.defaultExpanded ?? false);
|
|
33
|
+
</script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-center gap-2">
|
|
3
|
+
<input type="color" :value="String(value || '#ffffff')"
|
|
4
|
+
@input="emit('update', fieldKey, ($event.target as HTMLInputElement).value)"
|
|
5
|
+
class="w-6 h-6 rounded border border-[#333] cursor-pointer bg-transparent" />
|
|
6
|
+
<span class="flex-1 text-white/70">{{ label }}</span>
|
|
7
|
+
<input type="text" :value="value" @input="emit('update', fieldKey, ($event.target as HTMLInputElement).value)"
|
|
8
|
+
class="w-16 bg-[#1a1a1a] border border-[#333] rounded px-1.5 py-0.5 text-[10px] font-mono text-white/70 focus:border-blue-500 focus:outline-none" />
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
defineProps<{
|
|
14
|
+
label: string;
|
|
15
|
+
fieldKey: string;
|
|
16
|
+
value: any;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
(e: 'update', key: string, value: string): void;
|
|
21
|
+
}>();
|
|
22
|
+
</script>
|