@sphynxoid/stepwise 0.1.0 → 0.3.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/README.md +214 -127
- package/dist/adapters/driver-js.d.ts +1 -1
- package/dist/adapters/gsap.d.ts +1 -1
- package/dist/adapters/scroll.d.ts +1 -1
- package/dist/index.d.ts +422 -20
- package/dist/index.js +860 -156
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2 -0
- package/dist/{types-CPf9ZD_Z.d.ts → types-CNMxISXe.d.ts} +41 -7
- package/package.json +42 -20
package/dist/index.js
CHANGED
|
@@ -59,15 +59,30 @@ function useSteps(items, opts) {
|
|
|
59
59
|
|
|
60
60
|
// src/use-step-player.ts
|
|
61
61
|
import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
|
|
62
|
-
function useStepPlayer({
|
|
62
|
+
function useStepPlayer({
|
|
63
|
+
total,
|
|
64
|
+
baseDelayMs = 900,
|
|
65
|
+
autoPlay = false,
|
|
66
|
+
engine,
|
|
67
|
+
syncHash,
|
|
68
|
+
onEnd,
|
|
69
|
+
respectReducedMotion = true,
|
|
70
|
+
holdAt,
|
|
71
|
+
follow = false
|
|
72
|
+
}) {
|
|
63
73
|
const core = useSteps(total);
|
|
74
|
+
const { go, next: coreNext, prev: corePrev, first } = core;
|
|
64
75
|
const [started, setStarted] = useState2(autoPlay && total > 0);
|
|
65
|
-
const [playing, setPlaying] = useState2(autoPlay && total > 0);
|
|
76
|
+
const [playing, setPlaying] = useState2(autoPlay && (total > 0 || follow));
|
|
66
77
|
const [speed, setSpeed] = useState2(1);
|
|
67
78
|
const [ended, setEnded] = useState2(false);
|
|
68
79
|
const endedRef = useRef2(false);
|
|
69
80
|
const onEndRef = useRef2(onEnd);
|
|
70
81
|
onEndRef.current = onEnd;
|
|
82
|
+
const holdAtRef = useRef2(holdAt);
|
|
83
|
+
holdAtRef.current = holdAt;
|
|
84
|
+
const [held, setHeld] = useState2(null);
|
|
85
|
+
const releasedRef = useRef2(/* @__PURE__ */ new Set());
|
|
71
86
|
const markEnded = useCallback2(() => {
|
|
72
87
|
if (endedRef.current) return;
|
|
73
88
|
endedRef.current = true;
|
|
@@ -81,10 +96,21 @@ function useStepPlayer({ total, baseDelayMs = 900, autoPlay = false, engine, syn
|
|
|
81
96
|
}, []);
|
|
82
97
|
const index = started && core.index >= 0 ? core.index : -1;
|
|
83
98
|
const atEnd = total > 0 && index >= 0 && index >= total - 1;
|
|
84
|
-
const
|
|
85
|
-
engineRef.current = engine;
|
|
99
|
+
const live = follow && held === null && (started ? playing || atEnd : playing);
|
|
86
100
|
const totalRef = useRef2(total);
|
|
87
101
|
totalRef.current = total;
|
|
102
|
+
const cursorRef = useRef2(core.index);
|
|
103
|
+
cursorRef.current = core.index;
|
|
104
|
+
const playingRef = useRef2(playing);
|
|
105
|
+
playingRef.current = playing;
|
|
106
|
+
const atEndRef = useRef2(atEnd);
|
|
107
|
+
atEndRef.current = atEnd;
|
|
108
|
+
const heldRef = useRef2(held);
|
|
109
|
+
heldRef.current = held;
|
|
110
|
+
const startedRef = useRef2(started);
|
|
111
|
+
startedRef.current = started;
|
|
112
|
+
const engineRef = useRef2(engine);
|
|
113
|
+
engineRef.current = engine;
|
|
88
114
|
const engineApiRef = useRef2({});
|
|
89
115
|
useEffect2(() => {
|
|
90
116
|
if (!engine) return;
|
|
@@ -92,7 +118,7 @@ function useStepPlayer({ total, baseDelayMs = 900, autoPlay = false, engine, syn
|
|
|
92
118
|
get total() {
|
|
93
119
|
return totalRef.current;
|
|
94
120
|
},
|
|
95
|
-
onFrame: (f) =>
|
|
121
|
+
onFrame: (f) => go(f),
|
|
96
122
|
onEnd: () => {
|
|
97
123
|
markEnded();
|
|
98
124
|
setPlaying(false);
|
|
@@ -108,90 +134,135 @@ function useStepPlayer({ total, baseDelayMs = 900, autoPlay = false, engine, syn
|
|
|
108
134
|
engineApiRef.current = {};
|
|
109
135
|
handle.disconnect();
|
|
110
136
|
};
|
|
111
|
-
}, [engine]);
|
|
137
|
+
}, [engine, go, markEnded]);
|
|
138
|
+
useEffect2(() => {
|
|
139
|
+
if (!respectReducedMotion || !autoPlay) return;
|
|
140
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
141
|
+
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
142
|
+
setPlaying(false);
|
|
143
|
+
engineApiRef.current.pause?.();
|
|
144
|
+
}, []);
|
|
112
145
|
const start = useCallback2(() => {
|
|
113
146
|
setStarted(true);
|
|
114
147
|
setPlaying(true);
|
|
115
148
|
clearEnded();
|
|
116
|
-
|
|
149
|
+
setHeld(null);
|
|
150
|
+
releasedRef.current = /* @__PURE__ */ new Set();
|
|
151
|
+
first();
|
|
117
152
|
if (engineRef.current) {
|
|
118
153
|
engineApiRef.current.seek?.(0);
|
|
119
154
|
engineApiRef.current.play?.();
|
|
120
155
|
}
|
|
121
|
-
}, [
|
|
156
|
+
}, [first, clearEnded]);
|
|
122
157
|
const reset = useCallback2(() => {
|
|
123
158
|
setStarted(false);
|
|
124
159
|
setPlaying(false);
|
|
125
160
|
clearEnded();
|
|
161
|
+
setHeld(null);
|
|
162
|
+
releasedRef.current = /* @__PURE__ */ new Set();
|
|
126
163
|
if (engineRef.current) engineApiRef.current.pause?.();
|
|
127
164
|
}, [clearEnded]);
|
|
128
165
|
const seek = useCallback2(
|
|
129
166
|
(f) => {
|
|
130
|
-
|
|
167
|
+
const t = totalRef.current;
|
|
168
|
+
if (t === 0) return;
|
|
131
169
|
setStarted(true);
|
|
132
170
|
setPlaying(false);
|
|
133
171
|
clearEnded();
|
|
134
|
-
|
|
172
|
+
setHeld(null);
|
|
173
|
+
go(Math.max(0, Math.min(f, t - 1)));
|
|
135
174
|
if (engineRef.current) {
|
|
136
175
|
engineApiRef.current.pause?.();
|
|
137
176
|
engineApiRef.current.seek?.(f);
|
|
138
177
|
}
|
|
139
178
|
},
|
|
140
|
-
[
|
|
179
|
+
[go, clearEnded]
|
|
141
180
|
);
|
|
142
181
|
const next = useCallback2(
|
|
143
182
|
(onEmpty) => {
|
|
144
183
|
setPlaying(false);
|
|
145
|
-
|
|
184
|
+
const t = totalRef.current;
|
|
185
|
+
if (t === 0) {
|
|
146
186
|
onEmpty?.();
|
|
147
187
|
return;
|
|
148
188
|
}
|
|
149
189
|
setStarted(true);
|
|
150
190
|
clearEnded();
|
|
151
|
-
|
|
191
|
+
setHeld(null);
|
|
192
|
+
const target = Math.min(cursorRef.current + 1, t - 1);
|
|
193
|
+
coreNext();
|
|
152
194
|
if (engineRef.current) {
|
|
153
195
|
engineApiRef.current.pause?.();
|
|
154
|
-
engineApiRef.current.seek?.(
|
|
196
|
+
engineApiRef.current.seek?.(target);
|
|
155
197
|
}
|
|
156
198
|
},
|
|
157
|
-
[
|
|
199
|
+
[coreNext, clearEnded]
|
|
158
200
|
);
|
|
159
201
|
const prev = useCallback2(() => {
|
|
160
202
|
setPlaying(false);
|
|
161
203
|
clearEnded();
|
|
162
|
-
|
|
204
|
+
setHeld(null);
|
|
205
|
+
const target = Math.max(0, cursorRef.current - 1);
|
|
206
|
+
corePrev();
|
|
163
207
|
if (engineRef.current) {
|
|
164
208
|
engineApiRef.current.pause?.();
|
|
165
|
-
engineApiRef.current.seek?.(
|
|
209
|
+
engineApiRef.current.seek?.(target);
|
|
166
210
|
}
|
|
167
|
-
}, [
|
|
211
|
+
}, [corePrev, clearEnded]);
|
|
168
212
|
const toggle = useCallback2(
|
|
169
213
|
(onEmpty) => {
|
|
170
|
-
if (
|
|
214
|
+
if (totalRef.current === 0) {
|
|
171
215
|
onEmpty?.();
|
|
172
216
|
return;
|
|
173
217
|
}
|
|
174
|
-
if (!
|
|
175
|
-
|
|
176
|
-
setPlaying(true);
|
|
177
|
-
clearEnded();
|
|
178
|
-
core.first();
|
|
179
|
-
if (engineRef.current) {
|
|
180
|
-
engineApiRef.current.seek?.(0);
|
|
181
|
-
engineApiRef.current.play?.();
|
|
182
|
-
}
|
|
218
|
+
if (!playingRef.current && atEndRef.current) {
|
|
219
|
+
start();
|
|
183
220
|
return;
|
|
184
221
|
}
|
|
185
|
-
const nextPlaying = !
|
|
222
|
+
const nextPlaying = !playingRef.current;
|
|
186
223
|
setPlaying(nextPlaying);
|
|
187
224
|
setStarted(true);
|
|
225
|
+
if (nextPlaying) {
|
|
226
|
+
releasedRef.current.add(cursorRef.current);
|
|
227
|
+
setHeld(null);
|
|
228
|
+
}
|
|
188
229
|
if (engineRef.current) {
|
|
189
230
|
if (nextPlaying) engineApiRef.current.play?.();
|
|
190
231
|
else engineApiRef.current.pause?.();
|
|
191
232
|
}
|
|
192
233
|
},
|
|
193
|
-
[
|
|
234
|
+
[start]
|
|
194
235
|
);
|
|
236
|
+
const release = useCallback2(() => {
|
|
237
|
+
const h = heldRef.current;
|
|
238
|
+
if (h === null) return;
|
|
239
|
+
releasedRef.current.add(h);
|
|
240
|
+
setHeld(null);
|
|
241
|
+
setPlaying(true);
|
|
242
|
+
}, []);
|
|
243
|
+
const jump = useCallback2(
|
|
244
|
+
(f) => {
|
|
245
|
+
const t = totalRef.current;
|
|
246
|
+
if (t === 0) return;
|
|
247
|
+
setStarted(true);
|
|
248
|
+
go(Math.max(0, Math.min(f, t - 1)));
|
|
249
|
+
if (engineRef.current) engineApiRef.current.seek?.(f);
|
|
250
|
+
},
|
|
251
|
+
[go]
|
|
252
|
+
);
|
|
253
|
+
const goLive = useCallback2(() => {
|
|
254
|
+
const t = totalRef.current;
|
|
255
|
+
if (t === 0) return;
|
|
256
|
+
setStarted(true);
|
|
257
|
+
setPlaying(false);
|
|
258
|
+
clearEnded();
|
|
259
|
+
setHeld(null);
|
|
260
|
+
go(t - 1);
|
|
261
|
+
if (engineRef.current) {
|
|
262
|
+
engineApiRef.current.pause?.();
|
|
263
|
+
engineApiRef.current.seek?.(t - 1);
|
|
264
|
+
}
|
|
265
|
+
}, [go, clearEnded]);
|
|
195
266
|
const changeSpeed = useCallback2((s) => {
|
|
196
267
|
setSpeed(s);
|
|
197
268
|
if (engineRef.current) engineApiRef.current.rate?.(s);
|
|
@@ -199,6 +270,21 @@ function useStepPlayer({ total, baseDelayMs = 900, autoPlay = false, engine, syn
|
|
|
199
270
|
useEffect2(() => {
|
|
200
271
|
clearEnded();
|
|
201
272
|
}, [total, clearEnded]);
|
|
273
|
+
const prevTotal = useRef2(total);
|
|
274
|
+
useEffect2(() => {
|
|
275
|
+
const before = prevTotal.current;
|
|
276
|
+
prevTotal.current = total;
|
|
277
|
+
if (!follow || engineRef.current || total <= before) return;
|
|
278
|
+
if (heldRef.current !== null) return;
|
|
279
|
+
if (!startedRef.current) {
|
|
280
|
+
if (playingRef.current) setStarted(true);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (playingRef.current || cursorRef.current >= before - 1) setPlaying(true);
|
|
284
|
+
}, [total, follow]);
|
|
285
|
+
useEffect2(() => {
|
|
286
|
+
if (held !== null && held !== core.index) setHeld(null);
|
|
287
|
+
}, [held, core.index]);
|
|
202
288
|
useEffect2(() => {
|
|
203
289
|
if (engineRef.current) return;
|
|
204
290
|
if (!playing || !started || total === 0) return;
|
|
@@ -207,34 +293,49 @@ function useStepPlayer({ total, baseDelayMs = 900, autoPlay = false, engine, syn
|
|
|
207
293
|
setPlaying(false);
|
|
208
294
|
return;
|
|
209
295
|
}
|
|
210
|
-
|
|
296
|
+
if (holdAtRef.current?.(core.index) && !releasedRef.current.has(core.index)) {
|
|
297
|
+
setHeld(core.index);
|
|
298
|
+
setPlaying(false);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const id = setTimeout(() => coreNext(), baseDelayMs / speed);
|
|
211
302
|
return () => clearTimeout(id);
|
|
212
|
-
}, [playing, started, core, total, speed, baseDelayMs, markEnded]);
|
|
213
|
-
const hashKey = syncHash;
|
|
214
|
-
const coreGoRef = useRef2(core.go);
|
|
215
|
-
coreGoRef.current = core.go;
|
|
303
|
+
}, [playing, started, core.index, coreNext, total, speed, baseDelayMs, markEnded]);
|
|
216
304
|
const lastWrittenHash = useRef2("");
|
|
305
|
+
const pendingRead = useRef2(null);
|
|
217
306
|
const readHash = useCallback2(() => {
|
|
218
|
-
|
|
307
|
+
const t = totalRef.current;
|
|
308
|
+
if (!syncHash || typeof window === "undefined" || t === 0) return;
|
|
219
309
|
const hash = window.location.hash;
|
|
220
310
|
if (hash === lastWrittenHash.current) return;
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
223
|
-
const f = Number(
|
|
224
|
-
if (
|
|
311
|
+
const value = readHashParam(hash, syncHash);
|
|
312
|
+
if (value === null || !/^\d+$/.test(value)) return;
|
|
313
|
+
const f = Number(value);
|
|
314
|
+
if (f < t) {
|
|
315
|
+
pendingRead.current = f;
|
|
225
316
|
setStarted(true);
|
|
226
|
-
|
|
317
|
+
go(f);
|
|
227
318
|
}
|
|
228
|
-
}, [
|
|
319
|
+
}, [syncHash, go]);
|
|
320
|
+
const linkedRead = useRef2(false);
|
|
229
321
|
useEffect2(() => {
|
|
230
322
|
readHash();
|
|
323
|
+
linkedRead.current = totalRef.current > 0;
|
|
231
324
|
const onHashChange = () => readHash();
|
|
232
325
|
window.addEventListener("hashchange", onHashChange);
|
|
233
326
|
return () => window.removeEventListener("hashchange", onHashChange);
|
|
234
327
|
}, [readHash]);
|
|
328
|
+
useEffect2(() => {
|
|
329
|
+
if (linkedRead.current || total === 0) return;
|
|
330
|
+
linkedRead.current = true;
|
|
331
|
+
readHash();
|
|
332
|
+
}, [total, readHash]);
|
|
235
333
|
useEffect2(() => {
|
|
236
334
|
if (!syncHash || index < 0 || typeof window === "undefined") return;
|
|
237
|
-
const
|
|
335
|
+
const pending2 = pendingRead.current;
|
|
336
|
+
pendingRead.current = null;
|
|
337
|
+
if (pending2 !== null && pending2 !== index) return;
|
|
338
|
+
const next2 = writeHashParam(window.location.hash, syncHash, String(index));
|
|
238
339
|
if (window.location.hash !== next2) {
|
|
239
340
|
lastWrittenHash.current = next2;
|
|
240
341
|
window.history.replaceState(null, "", next2);
|
|
@@ -249,6 +350,11 @@ function useStepPlayer({ total, baseDelayMs = 900, autoPlay = false, engine, syn
|
|
|
249
350
|
speed,
|
|
250
351
|
atEnd,
|
|
251
352
|
ended,
|
|
353
|
+
held,
|
|
354
|
+
release,
|
|
355
|
+
jump,
|
|
356
|
+
live,
|
|
357
|
+
goLive,
|
|
252
358
|
start,
|
|
253
359
|
reset,
|
|
254
360
|
seek,
|
|
@@ -258,28 +364,598 @@ function useStepPlayer({ total, baseDelayMs = 900, autoPlay = false, engine, syn
|
|
|
258
364
|
setPlaying,
|
|
259
365
|
setSpeed: changeSpeed
|
|
260
366
|
}),
|
|
261
|
-
[total, index, playing, speed, atEnd, ended, start, reset, seek, next, prev, toggle, changeSpeed]
|
|
367
|
+
[total, index, playing, speed, atEnd, ended, held, release, jump, live, goLive, start, reset, seek, next, prev, toggle, changeSpeed]
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
function hashParts(hash) {
|
|
371
|
+
const body = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
372
|
+
return body ? body.split("&") : [];
|
|
373
|
+
}
|
|
374
|
+
function readHashParam(hash, key) {
|
|
375
|
+
for (const part of hashParts(hash)) {
|
|
376
|
+
const eq = part.indexOf("=");
|
|
377
|
+
if (eq > 0 && part.slice(0, eq) === key) return part.slice(eq + 1);
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
function writeHashParam(hash, key, value) {
|
|
382
|
+
const parts = hashParts(hash);
|
|
383
|
+
const pair = `${key}=${value}`;
|
|
384
|
+
const i = parts.findIndex((p) => p.startsWith(`${key}=`));
|
|
385
|
+
if (i >= 0) parts[i] = pair;
|
|
386
|
+
else parts.push(pair);
|
|
387
|
+
return `#${parts.join("&")}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/use-trace.ts
|
|
391
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useReducer, useRef as useRef3 } from "react";
|
|
392
|
+
|
|
393
|
+
// src/reanchor.ts
|
|
394
|
+
function reanchor(oldSteps, oldIndex, newSteps, keyOf) {
|
|
395
|
+
if (newSteps.length === 0) return -1;
|
|
396
|
+
if (oldIndex < 0 || oldSteps.length === 0) return 0;
|
|
397
|
+
const positions = /* @__PURE__ */ new Map();
|
|
398
|
+
newSteps.forEach((s, i) => {
|
|
399
|
+
const k = keyOf(s, i);
|
|
400
|
+
if (k !== void 0 && !positions.has(k)) positions.set(k, i);
|
|
401
|
+
});
|
|
402
|
+
for (let i = Math.min(oldIndex, oldSteps.length - 1); i >= 0; i--) {
|
|
403
|
+
const k = keyOf(oldSteps[i], i);
|
|
404
|
+
if (k === void 0) continue;
|
|
405
|
+
const j = positions.get(k);
|
|
406
|
+
if (j !== void 0) return j;
|
|
407
|
+
}
|
|
408
|
+
return 0;
|
|
409
|
+
}
|
|
410
|
+
var stepKey = (step) => step !== null && typeof step === "object" && typeof step.key === "string" ? step.key : void 0;
|
|
411
|
+
|
|
412
|
+
// src/use-trace.ts
|
|
413
|
+
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect3;
|
|
414
|
+
function useTrace(steps, opts) {
|
|
415
|
+
const { keyOf, holdAt, ...playerOpts } = opts ?? {};
|
|
416
|
+
const stepsRef = useRef3(steps);
|
|
417
|
+
const player = useStepPlayer({
|
|
418
|
+
...playerOpts,
|
|
419
|
+
total: steps.length,
|
|
420
|
+
holdAt: holdAt ? (f) => holdAt(f, stepsRef.current[f]) : void 0
|
|
421
|
+
});
|
|
422
|
+
stepsRef.current = steps;
|
|
423
|
+
const keyFn = keyOf ?? stepKey;
|
|
424
|
+
const shownRef = useRef3({ steps, index: player.index });
|
|
425
|
+
const shown = shownRef.current;
|
|
426
|
+
let anchored = null;
|
|
427
|
+
if (shown.steps !== steps && shown.index >= 0) {
|
|
428
|
+
const i = shown.index;
|
|
429
|
+
const k = i < shown.steps.length ? keyFn(shown.steps[i], i) : void 0;
|
|
430
|
+
if (k !== void 0 && i < steps.length && keyFn(steps[i], i) === k) anchored = i;
|
|
431
|
+
else if (steps.some((s, j) => keyFn(s, j) !== void 0)) anchored = reanchor(shown.steps, i, steps, keyFn);
|
|
432
|
+
}
|
|
433
|
+
const index = anchored ?? player.index;
|
|
434
|
+
const { jump } = player;
|
|
435
|
+
const [, rerender] = useReducer((n) => n + 1, 0);
|
|
436
|
+
useIsomorphicLayoutEffect(() => {
|
|
437
|
+
shownRef.current = { steps, index };
|
|
438
|
+
if (anchored === null) return;
|
|
439
|
+
if (anchored >= 0 && anchored !== player.index) jump(anchored);
|
|
440
|
+
rerender();
|
|
441
|
+
});
|
|
442
|
+
const frame = steps.length === 0 ? -1 : Math.max(0, Math.min(index, steps.length - 1));
|
|
443
|
+
const step = frame >= 0 ? steps[frame] : void 0;
|
|
444
|
+
const toneAt = useCallback3((i) => stepsRef.current[i]?.tone ?? "neutral", []);
|
|
445
|
+
const titleAt = useCallback3((i) => stepsRef.current[i]?.label ?? `step ${i + 1}`, []);
|
|
446
|
+
return useMemo3(() => ({ player, step, frame, toneAt, titleAt }), [player, step, frame, toneAt, titleAt]);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/use-simulation.ts
|
|
450
|
+
import { useMemo as useMemo4 } from "react";
|
|
451
|
+
|
|
452
|
+
// src/simulation.ts
|
|
453
|
+
function hashSeed(seed) {
|
|
454
|
+
let h = 2166136261;
|
|
455
|
+
for (let i = 0; i < seed.length; i++) {
|
|
456
|
+
h ^= seed.charCodeAt(i);
|
|
457
|
+
h = Math.imul(h, 16777619);
|
|
458
|
+
}
|
|
459
|
+
return h >>> 0;
|
|
460
|
+
}
|
|
461
|
+
function createRng(seed = 0) {
|
|
462
|
+
const base = typeof seed === "string" ? hashSeed(seed) : seed >>> 0;
|
|
463
|
+
let state = base;
|
|
464
|
+
const next = () => {
|
|
465
|
+
state = state + 1831565813 >>> 0;
|
|
466
|
+
let t = state;
|
|
467
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
468
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
469
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
470
|
+
};
|
|
471
|
+
const int = (min, max) => min + Math.floor(next() * (max - min + 1));
|
|
472
|
+
return {
|
|
473
|
+
seed: base,
|
|
474
|
+
next,
|
|
475
|
+
int,
|
|
476
|
+
pick(items) {
|
|
477
|
+
if (items.length === 0) throw new RangeError("rng.pick: empty array");
|
|
478
|
+
return items[int(0, items.length - 1)];
|
|
479
|
+
},
|
|
480
|
+
shuffle(items) {
|
|
481
|
+
const out = items.slice();
|
|
482
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
483
|
+
const j = int(0, i);
|
|
484
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
485
|
+
}
|
|
486
|
+
return out;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function runSimulation(simulation, input, opts) {
|
|
491
|
+
const maxSteps = opts?.maxSteps ?? 1e4;
|
|
492
|
+
const rng = createRng(opts?.seed ?? 0);
|
|
493
|
+
const steps = [];
|
|
494
|
+
let truncated = false;
|
|
495
|
+
let error = null;
|
|
496
|
+
try {
|
|
497
|
+
for (const step of simulation(input, { rng })) {
|
|
498
|
+
if (steps.length >= maxSteps) {
|
|
499
|
+
truncated = true;
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
steps.push(step);
|
|
503
|
+
}
|
|
504
|
+
} catch (e) {
|
|
505
|
+
error = e;
|
|
506
|
+
}
|
|
507
|
+
return { steps, truncated, error, seed: rng.seed };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/use-simulation.ts
|
|
511
|
+
function useSimulation(simulation, input, opts) {
|
|
512
|
+
const { seed, maxSteps, ...traceOpts } = opts ?? {};
|
|
513
|
+
const result = useMemo4(
|
|
514
|
+
() => runSimulation(simulation, input, { seed, maxSteps }),
|
|
515
|
+
[simulation, input, seed, maxSteps]
|
|
516
|
+
);
|
|
517
|
+
const trace = useTrace(result.steps, traceOpts);
|
|
518
|
+
return useMemo4(
|
|
519
|
+
() => ({ ...trace, steps: result.steps, truncated: result.truncated, error: result.error, seed: result.seed }),
|
|
520
|
+
[trace, result]
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/create-trace.ts
|
|
525
|
+
function createTrace() {
|
|
526
|
+
const frames = [];
|
|
527
|
+
const builder = {
|
|
528
|
+
step(step) {
|
|
529
|
+
frames.push(step);
|
|
530
|
+
return builder;
|
|
531
|
+
},
|
|
532
|
+
get length() {
|
|
533
|
+
return frames.length;
|
|
534
|
+
},
|
|
535
|
+
last() {
|
|
536
|
+
return frames[frames.length - 1];
|
|
537
|
+
},
|
|
538
|
+
build() {
|
|
539
|
+
return frames.slice();
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
return builder;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/use-synced-tracks.ts
|
|
546
|
+
import { useCallback as useCallback4, useMemo as useMemo5, useRef as useRef4 } from "react";
|
|
547
|
+
function alignTracks(tracks, key) {
|
|
548
|
+
const names = Object.keys(tracks);
|
|
549
|
+
const lens = names.map((n) => tracks[n].length);
|
|
550
|
+
const rows = [];
|
|
551
|
+
const holdCell = (len, at) => ({ index: len === 0 ? -1 : Math.min(at, len - 1), held: true });
|
|
552
|
+
if (!key) {
|
|
553
|
+
const length = Math.max(0, ...lens);
|
|
554
|
+
for (let i = 0; i < length; i++) {
|
|
555
|
+
const row = {};
|
|
556
|
+
names.forEach((n, t) => {
|
|
557
|
+
row[n] = i < lens[t] ? { index: i, held: false } : holdCell(lens[t], i);
|
|
558
|
+
});
|
|
559
|
+
rows.push(row);
|
|
560
|
+
}
|
|
561
|
+
return rows;
|
|
562
|
+
}
|
|
563
|
+
const keyed = names.map(
|
|
564
|
+
(n) => tracks[n].map((step, i) => {
|
|
565
|
+
const k = key(step, i, n);
|
|
566
|
+
return k == null ? null : k;
|
|
567
|
+
})
|
|
568
|
+
);
|
|
569
|
+
const qualified = keyed.map((ks) => {
|
|
570
|
+
const seen = /* @__PURE__ */ new Map();
|
|
571
|
+
return ks.map((k) => {
|
|
572
|
+
if (k === null) return null;
|
|
573
|
+
const n = seen.get(k) ?? 0;
|
|
574
|
+
seen.set(k, n + 1);
|
|
575
|
+
return `${k}\0${n}`;
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
const where = /* @__PURE__ */ new Map();
|
|
579
|
+
qualified.forEach(
|
|
580
|
+
(ks, t) => ks.forEach((k, i) => {
|
|
581
|
+
if (k === null) return;
|
|
582
|
+
let m = where.get(k);
|
|
583
|
+
if (!m) where.set(k, m = /* @__PURE__ */ new Map());
|
|
584
|
+
m.set(t, i);
|
|
585
|
+
})
|
|
586
|
+
);
|
|
587
|
+
for (const [k, m] of where) if (m.size < 2) where.delete(k);
|
|
588
|
+
const pos = names.map(() => 0);
|
|
589
|
+
while (pos.some((p, t) => p < lens[t])) {
|
|
590
|
+
const live = names.map((_, t) => pos[t] < lens[t]);
|
|
591
|
+
const blocked = names.map((_, t) => {
|
|
592
|
+
if (!live[t]) return false;
|
|
593
|
+
const k = qualified[t][pos[t]];
|
|
594
|
+
const m = k === null ? void 0 : where.get(k);
|
|
595
|
+
if (!m) return false;
|
|
596
|
+
for (const [u, at] of m) if (u !== t && at > pos[u]) return true;
|
|
597
|
+
return false;
|
|
598
|
+
});
|
|
599
|
+
const anyFree = live.some((l, t) => l && !blocked[t]);
|
|
600
|
+
const row = {};
|
|
601
|
+
names.forEach((n, t) => {
|
|
602
|
+
if (live[t] && (!blocked[t] || !anyFree)) {
|
|
603
|
+
row[n] = { index: pos[t], held: false };
|
|
604
|
+
pos[t]++;
|
|
605
|
+
} else {
|
|
606
|
+
row[n] = holdCell(lens[t], Math.max(0, pos[t] - 1));
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
rows.push(row);
|
|
610
|
+
}
|
|
611
|
+
return rows;
|
|
612
|
+
}
|
|
613
|
+
var TONE_RANK = ["bad", "good", "write", "accent", "neutral", "idle"];
|
|
614
|
+
function useSyncedTracks(tracks, opts) {
|
|
615
|
+
const { key, ...playerOpts } = opts ?? {};
|
|
616
|
+
const keyRef = useRef4(key);
|
|
617
|
+
keyRef.current = key;
|
|
618
|
+
const cache = useRef4(null);
|
|
619
|
+
const names = Object.keys(tracks);
|
|
620
|
+
const arrays = names.map((n) => tracks[n]);
|
|
621
|
+
const c = cache.current;
|
|
622
|
+
if (!c || c.keyed !== !!key || c.names.length !== names.length || names.some((n, i) => c.names[i] !== n || c.arrays[i] !== arrays[i])) {
|
|
623
|
+
cache.current = { names, arrays, keyed: !!key, rows: alignTracks(tracks, keyRef.current) };
|
|
624
|
+
}
|
|
625
|
+
const rows = cache.current.rows;
|
|
626
|
+
const player = useStepPlayer({ ...playerOpts, total: rows.length });
|
|
627
|
+
const tracksRef = useRef4(tracks);
|
|
628
|
+
tracksRef.current = tracks;
|
|
629
|
+
const rowsRef = useRef4(rows);
|
|
630
|
+
rowsRef.current = rows;
|
|
631
|
+
const frame = rows.length === 0 ? -1 : Math.max(0, Math.min(player.index, rows.length - 1));
|
|
632
|
+
const row = frame >= 0 ? rows[frame] : void 0;
|
|
633
|
+
const aligned = cache.current;
|
|
634
|
+
const view = useMemo5(() => {
|
|
635
|
+
const out = {};
|
|
636
|
+
aligned.names.forEach((n, t) => {
|
|
637
|
+
const list = aligned.arrays[t];
|
|
638
|
+
const cell = row?.[n];
|
|
639
|
+
const index = cell ? cell.index : list.length === 0 ? -1 : 0;
|
|
640
|
+
out[n] = {
|
|
641
|
+
step: index >= 0 ? list[index] : void 0,
|
|
642
|
+
index,
|
|
643
|
+
held: cell?.held ?? false,
|
|
644
|
+
done: list.length > 0 && index === list.length - 1,
|
|
645
|
+
length: list.length
|
|
646
|
+
};
|
|
647
|
+
});
|
|
648
|
+
return out;
|
|
649
|
+
}, [aligned, row]);
|
|
650
|
+
const moved = useCallback4((i) => {
|
|
651
|
+
const r = rowsRef.current[i];
|
|
652
|
+
if (!r) return [];
|
|
653
|
+
return Object.keys(r).filter((n) => !r[n].held).map((n) => ({ name: n, step: tracksRef.current[n]?.[r[n].index] })).filter((m) => !!m.step);
|
|
654
|
+
}, []);
|
|
655
|
+
const toneAt = useCallback4(
|
|
656
|
+
(i) => {
|
|
657
|
+
const tones = moved(i).map((m) => m.step.tone);
|
|
658
|
+
return TONE_RANK.find((t) => tones.includes(t)) ?? "neutral";
|
|
659
|
+
},
|
|
660
|
+
[moved]
|
|
661
|
+
);
|
|
662
|
+
const titleAt = useCallback4(
|
|
663
|
+
(i) => {
|
|
664
|
+
const parts = moved(i).map((m) => `${m.name}: ${m.step.label}`);
|
|
665
|
+
return parts.length ? parts.join(" \xB7 ") : `frame ${i + 1}`;
|
|
666
|
+
},
|
|
667
|
+
[moved]
|
|
668
|
+
);
|
|
669
|
+
return useMemo5(
|
|
670
|
+
() => ({ player, frame, total: rows.length, tracks: view, rows, toneAt, titleAt }),
|
|
671
|
+
[player, frame, rows, view, toneAt, titleAt]
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/use-lesson-stream.ts
|
|
676
|
+
import { useCallback as useCallback5, useEffect as useEffect5, useLayoutEffect as useLayoutEffect2, useMemo as useMemo6, useRef as useRef5, useState as useState4 } from "react";
|
|
677
|
+
|
|
678
|
+
// src/use-prefers-reduced-motion.ts
|
|
679
|
+
import { useEffect as useEffect4, useState as useState3 } from "react";
|
|
680
|
+
function usePrefersReducedMotion() {
|
|
681
|
+
const [reduced, setReduced] = useState3(false);
|
|
682
|
+
useEffect4(() => {
|
|
683
|
+
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
684
|
+
setReduced(mq.matches);
|
|
685
|
+
const onChange = (e) => setReduced(e.matches);
|
|
686
|
+
mq.addEventListener("change", onChange);
|
|
687
|
+
return () => mq.removeEventListener("change", onChange);
|
|
688
|
+
}, []);
|
|
689
|
+
return reduced;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/use-lesson-stream.ts
|
|
693
|
+
var useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect2 : useEffect5;
|
|
694
|
+
function useLessonStream(count, opts = {}) {
|
|
695
|
+
const { radius = 1, approachBelow = 600, approachAbove = 300, scrollToSeed = true } = opts;
|
|
696
|
+
const last = count - 1;
|
|
697
|
+
const seed = count === 0 ? -1 : Math.max(0, Math.min(opts.seed ?? 0, last));
|
|
698
|
+
const reducedMotion = usePrefersReducedMotion();
|
|
699
|
+
const core = useSteps(count, { initialIndex: Math.max(0, seed) });
|
|
700
|
+
const coreGoRef = useRef5(core.go);
|
|
701
|
+
coreGoRef.current = core.go;
|
|
702
|
+
const activeRef = useRef5(core.index);
|
|
703
|
+
activeRef.current = core.index;
|
|
704
|
+
const [range, setRange] = useState4(() => ({
|
|
705
|
+
first: Math.max(0, seed - radius),
|
|
706
|
+
last: Math.min(last, seed + radius)
|
|
707
|
+
}));
|
|
708
|
+
const first = Math.max(0, Math.min(range.first, last));
|
|
709
|
+
const lastMounted = Math.min(range.last, last);
|
|
710
|
+
const rangeRef = useRef5({ first, last: lastMounted });
|
|
711
|
+
rangeRef.current = { first, last: lastMounted };
|
|
712
|
+
const countRef = useRef5(count);
|
|
713
|
+
countRef.current = count;
|
|
714
|
+
const reducedRef = useRef5(reducedMotion);
|
|
715
|
+
reducedRef.current = reducedMotion;
|
|
716
|
+
const onActiveRef = useRef5(opts.onActiveChange);
|
|
717
|
+
onActiveRef.current = opts.onActiveChange;
|
|
718
|
+
const optsRef = useRef5({ approachBelow, approachAbove });
|
|
719
|
+
optsRef.current = { approachBelow, approachAbove };
|
|
720
|
+
const blocks = useRef5(/* @__PURE__ */ new Map());
|
|
721
|
+
const refCache = useRef5(/* @__PURE__ */ new Map());
|
|
722
|
+
const refFor = useCallback5((i) => {
|
|
723
|
+
let fn = refCache.current.get(i);
|
|
724
|
+
if (!fn) {
|
|
725
|
+
fn = (el) => {
|
|
726
|
+
if (el) blocks.current.set(i, el);
|
|
727
|
+
else blocks.current.delete(i);
|
|
728
|
+
};
|
|
729
|
+
refCache.current.set(i, fn);
|
|
730
|
+
}
|
|
731
|
+
return fn;
|
|
732
|
+
}, []);
|
|
733
|
+
const causeRef = useRef5("scroll");
|
|
734
|
+
const expandTo = useCallback5((i, cause) => {
|
|
735
|
+
const r = rangeRef.current;
|
|
736
|
+
if (i >= r.first && i <= r.last) return;
|
|
737
|
+
causeRef.current = cause;
|
|
738
|
+
const grown = { first: Math.min(r.first, i), last: Math.max(r.last, i) };
|
|
739
|
+
rangeRef.current = grown;
|
|
740
|
+
setRange(grown);
|
|
741
|
+
}, []);
|
|
742
|
+
const scheduleRef = useRef5(null);
|
|
743
|
+
useEffect5(() => {
|
|
744
|
+
if (count === 0) return;
|
|
745
|
+
let raf = 0;
|
|
746
|
+
const update = () => {
|
|
747
|
+
raf = 0;
|
|
748
|
+
if (blocks.current.size === 0) return;
|
|
749
|
+
const mid = window.innerHeight / 2;
|
|
750
|
+
let best = -1;
|
|
751
|
+
for (const [i, el] of blocks.current) {
|
|
752
|
+
const rect = el.getBoundingClientRect();
|
|
753
|
+
if (rect.top <= mid && rect.bottom >= mid) best = Math.max(best, i);
|
|
754
|
+
}
|
|
755
|
+
if (best === -1) best = Math.min(...blocks.current.keys());
|
|
756
|
+
if (best !== activeRef.current) {
|
|
757
|
+
activeRef.current = best;
|
|
758
|
+
coreGoRef.current(best);
|
|
759
|
+
onActiveRef.current?.(best);
|
|
760
|
+
}
|
|
761
|
+
const { first: f, last: l } = rangeRef.current;
|
|
762
|
+
const { approachBelow: below, approachAbove: above } = optsRef.current;
|
|
763
|
+
const lastEl = blocks.current.get(l);
|
|
764
|
+
if (lastEl && l < countRef.current - 1 && lastEl.getBoundingClientRect().bottom - window.innerHeight < below) {
|
|
765
|
+
expandTo(l + 1, "scroll");
|
|
766
|
+
}
|
|
767
|
+
const firstEl = blocks.current.get(f);
|
|
768
|
+
if (firstEl && f > 0 && firstEl.getBoundingClientRect().top > -above) {
|
|
769
|
+
expandTo(f - 1, "scroll");
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
const onScroll = () => {
|
|
773
|
+
if (!raf) raf = requestAnimationFrame(update);
|
|
774
|
+
};
|
|
775
|
+
scheduleRef.current = onScroll;
|
|
776
|
+
update();
|
|
777
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
778
|
+
window.addEventListener("resize", onScroll);
|
|
779
|
+
return () => {
|
|
780
|
+
scheduleRef.current = null;
|
|
781
|
+
window.removeEventListener("scroll", onScroll);
|
|
782
|
+
window.removeEventListener("resize", onScroll);
|
|
783
|
+
if (raf) cancelAnimationFrame(raf);
|
|
784
|
+
};
|
|
785
|
+
}, [count, expandTo]);
|
|
786
|
+
useEffect5(() => {
|
|
787
|
+
scheduleRef.current?.();
|
|
788
|
+
}, [first, lastMounted]);
|
|
789
|
+
const firstRef = useRef5(first);
|
|
790
|
+
useIsoLayoutEffect(() => {
|
|
791
|
+
if (first < firstRef.current && causeRef.current === "scroll") {
|
|
792
|
+
let height = 0;
|
|
793
|
+
for (let i = first; i < firstRef.current; i++) {
|
|
794
|
+
height += blocks.current.get(i)?.getBoundingClientRect().height ?? 0;
|
|
795
|
+
}
|
|
796
|
+
if (height) window.scrollBy({ top: height, behavior: "auto" });
|
|
797
|
+
}
|
|
798
|
+
causeRef.current = "scroll";
|
|
799
|
+
firstRef.current = first;
|
|
800
|
+
}, [first]);
|
|
801
|
+
useIsoLayoutEffect(() => {
|
|
802
|
+
if (!scrollToSeed || seed <= 0) return;
|
|
803
|
+
blocks.current.get(seed)?.scrollIntoView({ block: "start", behavior: "auto" });
|
|
804
|
+
}, []);
|
|
805
|
+
const [nav, setNav] = useState4(null);
|
|
806
|
+
useEffect5(() => {
|
|
807
|
+
if (!nav) return;
|
|
808
|
+
const raf = requestAnimationFrame(
|
|
809
|
+
() => blocks.current.get(nav.target)?.scrollIntoView({ behavior: reducedRef.current ? "auto" : "smooth", block: "start" })
|
|
810
|
+
);
|
|
811
|
+
return () => cancelAnimationFrame(raf);
|
|
812
|
+
}, [nav]);
|
|
813
|
+
const goTo = useCallback5(
|
|
814
|
+
(i) => {
|
|
815
|
+
const c = countRef.current;
|
|
816
|
+
if (c === 0) return;
|
|
817
|
+
const target = Math.max(0, Math.min(i, c - 1));
|
|
818
|
+
expandTo(target, "nav");
|
|
819
|
+
setNav({ target });
|
|
820
|
+
},
|
|
821
|
+
[expandTo]
|
|
822
|
+
);
|
|
823
|
+
const next = useCallback5(() => goTo(activeRef.current + 1), [goTo]);
|
|
824
|
+
const prev = useCallback5(() => goTo(activeRef.current - 1), [goTo]);
|
|
825
|
+
const mounted = useMemo6(() => {
|
|
826
|
+
const out = [];
|
|
827
|
+
for (let i = first; i <= lastMounted; i++) out.push(i);
|
|
828
|
+
return out;
|
|
829
|
+
}, [first, lastMounted]);
|
|
830
|
+
return useMemo6(
|
|
831
|
+
() => ({
|
|
832
|
+
active: core.index,
|
|
833
|
+
seed,
|
|
834
|
+
mounted,
|
|
835
|
+
complete: count > 0 && lastMounted === last,
|
|
836
|
+
refFor,
|
|
837
|
+
goTo,
|
|
838
|
+
next,
|
|
839
|
+
prev,
|
|
840
|
+
reducedMotion
|
|
841
|
+
}),
|
|
842
|
+
[core.index, seed, mounted, count, lastMounted, last, refFor, goTo, next, prev, reducedMotion]
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
function useFadeIn(initiallyVisible = false) {
|
|
846
|
+
const reduced = usePrefersReducedMotion();
|
|
847
|
+
const [shown, setShown] = useState4(initiallyVisible);
|
|
848
|
+
useEffect5(() => {
|
|
849
|
+
if (shown) return;
|
|
850
|
+
if (reduced) {
|
|
851
|
+
setShown(true);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const raf = requestAnimationFrame(() => setShown(true));
|
|
855
|
+
return () => cancelAnimationFrame(raf);
|
|
856
|
+
}, [shown, reduced]);
|
|
857
|
+
return shown;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/use-evaluator.ts
|
|
861
|
+
import { useCallback as useCallback6, useEffect as useEffect6, useMemo as useMemo7, useRef as useRef6, useState as useState5 } from "react";
|
|
862
|
+
|
|
863
|
+
// src/evaluate.ts
|
|
864
|
+
function toVerdict(result) {
|
|
865
|
+
return typeof result === "boolean" ? { status: result ? "pass" : "fail" } : result;
|
|
866
|
+
}
|
|
867
|
+
var pass = (feedback) => ({ status: "pass", feedback });
|
|
868
|
+
var fail = (feedback) => ({ status: "fail", feedback });
|
|
869
|
+
var pending = (feedback) => ({ status: "pending", feedback });
|
|
870
|
+
|
|
871
|
+
// src/use-evaluator.ts
|
|
872
|
+
var IDLE = { status: "idle", feedback: void 0, error: null };
|
|
873
|
+
function useEvaluator(evaluator, ctx, opts) {
|
|
874
|
+
const mode = opts?.mode ?? "auto";
|
|
875
|
+
const [state, setState] = useState5(IDLE);
|
|
876
|
+
const [attempts, setAttempts] = useState5(0);
|
|
877
|
+
const evaluatorRef = useRef6(evaluator);
|
|
878
|
+
evaluatorRef.current = evaluator;
|
|
879
|
+
const ctxRef = useRef6(ctx);
|
|
880
|
+
ctxRef.current = ctx;
|
|
881
|
+
const runRef = useRef6(null);
|
|
882
|
+
useEffect6(() => () => runRef.current?.abort(), []);
|
|
883
|
+
const run = useCallback6(async () => {
|
|
884
|
+
runRef.current?.abort();
|
|
885
|
+
const controller = new AbortController();
|
|
886
|
+
runRef.current = controller;
|
|
887
|
+
let result;
|
|
888
|
+
try {
|
|
889
|
+
result = evaluatorRef.current(ctxRef.current, controller.signal);
|
|
890
|
+
} catch (error) {
|
|
891
|
+
setState({ status: "fail", feedback: void 0, error });
|
|
892
|
+
return { status: "fail" };
|
|
893
|
+
}
|
|
894
|
+
if (!(result instanceof Promise)) {
|
|
895
|
+
const verdict = toVerdict(result);
|
|
896
|
+
setState({ status: verdict.status, feedback: verdict.feedback, error: null });
|
|
897
|
+
return verdict;
|
|
898
|
+
}
|
|
899
|
+
setState((s) => ({ status: "pending", feedback: s.feedback, error: null }));
|
|
900
|
+
try {
|
|
901
|
+
const verdict = toVerdict(await result);
|
|
902
|
+
if (controller.signal.aborted) return null;
|
|
903
|
+
setState({ status: verdict.status, feedback: verdict.feedback, error: null });
|
|
904
|
+
return verdict;
|
|
905
|
+
} catch (error) {
|
|
906
|
+
if (controller.signal.aborted) return null;
|
|
907
|
+
setState({ status: "fail", feedback: void 0, error });
|
|
908
|
+
return { status: "fail" };
|
|
909
|
+
}
|
|
910
|
+
}, []);
|
|
911
|
+
const check = useCallback6(() => {
|
|
912
|
+
setAttempts((n) => n + 1);
|
|
913
|
+
return run();
|
|
914
|
+
}, [run]);
|
|
915
|
+
const reset = useCallback6(() => {
|
|
916
|
+
runRef.current?.abort();
|
|
917
|
+
runRef.current = null;
|
|
918
|
+
setState(IDLE);
|
|
919
|
+
setAttempts(0);
|
|
920
|
+
}, []);
|
|
921
|
+
useEffect6(() => {
|
|
922
|
+
if (mode === "auto") {
|
|
923
|
+
void run();
|
|
924
|
+
} else {
|
|
925
|
+
runRef.current?.abort();
|
|
926
|
+
setState(IDLE);
|
|
927
|
+
}
|
|
928
|
+
}, [ctx, mode, run]);
|
|
929
|
+
return useMemo7(
|
|
930
|
+
() => ({ ...state, passed: state.status === "pass", attempts, check, reset }),
|
|
931
|
+
[state, attempts, check, reset]
|
|
262
932
|
);
|
|
263
933
|
}
|
|
264
934
|
|
|
265
935
|
// src/use-guided-tour.ts
|
|
266
|
-
import { useCallback as
|
|
936
|
+
import { useCallback as useCallback7, useEffect as useEffect7, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
|
|
937
|
+
function gateVerdict(step) {
|
|
938
|
+
const r = step?.gate?.();
|
|
939
|
+
return r === void 0 ? { status: "pass", feedback: void 0 } : toVerdict(r);
|
|
940
|
+
}
|
|
267
941
|
function useGuidedTour(steps, opts) {
|
|
268
|
-
const [running, setRunning] =
|
|
269
|
-
const runningRef =
|
|
942
|
+
const [running, setRunning] = useState6(false);
|
|
943
|
+
const runningRef = useRef7(running);
|
|
270
944
|
runningRef.current = running;
|
|
271
945
|
const core = useSteps(steps.length);
|
|
272
|
-
const stepsRef =
|
|
946
|
+
const stepsRef = useRef7(steps);
|
|
273
947
|
stepsRef.current = steps;
|
|
274
|
-
const driverRef =
|
|
948
|
+
const driverRef = useRef7(opts?.driver);
|
|
275
949
|
driverRef.current = opts?.driver;
|
|
276
|
-
const coreGoRef =
|
|
950
|
+
const coreGoRef = useRef7(core.go);
|
|
277
951
|
coreGoRef.current = core.go;
|
|
278
|
-
const
|
|
952
|
+
const cursorRef = useRef7(core.index);
|
|
953
|
+
cursorRef.current = core.index;
|
|
954
|
+
const isSkipped = useCallback7(
|
|
279
955
|
(i) => stepsRef.current[i]?.when?.() === false,
|
|
280
956
|
[]
|
|
281
957
|
);
|
|
282
|
-
const settle =
|
|
958
|
+
const settle = useCallback7(
|
|
283
959
|
(start2, dir) => {
|
|
284
960
|
const len = stepsRef.current.length;
|
|
285
961
|
let i = start2;
|
|
@@ -291,7 +967,7 @@ function useGuidedTour(steps, opts) {
|
|
|
291
967
|
},
|
|
292
968
|
[isSkipped]
|
|
293
969
|
);
|
|
294
|
-
const arrive =
|
|
970
|
+
const arrive = useCallback7(
|
|
295
971
|
(i, dir) => {
|
|
296
972
|
const target = settle(i, dir);
|
|
297
973
|
if (target === null) {
|
|
@@ -303,14 +979,14 @@ function useGuidedTour(steps, opts) {
|
|
|
303
979
|
},
|
|
304
980
|
[settle]
|
|
305
981
|
);
|
|
306
|
-
const goTo =
|
|
982
|
+
const goTo = useCallback7(
|
|
307
983
|
(i) => {
|
|
308
984
|
if (stepsRef.current.length === 0) return;
|
|
309
985
|
arrive(Math.max(0, Math.min(i, stepsRef.current.length - 1)), 1);
|
|
310
986
|
},
|
|
311
987
|
[arrive]
|
|
312
988
|
);
|
|
313
|
-
const goToId =
|
|
989
|
+
const goToId = useCallback7(
|
|
314
990
|
(id) => {
|
|
315
991
|
const i = stepsRef.current.findIndex((s) => s.id === id);
|
|
316
992
|
if (i >= 0) goTo(i);
|
|
@@ -318,11 +994,11 @@ function useGuidedTour(steps, opts) {
|
|
|
318
994
|
[goTo]
|
|
319
995
|
);
|
|
320
996
|
const index = running ? core.index : null;
|
|
321
|
-
|
|
997
|
+
useEffect7(() => {
|
|
322
998
|
if (!running) return;
|
|
323
999
|
stepsRef.current[core.index]?.onEnter?.();
|
|
324
1000
|
}, [running, core.index]);
|
|
325
|
-
|
|
1001
|
+
useEffect7(() => {
|
|
326
1002
|
const driver = driverRef.current;
|
|
327
1003
|
if (!driver) return;
|
|
328
1004
|
if (!running) {
|
|
@@ -341,14 +1017,14 @@ function useGuidedTour(steps, opts) {
|
|
|
341
1017
|
back: () => goTo(core.index - 1),
|
|
342
1018
|
end: () => setRunning(false)
|
|
343
1019
|
});
|
|
344
|
-
}, [running, core.index, goTo
|
|
345
|
-
|
|
346
|
-
const start =
|
|
1020
|
+
}, [running, core.index, goTo]);
|
|
1021
|
+
useEffect7(() => () => driverRef.current?.hide(), []);
|
|
1022
|
+
const start = useCallback7(() => {
|
|
347
1023
|
if (stepsRef.current.length === 0) return;
|
|
348
1024
|
arrive(0, 1);
|
|
349
1025
|
}, [arrive]);
|
|
350
|
-
const end =
|
|
351
|
-
const resolveNext =
|
|
1026
|
+
const end = useCallback7(() => setRunning(false), []);
|
|
1027
|
+
const resolveNext = useCallback7((i) => {
|
|
352
1028
|
const list = stepsRef.current;
|
|
353
1029
|
const edge = list[i]?.next?.();
|
|
354
1030
|
if (edge != null) {
|
|
@@ -357,30 +1033,35 @@ function useGuidedTour(steps, opts) {
|
|
|
357
1033
|
}
|
|
358
1034
|
return i + 1;
|
|
359
1035
|
}, []);
|
|
360
|
-
const next =
|
|
1036
|
+
const next = useCallback7(() => {
|
|
361
1037
|
if (!runningRef.current) return;
|
|
362
|
-
const i =
|
|
363
|
-
if (stepsRef.current[i]
|
|
1038
|
+
const i = cursorRef.current;
|
|
1039
|
+
if (gateVerdict(stepsRef.current[i]).status !== "pass") return;
|
|
364
1040
|
const target = resolveNext(i);
|
|
365
1041
|
if (target > stepsRef.current.length - 1) {
|
|
366
1042
|
setRunning(false);
|
|
367
1043
|
return;
|
|
368
1044
|
}
|
|
369
1045
|
arrive(target, 1);
|
|
370
|
-
}, [
|
|
371
|
-
const back =
|
|
1046
|
+
}, [arrive, resolveNext]);
|
|
1047
|
+
const back = useCallback7(() => {
|
|
372
1048
|
if (!runningRef.current) return;
|
|
373
|
-
const i =
|
|
1049
|
+
const i = cursorRef.current;
|
|
374
1050
|
if (i <= 0) return;
|
|
375
1051
|
arrive(i - 1, -1);
|
|
376
|
-
}, [
|
|
1052
|
+
}, [arrive]);
|
|
377
1053
|
const current = running ? stepsRef.current[core.index] ?? null : null;
|
|
378
|
-
const
|
|
379
|
-
|
|
1054
|
+
const verdict = current ? gateVerdict(current) : null;
|
|
1055
|
+
const blocked = verdict !== null && verdict.status !== "pass";
|
|
1056
|
+
const feedback = verdict?.feedback;
|
|
1057
|
+
return useMemo8(
|
|
1058
|
+
() => ({ index, current, active: running, blocked, feedback, start, end, next, back, goto: goTo, gotoId: goToId }),
|
|
1059
|
+
[index, current, running, blocked, feedback, start, end, next, back, goTo, goToId]
|
|
1060
|
+
);
|
|
380
1061
|
}
|
|
381
1062
|
|
|
382
1063
|
// src/use-chapters.ts
|
|
383
|
-
import { useCallback as
|
|
1064
|
+
import { useCallback as useCallback8, useEffect as useEffect8, useMemo as useMemo9, useRef as useRef8, useState as useState7 } from "react";
|
|
384
1065
|
|
|
385
1066
|
// src/flatten.ts
|
|
386
1067
|
function flattenChapters(chapters, depth = 0) {
|
|
@@ -399,17 +1080,17 @@ function flattenChapters(chapters, depth = 0) {
|
|
|
399
1080
|
|
|
400
1081
|
// src/use-chapters.ts
|
|
401
1082
|
function useChapters(chapters) {
|
|
402
|
-
const [mounted, setMounted] =
|
|
403
|
-
const flat =
|
|
404
|
-
const flatRef =
|
|
1083
|
+
const [mounted, setMounted] = useState7(false);
|
|
1084
|
+
const flat = useMemo9(() => flattenChapters(chapters), [chapters]);
|
|
1085
|
+
const flatRef = useRef8(flat);
|
|
405
1086
|
flatRef.current = flat;
|
|
406
|
-
const [active, setActive] =
|
|
407
|
-
const activeRef =
|
|
1087
|
+
const [active, setActive] = useState7(0);
|
|
1088
|
+
const activeRef = useRef8(0);
|
|
408
1089
|
activeRef.current = active;
|
|
409
1090
|
const core = useSteps(flat.length, { initialIndex: 0 });
|
|
410
|
-
const coreGoRef =
|
|
1091
|
+
const coreGoRef = useRef8(core.go);
|
|
411
1092
|
coreGoRef.current = core.go;
|
|
412
|
-
|
|
1093
|
+
useEffect8(() => {
|
|
413
1094
|
if (!mounted) return;
|
|
414
1095
|
let raf = 0;
|
|
415
1096
|
const update = () => {
|
|
@@ -447,8 +1128,8 @@ function useChapters(chapters) {
|
|
|
447
1128
|
if (raf) cancelAnimationFrame(raf);
|
|
448
1129
|
};
|
|
449
1130
|
}, [mounted, flat.length]);
|
|
450
|
-
|
|
451
|
-
const goTo =
|
|
1131
|
+
useEffect8(() => setMounted(true), []);
|
|
1132
|
+
const goTo = useCallback8((i) => {
|
|
452
1133
|
const entry = flatRef.current[i];
|
|
453
1134
|
if (!entry) return;
|
|
454
1135
|
setActive(i);
|
|
@@ -457,25 +1138,25 @@ function useChapters(chapters) {
|
|
|
457
1138
|
if (!el) return;
|
|
458
1139
|
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
459
1140
|
}, []);
|
|
460
|
-
const next =
|
|
1141
|
+
const next = useCallback8(() => {
|
|
461
1142
|
if (activeRef.current < flatRef.current.length - 1) goTo(activeRef.current + 1);
|
|
462
1143
|
}, [goTo]);
|
|
463
|
-
const prev =
|
|
1144
|
+
const prev = useCallback8(() => {
|
|
464
1145
|
if (activeRef.current > 0) goTo(activeRef.current - 1);
|
|
465
1146
|
}, [goTo]);
|
|
466
|
-
return { active: core.index, goTo, next, prev };
|
|
1147
|
+
return useMemo9(() => ({ active: core.index, goTo, next, prev }), [core.index, goTo, next, prev]);
|
|
467
1148
|
}
|
|
468
1149
|
|
|
469
1150
|
// src/use-global-keys.ts
|
|
470
|
-
import { useEffect as
|
|
1151
|
+
import { useEffect as useEffect9, useRef as useRef9 } from "react";
|
|
471
1152
|
var DEFAULT_IGNORED_TAGS = ["INPUT", "TEXTAREA", "SELECT", "BUTTON"];
|
|
472
1153
|
function useGlobalKeys(map, opts) {
|
|
473
|
-
const mapRef =
|
|
1154
|
+
const mapRef = useRef9(map);
|
|
474
1155
|
mapRef.current = map;
|
|
475
|
-
const ignoreRef =
|
|
476
|
-
const enabledRef =
|
|
1156
|
+
const ignoreRef = useRef9(new Set(opts?.ignoreTags ?? DEFAULT_IGNORED_TAGS));
|
|
1157
|
+
const enabledRef = useRef9(opts?.enabled ?? true);
|
|
477
1158
|
enabledRef.current = opts?.enabled ?? true;
|
|
478
|
-
|
|
1159
|
+
useEffect9(() => {
|
|
479
1160
|
const onKey = (e) => {
|
|
480
1161
|
const gate = enabledRef.current;
|
|
481
1162
|
const enabled = typeof gate === "function" ? gate() : gate;
|
|
@@ -493,7 +1174,7 @@ function useGlobalKeys(map, opts) {
|
|
|
493
1174
|
}
|
|
494
1175
|
|
|
495
1176
|
// src/use-discoveries.ts
|
|
496
|
-
import { useCallback as
|
|
1177
|
+
import { useCallback as useCallback9, useEffect as useEffect10, useMemo as useMemo10, useRef as useRef10, useState as useState8 } from "react";
|
|
497
1178
|
function localStorageStore(key = "stepwise.discoveries") {
|
|
498
1179
|
return {
|
|
499
1180
|
read() {
|
|
@@ -544,23 +1225,23 @@ function memoryStore(seed = []) {
|
|
|
544
1225
|
};
|
|
545
1226
|
}
|
|
546
1227
|
function useDiscoveries(opts) {
|
|
547
|
-
const defaultStoreRef =
|
|
548
|
-
const storeRef =
|
|
1228
|
+
const defaultStoreRef = useRef10(null);
|
|
1229
|
+
const storeRef = useRef10(null);
|
|
549
1230
|
if (opts?.store) {
|
|
550
1231
|
storeRef.current = opts.store;
|
|
551
1232
|
} else {
|
|
552
1233
|
if (!defaultStoreRef.current) defaultStoreRef.current = localStorageStore(opts?.key);
|
|
553
1234
|
storeRef.current = defaultStoreRef.current;
|
|
554
1235
|
}
|
|
555
|
-
const [ids, setIds] =
|
|
556
|
-
const [ready, setReady] =
|
|
557
|
-
const idsRef =
|
|
1236
|
+
const [ids, setIds] = useState8(() => /* @__PURE__ */ new Set());
|
|
1237
|
+
const [ready, setReady] = useState8(false);
|
|
1238
|
+
const idsRef = useRef10(ids);
|
|
558
1239
|
idsRef.current = ids;
|
|
559
|
-
|
|
1240
|
+
useEffect10(() => {
|
|
560
1241
|
setIds(new Set(storeRef.current.read()));
|
|
561
1242
|
setReady(true);
|
|
562
1243
|
}, []);
|
|
563
|
-
const markDiscovered =
|
|
1244
|
+
const markDiscovered = useCallback9((id) => {
|
|
564
1245
|
const additions = Array.isArray(id) ? id : [id];
|
|
565
1246
|
const merged = new Set(idsRef.current);
|
|
566
1247
|
let changed = false;
|
|
@@ -575,24 +1256,24 @@ function useDiscoveries(opts) {
|
|
|
575
1256
|
setIds(merged);
|
|
576
1257
|
storeRef.current.write([...merged]);
|
|
577
1258
|
}, []);
|
|
578
|
-
const reset =
|
|
1259
|
+
const reset = useCallback9(() => {
|
|
579
1260
|
const empty = /* @__PURE__ */ new Set();
|
|
580
1261
|
idsRef.current = empty;
|
|
581
1262
|
setIds(empty);
|
|
582
1263
|
storeRef.current.write([]);
|
|
583
1264
|
}, []);
|
|
584
|
-
const isDiscovered =
|
|
585
|
-
return
|
|
1265
|
+
const isDiscovered = useCallback9((id) => idsRef.current.has(id), []);
|
|
1266
|
+
return useMemo10(
|
|
586
1267
|
() => ({ discovered: ids, ready, isDiscovered, markDiscovered, reset }),
|
|
587
1268
|
[ids, ready, isDiscovered, markDiscovered, reset]
|
|
588
1269
|
);
|
|
589
1270
|
}
|
|
590
1271
|
|
|
591
1272
|
// src/use-focus-trap.ts
|
|
592
|
-
import { useEffect as
|
|
1273
|
+
import { useEffect as useEffect11, useRef as useRef11 } from "react";
|
|
593
1274
|
function useFocusTrap(active) {
|
|
594
|
-
const ref =
|
|
595
|
-
|
|
1275
|
+
const ref = useRef11(null);
|
|
1276
|
+
useEffect11(() => {
|
|
596
1277
|
if (!active) return;
|
|
597
1278
|
const container = ref.current;
|
|
598
1279
|
if (!container) return;
|
|
@@ -626,31 +1307,17 @@ function useFocusTrap(active) {
|
|
|
626
1307
|
return ref;
|
|
627
1308
|
}
|
|
628
1309
|
|
|
629
|
-
// src/use-prefers-reduced-motion.ts
|
|
630
|
-
import { useEffect as useEffect8, useState as useState6 } from "react";
|
|
631
|
-
function usePrefersReducedMotion() {
|
|
632
|
-
const [reduced, setReduced] = useState6(false);
|
|
633
|
-
useEffect8(() => {
|
|
634
|
-
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
635
|
-
setReduced(mq.matches);
|
|
636
|
-
const onChange = (e) => setReduced(e.matches);
|
|
637
|
-
mq.addEventListener("change", onChange);
|
|
638
|
-
return () => mq.removeEventListener("change", onChange);
|
|
639
|
-
}, []);
|
|
640
|
-
return reduced;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
1310
|
// src/glossary.tsx
|
|
644
1311
|
import {
|
|
645
1312
|
createContext,
|
|
646
|
-
useCallback as
|
|
1313
|
+
useCallback as useCallback10,
|
|
647
1314
|
useContext,
|
|
648
|
-
useEffect as
|
|
1315
|
+
useEffect as useEffect12,
|
|
649
1316
|
useId,
|
|
650
|
-
useLayoutEffect,
|
|
651
|
-
useMemo as
|
|
652
|
-
useRef as
|
|
653
|
-
useState as
|
|
1317
|
+
useLayoutEffect as useLayoutEffect3,
|
|
1318
|
+
useMemo as useMemo11,
|
|
1319
|
+
useRef as useRef12,
|
|
1320
|
+
useState as useState9
|
|
654
1321
|
} from "react";
|
|
655
1322
|
import { createPortal } from "react-dom";
|
|
656
1323
|
|
|
@@ -708,7 +1375,7 @@ function GlossaryProvider({
|
|
|
708
1375
|
theme = defaultTheme,
|
|
709
1376
|
children
|
|
710
1377
|
}) {
|
|
711
|
-
const value =
|
|
1378
|
+
const value = useMemo11(
|
|
712
1379
|
() => ({ byId: new Map(entries.map((e) => [e.id, e])), entries, theme }),
|
|
713
1380
|
[entries, theme]
|
|
714
1381
|
);
|
|
@@ -718,23 +1385,23 @@ function Term({ id, children, className }) {
|
|
|
718
1385
|
const ctx = useContext(GlossaryContext);
|
|
719
1386
|
const depth = useContext(DepthContext);
|
|
720
1387
|
const entry = ctx?.byId.get(id);
|
|
721
|
-
const [open, setOpen] =
|
|
722
|
-
const [pos, setPos] =
|
|
723
|
-
const openRef =
|
|
724
|
-
const wrapperRef =
|
|
725
|
-
const triggerRef =
|
|
726
|
-
const cardRef =
|
|
727
|
-
const openTimer =
|
|
728
|
-
const closeTimer =
|
|
1388
|
+
const [open, setOpen] = useState9(false);
|
|
1389
|
+
const [pos, setPos] = useState9(null);
|
|
1390
|
+
const openRef = useRef12(false);
|
|
1391
|
+
const wrapperRef = useRef12(null);
|
|
1392
|
+
const triggerRef = useRef12(null);
|
|
1393
|
+
const cardRef = useRef12(null);
|
|
1394
|
+
const openTimer = useRef12(0);
|
|
1395
|
+
const closeTimer = useRef12(0);
|
|
729
1396
|
const tipId = useId();
|
|
730
1397
|
const display = children ?? entry?.term ?? id;
|
|
731
|
-
const closeNow =
|
|
1398
|
+
const closeNow = useCallback10(() => {
|
|
732
1399
|
window.clearTimeout(openTimer.current);
|
|
733
1400
|
window.clearTimeout(closeTimer.current);
|
|
734
1401
|
openRef.current = false;
|
|
735
1402
|
setOpen(false);
|
|
736
1403
|
}, []);
|
|
737
|
-
const openNow =
|
|
1404
|
+
const openNow = useCallback10((focusCard = false) => {
|
|
738
1405
|
window.clearTimeout(closeTimer.current);
|
|
739
1406
|
window.clearTimeout(openTimer.current);
|
|
740
1407
|
openRef.current = true;
|
|
@@ -743,18 +1410,18 @@ function Term({ id, children, className }) {
|
|
|
743
1410
|
requestAnimationFrame(() => cardRef.current?.focus({ preventScroll: true }));
|
|
744
1411
|
}
|
|
745
1412
|
}, []);
|
|
746
|
-
const scheduleOpen =
|
|
1413
|
+
const scheduleOpen = useCallback10(() => {
|
|
747
1414
|
window.clearTimeout(closeTimer.current);
|
|
748
1415
|
if (openRef.current) return;
|
|
749
1416
|
window.clearTimeout(openTimer.current);
|
|
750
1417
|
openTimer.current = window.setTimeout(() => openNow(), OPEN_DELAY_MS);
|
|
751
1418
|
}, [openNow]);
|
|
752
|
-
const scheduleClose =
|
|
1419
|
+
const scheduleClose = useCallback10(() => {
|
|
753
1420
|
window.clearTimeout(openTimer.current);
|
|
754
1421
|
window.clearTimeout(closeTimer.current);
|
|
755
1422
|
closeTimer.current = window.setTimeout(closeNow, CLOSE_GRACE_MS);
|
|
756
1423
|
}, [closeNow]);
|
|
757
|
-
|
|
1424
|
+
useLayoutEffect3(() => {
|
|
758
1425
|
if (!open) {
|
|
759
1426
|
setPos(null);
|
|
760
1427
|
return;
|
|
@@ -791,7 +1458,7 @@ function Term({ id, children, className }) {
|
|
|
791
1458
|
if (raf) cancelAnimationFrame(raf);
|
|
792
1459
|
};
|
|
793
1460
|
}, [open]);
|
|
794
|
-
|
|
1461
|
+
useEffect12(() => {
|
|
795
1462
|
if (!open) return;
|
|
796
1463
|
const onPointerDown = (e) => {
|
|
797
1464
|
const target = e.target;
|
|
@@ -802,7 +1469,7 @@ function Term({ id, children, className }) {
|
|
|
802
1469
|
document.addEventListener("pointerdown", onPointerDown);
|
|
803
1470
|
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
804
1471
|
}, [open, closeNow]);
|
|
805
|
-
|
|
1472
|
+
useEffect12(() => () => {
|
|
806
1473
|
window.clearTimeout(openTimer.current);
|
|
807
1474
|
window.clearTimeout(closeTimer.current);
|
|
808
1475
|
}, []);
|
|
@@ -854,6 +1521,7 @@ function Term({ id, children, className }) {
|
|
|
854
1521
|
"span",
|
|
855
1522
|
{
|
|
856
1523
|
ref: wrapperRef,
|
|
1524
|
+
"data-stepwise": "",
|
|
857
1525
|
className: "relative inline",
|
|
858
1526
|
onMouseEnter: interactive ? scheduleOpen : void 0,
|
|
859
1527
|
onMouseLeave: interactive ? scheduleClose : void 0,
|
|
@@ -876,6 +1544,7 @@ function Term({ id, children, className }) {
|
|
|
876
1544
|
"span",
|
|
877
1545
|
{
|
|
878
1546
|
ref: cardRef,
|
|
1547
|
+
"data-stepwise": "",
|
|
879
1548
|
role: "tooltip",
|
|
880
1549
|
id: tipId,
|
|
881
1550
|
tabIndex: -1,
|
|
@@ -946,14 +1615,14 @@ function GlossaryAppendix({
|
|
|
946
1615
|
if (bare) {
|
|
947
1616
|
return /* @__PURE__ */ jsx(AppendixContext.Provider, { value: true, children: body });
|
|
948
1617
|
}
|
|
949
|
-
return /* @__PURE__ */ jsx(AppendixContext.Provider, { value: true, children: /* @__PURE__ */ jsxs("section", { "aria-label": title, className: cn("mt-16 border-t border-white/10 pt-10", className), children: [
|
|
1618
|
+
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: [
|
|
950
1619
|
/* @__PURE__ */ jsx("p", { className: cn("text-[11px] font-mono uppercase tracking-widest mb-6", theme.muted), children: title }),
|
|
951
1620
|
body
|
|
952
1621
|
] }) });
|
|
953
1622
|
}
|
|
954
1623
|
|
|
955
1624
|
// src/components.tsx
|
|
956
|
-
import { useEffect as
|
|
1625
|
+
import { useEffect as useEffect13, useState as useState10 } from "react";
|
|
957
1626
|
import { createPortal as createPortal2 } from "react-dom";
|
|
958
1627
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
959
1628
|
var TONE_SEGMENT_CLASS = defaultTheme.toneSegment;
|
|
@@ -975,7 +1644,7 @@ function CodeBlock({
|
|
|
975
1644
|
theme = defaultTheme,
|
|
976
1645
|
className
|
|
977
1646
|
}) {
|
|
978
|
-
return /* @__PURE__ */ jsx2("div", { className: cn(theme.codeBlock, className), children: lines.map((line, i) => {
|
|
1647
|
+
return /* @__PURE__ */ jsx2("div", { "data-stepwise": "", className: cn(theme.codeBlock, className), children: lines.map((line, i) => {
|
|
979
1648
|
const commentStart = line.indexOf("//");
|
|
980
1649
|
if (commentStart === 0) {
|
|
981
1650
|
return /* @__PURE__ */ jsx2("div", { className: cn("whitespace-pre", theme.codeMuted), children: line }, i);
|
|
@@ -1022,7 +1691,7 @@ function StepScrubber({
|
|
|
1022
1691
|
}
|
|
1023
1692
|
};
|
|
1024
1693
|
if (count === 0) {
|
|
1025
|
-
return /* @__PURE__ */ jsx2("div", { 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 }) });
|
|
1694
|
+
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 }) });
|
|
1026
1695
|
}
|
|
1027
1696
|
const valueText = labels?.frame ? labels.frame(index) : titleAt?.(index) ?? `frame ${index + 1} of ${count}`;
|
|
1028
1697
|
const segment = (i) => /* @__PURE__ */ jsx2(
|
|
@@ -1064,6 +1733,7 @@ function StepScrubber({
|
|
|
1064
1733
|
return /* @__PURE__ */ jsx2(
|
|
1065
1734
|
"div",
|
|
1066
1735
|
{
|
|
1736
|
+
"data-stepwise": "",
|
|
1067
1737
|
role: "slider",
|
|
1068
1738
|
tabIndex: 0,
|
|
1069
1739
|
"aria-label": labels?.scrubber ?? "Frames",
|
|
@@ -1091,17 +1761,21 @@ function PlayerBar({
|
|
|
1091
1761
|
className,
|
|
1092
1762
|
theme = defaultTheme,
|
|
1093
1763
|
groupOf,
|
|
1094
|
-
labels
|
|
1764
|
+
labels,
|
|
1765
|
+
live
|
|
1095
1766
|
}) {
|
|
1096
1767
|
const L = {
|
|
1097
1768
|
previous: labels?.previous ?? "Previous step",
|
|
1098
1769
|
play: labels?.play ?? "Play",
|
|
1099
1770
|
pause: labels?.pause ?? "Pause",
|
|
1100
1771
|
replay: labels?.replay ?? "Replay",
|
|
1101
|
-
next: labels?.next ?? "Next step"
|
|
1772
|
+
next: labels?.next ?? "Next step",
|
|
1773
|
+
live: labels?.live ?? "Live",
|
|
1774
|
+
goLive: labels?.goLive ?? "Go live"
|
|
1102
1775
|
};
|
|
1776
|
+
const behind = Math.max(0, player.total - 1 - Math.max(player.index, 0));
|
|
1103
1777
|
const playLabel = player.playing ? L.pause : player.total > 0 && player.atEnd ? L.replay : L.play;
|
|
1104
|
-
return /* @__PURE__ */ jsxs2("div", { className: cn("flex items-center gap-2 p-2", className), children: [
|
|
1778
|
+
return /* @__PURE__ */ jsxs2("div", { "data-stepwise": "", className: cn("flex items-center gap-2 p-2", className), children: [
|
|
1105
1779
|
leading,
|
|
1106
1780
|
/* @__PURE__ */ jsx2(StepButton, { variant: "ghost", onClick: () => player.prev(), className: "h-7 px-2 text-xs", "aria-label": L.previous, children: "\u2190" }),
|
|
1107
1781
|
/* @__PURE__ */ jsx2(StepButton, { variant: "outline", onClick: () => player.toggle(onEmpty), className: "h-7 px-3.5 text-[11px]", children: playLabel }),
|
|
@@ -1119,7 +1793,15 @@ function PlayerBar({
|
|
|
1119
1793
|
groupOf
|
|
1120
1794
|
}
|
|
1121
1795
|
),
|
|
1122
|
-
/* @__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" })
|
|
1796
|
+
/* @__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" }),
|
|
1797
|
+
live && (player.live || behind === 0 ? /* @__PURE__ */ jsxs2("span", { role: "status", className: "flex items-center gap-1 text-[10px] font-mono uppercase text-primary shrink-0", children: [
|
|
1798
|
+
/* @__PURE__ */ jsx2("span", { "aria-hidden": "true", className: cn("h-1.5 w-1.5 rounded-full bg-primary", player.playing && "motion-safe:animate-pulse") }),
|
|
1799
|
+
L.live
|
|
1800
|
+
] }) : /* @__PURE__ */ jsxs2(StepButton, { variant: "ghost", onClick: () => player.goLive?.(), className: "h-7 px-2 text-[10px] font-mono shrink-0", children: [
|
|
1801
|
+
L.goLive,
|
|
1802
|
+
" +",
|
|
1803
|
+
behind
|
|
1804
|
+
] }))
|
|
1123
1805
|
] });
|
|
1124
1806
|
}
|
|
1125
1807
|
function ChapterNav({
|
|
@@ -1163,6 +1845,7 @@ function ChapterNav({
|
|
|
1163
1845
|
return /* @__PURE__ */ jsx2(
|
|
1164
1846
|
"nav",
|
|
1165
1847
|
{
|
|
1848
|
+
"data-stepwise": "",
|
|
1166
1849
|
"aria-label": "Chapters",
|
|
1167
1850
|
className: cn(
|
|
1168
1851
|
"sticky top-20 md:top-24 z-30 rounded-sm shadow-[0_8px_24px_rgba(0,0,0,0.35)]",
|
|
@@ -1186,7 +1869,7 @@ function LessonPager({
|
|
|
1186
1869
|
className,
|
|
1187
1870
|
labels
|
|
1188
1871
|
}) {
|
|
1189
|
-
return /* @__PURE__ */ jsxs2("div", { className: cn("flex items-center gap-2", className), children: [
|
|
1872
|
+
return /* @__PURE__ */ jsxs2("div", { "data-stepwise": "", className: cn("flex items-center gap-2", className), children: [
|
|
1190
1873
|
/* @__PURE__ */ jsxs2(
|
|
1191
1874
|
StepButton,
|
|
1192
1875
|
{
|
|
@@ -1223,8 +1906,8 @@ function TourSpotlight({
|
|
|
1223
1906
|
target,
|
|
1224
1907
|
theme = defaultTheme
|
|
1225
1908
|
}) {
|
|
1226
|
-
const [rect, setRect] =
|
|
1227
|
-
|
|
1909
|
+
const [rect, setRect] = useState10(null);
|
|
1910
|
+
useEffect13(() => {
|
|
1228
1911
|
if (!target) {
|
|
1229
1912
|
setRect(null);
|
|
1230
1913
|
return;
|
|
@@ -1256,7 +1939,7 @@ function TourSpotlight({
|
|
|
1256
1939
|
}, [target]);
|
|
1257
1940
|
if (!rect) return null;
|
|
1258
1941
|
const pad = 6;
|
|
1259
|
-
return /* @__PURE__ */ jsx2("div", { className: "fixed inset-0 z-40 pointer-events-none", children: /* @__PURE__ */ jsx2(
|
|
1942
|
+
return /* @__PURE__ */ jsx2("div", { "data-stepwise": "", className: "fixed inset-0 z-40 pointer-events-none", children: /* @__PURE__ */ jsx2(
|
|
1260
1943
|
"div",
|
|
1261
1944
|
{
|
|
1262
1945
|
className: cn("absolute", theme.spotlight),
|
|
@@ -1280,7 +1963,8 @@ function TourCard({
|
|
|
1280
1963
|
labels,
|
|
1281
1964
|
className,
|
|
1282
1965
|
theme = defaultTheme,
|
|
1283
|
-
nextDisabled = false
|
|
1966
|
+
nextDisabled = false,
|
|
1967
|
+
feedback
|
|
1284
1968
|
}) {
|
|
1285
1969
|
const L = {
|
|
1286
1970
|
title: labels?.title ?? "Guided tour",
|
|
@@ -1294,6 +1978,7 @@ function TourCard({
|
|
|
1294
1978
|
"div",
|
|
1295
1979
|
{
|
|
1296
1980
|
ref: trapRef,
|
|
1981
|
+
"data-stepwise": "",
|
|
1297
1982
|
role: "dialog",
|
|
1298
1983
|
"aria-modal": "true",
|
|
1299
1984
|
"aria-label": `${L.title} \xB7 ${index + 1}/${total}`,
|
|
@@ -1327,6 +2012,7 @@ function TourCard({
|
|
|
1327
2012
|
/* @__PURE__ */ jsx2("p", { className: "text-sm font-bold text-foreground/95 mb-1.5", children: step.title }),
|
|
1328
2013
|
/* @__PURE__ */ jsx2("div", { className: "text-[12px] font-mono text-foreground/70 leading-relaxed", children: step.body }),
|
|
1329
2014
|
step.waitHint && /* @__PURE__ */ jsx2("p", { className: "text-[11px] font-mono text-accent mt-2 animate-pulse", children: step.waitHint }),
|
|
2015
|
+
/* @__PURE__ */ jsx2("div", { "aria-live": "polite", children: feedback != null && feedback !== false && /* @__PURE__ */ jsx2("p", { className: "text-[11px] font-mono text-foreground/70 mt-2", children: feedback }) }),
|
|
1330
2016
|
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 mt-3 pt-3 border-t border-white/10", children: [
|
|
1331
2017
|
/* @__PURE__ */ jsx2(StepButton, { variant: "outline", onClick: onBack, disabled: index === 0, className: "h-7 px-3 text-[11px]", children: L.back }),
|
|
1332
2018
|
/* @__PURE__ */ jsx2(
|
|
@@ -1352,10 +2038,10 @@ function ShortcutsDialog({
|
|
|
1352
2038
|
className,
|
|
1353
2039
|
theme = defaultTheme
|
|
1354
2040
|
}) {
|
|
1355
|
-
const [mounted, setMounted] =
|
|
2041
|
+
const [mounted, setMounted] = useState10(false);
|
|
1356
2042
|
const trapRef = useFocusTrap(open);
|
|
1357
|
-
|
|
1358
|
-
|
|
2043
|
+
useEffect13(() => setMounted(true), []);
|
|
2044
|
+
useEffect13(() => {
|
|
1359
2045
|
if (!open) return;
|
|
1360
2046
|
const onKey = (e) => {
|
|
1361
2047
|
if (e.key === "Escape") onClose();
|
|
@@ -1368,6 +2054,7 @@ function ShortcutsDialog({
|
|
|
1368
2054
|
/* @__PURE__ */ jsx2(
|
|
1369
2055
|
"div",
|
|
1370
2056
|
{
|
|
2057
|
+
"data-stepwise": "",
|
|
1371
2058
|
className: cn("fixed inset-0 z-50 flex items-center justify-center p-4", theme.overlay, className),
|
|
1372
2059
|
onClick: onClose,
|
|
1373
2060
|
children: /* @__PURE__ */ jsxs2(
|
|
@@ -1424,18 +2111,35 @@ export {
|
|
|
1424
2111
|
Term,
|
|
1425
2112
|
TourCard,
|
|
1426
2113
|
TourSpotlight,
|
|
2114
|
+
alignTracks,
|
|
1427
2115
|
cookieStore,
|
|
2116
|
+
createRng,
|
|
2117
|
+
createTrace,
|
|
1428
2118
|
defaultTheme,
|
|
2119
|
+
fail,
|
|
1429
2120
|
flattenChapters,
|
|
1430
2121
|
localStorageStore,
|
|
1431
2122
|
memoryStore,
|
|
2123
|
+
pass,
|
|
2124
|
+
pending,
|
|
2125
|
+
readHashParam,
|
|
2126
|
+
reanchor,
|
|
2127
|
+
runSimulation,
|
|
2128
|
+
toVerdict,
|
|
1432
2129
|
useChapters,
|
|
1433
2130
|
useDiscoveries,
|
|
2131
|
+
useEvaluator,
|
|
2132
|
+
useFadeIn,
|
|
1434
2133
|
useFocusTrap,
|
|
1435
2134
|
useGlobalKeys,
|
|
1436
2135
|
useGuidedTour,
|
|
2136
|
+
useLessonStream,
|
|
1437
2137
|
usePrefersReducedMotion,
|
|
2138
|
+
useSimulation,
|
|
1438
2139
|
useStepPlayer,
|
|
1439
|
-
useSteps
|
|
2140
|
+
useSteps,
|
|
2141
|
+
useSyncedTracks,
|
|
2142
|
+
useTrace,
|
|
2143
|
+
writeHashParam
|
|
1440
2144
|
};
|
|
1441
2145
|
//# sourceMappingURL=index.js.map
|