lumina-slides 8.9.4 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LUMINA_LLM_EXAMPLES.json +234 -0
  2. package/README.md +18 -18
  3. package/dist/lumina-slides.js +13207 -12659
  4. package/dist/lumina-slides.umd.cjs +215 -215
  5. package/dist/style.css +1 -1
  6. package/package.json +5 -4
  7. package/src/App.vue +16 -0
  8. package/src/animation/index.ts +11 -0
  9. package/src/animation/registry.ts +126 -0
  10. package/src/animation/stagger.ts +95 -0
  11. package/src/animation/types.ts +53 -0
  12. package/src/components/LandingPage.vue +229 -0
  13. package/src/components/LuminaDeck.vue +224 -0
  14. package/src/components/LuminaSpeakerNotes.vue +701 -0
  15. package/src/components/base/BaseSlide.vue +122 -0
  16. package/src/components/base/LuminaElement.vue +67 -0
  17. package/src/components/base/VideoPlayer.vue +204 -0
  18. package/src/components/layouts/LayoutAuto.vue +71 -0
  19. package/src/components/layouts/LayoutChart.vue +287 -0
  20. package/src/components/layouts/LayoutCustom.vue +92 -0
  21. package/src/components/layouts/LayoutDiagram.vue +253 -0
  22. package/src/components/layouts/LayoutFeatures.vue +121 -0
  23. package/src/components/layouts/LayoutFlex.vue +172 -0
  24. package/src/components/layouts/LayoutFree.vue +62 -0
  25. package/src/components/layouts/LayoutHalf.vue +127 -0
  26. package/src/components/layouts/LayoutStatement.vue +74 -0
  27. package/src/components/layouts/LayoutSteps.vue +106 -0
  28. package/src/components/layouts/LayoutTimeline.vue +104 -0
  29. package/src/components/layouts/LayoutVideo.vue +41 -0
  30. package/src/components/parts/FlexBullets.vue +45 -0
  31. package/src/components/parts/FlexButton.vue +132 -0
  32. package/src/components/parts/FlexImage.vue +54 -0
  33. package/src/components/parts/FlexOrdered.vue +44 -0
  34. package/src/components/parts/FlexSpacer.vue +13 -0
  35. package/src/components/parts/FlexStepper.vue +59 -0
  36. package/src/components/parts/FlexText.vue +29 -0
  37. package/src/components/parts/FlexTimeline.vue +67 -0
  38. package/src/components/parts/FlexTitle.vue +39 -0
  39. package/src/components/parts/LuminaBackground.vue +100 -0
  40. package/src/components/site/LivePreview.vue +101 -0
  41. package/src/components/site/SiteApi.vue +301 -0
  42. package/src/components/site/SiteDashboard.vue +604 -0
  43. package/src/components/site/SiteDocs.vue +3267 -0
  44. package/src/components/site/SiteExamples.vue +65 -0
  45. package/src/components/site/SiteFooter.vue +6 -0
  46. package/src/components/site/SiteHome.vue +362 -0
  47. package/src/components/site/SiteNavBar.vue +122 -0
  48. package/src/components/site/SitePlayground.vue +389 -0
  49. package/src/components/site/SitePromptBuilder.vue +266 -0
  50. package/src/components/site/SiteUserMenu.vue +90 -0
  51. package/src/components/studio/ActionEditor.vue +108 -0
  52. package/src/components/studio/ArrayEditor.vue +124 -0
  53. package/src/components/studio/CollapsibleSection.vue +33 -0
  54. package/src/components/studio/ColorField.vue +22 -0
  55. package/src/components/studio/EditorCanvas.vue +326 -0
  56. package/src/components/studio/EditorLayoutFeatures.vue +18 -0
  57. package/src/components/studio/EditorLayoutFixed.vue +46 -0
  58. package/src/components/studio/EditorLayoutFlex.vue +133 -0
  59. package/src/components/studio/EditorLayoutHalf.vue +18 -0
  60. package/src/components/studio/EditorLayoutStatement.vue +18 -0
  61. package/src/components/studio/EditorLayoutSteps.vue +18 -0
  62. package/src/components/studio/EditorLayoutTimeline.vue +18 -0
  63. package/src/components/studio/EditorNode.vue +89 -0
  64. package/src/components/studio/FieldEditor.vue +133 -0
  65. package/src/components/studio/IconPicker.vue +109 -0
  66. package/src/components/studio/LayerItem.vue +117 -0
  67. package/src/components/studio/LuminaStudio.vue +30 -0
  68. package/src/components/studio/SaveSuccessModal.vue +138 -0
  69. package/src/components/studio/SlideNavigator.vue +373 -0
  70. package/src/components/studio/SliderField.vue +44 -0
  71. package/src/components/studio/StudioInspector.vue +595 -0
  72. package/src/components/studio/StudioJsonEditor.vue +191 -0
  73. package/src/components/studio/StudioLayers.vue +145 -0
  74. package/src/components/studio/StudioSettings.vue +514 -0
  75. package/src/components/studio/StudioSidebar.vue +29 -0
  76. package/src/components/studio/StudioToolbar.vue +222 -0
  77. package/src/components/studio/fieldLabels.ts +224 -0
  78. package/src/components/studio/inspectors/DiagramEdgeEditor.vue +77 -0
  79. package/src/components/studio/inspectors/DiagramNodeEditor.vue +117 -0
  80. package/src/components/studio/nodes/StudioDiagramNode.vue +138 -0
  81. package/src/composables/useAuth.ts +87 -0
  82. package/src/composables/useEditor.ts +224 -0
  83. package/src/composables/useElementState.ts +81 -0
  84. package/src/composables/useFlexLayout.ts +122 -0
  85. package/src/composables/useKeyboard.ts +45 -0
  86. package/src/composables/useLumina.ts +32 -0
  87. package/src/composables/useStudio.ts +87 -0
  88. package/src/composables/useSwipeNav.ts +53 -0
  89. package/src/composables/useTransition.ts +373 -0
  90. package/src/core/Lumina.ts +819 -0
  91. package/src/core/animationConfig.ts +251 -0
  92. package/src/core/compression.ts +34 -0
  93. package/src/core/elementController.ts +170 -0
  94. package/src/core/elementId.ts +27 -0
  95. package/src/core/elementResolver.ts +207 -0
  96. package/src/core/events.ts +53 -0
  97. package/src/core/fonts.ts +100 -0
  98. package/src/core/presets.ts +231 -0
  99. package/src/core/prompts.ts +272 -0
  100. package/src/core/schema.ts +478 -0
  101. package/src/core/speaker-channel.ts +250 -0
  102. package/src/core/store.ts +461 -0
  103. package/src/core/theme.ts +666 -0
  104. package/src/core/types.ts +1611 -0
  105. package/src/directives/vStudio.ts +45 -0
  106. package/src/index.ts +175 -0
  107. package/src/main.ts +17 -0
  108. package/src/router/index.ts +92 -0
  109. package/src/style/main.css +462 -0
  110. package/src/utils/deep.ts +127 -0
  111. package/src/utils/firebase.ts +184 -0
  112. package/src/utils/streaming.ts +134 -0
  113. package/src/views/DashboardView.vue +32 -0
  114. package/src/views/DeckView.vue +289 -0
  115. package/src/views/HomeView.vue +17 -0
  116. package/src/views/SiteLayout.vue +21 -0
  117. package/src/views/StudioView.vue +61 -0
  118. package/src/vite-env.d.ts +6 -0
  119. package/IMPLEMENTATION.md +0 -418
