lumina-slides 9.0.1 → 9.0.3
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 +7604 -7548
- package/dist/lumina-slides.umd.cjs +192 -192
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/animation/registry.ts +13 -10
- package/src/animation/stagger.ts +1 -1
- package/src/animation/types.ts +3 -1
- package/src/components/LuminaDeck.vue +15 -7
- package/src/components/site/SiteDocs.vue +352 -467
- package/src/components/site/SiteExamples.vue +1 -0
- package/src/core/Lumina.ts +73 -26
- package/src/core/animationConfig.ts +1 -1
- package/src/core/elementController.ts +14 -8
- package/src/core/elementResolver.ts +11 -5
- package/src/core/store.ts +1 -1
- package/src/core/types.ts +28 -6
- package/src/index.ts +5 -2
- package/src/utils/templateInterpolation.ts +52 -0
- package/src/views/DeckView.vue +55 -31
|
@@ -36,6 +36,7 @@ const router = useRouter();
|
|
|
36
36
|
const decks = [
|
|
37
37
|
{ id: 'deck', title: 'Feature Showcase', description: 'A comprehensive tour of all major features.', icon: '🚀' },
|
|
38
38
|
{ id: 'layout-element-control', title: 'Element Control', description: 'Reveal-on-demand: elements start hidden and appear automatically.', icon: '🎭' },
|
|
39
|
+
{ id: 'layout-template-tags', title: 'Template Tags', description: '{{key}} placeholders in slide content, resolved from engine.data. Updates reactively.', icon: '🏷️' },
|
|
39
40
|
{ id: 'animation-presets', title: 'Animation: Presets', description: 'Built-in presets: fadeUp, scaleIn, spring. Use with animate() or revealInSequence.', icon: '✨' },
|
|
40
41
|
{ id: 'animation-stagger', title: 'Animation: Stagger', description: 'Stagger modes: center-out, wave, random. Control order and timing.', icon: '〰️' },
|
|
41
42
|
{ id: 'animation-timeline', title: 'Animation: Timeline', description: 'Remotion-style: scrub 0–1, keyframes per element, seekTo and playTimeline.', icon: '⏯️' },
|
package/src/core/Lumina.ts
CHANGED
|
@@ -25,7 +25,7 @@ import type { ElementPath } from './elementResolver';
|
|
|
25
25
|
import { bus } from './events';
|
|
26
26
|
import { ThemeManager, deepMerge } from './theme';
|
|
27
27
|
import { SpeakerChannel } from './speaker-channel';
|
|
28
|
-
import type { Deck, LuminaOptions, LuminaEventMap, LuminaDataStore, SpeakerSyncPayload, BaseSlideData, ElementController, RevealInSequenceOptions,
|
|
28
|
+
import type { Deck, LuminaOptions, LuminaEventMap, LuminaDataStore, SpeakerSyncPayload, BaseSlideData, ElementController, RevealInSequenceOptions, ThemeConfig } from './types';
|
|
29
29
|
import '../style/main.css';
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -114,13 +114,18 @@ export class Lumina {
|
|
|
114
114
|
// Watch for state changes and emit public events
|
|
115
115
|
watch(() => this.store.state.currentIndex, (newIndex, oldIndex) => {
|
|
116
116
|
this.store.resetElementStateForSlide(newIndex);
|
|
117
|
-
const
|
|
117
|
+
const deck = this.store.state.deck;
|
|
118
|
+
const currentSlide = deck?.slides?.[newIndex];
|
|
118
119
|
if (currentSlide) {
|
|
119
120
|
bus.emit('slideChange', {
|
|
120
121
|
index: newIndex,
|
|
121
122
|
previousIndex: oldIndex ?? 0,
|
|
122
123
|
slide: currentSlide
|
|
123
124
|
});
|
|
125
|
+
const rev = deck?.meta?.reveal ?? currentSlide?.reveal;
|
|
126
|
+
if (rev && typeof rev === 'object') {
|
|
127
|
+
this.revealInSequence(newIndex, this.getRevealOptionsFromDeck(deck!, newIndex));
|
|
128
|
+
}
|
|
124
129
|
}
|
|
125
130
|
});
|
|
126
131
|
|
|
@@ -130,8 +135,8 @@ export class Lumina {
|
|
|
130
135
|
// 1. Construct Theme Overrides from Meta
|
|
131
136
|
// - meta.themeConfig: full ThemeConfig object (e.g. from theme-custom-example.json)
|
|
132
137
|
// - meta.colors, meta.typography, etc.: direct overrides from Studio (take precedence)
|
|
133
|
-
const fromThemeConfig =
|
|
134
|
-
const direct = {
|
|
138
|
+
const fromThemeConfig = newMeta.themeConfig || {};
|
|
139
|
+
const direct: Partial<ThemeConfig> = {
|
|
135
140
|
colors: newMeta.colors,
|
|
136
141
|
typography: newMeta.typography,
|
|
137
142
|
spacing: newMeta.spacing,
|
|
@@ -139,7 +144,8 @@ export class Lumina {
|
|
|
139
144
|
effects: newMeta.effects,
|
|
140
145
|
components: newMeta.components
|
|
141
146
|
};
|
|
142
|
-
const
|
|
147
|
+
const base = (fromThemeConfig && typeof fromThemeConfig === 'object' && !Array.isArray(fromThemeConfig) ? fromThemeConfig : {});
|
|
148
|
+
const overrides = deepMerge<ThemeConfig>(base as ThemeConfig, direct);
|
|
143
149
|
|
|
144
150
|
// Resolve theme: deck.theme (top-level, e.g. from theme-ocean.json) takes precedence over
|
|
145
151
|
// meta.theme, then constructor options. Ensures examples and loaded decks keep their theme.
|
|
@@ -208,13 +214,17 @@ export class Lumina {
|
|
|
208
214
|
* engine.load({ meta: { title: "Q4 Review" }, slides: [ { type: "statement", title: "Intro" } ] });
|
|
209
215
|
*/
|
|
210
216
|
public load(deckData: Deck) {
|
|
211
|
-
if (!deckData || !Array.isArray(
|
|
217
|
+
if (!deckData || !Array.isArray(deckData.slides)) {
|
|
212
218
|
bus.emit('error', new Error('Invalid deck: expected { meta?, slides: [] }'));
|
|
213
219
|
return;
|
|
214
220
|
}
|
|
215
221
|
try {
|
|
216
222
|
this.store.loadDeck(deckData);
|
|
217
223
|
bus.emit('ready', deckData);
|
|
224
|
+
const rev = deckData.meta?.reveal ?? deckData.slides?.[0]?.reveal;
|
|
225
|
+
if (rev && typeof rev === 'object') {
|
|
226
|
+
this.revealInSequence(0, this.getRevealOptionsFromDeck(deckData, 0));
|
|
227
|
+
}
|
|
218
228
|
this.syncSpeakerNotes();
|
|
219
229
|
} catch (e) {
|
|
220
230
|
bus.emit('error', e instanceof Error ? e : new Error(String(e)));
|
|
@@ -230,7 +240,7 @@ export class Lumina {
|
|
|
230
240
|
* @example
|
|
231
241
|
* engine.patch({ meta: { title: "Updated Title" } });
|
|
232
242
|
*/
|
|
233
|
-
public patch(diff:
|
|
243
|
+
public patch(diff: Partial<Deck>) {
|
|
234
244
|
this.store.patchDeck(diff);
|
|
235
245
|
bus.emit('patch', { diff: diff ?? {} });
|
|
236
246
|
this.syncSpeakerNotes();
|
|
@@ -257,7 +267,7 @@ export class Lumina {
|
|
|
257
267
|
index: currentIndex,
|
|
258
268
|
id: slide?.id || currentIndex,
|
|
259
269
|
type: slide?.type,
|
|
260
|
-
title: (slide
|
|
270
|
+
title: (typeof slide?.title === 'string' ? slide.title : null) || "(No Title)"
|
|
261
271
|
},
|
|
262
272
|
narrative: `User is currently on slide ${currentIndex + 1}. Session Flow: ${context || "Just started"}.`,
|
|
263
273
|
engagementLevel: interest,
|
|
@@ -368,7 +378,8 @@ export class Lumina {
|
|
|
368
378
|
if (!container) return null;
|
|
369
379
|
const root = container.querySelector('[data-lumina-root]') || container;
|
|
370
380
|
const escaped = id.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
371
|
-
|
|
381
|
+
const el = root.querySelector(`[data-lumina-id="${escaped}"]`);
|
|
382
|
+
return el instanceof HTMLElement ? el : null;
|
|
372
383
|
}
|
|
373
384
|
|
|
374
385
|
/**
|
|
@@ -422,28 +433,50 @@ export class Lumina {
|
|
|
422
433
|
return createElementController(this.store, this, idOrSlide);
|
|
423
434
|
}
|
|
424
435
|
|
|
436
|
+
/**
|
|
437
|
+
* Returns the element at the given path on the current slide. Shorthand for
|
|
438
|
+
* engine.element(engine.currentSlideIndex, path). Use when you don't need the slide index.
|
|
439
|
+
*
|
|
440
|
+
* @param path - Logical path (e.g. "title", "features.0", ["elements", 0, "elements", 1]).
|
|
441
|
+
* @returns ElementController for that element.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* engine.elementInCurrent("title").show();
|
|
445
|
+
* engine.elementInCurrent("features.0").animate({ from: { opacity: 0 }, to: { opacity: 1 }, duration: 0.5 });
|
|
446
|
+
*
|
|
447
|
+
* @see element
|
|
448
|
+
* @see elements
|
|
449
|
+
*/
|
|
450
|
+
public elementInCurrent(path: string | ElementPath): ElementController {
|
|
451
|
+
const i = this.store.state.currentIndex ?? 0;
|
|
452
|
+
return this.element(i, path);
|
|
453
|
+
}
|
|
454
|
+
|
|
425
455
|
/**
|
|
426
456
|
* Returns all element ids for a slide. Use to discover which ids exist (e.g. for
|
|
427
457
|
* meta.initialElementState, or to iterate and control elements). Each id can be passed to
|
|
428
458
|
* engine.element(id). Uses the same path→id logic as element(slideIndex, path); ids follow
|
|
429
|
-
* resolveId (explicit id, slide.ids, or elemId(slideIndex, ...path)).
|
|
459
|
+
* resolveId (explicit id, slide.ids, or slide.id / elemId(slideIndex, ...path) as fallback).
|
|
430
460
|
*
|
|
431
|
-
* @param slideIndex - Zero-based slide index. If out of range or deck not loaded, returns [].
|
|
432
|
-
* @returns Array of id strings (e.g. ["s0-slide","s0-tag","s0-title","s0-subtitle"]
|
|
461
|
+
* @param slideIndex - Zero-based slide index. Omit to use the current slide. If out of range or deck not loaded, returns [].
|
|
462
|
+
* @returns Array of id strings (e.g. ["s0-slide","s0-tag","s0-title","s0-subtitle"] or ["intro-slide","intro-tag",...] when slide.id is set).
|
|
433
463
|
*
|
|
434
464
|
* @example
|
|
435
|
-
*
|
|
436
|
-
*
|
|
465
|
+
* engine.elements(); // current slide
|
|
466
|
+
* engine.elements(0);
|
|
467
|
+
* engine.elements().forEach(id => { if (id.endsWith('-title')) engine.element(id).hide(); });
|
|
437
468
|
*
|
|
438
469
|
* @see element
|
|
470
|
+
* @see elementInCurrent
|
|
439
471
|
* @see getElementIds
|
|
440
472
|
* @see resolveId
|
|
441
473
|
* @see DeckMeta.initialElementState
|
|
442
474
|
*/
|
|
443
|
-
public elements(slideIndex
|
|
444
|
-
const
|
|
475
|
+
public elements(slideIndex?: number): string[] {
|
|
476
|
+
const i = slideIndex ?? this.store.state.currentIndex ?? 0;
|
|
477
|
+
const slide = this.store.state.deck?.slides[i];
|
|
445
478
|
if (!slide) return [];
|
|
446
|
-
return getElementIds(slide,
|
|
479
|
+
return getElementIds(slide, i);
|
|
447
480
|
}
|
|
448
481
|
|
|
449
482
|
/**
|
|
@@ -529,13 +562,28 @@ export class Lumina {
|
|
|
529
562
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
530
563
|
}
|
|
531
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Merges meta.reveal and slide.reveal into options for revealInSequence. slide overrides meta.
|
|
567
|
+
* Used when auto-running reveal from declarative slide.reveal or meta.reveal in the deck JSON.
|
|
568
|
+
*/
|
|
569
|
+
private getRevealOptionsFromDeck(
|
|
570
|
+
deck: { meta?: { reveal?: RevealInSequenceOptions }; slides?: ReadonlyArray<{ reveal?: RevealInSequenceOptions }> },
|
|
571
|
+
index: number
|
|
572
|
+
): RevealInSequenceOptions {
|
|
573
|
+
const meta = deck.meta;
|
|
574
|
+
const slide = deck.slides?.[index];
|
|
575
|
+
const a = (meta?.reveal && typeof meta.reveal === 'object') ? meta.reveal : {};
|
|
576
|
+
const b = (slide?.reveal && typeof slide.reveal === 'object') ? slide.reveal : {};
|
|
577
|
+
return { ...a, ...b };
|
|
578
|
+
}
|
|
579
|
+
|
|
532
580
|
/**
|
|
533
581
|
* Reveals slide elements one by one with optional stagger and animation. Optionally
|
|
534
582
|
* hides all first (hideFirst: true). Resolves when the sequence has been scheduled
|
|
535
583
|
* (does not wait for the last animation to finish).
|
|
536
584
|
*
|
|
537
585
|
* @param slideIndex - Zero-based slide index. Omit to use the current slide.
|
|
538
|
-
* @param options - Stagger delay, animation from/to, and filters (only, exclude).
|
|
586
|
+
* @param options - Stagger delay, animation from/to, and filters (only, exclude). Or from slide.reveal / meta.reveal when auto-run.
|
|
539
587
|
* @returns Promise that resolves after the last element's scheduled reveal time.
|
|
540
588
|
*
|
|
541
589
|
* @example
|
|
@@ -554,6 +602,7 @@ export class Lumina {
|
|
|
554
602
|
const delayMs = opts.delayMs ?? cfg.revealDelayMs;
|
|
555
603
|
const animate = opts.animate !== false;
|
|
556
604
|
|
|
605
|
+
const motions = this.store.state.deck?.meta?.motions;
|
|
557
606
|
const resolved = resolveAnimationFromInput(
|
|
558
607
|
{
|
|
559
608
|
preset: opts.preset,
|
|
@@ -563,7 +612,8 @@ export class Lumina {
|
|
|
563
612
|
ease: opts.ease,
|
|
564
613
|
duration: opts.duration,
|
|
565
614
|
},
|
|
566
|
-
{ from: { opacity: 0, y: 16 }, to: { opacity: 1, y: 0 }, ease: cfg.revealEase, duration: cfg.revealDuration }
|
|
615
|
+
{ from: { opacity: 0, y: 16 }, to: { opacity: 1, y: 0 }, ease: cfg.revealEase, duration: cfg.revealDuration },
|
|
616
|
+
motions
|
|
567
617
|
);
|
|
568
618
|
|
|
569
619
|
const steps = computeStagger({
|
|
@@ -628,7 +678,7 @@ export class Lumina {
|
|
|
628
678
|
*/
|
|
629
679
|
public seekTo(progress: number, slideIndex?: number): this {
|
|
630
680
|
const i = slideIndex ?? this.currentSlideIndex;
|
|
631
|
-
const slide = this.store.state.deck?.slides[i]
|
|
681
|
+
const slide = this.store.state.deck?.slides[i];
|
|
632
682
|
const timeline = slide?.timelineTracks;
|
|
633
683
|
this._timelineProgress = Math.max(0, Math.min(1, progress));
|
|
634
684
|
bus.emit('timelineSeek', { progress: this._timelineProgress, slideIndex: i });
|
|
@@ -654,7 +704,7 @@ export class Lumina {
|
|
|
654
704
|
*/
|
|
655
705
|
public playTimeline(durationSeconds = 5, slideIndex?: number): [Promise<void>, () => void] {
|
|
656
706
|
const i = slideIndex ?? this.currentSlideIndex;
|
|
657
|
-
const slide = this.store.state.deck?.slides[i]
|
|
707
|
+
const slide = this.store.state.deck?.slides[i];
|
|
658
708
|
if (!slide?.timelineTracks || typeof slide.timelineTracks !== 'object') {
|
|
659
709
|
return [Promise.resolve(), () => {}];
|
|
660
710
|
}
|
|
@@ -799,11 +849,8 @@ export class Lumina {
|
|
|
799
849
|
const currentSlide = deck.slides[currentIndex];
|
|
800
850
|
const nextSlide = deck.slides[currentIndex + 1];
|
|
801
851
|
|
|
802
|
-
const getSlideTitle = (slide: BaseSlideData | undefined): string | undefined =>
|
|
803
|
-
|
|
804
|
-
if ('title' in slide && typeof (slide as any).title === 'string') return (slide as any).title;
|
|
805
|
-
return undefined;
|
|
806
|
-
};
|
|
852
|
+
const getSlideTitle = (slide: BaseSlideData | undefined): string | undefined =>
|
|
853
|
+
(slide && typeof slide.title === 'string' ? slide.title : undefined);
|
|
807
854
|
|
|
808
855
|
this.speakerChannel.send({
|
|
809
856
|
action: 'state',
|
|
@@ -232,7 +232,7 @@ function pickCanonical(src: Record<string, unknown> | null | undefined): Partial
|
|
|
232
232
|
* @see ResolvedTransitionConfig
|
|
233
233
|
* @see LuminaAnimationOptions
|
|
234
234
|
*/
|
|
235
|
-
export function resolveTransitionConfig(store: LuminaStore | null | undefined, slideData?: BaseSlideData | null): ResolvedTransitionConfig {
|
|
235
|
+
export function resolveTransitionConfig(store: LuminaStore | null | undefined, slideData?: Readonly<BaseSlideData> | null): ResolvedTransitionConfig {
|
|
236
236
|
const base = { ...ANIMATION_DEFAULTS } as unknown as Record<string, unknown>;
|
|
237
237
|
|
|
238
238
|
const opt = (store?.state?.options?.animation || {}) as Record<string, unknown>;
|
|
@@ -40,7 +40,7 @@ export function createElementController(
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function getClass(): string {
|
|
43
|
-
return
|
|
43
|
+
return store.state.elementState[id]?.class ?? '';
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const ctrl: IElementController = {
|
|
@@ -121,12 +121,17 @@ export function createElementController(
|
|
|
121
121
|
const vb = keyframes[String(b)] ?? {};
|
|
122
122
|
const t = (progress - a) / (b - a);
|
|
123
123
|
state = {};
|
|
124
|
-
|
|
124
|
+
const numericKeys = ['opacity', 'x', 'y', 'scale', 'rotate'] as const;
|
|
125
|
+
for (const prop of numericKeys) {
|
|
125
126
|
const pa = va[prop];
|
|
126
127
|
const pb = vb[prop];
|
|
127
|
-
if (typeof pa === 'number' && typeof pb === 'number')
|
|
128
|
-
|
|
129
|
-
else if (
|
|
128
|
+
if (typeof pa === 'number' && typeof pb === 'number') {
|
|
129
|
+
state[prop] = pa + (pb - pa) * t;
|
|
130
|
+
} else if (typeof pb === 'number') {
|
|
131
|
+
state[prop] = pb;
|
|
132
|
+
} else if (typeof pa === 'number') {
|
|
133
|
+
state[prop] = pa;
|
|
134
|
+
}
|
|
130
135
|
}
|
|
131
136
|
if (typeof vb.visible === 'boolean') state.visible = vb.visible;
|
|
132
137
|
else if (typeof va.visible === 'boolean') state.visible = va.visible;
|
|
@@ -153,14 +158,15 @@ export function createElementController(
|
|
|
153
158
|
const el = engine.getElementById(id);
|
|
154
159
|
if (!el) return;
|
|
155
160
|
const cfg = resolveTransitionConfig(store, undefined);
|
|
161
|
+
const motions = store.state.deck?.meta?.motions;
|
|
156
162
|
const { from, to, ease, duration } = resolveAnimationFromInput(opts, {
|
|
157
163
|
duration: cfg.elementDuration,
|
|
158
164
|
ease: cfg.elementEase,
|
|
159
|
-
});
|
|
165
|
+
}, motions);
|
|
160
166
|
const cb = onCompleteOverride ?? opts.onComplete;
|
|
161
|
-
const toVars = { ...to, duration, ease, overwrite: true, onComplete: cb }
|
|
167
|
+
const toVars: gsap.TweenVars = { ...to, duration, ease, overwrite: true, onComplete: cb };
|
|
162
168
|
if (from && Object.keys(from).length) {
|
|
163
|
-
gsap.fromTo(el, from
|
|
169
|
+
gsap.fromTo(el, from, toVars);
|
|
164
170
|
} else {
|
|
165
171
|
gsap.to(el, toVars);
|
|
166
172
|
}
|
|
@@ -40,7 +40,7 @@ export function pathToKey(path: ElementPath): string {
|
|
|
40
40
|
* @param path - Path segments (e.g. ['features', 0] or ['elements', 0, 'elements', 1]).
|
|
41
41
|
* @returns The value at that path, or undefined if missing.
|
|
42
42
|
*/
|
|
43
|
-
export function getValueAt(slide: BaseSlideData
|
|
43
|
+
export function getValueAt(slide: Readonly<BaseSlideData>, path: ElementPath): unknown {
|
|
44
44
|
if (!slide || !path.length) return undefined;
|
|
45
45
|
return getByPath(slide, pathToKey(path));
|
|
46
46
|
}
|
|
@@ -71,7 +71,8 @@ export function parsePath(input: string | ElementPath): ElementPath {
|
|
|
71
71
|
* 1. Path [] or ['slide'] → slide.id or elemId(slideIndex, 'slide').
|
|
72
72
|
* 2. If the value at path is an object with string `id` → use it (e.g. feature.id, node.id).
|
|
73
73
|
* 3. If slide.ids[pathToKey(path)] exists → use it (supports compound keys: 'tag', 'features.0', 'elements.0.elements.1').
|
|
74
|
-
* 4. Otherwise →
|
|
74
|
+
* 4. Otherwise → if slide.id is set, "{slide.id}-{path}"; else elemId(slideIndex, ...path)
|
|
75
|
+
* (e.g. "intro-tag" with slide.id "intro", or "s0-tag" when no slide.id).
|
|
75
76
|
*
|
|
76
77
|
* @param slide - The slide data.
|
|
77
78
|
* @param slideIndex - Zero-based slide index (for elemId fallback).
|
|
@@ -83,7 +84,7 @@ export function parsePath(input: string | ElementPath): ElementPath {
|
|
|
83
84
|
* @see elemId
|
|
84
85
|
*/
|
|
85
86
|
export function resolveId(
|
|
86
|
-
slide: BaseSlideData
|
|
87
|
+
slide: Readonly<BaseSlideData>,
|
|
87
88
|
slideIndex: number,
|
|
88
89
|
path: ElementPath
|
|
89
90
|
): string {
|
|
@@ -99,11 +100,16 @@ export function resolveId(
|
|
|
99
100
|
if (key && slideAny?.ids?.[key]) {
|
|
100
101
|
return slideAny.ids[key];
|
|
101
102
|
}
|
|
103
|
+
// Fallback: use slide.id as namespace when present, so IDs stay stable when slides are
|
|
104
|
+
// inserted, removed or reordered. Otherwise s{N}-{path} (same as before).
|
|
105
|
+
if (slideAny?.id != null && slideAny.id !== '') {
|
|
106
|
+
return `${slideAny.id}-${path.map(String).join('-')}`;
|
|
107
|
+
}
|
|
102
108
|
return elemId(slideIndex, ...path);
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/** Function that returns all logical element paths for a given slide. One per layout type. */
|
|
106
|
-
export type PathGenerator = (slide: BaseSlideData) => ElementPath[];
|
|
112
|
+
export type PathGenerator = (slide: Readonly<BaseSlideData>) => ElementPath[];
|
|
107
113
|
|
|
108
114
|
/** Map of slide.type → path generator. Extend to support new layouts. */
|
|
109
115
|
const PATH_GENERATORS: Record<string, PathGenerator> = {
|
|
@@ -202,6 +208,6 @@ export function getElementPaths(slide: BaseSlideData): ElementPath[] {
|
|
|
202
208
|
* @see Lumina.elements
|
|
203
209
|
* @see DeckMeta.initialElementState
|
|
204
210
|
*/
|
|
205
|
-
export function getElementIds(slide: BaseSlideData
|
|
211
|
+
export function getElementIds(slide: Readonly<BaseSlideData>, slideIndex: number): string[] {
|
|
206
212
|
return getElementPaths(slide).map((p) => resolveId(slide, slideIndex, p));
|
|
207
213
|
}
|
package/src/core/store.ts
CHANGED
|
@@ -267,7 +267,7 @@ export function createStore(initialOptions: LuminaOptions = {}) {
|
|
|
267
267
|
* Feature: Diff Updates
|
|
268
268
|
* Patches the current deck with partial data.
|
|
269
269
|
*/
|
|
270
|
-
function patchDeck(partial:
|
|
270
|
+
function patchDeck(partial: Partial<Deck>) {
|
|
271
271
|
if (!state.deck) return;
|
|
272
272
|
const merged = deepMerge(state.deck, partial);
|
|
273
273
|
state.deck = ensureDragKeys(merged);
|
package/src/core/types.ts
CHANGED
|
@@ -362,6 +362,17 @@ export interface AnimateOptions {
|
|
|
362
362
|
onComplete?: () => void;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Motion definition for meta.motions. Defines from/to/ease (and optional duration) for
|
|
367
|
+
* use when reveal.motion or animate({ motion: id }) references it. Overrides built-in presets when same id.
|
|
368
|
+
*/
|
|
369
|
+
export interface MotionDef {
|
|
370
|
+
from?: Record<string, string | number>;
|
|
371
|
+
to?: Record<string, string | number>;
|
|
372
|
+
ease?: string;
|
|
373
|
+
duration?: number;
|
|
374
|
+
}
|
|
375
|
+
|
|
365
376
|
/**
|
|
366
377
|
* Options for `Lumina.revealInSequence`. Controls staggered reveal of slide elements.
|
|
367
378
|
*
|
|
@@ -401,9 +412,9 @@ export interface RevealInSequenceOptions {
|
|
|
401
412
|
/** If true, hide all elements before starting the sequence. Default: true */
|
|
402
413
|
hideFirst?: boolean;
|
|
403
414
|
/** Only reveal these element ids (subset). Order follows slide elements. */
|
|
404
|
-
only?: string[];
|
|
415
|
+
only?: readonly string[];
|
|
405
416
|
/** Exclude these element ids from the sequence. */
|
|
406
|
-
exclude?: string[];
|
|
417
|
+
exclude?: readonly string[];
|
|
407
418
|
}
|
|
408
419
|
|
|
409
420
|
/**
|
|
@@ -437,8 +448,8 @@ export type TimelineKeyframes = Record<string, TimelineKeyframeState>;
|
|
|
437
448
|
export type TimelineTracks = Record<string, TimelineKeyframes>;
|
|
438
449
|
|
|
439
450
|
/**
|
|
440
|
-
* Fluent API to control one slide element in real time. Returned by `engine.element(id)
|
|
441
|
-
*
|
|
451
|
+
* Fluent API to control one slide element in real time. Returned by `engine.element(id)`,
|
|
452
|
+
* `engine.element(slideIndex, path)`, and `engine.elementInCurrent(path)`. All methods return `this` for chaining.
|
|
442
453
|
*
|
|
443
454
|
* Use for: starting with an empty slide and revealing items in order, hiding highlights,
|
|
444
455
|
* animating entrances on demand, or syncing with voice/agent.
|
|
@@ -451,6 +462,7 @@ export type TimelineTracks = Record<string, TimelineKeyframes>;
|
|
|
451
462
|
* engine.element(0, 'subtitle').show();
|
|
452
463
|
*
|
|
453
464
|
* @see Lumina.element
|
|
465
|
+
* @see Lumina.elementInCurrent
|
|
454
466
|
* @see Lumina.elements
|
|
455
467
|
* @see ElementState
|
|
456
468
|
* @see AnimateOptions
|
|
@@ -515,6 +527,10 @@ interface SlideBase {
|
|
|
515
527
|
backgroundOpacity?: number;
|
|
516
528
|
/** Optional custom CSS class for the slide content container. */
|
|
517
529
|
class?: string;
|
|
530
|
+
/** Optional title used by exportState and speaker notes when the slide type has no required title. */
|
|
531
|
+
title?: string;
|
|
532
|
+
/** Declarative reveal options. When present, engine runs revealInSequence on ready (slide 0) and on slideChange. */
|
|
533
|
+
reveal?: RevealInSequenceOptions;
|
|
518
534
|
}
|
|
519
535
|
|
|
520
536
|
/**
|
|
@@ -852,6 +868,12 @@ export interface DeckMeta {
|
|
|
852
868
|
* defaultVisible: false = start all elements hidden; override per-id via initialElementState.
|
|
853
869
|
*/
|
|
854
870
|
elementControl?: { defaultVisible?: boolean };
|
|
871
|
+
/** Default reveal options for all slides. Overridden by slide.reveal. */
|
|
872
|
+
reveal?: RevealInSequenceOptions;
|
|
873
|
+
/** Motion definitions (id -> { from, to, ease?, duration? }). Used when reveal.motion or animate({ motion: id }). */
|
|
874
|
+
motions?: Record<string, MotionDef>;
|
|
875
|
+
/** Full or partial theme overrides (e.g. from theme-custom-example.json). */
|
|
876
|
+
themeConfig?: ThemeConfig | object;
|
|
855
877
|
[key: string]: any;
|
|
856
878
|
}
|
|
857
879
|
|
|
@@ -1408,7 +1430,7 @@ export interface LuminaOptions {
|
|
|
1408
1430
|
selector?: string;
|
|
1409
1431
|
/** Whether to loop back to the start after the last slide. */
|
|
1410
1432
|
loop?: boolean;
|
|
1411
|
-
/** Master switch for slide-by-slide navigation. When false, the on-screen prev/next buttons are
|
|
1433
|
+
/** Master switch for slide-by-slide navigation. When false, the on-screen prev/next arrow buttons are hidden and keyboard/touch do nothing. When true, keyboard and touch can be toggled via `keyboard` and `touch`; the nav buttons are shown/hidden with `ui.showControls`. Default: true. */
|
|
1412
1434
|
navigation?: boolean;
|
|
1413
1435
|
/** Enable or disable keyboard shortcuts for next/prev. Only has effect when `navigation` is true. Default: true. */
|
|
1414
1436
|
keyboard?: boolean;
|
|
@@ -1467,7 +1489,7 @@ export type LuminaEventType =
|
|
|
1467
1489
|
export interface SlideChangePayload {
|
|
1468
1490
|
index: number;
|
|
1469
1491
|
previousIndex: number;
|
|
1470
|
-
slide: BaseSlideData
|
|
1492
|
+
slide: Readonly<BaseSlideData>;
|
|
1471
1493
|
}
|
|
1472
1494
|
|
|
1473
1495
|
/**
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* **Deck:** `{ meta: { title, initialElementState?, elementControl?, effects? }, slides: [...] }`
|
|
11
11
|
* Slide types: statement, features, half, timeline, steps, flex, chart, diagram, custom, video.
|
|
12
12
|
*
|
|
13
|
-
* **Element control:** `engine.element(id)
|
|
13
|
+
* **Element control:** `engine.element(id)`, `engine.element(slideIndex, path)`, or `engine.elementInCurrent(path)` → ElementController:
|
|
14
14
|
* `.show()`, `.hide()`, `.toggle()`, `.opacity(n)`, `.transform(s)`, `.animate({ preset?, from?, to?, duration?, ease? })`.
|
|
15
15
|
* Presets: fadeUp, fadeIn, scaleIn, slideLeft, slideRight, zoomIn, blurIn, spring, drop, fadeOut. `to` optional when using preset.
|
|
16
16
|
* Ids: `engine.elements(slideIndex)` or `s{N}-{path}` (e.g. s0-tag, s1-features-0). `meta.initialElementState`:
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*
|
|
31
31
|
* **Events:** `engine.on("ready"|"slideChange"|"action"|"error"|"patch"|"destroy"|"navigate"|"themeChange"|"speakerNotesOpen"|"speakerNotesClose"|"timelineSeek"|"timelineComplete"|"revealStart"|"revealComplete"|"revealElement", handler)`. `engine.emitError(err)` to forward errors.
|
|
32
32
|
* **State for LLM:** `engine.exportState()` → `{ currentSlide, narrative, history }`.
|
|
33
|
-
* **Key-value store:** `engine.data.get(key)`, `engine.data.set(key, value)`, `engine.data.has(key)`, `engine.data.delete(key)`, `engine.data.clear()`, `engine.data.keys()`. Persists across deck loads.
|
|
33
|
+
* **Key-value store:** `engine.data.get(key)`, `engine.data.set(key, value)`, `engine.data.has(key)`, `engine.data.delete(key)`, `engine.data.clear()`, `engine.data.keys()`. Persists across deck loads. **Template tags:** any slide string supports `{{key}}` resolved from engine.data; updates reactively when data changes. Helpers: `interpolateString`, `interpolateObject`.
|
|
34
34
|
*
|
|
35
35
|
* **Streaming:** `parsePartialJson(buffer)` then `engine.load(json)`. Or `createDebouncedLoader(engine.load, ms)`.
|
|
36
36
|
* **Prompts:** `generateSystemPrompt({ mode, includeSchema, includeTheming })`, `generateThemePrompt()`.
|
|
@@ -146,6 +146,9 @@ export {
|
|
|
146
146
|
type PresetDef,
|
|
147
147
|
} from './animation';
|
|
148
148
|
|
|
149
|
+
// Template interpolation: {{tag}} in slide strings resolved from engine.data
|
|
150
|
+
export { interpolateString, interpolateObject } from './utils/templateInterpolation';
|
|
151
|
+
|
|
149
152
|
// Element control: resolution and discovery (for engine.element(id), engine.element(slideIndex, path), engine.elements(slideIndex))
|
|
150
153
|
export { elemId } from './core/elementId';
|
|
151
154
|
export {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template interpolation for Lumina slide content.
|
|
3
|
+
*
|
|
4
|
+
* Replaces `{{key}}` placeholders in strings with values from a key-value store
|
|
5
|
+
* (e.g. engine.data / store.userData). When the store changes, any component
|
|
6
|
+
* that uses the interpolated result will reactively update.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* interpolateString('Hello {{name}}', { name: 'World' }) // 'Hello World'
|
|
10
|
+
* interpolateObject({ title: '{{product}}', subtitle: 'v{{version}}' }, { product: 'Lumina', version: 1 })
|
|
11
|
+
* // { title: 'Lumina', subtitle: 'v1' }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Matches {{key}} or {{ key }}. Key: alphanumeric, underscore. */
|
|
15
|
+
const TAG_RE = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Replaces all {{key}} placeholders in a string with values from `data`.
|
|
19
|
+
* Missing keys render as empty string. Non-string values are coerced with String().
|
|
20
|
+
*
|
|
21
|
+
* @param str - Raw string possibly containing `{{key}}` tags.
|
|
22
|
+
* @param data - Key-value map (e.g. engine.data / store.state.userData).
|
|
23
|
+
* @returns Interpolated string.
|
|
24
|
+
*/
|
|
25
|
+
export function interpolateString(str: string, data: Record<string, unknown>): string {
|
|
26
|
+
if (typeof str !== 'string') return str;
|
|
27
|
+
return str.replace(TAG_RE, (_, key: string) => {
|
|
28
|
+
const v = data[key];
|
|
29
|
+
return v == null ? '' : String(v);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Recursively interpolates all string values in an object/array with `{{key}}`
|
|
35
|
+
* placeholders. Other types (number, boolean, null, etc.) are returned unchanged.
|
|
36
|
+
* Does not mutate the input; returns a new structure.
|
|
37
|
+
*
|
|
38
|
+
* @param obj - Object, array, or primitive (strings are interpolated).
|
|
39
|
+
* @param data - Key-value map (e.g. engine.data / store.state.userData).
|
|
40
|
+
* @returns New structure with all strings interpolated.
|
|
41
|
+
*/
|
|
42
|
+
export function interpolateObject<T>(obj: T, data: Record<string, unknown>): T {
|
|
43
|
+
if (obj == null) return obj;
|
|
44
|
+
if (typeof obj === 'string') return interpolateString(obj, data) as T;
|
|
45
|
+
if (Array.isArray(obj)) return obj.map((item) => interpolateObject(item, data)) as T;
|
|
46
|
+
if (typeof obj === 'object' && obj.constructor === Object) {
|
|
47
|
+
return Object.fromEntries(
|
|
48
|
+
Object.entries(obj).map(([k, v]) => [k, interpolateObject(v, data)])
|
|
49
|
+
) as T;
|
|
50
|
+
}
|
|
51
|
+
return obj;
|
|
52
|
+
}
|