lumina-slides 8.9.2 → 8.9.4

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/IMPLEMENTATION.md CHANGED
@@ -1,44 +1,46 @@
1
1
  # Implementing lumina-slides
2
2
 
3
- Guía para integrar la librería `lumina-slides` en tu aplicación. Pensada para desarrolladores y para **agentes de código** (Cursor, Copilot, etc.) que escriben el código que consume la librería.
3
+ Guide for integrating the `lumina-slides` library into your application. For developers and **code agents** (Cursor, Copilot, etc.) that write the code that consumes the library.
4
+
5
+ **Vanilla JavaScript library:** use `import { Lumina } from "lumina-slides"`, `new Lumina(selector)`, and `engine.load(deck)`. The host app can be vanilla JS, React, or any framework; **Vue is not required**. All control (render, element control, animations, events) is done via the engine API and the deck JSON. The API is designed for vanilla JS.
4
6
 
5
7
  ---
6
8
 
7
- ## 1. Instalación
9
+ ## 1. Installation
8
10
 
9
11
  ```bash
10
12
  npm install lumina-slides
11
- # Opcional, solo si usas slides tipo "chart":
13
+ # Optional, only if you use "chart" slides:
12
14
  npm install lumina-slides chart.js
13
15
  ```
14
16
 
15
17
  ---
16
18
 
17
- ## 2. Imports principales
19
+ ## 2. Main imports
18
20
 
19
21
  ```ts
20
22
  import { Lumina, parsePartialJson, generateSystemPrompt, getLuminaJsonSchema } from "lumina-slides";
21
23
  import "lumina-slides/style.css";
22
24
  ```
23
25
 
24
- Otras exportaciones útiles: `createDebouncedLoader`, `isSlideReady`, `generateThemePrompt`, tipos `Deck`, `LuminaOptions`, `LuminaEventMap`, etc.
26
+ Other useful exports: `createDebouncedLoader`, `isSlideReady`, `generateThemePrompt`, types `Deck`, `LuminaOptions`, `LuminaEventMap`, etc.
25
27
 
26
28
  ---
27
29
 
28
- ## 3. Crear el motor y cargar un deck
30
+ ## 3. Create the engine and load a deck
29
31
 
30
32
  ```ts
31
33
  const engine = new Lumina("#app", {
32
- theme: "ocean", // preset: default, ocean, midnight, forest, cyber, latte, sunset, monochrome
34
+ theme: "ocean", // presets: default, ocean, midnight, forest, cyber, latte, sunset, monochrome
33
35
  loop: true,
34
36
  navigation: true,
35
37
  touch: true,
36
38
  });
37
39
 
38
40
  engine.load({
39
- meta: { title: "Mi presentación" },
41
+ meta: { title: "My presentation" },
40
42
  slides: [
41
- { type: "statement", title: "Título", subtitle: "Subtítulo" },
43
+ { type: "statement", title: "Title", subtitle: "Subtitle" },
42
44
  { type: "features", title: "Features", features: [
43
45
  { title: "A", desc: "Desc A", icon: "star" },
44
46
  { title: "B", desc: "Desc B", icon: "zap" },
@@ -49,34 +51,34 @@ engine.load({
49
51
 
50
52
  ---
51
53
 
52
- ## 4. Actualizar el deck (patch)
54
+ ## 4. Update the deck (patch)
53
55
 
54
- Para cambios incrementales sin recargar todo:
56
+ For incremental updates without reloading:
55
57
 
56
58
  ```ts
57
- engine.patch({ meta: { title: "Nuevo título" } });
58
- // slides se reemplaza por completo si lo pasas:
59
- // engine.patch({ slides: [...nuevosSlides] });
59
+ engine.patch({ meta: { title: "New title" } });
60
+ // slides are replaced entirely if provided:
61
+ // engine.patch({ slides: [...newSlides] });
60
62
  ```
61
63
 
62
64
  ---
63
65
 
64
- ## 5. Eventos
66
+ ## 5. Events
65
67
 
66
68
  ```ts
67
- engine.on("ready", (deck) => { /* deck cargado */ });
68
- engine.on("slideChange", ({ index, previousIndex, slide }) => { /* cambió la diapositiva */ });
69
+ engine.on("ready", (deck) => { /* deck loaded */ });
70
+ engine.on("slideChange", ({ index, previousIndex, slide }) => { /* slide changed */ });
69
71
  engine.on("action", (p) => {
70
- // p: { type, label?, value? } — p.ej. clicks en botones
72
+ // p: { type, label?, value? } — e.g. button clicks
71
73
  });
