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
@@ -1,232 +1,237 @@
1
- <template>
2
- <div class="h-full w-full relative flex flex-col text-white overflow-hidden" data-lumina-root
3
- style="background-color: var(--lumina-colors-background, #030303); color: var(--lumina-colors-text, #ffffff);">
4
-
5
- <!-- DYNAMIC BACKGROUND -->
6
- <LuminaBackground :orb-color="slide?.meta?.orbColor" :orb-pos="orbPosition" />
7
-
8
- <!-- INSPECTOR TOGGLE -->
9
- <div v-if="isDebug" class="absolute top-0 right-0 p-6 z-50 flex gap-4 pointer-events-auto">
10
- <button @click="ui.showJson = !ui.showJson"
11
- class="w-8 h-8 rounded-full glass-panel flex items-center justify-center text-gray-400 hover:text-white transition"
12
- title="JSON"><i class="ph-thin ph-code text-xs"></i></button>
13
- </div>
14
-
15
- <!-- JSON INSPECTOR -->
16
- <transition name="fade">
17
- <div v-if="ui.showJson" class="fixed inset-0 z-[100] flex justify-end" @click.self="ui.showJson = false">
18
- <!-- Backdrop -->
19
- <div class="absolute inset-0 bg-black/20 backdrop-blur-sm transition-opacity"
20
- @click="ui.showJson = false">
21
- </div>
22
-
23
- <!-- Panel -->
24
- <div
25
- class="relative h-full w-full md:w-[450px] bg-[#0a0a0a]/95 border-l border-white/10 shadow-2xl overflow-auto text-left flex flex-col transform transition-transform duration-300">
26
-
27
- <!-- Header -->
28
- <div class="p-6 pb-2 border-b border-white/5 flex items-center justify-between shrink-0">
29
- <span class="text-xs font-bold text-gray-400 uppercase tracking-widest">Slide Data</span>
30
- <button @click="ui.showJson = false"
31
- class="w-8 h-8 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center text-white/50 hover:text-white transition">
32
- <i class="ph-thin ph-x"></i>
33
- </button>
34
- </div>
35
-
36
- <!-- Content -->
37
- <div class="p-6">
38
- <pre
39
- class="text-[10px] font-mono text-green-400 whitespace-pre-wrap">{{ JSON.stringify(slide, null, 2) }}</pre>
40
- </div>
41
- </div>
42
- </div>
43
- </transition>
44
-
45
- <!-- VIEWPORT -->
46
- <main ref="viewport" class="flex-1 min-h-0 w-full relative overflow-y-auto overflow-x-hidden z-10 scroll-smooth touch-pan-y pb-24 md:pb-0"
47
- @touchstart="onTouchStart" @touchend="onTouchEnd">
48
- <transition :css="false" @leave="onLeave" @enter="onEnter" mode="out-in">
49
- <component v-if="currentSlideComponent" :is="currentSlideComponent" :key="index" :data="interpolatedSlide ?? slide"
50
- :slide-index="index" @action="handleAction" class="min-h-full w-full">
51
- </component>
52
- </transition>
53
- </main>
54
-
55
- <!-- NAV BAR: fixed on mobile so controls stay visible (container can exceed 100vh); absolute on md+ -->
56
- <div v-if="uiOptions?.visible"
57
- class="fixed md:absolute bottom-0 left-0 right-0 w-full z-40 px-8 py-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] bg-gradient-to-t from-black/60 via-black/20 to-transparent flex justify-between items-end pointer-events-none">
58
- <div v-if="uiOptions?.showSlideCount" class="pointer-events-auto text-left">
59
- <h2 class="text-[10px] font-bold tracking-[0.2em] uppercase mb-1"
60
- :style="{ color: 'var(--lumina-color-muted)' }">{{ deckTitle }}</h2>
61
- <div class="flex items-center gap-2 text-sm font-mono"
62
- :style="{ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))', opacity: 0.8 }">
63
- <span>{{ (index + 1).toString().padStart(2, '0') }}</span>
64
- <div v-if="uiOptions?.showProgressBar" class="h-px w-8"
65
- :style="{ backgroundColor: 'var(--lumina-color-text-safe, var(--lumina-color-text))', opacity: 0.3 }">
66
- </div>
67
- <span>{{ total.toString().padStart(2, '0') }}</span>
68
- </div>
69
- </div>
70
- <!-- Spacer if count hidden -->
71
- <div v-else class="flex-1"></div>
72
-
73
- <div v-if="uiOptions?.showControls" class="pointer-events-auto flex gap-3">
74
- <!-- Notes Toggle (Sync Window) -->
75
- <button v-if="slide?.notes" @click="openSpeakerNotes"
76
- class="w-12 h-12 rounded-full glass-panel hover:bg-white/10 flex items-center justify-center transition active:scale-95"
77
- title="Open Speaker View">
78
- <i class="ph-thin ph-presentation text-sm text-gray-400 hover:text-white"></i>
79
- </button>
80
-
81
- <button v-if="isNavEnabled" @click="prev"
82
- :class="['w-12 h-12 rounded-full glass-panel hover:bg-white/10 flex items-center justify-center transition active:scale-95', !hasPrev ? 'opacity-30 cursor-not-allowed' : '']"
83
- :disabled="!hasPrev"><i class="ph-thin ph-arrow-left"></i></button>
84
- <button v-if="isNavEnabled" @click="next"
85
- :class="['w-12 h-12 rounded-full bg-white text-black hover:scale-105 flex items-center justify-center transition shadow-lg active:scale-95', !hasNext ? 'opacity-30 cursor-not-allowed' : '']"
86
- :disabled="!hasNext"><i class="ph-thin ph-arrow-right"></i></button>
87
- </div>
88
- </div>
89
- </div>
90
- </template>
91
-
92
- <script setup lang="ts">
93
- import { ref, computed, watch, inject } from 'vue';
94
- import { useLumina } from '../composables/useLumina';
95
- import { useKeyboard } from '../composables/useKeyboard';
96
- import { useSwipeNav } from '../composables/useSwipeNav';
97
- import { bus } from '../core/events';
98
- import gsap from 'gsap';
99
- import { StoreKey } from '../core/store';
100
- import { resolveTransitionConfig } from '../core/animationConfig';
101
- import { interpolateObject } from '../utils/templateInterpolation';
102
- import LuminaBackground from './parts/LuminaBackground.vue';
103
-
104
- // Composable Integration
105
- const { slide, index, total, next, prev, options } = useLumina();
106
-
107
- const viewport = ref<HTMLElement | null>(null);
108
-
109
- const store = inject(StoreKey)!;
110
-
111
- const hasNext = computed(() => store.hasNext());
112
- const hasPrev = computed(() => store.hasPrev());
113
- const deckTitle = computed(() => store.state.deck?.meta?.title);
114
- const uiOptions = computed(() => store.state.options.ui);
115
- const isNavEnabled = computed(() => store.state.options.navigation);
116
-
117
- useKeyboard();
118
- const { onTouchStart, onTouchEnd } = useSwipeNav();
119
-
120
- // Random orb position state
121
- const currentOrbPosition = ref({ top: '-20%', left: '-10%' });
122
-
123
- // Function to generate random orb position
124
- const generateRandomOrbPosition = () => {
125
- // Generate true random positions within a safe range to keep the 80vw orb visible
126
- // Range: -20% to 60% provides good coverage without losing the orb
127
- const randomRange = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1) + min);
128
-
129
- return {
130
- top: `${randomRange(-25, 65)}%`,
131
- left: `${randomRange(-25, 65)}%`
132
- };
133
- };
134
-
135
- // Watch for slide index changes to update orb position
136
- watch(index, (newIndex, oldIndex) => {
137
- // First slide (index 0) keeps default position
138
- if (newIndex === 0) {
139
- currentOrbPosition.value = { top: '-20%', left: '-10%' };
140
- } else if (oldIndex !== undefined) {
141
- // Only randomize when actually navigating between slides (not on initial load)
142
- currentOrbPosition.value = generateRandomOrbPosition();
143
- }
144
-
145
- // Reset scroll position
146
- if (viewport.value) {
147
- viewport.value.scrollTop = 0;
148
- }
149
- }, { immediate: true });
150
-
151
- const ui = ref({ showJson: false }); // Removed showNotes
152
- const engine = inject('LuminaEngine') as any;
153
-
154
- const openSpeakerNotes = () => {
155
- if (engine && typeof engine.openSpeakerNotes === 'function') {
156
- engine.openSpeakerNotes();
157
- } else {
158
- console.warn('Lumina Engine not found or openSpeakerNotes not available');
159
- alert('Speaker notes require the full Lumina Engine environment.');
160
- }
161
- };
162
-
163
- const isDebug = computed(() => options.debug);
164
-
165
- const currentSlideComponent = computed(() => {
166
- if (!slide.value || !slide.value.type) return null;
167
- return `layout-${slide.value.type}`;
168
- });
169
-
170
- /** Slide data with {{tag}} placeholders resolved from store.userData (engine.data). Updates reactively when userData changes. */
171
- const interpolatedSlide = computed(() => {
172
- const s = slide.value;
173
- if (s == null) return null;
174
- return interpolateObject(s, store.state.userData);
175
- });
176
-
177
- const orbPosition = computed(() => ({
178
- top: slide.value?.meta?.orbPos?.top || currentOrbPosition.value.top,
179
- left: slide.value?.meta?.orbPos?.left || currentOrbPosition.value.left,
180
- }));
181
-
182
- const handleAction = (payload: any) => {
183
- bus.emit('action', payload);
184
- };
185
-
186
-
187
-
188
- // --- Viewport Transitions ---
189
- const onEnter = (el: Element, done: () => void) => {
190
- const element = el as HTMLElement;
191
- const vm = (el as any).__vue_app__ || (el as any).__vue_parent__ || (el as any)?.__vueParentComponent?.proxy;
192
-
193
- // Set initial opacity to 1 so the slide container is visible
194
- element.style.opacity = '1';
195
-
196
- // Trigger internal content animations
197
- if (vm && typeof vm.animateIn === 'function') {
198
- vm.animateIn();
199
- }
200
-
201
- done();
202
- };
203
-
204
- const onLeave = async (el: Element, done: () => void) => {
205
- const cfg = resolveTransitionConfig(store, undefined);
206
- if (cfg.enabled === false) {
207
- done();
208
- return;
209
- }
210
-
211
- const vm = (el as any).__vue_app__ || (el as any).__vue_parent__ || (el as any)?.__vueParentComponent?.proxy;
212
- if (vm && typeof vm.animateOut === 'function') {
213
- await vm.animateOut();
214
- }
215
-
216
- const { durationOut, easeOut, type } = cfg;
217
- const tl = gsap.timeline({ onComplete: done });
218
-
219
- if (type === 'fade') {
220
- tl.to(el, { opacity: 0, duration: durationOut });
221
- } else if (type === 'zoom') {
222
- tl.to(el, { opacity: 0, scale: 0.95, filter: 'blur(10px)', duration: durationOut });
223
- } else {
224
- tl.to(el, {
225
- opacity: 0,
226
- y: -30,
227
- duration: durationOut,
228
- ease: easeOut
229
- });
230
- }
231
- };
232
- </script>
1
+ <template>
2
+ <div class="h-full w-full relative flex flex-col text-white overflow-hidden" data-lumina-root
3
+ style="background-color: var(--lumina-colors-background, #030303); color: var(--lumina-colors-text, #ffffff);">
4
+
5
+ <!-- DYNAMIC BACKGROUND -->
6
+ <LuminaBackground :orb-color="slide?.meta?.orbColor" :orb-pos="orbPosition" />
7
+
8
+ <!-- INSPECTOR TOGGLE -->
9
+ <div v-if="isDebug" class="absolute top-0 right-0 p-6 z-50 flex gap-4 pointer-events-auto">
10
+ <button @click="ui.showJson = !ui.showJson"
11
+ class="w-8 h-8 rounded-full glass-panel flex items-center justify-center text-gray-400 hover:text-white transition"
12
+ title="JSON"><i class="ph-thin ph-code text-xs"></i></button>
13
+ </div>
14
+
15
+ <!-- JSON INSPECTOR -->
16
+ <transition name="fade">
17
+ <div v-if="ui.showJson" class="fixed inset-0 z-[100] flex justify-end" @click.self="ui.showJson = false">
18
+ <!-- Backdrop -->
19
+ <div class="absolute inset-0 bg-black/20 backdrop-blur-sm transition-opacity"
20
+ @click="ui.showJson = false">
21
+ </div>
22
+
23
+ <!-- Panel -->
24
+ <div
25
+ class="relative h-full w-full md:w-[450px] bg-[#0a0a0a]/95 border-l border-white/10 shadow-2xl overflow-auto text-left flex flex-col transform transition-transform duration-300">
26
+
27
+ <!-- Header -->
28
+ <div class="p-6 pb-2 border-b border-white/5 flex items-center justify-between shrink-0">
29
+ <span class="text-xs font-bold text-gray-400 uppercase tracking-widest">Slide Data</span>
30
+ <button @click="ui.showJson = false"
31
+ class="w-8 h-8 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center text-white/50 hover:text-white transition">
32
+ <i class="ph-thin ph-x"></i>
33
+ </button>
34
+ </div>
35
+
36
+ <!-- Content -->
37
+ <div class="p-6">
38
+ <pre
39
+ class="text-[10px] font-mono text-green-400 whitespace-pre-wrap">{{ JSON.stringify(slide, null, 2) }}</pre>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </transition>
44
+
45
+ <!-- VIEWPORT -->
46
+ <main ref="viewport" class="flex-1 min-h-0 w-full relative overflow-y-auto overflow-x-hidden z-10 scroll-smooth touch-pan-y pb-24 md:pb-0"
47
+ @touchstart="onTouchStart" @touchend="onTouchEnd">
48
+ <transition :css="false" @leave="onLeave" @enter="onEnter" mode="out-in">
49
+ <component v-if="currentSlideComponent" :is="currentSlideComponent" :key="index" :data="interpolatedSlide ?? slide"
50
+ :slide-index="index" @action="handleAction" class="min-h-full w-full">
51
+ </component>
52
+ </transition>
53
+ </main>
54
+
55
+ <!-- NAV BAR: fixed on mobile so controls stay visible (container can exceed 100vh); absolute on md+ -->
56
+ <div v-if="uiOptions?.visible"
57
+ class="fixed md:absolute bottom-0 left-0 right-0 w-full z-40 px-8 py-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] bg-gradient-to-t from-black/60 via-black/20 to-transparent flex justify-between items-end pointer-events-none">
58
+ <div v-if="uiOptions?.showSlideCount" class="pointer-events-auto text-left">
59
+ <h2 class="text-[10px] font-bold tracking-[0.2em] uppercase mb-1"
60
+ :style="{ color: 'var(--lumina-color-muted)' }">{{ deckTitle }}</h2>
61
+ <div class="flex items-center gap-2 text-sm font-mono"
62
+ :style="{ color: 'var(--lumina-color-text-safe, var(--lumina-color-text))', opacity: 0.8 }">
63
+ <span>{{ (index + 1).toString().padStart(2, '0') }}</span>
64
+ <div v-if="uiOptions?.showProgressBar" class="h-px w-8"
65
+ :style="{ backgroundColor: 'var(--lumina-color-text-safe, var(--lumina-color-text))', opacity: 0.3 }">
66
+ </div>
67
+ <span>{{ total.toString().padStart(2, '0') }}</span>
68
+ </div>
69
+ </div>
70
+ <!-- Spacer if count hidden -->
71
+ <div v-else class="flex-1"></div>
72
+
73
+ <div v-if="uiOptions?.showControls" class="pointer-events-auto flex gap-3">
74
+ <!-- Notes Toggle (Sync Window) -->
75
+ <button v-if="slide?.notes" @click="openSpeakerNotes"
76
+ class="w-12 h-12 rounded-full glass-panel hover:bg-white/10 flex items-center justify-center transition active:scale-95"
77
+ title="Open Speaker View">
78
+ <i class="ph-thin ph-presentation text-sm text-gray-400 hover:text-white"></i>
79
+ </button>
80
+
81
+ <button v-if="isNavEnabled" @click="prev"
82
+ :class="['w-12 h-12 rounded-full glass-panel hover:bg-white/10 flex items-center justify-center transition active:scale-95', !hasPrev ? 'opacity-30 cursor-not-allowed' : '']"
83
+ :disabled="!hasPrev"><i class="ph-thin ph-arrow-left"></i></button>
84
+ <button v-if="isNavEnabled" @click="next"
85
+ :class="['w-12 h-12 rounded-full bg-white text-black hover:scale-105 flex items-center justify-center transition shadow-lg active:scale-95', !hasNext ? 'opacity-30 cursor-not-allowed' : '']"
86
+ :disabled="!hasNext"><i class="ph-thin ph-arrow-right"></i></button>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </template>
91
+
92
+ <script setup lang="ts">
93
+ import { ref, computed, watch, inject } from 'vue';
94
+ import { useLumina } from '../composables/useLumina';
95
+ import { useKeyboard } from '../composables/useKeyboard';
96
+ import { useSwipeNav } from '../composables/useSwipeNav';
97
+ import { bus } from '../core/events';
98
+ import gsap from 'gsap';
99
+ import { StoreKey } from '../core/store';
100
+ import { resolveTransitionConfig } from '../core/animationConfig';
101
+ import { interpolateObject } from '../utils/templateInterpolation';
102
+ import LuminaBackground from './parts/LuminaBackground.vue';
103
+
104
+ // Composable Integration
105
+ const { slide, index, total, next, prev, options } = useLumina();
106
+
107
+ const viewport = ref<HTMLElement | null>(null);
108
+
109
+ const store = inject(StoreKey)!;
110
+
111
+ const hasNext = computed(() => store.hasNext());
112
+ const hasPrev = computed(() => store.hasPrev());
113
+ const deckTitle = computed(() => store.state.deck?.meta?.title);
114
+ const uiOptions = computed(() => store.state.options.ui);
115
+ const isNavEnabled = computed(() => store.state.options.navigation);
116
+
117
+ useKeyboard();
118
+ const { onTouchStart, onTouchEnd } = useSwipeNav();
119
+
120
+ // Random orb position state
121
+ const currentOrbPosition = ref({ top: '-20%', left: '-10%' });
122
+
123
+ // Function to generate random orb position
124
+ const generateRandomOrbPosition = () => {
125
+ // Generate true random positions within a safe range to keep the 80vw orb visible
126
+ // Range: -20% to 60% provides good coverage without losing the orb
127
+ const randomRange = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1) + min);
128
+
129
+ return {
130
+ top: `${randomRange(-25, 65)}%`,
131
+ left: `${randomRange(-25, 65)}%`
132
+ };
133
+ };
134
+
135
+ // Watch for slide index changes to update orb position
136
+ watch(index, (newIndex, oldIndex) => {
137
+ // First slide (index 0) keeps default position
138
+ if (newIndex === 0) {
139
+ currentOrbPosition.value = { top: '-20%', left: '-10%' };
140
+ } else if (oldIndex !== undefined) {
141
+ // Only randomize when actually navigating between slides (not on initial load)
142
+ currentOrbPosition.value = generateRandomOrbPosition();
143
+ }
144
+
145
+ // Reset scroll position
146
+ if (viewport.value) {
147
+ viewport.value.scrollTop = 0;
148
+ }
149
+ }, { immediate: true });
150
+
151
+ const ui = ref({ showJson: false }); // Removed showNotes
152
+ const engine = inject('LuminaEngine') as any;
153
+
154
+ const openSpeakerNotes = () => {
155
+ if (engine && typeof engine.openSpeakerNotes === 'function') {
156
+ engine.openSpeakerNotes();
157
+ } else {
158
+ console.warn('Lumina Engine not found or openSpeakerNotes not available');
159
+ alert('Speaker notes require the full Lumina Engine environment.');
160
+ }
161
+ };
162
+
163
+ const isDebug = computed(() => options.debug);
164
+
165
+ const currentSlideComponent = computed(() => {
166
+ if (!slide.value || !slide.value.type) return null;
167
+ return `layout-${slide.value.type}`;
168
+ });
169
+
170
+ /** Slide data with {{tag}} placeholders resolved from store.userData (engine.data). Updates reactively when userData changes. */
171
+ const interpolatedSlide = computed(() => {
172
+ const s = slide.value;
173
+ if (s == null) return null;
174
+ return interpolateObject(s, store.state.userData);
175
+ });
176
+
177
+ const orbPosition = computed(() => ({
178
+ top: slide.value?.meta?.orbPos?.top || currentOrbPosition.value.top,
179
+ left: slide.value?.meta?.orbPos?.left || currentOrbPosition.value.left,
180
+ }));
181
+
182
+ const handleAction = (payload: any) => {
183
+ bus.emit('action', payload);
184
+ };
185
+
186
+
187
+
188
+ // --- Viewport Transitions ---
189
+ const onEnter = (el: Element, done: () => void) => {
190
+ const element = el as HTMLElement;
191
+ const vm = (el as any).__vue_app__ || (el as any).__vue_parent__ || (el as any)?.__vueParentComponent?.proxy;
192
+
193
+ // Set initial opacity to 1 so the slide container is visible
194
+ element.style.opacity = '1';
195
+
196
+ // Trigger internal content animations
197
+ if (vm && typeof vm.animateIn === 'function') {
198
+ vm.animateIn();
199
+ }
200
+
201
+ // Run reveal cascade once the slide is mounted (elements exist in DOM)
202
+ if (engine && typeof engine.runRevealForSlideIfNeeded === 'function') {
203
+ engine.runRevealForSlideIfNeeded(store.state.currentIndex);
204
+ }
205
+
206
+ done();
207
+ };
208
+
209
+ const onLeave = async (el: Element, done: () => void) => {
210
+ const cfg = resolveTransitionConfig(store, undefined);
211
+ if (cfg.enabled === false) {
212
+ done();
213
+ return;
214
+ }
215
+
216
+ const vm = (el as any).__vue_app__ || (el as any).__vue_parent__ || (el as any)?.__vueParentComponent?.proxy;
217
+ if (vm && typeof vm.animateOut === 'function') {
218
+ await vm.animateOut();
219
+ }
220
+
221
+ const { durationOut, easeOut, type } = cfg;
222
+ const tl = gsap.timeline({ onComplete: done });
223
+
224
+ if (type === 'fade') {
225
+ tl.to(el, { opacity: 0, duration: durationOut });
226
+ } else if (type === 'zoom') {
227
+ tl.to(el, { opacity: 0, scale: 0.95, filter: 'blur(10px)', duration: durationOut });
228
+ } else {
229
+ tl.to(el, {
230
+ opacity: 0,
231
+ y: -30,
232
+ duration: durationOut,
233
+ ease: easeOut
234
+ });
235
+ }
236
+ };
237
+ </script>
@@ -58,6 +58,8 @@ const propClass = computed(() => {
58
58
 
59
59
  const mergedStyle = computed(() => {
60
60
  const resolved = unref(state.style) ?? {};
61
+ // State (opacity, transform from element control) must override props for visibility.
62
+ // Props provide layout styles (display, position, sizing); state provides opacity/transform.
61
63
  const out = { ...(props.style || {}), ...resolved };
62
64
  if ('opacity' in resolved && out.transition == null) {
63
65
  out.transition = 'opacity var(--lumina-element-opacity-transition, 0.35s ease-out)';