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.
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 +13153 -12619
  4. package/dist/lumina-slides.umd.cjs +217 -217
  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 +3273 -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 +465 -0
  103. package/src/core/theme.ts +666 -0
  104. package/src/core/types.ts +1615 -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,138 @@
1
+ <template>
2
+ <!-- Single root element required for Vue attribute inheritance -->
3
+ <div class="studio-diagram-node" :data-lumina-id="elementId || undefined">
4
+ <NodeResizer v-if="editable && selected" :min-width="80" :min-height="40" />
5
+
6
+ <!-- Image Node -->
7
+ <div v-if="isImageNode" class="node-content node-image" :style="imageContainerStyle">
8
+ <img v-if="data?.imageUrl" :src="data.imageUrl" alt="" class="node-img" />
9
+ <div v-else class="placeholder">
10
+ <i class="ph-thin ph-image text-2xl text-gray-400"></i>
11
+ <span class="text-xs text-gray-400">No Image</span>
12
+ </div>
13
+ </div>
14
+
15
+ <!-- Regular Node -->
16
+ <div v-else class="node-content" :style="contentStyle">
17
+ {{ displayLabel || data?.label || 'Node' }}
18
+ </div>
19
+
20
+ <!-- Handles only visible in edit mode -->
21
+ <template v-if="editable">
22
+ <Handle type="target" :position="Position.Top" class="node-handle" />
23
+ <Handle type="source" :position="Position.Bottom" class="node-handle" />
24
+ <Handle type="target" :position="Position.Left" class="node-handle" />
25
+ <Handle type="source" :position="Position.Right" class="node-handle" />
26
+ </template>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import { computed, type VNode, type Component } from 'vue';
32
+ import { Handle, Position } from '@vue-flow/core';
33
+ import { NodeResizer } from '@vue-flow/node-resizer';
34
+ import '@vue-flow/node-resizer/dist/style.css';
35
+
36
+ const props = defineProps<{
37
+ id: string;
38
+ data: any;
39
+ selected?: boolean;
40
+ // VueFlow can pass label as string, object, VNode, or Component
41
+ label?: string | object | VNode | Component;
42
+ type?: string;
43
+ style?: Record<string, any>;
44
+ editable?: boolean;
45
+ /** Element id for engine.element(id) control; applied as data-lumina-id on the root. */
46
+ elementId?: string;
47
+ }>();
48
+
49
+ // Extract a displayable string from the label prop
50
+ const displayLabel = computed(() => {
51
+ if (!props.label) return null;
52
+ if (typeof props.label === 'string') return props.label;
53
+ // For objects/VNodes/Components, try to extract text or return null
54
+ if (typeof props.label === 'object' && 'toString' in props.label) {
55
+ const str = props.label.toString();
56
+ // Avoid returning "[object Object]"
57
+ if (str !== '[object Object]') return str;
58
+ }
59
+ return null;
60
+ });
61
+
62
+ // Check if this is an image node
63
+ const isImageNode = computed(() => props.data?.type === 'image' || props.data?.shape === 'image');
64
+
65
+ // Shape is stored in data.shape for styling
66
+ const shape = computed(() => props.data?.shape || 'rectangle');
67
+
68
+ const contentStyle = computed(() => ({
69
+ backgroundColor: props.style?.backgroundColor || props.data?.style?.backgroundColor || '#ffffff',
70
+ color: props.style?.color || props.data?.style?.color || '#000000',
71
+ borderRadius: shape.value === 'rounded' ? '12px' : '4px',
72
+ border: props.editable && props.selected ? '2px solid #3b82f6' : '1px solid #ccc',
73
+ }));
74
+
75
+ const imageContainerStyle = computed(() => ({
76
+ borderRadius: '4px',
77
+ border: props.editable && props.selected ? '2px solid #3b82f6' : 'none',
78
+ overflow: 'hidden',
79
+ }));
80
+ </script>
81
+
82
+ <style scoped>
83
+ .studio-diagram-node {
84
+ width: 100%;
85
+ height: 100%;
86
+ position: relative;
87
+ }
88
+
89
+ .node-content {
90
+ width: 100%;
91
+ height: 100%;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ padding: 8px 16px;
96
+ box-sizing: border-box;
97
+ text-align: center;
98
+ font-size: 12px;
99
+ word-break: break-word;
100
+ }
101
+
102
+ .node-image {
103
+ padding: 0;
104
+ background: transparent;
105
+ }
106
+
107
+ .node-img {
108
+ width: 100%;
109
+ height: 100%;
110
+ object-fit: cover;
111
+ }
112
+
113
+ .placeholder {
114
+ display: flex;
115
+ flex-direction: column;
116
+ align-items: center;
117
+ justify-content: center;
118
+ width: 100%;
119
+ height: 100%;
120
+ min-height: 60px;
121
+ background: rgba(255, 255, 255, 0.1);
122
+ border: 1px dashed rgba(255, 255, 255, 0.3);
123
+ border-radius: 4px;
124
+ }
125
+
126
+ .node-handle {
127
+ width: 10px;
128
+ height: 10px;
129
+ background-color: rgba(59, 130, 246, 0.6);
130
+ border: 2px solid white;
131
+ border-radius: 50%;
132
+ }
133
+
134
+ .node-handle:hover {
135
+ background-color: #3b82f6;
136
+ transform: scale(1.2);
137
+ }
138
+ </style>
@@ -0,0 +1,87 @@
1
+ import { ref, computed } from 'vue';
2
+ import {
3
+ signInWithPopup,
4
+ signOut as firebaseSignOut,
5
+ onAuthStateChanged,
6
+ User,
7
+ AuthError
8
+ } from 'firebase/auth';
9
+ import { getFirebaseAuth, getAuthProvider } from '../utils/firebase';
10
+
11
+ // Global state (Singleton pattern for composable)
12
+ const currentUser = ref<User | null>(null);
13
+ const isLoading = ref(true);
14
+ let isListenerAttached = false;
15
+
16
+ /**
17
+ * Composable for Firebase Authentication.
18
+ * Provides reactive user state and login/logout methods.
19
+ */
20
+ export function useAuth() {
21
+
22
+ /**
23
+ * Helper to safely get auth instance or throw.
24
+ */
25
+ const ensureAuth = () => {
26
+ try {
27
+ return { auth: getFirebaseAuth(), provider: getAuthProvider() };
28
+ } catch (e) {
29
+ throw new Error("Authentication service is not ready. Ensure initFirebase() is called in main.ts");
30
+ }
31
+ };
32
+
33
+ const loginWithGoogle = async (): Promise<User> => {
34
+ const { auth, provider } = ensureAuth();
35
+
36
+ try {
37
+ const result = await signInWithPopup(auth, provider);
38
+ currentUser.value = result.user;
39
+ return result.user;
40
+ } catch (error: unknown) {
41
+ const authError = error as AuthError;
42
+ console.error(`[Auth] Login failed: ${authError.code} - ${authError.message}`);
43
+ throw authError; // Propagate for UI handling
44
+ }
45
+ };
46
+
47
+ const logout = async (): Promise<void> => {
48
+ try {
49
+ const auth = getFirebaseAuth(); // Can just get auth, provider not needed
50
+ await firebaseSignOut(auth);
51
+ currentUser.value = null;
52
+ } catch (error: unknown) {
53
+ console.error("[Auth] Logout failed", error);
54
+ throw error;
55
+ }
56
+ };
57
+
58
+ /**
59
+ * Initialize the Auth listener.
60
+ * Idempotent: calling it multiple times is safe.
61
+ */
62
+ const initAuthListener = (): void => {
63
+ if (isListenerAttached) return;
64
+
65
+ try {
66
+ const auth = getFirebaseAuth();
67
+ onAuthStateChanged(auth, (user) => {
68
+ currentUser.value = user;
69
+ isLoading.value = false;
70
+ });
71
+ isListenerAttached = true;
72
+ } catch (e) {
73
+ // If firebase isn't ready yet, this might fail.
74
+ // In a strict app, this shouldn't happen if main.ts inits first.
75
+ console.warn("[Auth] Failed to attach listener:", e);
76
+ }
77
+ };
78
+
79
+ return {
80
+ user: computed(() => currentUser.value),
81
+ isAuthenticated: computed(() => !!currentUser.value),
82
+ isLoading: computed(() => isLoading.value),
83
+ loginWithGoogle,
84
+ logout,
85
+ initAuthListener
86
+ };
87
+ }
@@ -0,0 +1,224 @@
1
+ import { ref, computed, inject, InjectionKey, watch, onMounted, onUnmounted } from 'vue';
2
+ import { StoreKey, type LuminaStore } from '../core/store';
3
+ import { getByPath } from '../utils/deep';
4
+
5
+ interface HistorySnapshot {
6
+ deck: any;
7
+ currentIndex: number;
8
+ timestamp: number;
9
+ }
10
+
11
+ export const EditorKey: InjectionKey<ReturnType<typeof createEditor>> = Symbol('EditorKey');
12
+
13
+ export function createEditor(manualStore?: LuminaStore) {
14
+ const store = manualStore || inject(StoreKey);
15
+ if (!store) {
16
+ throw new Error("Lumina Editor requires Lumina Store.");
17
+ }
18
+
19
+ // --- State ---
20
+ const selection = ref<string | null>(null);
21
+ const clipboard = ref<any>(null);
22
+ const history = ref<HistorySnapshot[]>([]);
23
+ const historyIndex = ref(-1);
24
+
25
+ // --- Selection ---
26
+ const select = (path: string | null) => {
27
+ selection.value = path;
28
+ };
29
+
30
+ const getSelectedData = () => {
31
+ if (!selection.value || !store.state.deck) return null;
32
+ return getByPath(store.state.deck, selection.value);
33
+ };
34
+
35
+ // --- History Management ---
36
+ let debounceTimer: any = null;
37
+ const isUndoRedoOp = ref(false); // Internal flag to prevent history loops
38
+
39
+ const commit = (immediate = false) => {
40
+ if (isUndoRedoOp.value) return;
41
+
42
+ // Clear any pending debounce
43
+ if (debounceTimer) {
44
+ clearTimeout(debounceTimer);
45
+ debounceTimer = null;
46
+ }
47
+
48
+ const pushToHistory = () => {
49
+ // Remove future history if we were in the middle of the stack
50
+ if (historyIndex.value < history.value.length - 1) {
51
+ history.value = history.value.slice(0, historyIndex.value + 1);
52
+ }
53
+
54
+ if (store.state.deck) {
55
+ // Deduplicate: Don't push if identical to current head
56
+ const currentHead = history.value[historyIndex.value];
57
+ const newSnapshot = JSON.parse(JSON.stringify(store.state.deck));
58
+ const currentIndex = store.state.currentIndex;
59
+
60
+ if (currentHead &&
61
+ JSON.stringify(currentHead.deck) === JSON.stringify(newSnapshot) &&
62
+ currentHead.currentIndex === currentIndex
63
+ ) {
64
+ return;
65
+ }
66
+
67
+ history.value.push({
68
+ deck: newSnapshot,
69
+ currentIndex: currentIndex,
70
+ timestamp: Date.now()
71
+ });
72
+ historyIndex.value++;
73
+
74
+ // Limit history size
75
+ if (history.value.length > 50) {
76
+ history.value.shift();
77
+ historyIndex.value--;
78
+ }
79
+ }
80
+ };
81
+
82
+ if (immediate) {
83
+ pushToHistory();
84
+ } else {
85
+ // Debounce for text typing etc.
86
+ debounceTimer = setTimeout(pushToHistory, 500);
87
+ }
88
+ };
89
+
90
+ // Watch for ANY change in the deck
91
+ watch(
92
+ () => store.state.deck,
93
+ (newVal) => {
94
+ if (newVal && !isUndoRedoOp.value) {
95
+ commit(false); // Debounced by default
96
+ }
97
+ },
98
+ { deep: true }
99
+ );
100
+
101
+ // Also watch index changes so navigation is undoable
102
+ watch(
103
+ () => store.state.currentIndex,
104
+ () => {
105
+ if (!isUndoRedoOp.value) {
106
+ commit(false);
107
+ }
108
+ }
109
+ );
110
+
111
+ const undo = () => {
112
+ // Force commit any pending changes (e.g. from debounce)
113
+ // so we can undo from the current state
114
+ commit(true);
115
+
116
+ if (historyIndex.value > 0) {
117
+ isUndoRedoOp.value = true;
118
+ historyIndex.value--;
119
+ const snapshot = history.value[historyIndex.value];
120
+ if (snapshot) {
121
+ store.loadDeck(JSON.parse(JSON.stringify(snapshot.deck)));
122
+ store.goto(snapshot.currentIndex); // Restore slide position
123
+ }
124
+ // Wait for Vue reactivity to settle before re-enabling watcher
125
+ setTimeout(() => { isUndoRedoOp.value = false; }, 100);
126
+ }
127
+ };
128
+
129
+ const redo = () => {
130
+ // Force commit shouldn't be necessary for redo usually (as we are going forward),
131
+ // but for consistency if we made a change then decided to redo??
132
+ // Actually if we make a change, history is sliced, so we can't redo.
133
+ // So redo checks are safe.
134
+ // But let's keep logic simple.
135
+
136
+ if (historyIndex.value < history.value.length - 1) {
137
+ isUndoRedoOp.value = true;
138
+ historyIndex.value++;
139
+ const snapshot = history.value[historyIndex.value];
140
+ if (snapshot) {
141
+ store.loadDeck(JSON.parse(JSON.stringify(snapshot.deck)));
142
+ store.goto(snapshot.currentIndex); // Restore slide position
143
+ }
144
+ setTimeout(() => { isUndoRedoOp.value = false; }, 100);
145
+ }
146
+ };
147
+
148
+ // --- Keyboard Shortcuts ---
149
+ const handleKeydown = (e: KeyboardEvent) => {
150
+ // Only trigger if no input/textarea is focused (unless it's just a general command)
151
+ // Actually, standard undo/redo SHOULD work in inputs too, but browser handles that natively for text.
152
+ // We want app-level undo.
153
+
154
+ if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
155
+ if (e.shiftKey) {
156
+ e.preventDefault();
157
+ redo();
158
+ } else {
159
+ e.preventDefault();
160
+ undo();
161
+ }
162
+ } else if ((e.ctrlKey || e.metaKey) && e.key === 'y') {
163
+ e.preventDefault();
164
+ redo();
165
+ }
166
+ };
167
+
168
+ onMounted(() => {
169
+ window.addEventListener('keydown', handleKeydown);
170
+ // Initial commit
171
+ if (store.state.deck && history.value.length === 0) {
172
+ commit(true);
173
+ }
174
+ });
175
+
176
+ onUnmounted(() => {
177
+ window.removeEventListener('keydown', handleKeydown);
178
+ });
179
+
180
+ // --- Clipboard ---
181
+ const copy = () => {
182
+ if (!selection.value || !store.state.deck) return;
183
+ const data = getByPath(store.state.deck, selection.value);
184
+ if (data) {
185
+ clipboard.value = JSON.parse(JSON.stringify(data));
186
+ }
187
+ };
188
+
189
+ const paste = (targetArrayPath: string) => {
190
+ if (!clipboard.value) return;
191
+ store.addNode(targetArrayPath, JSON.parse(JSON.stringify(clipboard.value)));
192
+ // Watcher will catch this
193
+ };
194
+
195
+ // --- Update Helpers ---
196
+ const updateSelected = (key: string, value: any) => {
197
+ if (!selection.value) return;
198
+ store.updateNode(`${selection.value}.${key}`, value);
199
+ };
200
+
201
+ return {
202
+ selection,
203
+ clipboard,
204
+ store,
205
+ canUndo: computed(() => historyIndex.value > 0),
206
+ canRedo: computed(() => historyIndex.value < history.value.length - 1),
207
+ select,
208
+ getSelectedData,
209
+ updateSelected,
210
+ commit, // Exposed but mostly internal now
211
+ undo,
212
+ redo,
213
+ copy,
214
+ paste
215
+ };
216
+ }
217
+
218
+ export function useEditor() {
219
+ const editor = inject(EditorKey);
220
+ if (!editor) {
221
+ throw new Error("useEditor must be used within LuminaStudio");
222
+ }
223
+ return editor;
224
+ }
@@ -0,0 +1,81 @@
1
+ import { computed, inject, type MaybeRefOrGetter, toValue } from 'vue';
2
+ import { StoreKey } from '../core/store';
3
+ import type { ElementState } from '../core/types';
4
+
5
+ /** @internal */
6
+ type CRef<T> = import('vue').ComputedRef<T>;
7
+
8
+ /**
9
+ * Return type of {@link useElementState}. Composable bindings for visibility, style, and class
10
+ * derived from the engine’s element state (written by engine.element(id).show/hide/opacity/etc.).
11
+ */
12
+ export interface UseElementStateReturn {
13
+ /** false when engine.element(id).hide() was called; true otherwise. Default true. */
14
+ visible: CRef<boolean>;
15
+ /** Combined style: opacity, transform, and any style from the store. Apply as :style="state.style". */
16
+ style: CRef<Record<string, string | number>>;
17
+ /** Extra CSS classes from the store. Merge with component classes. */
18
+ class: CRef<string>;
19
+ }
20
+
21
+ /**
22
+ * Composable that subscribes to the engine’s element state for a given id. Use in custom
23
+ * layouts when you need to apply visibility/style/class yourself instead of using
24
+ * LuminaElement. Requires Lumina app context (StoreKey). If store is missing, returns
25
+ * safe defaults (visible true, empty style and class).
26
+ *
27
+ * @param id - Element id: string, Ref, or getter. From resolveId, elemId, or explicit id.
28
+ * @returns {UseElementStateReturn} visible, style, class. Bind :style and :class. When visible is
29
+ * false, style includes opacity: 0 so the element keeps its layout space (no v-show/display:none).
30
+ *
31
+ * @example
32
+ * const id = computed(() => resolveId(props.data, props.slideIndex, ['tag']));
33
+ * const state = useElementState(id);
34
+ * // In template: <span :style="state.style" :class="state.class">...</span>
35
+ *
36
+ * @see LuminaElement
37
+ * @see resolveId
38
+ * @see ElementState
39
+ */
40
+ export function useElementState(id: MaybeRefOrGetter<string>): UseElementStateReturn {
41
+ const store = inject(StoreKey);
42
+ if (!store) {
43
+ return {
44
+ visible: computed(() => true),
45
+ style: computed(() => ({})),
46
+ class: computed(() => '')
47
+ };
48
+ }
49
+
50
+ const visible = computed(() => {
51
+ const resolved = toValue(id);
52
+ if (!resolved) return true;
53
+ const s = store.state.elementState[resolved];
54
+ return s?.visible !== false;
55
+ });
56
+
57
+ const style = computed(() => {
58
+ const resolved = toValue(id);
59
+ if (!resolved) return {};
60
+ const s = store.state.elementState[resolved] as ElementState | undefined;
61
+ const out: Record<string, string | number> = {};
62
+ // When not visible: opacity 0 so the element keeps its layout space (like slides in any app)
63
+ if (s?.visible === false) {
64
+ out.opacity = 0;
65
+ } else if (s?.opacity !== undefined) {
66
+ out.opacity = s.opacity;
67
+ }
68
+ if (s?.transform) out.transform = s.transform;
69
+ if (s?.style && typeof s.style === 'object') Object.assign(out, s.style);
70
+ return out;
71
+ });
72
+
73
+ const classVal = computed(() => {
74
+ const resolved = toValue(id);
75
+ if (!resolved) return '';
76
+ const s = store.state.elementState[resolved];
77
+ return (s?.class as string) || '';
78
+ });
79
+
80
+ return { visible, style, class: classVal };
81
+ }
@@ -0,0 +1,122 @@
1
+ import type { FlexElementContent } from '../core/types';
2
+ import type { CSSProperties } from 'vue';
3
+
4
+ // Constants
5
+ export const spacingVarMap: Record<string, string> = {
6
+ 'none': 'var(--lumina-space-none, 0)',
7
+ 'xs': 'var(--lumina-space-xs)',
8
+ 'sm': 'var(--lumina-space-sm)',
9
+ 'md': 'var(--lumina-space-md)',
10
+ 'lg': 'var(--lumina-space-lg)',
11
+ 'xl': 'var(--lumina-space-xl)',
12
+ '2xl': 'var(--lumina-space-2xl)',
13
+ '3xl': 'var(--lumina-space-3xl)',
14
+ '4xl': 'var(--lumina-space-4xl)',
15
+ };
16
+
17
+ export const sizeMap: Record<string, string> = {
18
+ 'auto': 'auto',
19
+ 'quarter': '25%',
20
+ 'third': '33.333%',
21
+ 'half': '50%',
22
+ 'two-thirds': '66.666%',
23
+ 'three-quarters': '75%',
24
+ 'full': '100%',
25
+ };
26
+
27
+ export const valignMap: Record<string, string> = {
28
+ 'top': 'flex-start',
29
+ 'Top': 'flex-start',
30
+ 'center': 'center',
31
+ 'Center': 'center',
32
+ 'bottom': 'flex-end',
33
+ 'Bottom': 'flex-end',
34
+ };
35
+
36
+ export const halignMap: Record<string, string> = {
37
+ 'left': 'flex-start',
38
+ 'Left': 'flex-start',
39
+ 'center': 'center',
40
+ 'Center': 'center',
41
+ 'right': 'flex-end',
42
+ 'Right': 'flex-end',
43
+ };
44
+
45
+ // Helpers
46
+ export const getSizeClass = (size?: string): string => {
47
+ if (!size || size === 'auto') return 'flex-1';
48
+ return '';
49
+ };
50
+
51
+ export const getFlexItemStyle = (element: { size?: string }, directionVertical: boolean): CSSProperties => {
52
+ const size = element.size || 'auto';
53
+ if (size === 'auto') return { flex: '1 1 auto' };
54
+ return {
55
+ flex: `0 0 ${sizeMap[size]}`,
56
+ maxWidth: !directionVertical ? sizeMap[size] : undefined,
57
+ maxHeight: directionVertical ? sizeMap[size] : undefined,
58
+ };
59
+ };
60
+
61
+ export const getFlexContainerStyle = (element: FlexElementContent): CSSProperties => {
62
+ const valign = element.valign || 'center';
63
+ const halign = element.halign || 'left';
64
+ const gap = element.gap || 'md';
65
+ const padding = element.padding || 'lg';
66
+
67
+ return {
68
+ display: 'flex',
69
+ flexDirection: 'column', // Content containers are always column based for children
70
+ justifyContent: valignMap[valign] as CSSProperties['justifyContent'],
71
+ alignItems: halignMap[halign] as CSSProperties['alignItems'],
72
+ gap: spacingVarMap[gap],
73
+ padding: spacingVarMap[padding],
74
+ };
75
+ };
76
+
77
+ // Deprecated: kept for backward compat if needed, but we will update consumers
78
+ export const getElementStyle = getFlexItemStyle;
79
+ export const getContentStyle = (element: FlexElementContent & { size?: string }): CSSProperties => {
80
+ // Legacy combined style
81
+ return {
82
+ ...getFlexItemStyle(element, false), // Default assumption
83
+ ...getFlexContainerStyle(element)
84
+ };
85
+ };
86
+
87
+ export const getTopLevelStyle = (): CSSProperties => ({
88
+ display: 'flex',
89
+ alignItems: 'center',
90
+ padding: spacingVarMap['lg'],
91
+ });
92
+
93
+ /**
94
+ * Alignment for the main flex slide container (all elements as a group).
95
+ * When direction is horizontal: justify-content = halign, align-items = valign.
96
+ * When direction is vertical: justify-content = valign, align-items = halign.
97
+ */
98
+ export const getFlexSlideMainStyle = (
99
+ direction: 'horizontal' | 'vertical' | undefined,
100
+ halign: string | undefined,
101
+ valign: string | undefined
102
+ ): CSSProperties => {
103
+ const jc = direction === 'vertical' ? valignMap[valign || 'top'] : halignMap[halign || 'left'];
104
+ const ai = direction === 'vertical' ? halignMap[halign || 'left'] : valignMap[valign || 'top'];
105
+ return {
106
+ justifyContent: jc as CSSProperties['justifyContent'],
107
+ alignItems: ai as CSSProperties['alignItems'],
108
+ };
109
+ };
110
+
111
+ export const getRoundedClass = (rounded?: string, fill?: boolean) => {
112
+ if (fill !== false && !rounded) return 'w-full h-full';
113
+ const map: Record<string, string> = {
114
+ 'none': 'w-full h-full rounded-none',
115
+ 'sm': 'w-full h-full rounded-sm',
116
+ 'md': 'w-full h-full rounded-md',
117
+ 'lg': 'w-full h-full rounded-lg',
118
+ 'xl': 'w-full h-full rounded-xl',
119
+ 'full': 'w-full h-full rounded-full',
120
+ };
121
+ return map[rounded || 'lg'] || 'w-full h-full rounded-lg';
122
+ };
@@ -0,0 +1,45 @@
1
+ import { onMounted, onUnmounted, inject } from 'vue';
2
+ import { StoreKey } from '../core/store';
3
+
4
+ /**
5
+ * GLOBAL KEYBOARD HANDLER
6
+ *
7
+ * Binds global keyboard events (Arrow Keys, Enter, etc.) to store actions.
8
+ * Throttles input to prevent accidental double-skips.
9
+ * Respects 'navigation' and 'keyboard' global flags.
10
+ */
11
+ export function useKeyboard() {
12
+ const store = inject(StoreKey);
13
+
14
+ if (!store) return;
15
+
16
+ let lastTime = 0;
17
+ const THROTTLE_MS = 300;
18
+
19
+ const handleKeydown = (e: KeyboardEvent) => {
20
+ const { options } = store.state;
21
+ if (!options.keyboard || !options.navigation) return;
22
+
23
+ const now = Date.now();
24
+ if (now - lastTime < THROTTLE_MS) return;
25
+
26
+ const nextKeys = options.keys?.next || [];
27
+ const prevKeys = options.keys?.prev || [];
28
+
29
+ if (nextKeys.includes(e.key)) {
30
+ store.next();
31
+ lastTime = now;
32
+ } else if (prevKeys.includes(e.key)) {
33
+ store.prev();
34
+ lastTime = now;
35
+ }
36
+ };
37
+
38
+ onMounted(() => {
39
+ window.addEventListener('keydown', handleKeydown);
40
+ });
41
+
42
+ onUnmounted(() => {
43
+ window.removeEventListener('keydown', handleKeydown);
44
+ });
45
+ }