lumina-slides 9.0.5 → 9.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +63 -0
  2. package/dist/lumina-slides.js +21750 -19334
  3. package/dist/lumina-slides.umd.cjs +223 -223
  4. package/dist/style.css +1 -1
  5. package/package.json +1 -1
  6. package/src/components/LandingPage.vue +1 -1
  7. package/src/components/LuminaDeck.vue +237 -232
  8. package/src/components/base/LuminaElement.vue +2 -0
  9. package/src/components/layouts/LayoutFeatures.vue +125 -123
  10. package/src/components/layouts/LayoutFlex.vue +212 -212
  11. package/src/components/layouts/LayoutStatement.vue +5 -2
  12. package/src/components/layouts/LayoutSteps.vue +110 -108
  13. package/src/components/parts/FlexHtml.vue +65 -65
  14. package/src/components/parts/FlexImage.vue +81 -81
  15. package/src/components/site/SiteDocs.vue +3313 -3314
  16. package/src/components/site/SiteExamples.vue +66 -66
  17. package/src/components/studio/EditorLayoutChart.vue +18 -0
  18. package/src/components/studio/EditorLayoutCustom.vue +18 -0
  19. package/src/components/studio/EditorLayoutVideo.vue +18 -0
  20. package/src/components/studio/LuminaStudioEmbed.vue +68 -0
  21. package/src/components/studio/StudioEmbedRoot.vue +19 -0
  22. package/src/components/studio/StudioInspector.vue +1113 -7
  23. package/src/components/studio/StudioJsonEditor.vue +10 -3
  24. package/src/components/studio/StudioSettings.vue +658 -7
  25. package/src/components/studio/StudioToolbar.vue +26 -7
  26. package/src/composables/useElementState.ts +12 -1
  27. package/src/composables/useFlexLayout.ts +128 -128
  28. package/src/core/Lumina.ts +174 -113
  29. package/src/core/animationConfig.ts +10 -0
  30. package/src/core/elementController.ts +18 -0
  31. package/src/core/elementResolver.ts +4 -2
  32. package/src/core/schema.ts +503 -503
  33. package/src/core/store.ts +465 -465
  34. package/src/core/types.ts +26 -11
  35. package/src/index.ts +2 -2
  36. package/src/utils/prepareDeckForExport.ts +47 -0
  37. package/src/utils/templateInterpolation.ts +52 -52
  38. package/src/views/DeckView.vue +313 -313
@@ -3,7 +3,7 @@
3
3
  <!-- Logo -->
4
4
  <div class="flex items-center gap-3">
5
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">
6
+ <router-link v-if="!embedMode" :to="{ name: 'dashboard' }" class="text-white/40 hover:text-white text-xs transition">
7
7
  <i class="ph-thin ph-arrow-left mr-1"></i> Exit
8
8
  </router-link>
9
9
  </div>
@@ -52,7 +52,13 @@
52
52
  <i class="ph-thin ph-arrow-u-up-right"></i>
53
53
  </button>
54
54
  <div class="w-px h-4 bg-[#444] mx-1"></div>
55
- <button @click="saveToFirestore" :disabled="isSaving"
55
+ <button v-if="embedMode" @click="emitSave" :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 class="ph-thin ph-floppy-disk"></i>
59
+ {{ isSaving ? 'SAVING...' : 'SAVE' }}
60
+ </button>
61
+ <button v-else @click="saveToFirestore" :disabled="isSaving"
56
62
  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
63
  <i v-if="isSaving" class="ph-thin ph-spinner ph-spin"></i>
58
64
  <i v-else-if="showSavedMessage" class="ph-thin ph-check"></i>
@@ -77,15 +83,19 @@
77
83
  </template>
78
84
 
79
85
  <script setup lang="ts">
80
- import { computed, ref, onMounted, watch, toRaw } from 'vue';
86
+ import { computed, ref, onMounted, watch } from 'vue';
81
87
  import { useRoute } from 'vue-router';
82
88
  import { useEditor } from '../../composables/useEditor';
83
89
  import { useAuth } from '../../composables/useAuth';
84
90
  import { saveDeck, updateDeck } from '../../utils/firebase';
85
91
  import type { Deck } from '../../core/types';
92
+ import { prepareDeckForExport } from '../../utils/prepareDeckForExport';
86
93
  // import SaveSuccessModal from './SaveSuccessModal.vue';
87
94
  import StudioJsonEditor from './StudioJsonEditor.vue';
