@sphynxoid/stepwise 0.0.0-stage → 0.2.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.
package/dist/index.js ADDED
@@ -0,0 +1,1828 @@
1
+ "use client";
2
+
3
+ // src/use-steps.ts
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ function useSteps(items, opts) {
6
+ const count = typeof items === "number" ? items : items.length;
7
+ const countRef = useRef(count);
8
+ countRef.current = count;
9
+ const onChangeRef = useRef(opts?.onChange);
10
+ onChangeRef.current = opts?.onChange;
11
+ const [cursor, setCursor] = useState(() => {
12
+ const initial = opts?.initialIndex ?? 0;
13
+ return count === 0 ? -1 : Math.max(0, Math.min(initial, count - 1));
14
+ });
15
+ const index = count === 0 ? -1 : Math.max(0, Math.min(cursor, count - 1));
16
+ const indexRef = useRef(index);
17
+ indexRef.current = index;
18
+ useEffect(() => {
19
+ if (count === 0) {
20
+ if (cursor !== -1) setCursor(-1);
21
+ } else if (cursor < 0 || cursor > count - 1) {
22
+ setCursor(Math.max(0, Math.min(cursor, count - 1)));
23
+ }
24
+ }, [count, cursor]);
25
+ const go = useCallback((i) => {
26
+ const c = countRef.current;
27
+ if (c === 0) return;
28
+ const clamped = Math.max(0, Math.min(i, c - 1));
29
+ if (clamped === indexRef.current) return;
30
+ indexRef.current = clamped;
31
+ setCursor(clamped);
32
+ onChangeRef.current?.(clamped);
33
+ }, []);
34
+ const next = useCallback(() => {
35
+ go(indexRef.current + 1);
36
+ }, [go]);
37
+ const prev = useCallback(() => {
38
+ go(indexRef.current - 1);
39
+ }, [go]);
40
+ const first = useCallback(() => {
41
+ go(0);
42
+ }, [go]);
43
+ return useMemo(
44
+ () => ({
45
+ count,
46
+ index,
47
+ atStart: index <= 0,
48
+ atEnd: count > 0 && index >= count - 1,
49
+ canNext: count > 0 && index < count - 1,
50
+ canPrev: count > 0 && index > 0,
51
+ go,
52
+ next,
53
+ prev,
54
+ first
55
+ }),
56
+ [count, index, go, next, prev, first]
57
+ );
58
+ }
59
+
60
+ // src/use-step-player.ts
61
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
62
+ function useStepPlayer({
63
+ total,
64
+ baseDelayMs = 900,
65
+ autoPlay = false,
66
+ engine,
67
+ syncHash,
68
+ onEnd,
69
+ respectReducedMotion = true
70
+ }) {
71
+ const core = useSteps(total);
72
+ const { go, next: coreNext, prev: corePrev, first } = core;
73
+ const [started, setStarted] = useState2(autoPlay && total > 0);
74
+ const [playing, setPlaying] = useState2(autoPlay && total > 0);
75
+ const [speed, setSpeed] = useState2(1);
76
+ const [ended, setEnded] = useState2(false);
77
+ const endedRef = useRef2(false);
78
+ const onEndRef = useRef2(onEnd);
79
+ onEndRef.current = onEnd;
80
+ const markEnded = useCallback2(() => {
81
+ if (endedRef.current) return;
82
+ endedRef.current = true;
83
+ setEnded(true);
84
+ onEndRef.current?.();
85
+ }, []);
86
+ const clearEnded = useCallback2(() => {
87
+ if (!endedRef.current) return;
88
+ endedRef.current = false;
89
+ setEnded(false);
90
+ }, []);
91
+ const index = started && core.index >= 0 ? core.index : -1;
92
+ const atEnd = total > 0 && index >= 0 && index >= total - 1;
93
+ const totalRef = useRef2(total);
94
+ totalRef.current = total;
95
+ const cursorRef = useRef2(core.index);
96
+ cursorRef.current = core.index;
97
+ const playingRef = useRef2(playing);
98
+ playingRef.current = playing;
99
+ const atEndRef = useRef2(atEnd);
100
+ atEndRef.current = atEnd;
101
+ const engineRef = useRef2(engine);
102
+ engineRef.current = engine;
103
+ const engineApiRef = useRef2({});
104
+ useEffect2(() => {
105
+ if (!engine) return;
106
+ const handle = engine.connect({
107
+ get total() {
108
+ return totalRef.current;
109
+ },
110
+ onFrame: (f) => go(f),
111
+ onEnd: () => {
112
+ markEnded();
113
+ setPlaying(false);
114
+ }
115
+ });
116
+ engineApiRef.current = {
117
+ play: () => handle.play(),
118
+ pause: () => handle.pause(),
119
+ seek: (f) => handle.seek(f),
120
+ rate: (r) => handle.setRate(r)
121
+ };
122
+ return () => {
123
+ engineApiRef.current = {};
124
+ handle.disconnect();
125
+ };
126
+ }, [engine, go, markEnded]);
127
+ useEffect2(() => {
128
+ if (!respectReducedMotion || !autoPlay) return;
129
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
130
+ if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
131
+ setPlaying(false);
132
+ engineApiRef.current.pause?.();
133
+ }, []);
134
+ const start = useCallback2(() => {
135
+ setStarted(true);
136
+ setPlaying(true);
137
+ clearEnded();
138
+ first();
139
+ if (engineRef.current) {
140
+ engineApiRef.current.seek?.(0);
141
+ engineApiRef.current.play?.();
142
+ }
143
+ }, [first, clearEnded]);
144
+ const reset = useCallback2(() => {
145
+ setStarted(false);
146
+ setPlaying(false);
147
+ clearEnded();
148
+ if (engineRef.current) engineApiRef.current.pause?.();
149
+ }, [clearEnded]);
150
+ const seek = useCallback2(
151
+ (f) => {
152
+ const t = totalRef.current;
153
+ if (t === 0) return;
154
+ setStarted(true);
155
+ setPlaying(false);
156
+ clearEnded();
157
+ go(Math.max(0, Math.min(f, t - 1)));
158
+ if (engineRef.current) {
159
+ engineApiRef.current.pause?.();
160
+ engineApiRef.current.seek?.(f);
161
+ }
162
+ },
163
+ [go, clearEnded]
164
+ );
165
+ const next = useCallback2(
166
+ (onEmpty) => {
167
+ setPlaying(false);
168
+ const t = totalRef.current;
169
+ if (t === 0) {
170
+ onEmpty?.();
171
+ return;
172
+ }
173
+ setStarted(true);
174
+ clearEnded();
175
+ const target = Math.min(cursorRef.current + 1, t - 1);
176
+ coreNext();
177
+ if (engineRef.current) {
178
+ engineApiRef.current.pause?.();
179
+ engineApiRef.current.seek?.(target);
180
+ }
181
+ },
182
+ [coreNext, clearEnded]
183
+ );
184
+ const prev = useCallback2(() => {
185
+ setPlaying(false);
186
+ clearEnded();
187
+ const target = Math.max(0, cursorRef.current - 1);
188
+ corePrev();
189
+ if (engineRef.current) {
190
+ engineApiRef.current.pause?.();
191
+ engineApiRef.current.seek?.(target);
192
+ }
193
+ }, [corePrev, clearEnded]);
194
+ const toggle = useCallback2(
195
+ (onEmpty) => {
196
+ if (totalRef.current === 0) {
197
+ onEmpty?.();
198
+ return;
199
+ }
200
+ if (!playingRef.current && atEndRef.current) {
201
+ start();
202
+ return;
203
+ }
204
+ const nextPlaying = !playingRef.current;
205
+ setPlaying(nextPlaying);
206
+ setStarted(true);
207
+ if (engineRef.current) {
208
+ if (nextPlaying) engineApiRef.current.play?.();
209
+ else engineApiRef.current.pause?.();
210
+ }
211
+ },
212
+ [start]
213
+ );
214
+ const changeSpeed = useCallback2((s) => {
215
+ setSpeed(s);
216
+ if (engineRef.current) engineApiRef.current.rate?.(s);
217
+ }, []);
218
+ useEffect2(() => {
219
+ clearEnded();
220
+ }, [total, clearEnded]);
221
+ useEffect2(() => {
222
+ if (engineRef.current) return;
223
+ if (!playing || !started || total === 0) return;
224
+ if (core.index >= total - 1) {
225
+ markEnded();
226
+ setPlaying(false);
227
+ return;
228
+ }
229
+ const id = setTimeout(() => coreNext(), baseDelayMs / speed);
230
+ return () => clearTimeout(id);
231
+ }, [playing, started, core.index, coreNext, total, speed, baseDelayMs, markEnded]);
232
+ const lastWrittenHash = useRef2("");
233
+ const readHash = useCallback2(() => {
234
+ if (!syncHash || typeof window === "undefined" || total === 0) return;
235
+ const hash = window.location.hash;
236
+ if (hash === lastWrittenHash.current) return;
237
+ const value = readHashParam(hash, syncHash);
238
+ if (value === null || !/^\d+$/.test(value)) return;
239
+ const f = Number(value);
240
+ if (f < total) {
241
+ setStarted(true);
242
+ go(f);
243
+ }
244
+ }, [syncHash, total, go]);
245
+ useEffect2(() => {
246
+ readHash();
247
+ const onHashChange = () => readHash();
248
+ window.addEventListener("hashchange", onHashChange);
249
+ return () => window.removeEventListener("hashchange", onHashChange);
250
+ }, [readHash]);
251
+ useEffect2(() => {
252
+ if (!syncHash || index < 0 || typeof window === "undefined") return;
253
+ const next2 = writeHashParam(window.location.hash, syncHash, String(index));
254
+ if (window.location.hash !== next2) {
255
+ lastWrittenHash.current = next2;
256
+ window.history.replaceState(null, "", next2);
257
+ }
258
+ }, [syncHash, index]);
259
+ return useMemo2(
260
+ () => ({
261
+ total,
262
+ frame: index,
263
+ index,
264
+ playing,
265
+ speed,
266
+ atEnd,
267
+ ended,
268
+ start,
269
+ reset,
270
+ seek,
271
+ next,
272
+ prev,
273
+ toggle,
274
+ setPlaying,
275
+ setSpeed: changeSpeed
276
+ }),
277
+ [total, index, playing, speed, atEnd, ended, start, reset, seek, next, prev, toggle, changeSpeed]
278
+ );
279
+ }
280
+ function hashParts(hash) {
281
+ const body = hash.startsWith("#") ? hash.slice(1) : hash;
282
+ return body ? body.split("&") : [];
283
+ }
284
+ function readHashParam(hash, key) {
285
+ for (const part of hashParts(hash)) {
286
+ const eq = part.indexOf("=");
287
+ if (eq > 0 && part.slice(0, eq) === key) return part.slice(eq + 1);
288
+ }
289
+ return null;
290
+ }
291
+ function writeHashParam(hash, key, value) {
292
+ const parts = hashParts(hash);
293
+ const pair = `${key}=${value}`;
294
+ const i = parts.findIndex((p) => p.startsWith(`${key}=`));
295
+ if (i >= 0) parts[i] = pair;
296
+ else parts.push(pair);
297
+ return `#${parts.join("&")}`;
298
+ }
299
+
300
+ // src/use-trace.ts
301
+ import { useCallback as useCallback3, useMemo as useMemo3, useRef as useRef3 } from "react";
302
+ function useTrace(steps, opts) {
303
+ const player = useStepPlayer({ ...opts, total: steps.length });
304
+ const stepsRef = useRef3(steps);
305
+ stepsRef.current = steps;
306
+ const frame = steps.length === 0 ? -1 : Math.max(0, Math.min(player.index, steps.length - 1));
307
+ const step = frame >= 0 ? steps[frame] : void 0;
308
+ const toneAt = useCallback3((i) => stepsRef.current[i]?.tone ?? "neutral", []);
309
+ const titleAt = useCallback3((i) => stepsRef.current[i]?.label ?? `step ${i + 1}`, []);
310
+ return useMemo3(() => ({ player, step, frame, toneAt, titleAt }), [player, step, frame, toneAt, titleAt]);
311
+ }
312
+
313
+ // src/create-trace.ts
314
+ function createTrace() {
315
+ const frames = [];
316
+ const builder = {
317
+ step(step) {
318
+ frames.push(step);
319
+ return builder;
320
+ },
321
+ get length() {
322
+ return frames.length;
323
+ },
324
+ last() {
325
+ return frames[frames.length - 1];
326
+ },
327
+ build() {
328
+ return frames.slice();
329
+ }
330
+ };
331
+ return builder;
332
+ }
333
+
334
+ // src/use-synced-tracks.ts
335
+ import { useCallback as useCallback4, useMemo as useMemo4, useRef as useRef4 } from "react";
336
+ function alignTracks(tracks, key) {
337
+ const names = Object.keys(tracks);
338
+ const lens = names.map((n) => tracks[n].length);
339
+ const rows = [];
340
+ const holdCell = (len, at) => ({ index: len === 0 ? -1 : Math.min(at, len - 1), held: true });
341
+ if (!key) {
342
+ const length = Math.max(0, ...lens);
343
+ for (let i = 0; i < length; i++) {
344
+ const row = {};
345
+ names.forEach((n, t) => {
346
+ row[n] = i < lens[t] ? { index: i, held: false } : holdCell(lens[t], i);
347
+ });
348
+ rows.push(row);
349
+ }
350
+ return rows;
351
+ }
352
+ const keyed = names.map(
353
+ (n) => tracks[n].map((step, i) => {
354
+ const k = key(step, i, n);
355
+ return k == null ? null : k;
356
+ })
357
+ );
358
+ const qualified = keyed.map((ks) => {
359
+ const seen = /* @__PURE__ */ new Map();
360
+ return ks.map((k) => {
361
+ if (k === null) return null;
362
+ const n = seen.get(k) ?? 0;
363
+ seen.set(k, n + 1);
364
+ return `${k}\0${n}`;
365
+ });
366
+ });
367
+ const where = /* @__PURE__ */ new Map();
368
+ qualified.forEach(
369
+ (ks, t) => ks.forEach((k, i) => {
370
+ if (k === null) return;
371
+ let m = where.get(k);
372
+ if (!m) where.set(k, m = /* @__PURE__ */ new Map());
373
+ m.set(t, i);
374
+ })
375
+ );
376
+ for (const [k, m] of where) if (m.size < 2) where.delete(k);
377
+ const pos = names.map(() => 0);
378
+ while (pos.some((p, t) => p < lens[t])) {
379
+ const live = names.map((_, t) => pos[t] < lens[t]);
380
+ const blocked = names.map((_, t) => {
381
+ if (!live[t]) return false;
382
+ const k = qualified[t][pos[t]];
383
+ const m = k === null ? void 0 : where.get(k);
384
+ if (!m) return false;
385
+ for (const [u, at] of m) if (u !== t && at > pos[u]) return true;
386
+ return false;
387
+ });
388
+ const anyFree = live.some((l, t) => l && !blocked[t]);
389
+ const row = {};
390
+ names.forEach((n, t) => {
391
+ if (live[t] && (!blocked[t] || !anyFree)) {
392
+ row[n] = { index: pos[t], held: false };
393
+ pos[t]++;
394
+ } else {
395
+ row[n] = holdCell(lens[t], Math.max(0, pos[t] - 1));
396
+ }
397
+ });
398
+ rows.push(row);
399
+ }
400
+ return rows;
401
+ }
402
+ var TONE_RANK = ["bad", "good", "write", "accent", "neutral", "idle"];
403
+ function useSyncedTracks(tracks, opts) {
404
+ const { key, ...playerOpts } = opts ?? {};
405
+ const keyRef = useRef4(key);
406
+ keyRef.current = key;
407
+ const cache = useRef4(null);
408
+ const names = Object.keys(tracks);
409
+ const arrays = names.map((n) => tracks[n]);
410
+ const c = cache.current;
411
+ if (!c || c.keyed !== !!key || c.names.length !== names.length || names.some((n, i) => c.names[i] !== n || c.arrays[i] !== arrays[i])) {
412
+ cache.current = { names, arrays, keyed: !!key, rows: alignTracks(tracks, keyRef.current) };
413
+ }
414
+ const rows = cache.current.rows;
415
+ const player = useStepPlayer({ ...playerOpts, total: rows.length });
416
+ const tracksRef = useRef4(tracks);
417
+ tracksRef.current = tracks;
418
+ const rowsRef = useRef4(rows);
419
+ rowsRef.current = rows;
420
+ const frame = rows.length === 0 ? -1 : Math.max(0, Math.min(player.index, rows.length - 1));
421
+ const row = frame >= 0 ? rows[frame] : void 0;
422
+ const aligned = cache.current;
423
+ const view = useMemo4(() => {
424
+ const out = {};
425
+ aligned.names.forEach((n, t) => {
426
+ const list = aligned.arrays[t];
427
+ const cell = row?.[n];
428
+ const index = cell ? cell.index : list.length === 0 ? -1 : 0;
429
+ out[n] = {
430
+ step: index >= 0 ? list[index] : void 0,
431
+ index,
432
+ held: cell?.held ?? false,
433
+ done: list.length > 0 && index === list.length - 1,
434
+ length: list.length
435
+ };
436
+ });
437
+ return out;
438
+ }, [aligned, row]);
439
+ const moved = useCallback4((i) => {
440
+ const r = rowsRef.current[i];
441
+ if (!r) return [];
442
+ return Object.keys(r).filter((n) => !r[n].held).map((n) => ({ name: n, step: tracksRef.current[n]?.[r[n].index] })).filter((m) => !!m.step);
443
+ }, []);
444
+ const toneAt = useCallback4(
445
+ (i) => {
446
+ const tones = moved(i).map((m) => m.step.tone);
447
+ return TONE_RANK.find((t) => tones.includes(t)) ?? "neutral";
448
+ },
449
+ [moved]
450
+ );
451
+ const titleAt = useCallback4(
452
+ (i) => {
453
+ const parts = moved(i).map((m) => `${m.name}: ${m.step.label}`);
454
+ return parts.length ? parts.join(" \xB7 ") : `frame ${i + 1}`;
455
+ },
456
+ [moved]
457
+ );
458
+ return useMemo4(
459
+ () => ({ player, frame, total: rows.length, tracks: view, rows, toneAt, titleAt }),
460
+ [player, frame, rows, view, toneAt, titleAt]
461
+ );
462
+ }
463
+
464
+ // src/use-lesson-stream.ts
465
+ import { useCallback as useCallback5, useEffect as useEffect4, useLayoutEffect, useMemo as useMemo5, useRef as useRef5, useState as useState4 } from "react";
466
+
467
+ // src/use-prefers-reduced-motion.ts
468
+ import { useEffect as useEffect3, useState as useState3 } from "react";
469
+ function usePrefersReducedMotion() {
470
+ const [reduced, setReduced] = useState3(false);
471
+ useEffect3(() => {
472
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
473
+ setReduced(mq.matches);
474
+ const onChange = (e) => setReduced(e.matches);
475
+ mq.addEventListener("change", onChange);
476
+ return () => mq.removeEventListener("change", onChange);
477
+ }, []);
478
+ return reduced;
479
+ }
480
+
481
+ // src/use-lesson-stream.ts
482
+ var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect4;
483
+ function useLessonStream(count, opts = {}) {
484
+ const { radius = 1, approachBelow = 600, approachAbove = 300, scrollToSeed = true } = opts;
485
+ const last = count - 1;
486
+ const seed = count === 0 ? -1 : Math.max(0, Math.min(opts.seed ?? 0, last));
487
+ const reducedMotion = usePrefersReducedMotion();
488
+ const core = useSteps(count, { initialIndex: Math.max(0, seed) });
489
+ const coreGoRef = useRef5(core.go);
490
+ coreGoRef.current = core.go;
491
+ const activeRef = useRef5(core.index);
492
+ activeRef.current = core.index;
493
+ const [range, setRange] = useState4(() => ({
494
+ first: Math.max(0, seed - radius),
495
+ last: Math.min(last, seed + radius)
496
+ }));
497
+ const first = Math.max(0, Math.min(range.first, last));
498
+ const lastMounted = Math.min(range.last, last);
499
+ const rangeRef = useRef5({ first, last: lastMounted });
500
+ rangeRef.current = { first, last: lastMounted };
501
+ const countRef = useRef5(count);
502
+ countRef.current = count;
503
+ const reducedRef = useRef5(reducedMotion);
504
+ reducedRef.current = reducedMotion;
505
+ const onActiveRef = useRef5(opts.onActiveChange);
506
+ onActiveRef.current = opts.onActiveChange;
507
+ const optsRef = useRef5({ approachBelow, approachAbove });
508
+ optsRef.current = { approachBelow, approachAbove };
509
+ const blocks = useRef5(/* @__PURE__ */ new Map());
510
+ const refCache = useRef5(/* @__PURE__ */ new Map());
511
+ const refFor = useCallback5((i) => {
512
+ let fn = refCache.current.get(i);
513
+ if (!fn) {
514
+ fn = (el) => {
515
+ if (el) blocks.current.set(i, el);
516
+ else blocks.current.delete(i);
517
+ };
518
+ refCache.current.set(i, fn);
519
+ }
520
+ return fn;
521
+ }, []);
522
+ const causeRef = useRef5("scroll");
523
+ const expandTo = useCallback5((i, cause) => {
524
+ const r = rangeRef.current;
525
+ if (i >= r.first && i <= r.last) return;
526
+ causeRef.current = cause;
527
+ const grown = { first: Math.min(r.first, i), last: Math.max(r.last, i) };
528
+ rangeRef.current = grown;
529
+ setRange(grown);
530
+ }, []);
531
+ const scheduleRef = useRef5(null);
532
+ useEffect4(() => {
533
+ if (count === 0) return;
534
+ let raf = 0;
535
+ const update = () => {
536
+ raf = 0;
537
+ if (blocks.current.size === 0) return;
538
+ const mid = window.innerHeight / 2;
539
+ let best = -1;
540
+ for (const [i, el] of blocks.current) {
541
+ const rect = el.getBoundingClientRect();
542
+ if (rect.top <= mid && rect.bottom >= mid) best = Math.max(best, i);
543
+ }
544
+ if (best === -1) best = Math.min(...blocks.current.keys());
545
+ if (best !== activeRef.current) {
546
+ activeRef.current = best;
547
+ coreGoRef.current(best);
548
+ onActiveRef.current?.(best);
549
+ }
550
+ const { first: f, last: l } = rangeRef.current;
551
+ const { approachBelow: below, approachAbove: above } = optsRef.current;
552
+ const lastEl = blocks.current.get(l);
553
+ if (lastEl && l < countRef.current - 1 && lastEl.getBoundingClientRect().bottom - window.innerHeight < below) {
554
+ expandTo(l + 1, "scroll");
555
+ }
556
+ const firstEl = blocks.current.get(f);
557
+ if (firstEl && f > 0 && firstEl.getBoundingClientRect().top > -above) {
558
+ expandTo(f - 1, "scroll");
559
+ }
560
+ };
561
+ const onScroll = () => {
562
+ if (!raf) raf = requestAnimationFrame(update);
563
+ };
564
+ scheduleRef.current = onScroll;
565
+ update();
566
+ window.addEventListener("scroll", onScroll, { passive: true });
567
+ window.addEventListener("resize", onScroll);
568
+ return () => {
569
+ scheduleRef.current = null;
570
+ window.removeEventListener("scroll", onScroll);
571
+ window.removeEventListener("resize", onScroll);
572
+ if (raf) cancelAnimationFrame(raf);
573
+ };
574
+ }, [count, expandTo]);
575
+ useEffect4(() => {
576
+ scheduleRef.current?.();
577
+ }, [first, lastMounted]);
578
+ const firstRef = useRef5(first);
579
+ useIsoLayoutEffect(() => {
580
+ if (first < firstRef.current && causeRef.current === "scroll") {
581
+ let height = 0;
582
+ for (let i = first; i < firstRef.current; i++) {
583
+ height += blocks.current.get(i)?.getBoundingClientRect().height ?? 0;
584
+ }
585
+ if (height) window.scrollBy({ top: height, behavior: "auto" });
586
+ }
587
+ causeRef.current = "scroll";
588
+ firstRef.current = first;
589
+ }, [first]);
590
+ useIsoLayoutEffect(() => {
591
+ if (!scrollToSeed || seed <= 0) return;
592
+ blocks.current.get(seed)?.scrollIntoView({ block: "start", behavior: "auto" });
593
+ }, []);
594
+ const [nav, setNav] = useState4(null);
595
+ useEffect4(() => {
596
+ if (!nav) return;
597
+ const raf = requestAnimationFrame(
598
+ () => blocks.current.get(nav.target)?.scrollIntoView({ behavior: reducedRef.current ? "auto" : "smooth", block: "start" })
599
+ );
600
+ return () => cancelAnimationFrame(raf);
601
+ }, [nav]);
602
+ const goTo = useCallback5(
603
+ (i) => {
604
+ const c = countRef.current;
605
+ if (c === 0) return;
606
+ const target = Math.max(0, Math.min(i, c - 1));
607
+ expandTo(target, "nav");
608
+ setNav({ target });
609
+ },
610
+ [expandTo]
611
+ );
612
+ const next = useCallback5(() => goTo(activeRef.current + 1), [goTo]);
613
+ const prev = useCallback5(() => goTo(activeRef.current - 1), [goTo]);
614
+ const mounted = useMemo5(() => {
615
+ const out = [];
616
+ for (let i = first; i <= lastMounted; i++) out.push(i);
617
+ return out;
618
+ }, [first, lastMounted]);
619
+ return useMemo5(
620
+ () => ({
621
+ active: core.index,
622
+ seed,
623
+ mounted,
624
+ complete: count > 0 && lastMounted === last,
625
+ refFor,
626
+ goTo,
627
+ next,
628
+ prev,
629
+ reducedMotion
630
+ }),
631
+ [core.index, seed, mounted, count, lastMounted, last, refFor, goTo, next, prev, reducedMotion]
632
+ );
633
+ }
634
+ function useFadeIn(initiallyVisible = false) {
635
+ const reduced = usePrefersReducedMotion();
636
+ const [shown, setShown] = useState4(initiallyVisible);
637
+ useEffect4(() => {
638
+ if (shown) return;
639
+ if (reduced) {
640
+ setShown(true);
641
+ return;
642
+ }
643
+ const raf = requestAnimationFrame(() => setShown(true));
644
+ return () => cancelAnimationFrame(raf);
645
+ }, [shown, reduced]);
646
+ return shown;
647
+ }
648
+
649
+ // src/use-guided-tour.ts
650
+ import { useCallback as useCallback6, useEffect as useEffect5, useMemo as useMemo6, useRef as useRef6, useState as useState5 } from "react";
651
+ function useGuidedTour(steps, opts) {
652
+ const [running, setRunning] = useState5(false);
653
+ const runningRef = useRef6(running);
654
+ runningRef.current = running;
655
+ const core = useSteps(steps.length);
656
+ const stepsRef = useRef6(steps);
657
+ stepsRef.current = steps;
658
+ const driverRef = useRef6(opts?.driver);
659
+ driverRef.current = opts?.driver;
660
+ const coreGoRef = useRef6(core.go);
661
+ coreGoRef.current = core.go;
662
+ const cursorRef = useRef6(core.index);
663
+ cursorRef.current = core.index;
664
+ const isSkipped = useCallback6(
665
+ (i) => stepsRef.current[i]?.when?.() === false,
666
+ []
667
+ );
668
+ const settle = useCallback6(
669
+ (start2, dir) => {
670
+ const len = stepsRef.current.length;
671
+ let i = start2;
672
+ while (i >= 0 && i < len && isSkipped(i)) i += dir;
673
+ if (i >= 0 && i < len) return i;
674
+ if (dir === 1) return null;
675
+ for (let j = 0; j < len; j++) if (!isSkipped(j)) return j;
676
+ return null;
677
+ },
678
+ [isSkipped]
679
+ );
680
+ const arrive = useCallback6(
681
+ (i, dir) => {
682
+ const target = settle(i, dir);
683
+ if (target === null) {
684
+ setRunning(false);
685
+ return;
686
+ }
687
+ setRunning(true);
688
+ coreGoRef.current(target);
689
+ },
690
+ [settle]
691
+ );
692
+ const goTo = useCallback6(
693
+ (i) => {
694
+ if (stepsRef.current.length === 0) return;
695
+ arrive(Math.max(0, Math.min(i, stepsRef.current.length - 1)), 1);
696
+ },
697
+ [arrive]
698
+ );
699
+ const goToId = useCallback6(
700
+ (id) => {
701
+ const i = stepsRef.current.findIndex((s) => s.id === id);
702
+ if (i >= 0) goTo(i);
703
+ },
704
+ [goTo]
705
+ );
706
+ const index = running ? core.index : null;
707
+ useEffect5(() => {
708
+ if (!running) return;
709
+ stepsRef.current[core.index]?.onEnter?.();
710
+ }, [running, core.index]);
711
+ useEffect5(() => {
712
+ const driver = driverRef.current;
713
+ if (!driver) return;
714
+ if (!running) {
715
+ driver.hide();
716
+ return;
717
+ }
718
+ const step = stepsRef.current[core.index];
719
+ if (!step) {
720
+ driver.hide();
721
+ return;
722
+ }
723
+ driver.show(step, {
724
+ index: core.index,
725
+ total: stepsRef.current.length,
726
+ next: () => goTo(core.index + 1),
727
+ back: () => goTo(core.index - 1),
728
+ end: () => setRunning(false)
729
+ });
730
+ }, [running, core.index, goTo]);
731
+ useEffect5(() => () => driverRef.current?.hide(), []);
732
+ const start = useCallback6(() => {
733
+ if (stepsRef.current.length === 0) return;
734
+ arrive(0, 1);
735
+ }, [arrive]);
736
+ const end = useCallback6(() => setRunning(false), []);
737
+ const resolveNext = useCallback6((i) => {
738
+ const list = stepsRef.current;
739
+ const edge = list[i]?.next?.();
740
+ if (edge != null) {
741
+ const t = list.findIndex((s) => s.id === edge);
742
+ if (t >= 0) return t;
743
+ }
744
+ return i + 1;
745
+ }, []);
746
+ const next = useCallback6(() => {
747
+ if (!runningRef.current) return;
748
+ const i = cursorRef.current;
749
+ if (stepsRef.current[i]?.gate?.() === false) return;
750
+ const target = resolveNext(i);
751
+ if (target > stepsRef.current.length - 1) {
752
+ setRunning(false);
753
+ return;
754
+ }
755
+ arrive(target, 1);
756
+ }, [arrive, resolveNext]);
757
+ const back = useCallback6(() => {
758
+ if (!runningRef.current) return;
759
+ const i = cursorRef.current;
760
+ if (i <= 0) return;
761
+ arrive(i - 1, -1);
762
+ }, [arrive]);
763
+ const current = running ? stepsRef.current[core.index] ?? null : null;
764
+ const blocked = current?.gate?.() === false;
765
+ return useMemo6(
766
+ () => ({ index, current, active: running, blocked, start, end, next, back, goto: goTo, gotoId: goToId }),
767
+ [index, current, running, blocked, start, end, next, back, goTo, goToId]
768
+ );
769
+ }
770
+
771
+ // src/use-chapters.ts
772
+ import { useCallback as useCallback7, useEffect as useEffect6, useMemo as useMemo7, useRef as useRef7, useState as useState6 } from "react";
773
+
774
+ // src/flatten.ts
775
+ function flattenChapters(chapters, depth = 0) {
776
+ const out = [];
777
+ const walk = (list, depth2, parentIndex, rootIndex) => {
778
+ for (const chapter of list) {
779
+ const index = out.length;
780
+ const effectiveRoot = depth2 === 0 ? index : rootIndex;
781
+ out.push({ chapter, depth: depth2, rootIndex: effectiveRoot, parentIndex });
782
+ if (chapter.children?.length) walk(chapter.children, depth2 + 1, index, effectiveRoot);
783
+ }
784
+ };
785
+ walk(chapters, depth, -1, -1);
786
+ return out;
787
+ }
788
+
789
+ // src/use-chapters.ts
790
+ function useChapters(chapters) {
791
+ const [mounted, setMounted] = useState6(false);
792
+ const flat = useMemo7(() => flattenChapters(chapters), [chapters]);
793
+ const flatRef = useRef7(flat);
794
+ flatRef.current = flat;
795
+ const [active, setActive] = useState6(0);
796
+ const activeRef = useRef7(0);
797
+ activeRef.current = active;
798
+ const core = useSteps(flat.length, { initialIndex: 0 });
799
+ const coreGoRef = useRef7(core.go);
800
+ coreGoRef.current = core.go;
801
+ useEffect6(() => {
802
+ if (!mounted) return;
803
+ let raf = 0;
804
+ const update = () => {
805
+ raf = 0;
806
+ const flat2 = flatRef.current;
807
+ const mid = window.innerHeight / 2;
808
+ let best = -1;
809
+ for (let i = 0; i < flat2.length; i++) {
810
+ const el = document.getElementById(flat2[i].chapter.id);
811
+ if (!el) continue;
812
+ const rect = el.getBoundingClientRect();
813
+ if (rect.top <= mid && rect.bottom >= mid) best = i;
814
+ }
815
+ if (best === -1) {
816
+ for (let i = 0; i < flat2.length; i++) {
817
+ const el = document.getElementById(flat2[i].chapter.id);
818
+ if (el && el.getBoundingClientRect().top <= mid) best = i;
819
+ }
820
+ if (best === -1) best = 0;
821
+ }
822
+ if (best !== activeRef.current) {
823
+ setActive(best);
824
+ coreGoRef.current(best);
825
+ }
826
+ };
827
+ const onScroll = () => {
828
+ if (!raf) raf = requestAnimationFrame(update);
829
+ };
830
+ update();
831
+ window.addEventListener("scroll", onScroll, { passive: true });
832
+ window.addEventListener("resize", onScroll);
833
+ return () => {
834
+ window.removeEventListener("scroll", onScroll);
835
+ window.removeEventListener("resize", onScroll);
836
+ if (raf) cancelAnimationFrame(raf);
837
+ };
838
+ }, [mounted, flat.length]);
839
+ useEffect6(() => setMounted(true), []);
840
+ const goTo = useCallback7((i) => {
841
+ const entry = flatRef.current[i];
842
+ if (!entry) return;
843
+ setActive(i);
844
+ coreGoRef.current(i);
845
+ const el = document.getElementById(entry.chapter.id);
846
+ if (!el) return;
847
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
848
+ }, []);
849
+ const next = useCallback7(() => {
850
+ if (activeRef.current < flatRef.current.length - 1) goTo(activeRef.current + 1);
851
+ }, [goTo]);
852
+ const prev = useCallback7(() => {
853
+ if (activeRef.current > 0) goTo(activeRef.current - 1);
854
+ }, [goTo]);
855
+ return useMemo7(() => ({ active: core.index, goTo, next, prev }), [core.index, goTo, next, prev]);
856
+ }
857
+
858
+ // src/use-global-keys.ts
859
+ import { useEffect as useEffect7, useRef as useRef8 } from "react";
860
+ var DEFAULT_IGNORED_TAGS = ["INPUT", "TEXTAREA", "SELECT", "BUTTON"];
861
+ function useGlobalKeys(map, opts) {
862
+ const mapRef = useRef8(map);
863
+ mapRef.current = map;
864
+ const ignoreRef = useRef8(new Set(opts?.ignoreTags ?? DEFAULT_IGNORED_TAGS));
865
+ const enabledRef = useRef8(opts?.enabled ?? true);
866
+ enabledRef.current = opts?.enabled ?? true;
867
+ useEffect7(() => {
868
+ const onKey = (e) => {
869
+ const gate = enabledRef.current;
870
+ const enabled = typeof gate === "function" ? gate() : gate;
871
+ if (!enabled && e.key !== "Escape") return;
872
+ const tag = e.target?.tagName;
873
+ if (e.key !== "Escape" && tag && ignoreRef.current.has(tag)) return;
874
+ const fn = mapRef.current[e.key] ?? mapRef.current[e.key.toLowerCase()];
875
+ if (!fn) return;
876
+ if (e.key !== "Escape") e.preventDefault();
877
+ fn(e);
878
+ };
879
+ window.addEventListener("keydown", onKey);
880
+ return () => window.removeEventListener("keydown", onKey);
881
+ }, []);
882
+ }
883
+
884
+ // src/use-discoveries.ts
885
+ import { useCallback as useCallback8, useEffect as useEffect8, useMemo as useMemo8, useRef as useRef9, useState as useState7 } from "react";
886
+ function localStorageStore(key = "stepwise.discoveries") {
887
+ return {
888
+ read() {
889
+ if (typeof window === "undefined") return [];
890
+ try {
891
+ const parsed = JSON.parse(window.localStorage.getItem(key) ?? "[]");
892
+ return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
893
+ } catch {
894
+ return [];
895
+ }
896
+ },
897
+ write(ids) {
898
+ if (typeof window === "undefined") return;
899
+ try {
900
+ window.localStorage.setItem(key, JSON.stringify(ids));
901
+ } catch {
902
+ }
903
+ }
904
+ };
905
+ }
906
+ function cookieStore(key = "stepwise.discoveries") {
907
+ const read = () => {
908
+ if (typeof document === "undefined") return [];
909
+ const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${key}=([^;]*)`));
910
+ if (!match) return [];
911
+ try {
912
+ const parsed = JSON.parse(decodeURIComponent(match[1]));
913
+ return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
914
+ } catch {
915
+ return [];
916
+ }
917
+ };
918
+ return {
919
+ read,
920
+ write(ids) {
921
+ if (typeof document === "undefined") return;
922
+ document.cookie = `${key}=${encodeURIComponent(JSON.stringify(ids))}; max-age=31536000; path=/; SameSite=Lax`;
923
+ }
924
+ };
925
+ }
926
+ function memoryStore(seed = []) {
927
+ let ids = [...seed];
928
+ return {
929
+ read: () => [...ids],
930
+ write(next) {
931
+ ids = [...next];
932
+ }
933
+ };
934
+ }
935
+ function useDiscoveries(opts) {
936
+ const defaultStoreRef = useRef9(null);
937
+ const storeRef = useRef9(null);
938
+ if (opts?.store) {
939
+ storeRef.current = opts.store;
940
+ } else {
941
+ if (!defaultStoreRef.current) defaultStoreRef.current = localStorageStore(opts?.key);
942
+ storeRef.current = defaultStoreRef.current;
943
+ }
944
+ const [ids, setIds] = useState7(() => /* @__PURE__ */ new Set());
945
+ const [ready, setReady] = useState7(false);
946
+ const idsRef = useRef9(ids);
947
+ idsRef.current = ids;
948
+ useEffect8(() => {
949
+ setIds(new Set(storeRef.current.read()));
950
+ setReady(true);
951
+ }, []);
952
+ const markDiscovered = useCallback8((id) => {
953
+ const additions = Array.isArray(id) ? id : [id];
954
+ const merged = new Set(idsRef.current);
955
+ let changed = false;
956
+ for (const x of additions) {
957
+ if (!merged.has(x)) {
958
+ merged.add(x);
959
+ changed = true;
960
+ }
961
+ }
962
+ if (!changed) return;
963
+ idsRef.current = merged;
964
+ setIds(merged);
965
+ storeRef.current.write([...merged]);
966
+ }, []);
967
+ const reset = useCallback8(() => {
968
+ const empty = /* @__PURE__ */ new Set();
969
+ idsRef.current = empty;
970
+ setIds(empty);
971
+ storeRef.current.write([]);
972
+ }, []);
973
+ const isDiscovered = useCallback8((id) => idsRef.current.has(id), []);
974
+ return useMemo8(
975
+ () => ({ discovered: ids, ready, isDiscovered, markDiscovered, reset }),
976
+ [ids, ready, isDiscovered, markDiscovered, reset]
977
+ );
978
+ }
979
+
980
+ // src/use-focus-trap.ts
981
+ import { useEffect as useEffect9, useRef as useRef10 } from "react";
982
+ function useFocusTrap(active) {
983
+ const ref = useRef10(null);
984
+ useEffect9(() => {
985
+ if (!active) return;
986
+ const container = ref.current;
987
+ if (!container) return;
988
+ const previous = document.activeElement;
989
+ container.focus({ preventScroll: true });
990
+ const onKey = (e) => {
991
+ if (e.key !== "Tab") return;
992
+ const focusables = Array.from(
993
+ container.querySelectorAll(
994
+ 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
995
+ )
996
+ ).filter((el) => el.offsetParent !== null || el === container);
997
+ if (focusables.length === 0) return;
998
+ const first = focusables[0];
999
+ const last = focusables[focusables.length - 1];
1000
+ const current = document.activeElement;
1001
+ if (e.shiftKey && (current === first || !container.contains(current))) {
1002
+ e.preventDefault();
1003
+ last.focus();
1004
+ } else if (!e.shiftKey && (current === last || !container.contains(current))) {
1005
+ e.preventDefault();
1006
+ first.focus();
1007
+ }
1008
+ };
1009
+ document.addEventListener("keydown", onKey, true);
1010
+ return () => {
1011
+ document.removeEventListener("keydown", onKey, true);
1012
+ previous?.focus({ preventScroll: true });
1013
+ };
1014
+ }, [active]);
1015
+ return ref;
1016
+ }
1017
+
1018
+ // src/glossary.tsx
1019
+ import {
1020
+ createContext,
1021
+ useCallback as useCallback9,
1022
+ useContext,
1023
+ useEffect as useEffect10,
1024
+ useId,
1025
+ useLayoutEffect as useLayoutEffect2,
1026
+ useMemo as useMemo9,
1027
+ useRef as useRef11,
1028
+ useState as useState8
1029
+ } from "react";
1030
+ import { createPortal } from "react-dom";
1031
+
1032
+ // src/cn.ts
1033
+ import { clsx } from "clsx";
1034
+ import { twMerge } from "tailwind-merge";
1035
+ function cn(...inputs) {
1036
+ return twMerge(clsx(inputs));
1037
+ }
1038
+
1039
+ // src/theme.ts
1040
+ var defaultTheme = {
1041
+ toneSegment: {
1042
+ neutral: "bg-white/10",
1043
+ accent: "bg-accent/70",
1044
+ good: "bg-primary",
1045
+ bad: "bg-destructive/70",
1046
+ write: "bg-foreground/30",
1047
+ idle: "bg-white/5"
1048
+ },
1049
+ toneChip: {
1050
+ neutral: "text-foreground/50 border-white/10",
1051
+ accent: "text-accent border-accent/40",
1052
+ good: "text-primary border-primary/40",
1053
+ bad: "text-destructive border-destructive/40",
1054
+ write: "text-foreground/70 border-white/20",
1055
+ idle: "text-foreground/30 border-white/10"
1056
+ },
1057
+ buttonBase: "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-sm font-mono transition-colors duration-200 disabled:pointer-events-none disabled:opacity-50 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
1058
+ buttonVariants: {
1059
+ ghost: "border border-transparent text-foreground/70 hover:bg-white/5 hover:text-foreground",
1060
+ outline: "border border-primary/40 bg-background/40 text-foreground/85 hover:border-primary/70 hover:text-foreground",
1061
+ primary: "border border-primary bg-primary/90 text-primary-foreground hover:bg-primary"
1062
+ },
1063
+ surface: "border border-white/10 bg-background/85 backdrop-blur-md",
1064
+ overlay: "bg-black/70 backdrop-blur-sm",
1065
+ codeBlock: "rounded-sm border border-white/10 bg-black/40 px-2.5 py-2 font-mono text-[11px] leading-5 overflow-x-auto",
1066
+ codeMuted: "text-foreground/35",
1067
+ kbd: "font-mono text-[10px] px-1.5 py-0.5 border border-white/20 bg-white/5 rounded-sm text-foreground/80",
1068
+ spotlight: "rounded-sm ring-2 ring-primary/80 transition-all duration-300",
1069
+ muted: "text-foreground/40"
1070
+ };
1071
+
1072
+ // src/glossary.tsx
1073
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1074
+ var MAX_DEPTH = 4;
1075
+ var CARD_WIDTH = 288;
1076
+ var OPEN_DELAY_MS = 60;
1077
+ var CLOSE_GRACE_MS = 120;
1078
+ var GlossaryContext = createContext(null);
1079
+ var DepthContext = createContext(0);
1080
+ var AppendixContext = createContext(false);
1081
+ function GlossaryProvider({
1082
+ entries,
1083
+ theme = defaultTheme,
1084
+ children
1085
+ }) {
1086
+ const value = useMemo9(
1087
+ () => ({ byId: new Map(entries.map((e) => [e.id, e])), entries, theme }),
1088
+ [entries, theme]
1089
+ );
1090
+ return /* @__PURE__ */ jsx(GlossaryContext.Provider, { value, children });
1091
+ }
1092
+ function Term({ id, children, className }) {
1093
+ const ctx = useContext(GlossaryContext);
1094
+ const depth = useContext(DepthContext);
1095
+ const entry = ctx?.byId.get(id);
1096
+ const [open, setOpen] = useState8(false);
1097
+ const [pos, setPos] = useState8(null);
1098
+ const openRef = useRef11(false);
1099
+ const wrapperRef = useRef11(null);
1100
+ const triggerRef = useRef11(null);
1101
+ const cardRef = useRef11(null);
1102
+ const openTimer = useRef11(0);
1103
+ const closeTimer = useRef11(0);
1104
+ const tipId = useId();
1105
+ const display = children ?? entry?.term ?? id;
1106
+ const closeNow = useCallback9(() => {
1107
+ window.clearTimeout(openTimer.current);
1108
+ window.clearTimeout(closeTimer.current);
1109
+ openRef.current = false;
1110
+ setOpen(false);
1111
+ }, []);
1112
+ const openNow = useCallback9((focusCard = false) => {
1113
+ window.clearTimeout(closeTimer.current);
1114
+ window.clearTimeout(openTimer.current);
1115
+ openRef.current = true;
1116
+ setOpen(true);
1117
+ if (focusCard) {
1118
+ requestAnimationFrame(() => cardRef.current?.focus({ preventScroll: true }));
1119
+ }
1120
+ }, []);
1121
+ const scheduleOpen = useCallback9(() => {
1122
+ window.clearTimeout(closeTimer.current);
1123
+ if (openRef.current) return;
1124
+ window.clearTimeout(openTimer.current);
1125
+ openTimer.current = window.setTimeout(() => openNow(), OPEN_DELAY_MS);
1126
+ }, [openNow]);
1127
+ const scheduleClose = useCallback9(() => {
1128
+ window.clearTimeout(openTimer.current);
1129
+ window.clearTimeout(closeTimer.current);
1130
+ closeTimer.current = window.setTimeout(closeNow, CLOSE_GRACE_MS);
1131
+ }, [closeNow]);
1132
+ useLayoutEffect2(() => {
1133
+ if (!open) {
1134
+ setPos(null);
1135
+ return;
1136
+ }
1137
+ let raf = 0;
1138
+ const update = () => {
1139
+ raf = 0;
1140
+ const trigger = triggerRef.current;
1141
+ const card = cardRef.current;
1142
+ if (!trigger) return;
1143
+ const r = trigger.getBoundingClientRect();
1144
+ const margin = 8;
1145
+ const gap = 8;
1146
+ const cw = card?.offsetWidth ?? CARD_WIDTH;
1147
+ const ch = card?.offsetHeight ?? 160;
1148
+ let top = r.bottom + gap;
1149
+ if (top + ch > window.innerHeight - margin) {
1150
+ top = r.top - gap - ch;
1151
+ if (top < margin) top = margin;
1152
+ }
1153
+ let left = r.left;
1154
+ left = Math.max(margin, Math.min(left, window.innerWidth - cw - margin));
1155
+ setPos({ left, top });
1156
+ };
1157
+ const onMove = () => {
1158
+ if (!raf) raf = requestAnimationFrame(update);
1159
+ };
1160
+ update();
1161
+ window.addEventListener("scroll", onMove, { passive: true });
1162
+ window.addEventListener("resize", onMove);
1163
+ return () => {
1164
+ window.removeEventListener("scroll", onMove);
1165
+ window.removeEventListener("resize", onMove);
1166
+ if (raf) cancelAnimationFrame(raf);
1167
+ };
1168
+ }, [open]);
1169
+ useEffect10(() => {
1170
+ if (!open) return;
1171
+ const onPointerDown = (e) => {
1172
+ const target = e.target;
1173
+ if (wrapperRef.current?.contains(target)) return;
1174
+ if (cardRef.current?.contains(target)) return;
1175
+ closeNow();
1176
+ };
1177
+ document.addEventListener("pointerdown", onPointerDown);
1178
+ return () => document.removeEventListener("pointerdown", onPointerDown);
1179
+ }, [open, closeNow]);
1180
+ useEffect10(() => () => {
1181
+ window.clearTimeout(openTimer.current);
1182
+ window.clearTimeout(closeTimer.current);
1183
+ }, []);
1184
+ const capped = depth >= MAX_DEPTH;
1185
+ const inAppendix = useContext(AppendixContext);
1186
+ const interactive = !capped && !inAppendix;
1187
+ const t = ctx?.theme ?? defaultTheme;
1188
+ if (inAppendix) {
1189
+ const linked = ctx?.byId.get(id);
1190
+ if (!linked) return /* @__PURE__ */ jsx("span", { className, children: display });
1191
+ return /* @__PURE__ */ jsx(
1192
+ "a",
1193
+ {
1194
+ href: `#glossary-${linked.id}`,
1195
+ title: linked.term,
1196
+ className: cn("text-accent underline underline-offset-4 hover:text-primary", className),
1197
+ children: display
1198
+ }
1199
+ );
1200
+ }
1201
+ const triggerProps = interactive ? {
1202
+ role: "button",
1203
+ tabIndex: 0,
1204
+ "aria-expanded": open,
1205
+ "aria-describedby": open ? tipId : void 0,
1206
+ onMouseEnter: scheduleOpen,
1207
+ onMouseLeave: scheduleClose,
1208
+ onFocus: () => openNow(),
1209
+ onBlur: (e) => {
1210
+ const next = e.relatedTarget;
1211
+ if (next && (wrapperRef.current?.contains(next) || cardRef.current?.contains(next))) return;
1212
+ scheduleClose();
1213
+ },
1214
+ onClick: () => openNow(),
1215
+ onKeyDown: (e) => {
1216
+ if (e.key === "Escape") {
1217
+ e.stopPropagation();
1218
+ closeNow();
1219
+ } else if ((e.key === "Enter" || e.key === " ") && !open) {
1220
+ e.preventDefault();
1221
+ openNow(true);
1222
+ }
1223
+ }
1224
+ } : {};
1225
+ if (!ctx || !entry) {
1226
+ return /* @__PURE__ */ jsx("span", { className: cn("text-foreground/85", className), children: display });
1227
+ }
1228
+ return /* @__PURE__ */ jsxs(
1229
+ "span",
1230
+ {
1231
+ ref: wrapperRef,
1232
+ "data-stepwise": "",
1233
+ className: "relative inline",
1234
+ onMouseEnter: interactive ? scheduleOpen : void 0,
1235
+ onMouseLeave: interactive ? scheduleClose : void 0,
1236
+ children: [
1237
+ /* @__PURE__ */ jsx(
1238
+ "span",
1239
+ {
1240
+ ref: triggerRef,
1241
+ ...triggerProps,
1242
+ className: cn(
1243
+ "border-b border-dotted",
1244
+ interactive ? "border-primary/50 text-foreground/85 hover:text-primary focus-visible:text-primary cursor-help" : "text-foreground/70",
1245
+ className
1246
+ ),
1247
+ children: display
1248
+ }
1249
+ ),
1250
+ open && createPortal(
1251
+ /* @__PURE__ */ jsx(DepthContext.Provider, { value: depth + 1, children: /* @__PURE__ */ jsxs(
1252
+ "span",
1253
+ {
1254
+ ref: cardRef,
1255
+ "data-stepwise": "",
1256
+ role: "tooltip",
1257
+ id: tipId,
1258
+ tabIndex: -1,
1259
+ onKeyDown: (e) => {
1260
+ if (e.key === "Escape") {
1261
+ e.stopPropagation();
1262
+ closeNow();
1263
+ triggerRef.current?.focus({ preventScroll: true });
1264
+ }
1265
+ },
1266
+ onMouseEnter: interactive ? () => window.clearTimeout(closeTimer.current) : void 0,
1267
+ onMouseLeave: interactive ? scheduleClose : void 0,
1268
+ className: cn(
1269
+ "fixed z-50 block w-72 max-w-[calc(100vw-1rem)] rounded-sm p-3 shadow-2xl outline-none motion-reduce:transition-none",
1270
+ "transition-opacity duration-150",
1271
+ t.surface,
1272
+ "border-white/15 bg-popover",
1273
+ pos === null && "invisible"
1274
+ ),
1275
+ style: pos ? { left: pos.left, top: pos.top, maxHeight: "min(60vh, 20rem)", overflowY: "auto" } : { visibility: "hidden" },
1276
+ children: [
1277
+ /* @__PURE__ */ jsx("span", { className: "mb-1 block font-mono text-[11px] uppercase tracking-widest text-primary/90", children: entry.term }),
1278
+ /* @__PURE__ */ jsx("span", { className: "block text-[11px] font-mono leading-relaxed text-foreground/70", children: entry.definition })
1279
+ ]
1280
+ }
1281
+ ) }),
1282
+ document.body
1283
+ )
1284
+ ]
1285
+ }
1286
+ );
1287
+ }
1288
+ function GlossaryAppendix({
1289
+ entries,
1290
+ title = "Glossary",
1291
+ intro,
1292
+ footer,
1293
+ columns = 2,
1294
+ bare = false,
1295
+ className,
1296
+ entryClassName,
1297
+ renderEntry
1298
+ }) {
1299
+ const ctxEntries = useContext(GlossaryContext)?.entries;
1300
+ const theme = useContext(GlossaryContext)?.theme ?? defaultTheme;
1301
+ const list = entries ?? ctxEntries;
1302
+ if (!list || list.length === 0) return null;
1303
+ const grid = /* @__PURE__ */ jsx(
1304
+ "div",
1305
+ {
1306
+ className: cn(
1307
+ "grid grid-cols-1 gap-x-10 gap-y-8",
1308
+ columns === 2 ? "xl:grid-cols-2" : "grid-cols-1"
1309
+ ),
1310
+ children: list.map(
1311
+ (entry) => renderEntry ? /* @__PURE__ */ jsx("div", { id: `glossary-${entry.id}`, className: "scroll-mt-40", children: renderEntry(entry) }, entry.id) : /* @__PURE__ */ jsxs("div", { id: `glossary-${entry.id}`, className: cn("scroll-mt-40", entryClassName), children: [
1312
+ /* @__PURE__ */ jsx("h3", { className: "font-mono text-[11px] uppercase tracking-widest text-primary/90 mb-1.5", children: entry.term }),
1313
+ /* @__PURE__ */ jsx("div", { className: "text-[12px] font-mono text-foreground/60 leading-relaxed [&_strong]:text-foreground/80", children: entry.definition })
1314
+ ] }, entry.id)
1315
+ )
1316
+ }
1317
+ );
1318
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
1319
+ intro && /* @__PURE__ */ jsx("div", { className: "mb-8 text-[12px] font-mono text-foreground/55 leading-relaxed max-w-3xl [&_a]:text-accent [&_a]:underline [&_a]:underline-offset-4", children: intro }),
1320
+ grid,
1321
+ footer && /* @__PURE__ */ jsx("div", { className: "mt-8", children: footer })
1322
+ ] });
1323
+ if (bare) {
1324
+ return /* @__PURE__ */ jsx(AppendixContext.Provider, { value: true, children: body });
1325
+ }
1326
+ return /* @__PURE__ */ jsx(AppendixContext.Provider, { value: true, children: /* @__PURE__ */ jsxs("section", { "data-stepwise": "", "aria-label": title, className: cn("mt-16 border-t border-white/10 pt-10", className), children: [
1327
+ /* @__PURE__ */ jsx("p", { className: cn("text-[11px] font-mono uppercase tracking-widest mb-6", theme.muted), children: title }),
1328
+ body
1329
+ ] }) });
1330
+ }
1331
+
1332
+ // src/components.tsx
1333
+ import { useEffect as useEffect11, useState as useState9 } from "react";
1334
+ import { createPortal as createPortal2 } from "react-dom";
1335
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1336
+ var TONE_SEGMENT_CLASS = defaultTheme.toneSegment;
1337
+ var TONE_CHIP_CLASS = defaultTheme.toneChip;
1338
+ var BTN_BASE = defaultTheme.buttonBase;
1339
+ var BTN_GHOST = defaultTheme.buttonVariants.ghost;
1340
+ var BTN_OUTLINE = defaultTheme.buttonVariants.outline;
1341
+ var BTN_PRIMARY = defaultTheme.buttonVariants.primary;
1342
+ function StepButton({
1343
+ variant = "ghost",
1344
+ className,
1345
+ ...props
1346
+ }) {
1347
+ const variantClass = variant === "outline" ? BTN_OUTLINE : variant === "primary" ? BTN_PRIMARY : BTN_GHOST;
1348
+ return /* @__PURE__ */ jsx2("button", { type: "button", ...props, className: cn(BTN_BASE, variantClass, className) });
1349
+ }
1350
+ function CodeBlock({
1351
+ lines,
1352
+ theme = defaultTheme,
1353
+ className
1354
+ }) {
1355
+ return /* @__PURE__ */ jsx2("div", { "data-stepwise": "", className: cn(theme.codeBlock, className), children: lines.map((line, i) => {
1356
+ const commentStart = line.indexOf("//");
1357
+ if (commentStart === 0) {
1358
+ return /* @__PURE__ */ jsx2("div", { className: cn("whitespace-pre", theme.codeMuted), children: line }, i);
1359
+ }
1360
+ if (commentStart > 0) {
1361
+ return /* @__PURE__ */ jsxs2("div", { className: "whitespace-pre", children: [
1362
+ /* @__PURE__ */ jsx2("span", { className: "text-foreground/85", children: line.slice(0, commentStart) }),
1363
+ /* @__PURE__ */ jsx2("span", { className: theme.codeMuted, children: line.slice(commentStart) })
1364
+ ] }, i);
1365
+ }
1366
+ return /* @__PURE__ */ jsx2("div", { className: "whitespace-pre text-foreground/85", children: line }, i);
1367
+ }) });
1368
+ }
1369
+ function StepScrubber({
1370
+ count,
1371
+ index,
1372
+ toneAt,
1373
+ titleAt,
1374
+ onSeek,
1375
+ emptyHint,
1376
+ className,
1377
+ theme = defaultTheme,
1378
+ labels,
1379
+ groupOf
1380
+ }) {
1381
+ const onKey = (e) => {
1382
+ if (count === 0) return;
1383
+ if (e.key === "ArrowRight" || e.key === "ArrowUp") {
1384
+ e.preventDefault();
1385
+ e.stopPropagation();
1386
+ onSeek(Math.min(count - 1, index + 1));
1387
+ } else if (e.key === "ArrowLeft" || e.key === "ArrowDown") {
1388
+ e.preventDefault();
1389
+ e.stopPropagation();
1390
+ onSeek(Math.max(0, index - 1));
1391
+ } else if (e.key === "Home") {
1392
+ e.preventDefault();
1393
+ e.stopPropagation();
1394
+ onSeek(0);
1395
+ } else if (e.key === "End") {
1396
+ e.preventDefault();
1397
+ e.stopPropagation();
1398
+ onSeek(count - 1);
1399
+ }
1400
+ };
1401
+ if (count === 0) {
1402
+ return /* @__PURE__ */ jsx2("div", { "data-stepwise": "", className: cn("flex-1 flex items-center min-w-0", className), children: emptyHint && /* @__PURE__ */ jsx2("span", { className: cn("text-xs font-mono px-2", theme.muted), children: emptyHint }) });
1403
+ }
1404
+ const valueText = labels?.frame ? labels.frame(index) : titleAt?.(index) ?? `frame ${index + 1} of ${count}`;
1405
+ const segment = (i) => /* @__PURE__ */ jsx2(
1406
+ "div",
1407
+ {
1408
+ "aria-hidden": "true",
1409
+ title: titleAt ? titleAt(i) : `frame ${i + 1}`,
1410
+ onClick: () => onSeek(i),
1411
+ className: cn(
1412
+ "h-3.5 flex-1 min-w-[3px] rounded-[2px] transition-all cursor-pointer",
1413
+ theme.toneSegment[toneAt?.(i) ?? "neutral"],
1414
+ i === index && "ring-2 ring-primary scale-y-150"
1415
+ )
1416
+ },
1417
+ i
1418
+ );
1419
+ let rows;
1420
+ if (groupOf) {
1421
+ const groups = [];
1422
+ for (let i = 0; i < count; i++) {
1423
+ const g = groupOf(i);
1424
+ const lastGroup = groups[groups.length - 1];
1425
+ if (lastGroup && groupOf(lastGroup[0]) === g) lastGroup.push(i);
1426
+ else groups.push([i]);
1427
+ }
1428
+ rows = groups.map((units, gi) => /* @__PURE__ */ jsx2(
1429
+ "div",
1430
+ {
1431
+ "aria-hidden": "true",
1432
+ className: "flex-1 min-w-[3px] flex gap-[2px] items-center",
1433
+ style: { flexGrow: units.length },
1434
+ children: units.map(segment)
1435
+ },
1436
+ gi
1437
+ ));
1438
+ } else {
1439
+ rows = Array.from({ length: count }, (_, i) => segment(i));
1440
+ }
1441
+ return /* @__PURE__ */ jsx2(
1442
+ "div",
1443
+ {
1444
+ "data-stepwise": "",
1445
+ role: "slider",
1446
+ tabIndex: 0,
1447
+ "aria-label": labels?.scrubber ?? "Frames",
1448
+ "aria-valuemin": 1,
1449
+ "aria-valuemax": count,
1450
+ "aria-valuenow": index + 1,
1451
+ "aria-valuetext": valueText,
1452
+ "aria-orientation": "horizontal",
1453
+ onKeyDown: onKey,
1454
+ className: cn(
1455
+ "flex-1 flex items-center gap-[3px] min-w-0 cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-primary/60 rounded-sm",
1456
+ className
1457
+ ),
1458
+ children: rows
1459
+ }
1460
+ );
1461
+ }
1462
+ function PlayerBar({
1463
+ player,
1464
+ toneAt,
1465
+ titleAt,
1466
+ leading,
1467
+ onEmpty,
1468
+ emptyHint,
1469
+ className,
1470
+ theme = defaultTheme,
1471
+ groupOf,
1472
+ labels
1473
+ }) {
1474
+ const L = {
1475
+ previous: labels?.previous ?? "Previous step",
1476
+ play: labels?.play ?? "Play",
1477
+ pause: labels?.pause ?? "Pause",
1478
+ replay: labels?.replay ?? "Replay",
1479
+ next: labels?.next ?? "Next step"
1480
+ };
1481
+ const playLabel = player.playing ? L.pause : player.total > 0 && player.atEnd ? L.replay : L.play;
1482
+ return /* @__PURE__ */ jsxs2("div", { "data-stepwise": "", className: cn("flex items-center gap-2 p-2", className), children: [
1483
+ leading,
1484
+ /* @__PURE__ */ jsx2(StepButton, { variant: "ghost", onClick: () => player.prev(), className: "h-7 px-2 text-xs", "aria-label": L.previous, children: "\u2190" }),
1485
+ /* @__PURE__ */ jsx2(StepButton, { variant: "outline", onClick: () => player.toggle(onEmpty), className: "h-7 px-3.5 text-[11px]", children: playLabel }),
1486
+ /* @__PURE__ */ jsx2(StepButton, { variant: "ghost", onClick: () => player.next(onEmpty), className: "h-7 px-2 text-xs", "aria-label": L.next, children: "\u2192" }),
1487
+ /* @__PURE__ */ jsx2(
1488
+ StepScrubber,
1489
+ {
1490
+ count: player.total,
1491
+ index: player.index,
1492
+ toneAt,
1493
+ titleAt,
1494
+ onSeek: (i) => player.seek(i),
1495
+ emptyHint,
1496
+ theme,
1497
+ groupOf
1498
+ }
1499
+ ),
1500
+ /* @__PURE__ */ jsx2("span", { className: cn("text-[10px] font-mono w-11 text-right shrink-0", theme.muted), children: player.total > 0 ? `${player.index + 1}/${player.total}` : "\u2014" })
1501
+ ] });
1502
+ }
1503
+ function ChapterNav({
1504
+ chapters,
1505
+ active,
1506
+ onNavigate,
1507
+ hint,
1508
+ className,
1509
+ theme = defaultTheme
1510
+ }) {
1511
+ const flat = flattenChapters(chapters);
1512
+ const renderRow = (items, depth) => {
1513
+ const indent = "\u2014 ".repeat(depth);
1514
+ return items.map((chapter) => {
1515
+ const flatIndex = flat.findIndex((f) => f.chapter.id === chapter.id);
1516
+ const isActive = flatIndex === active;
1517
+ return /* @__PURE__ */ jsxs2("div", { className: cn("flex items-center", depth > 0 && "gap-1"), children: [
1518
+ /* @__PURE__ */ jsxs2(
1519
+ StepButton,
1520
+ {
1521
+ type: "button",
1522
+ onClick: () => onNavigate(flatIndex),
1523
+ "aria-current": isActive ? "true" : void 0,
1524
+ title: chapter.hint ?? chapter.label,
1525
+ className: cn(
1526
+ "flex items-center gap-1.5 px-2.5 py-1 rounded-sm text-[11px] font-mono border transition-colors cursor-pointer whitespace-nowrap",
1527
+ depth > 0 && "px-2 text-[10px] border-transparent",
1528
+ isActive ? depth === 0 ? "border-primary/50 text-primary bg-primary/10" : "text-primary bg-primary/5" : depth === 0 ? "border-transparent text-foreground/50 hover:text-foreground hover:border-white/15" : "text-foreground/45 hover:text-foreground"
1529
+ ),
1530
+ children: [
1531
+ depth === 0 && chapter.numbered !== false && /* @__PURE__ */ jsx2("span", { className: cn("text-[9px]", isActive ? "text-primary/80" : "text-foreground/30"), children: String(flatIndex + 1).padStart(2, "0") }),
1532
+ depth > 0 && /* @__PURE__ */ jsx2("span", { className: "text-foreground/25", children: indent }),
1533
+ chapter.label
1534
+ ]
1535
+ }
1536
+ ),
1537
+ chapter.children && chapter.children.length > 0 && /* @__PURE__ */ jsx2("div", { className: "flex flex-wrap items-center gap-1", children: renderRow(chapter.children, depth + 1) })
1538
+ ] }, chapter.id);
1539
+ });
1540
+ };
1541
+ return /* @__PURE__ */ jsx2(
1542
+ "nav",
1543
+ {
1544
+ "data-stepwise": "",
1545
+ "aria-label": "Chapters",
1546
+ className: cn(
1547
+ "sticky top-20 md:top-24 z-30 rounded-sm shadow-[0_8px_24px_rgba(0,0,0,0.35)]",
1548
+ theme.surface,
1549
+ className
1550
+ ),
1551
+ children: /* @__PURE__ */ jsxs2("div", { className: "p-1.5", children: [
1552
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1 overflow-x-auto", children: [
1553
+ /* @__PURE__ */ jsx2("span", { className: cn("shrink-0 px-2 text-[9px] font-mono uppercase tracking-widest", theme.muted), children: "Chapters" }),
1554
+ renderRow(chapters, 0)
1555
+ ] }),
1556
+ hint && /* @__PURE__ */ jsx2("div", { className: "flex justify-end px-2 pt-1.5", children: /* @__PURE__ */ jsx2("span", { className: cn("text-[10px] font-mono hidden md:inline", theme.muted), children: hint }) })
1557
+ ] })
1558
+ }
1559
+ );
1560
+ }
1561
+ function LessonPager({
1562
+ prev,
1563
+ next,
1564
+ onNavigate,
1565
+ className,
1566
+ labels
1567
+ }) {
1568
+ return /* @__PURE__ */ jsxs2("div", { "data-stepwise": "", className: cn("flex items-center gap-2", className), children: [
1569
+ /* @__PURE__ */ jsxs2(
1570
+ StepButton,
1571
+ {
1572
+ variant: "outline",
1573
+ onClick: () => onNavigate("prev"),
1574
+ disabled: !prev,
1575
+ title: prev?.hint ?? prev?.label,
1576
+ className: "h-9 px-3 text-[11px]",
1577
+ "aria-label": prev ? `${labels?.previous ?? "Previous lesson"}: ${prev.label}` : "No previous lesson",
1578
+ children: [
1579
+ "\u2190 ",
1580
+ prev?.label ?? "\u2014"
1581
+ ]
1582
+ }
1583
+ ),
1584
+ /* @__PURE__ */ jsxs2(
1585
+ StepButton,
1586
+ {
1587
+ variant: "outline",
1588
+ onClick: () => onNavigate("next"),
1589
+ disabled: !next,
1590
+ title: next?.hint ?? next?.label,
1591
+ className: "h-9 px-3 text-[11px] ml-auto",
1592
+ "aria-label": next ? `${labels?.next ?? "Next lesson"}: ${next.label}` : "No next lesson",
1593
+ children: [
1594
+ next?.label ?? "\u2014",
1595
+ " \u2192"
1596
+ ]
1597
+ }
1598
+ )
1599
+ ] });
1600
+ }
1601
+ function TourSpotlight({
1602
+ target,
1603
+ theme = defaultTheme
1604
+ }) {
1605
+ const [rect, setRect] = useState9(null);
1606
+ useEffect11(() => {
1607
+ if (!target) {
1608
+ setRect(null);
1609
+ return;
1610
+ }
1611
+ let el = null;
1612
+ let scrolled = false;
1613
+ let raf = 0;
1614
+ let last = "";
1615
+ const tick = () => {
1616
+ if (!el) {
1617
+ el = document.getElementById(target) ?? document.querySelector(`[data-tour="${target}"]`);
1618
+ if (el && !scrolled) {
1619
+ scrolled = true;
1620
+ el.scrollIntoView({ block: "center", behavior: "smooth", inline: "nearest" });
1621
+ }
1622
+ }
1623
+ if (el) {
1624
+ const r = el.getBoundingClientRect();
1625
+ const key = [r.top, r.left, r.width, r.height].map((n) => Math.round(n)).join(",");
1626
+ if (key !== last) {
1627
+ last = key;
1628
+ setRect(r);
1629
+ }
1630
+ }
1631
+ raf = requestAnimationFrame(tick);
1632
+ };
1633
+ raf = requestAnimationFrame(tick);
1634
+ return () => cancelAnimationFrame(raf);
1635
+ }, [target]);
1636
+ if (!rect) return null;
1637
+ const pad = 6;
1638
+ return /* @__PURE__ */ jsx2("div", { "data-stepwise": "", className: "fixed inset-0 z-40 pointer-events-none", children: /* @__PURE__ */ jsx2(
1639
+ "div",
1640
+ {
1641
+ className: cn("absolute", theme.spotlight),
1642
+ style: {
1643
+ top: rect.top - pad,
1644
+ left: rect.left - pad,
1645
+ width: rect.width + pad * 2,
1646
+ height: rect.height + pad * 2,
1647
+ boxShadow: "0 0 0 9999px rgba(0,0,0,0.55)"
1648
+ }
1649
+ }
1650
+ ) });
1651
+ }
1652
+ function TourCard({
1653
+ step,
1654
+ index,
1655
+ total,
1656
+ onNext,
1657
+ onBack,
1658
+ onEnd,
1659
+ labels,
1660
+ className,
1661
+ theme = defaultTheme,
1662
+ nextDisabled = false
1663
+ }) {
1664
+ const L = {
1665
+ title: labels?.title ?? "Guided tour",
1666
+ back: labels?.back ?? "Back",
1667
+ next: labels?.next ?? "Next",
1668
+ finish: labels?.finish ?? "Finish",
1669
+ end: labels?.end ?? "End tour"
1670
+ };
1671
+ const trapRef = useFocusTrap(true);
1672
+ return /* @__PURE__ */ jsxs2(
1673
+ "div",
1674
+ {
1675
+ ref: trapRef,
1676
+ "data-stepwise": "",
1677
+ role: "dialog",
1678
+ "aria-modal": "true",
1679
+ "aria-label": `${L.title} \xB7 ${index + 1}/${total}`,
1680
+ tabIndex: -1,
1681
+ className: cn(
1682
+ "fixed bottom-6 left-1/2 -translate-x-1/2 z-50 w-[min(92vw,26rem)] rounded-sm p-4 outline-none",
1683
+ theme.surface,
1684
+ "border-primary/30 bg-background/95 shadow-2xl",
1685
+ className
1686
+ ),
1687
+ children: [
1688
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between mb-2", children: [
1689
+ /* @__PURE__ */ jsxs2("span", { className: "text-[10px] font-mono uppercase tracking-widest text-primary/70", children: [
1690
+ L.title,
1691
+ " \xB7 ",
1692
+ index + 1,
1693
+ "/",
1694
+ total
1695
+ ] }),
1696
+ /* @__PURE__ */ jsx2(
1697
+ StepButton,
1698
+ {
1699
+ type: "button",
1700
+ onClick: onEnd,
1701
+ "aria-label": L.end,
1702
+ className: "text-foreground/40 hover:text-foreground cursor-pointer leading-none",
1703
+ children: "\xD7"
1704
+ }
1705
+ )
1706
+ ] }),
1707
+ /* @__PURE__ */ jsx2("p", { className: "text-sm font-bold text-foreground/95 mb-1.5", children: step.title }),
1708
+ /* @__PURE__ */ jsx2("div", { className: "text-[12px] font-mono text-foreground/70 leading-relaxed", children: step.body }),
1709
+ step.waitHint && /* @__PURE__ */ jsx2("p", { className: "text-[11px] font-mono text-accent mt-2 animate-pulse", children: step.waitHint }),
1710
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 mt-3 pt-3 border-t border-white/10", children: [
1711
+ /* @__PURE__ */ jsx2(StepButton, { variant: "outline", onClick: onBack, disabled: index === 0, className: "h-7 px-3 text-[11px]", children: L.back }),
1712
+ /* @__PURE__ */ jsx2(
1713
+ StepButton,
1714
+ {
1715
+ variant: "primary",
1716
+ onClick: onNext,
1717
+ disabled: nextDisabled,
1718
+ className: "h-7 px-4 text-[11px] ml-auto",
1719
+ children: index === total - 1 ? L.finish : L.next
1720
+ }
1721
+ )
1722
+ ] })
1723
+ ]
1724
+ }
1725
+ );
1726
+ }
1727
+ function ShortcutsDialog({
1728
+ open,
1729
+ onClose,
1730
+ items,
1731
+ title = "Keyboard shortcuts",
1732
+ className,
1733
+ theme = defaultTheme
1734
+ }) {
1735
+ const [mounted, setMounted] = useState9(false);
1736
+ const trapRef = useFocusTrap(open);
1737
+ useEffect11(() => setMounted(true), []);
1738
+ useEffect11(() => {
1739
+ if (!open) return;
1740
+ const onKey = (e) => {
1741
+ if (e.key === "Escape") onClose();
1742
+ };
1743
+ document.addEventListener("keydown", onKey);
1744
+ return () => document.removeEventListener("keydown", onKey);
1745
+ }, [open, onClose]);
1746
+ if (!open || !mounted) return null;
1747
+ return createPortal2(
1748
+ /* @__PURE__ */ jsx2(
1749
+ "div",
1750
+ {
1751
+ "data-stepwise": "",
1752
+ className: cn("fixed inset-0 z-50 flex items-center justify-center p-4", theme.overlay, className),
1753
+ onClick: onClose,
1754
+ children: /* @__PURE__ */ jsxs2(
1755
+ "div",
1756
+ {
1757
+ ref: trapRef,
1758
+ role: "dialog",
1759
+ "aria-modal": "true",
1760
+ "aria-label": title,
1761
+ tabIndex: -1,
1762
+ onClick: (e) => e.stopPropagation(),
1763
+ className: cn(
1764
+ "w-full max-w-md rounded-sm p-5 shadow-2xl outline-none",
1765
+ theme.surface,
1766
+ "border-white/15 bg-card"
1767
+ ),
1768
+ children: [
1769
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between mb-4", children: [
1770
+ /* @__PURE__ */ jsx2("h3", { className: "text-lg text-primary", children: title }),
1771
+ /* @__PURE__ */ jsx2(
1772
+ StepButton,
1773
+ {
1774
+ type: "button",
1775
+ onClick: onClose,
1776
+ "aria-label": "Close shortcuts",
1777
+ className: "text-foreground/40 hover:text-foreground cursor-pointer leading-none text-lg",
1778
+ children: "\xD7"
1779
+ }
1780
+ )
1781
+ ] }),
1782
+ /* @__PURE__ */ jsx2("dl", { className: "space-y-2", children: items.map((s) => /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-4", children: [
1783
+ /* @__PURE__ */ jsx2("dt", { children: /* @__PURE__ */ jsx2("kbd", { className: theme.kbd, children: s.keys }) }),
1784
+ /* @__PURE__ */ jsx2("dd", { className: "font-mono text-[11px] text-foreground/60 text-right", children: s.action })
1785
+ ] }, s.keys)) })
1786
+ ]
1787
+ }
1788
+ )
1789
+ }
1790
+ ),
1791
+ document.body
1792
+ );
1793
+ }
1794
+ export {
1795
+ ChapterNav,
1796
+ CodeBlock,
1797
+ GlossaryAppendix,
1798
+ GlossaryProvider,
1799
+ LessonPager,
1800
+ PlayerBar,
1801
+ ShortcutsDialog,
1802
+ StepScrubber,
1803
+ TONE_CHIP_CLASS,
1804
+ TONE_SEGMENT_CLASS,
1805
+ Term,
1806
+ TourCard,
1807
+ TourSpotlight,
1808
+ alignTracks,
1809
+ cookieStore,
1810
+ createTrace,
1811
+ defaultTheme,
1812
+ flattenChapters,
1813
+ localStorageStore,
1814
+ memoryStore,
1815
+ useChapters,
1816
+ useDiscoveries,
1817
+ useFadeIn,
1818
+ useFocusTrap,
1819
+ useGlobalKeys,
1820
+ useGuidedTour,
1821
+ useLessonStream,
1822
+ usePrefersReducedMotion,
1823
+ useStepPlayer,
1824
+ useSteps,
1825
+ useSyncedTracks,
1826
+ useTrace
1827
+ };
1828
+ //# sourceMappingURL=index.js.map