@@ -0,0 +1,222 @@
1
+ <template>
2
+ <div class="h-12 bg-[#1a1a1a] border-b border-[#333] flex items-center px-4 justify-between select-none">
3
+ <!-- Logo -->
4
+ <div class="flex items-center gap-3">
5
+ <span class="font-heading font-bold text-white tracking-wider">LUMINA STUDIO</span>
6
+ <router-link :to="{ name: 'dashboard' }" class="text-white/40 hover:text-white text-xs transition">
7
+ <i class="ph-thin ph-arrow-left mr-1"></i> Exit
8
+ </router-link>
9
+ </div>
10
+
11
+ <!-- Tools (Only for Flex slides) -->
12
+ <div v-if="canAddElements"
13
+ class="flex items-center gap-1.5 bg-[#252525] p-1.5 rounded-xl border border-white/10 shadow-lg shadow-black/20">
14
+ <button @click="addElement('title')"
15
+ class="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-white/10 text-white/50 hover:text-white transition-all duration-200 active:scale-95"
16
+ title="Add Title">
17
+ <i class="ph-thin ph-text-h text-lg"></i>
18
+ </button>
19
+ <button @click="addElement('text')"
20
+ class="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-white/10 text-white/50 hover:text-white transition-all duration-200 active:scale-95"
21
+ title="Add Text">
22
+ <i class="ph-thin ph-text-align-left text-lg"></i>
23
+ </button>
24
+
25
+ <div class="w-px h-6 bg-white/5 mx-1"></div>
26
+
27
+ <button @click="addElement('image')"
28
+ class="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-white/10 text-white/50 hover:text-white transition-all duration-200 active:scale-95"
29
+ title="Add Image">
30
+ <i class="ph-thin ph-image text-lg"></i>
31
+ </button>
32
+ <button @click="addElement('button')"
33
+ class="w-10 h-10 flex items-center justify-center rounded-lg hover:bg-white/10 text-white/50 hover:text-white transition-all duration-200 active:scale-95"
34
+ title="Add Button">
35
+ <i class="ph-thin ph-rectangle text-lg"></i>
36
+ </button>
37
+ </div>
38
+
39
+ <!-- Placeholder for symmetry when hidden -->
40
+ <div v-else class="flex-1 opacity-0 pointer-events-none"></div>
41
+
42
+ <!-- Actions -->
43
+ <div class="flex items-center gap-2">
44
+ <button @click="editor.undo()" :disabled="!editor.canUndo.value"
45
+ class="p-1.5 rounded hover:bg-[#333] text-white/70 hover:text-white disabled:opacity-30 disabled:hover:bg-transparent transition"
46
+ title="Undo (Ctrl+Z)">
47
+ <i class="ph-thin ph-arrow-u-up-left"></i>
48
+ </button>
49
+ <button @click="editor.redo()" :disabled="!editor.canRedo.value"
50
+ class="p-1.5 rounded hover:bg-[#333] text-white/70 hover:text-white disabled:opacity-30 disabled:hover:bg-transparent transition"
51
+ title="Redo (Ctrl+Y)">
52
+ <i class="ph-thin ph-arrow-u-up-right"></i>
53
+ </button>
54
+ <div class="w-px h-4 bg-[#444] mx-1"></div>
55
+ <button @click="saveToFirestore" :disabled="isSaving"
56
+ class="min-w-[80px] px-3 py-1 bg-green-600 hover:bg-green-500 disabled:bg-green-800 text-white text-xs font-bold rounded transition flex items-center justify-center gap-1">
57
+ <i v-if="isSaving" class="ph-thin ph-spinner ph-spin"></i>
58
+ <i v-else-if="showSavedMessage" class="ph-thin ph-check"></i>
59
+ <i v-else class="ph-thin ph-cloud-arrow-up"></i>
60
+ {{ isSaving ? 'SAVING...' : (showSavedMessage ? 'SAVED' : 'SAVE') }}
61
+ </button>
62
+ <button @click="showJsonEditor = true"
63
+ class="px-3 py-1 bg-[#333] hover:bg-[#444] text-white/80 text-xs font-bold rounded transition flex items-center gap-1"
64
+ title="Edit RAW Data">
65
+ <i class="ph-thin ph-code"></i> JSON
66
+ </button>
67
+ <button @click="exportDeck"
68
+ class="px-3 py-1 bg-blue-600 hover:bg-blue-500 text-white text-xs font-bold rounded transition flex items-center gap-1">
69
+ <i class="ph-thin ph-download-simple"></i> EXPORT
70
+ </button>
71
+ </div>
72
+ <Teleport to="body">
73
+ <!-- Save Success Modal Removed -->
74
+ <StudioJsonEditor :is-open="showJsonEditor" @close="showJsonEditor = false" />
75
+ </Teleport>
76
+ </div>
77
+ </template>
78
+
79
+ <script setup lang="ts">
80
+ import { computed, ref, onMounted, watch, toRaw } from 'vue';
81
+ import { useRoute } from 'vue-router';
82
+ import { useEditor } from '../../composables/useEditor';
83
+ import { useAuth } from '../../composables/useAuth';
84
+ import { saveDeck, updateDeck } from '../../utils/firebase';
85
+ import type { Deck } from '../../core/types';
86
+ // import SaveSuccessModal from './SaveSuccessModal.vue';
87
+ import StudioJsonEditor from './StudioJsonEditor.vue';
88
+
89
+ const editor = useEditor();
90
+ const route = useRoute();
91
+ const { user } = useAuth();
92
+ const isSaving = ref(false);
93
+ // const showSaveModal = ref(false);
94
+ const showSavedMessage = ref(false);
95
+ const showJsonEditor = ref(false);
96
+ const savedDeckId = ref('');
97
+ const isUpdate = ref(false);
98
+
99
+ const canAddElements = computed(() => {
100
+ const currentIndex = editor.store.state.currentIndex;
101
+ const slide = editor.store.state.deck?.slides[currentIndex];
102
+ return slide?.type === 'flex';
103
+ });
104
+
105
+ onMounted(() => {
106
+ // Priority 1: Route Params (the most reliable in Vue Router apps)
107
+ // Priority 2: Store Meta (if already loaded)
108
+ const idFromRoute = route.params.id as string;
109
+ const idFromStore = editor.store.state.deck?.meta?.id;
110
+
111
+ if (idFromRoute) {
112
+ savedDeckId.value = idFromRoute;
113
+ } else if (idFromStore) {
114
+ savedDeckId.value = idFromStore;
115
+ }
116
+ });
117
+
118
+ // Keep savedDeckId in sync with route changes (e.g., if we go from /studio to /studio/123)
119
+ watch(() => route.params.id, (newId) => {
120
+ if (newId) savedDeckId.value = newId as string;
121
+ }, { immediate: true });
122
+
123
+ const addElement = (type: string) => {
124
+ const currentIndex = editor.store.state.currentIndex;
125
+ const slide = editor.store.state.deck?.slides[currentIndex];
126
+
127
+ if (!slide) return;
128
+
129
+ const templates: Record<string, any> = {
130
+ 'title': { type: 'title', text: 'New Title', dragKey: Date.now() + Math.random() },
131
+ 'text': { type: 'text', text: 'New paragraph text...', dragKey: Date.now() + Math.random() },
132
+ 'image': { type: 'image', src: 'https://via.placeholder.com/400x300', alt: 'Placeholder', dragKey: Date.now() + Math.random() },
133
+ 'button': { type: 'button', label: 'Click Me', variant: 'primary', dragKey: Date.now() + Math.random() }
134
+ };
135
+
136
+ const newElement = templates[type];
137
+ if (!newElement) return;
138
+
139
+ // Find the first content container or root elements array
140
+ if (slide.type === 'flex' && Array.isArray((slide as any).elements)) {
141
+ const elements = (slide as any).elements;
142
+ // Find first content container
143
+ const contentIdx = elements.findIndex((el: any) => el.type === 'content');
144
+ if (contentIdx >= 0 && Array.isArray(elements[contentIdx].elements)) {
145
+ editor.store.addNode(`slides.${currentIndex}.elements.${contentIdx}.elements`, newElement);
146
+ } else {
147
+ editor.store.addNode(`slides.${currentIndex}.elements`, newElement);
148
+ }
149
+ // editor.commit(); // Handled by watch
150
+ }
151
+ };
152
+
153
+ const saveToFirestore = async () => {
154
+ isSaving.value = true;
155
+ try {
156
+ // Use toRaw to strip Vue proxies for a cleaner JSON stringify
157
+ const rawDeck = toRaw(editor.store.state.deck);
158
+ if (!rawDeck) {
159
+ alert('No deck found to save.');
160
+ return;
161
+ }
162
+
163
+ const deckToSave = JSON.parse(JSON.stringify(rawDeck));
164
+
165
+
166
+
167
+ // Ensure meta exists
168
+ if (!deckToSave.meta) deckToSave.meta = {};
169
+
170
+ // Use the most up-to-date ID available
171
+ const currentId = savedDeckId.value || deckToSave.meta.id;
172
+
173
+ if (currentId) {
174
+ // Update existing
175
+ await updateDeck(currentId, deckToSave as Deck);
176
+ isUpdate.value = true;
177
+ } else {
178
+ // Create new
179
+ const userId = user.value?.uid;
180
+ const userName = user.value?.displayName || undefined;
181
+ const newId = await saveDeck(deckToSave as Deck, userId, userName);
182
+
183
+ // CRITICAL: Update local ref so next click is an UPDATE
184
+ savedDeckId.value = newId;
185
+
186
+ // ALSO CRITICAL: Update the store so other components know the ID
187
+ // We use updateNode to modify the deck meta in the store safely
188
+ editor.store.updateNode('meta.id', newId);
189
+
190
+ isUpdate.value = false;
191
+ }
192
+
193
+ // Show inline success message
194
+ showSavedMessage.value = true;
195
+ setTimeout(() => {
196
+ showSavedMessage.value = false;
197
+ }, 3000);
198
+
199
+ } catch (error: any) {
200
+ console.error('Save failed:', error);
201
+ alert(`Failed to save: ${error.message || 'Unknown error'}. Check console and ensure Firebase is initialized.`);
202
+ } finally {
203
+ isSaving.value = false;
204
+ }
205
+ };
206
+
207
+ const exportDeck = () => {
208
+ const deck = editor.store.state.deck;
209
+ if (!deck) return;
210
+
211
+ const json = JSON.stringify(deck, null, 2);
212
+ const blob = new Blob([json], { type: 'application/json' });
213
+ const url = URL.createObjectURL(blob);
214
+
215
+ const a = document.createElement('a');
216
+ a.href = url;
217
+ a.download = `${deck.meta?.title || 'deck'}.json`;
218
+ a.click();
219
+
220
+ URL.revokeObjectURL(url);
221
+ };
222
+ </script>
@@ -0,0 +1,224 @@
1
+ /**
2
+ * FIELD LABELS & VALUE MAPPINGS
3
+ * Provides user-friendly labels for technical field names and enum values.
4
+ */
5
+
6
+ // Field name to friendly label mapping
7
+ export const FIELD_LABELS: Record<string, string> = {
8
+ // Layout
9
+ 'direction': 'Layout Direction',
10
+ 'gap': 'Spacing',
11
+ 'padding': 'Inner Padding',
12
+ 'halign': 'Horizontal Align',
13
+ 'valign': 'Vertical Align',
14
+ 'align': 'Text Alignment',
15
+ 'size': 'Size',
16
+ 'imageSide': 'Image Position',
17
+
18
+ // Typography
19
+ 'title': 'Title',
20
+ 'subtitle': 'Subtitle',
21
+ 'text': 'Text Content',
22
+ 'tag': 'Label / Tag',
23
+ 'description': 'Description',
24
+ 'desc': 'Description',
25
+ 'label': 'Label',
26
+ 'name': 'Name',
27
+
28
+ // Media
29
+ 'src': 'Image URL',
30
+ 'image': 'Image',
31
+ 'poster': 'Video Poster',
32
+ 'alt': 'Alt Text',
33
+ 'background': 'Background',
34
+ 'icon': 'Icon',
35
+
36
+ // Actions
37
+ 'action': 'Action ID',
38
+ 'actionType': 'Action Type',
39
+ 'href': 'Link URL',
40
+ 'gotoSlide': 'Go to Slide',
41
+ 'target': 'Open In',
42
+ 'cta': 'Call to Action',
43
+ 'url': 'URL',
44
+
45
+ // Styling
46
+ 'variant': 'Style Variant',
47
+ 'rounded': 'Corner Radius',
48
+ 'fit': 'Object Fit',
49
+ 'fill': 'Fill Container',
50
+ 'type': 'Type',
51
+
52
+ // Chart
53
+ 'chartType': 'Chart Type',
54
+ 'data': 'Data',
55
+
56
+ // Layout Types
57
+ 'elements': 'Elements',
58
+ 'features': 'Features',
59
+ 'timeline': 'Timeline',
60
+ 'steps': 'Steps',
61
+ 'paragraphs': 'Paragraphs',
62
+ 'bullets': 'Bullet Points',
63
+
64
+ // Meta
65
+ 'notes': 'Speaker Notes',
66
+ 'sizing': 'Sizing Mode',
67
+ 'id': 'ID',
68
+ 'meta': 'Metadata',
69
+ 'orbColor': 'Orb Color',
70
+ 'orbPos': 'Orb Position',
71
+
72
+ // Timeline/Steps specific
73
+ 'date': 'Date',
74
+ 'step': 'Step Number',
75
+ 'year': 'Year',
76
+ };
77
+
78
+ // Enum value to friendly display name
79
+ export const VALUE_LABELS: Record<string, Record<string, string>> = {
80
+ 'direction': {
81
+ 'horizontal': '→ Horizontal',
82
+ 'vertical': '↓ Vertical',
83
+ },
84
+ 'gap': {
85
+ 'none': 'None',
86
+ 'xs': 'Extra Small',
87
+ 'sm': 'Small',
88
+ 'md': 'Medium',
89
+ 'lg': 'Large',
90
+ 'xl': 'Extra Large',
91
+ '2xl': '2X Large',
92
+ '3xl': '3X Large',
93
+ '4xl': 'Huge',
94
+ },
95
+ 'padding': {
96
+ 'none': 'None',
97
+ 'xs': 'Extra Small',
98
+ 'sm': 'Small',
99
+ 'md': 'Medium',
100
+ 'lg': 'Large',
101
+ 'xl': 'Extra Large',
102
+ '2xl': '2X Large',
103
+ '3xl': '3X Large',
104
+ '4xl': 'Huge',
105
+ },
106
+ 'size': {
107
+ 'auto': 'Auto',
108
+ 'quarter': '25%',
109
+ 'third': '33%',
110
+ 'half': '50%',
111
+ 'two-thirds': '66%',
112
+ 'three-quarters': '75%',
113
+ 'full': '100%',
114
+ },
115
+ 'halign': {
116
+ 'left': '← Left',
117
+ 'center': '↔ Center',
118
+ 'right': '→ Right',
119
+ },
120
+ 'valign': {
121
+ 'top': '↑ Top',
122
+ 'center': '↕ Center',
123
+ 'bottom': '↓ Bottom',
124
+ },
125
+ 'align': {
126
+ 'left': '← Left',
127
+ 'center': '↔ Center',
128
+ 'right': '→ Right',
129
+ },
130
+ 'imageSide': {
131
+ 'left': '← Left',
132
+ 'right': '→ Right',
133
+ },
134
+ 'variant': {
135
+ 'primary': '● Primary',
136
+ 'secondary': '○ Secondary',
137
+ 'outline': '□ Outline',
138
+ 'ghost': '◌ Ghost',
139
+ },
140
+ 'rounded': {
141
+ 'none': 'None',
142
+ 'sm': 'Slight',
143
+ 'md': 'Medium',
144
+ 'lg': 'Large',
145
+ 'xl': 'Extra Large',
146
+ '2xl': '2X Large',
147
+ '3xl': '3X Large',
148
+ 'full': 'Full Circle',
149
+ },
150
+ 'fit': {
151
+ 'cover': 'Cover (fill)',
152
+ 'contain': 'Contain (fit)',
153
+ },
154
+ 'fill': {
155
+ 'true': 'Yes',
156
+ 'false': 'No',
157
+ },
158
+ 'chartType': {
159
+ 'bar': '📊 Bar Chart',
160
+ 'line': '📈 Line Chart',
161
+ 'pie': '🥧 Pie Chart',
162
+ 'doughnut': '🍩 Doughnut',
163
+ 'radar': '🕸 Radar',
164
+ 'polarArea': '🎯 Polar Area',
165
+ },
166
+ 'sizing': {
167
+ 'viewport': 'Full Screen',
168
+ 'container': 'Container',
169
+ },
170
+ 'type': {
171
+ 'title': '📌 Heading',
172
+ 'text': '📄 Paragraph',
173
+ 'content': '📝 Content',
174
+ 'image': '🖼 Image',
175
+ 'video': '🎥 Video',
176
+ 'button': '🔘 Button',
177
+ 'spacer': '⬜ Spacer',
178
+ },
179
+ 'actionType': {
180
+ 'none': '⊘ None',
181
+ 'url': '🔗 Open URL',
182
+ 'slide': '📑 Go to Slide',
183
+ 'download': '⬇️ Download',
184
+ 'event': '⚡ Custom Event',
185
+ },
186
+ 'target': {
187
+ '_blank': 'New Tab',
188
+ '_self': 'Same Tab',
189
+ },
190
+ };
191
+
192
+ // Field groupings for organized display
193
+ export const FIELD_GROUPS: Record<string, string[]> = {
194
+ 'Layout': ['direction', 'gap', 'padding', 'halign', 'valign', 'size', 'imageSide'],
195
+ 'Content': ['title', 'subtitle', 'text', 'tag', 'description', 'desc', 'label', 'cta'],
196
+ 'Media': ['src', 'image', 'poster', 'alt', 'background', 'icon'],
197
+ 'Styling': ['variant', 'rounded', 'fit', 'fill'],
198
+ 'Actions': ['action', 'href', 'url'],
199
+ 'Data': ['elements', 'features', 'timeline', 'steps', 'paragraphs', 'bullets', 'data'],
200
+ 'Meta': ['notes', 'sizing', 'type', 'id'],
201
+ };
202
+
203
+ // Helper functions
204
+ export function getFieldLabel(key: string): string {
205
+ return FIELD_LABELS[key] || formatCamelCase(key);
206
+ }
207
+
208
+ export function getValueLabel(field: string, value: string): string {
209
+ return VALUE_LABELS[field]?.[value] || value;
210
+ }
211
+
212
+ export function getValueOptions(field: string): { value: string; label: string }[] | null {
213
+ const mapping = VALUE_LABELS[field];
214
+ if (!mapping) return null;
215
+ return Object.entries(mapping).map(([value, label]) => ({ value, label }));
216
+ }
217
+
218
+ // Convert camelCase to Title Case as fallback
219
+ function formatCamelCase(str: string): string {
220
+ return str
221
+ .replace(/([A-Z])/g, ' $1')
222
+ .replace(/^./, s => s.toUpperCase())
223
+ .trim();
224
+ }
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <div class="space-y-4">
3
+ <!-- Edge Properties -->
4
+ <div class="space-y-3">
5
+ <div class="text-white/30 text-[10px] uppercase tracking-widest">Connection Properties</div>
6
+
7
+ <!-- Label -->
8
+ <div class="space-y-1">
9
+ <label class="text-white/50 text-[10px] uppercase">Label</label>
10
+ <input type="text" :value="data.label || ''"
11
+ @input="update('label', ($event.target as HTMLInputElement).value)"
12
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white focus:border-blue-500 focus:outline-none" />
13
+ </div>
14
+
15
+ <!-- Type -->
16
+ <div class="space-y-1">
17
+ <label class="text-white/50 text-[10px] uppercase">Line Type</label>
18
+ <select :value="data.type || 'default'"
19
+ @change="update('type', ($event.target as HTMLSelectElement).value)"
20
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white focus:border-blue-500 focus:outline-none">
21
+ <option value="default">Bezier (Default)</option>
22
+ <option value="straight">Straight</option>
23
+ <option value="step">Step</option>
24
+ <option value="smoothstep">Smooth Step</option>
25
+ </select>
26
+ </div>
27
+
28
+ <!-- Animated Toggle -->
29
+ <div class="flex items-center justify-between">
30
+ <label class="text-white/50 text-[10px] uppercase">Animated</label>
31
+ <button @click="update('animated', !data.animated)"
32
+ class="w-8 h-4 rounded-full relative transition-colors duration-200"
33
+ :class="data.animated ? 'bg-blue-600' : 'bg-[#333]'">
34
+ <div class="absolute top-0.5 left-0.5 w-3 h-3 bg-white rounded-full transition-transform duration-200"
35
+ :class="data.animated ? 'translate-x-4' : 'translate-x-0'"></div>
36
+ </button>
37
+ </div>
38
+
39
+ <!-- Style / Color -->
40
+ <div class="space-y-1">
41
+ <label class="text-white/50 text-[10px] uppercase">Stroke Color</label>
42
+ <div class="flex gap-2">
43
+ <input type="color" :value="data.style?.stroke || '#b1b1b7'"
44
+ @input="updateStyle('stroke', ($event.target as HTMLInputElement).value)"
45
+ class="bg-transparent w-6 h-6 p-0 border-0 cursor-pointer" />
46
+ <input type="text" :value="data.style?.stroke || ''" placeholder="#b1b1b7"
47
+ @input="updateStyle('stroke', ($event.target as HTMLInputElement).value)"
48
+ class="flex-1 bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- Actions -->
54
+ <div class="pt-4 border-t border-[#333]">
55
+ <button @click="$emit('delete')"
56
+ class="w-full px-3 py-2 rounded bg-red-900/30 border border-red-500/30 text-red-400 hover:bg-red-900/50 transition flex items-center justify-center gap-2">
57
+ <i class="ph-thin ph-trash text-xs"></i> Delete Connection
58
+ </button>
59
+ </div>
60
+ </div>
61
+ </template>
62
+
63
+ <script setup lang="ts">
64
+ const props = defineProps<{
65
+ data: any; // Edge object
66
+ }>();
67
+
68
+ const emit = defineEmits(['update', 'delete']);
69
+
70
+ const update = (key: string, value: any) => {
71
+ emit('update', key, value);
72
+ };
73
+
74
+ const updateStyle = (key: string, value: string) => {
75
+ emit('update', `style.${key}`, value);
76
+ };
77
+ </script>
@@ -0,0 +1,117 @@
1
+ <template>
2
+ <div class="space-y-4">
3
+ <!-- Node Properties -->
4
+ <div class="space-y-3">
5
+ <div class="text-white/30 text-[10px] uppercase tracking-widest">Node Properties</div>
6
+
7
+ <!-- Label -->
8
+ <div class="space-y-1">
9
+ <label class="text-white/50 text-[10px] uppercase">Label</label>
10
+ <input type="text" :value="data.label"
11
+ @input="update('label', ($event.target as HTMLInputElement).value)"
12
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white focus:border-blue-500 focus:outline-none" />
13
+ </div>
14
+
15
+ <!-- Shape (visual only, stored in data.shape) -->
16
+ <div class="space-y-1">
17
+ <label class="text-white/50 text-[10px] uppercase">Shape</label>
18
+ <select :value="data.data?.shape || data.data?.type || 'rectangle'"
19
+ @change="updateData('shape', ($event.target as HTMLSelectElement).value)"
20
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white focus:border-blue-500 focus:outline-none">
21
+ <option value="rectangle">Rectangle</option>
22
+ <option value="rounded">Rounded</option>
23
+ <option value="image">Image</option>
24
+ </select>
25
+ </div>
26
+
27
+ <!-- Image URL (only for image nodes) -->
28
+ <div v-if="isImageNode" class="space-y-1">
29
+ <label class="text-white/50 text-[10px] uppercase">Image URL</label>
30
+ <input type="text" :value="data.data?.imageUrl || ''" placeholder="https://..."
31
+ @input="updateData('imageUrl', ($event.target as HTMLInputElement).value)"
32
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white focus:border-blue-500 focus:outline-none" />
33
+ </div>
34
+
35
+ <!-- Position (Fine Tuning) -->
36
+ <div class="grid grid-cols-2 gap-2">
37
+ <div class="space-y-1">
38
+ <label class="text-white/50 text-[10px] uppercase">X Position</label>
39
+ <input type="number" :value="data.position?.x"
40
+ @input="updatePosition('x', parseFloat(($event.target as HTMLInputElement).value))"
41
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
42
+ </div>
43
+ <div class="space-y-1">
44
+ <label class="text-white/50 text-[10px] uppercase">Y Position</label>
45
+ <input type="number" :value="data.position?.y"
46
+ @input="updatePosition('y', parseFloat(($event.target as HTMLInputElement).value))"
47
+ class="w-full bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white" />
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Style / Color (hide for image nodes) -->
52
+ <div v-if="!isImageNode" class="space-y-1">
53
+ <label class="text-white/50 text-[10px] uppercase">Background Color</label>
54
+ <div class="flex gap-2">
55
+ <input type="color" :value="data.style?.backgroundColor || '#ffffff'"
56
+ @input="updateStyle('backgroundColor', ($event.target as HTMLInputElement).value)"
57
+ class="bg-transparent w-6 h-6 p-0 border-0 cursor-pointer" />
58
+ <input type="text" :value="data.style?.backgroundColor || ''" placeholder="#ffffff"
59
+ @input="updateStyle('backgroundColor', ($event.target as HTMLInputElement).value)"
60
+ class="flex-1 bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
61
+ </div>
62
+ </div>
63
+
64
+ <div v-if="!isImageNode" class="space-y-1">
65
+ <label class="text-white/50 text-[10px] uppercase">Text Color</label>
66
+ <div class="flex gap-2">
67
+ <input type="color" :value="data.style?.color || '#000000'"
68
+ @input="updateStyle('color', ($event.target as HTMLInputElement).value)"
69
+ class="bg-transparent w-6 h-6 p-0 border-0 cursor-pointer" />
70
+ <input type="text" :value="data.style?.color || ''" placeholder="#000000"
71
+ @input="updateStyle('color', ($event.target as HTMLInputElement).value)"
72
+ class="flex-1 bg-[#1a1a1a] border border-[#333] rounded px-2 py-1 text-xs text-white font-mono" />
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Actions -->
78
+ <div class="pt-4 border-t border-[#333]">
79
+ <button @click="$emit('delete')"
80
+ class="w-full px-3 py-2 rounded bg-red-900/30 border border-red-500/30 text-red-400 hover:bg-red-900/50 transition flex items-center justify-center gap-2">
81
+ <i class="ph-thin ph-trash text-xs"></i> Delete Node
82
+ </button>
83
+ </div>
84
+ </div>
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ import { computed } from 'vue';
89
+
90
+ const props = defineProps<{
91
+ data: any; // Node object
92
+ }>();
93
+
94
+ const emit = defineEmits(['update', 'delete']);
95
+
96
+ // Check if this is an image node
97
+ const isImageNode = computed(() =>
98
+ props.data?.data?.shape === 'image' || props.data?.data?.type === 'image'
99
+ );
100
+
101
+ const update = (key: string, value: any) => {
102
+ emit('update', key, value);
103
+ };
104
+
105
+ const updatePosition = (axis: 'x' | 'y', value: number) => {
106
+ emit('update', `position.${axis}`, value);
107
+ };
108
+
109
+ const updateStyle = (key: string, value: string) => {
110
+ // Store style in data.style so custom node can access it via props.data.style
111
+ emit('update', `data.style.${key}`, value);
112
+ };
113
+
114
+ const updateData = (key: string, value: string) => {
115
+ emit('update', `data.${key}`, value);
116
+ };
117
+ </script>