lumina-slides 8.9.4 → 9.0.0

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 (119) hide show
  1. package/LUMINA_LLM_EXAMPLES.json +234 -0
  2. package/README.md +18 -18
  3. package/dist/lumina-slides.js +13207 -12659
  4. package/dist/lumina-slides.umd.cjs +215 -215
  5. package/dist/style.css +1 -1
  6. package/package.json +5 -4
  7. package/src/App.vue +16 -0
  8. package/src/animation/index.ts +11 -0
  9. package/src/animation/registry.ts +126 -0
  10. package/src/animation/stagger.ts +95 -0
  11. package/src/animation/types.ts +53 -0
  12. package/src/components/LandingPage.vue +229 -0
  13. package/src/components/LuminaDeck.vue +224 -0
  14. package/src/components/LuminaSpeakerNotes.vue +701 -0
  15. package/src/components/base/BaseSlide.vue +122 -0
  16. package/src/components/base/LuminaElement.vue +67 -0
  17. package/src/components/base/VideoPlayer.vue +204 -0
  18. package/src/components/layouts/LayoutAuto.vue +71 -0
  19. package/src/components/layouts/LayoutChart.vue +287 -0
  20. package/src/components/layouts/LayoutCustom.vue +92 -0
  21. package/src/components/layouts/LayoutDiagram.vue +253 -0
  22. package/src/components/layouts/LayoutFeatures.vue +121 -0
  23. package/src/components/layouts/LayoutFlex.vue +172 -0
  24. package/src/components/layouts/LayoutFree.vue +62 -0
  25. package/src/components/layouts/LayoutHalf.vue +127 -0
  26. package/src/components/layouts/LayoutStatement.vue +74 -0
  27. package/src/components/layouts/LayoutSteps.vue +106 -0
  28. package/src/components/layouts/LayoutTimeline.vue +104 -0
  29. package/src/components/layouts/LayoutVideo.vue +41 -0
  30. package/src/components/parts/FlexBullets.vue +45 -0
  31. package/src/components/parts/FlexButton.vue +132 -0
  32. package/src/components/parts/FlexImage.vue +54 -0
  33. package/src/components/parts/FlexOrdered.vue +44 -0
  34. package/src/components/parts/FlexSpacer.vue +13 -0
  35. package/src/components/parts/FlexStepper.vue +59 -0
  36. package/src/components/parts/FlexText.vue +29 -0
  37. package/src/components/parts/FlexTimeline.vue +67 -0
  38. package/src/components/parts/FlexTitle.vue +39 -0
  39. package/src/components/parts/LuminaBackground.vue +100 -0
  40. package/src/components/site/LivePreview.vue +101 -0
  41. package/src/components/site/SiteApi.vue +301 -0
  42. package/src/components/site/SiteDashboard.vue +604 -0
  43. package/src/components/site/SiteDocs.vue +3267 -0
  44. package/src/components/site/SiteExamples.vue +65 -0
  45. package/src/components/site/SiteFooter.vue +6 -0
  46. package/src/components/site/SiteHome.vue +362 -0
  47. package/src/components/site/SiteNavBar.vue +122 -0
  48. package/src/components/site/SitePlayground.vue +389 -0
  49. package/src/components/site/SitePromptBuilder.vue +266 -0
  50. package/src/components/site/SiteUserMenu.vue +90 -0
  51. package/src/components/studio/ActionEditor.vue +108 -0
  52. package/src/components/studio/ArrayEditor.vue +124 -0
  53. package/src/components/studio/CollapsibleSection.vue +33 -0
  54. package/src/components/studio/ColorField.vue +22 -0
  55. package/src/components/studio/EditorCanvas.vue +326 -0
  56. package/src/components/studio/EditorLayoutFeatures.vue +18 -0
  57. package/src/components/studio/EditorLayoutFixed.vue +46 -0
  58. package/src/components/studio/EditorLayoutFlex.vue +133 -0
  59. package/src/components/studio/EditorLayoutHalf.vue +18 -0
  60. package/src/components/studio/EditorLayoutStatement.vue +18 -0
  61. package/src/components/studio/EditorLayoutSteps.vue +18 -0
  62. package/src/components/studio/EditorLayoutTimeline.vue +18 -0
  63. package/src/components/studio/EditorNode.vue +89 -0
  64. package/src/components/studio/FieldEditor.vue +133 -0
  65. package/src/components/studio/IconPicker.vue +109 -0
  66. package/src/components/studio/LayerItem.vue +117 -0
  67. package/src/components/studio/LuminaStudio.vue +30 -0
  68. package/src/components/studio/SaveSuccessModal.vue +138 -0
  69. package/src/components/studio/SlideNavigator.vue +373 -0
  70. package/src/components/studio/SliderField.vue +44 -0
  71. package/src/components/studio/StudioInspector.vue +595 -0
  72. package/src/components/studio/StudioJsonEditor.vue +191 -0
  73. package/src/components/studio/StudioLayers.vue +145 -0
  74. package/src/components/studio/StudioSettings.vue +514 -0
  75. package/src/components/studio/StudioSidebar.vue +29 -0
  76. package/src/components/studio/StudioToolbar.vue +222 -0
  77. package/src/components/studio/fieldLabels.ts +224 -0
  78. package/src/components/studio/inspectors/DiagramEdgeEditor.vue +77 -0
  79. package/src/components/studio/inspectors/DiagramNodeEditor.vue +117 -0
  80. package/src/components/studio/nodes/StudioDiagramNode.vue +138 -0
  81. package/src/composables/useAuth.ts +87 -0
  82. package/src/composables/useEditor.ts +224 -0
  83. package/src/composables/useElementState.ts +81 -0
  84. package/src/composables/useFlexLayout.ts +122 -0
  85. package/src/composables/useKeyboard.ts +45 -0
  86. package/src/composables/useLumina.ts +32 -0
  87. package/src/composables/useStudio.ts +87 -0
  88. package/src/composables/useSwipeNav.ts +53 -0
  89. package/src/composables/useTransition.ts +373 -0
  90. package/src/core/Lumina.ts +819 -0
  91. package/src/core/animationConfig.ts +251 -0
  92. package/src/core/compression.ts +34 -0
  93. package/src/core/elementController.ts +170 -0
  94. package/src/core/elementId.ts +27 -0
  95. package/src/core/elementResolver.ts +207 -0
  96. package/src/core/events.ts +53 -0
  97. package/src/core/fonts.ts +100 -0
  98. package/src/core/presets.ts +231 -0
  99. package/src/core/prompts.ts +272 -0
  100. package/src/core/schema.ts +478 -0
  101. package/src/core/speaker-channel.ts +250 -0
  102. package/src/core/store.ts +461 -0
  103. package/src/core/theme.ts +666 -0
  104. package/src/core/types.ts +1611 -0
  105. package/src/directives/vStudio.ts +45 -0
  106. package/src/index.ts +175 -0
  107. package/src/main.ts +17 -0
  108. package/src/router/index.ts +92 -0
  109. package/src/style/main.css +462 -0
  110. package/src/utils/deep.ts +127 -0
  111. package/src/utils/firebase.ts +184 -0
  112. package/src/utils/streaming.ts +134 -0
  113. package/src/views/DashboardView.vue +32 -0
  114. package/src/views/DeckView.vue +289 -0
  115. package/src/views/HomeView.vue +17 -0
  116. package/src/views/SiteLayout.vue +21 -0
  117. package/src/views/StudioView.vue +61 -0
  118. package/src/vite-env.d.ts +6 -0
  119. package/IMPLEMENTATION.md +0 -418
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Animation config — centralized defaults and resolution for all animation timings, delays, and eases.
3
+ *
4
+ * @description
5
+ * Single source of truth for animation constants used by useTransition, Lumina.revealInSequence,
6
+ * elementController, LuminaDeck onLeave, and LuminaElement. Every timing and ease can be overridden via
7
+ * `options.animation`, `deck.meta.effects`, or `slide.meta.effects`. Precedence: slide overrides deck
8
+ * over options over ANIMATION_DEFAULTS.
9
+ *
10
+ * Legacy keys in meta.effects are mapped: `animationDuration`→durationIn, `animationDurationOut`→durationOut,
11
+ * `animationStagger`→stagger, `animationEase`→ease, `animationsEnabled`→enabled, `animationType`→type,
12
+ * `entranceReveal` (unchanged).
13
+ *
14
+ * @example
15
+ * // Resolve config for a slide (e.g. in a custom component or tooling)
16
+ * import { resolveTransitionConfig, ANIMATION_DEFAULTS } from 'lumina-slides';
17
+ * const cfg = resolveTransitionConfig(store, slide);
18
+ * console.log(cfg.durationIn, cfg.revealDelayMs, cfg.elementDuration);
19
+ *
20
+ * @example
21
+ * // Override via options when creating the engine
22
+ * new Lumina("#app", { animation: { durationIn: 1.2, revealDelayMs: 500, elementDuration: 0.6 } });
23
+ *
24
+ * @example
25
+ * // Override via deck or slide meta (in JSON)
26
+ * { "meta": { "effects": { "animationDuration": 1, "revealDelayMs": 400 } }, "slides": [...] }
27
+ *
28
+ * @module animationConfig
29
+ * @see Lumina
30
+ * @see LuminaAnimationOptions
31
+ * @see IMPLEMENTATION.md Animation options section
32
+ */
33
+
34
+ import type { LuminaStore } from './store';
35
+ import type { BaseSlideData } from './types';
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // DEFAULTS — Single source of truth for all animation constants
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Default values for every animation setting. All keys are guaranteed to exist.
43
+ * Override via `options.animation`, `deck.meta.effects`, or `slide.meta.effects`; see resolveTransitionConfig.
44
+ *
45
+ * @constant
46
+ * @type {Readonly<Record<string, string|number|boolean>>}
47
+ * @see resolveTransitionConfig
48
+ * @see LuminaAnimationOptions
49
+ */
50
+ export const ANIMATION_DEFAULTS = {
51
+ // --- Global ---
52
+ enabled: true,
53
+ type: 'fade' as const,
54
+ durationIn: 0.8,
55
+ durationOut: 0.5,
56
+ stagger: 0.1,
57
+ ease: 'power3.out',
58
+ entranceReveal: false,
59
+
60
+ // --- useTransition: entrance (by element type) ---
61
+ /** Multiplier for image reveal duration (durationIn * this). */
62
+ imageDurationMultiplier: 1.5,
63
+ imageEase: 'expo.out',
64
+ /** Stagger between character spans in split text. */
65
+ characterStagger: 0.015,
66
+ characterDurationFactor: 0.8,
67
+ characterEase: 'back.out(2)',
68
+ /** Stagger between .reveal-child elements. */
69
+ childRevealStagger: 0.05,
70
+ childRevealDurationFactor: 0.6,
71
+ childRevealStart: 0.2,
72
+ childRevealEase: 'power2.out',
73
+ /** Duration for standard (non-elastic) when durationIn not set. */
74
+ durationInStandard: 0.7,
75
+ /** Duration for elastic-up type. */
76
+ durationInElastic: 1.2,
77
+ easeElastic: 'elastic.out(1, 0.5)',
78
+ easeSpring: 'back.out(1.5)',
79
+
80
+ // --- useTransition: exit ---
81
+ easeOut: 'power2.in',
82
+ exitCharStagger: 0.005,
83
+ exitStaggerFactor: 0.5,
84
+ exitImageDurationFactor: 1.2,
85
+ exitChildDurationFactor: 0.8,
86
+
87
+ // --- Lumina.revealInSequence ---
88
+ revealDelayMs: 400,
89
+ revealDuration: 0.45,
90
+ revealEase: 'power2.out',
91
+
92
+ // --- elementController.animate ---
93
+ elementDuration: 0.5,
94
+ elementEase: 'power2.out',
95
+
96
+ // --- LuminaDeck onLeave (uses durationOut, easeOut) ---
97
+ // (no extra keys; durationOut, easeOut above)
98
+
99
+ // --- LuminaElement opacity transition (CSS fallback) ---
100
+ elementOpacityTransition: '0.35s ease-out',
101
+ } as const;
102
+
103
+ /**
104
+ * Fully resolved animation config. Every field is defined (no undefined). Produced by resolveTransitionConfig.
105
+ * Used internally by useTransition, Lumina.revealInSequence, elementController, LuminaDeck onLeave.
106
+ *
107
+ * @typedef {Object} ResolvedTransitionConfig
108
+ * @property {boolean} enabled - Global animations on/off.
109
+ * @property {string} type - 'fade'|'slide'|'zoom'|'cascade'|'blur'|'elastic-up'|'spring'|etc.
110
+ * @property {number} durationIn - Entry animation duration (s). Default 0.8.
111
+ * @property {number} durationOut - Exit animation duration (s). Default 0.5.
112
+ * @property {number} stagger - Delay between staggered elements (s). Default 0.1.
113
+ * @property {string} ease - GSAP ease (e.g. 'power3.out').
114
+ * @property {boolean} entranceReveal - If true, run GSAP reveal on .reveal-* when entering.
115
+ * @property {number} imageDurationMultiplier - Image duration = durationIn * this. Default 1.5.
116
+ * @property {string} imageEase - Image reveal ease. Default 'expo.out'.
117
+ * @property {number} characterStagger - Stagger between split-text chars (s). Default 0.015.
118
+ * @property {number} characterDurationFactor - Character duration factor. Default 0.8.
119
+ * @property {string} characterEase - Character ease. Default 'back.out(2)'.
120
+ * @property {number} childRevealStagger - Stagger between .reveal-child (s). Default 0.05.
121
+ * @property {number} childRevealDurationFactor - Child duration factor. Default 0.6.
122
+ * @property {number} childRevealStart - Timeline start for child reveal. Default 0.2.
123
+ * @property {string} childRevealEase - Child reveal ease. Default 'power2.out'.
124
+ * @property {number} durationInStandard - Fallback when type is not elastic. Default 0.7.
125
+ * @property {number} durationInElastic - Duration for elastic-up. Default 1.2.
126
+ * @property {string} easeElastic - Ease for elastic-up. Default 'elastic.out(1, 0.5)'.
127
+ * @property {string} easeSpring - Ease for spring. Default 'back.out(1.5)'.
128
+ * @property {string} easeOut - Exit/leave ease. Default 'power2.in'.
129
+ * @property {number} exitCharStagger - Char stagger on exit. Default 0.005.
130
+ * @property {number} exitStaggerFactor - Exit element stagger = stagger * this. Default 0.5.
131
+ * @property {number} exitImageDurationFactor - Exit image duration factor. Default 1.2.
132
+ * @property {number} exitChildDurationFactor - Exit child duration factor. Default 0.8.
133
+ * @property {number} revealDelayMs - revealInSequence delay between elements (ms). Default 400.
134
+ * @property {number} revealDuration - revealInSequence per-element duration (s). Default 0.45.
135
+ * @property {string} revealEase - revealInSequence ease. Default 'power2.out'.
136
+ * @property {number} elementDuration - element().animate() default duration (s). Default 0.5.
137
+ * @property {string} elementEase - element().animate() default ease. Default 'power2.out'.
138
+ * @property {string} elementOpacityTransition - CSS value for LuminaElement opacity. Default '0.35s ease-out'.
139
+ */
140
+ export type ResolvedTransitionConfig = {
141
+ enabled: boolean;
142
+ type: string;
143
+ durationIn: number;
144
+ durationOut: number;
145
+ stagger: number;
146
+ ease: string;
147
+ entranceReveal: boolean;
148
+ imageDurationMultiplier: number;
149
+ imageEase: string;
150
+ characterStagger: number;
151
+ characterDurationFactor: number;
152
+ characterEase: string;
153
+ childRevealStagger: number;
154
+ childRevealDurationFactor: number;
155
+ childRevealStart: number;
156
+ childRevealEase: string;
157
+ durationInStandard: number;
158
+ durationInElastic: number;
159
+ easeElastic: string;
160
+ easeSpring: string;
161
+ easeOut: string;
162
+ exitCharStagger: number;
163
+ exitStaggerFactor: number;
164
+ exitImageDurationFactor: number;
165
+ exitChildDurationFactor: number;
166
+ revealDelayMs: number;
167
+ revealDuration: number;
168
+ revealEase: string;
169
+ elementDuration: number;
170
+ elementEase: string;
171
+ elementOpacityTransition: string;
172
+ };
173
+
174
+ const LEGACY_MAP: Record<string, keyof ResolvedTransitionConfig> = {
175
+ animationsEnabled: 'enabled',
176
+ animationType: 'type',
177
+ animationDuration: 'durationIn',
178
+ animationDurationOut: 'durationOut',
179
+ animationStagger: 'stagger',
180
+ animationEase: 'ease',
181
+ entranceReveal: 'entranceReveal',
182
+ };
183
+
184
+ function mapLegacy(src: Record<string, unknown> | null | undefined): Partial<ResolvedTransitionConfig> {
185
+ if (!src || typeof src !== 'object') return {};
186
+ const out: Partial<ResolvedTransitionConfig> = {};
187
+ for (const [k, v] of Object.entries(src)) {
188
+ if (v === undefined) continue;
189
+ const canon = LEGACY_MAP[k] ?? (k as keyof ResolvedTransitionConfig);
190
+ if (Object.prototype.hasOwnProperty.call(ANIMATION_DEFAULTS, canon)) {
191
+ (out as Record<string, unknown>)[canon] = v;
192
+ }
193
+ }
194
+ return out;
195
+ }
196
+
197
+ function pickCanonical(src: Record<string, unknown> | null | undefined): Partial<ResolvedTransitionConfig> {
198
+ if (!src || typeof src !== 'object') return {};
199
+ const out: Partial<ResolvedTransitionConfig> = {};
200
+ const keys = Object.keys(ANIMATION_DEFAULTS) as (keyof ResolvedTransitionConfig)[];
201
+ for (const k of keys) {
202
+ if (Object.prototype.hasOwnProperty.call(src, k) && (src as Record<string, unknown>)[k] !== undefined) {
203
+ (out as Record<string, unknown>)[k] = (src as Record<string, unknown>)[k];
204
+ }
205
+ }
206
+ return out;
207
+ }
208
+
209
+ /**
210
+ * Resolves the full animation config by merging, in order (later overrides earlier):
211
+ * 1. ANIMATION_DEFAULTS
212
+ * 2. options.animation (with legacy key mapping)
213
+ * 3. deck.meta.effects (with legacy key mapping)
214
+ * 4. slide.meta.effects (with legacy key mapping)
215
+ *
216
+ * Use when you need a single config object for transitions, revealInSequence, element().animate(),
217
+ * or deck leave. When `store` or `slideData` is null/undefined, only the layers you have are merged.
218
+ *
219
+ * @param store - Lumina store (state.options, state.deck). Can be null if building config without engine.
220
+ * @param slideData - Optional slide for slide-level overrides (slide.meta.effects). Omit for deck-level only.
221
+ * @returns Resolved config with every key defined (no undefined). Safe to pass to GSAP or timing logic.
222
+ *
223
+ * @example
224
+ * const cfg = resolveTransitionConfig(store, currentSlide);
225
+ * gsap.to(el, { duration: cfg.durationIn, ease: cfg.ease });
226
+ *
227
+ * @example
228
+ * // Without slide (deck + options only)
229
+ * const cfg = resolveTransitionConfig(store, undefined);
230
+ *
231
+ * @see ANIMATION_DEFAULTS
232
+ * @see ResolvedTransitionConfig
233
+ * @see LuminaAnimationOptions
234
+ */
235
+ export function resolveTransitionConfig(store: LuminaStore | null | undefined, slideData?: BaseSlideData | null): ResolvedTransitionConfig {
236
+ const base = { ...ANIMATION_DEFAULTS } as unknown as Record<string, unknown>;
237
+
238
+ const opt = (store?.state?.options?.animation || {}) as Record<string, unknown>;
239
+ const deckEff = (store?.state?.deck?.meta?.effects || {}) as Record<string, unknown>;
240
+ const slideEff = (slideData?.meta?.effects || {}) as Record<string, unknown>;
241
+
242
+ const apply = (raw: Record<string, unknown>) => {
243
+ Object.assign(base, mapLegacy(raw), pickCanonical(raw));
244
+ };
245
+
246
+ apply(opt);
247
+ apply(deckEff);
248
+ apply(slideEff);
249
+
250
+ return base as unknown as ResolvedTransitionConfig;
251
+ }
@@ -0,0 +1,34 @@
1
+
2
+ /**
3
+ * Feature: Token Optimization (Compression)
4
+ * A dictionary of common values mapped to short codes.
5
+ * This can be provided to the LLM to reduce output size.
6
+ */
7
+
8
+ export const COMPRESSION_DICT = {
9
+ // Icons
10
+ 'i:s': 'star',
11
+ 'i:u': 'users',
12
+ 'i:c': 'check',
13
+ 'i:a': 'arrow-right',
14
+
15
+ // Colors (Tailwind standard approximation)
16
+ 'c:p': '#6366f1', // Primary (Indigo)
17
+ 'c:s': '#10b981', // Success (Emerald)
18
+ 'c:d': '#ef4444', // Danger (Red)
19
+
20
+ // Variants
21
+ 'v:g': 'gradient',
22
+ 'v:o': 'outlined',
23
+ 'v:s': 'soft',
24
+ };
25
+
26
+ /**
27
+ * Expands a compressed value if it exists in the dictionary.
28
+ */
29
+ export function expandValue(val: string): string {
30
+ if (val in COMPRESSION_DICT) {
31
+ return (COMPRESSION_DICT as any)[val];
32
+ }
33
+ return val;
34
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Element controller factory. Creates the fluent API returned by `engine.element(id)` and
3
+ * `engine.element(slideIndex, path)`. Use the public API; this module is for implementation.
4
+ *
5
+ * @module elementController
6
+ * @see Lumina.element
7
+ * @see ElementController
8
+ * @see AnimateOptions
9
+ */
10
+ import gsap from 'gsap';
11
+ import type { LuminaStore } from './store';
12
+ import { resolveTransitionConfig } from './animationConfig';
13
+ import { resolveAnimationFromInput } from '../animation';
14
+ import type { AnimateOptions, ElementController as IElementController, ElementState, TimelineKeyframes, TimelineKeyframeState } from './types';
15
+
16
+ /**
17
+ * Minimal engine interface for resolving a DOM node by element id. Implemented by {@link Lumina}.
18
+ */
19
+ export interface LuminaEngineLike {
20
+ getElementById(id: string): HTMLElement | null;
21
+ }
22
+
23
+ /**
24
+ * Creates an {@link ElementController} for the given element id. Used internally by
25
+ * `engine.element(id)` and `engine.element(slideIndex, path)`. Prefer the public API.
26
+ *
27
+ * @param store - Lumina store (setElementState, state.elementState).
28
+ * @param engine - Engine with getElementById (for animate).
29
+ * @param id - Resolved element id (from resolveId or user).
30
+ * @returns Fluent controller for show/hide/opacity/transform/css/class/animate.
31
+ * @internal
32
+ */
33
+ export function createElementController(
34
+ store: LuminaStore,
35
+ engine: LuminaEngineLike,
36
+ id: string
37
+ ): IElementController {
38
+ function set(patch: Partial<ElementState>) {
39
+ store.setElementState(id, patch);
40
+ }
41
+
42
+ function getClass(): string {
43
+ return (store.state.elementState[id]?.class as string) || '';
44
+ }
45
+
46
+ const ctrl: IElementController = {
47
+ show() {
48
+ set({ visible: true, opacity: 1 });
49
+ const dom = engine.getElementById(id);
50
+ const reveal = dom?.closest('.reveal-zoom, .reveal-up, .reveal-card, .reveal-left, .reveal-right');
51
+ if (reveal) gsap.set(reveal, { opacity: 1 });
52
+ return ctrl;
53
+ },
54
+ hide() {
55
+ set({ visible: false });
56
+ return ctrl;
57
+ },
58
+ toggle(force?: boolean) {
59
+ const cur = store.state.elementState[id]?.visible !== false;
60
+ set({ visible: force !== undefined ? force : !cur });
61
+ return ctrl;
62
+ },
63
+ opacity(value: number) {
64
+ set({ opacity: value });
65
+ return ctrl;
66
+ },
67
+ transform(value: string) {
68
+ set({ transform: value });
69
+ return ctrl;
70
+ },
71
+ css(style: Record<string, string | number>) {
72
+ set({ style });
73
+ return ctrl;
74
+ },
75
+ addClass(className: string) {
76
+ const cur = getClass();
77
+ const next = (cur + ' ' + className).trim();
78
+ set({ class: next });
79
+ return ctrl;
80
+ },
81
+ removeClass(className: string) {
82
+ const cur = getClass();
83
+ const next = cur
84
+ .split(/\s+/)
85
+ .filter((c) => c && c !== className)
86
+ .join(' ');
87
+ set({ class: next });
88
+ return ctrl;
89
+ },
90
+ animate(options: AnimateOptions) {
91
+ runAnimate(options);
92
+ return ctrl;
93
+ },
94
+ animateAsync(options: AnimateOptions): Promise<void> {
95
+ return new Promise((resolve) => {
96
+ runAnimate(options, () => {
97
+ options.onComplete?.();
98
+ resolve();
99
+ });
100
+ });
101
+ },
102
+ animateToProgress(progress: number, keyframes: TimelineKeyframes) {
103
+ const keys = Object.keys(keyframes)
104
+ .map((k) => parseFloat(k))
105
+ .filter((n) => !Number.isNaN(n))
106
+ .sort((a, b) => a - b);
107
+ if (keys.length === 0) return ctrl;
108
+ const k0 = keys[0];
109
+ const k1 = keys[keys.length - 1];
110
+ let state: TimelineKeyframeState;
111
+ if (progress <= k0) {
112
+ state = keyframes[String(k0)] ?? {};
113
+ } else if (progress >= k1) {
114
+ state = keyframes[String(k1)] ?? {};
115
+ } else {
116
+ let i = 0;
117
+ while (i + 1 < keys.length && keys[i + 1] <= progress) i++;
118
+ const a = keys[i];
119
+ const b = keys[i + 1];
120
+ const va = keyframes[String(a)] ?? {};
121
+ const vb = keyframes[String(b)] ?? {};
122
+ const t = (progress - a) / (b - a);
123
+ state = {};
124
+ for (const prop of ['opacity', 'x', 'y', 'scale', 'rotate'] as const) {
125
+ const pa = va[prop];
126
+ const pb = vb[prop];
127
+ if (typeof pa === 'number' && typeof pb === 'number') (state as Record<string, number>)[prop] = pa + (pb - pa) * t;
128
+ else if (pb !== undefined) (state as Record<string, unknown>)[prop] = pb;
129
+ else if (pa !== undefined) (state as Record<string, unknown>)[prop] = pa;
130
+ }
131
+ if (typeof vb.visible === 'boolean') state.visible = vb.visible;
132
+ else if (typeof va.visible === 'boolean') state.visible = va.visible;
133
+ }
134
+ const opacity = state.opacity !== undefined ? state.opacity : 1;
135
+ const parts: string[] = [];
136
+ if (state.x !== undefined || state.y !== undefined) {
137
+ parts.push(`translate(${state.x ?? 0}px, ${state.y ?? 0}px)`);
138
+ }
139
+ if (state.scale !== undefined) {
140
+ parts.push(`scale(${state.scale})`);
141
+ }
142
+ if (state.rotate !== undefined) {
143
+ parts.push(`rotate(${state.rotate}deg)`);
144
+ }
145
+ const transform = parts.length ? parts.join(' ') : '';
146
+ const visible = state.visible !== undefined ? state.visible : opacity > 0;
147
+ set({ opacity, transform, visible });
148
+ return ctrl;
149
+ }
150
+ };
151
+
152
+ function runAnimate(opts: AnimateOptions, onCompleteOverride?: () => void) {
153
+ const el = engine.getElementById(id);
154
+ if (!el) return;
155
+ const cfg = resolveTransitionConfig(store, undefined);
156
+ const { from, to, ease, duration } = resolveAnimationFromInput(opts, {
157
+ duration: cfg.elementDuration,
158
+ ease: cfg.elementEase,
159
+ });
160
+ const cb = onCompleteOverride ?? opts.onComplete;
161
+ const toVars = { ...to, duration, ease, overwrite: true, onComplete: cb } as gsap.TweenVars;
162
+ if (from && Object.keys(from).length) {
163
+ gsap.fromTo(el, from as gsap.TweenVars, toVars);
164
+ } else {
165
+ gsap.to(el, toVars);
166
+ }
167
+ }
168
+
169
+ return ctrl;
170
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Builds a stable, unique element id from slide index and a logical path. Used as fallback
3
+ * when the slide or child object does not define an explicit `id`. The format is
4
+ * `s{slideIndex}-{path0}-{path1}-...` (e.g. "s0-tag", "s1-features-2", "s2-elements-0-elements-1").
5
+ *
6
+ * Prefer resolveId when you have slide data (it respects explicit ids and slide.ids). Use
7
+ * elemId when constructing an id without the full slide (e.g. for initialElementState keys
8
+ * when you know the path convention).
9
+ *
10
+ * @param slideIndex - Zero-based slide index. If undefined, 0 is used.
11
+ * @param path - Rest args: property names and indices (e.g. 'tag'; 'features', 2; 'elements', 0, 'elements', 1).
12
+ * @returns Id string. Safe for data-lumina-id, engine.element(id), and meta.initialElementState.
13
+ *
14
+ * @example
15
+ * elemId(0, 'tag') // "s0-tag"
16
+ * elemId(1, 'features', 2) // "s1-features-2"
17
+ * elemId(2, 'elements', 0, 'elements', 1) // "s2-elements-0-elements-1"
18
+ *
19
+ * @see resolveId
20
+ * @see getElementIds
21
+ * @see DeckMeta.initialElementState
22
+ */
23
+ export function elemId(slideIndex: number | undefined, ...path: (string | number)[]): string {
24
+ const s = slideIndex ?? 0;
25
+ if (path.length === 0) return `s${s}`;
26
+ return `s${s}-${path.map(String).join('-')}`;
27
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * ELEMENT RESOLVER
3
+ *
4
+ * Central, schema-aware logic to:
5
+ * - Resolve a logical path (e.g. ["elements", 0, "elements", 1]) to a stable element id.
6
+ * - Enumerate all element paths for a slide (for engine.elements(slideIndex) and path-based element()).
7
+ *
8
+ * Supports: explicit `id` on objects, `ids` on the slide for named fields, and fallback to
9
+ * elemId(slideIndex, ...path). Works for all built-in layouts; extend PATH_GENERATORS for new types.
10
+ *
11
+ * @see resolveId
12
+ * @see getElementPaths
13
+ * @see getElementIds
14
+ * @see Lumina.element
15
+ * @see Lumina.elements
16
+ */
17
+
18
+ import { getByPath } from '../utils/deep';
19
+ import { elemId } from './elementId';
20
+ import type { BaseSlideData } from './types';
21
+
22
+ /**
23
+ * Logical path into slide data: property names and array indices. Used by resolveId and
24
+ * engine.element(slideIndex, path). String form: "tag", "features.0", "elements.0.elements.1".
25
+ */
26
+ export type ElementPath = (string | number)[];
27
+
28
+ /**
29
+ * Converts an ElementPath to a dot-notation key for getByPath (e.g. ["features", 0] → "features.0").
30
+ */
31
+ export function pathToKey(path: ElementPath): string {
32
+ return path.join('.');
33
+ }
34
+
35
+ /**
36
+ * Reads the value at `path` in the slide object. Uses dot-notation traversal; indices are
37
+ * stringified (e.g. "features.0").
38
+ *
39
+ * @param slide - Slide data (BaseSlideData). Can be any object for generic traversal.
40
+ * @param path - Path segments (e.g. ['features', 0] or ['elements', 0, 'elements', 1]).
41
+ * @returns The value at that path, or undefined if missing.
42
+ */
43
+ export function getValueAt(slide: BaseSlideData, path: ElementPath): unknown {
44
+ if (!slide || !path.length) return undefined;
45
+ return getByPath(slide, pathToKey(path));
46
+ }
47
+
48
+ /**
49
+ * Parses a path from string or array. Use for engine.element(slideIndex, path) when
50
+ * path is a string like "features.0" or "elements.0.elements.1".
51
+ *
52
+ * Rules: split by "."; numeric segments become numbers, rest stay strings.
53
+ *
54
+ * @param input - String ("features.0") or already-parsed ElementPath.
55
+ * @returns Normalized ElementPath. Empty array if input is not string or array.
56
+ *
57
+ * @example
58
+ * parsePath("tag") // ['tag']
59
+ * parsePath("features.0") // ['features', 0]
60
+ * parsePath("elements.0.elements.1") // ['elements', 0, 'elements', 1]
61
+ */
62
+ export function parsePath(input: string | ElementPath): ElementPath {
63
+ if (Array.isArray(input)) return input;
64
+ if (typeof input !== 'string') return [];
65
+ return input.split('.').map((p) => (/^\d+$/.test(p) ? parseInt(p, 10) : p));
66
+ }
67
+
68
+ /**
69
+ * Resolves the stable element id for a (slide, slideIndex, path). This id is used in
70
+ * data-lumina-id, engine.element(id), and meta.initialElementState. Resolution order:
71
+ * 1. Path [] or ['slide'] → slide.id or elemId(slideIndex, 'slide').
72
+ * 2. If the value at path is an object with string `id` → use it (e.g. feature.id, node.id).
73
+ * 3. If slide.ids[pathToKey(path)] exists → use it (supports compound keys: 'tag', 'features.0', 'elements.0.elements.1').
74
+ * 4. Otherwise → elemId(slideIndex, ...path) (e.g. "s0-tag", "s1-elements-0-elements-1").
75
+ *
76
+ * @param slide - The slide data.
77
+ * @param slideIndex - Zero-based slide index (for elemId fallback).
78
+ * @param path - Logical path (from getElementPaths or parsePath).
79
+ * @returns Element id string. Use in engine.element(id) or meta.initialElementState.
80
+ *
81
+ * @see getElementPaths
82
+ * @see getElementIds
83
+ * @see elemId
84
+ */
85
+ export function resolveId(
86
+ slide: BaseSlideData,
87
+ slideIndex: number,
88
+ path: ElementPath
89
+ ): string {
90
+ const slideAny = slide as { id?: string; ids?: Record<string, string> };
91
+ if (path.length === 0 || (path.length === 1 && path[0] === 'slide')) {
92
+ return slideAny?.id || elemId(slideIndex, 'slide');
93
+ }
94
+ const obj = getValueAt(slide, path);
95
+ if (obj != null && typeof obj === 'object' && typeof (obj as { id?: string }).id === 'string') {
96
+ return (obj as { id: string }).id;
97
+ }
98
+ const key = pathToKey(path);
99
+ if (key && slideAny?.ids?.[key]) {
100
+ return slideAny.ids[key];
101
+ }
102
+ return elemId(slideIndex, ...path);
103
+ }
104
+
105
+ /** Function that returns all logical element paths for a given slide. One per layout type. */
106
+ export type PathGenerator = (slide: BaseSlideData) => ElementPath[];
107
+
108
+ /** Map of slide.type → path generator. Extend to support new layouts. */
109
+ const PATH_GENERATORS: Record<string, PathGenerator> = {
110
+ statement: () => [['tag'], ['title'], ['subtitle']],
111
+ features: (s) => {
112
+ const arr = getValueAt(s, ['features']);
113
+ const list = Array.isArray(arr) ? arr : [];
114
+ return [['header'], ...list.map((_: unknown, i: number) => ['features', i] as ElementPath)];
115
+ },
116
+ half: () => [['media'], ['tag'], ['title'], ['paragraphs'], ['cta']],
117
+ timeline: (s) => {
118
+ const arr = getValueAt(s, ['timeline']);
119
+ const list = Array.isArray(arr) ? arr : [];
120
+ return [['title'], ['subtitle'], ...list.map((_: unknown, i: number) => ['timeline', i] as ElementPath)];
121
+ },
122
+ steps: (s) => {
123
+ const arr = getValueAt(s, ['steps']);
124
+ const list = Array.isArray(arr) ? arr : [];
125
+ return [['header'], ...list.map((_: unknown, i: number) => ['steps', i] as ElementPath)];
126
+ },
127
+ flex: (s) => {
128
+ const paths: ElementPath[] = [];
129
+ const els = getValueAt(s, ['elements']);
130
+ const list = Array.isArray(els) ? els : [];
131
+ list.forEach((e: unknown, i: number) => {
132
+ paths.push(['elements', i]);
133
+ const ob = e && typeof e === 'object' ? (e as { type?: string; elements?: unknown[] }) : null;
134
+ if (ob?.type === 'content' && Array.isArray(ob.elements)) {
135
+ ob.elements.forEach((_: unknown, j: number) => paths.push(['elements', i, 'elements', j]));
136
+ }
137
+ });
138
+ return paths;
139
+ },
140
+ chart: () => [['title'], ['subtitle'], ['chart']],
141
+ diagram: (s) => {
142
+ const p: ElementPath[] = [];
143
+ const nodes = getValueAt(s, ['nodes']);
144
+ const edges = getValueAt(s, ['edges']);
145
+ (Array.isArray(nodes) ? nodes : []).forEach((_: unknown, i: number) => p.push(['nodes', i]));
146
+ (Array.isArray(edges) ? edges : []).forEach((_: unknown, i: number) => p.push(['edges', i]));
147
+ return p;
148
+ },
149
+ custom: () => [],
150
+ video: () => [['video'], ['title']],
151
+ free: (s) => {
152
+ const arr = getValueAt(s, ['elements']);
153
+ const list = Array.isArray(arr) ? arr : [];
154
+ return list.map((_: unknown, i: number) => ['elements', i] as ElementPath);
155
+ },
156
+ };
157
+
158
+ /**
159
+ * Registers or overrides the path generator for a slide type. Use to support custom layouts
160
+ * or to change how elements are enumerated for engine.elements(slideIndex) and path-based
161
+ * engine.element(slideIndex, path).
162
+ *
163
+ * @param type - Slide type (e.g. 'statement', 'features', or a custom type like 'diagram-v2').
164
+ * @param fn - Generator that returns ElementPath[] for a slide of that type.
165
+ *
166
+ * @example
167
+ * import { registerElementPaths, getElementPaths } from 'lumina-slides';
168
+ * registerElementPaths('my-layout', (slide) => [['title'], ['blocks', 0], ['blocks', 1]]);
169
+ */
170
+ export function registerElementPaths(type: string, fn: PathGenerator): void {
171
+ PATH_GENERATORS[type] = fn;
172
+ }
173
+
174
+ /**
175
+ * Returns all logical element paths for a slide. Used by {@link getElementIds} and thus
176
+ * engine.elements(slideIndex). Always includes the slide root ['slide']; the rest come from
177
+ * PATH_GENERATORS for the slide's type. Add new layout types by extending PATH_GENERATORS.
178
+ *
179
+ * @param slide - The slide data (must have `type`).
180
+ * @returns Array of paths, e.g. [['slide'], ['tag'], ['title'], ['subtitle']] for statement.
181
+ *
182
+ * @see getElementIds
183
+ * @see resolveId
184
+ * @see Lumina.elements
185
+ */
186
+ export function getElementPaths(slide: BaseSlideData): ElementPath[] {
187
+ const t = (slide?.type as string) || 'unknown';
188
+ const gen = PATH_GENERATORS[t] || (() => []);
189
+ return [['slide'], ...gen(slide)];
190
+ }
191
+
192
+ /**
193
+ * Returns all element ids for a slide. Used by engine.elements(slideIndex) and useful to
194
+ * know which ids can be passed to engine.element(id) or to meta.initialElementState.
195
+ *
196
+ * @param slide - The slide data.
197
+ * @param slideIndex - Zero-based slide index (for resolveId).
198
+ * @returns Array of id strings, e.g. ["s0-slide","s0-tag","s0-title","s0-subtitle"].
199
+ *
200
+ * @see getElementPaths
201
+ * @see resolveId
202
+ * @see Lumina.elements
203
+ * @see DeckMeta.initialElementState
204
+ */
205
+ export function getElementIds(slide: BaseSlideData, slideIndex: number): string[] {
206
+ return getElementPaths(slide).map((p) => resolveId(slide, slideIndex, p));
207
+ }