72
- engine.on("error", (err) => { /* error interno */ });
74
+ engine.on("error", (err) => { /* internal error */ });
73
75
  ```
74
76
 
75
- Para desuscribirse: `engine.off("slideChange", handler)` con la misma referencia de función.
77
+ To unsubscribe: `engine.off("slideChange", handler)` with the same function reference.
76
78
 
77
79
  ---
78
80
 
79
- ## 6. Navegación programática
81
+ ## 6. Programmatic navigation
80
82
 
81
83
  ```ts
82
84
  engine.next();
@@ -87,19 +89,19 @@ const i = engine.currentSlideIndex;
87
89
 
88
90
  ---
89
91
 
90
- ## 7. Estado para el agente / LLM
92
+ ## 7. State for the agent / LLM
91
93
 
92
94
  ```ts
93
95
  const state = engine.exportState();
94
96
  // { status, currentSlide: { index, id, type, title }, narrative, engagementLevel, history }
95
- // Útil para incluir en el contexto de un LLM: "El usuario está en la diapositiva X, ha hecho Y".
97
+ // Use in LLM context: "User is on slide X, did Y".
96
98
  ```
97
99
 
98
100
  ---
99
101
 
100
- ## 8. Streaming desde un LLM
102
+ ## 8. Streaming from an LLM
101
103
 
102
- Cuando el modelo va devolviendo JSON por chunks:
104
+ When the model streams JSON in chunks:
103
105
 
104
106
  ```ts
105
107
  import { Lumina, parsePartialJson } from "lumina-slides";
@@ -107,44 +109,44 @@ import { Lumina, parsePartialJson } from "lumina-slides";
107
109
  const engine = new Lumina("#app", { theme: "midnight" });
108
110
  let buffer = "";
109
111
 
110
- async function onStream(chunk: string) {
112
+ function onStream(chunk) {
111
113
  buffer += chunk;
112
114
  const json = parsePartialJson(buffer);
113
115
  if (json) engine.load(json);
114
116
  }
115
117
  ```
116
118
 
117
- O con debounce para evitar parpadeos:
119
+ Or with debounce to reduce flicker:
118
120
 
119
121
  ```ts
120
122
  import { createDebouncedLoader } from "lumina-slides";
121
123
 
122
124
  const onChunk = createDebouncedLoader((deck) => engine.load(deck), 80);
123
- // En el bucle de stream: onChunk(buffer);
125
+ // In the stream loop: onChunk(buffer);
124
126
  ```
125
127
 
126
128
  ---
127
129
 
128
- ## 9. Generar el system prompt para un LLM que crea slides
130
+ ## 9. Generate the system prompt for an LLM that creates slides
129
131
 
130
- Si un LLM va a generar el JSON del deck:
132
+ If an LLM will generate the deck JSON:
131
133
 
132
134
  ```ts
133
135
  const systemPrompt = generateSystemPrompt({
134
- mode: "fast", // 'reasoning' si quieres CoT antes del JSON
136
+ mode: "fast", // 'reasoning' for CoT before JSON
135
137
  includeSchema: true,
136
138
  includeTheming: true,
137
139
  });
138
- // Usar systemPrompt como mensaje de sistema en OpenAI, Anthropic, etc.
140
+ // Use systemPrompt as the system message in OpenAI, Anthropic, etc.
139
141
  ```
140
142
 
141
- Solo temas: `generateThemePrompt()`.
143
+ Theming only: `generateThemePrompt()`.
142
144
 
143
145
  ---
144
146
 
145
- ## 10. Schema JSON
147
+ ## 10. JSON Schema
146
148
 
147
- Para validar o para dárselo al LLM:
149
+ To validate or pass to the LLM:
148
150
 
149
151
  ```ts
150
152
  const schema = getLuminaJsonSchema();
@@ -152,9 +154,9 @@ const schema = getLuminaJsonSchema();
152
154
 
153
155
  ---
154
156
 
155
- ## 11. Destruir la instancia
157
+ ## 11. Destroy the instance
156
158
 
157
- Al desmontar el componente o salir de la vista:
159
+ When unmounting or leaving the view:
158
160
 
159
161
  ```ts
