lumina-slides 9.0.4 → 9.0.6
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/dist/lumina-slides.js +21984 -19455
- package/dist/lumina-slides.umd.cjs +223 -223
- package/dist/style.css +1 -1
- package/package.json +3 -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 +123 -123
- package/src/components/layouts/LayoutFlex.vue +212 -172
- package/src/components/layouts/LayoutStatement.vue +5 -2
- package/src/components/layouts/LayoutSteps.vue +108 -108
- package/src/components/parts/FlexHtml.vue +65 -0
- package/src/components/parts/FlexImage.vue +81 -54
- package/src/components/site/SiteDocs.vue +3313 -3182
- 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/StudioSettings.vue +658 -7
- package/src/components/studio/StudioToolbar.vue +20 -2
- package/src/composables/useElementState.ts +12 -1
- package/src/composables/useFlexLayout.ts +128 -122
- 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 -478
- package/src/core/store.ts +465 -465
- package/src/core/types.ts +59 -14
- package/src/index.ts +2 -2
- package/src/utils/templateInterpolation.ts +52 -52
- package/src/views/DeckView.vue +313 -313
package/src/core/Lumina.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { createApp, App as VueApp, watch } from 'vue';
|
|
1
|
+
import { createApp, App as VueApp, watch, ref, type Ref } from 'vue';
|
|
2
2
|
import LuminaDeck from '../components/LuminaDeck.vue';
|
|
3
3
|
import LuminaStudio from '../components/studio/LuminaStudio.vue';
|
|
4
|
+
import StudioEmbedRoot from '../components/studio/StudioEmbedRoot.vue';
|
|
4
5
|
import LuminaSpeakerNotes from '../components/LuminaSpeakerNotes.vue';
|
|
5
6
|
import LayoutStatement from '../components/layouts/LayoutStatement.vue';
|
|
6
7
|
import LayoutHalf from '../components/layouts/LayoutHalf.vue';
|
|
@@ -18,7 +19,7 @@ import LuminaElement from '../components/base/LuminaElement.vue';
|
|
|
18
19
|
import { createStore, StoreKey, LuminaStore } from './store';
|
|
19
20
|
import { resolveTransitionConfig } from './animationConfig';
|
|
20
21
|
import { computeStagger, resolveAnimationFromInput } from '../animation';
|
|
21
|
-
import { createElementController } from './elementController';
|
|
22
|
+
import { createElementController, createNoopElementController } from './elementController';
|
|
22
23
|
import { parsePath, resolveId, getElementIds } from './elementResolver';
|
|
23
24
|
import { elemId } from './elementId';
|
|
24
25
|
import type { ElementPath } from './elementResolver';
|
|
@@ -34,8 +35,8 @@ import '../style/main.css';
|
|
|
34
35
|
* @description
|
|
35
36
|
* Mount a slide deck into a DOM element, load/patch deck data, subscribe to events (slideChange, action),
|
|
36
37
|
* and export state for LLM context. Vanilla JS: use `engine.load(deck)`, `engine.element(id)`, `engine.on(...)`.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* Elements start hidden by default and cascade-reveal on slide enter. Set `elementControl.defaultVisible: true`
|
|
39
|
+
* to show all, or `slide.reveal: false` for manual control. Use `engine.element(id).show()` or `revealInSequence`.
|
|
39
40
|
* `engine.element(id).animate()` and `getElementById` require the element's slide to be mounted (no-op/null otherwise).
|
|
40
41
|
*
|
|
41
42
|
* @example
|
|
@@ -44,9 +45,7 @@ import '../style/main.css';
|
|
|
44
45
|
* import "lumina-slides/style.css";
|
|
45
46
|
*
|
|
46
47
|
* const engine = new Lumina("#app", { theme: "ocean", loop: true });
|
|
47
|
-
* engine.
|
|
48
|
-
* engine.on("slideChange", (e) => engine.revealInSequence(e.index));
|
|
49
|
-
* engine.load({ meta: { title: "Demo", elementControl: { defaultVisible: false } }, slides: [
|
|
48
|
+
* engine.load({ meta: { title: "Demo" }, slides: [
|
|
50
49
|
* { type: "statement", title: "Hello", subtitle: "World" }
|
|
51
50
|
* ]});
|
|
52
51
|
* engine.on("action", (p) => console.log("clicked", p));
|
|
@@ -59,9 +58,16 @@ import '../style/main.css';
|
|
|
59
58
|
* @see IMPLEMENTATION.md
|
|
60
59
|
* @see AGENTS.md
|
|
61
60
|
*/
|
|
61
|
+
const DEFAULT_EMBED_DECK: Deck = {
|
|
62
|
+
meta: { title: 'Untitled' },
|
|
63
|
+
slides: [{ type: 'statement', title: 'New Slide', subtitle: '' }]
|
|
64
|
+
};
|
|
65
|
+
|
|
62
66
|
export class Lumina {
|
|
63
67
|
private app: VueApp;
|
|
64
|
-
private store: LuminaStore;
|
|
68
|
+
private store: LuminaStore | null = null;
|
|
69
|
+
/** When studioEmbed is true, deck is driven by this ref; store is not used. */
|
|
70
|
+
private _embedDeckRef: Ref<Deck | null> | null = null;
|
|
65
71
|
|
|
66
72
|
/** Key-value store: `engine.data.get/set/has/delete/clear/keys`. Persists across deck loads. */
|
|
67
73
|
public readonly data: LuminaDataStore;
|
|
@@ -86,88 +92,93 @@ export class Lumina {
|
|
|
86
92
|
// Generate unique channel ID for this instance
|
|
87
93
|
this.speakerChannelId = `lumina-speaker-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
88
94
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
95
|
+
const isStudioEmbed = !!options.studioEmbed;
|
|
96
|
+
|
|
97
|
+
if (isStudioEmbed) {
|
|
98
|
+
this._embedDeckRef = ref<Deck | null>(
|
|
99
|
+
options.initialDeck && Array.isArray(options.initialDeck.slides)
|
|
100
|
+
? options.initialDeck
|
|
101
|
+
: DEFAULT_EMBED_DECK
|
|
102
|
+
);
|
|
103
|
+
this.data = {
|
|
104
|
+
get: () => '',
|
|
105
|
+
set: () => this.data!,
|
|
106
|
+
has: () => false,
|
|
107
|
+
delete: () => false,
|
|
108
|
+
clear: () => {},
|
|
109
|
+
keys: () => []
|
|
110
|
+
} as LuminaDataStore;
|
|
111
|
+
|
|
112
|
+
if (options.theme) ThemeManager.inject(options.theme);
|
|
113
|
+
|
|
114
|
+
this.app = createApp(StudioEmbedRoot, { deckRef: this._embedDeckRef });
|
|
115
|
+
} else {
|
|
116
|
+
this.store = createStore(options);
|
|
117
|
+
const dataApi: LuminaDataStore = {
|
|
118
|
+
get: (k) => this.store!.getUserData(k),
|
|
119
|
+
set: (k, v) => { this.store!.setUserData(k, v); return dataApi; },
|
|
120
|
+
has: (k) => this.store!.hasUserData(k),
|
|
121
|
+
delete: (k) => this.store!.deleteUserData(k),
|
|
122
|
+
clear: () => this.store!.clearUserData(),
|
|
123
|
+
keys: () => Object.keys(this.store!.state.userData)
|
|
124
|
+
};
|
|
125
|
+
this.data = dataApi;
|
|
126
|
+
|
|
127
|
+
if (options.theme) ThemeManager.inject(options.theme);
|
|
128
|
+
|
|
129
|
+
this.app = options.studio ? createApp(LuminaStudio) : createApp(LuminaDeck);
|
|
130
|
+
this.app.provide(StoreKey, this.store);
|
|
131
|
+
this.app.provide('LuminaEngine', this);
|
|
132
|
+
|
|
133
|
+
watch(() => this.store!.state.currentIndex, (newIndex, oldIndex) => {
|
|
134
|
+
this.store!.resetElementStateForSlide(newIndex);
|
|
135
|
+
const deck = this.store!.state.deck;
|
|
136
|
+
const currentSlide = deck?.slides?.[newIndex];
|
|
137
|
+
if (currentSlide) {
|
|
138
|
+
bus.emit('slideChange', {
|
|
139
|
+
index: newIndex,
|
|
140
|
+
previousIndex: oldIndex ?? 0,
|
|
141
|
+
slide: currentSlide
|
|
142
|
+
});
|
|
143
|
+
// Reveal runs from Deck onEnter when the slide has mounted (elements in DOM).
|
|
128
144
|
}
|
|
129
|
-
}
|
|
130
|
-
});
|
|
145
|
+
});
|
|
131
146
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const deck = this.store.state.deck;
|
|
153
|
-
const themeOrName = deck?.theme ?? newMeta.theme ?? this.store.state.options?.theme ?? 'default';
|
|
154
|
-
|
|
155
|
-
// 2. Dynamic Theme Application
|
|
156
|
-
// This ensures that any change in Studio (which updates deck.meta) is immediately reflected
|
|
157
|
-
// via CSS variables injected by ThemeManager.
|
|
158
|
-
ThemeManager.inject(themeOrName, overrides);
|
|
159
|
-
|
|
160
|
-
// 3. Fallback/Direct variable handling if needed (e.g. for specific legacy props)
|
|
161
|
-
if (newMeta.orbColor) {
|
|
162
|
-
// Update the primary color variable as a fallback or if specifically mapped
|
|
163
|
-
document.documentElement.style.setProperty('--lumina-colors-primary', newMeta.orbColor);
|
|
147
|
+
watch(() => this.store!.state.deck?.meta, (newMeta) => {
|
|
148
|
+
if (newMeta) {
|
|
149
|
+
const fromThemeConfig = newMeta.themeConfig || {};
|
|
150
|
+
const direct: Partial<ThemeConfig> = {
|
|
151
|
+
colors: newMeta.colors,
|
|
152
|
+
typography: newMeta.typography,
|
|
153
|
+
spacing: newMeta.spacing,
|
|
154
|
+
borderRadius: newMeta.borderRadius,
|
|
155
|
+
effects: newMeta.effects,
|
|
156
|
+
components: newMeta.components
|
|
157
|
+
};
|
|
158
|
+
const base = (fromThemeConfig && typeof fromThemeConfig === 'object' && !Array.isArray(fromThemeConfig) ? fromThemeConfig : {});
|
|
159
|
+
const overrides = deepMerge<ThemeConfig>(base as ThemeConfig, direct);
|
|
160
|
+
const deck = this.store!.state.deck;
|
|
161
|
+
const themeOrName = deck?.theme ?? newMeta.theme ?? this.store!.state.options?.theme ?? 'default';
|
|
162
|
+
ThemeManager.inject(themeOrName, overrides);
|
|
163
|
+
if (newMeta.orbColor) {
|
|
164
|
+
document.documentElement.style.setProperty('--lumina-colors-primary', newMeta.orbColor);
|
|
165
|
+
}
|
|
166
|
+
bus.emit('themeChange', { theme: typeof themeOrName === 'string' ? themeOrName : 'custom' });
|
|
164
167
|
}
|
|
168
|
+
}, { deep: true });
|
|
169
|
+
|
|
170
|
+
bus.on('action', (payload) => {
|
|
171
|
+
if (payload.type === 'slide-update' && typeof payload.slideIndex === 'number') {
|
|
172
|
+
const { slideIndex, patch } = payload;
|
|
173
|
+
if (patch && typeof patch === 'object') {
|
|
174
|
+
if ('nodes' in patch) this.store!.updateNode(`slides.${slideIndex}.nodes`, patch.nodes);
|
|
175
|
+
if ('edges' in patch) this.store!.updateNode(`slides.${slideIndex}.edges`, patch.edges);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
this.store!.recordAction(payload);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
165
181
|
|
|
166
|
-
bus.emit('themeChange', { theme: typeof themeOrName === 'string' ? themeOrName : 'custom' });
|
|
167
|
-
}
|
|
168
|
-
}, { deep: true });
|
|
169
|
-
|
|
170
|
-
// Register Core Components
|
|
171
182
|
this.app.component('layout-statement', LayoutStatement);
|
|
172
183
|
this.app.component('layout-half', LayoutHalf);
|
|
173
184
|
this.app.component('layout-features', LayoutFeatures);
|
|
@@ -182,26 +193,9 @@ export class Lumina {
|
|
|
182
193
|
this.app.component('layout-free', LayoutFree);
|
|
183
194
|
this.app.component('LuminaElement', LuminaElement);
|
|
184
195
|
|
|
185
|
-
// Internal Event Listeners
|
|
186
|
-
bus.on('action', (payload) => {
|
|
187
|
-
// Feature: Real-time Slide Updates (e.g. from Diagram Layout)
|
|
188
|
-
if (payload.type === 'slide-update' && typeof payload.slideIndex === 'number') {
|
|
189
|
-
const { slideIndex, patch } = payload;
|
|
190
|
-
if (patch && typeof patch === 'object') {
|
|
191
|
-
if ('nodes' in patch) {
|
|
192
|
-
this.store.updateNode(`slides.${slideIndex}.nodes`, patch.nodes);
|
|
193
|
-
}
|
|
194
|
-
if ('edges' in patch) {
|
|
195
|
-
this.store.updateNode(`slides.${slideIndex}.edges`, patch.edges);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
this.store.recordAction(payload);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
196
|
this.app.mount(this.selector);
|
|
203
197
|
|
|
204
|
-
this.syncSpeakerNotes();
|
|
198
|
+
if (!isStudioEmbed) this.syncSpeakerNotes();
|
|
205
199
|
}
|
|
206
200
|
|
|
207
201
|
/**
|
|
@@ -219,13 +213,14 @@ export class Lumina {
|
|
|
219
213
|
return;
|
|
220
214
|
}
|
|
221
215
|
try {
|
|
222
|
-
this.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
216
|
+
if (this._embedDeckRef) {
|
|
217
|
+
this._embedDeckRef.value = deckData;
|
|
218
|
+
} else if (this.store) {
|
|
219
|
+
this.store.loadDeck(deckData);
|
|
220
|
+
// Reveal runs from Deck onEnter when the first slide has mounted.
|
|
221
|
+
this.syncSpeakerNotes();
|
|
227
222
|
}
|
|
228
|
-
|
|
223
|
+
bus.emit('ready', deckData);
|
|
229
224
|
} catch (e) {
|
|
230
225
|
bus.emit('error', e instanceof Error ? e : new Error(String(e)));
|
|
231
226
|
}
|
|
@@ -241,6 +236,7 @@ export class Lumina {
|
|
|
241
236
|
* engine.patch({ meta: { title: "Updated Title" } });
|
|
242
237
|
*/
|
|
243
238
|
public patch(diff: Partial<Deck>) {
|
|
239
|
+
if (!this.store) return;
|
|
244
240
|
this.store.patchDeck(diff);
|
|
245
241
|
bus.emit('patch', { diff: diff ?? {} });
|
|
246
242
|
this.syncSpeakerNotes();
|
|
@@ -256,6 +252,7 @@ export class Lumina {
|
|
|
256
252
|
* // "User is on slide 3 (features). Session: clicked 'Learn More' -> navigated next."
|
|
257
253
|
*/
|
|
258
254
|
public exportState() {
|
|
255
|
+
if (!this.store) return Object.freeze({ status: 'active' as const, currentSlide: null, narrative: '', engagementLevel: 'Low Engagement', history: [] });
|
|
259
256
|
const { currentIndex, deck, actionHistory } = this.store.state;
|
|
260
257
|
const slide = deck?.slides[currentIndex];
|
|
261
258
|
const context = actionHistory.map(a => `User ${a.type}ed on ${a.label || a.value}`).join(' -> ');
|
|
@@ -323,9 +320,9 @@ export class Lumina {
|
|
|
323
320
|
bus.clear();
|
|
324
321
|
}
|
|
325
322
|
|
|
326
|
-
/** Zero-based index of the currently visible slide. */
|
|
323
|
+
/** Zero-based index of the currently visible slide. In studioEmbed mode returns 0. */
|
|
327
324
|
public get currentSlideIndex() {
|
|
328
|
-
return this.store.state.currentIndex;
|
|
325
|
+
return this.store ? this.store.state.currentIndex : 0;
|
|
329
326
|
}
|
|
330
327
|
|
|
331
328
|
/**
|
|
@@ -333,6 +330,7 @@ export class Lumina {
|
|
|
333
330
|
* @param index - Zero-based slide index. Clamped to [0, slides.length - 1].
|
|
334
331
|
*/
|
|
335
332
|
public goTo(index: number) {
|
|
333
|
+
if (!this.store) return;
|
|
336
334
|
const from = this.store.state.currentIndex;
|
|
337
335
|
this.store.goto(index);
|
|
338
336
|
if (this.store.state.currentIndex !== from) {
|
|
@@ -343,6 +341,7 @@ export class Lumina {
|
|
|
343
341
|
|
|
344
342
|
/** Advances to the next slide (or first if loop and at end). */
|
|
345
343
|
public next() {
|
|
344
|
+
if (!this.store) return;
|
|
346
345
|
const from = this.store.state.currentIndex;
|
|
347
346
|
this.store.next();
|
|
348
347
|
if (this.store.state.currentIndex !== from) {
|
|
@@ -353,6 +352,7 @@ export class Lumina {
|
|
|
353
352
|
|
|
354
353
|
/** Goes to the previous slide (or last if loop and at start). */
|
|
355
354
|
public prev() {
|
|
355
|
+
if (!this.store) return;
|
|
356
356
|
const from = this.store.state.currentIndex;
|
|
357
357
|
this.store.prev();
|
|
358
358
|
if (this.store.state.currentIndex !== from) {
|
|
@@ -424,6 +424,7 @@ export class Lumina {
|
|
|
424
424
|
public element(id: string): ElementController;
|
|
425
425
|
public element(slideIndex: number, path: string | ElementPath): ElementController;
|
|
426
426
|
public element(idOrSlide: string | number, path?: string | ElementPath): ElementController {
|
|
427
|
+
if (!this.store) return createNoopElementController(this, typeof idOrSlide === 'string' ? idOrSlide : '');
|
|
427
428
|
if (typeof idOrSlide === 'number') {
|
|
428
429
|
const slide = this.store.state.deck?.slides[idOrSlide];
|
|
429
430
|
const p = parsePath(path!);
|
|
@@ -448,7 +449,7 @@ export class Lumina {
|
|
|
448
449
|
* @see elements
|
|
449
450
|
*/
|
|
450
451
|
public elementInCurrent(path: string | ElementPath): ElementController {
|
|
451
|
-
const i = this.store.state.currentIndex ?? 0;
|
|
452
|
+
const i = this.store ? this.store.state.currentIndex ?? 0 : 0;
|
|
452
453
|
return this.element(i, path);
|
|
453
454
|
}
|
|
454
455
|
|
|
@@ -473,6 +474,7 @@ export class Lumina {
|
|
|
473
474
|
* @see DeckMeta.initialElementState
|
|
474
475
|
*/
|
|
475
476
|
public elements(slideIndex?: number): string[] {
|
|
477
|
+
if (!this.store) return [];
|
|
476
478
|
const i = slideIndex ?? this.store.state.currentIndex ?? 0;
|
|
477
479
|
const slide = this.store.state.deck?.slides[i];
|
|
478
480
|
if (!slide) return [];
|
|
@@ -577,6 +579,62 @@ export class Lumina {
|
|
|
577
579
|
return { ...a, ...b };
|
|
578
580
|
}
|
|
579
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Options for the default cascade when no meta.reveal or slide.reveal is configured.
|
|
584
|
+
* Uses defaultCascadeDelayMs and defaultCascadeDuration from config; excludes the slide root.
|
|
585
|
+
*/
|
|
586
|
+
private getDefaultCascadeOptions(index: number): RevealInSequenceOptions {
|
|
587
|
+
if (!this.store) return {};
|
|
588
|
+
const deck = this.store.state.deck;
|
|
589
|
+
const slide = deck?.slides?.[index] as BaseSlideData | undefined;
|
|
590
|
+
const cfg = resolveTransitionConfig(this.store, slide ?? undefined);
|
|
591
|
+
const slideRootId = slide ? resolveId(slide, index, ['slide']) : undefined;
|
|
592
|
+
return {
|
|
593
|
+
delayMs: cfg.defaultCascadeDelayMs,
|
|
594
|
+
duration: cfg.defaultCascadeDuration,
|
|
595
|
+
...(slideRootId ? { exclude: [slideRootId] as readonly string[] } : {}),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Runs reveal for a slide if configured (default cascade, meta.reveal, or slide.reveal).
|
|
601
|
+
* Call when the slide has mounted (e.g. from Deck's onEnter) so elements exist in the DOM.
|
|
602
|
+
* Returns the reveal promise or undefined if no reveal runs.
|
|
603
|
+
*/
|
|
604
|
+
public runRevealForSlideIfNeeded(slideIndex?: number): Promise<void> | undefined {
|
|
605
|
+
if (!this.store) return undefined;
|
|
606
|
+
const i = slideIndex ?? this.currentSlideIndex;
|
|
607
|
+
const opts = this.getRevealDecision(i);
|
|
608
|
+
if (opts === null) return undefined;
|
|
609
|
+
return this.revealInSequence(i, opts);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Decides whether and how to run reveal for a slide. Returns options to pass to revealInSequence,
|
|
614
|
+
* or null if no reveal should run.
|
|
615
|
+
*/
|
|
616
|
+
private getRevealDecision(index: number): RevealInSequenceOptions | null {
|
|
617
|
+
const deck = this.store?.state.deck;
|
|
618
|
+
const slide = deck?.slides?.[index];
|
|
619
|
+
if (!slide) return null;
|
|
620
|
+
|
|
621
|
+
const rev = deck?.meta?.reveal ?? (slide as { reveal?: unknown }).reveal;
|
|
622
|
+
|
|
623
|
+
// Explicit opt-out: no reveal
|
|
624
|
+
if (rev === false) return null;
|
|
625
|
+
|
|
626
|
+
// Timeline-driven: no cascade
|
|
627
|
+
if ((slide as { timelineTracks?: unknown }).timelineTracks != null) return null;
|
|
628
|
+
|
|
629
|
+
// Explicit reveal config: use it
|
|
630
|
+
if (rev && typeof rev === 'object') {
|
|
631
|
+
return this.getRevealOptionsFromDeck(deck!, index);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Default: fast cascade
|
|
635
|
+
return this.getDefaultCascadeOptions(index);
|
|
636
|
+
}
|
|
637
|
+
|
|
580
638
|
/**
|
|
581
639
|
* Reveals slide elements one by one with optional stagger and animation. Optionally
|
|
582
640
|
* hides all first (hideFirst: true). Resolves when the sequence has been scheduled
|
|
@@ -591,6 +649,7 @@ export class Lumina {
|
|
|
591
649
|
* await engine.revealInSequence(0, { only: ['s0-tag', 's0-title'], hideFirst: true });
|
|
592
650
|
*/
|
|
593
651
|
public revealInSequence(slideIndex?: number, options?: RevealInSequenceOptions): Promise<void> {
|
|
652
|
+
if (!this.store) return Promise.resolve();
|
|
594
653
|
const i = slideIndex ?? this.currentSlideIndex;
|
|
595
654
|
let ids = this.elements(i);
|
|
596
655
|
const opts = options ?? {};
|
|
@@ -677,6 +736,7 @@ export class Lumina {
|
|
|
677
736
|
* slider.oninput = () => engine.seekTo(parseFloat(slider.value));
|
|
678
737
|
*/
|
|
679
738
|
public seekTo(progress: number, slideIndex?: number): this {
|
|
739
|
+
if (!this.store) return this;
|
|
680
740
|
const i = slideIndex ?? this.currentSlideIndex;
|
|
681
741
|
const slide = this.store.state.deck?.slides[i];
|
|
682
742
|
const timeline = slide?.timelineTracks;
|
|
@@ -703,6 +763,7 @@ export class Lumina {
|
|
|
703
763
|
* // cancel() to stop
|
|
704
764
|
*/
|
|
705
765
|
public playTimeline(durationSeconds = 5, slideIndex?: number): [Promise<void>, () => void] {
|
|
766
|
+
if (!this.store) return [Promise.resolve(), () => {}];
|
|
706
767
|
const i = slideIndex ?? this.currentSlideIndex;
|
|
707
768
|
const slide = this.store.state.deck?.slides[i];
|
|
708
769
|
if (!slide?.timelineTracks || typeof slide.timelineTracks !== 'object') {
|
|
@@ -89,6 +89,12 @@ export const ANIMATION_DEFAULTS = {
|
|
|
89
89
|
revealDuration: 0.45,
|
|
90
90
|
revealEase: 'power2.out',
|
|
91
91
|
|
|
92
|
+
// --- Default cascade (when no meta.reveal or slide.reveal) ---
|
|
93
|
+
/** Delay in ms between elements for implicit default cascade. Default 120. */
|
|
94
|
+
defaultCascadeDelayMs: 120,
|
|
95
|
+
/** Per-element duration (s) for implicit default cascade. Default 0.3. */
|
|
96
|
+
defaultCascadeDuration: 0.3,
|
|
97
|
+
|
|
92
98
|
// --- elementController.animate ---
|
|
93
99
|
elementDuration: 0.5,
|
|
94
100
|
elementEase: 'power2.out',
|
|
@@ -133,6 +139,8 @@ export const ANIMATION_DEFAULTS = {
|
|
|
133
139
|
* @property {number} revealDelayMs - revealInSequence delay between elements (ms). Default 400.
|
|
134
140
|
* @property {number} revealDuration - revealInSequence per-element duration (s). Default 0.45.
|
|
135
141
|
* @property {string} revealEase - revealInSequence ease. Default 'power2.out'.
|
|
142
|
+
* @property {number} defaultCascadeDelayMs - Default cascade delay between elements (ms). Default 120.
|
|
143
|
+
* @property {number} defaultCascadeDuration - Default cascade per-element duration (s). Default 0.3.
|
|
136
144
|
* @property {number} elementDuration - element().animate() default duration (s). Default 0.5.
|
|
137
145
|
* @property {string} elementEase - element().animate() default ease. Default 'power2.out'.
|
|
138
146
|
* @property {string} elementOpacityTransition - CSS value for LuminaElement opacity. Default '0.35s ease-out'.
|
|
@@ -166,6 +174,8 @@ export type ResolvedTransitionConfig = {
|
|
|
166
174
|
revealDelayMs: number;
|
|
167
175
|
revealDuration: number;
|
|
168
176
|
revealEase: string;
|
|
177
|
+
defaultCascadeDelayMs: number;
|
|
178
|
+
defaultCascadeDuration: number;
|
|
169
179
|
elementDuration: number;
|
|
170
180
|
elementEase: string;
|
|
171
181
|
elementOpacityTransition: string;
|
|
@@ -174,3 +174,21 @@ export function createElementController(
|
|
|
174
174
|
|
|
175
175
|
return ctrl;
|
|
176
176
|
}
|
|
177
|
+
|
|
178
|
+
/** No-op controller when engine has no store (e.g. studioEmbed mode). */
|
|
179
|
+
export function createNoopElementController(_engine: LuminaEngineLike, _id: string): IElementController {
|
|
180
|
+
const ctrl: IElementController = {
|
|
181
|
+
show: () => ctrl,
|
|
182
|
+
hide: () => ctrl,
|
|
183
|
+
toggle: () => ctrl,
|
|
184
|
+
opacity: () => ctrl,
|
|
185
|
+
transform: () => ctrl,
|
|
186
|
+
css: () => ctrl,
|
|
187
|
+
addClass: () => ctrl,
|
|
188
|
+
removeClass: () => ctrl,
|
|
189
|
+
animate: () => ctrl,
|
|
190
|
+
animateAsync: () => Promise.resolve(),
|
|
191
|
+
animateToProgress: () => ctrl
|
|
192
|
+
};
|
|
193
|
+
return ctrl;
|
|
194
|
+
}
|
|
@@ -122,7 +122,8 @@ const PATH_GENERATORS: Record<string, PathGenerator> = {
|
|
|
122
122
|
features: (s) => {
|
|
123
123
|
const arr = getValueAt(s, ['features']);
|
|
124
124
|
const list = Array.isArray(arr) ? arr : [];
|
|
125
|
-
|
|
125
|
+
// Omit header (parent of title/description) to avoid animating container + children = jump
|
|
126
|
+
const paths: ElementPath[] = [['title']];
|
|
126
127
|
if (getValueAt(s, ['description'])) paths.push(['description']);
|
|
127
128
|
return [...paths, ...list.map((_: unknown, i: number) => ['features', i] as ElementPath)];
|
|
128
129
|
},
|
|
@@ -142,7 +143,8 @@ const PATH_GENERATORS: Record<string, PathGenerator> = {
|
|
|
142
143
|
steps: (s) => {
|
|
143
144
|
const arr = getValueAt(s, ['steps']);
|
|
144
145
|
const list = Array.isArray(arr) ? arr : [];
|
|
145
|
-
|
|
146
|
+
// Omit header (parent of title/subtitle) to avoid animating container + children = jump
|
|
147
|
+
const paths: ElementPath[] = [['title']];
|
|
146
148
|
if (getValueAt(s, ['subtitle'])) paths.push(['subtitle']);
|
|
147
149
|
return [...paths, ...list.map((_: unknown, i: number) => ['steps', i] as ElementPath)];
|
|
148
150
|
},
|