88
95
 
96
+ const props = withDefaults(defineProps<{ embedMode?: boolean }>(), { embedMode: false });
97
+ const emit = defineEmits<{ save: [deck: Deck] }>();
98
+
89
99
  const editor = useEditor();
90
100
  const route = useRoute();
91
101
  const { user } = useAuth();
@@ -150,17 +160,25 @@ const addElement = (type: string) => {
150
160
  }
151
161
  };
152
162
 
163
+ const emitSave = () => {
164
+ const rawDeck = editor.store.state.deck;
165
+ if (!rawDeck) return;
166
+ isSaving.value = true;
167
+ const deckCopy = prepareDeckForExport(rawDeck);
168
+ emit('save', deckCopy);
169
+ isSaving.value = false;
170
+ };
171
+
153
172
  const saveToFirestore = async () => {
154
173
  isSaving.value = true;
155
174
  try {
156
- // Use toRaw to strip Vue proxies for a cleaner JSON stringify
157
- const rawDeck = toRaw(editor.store.state.deck);
175
+ const rawDeck = editor.store.state.deck;
158
176
  if (!rawDeck) {
159
177
  alert('No deck found to save.');
160
178
  return;
161
179
  }
162
180
 
163
- const deckToSave = JSON.parse(JSON.stringify(rawDeck));
181
+ const deckToSave = prepareDeckForExport(rawDeck);
164
182
 
165
183
 
166
184
 
@@ -208,7 +226,8 @@ const exportDeck = () => {
208
226
  const deck = editor.store.state.deck;
209
227
  if (!deck) return;
210
228
 
211
- const json = JSON.stringify(deck, null, 2);
229
+ const deckForExport = prepareDeckForExport(deck);
230
+ const json = JSON.stringify(deckForExport, null, 2);
212
231
  const blob = new Blob([json], { type: 'application/json' });
213
232
  const url = URL.createObjectURL(blob);
214
233
 
@@ -66,7 +66,18 @@ export function useElementState(id: MaybeRefOrGetter<string>): UseElementStateRe
66
66
  out.opacity = s.opacity;
67
67
  }
68
68
  if (s?.transform) out.transform = s.transform;
69
- if (s?.style && typeof s.style === 'object') Object.assign(out, s.style);
69
+ // Only apply safe animation/visual styles from state, not layout-critical properties
70
+ // This prevents state from breaking component layouts (e.g., display, position, flex properties)
71
+ if (s?.style && typeof s.style === 'object') {
72
+ const safeStyleProps = ['opacity', 'transform', 'transition', 'filter', 'backdropFilter',
73
+ 'willChange', 'pointerEvents', 'zIndex', 'color', 'backgroundColor', 'borderColor'];
74
+ Object.entries(s.style).forEach(([key, value]) => {
75
+ // Only copy safe properties that don't affect layout structure
76
+ if (safeStyleProps.includes(key) || key.startsWith('--')) {
77
+ out[key] = value;
78
+ }
79
+ });
80
+ }
70
81
  return out;
71
82
  });
72
83
 
