lumina-slides 9.0.5 → 9.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/lumina-slides.js +21750 -19334
- package/dist/lumina-slides.umd.cjs +223 -223
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/LandingPage.vue +1 -1
- package/src/components/LuminaDeck.vue +237 -232
- package/src/components/base/LuminaElement.vue +2 -0
- package/src/components/layouts/LayoutFeatures.vue +125 -123
- package/src/components/layouts/LayoutFlex.vue +212 -212
- package/src/components/layouts/LayoutStatement.vue +5 -2
- package/src/components/layouts/LayoutSteps.vue +110 -108
- package/src/components/parts/FlexHtml.vue +65 -65
- package/src/components/parts/FlexImage.vue +81 -81
- package/src/components/site/SiteDocs.vue +3313 -3314
- package/src/components/site/SiteExamples.vue +66 -66
- package/src/components/studio/EditorLayoutChart.vue +18 -0
- package/src/components/studio/EditorLayoutCustom.vue +18 -0
- package/src/components/studio/EditorLayoutVideo.vue +18 -0
- package/src/components/studio/LuminaStudioEmbed.vue +68 -0
- package/src/components/studio/StudioEmbedRoot.vue +19 -0
- package/src/components/studio/StudioInspector.vue +1113 -7
- package/src/components/studio/StudioJsonEditor.vue +10 -3
- package/src/components/studio/StudioSettings.vue +658 -7
- package/src/components/studio/StudioToolbar.vue +26 -7
- package/src/composables/useElementState.ts +12 -1
- package/src/composables/useFlexLayout.ts +128 -128
- package/src/core/Lumina.ts +174 -113
- package/src/core/animationConfig.ts +10 -0
- package/src/core/elementController.ts +18 -0
- package/src/core/elementResolver.ts +4 -2
- package/src/core/schema.ts +503 -503
- package/src/core/store.ts +465 -465
- package/src/core/types.ts +26 -11
- package/src/index.ts +2 -2
- package/src/utils/prepareDeckForExport.ts +47 -0
- package/src/utils/templateInterpolation.ts +52 -52
- package/src/views/DeckView.vue +313 -313
|
@@ -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="
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|