160
162
  engine.destroy();
@@ -162,11 +164,19 @@ engine.destroy();
162
164
 
163
165
  ---
164
166
 
165
- ## Estructura de un `Deck`
167
+ ## Deck structure
166
168
 
167
169
  ```ts
168
170
  interface Deck {
169
- meta: { title: string; author?: string; theme?: string; themeConfig?: ThemeConfig; [k: string]: any };
171
+ meta: {
172
+ title: string;
173
+ author?: string;
174
+ theme?: string;
175
+ themeConfig?: ThemeConfig;
176
+ elementControl?: { defaultVisible?: boolean }; // false = all hidden by default
177
+ initialElementState?: { [id: string]: { visible?: boolean; opacity?: number; transform?: string; class?: string; style?: Record<string, string | number> } };
178
+ [k: string]: any;
179
+ };
170
180
  slides: Array<
171
181
  | { type: "statement"; title: string; subtitle?: string; tag?: string; notes?: string }
172
182
  | { type: "features"; title: string; features: { title: string; desc: string; icon?: string }[]; description?: string; notes?: string }
@@ -181,12 +191,228 @@ interface Deck {
181
191
  }
182
192
  ```
183
193
 
184
- Los tipos detallados están en el paquete: `Deck`, `SlideStatement`, `SlideFeatures`, `LuminaOptions`, `LuminaEventMap`, etc.
194
+ Detailed types in the package: `Deck`, `SlideStatement`, `SlideFeatures`, `LuminaOptions`, `LuminaEventMap`, etc.
195
+
196
+ ---
197
+
198
+ ## Element IDs and `data-lumina-id` (vanilla JS / JSON API)
199
+
200
+ The library assigns a `data-lumina-id` attribute to each controllable element when **rendering the deck from JSON**. You use only `engine.load(deck)` and the engine API; no framework or component code is required.
201
+
202
+ **ID resolution order:** (1) `slide.id` for the root; (2) if the object at that path has `id: string`, it is used; (3) `slide.ids[path]` if present; (4) fallback `s{N}-{path}` (e.g. `s0-tag`, `s1-features-2`).
203
+
204
+ **Discovering IDs from vanilla JS:** `engine.elements(slideIndex)` returns the array of ids for that slide. To predict ids without mounting: `getElementIds(slide, slideIndex)` or `resolveId(slide, slideIndex, path)` / `elemId(slideIndex, ...path)` (exported for advanced use).
205
+
206
+ **Paths per slide type** (default id is `s{N}-{path}`):
207
+
208
+ | Type | Paths (e.g. `s0-tag`, `s1-features-0`) |
209
+ |----------|----------------------------------------|
210
+ | statement| `tag`, `title`, `subtitle` |
211
+ | features | `header`, `features.0`, `features.1`… |
212
+ | half | `media`, `tag`, `title`, `paragraphs`, `cta` |
213
+ | timeline | `title`, `subtitle`, `timeline.0`… |
214
+ | steps | `header`, `steps.0`, `steps.1`… |
215
+ | flex | `elements.0`, `elements.0.elements.0`… |
216
+ | chart | `title`, `subtitle`, `chart` |
217
+ | video | `video`, `title` |
218
+ | diagram | `nodes.0`, `nodes.1`…, `edges.0`… |
219
+ | custom | (none by default) |
220
+
221
+ **Control from JSON:**
222
+
223
+ - `meta.initialElementState`: `{ [id]: { visible?: false, opacity?, transform?, class?, style? } }` — initial state when the deck loads.
224
+ - `meta.elementControl`: `{ defaultVisible?: boolean }` — if `false`, all elements start hidden; override per id with `initialElementState`.
225
+
226
+ **APIs (vanilla JS):** `engine.element(id)`, `engine.element(slideIndex, path)`, `engine.getElementById(id)`, `engine.elements(slideIndex)`.
227
+
228
+ ---
229
+
230
+ ## Element control and animations (vanilla JS)
231
+
232
+ Use `engine.element(id)` or `engine.element(slideIndex, path)` to get an **ElementController**:
233
+
234
+ - **Visibility:** `.show()`, `.hide()`, `.toggle(force?)`
235
+ - **Style:** `.opacity(n)`, `.transform(s)`, `.css({ ... })`, `.addClass(name)`, `.removeClass(name)`
236
+ - **Animations:** `.animate({ from?, to, duration?, ease?, onComplete? })`, `.animateAsync(...)`
237
+
238
+ `engine.getElementById(id)` returns the DOM node with `data-lumina-id`, or `null` if the slide is not mounted. Animations (GSAP) are no-op when the node is null.
239
+
240
+ **Avoiding null / no visible effect:** The element must be in the DOM when you call `.show()` or `.animate()`. Pattern in **vanilla JS**:
241
+
242
+ 1. **Register `ready` and `slideChange` before `engine.load(deck)`** so callbacks run when the deck/slide is mounted.
243
+
244
+ 2. **In `ready`:** use `setTimeout` (e.g. `(i + 1) * 700` ms per element) before each `engine.element(id).show()` for the first slide.
245
+
246
+ 3. **In `slideChange`:** when `index` is the slide with controlled elements, repeat the same `setTimeout` + `.show()` for that slide’s ids.
247
+
248
+ 4. **Order:** `new Lumina(...)` → `engine.on('ready', ...)` → `engine.on('slideChange', ...)` → `engine.load(deck)`.
249
+
250
+ **Example (vanilla JS) — simple reveal with delay:**
251
+
252
+ ```js
253
+ const engine = new Lumina("#app", { theme: "midnight" });
254
+
255
+ engine.on("ready", (deck) => {
256
+ const ids = engine.elements(0);
257
+ ids.forEach((id, i) => {
258
+ setTimeout(() => engine.element(id).show(), (i + 1) * 500);
259
+ });
260
+ });
261
+
262
+ engine.on("slideChange", ({ index }) => {
263
+ const ids = engine.elements(index);
264
+ ids.forEach((id, i) => {
265
+ setTimeout(() => engine.element(id).show(), (i + 1) * 500);
266
+ });
267
+ });
268
+
269
+ engine.load({
270
+ meta: {
271
+ title: "Reveal",
272
+ elementControl: { defaultVisible: false },
273
+ initialElementState: { "s0-tag": { visible: false }, "s0-title": { visible: false }, "s0-subtitle": { visible: false } },
274
+ },
275
+ slides: [{ type: "statement", title: "Hello", subtitle: "World", tag: "Intro" }],
276
+ });
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Delay-controlled animations (vanilla JS, no Vue)
282
+
283
+ Staggered reveals and per-element animations are driven from vanilla JS using `setTimeout`, `engine.element(id).show()`, and `engine.element(id).animate()`. Register `ready` and `slideChange` **before** `engine.load(deck)`.
284
+
285
+ **Helper to reveal a slide’s elements with delay and optional animation:**
286
+
287
+ ```js
288
+ /**
289
+ * Reveal all elements of a slide with staggered delay. Pure JavaScript, no Vue.
290
+ * @param {import('lumina-slides').Lumina} engine
291
+ * @param {number} slideIndex
292
+ * @param {Object} [opts]
293
+ * @param {number} [opts.delayMs=500] - Delay between each element (ms).
294
+ * @param {boolean} [opts.animate=true] - If true, animate from { opacity: 0, y: 20 } to { opacity: 1, y: 0 }.
295
+ * @param {number} [opts.animDuration=0.5] - Animation duration in seconds.
296
+ */
297
+ function revealSlideWithDelay(engine, slideIndex, opts = {}) {
298
+ const { delayMs = 500, animate = true, animDuration = 0.5 } = opts;
299
+ const ids = engine.elements(slideIndex);
300
+ if (!ids || !ids.length) return;
301
+
302
+ ids.forEach((id, i) => {
303
+ const t = (i + 1) * delayMs;
304
+ setTimeout(() => {
305
+ const el = engine.element(id);
306
+ el.show();
307
+ if (animate) {
308
+ el.animate({
309
+ from: { opacity: 0, y: 20 },
310
+ to: { opacity: 1, y: 0 },
311
+ duration: animDuration,
312
+ ease: "power2.out",
313
+ });
314
+ }
315
+ }, t);
316
+ });
317
+ }
318
+ ```
319
+
320
+ **Full HTML + vanilla JS example (delay-controlled animations):**
321
+
322
+ ```html
323
+ <!DOCTYPE html>
324
+ <html lang="en">
325
+ <head>
326
+ <meta charset="UTF-8" />
327
+ <title>Lumina – delay-controlled animations (vanilla JS)</title>
328
+ <link rel="stylesheet" href="node_modules/lumina-slides/dist/style.css" />
329
+ </head>
330
+ <body>
331
+ <div id="app" style="width:100vw;height:100vh;"></div>
332
+ <script type="module">
333
+ import { Lumina } from "lumina-slides";
334
+
335
+ const engine = new Lumina("#app", { theme: "midnight" });
336
+
337
+ function revealSlideWithDelay(engine, slideIndex, opts = {}) {
338
+ const { delayMs = 500, animate = true, animDuration = 0.5 } = opts;
339
+ const ids = engine.elements(slideIndex);
340
+ if (!ids || !ids.length) return;
341
+ ids.forEach((id, i) => {
342
+ setTimeout(() => {
343
+ engine.element(id).show();
344
+ if (animate) {
345
+ engine.element(id).animate({
346
+ from: { opacity: 0, y: 20 },
347
+ to: { opacity: 1, y: 0 },
348
+ duration: animDuration,
349
+ ease: "power2.out",
350
+ });
351
+ }
352
+ }, (i + 1) * delayMs);
353
+ });
354
+ }
355
+
356
+ // Must register before load so callbacks run when DOM is ready
357
+ engine.on("ready", () => revealSlideWithDelay(engine, 0, { delayMs: 600, animDuration: 0.5 }));
358
+ engine.on("slideChange", ({ index }) => revealSlideWithDelay(engine, index, { delayMs: 600, animDuration: 0.5 }));
359
+
360
+ engine.load({
361
+ meta: {
362
+ title: "Staggered reveal",
363
+ elementControl: { defaultVisible: false },
364
+ initialElementState: {
365
+ "s0-tag": { visible: false },
366
+ "s0-title": { visible: false },
367
+ "s0-subtitle": { visible: false },
368
+ "s1-header": { visible: false },
369
+ "s1-features-0": { visible: false },
370
+ "s1-features-1": { visible: false },
371
+ },
372
+ },
373
+ slides: [
374
+ { type: "statement", title: "Welcome", subtitle: "Each line appears with a delay", tag: "Intro" },
375
+ { type: "features", title: "Highlights", features: [
376
+ { title: "First", desc: "Revealed after 600ms", icon: "star" },
377
+ { title: "Second", desc: "Revealed after 1200ms", icon: "zap" },
378
+ ]},
379
+ ],
380
+ });
381
+ </script>
382
+ </body>
383
+ </html>
384
+ ```
385
+
386
+ **Animation-only (no show/hide), with custom stagger:**
387
+
388
+ ```js
389
+ engine.on("ready", () => {
390
+ const ids = engine.elements(0);
391
+ ids.forEach((id, i) => {
392
+ setTimeout(() => {
393
+ engine.element(id).animate({
394
+ from: { opacity: 0, scale: 0.95 },
395
+ to: { opacity: 1, scale: 1 },
396
+ duration: 0.6,
397
+ ease: "back.out(1.2)",
398
+ });
399
+ }, i * 400);
400
+ });
401
+ });
402
+ ```
403
+
404
+ ---
405
+
406
+ ## Animation options (engine and deck)
407
+
408
+ - **Engine options:** `options.animation: { enabled?: boolean, type?: 'fade'|'cascade'|'zoom', durationIn?, durationOut? }` when creating `new Lumina(selector, options)`.
409
+ - **Slide transitions** are applied by the engine; you listen to `slideChange` for timing.
410
+ - **Per-element animations** are done with `engine.element(id).animate({ from: { opacity: 0, y: 20 }, to: { opacity: 1, y: 0 }, duration: 0.5 })` from vanilla JS.
185
411
 
186
412
  ---
187
413
 
188
- ## Dónde seguir
414
+ ## Where to go next
189
415
 
190
- - **API y tipos**: los JSDoc en el código y los `.d.ts` generados.
191
- - **Guía para agentes que generan slides**: [AGENTS.md](./AGENTS.md).
192
- - **Docs y playground**: [lumina-slides](https://pailletjuanpablo.github.io/lumina-slides/).
416
+ - **API and types:** JSDoc in the code and generated `.d.ts`.
417
+ - **Guide for agents that generate slides:** [AGENTS.md](./AGENTS.md).
418
+ - **Docs and playground:** [lumina-slides](https://pailletjuanpablo.github.io/lumina-slides/).