@@ -1,128 +1,128 @@
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
- const direction = element.direction || 'vertical';
67
-
68
- return {
69
- display: 'flex',
70
- flexDirection: direction === 'horizontal' ? 'row' : 'column',
71
- justifyContent: direction === 'horizontal'
72
- ? halignMap[halign] as CSSProperties['justifyContent']
73
- : valignMap[valign] as CSSProperties['justifyContent'],
74
- alignItems: direction === 'horizontal'
75
- ? valignMap[valign] as CSSProperties['alignItems']
76
- : halignMap[halign] as CSSProperties['alignItems'],
77
- gap: spacingVarMap[gap],
78
- padding: spacingVarMap[padding],
79
- ...(element.style || {}),
80
- };
81
- };
82
-
83
- // Deprecated: kept for backward compat if needed, but we will update consumers
84
- export const getElementStyle = getFlexItemStyle;
85
- export const getContentStyle = (element: FlexElementContent & { size?: string }): CSSProperties => {
86
- // Legacy combined style
87
- return {
88
- ...getFlexItemStyle(element, false), // Default assumption
89
- ...getFlexContainerStyle(element)
90
- };
91
- };
92
-
93
- export const getTopLevelStyle = (): CSSProperties => ({
94
- display: 'flex',
95
- alignItems: 'center',
96
- padding: spacingVarMap['lg'],
97
- });
98
-
99
- /**
100
- * Alignment for the main flex slide container (all elements as a group).
101
- * When direction is horizontal: justify-content = halign, align-items = valign.
102
- * When direction is vertical: justify-content = valign, align-items = halign.
103
- */
104
- export const getFlexSlideMainStyle = (
105
- direction: 'horizontal' | 'vertical' | undefined,
106
- halign: string | undefined,
107
- valign: string | undefined
108
- ): CSSProperties => {
109
- const jc = direction === 'vertical' ? valignMap[valign || 'top'] : halignMap[halign || 'left'];
110
- const ai = direction === 'vertical' ? halignMap[halign || 'left'] : valignMap[valign || 'top'];
111
- return {
112
- justifyContent: jc as CSSProperties['justifyContent'],
113
- alignItems: ai as CSSProperties['alignItems'],
114
- };
115
- };
116
-
117
- export const getRoundedClass = (rounded?: string, fill?: boolean) => {
118
- if (fill !== false && !rounded) return 'w-full h-full';
119
- const map: Record<string, string> = {
120
- 'none': 'w-full h-full rounded-none',
121
- 'sm': 'w-full h-full rounded-sm',
122
- 'md': 'w-full h-full rounded-md',
123
- 'lg': 'w-full h-full rounded-lg',
124
- 'xl': 'w-full h-full rounded-xl',
125
- 'full': 'w-full h-full rounded-full',
126
- };
127
- return map[rounded || 'lg'] || 'w-full h-full rounded-lg';
128
- };
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
+ const direction = element.direction || 'vertical';
67
+
68
+ return {
69
+ display: 'flex',
70
+ flexDirection: direction === 'horizontal' ? 'row' : 'column',
71
+ justifyContent: direction === 'horizontal'
72
+ ? halignMap[halign] as CSSProperties['justifyContent']
73
+ : valignMap[valign] as CSSProperties['justifyContent'],
74
+ alignItems: direction === 'horizontal'
75
+ ? valignMap[valign] as CSSProperties['alignItems']
76
+ : halignMap[halign] as CSSProperties['alignItems'],
77
+ gap: spacingVarMap[gap],
78
+ padding: spacingVarMap[padding],
79
+ ...(element.style || {}),
80
+ };
81
+ };
82
+
83
+ // Deprecated: kept for backward compat if needed, but we will update consumers
84
+ export const getElementStyle = getFlexItemStyle;
85
+ export const getContentStyle = (element: FlexElementContent & { size?: string }): CSSProperties => {
86
+ // Legacy combined style
87
+ return {
88
+ ...getFlexItemStyle(element, false), // Default assumption
89
+ ...getFlexContainerStyle(element)
90
+ };
91
+ };
92
+
93
+ export const getTopLevelStyle = (): CSSProperties => ({
94
+ display: 'flex',
95
+ alignItems: 'center',
96
+ padding: spacingVarMap['lg'],
97
+ });
98
+
99
+ /**
100
+ * Alignment for the main flex slide container (all elements as a group).
101
+ * When direction is horizontal: justify-content = halign, align-items = valign.
102
+ * When direction is vertical: justify-content = valign, align-items = halign.
103
+ */
104
+ export const getFlexSlideMainStyle = (
105
+ direction: 'horizontal' | 'vertical' | undefined,
106
+ halign: string | undefined,
107
+ valign: string | undefined
108
+ ): CSSProperties => {
109
+ const jc = direction === 'vertical' ? valignMap[valign || 'top'] : halignMap[halign || 'left'];
110
+ const ai = direction === 'vertical' ? halignMap[halign || 'left'] : valignMap[valign || 'top'];
111
+ return {
112
+ justifyContent: jc as CSSProperties['justifyContent'],
113
+ alignItems: ai as CSSProperties['alignItems'],
114
+ };
115
+ };
116
+
117
+ export const getRoundedClass = (rounded?: string, fill?: boolean) => {
118
+ if (fill !== false && !rounded) return 'w-full h-full';
119
+ const map: Record<string, string> = {
120
+ 'none': 'w-full h-full rounded-none',
121
+ 'sm': 'w-full h-full rounded-sm',
122
+ 'md': 'w-full h-full rounded-md',
123
+ 'lg': 'w-full h-full rounded-lg',
124
+ 'xl': 'w-full h-full rounded-xl',
125
+ 'full': 'w-full h-full rounded-full',
126
+ };
127
+ return map[rounded || 'lg'] || 'w-full h-full rounded-lg';
128
+ };