@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.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as PlayerEngine, a as TourStepInput, T as TourDriver, C as Chapter,
|
|
2
|
-
export {
|
|
1
|
+
import { P as PlayerEngine, S as StepwiseStep, a as StepTone, V as VerdictStatus, b as Verdict, E as Evaluator, c as TourStepInput, T as TourDriver, C as Chapter, d as ShortcutItem } from './types-CNMxISXe.js';
|
|
2
|
+
export { e as EvaluatorResult, f as PlayerEngineApi, g as PlayerEngineHandle, h as TourContext, i as fail, p as pass, j as pending, t as toVerdict } from './types-CNMxISXe.js';
|
|
3
3
|
import * as react from 'react';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
5
5
|
|
|
@@ -53,7 +53,8 @@ interface StepPlayerOptions {
|
|
|
53
53
|
engine?: PlayerEngine;
|
|
54
54
|
/**
|
|
55
55
|
* Deep-link the current frame into the URL hash (`#<key>=<index>`), opt-in.
|
|
56
|
-
* The initial hash is honored once on mount.
|
|
56
|
+
* The initial hash is honored once on mount. Only this key is written —
|
|
57
|
+
* other `&`-separated hash parts (anchors, other players' keys) are kept.
|
|
57
58
|
*/
|
|
58
59
|
syncHash?: string;
|
|
59
60
|
/**
|
|
@@ -62,6 +63,30 @@ interface StepPlayerOptions {
|
|
|
62
63
|
* reset/new timeline) — the next full playthrough fires it again.
|
|
63
64
|
*/
|
|
64
65
|
onEnd?: () => void;
|
|
66
|
+
/**
|
|
67
|
+
* Honor `prefers-reduced-motion: reduce` by not auto-advancing: with
|
|
68
|
+
* `autoPlay`, the player still lands on frame 0 but stays paused until the
|
|
69
|
+
* user presses play. Default true.
|
|
70
|
+
*/
|
|
71
|
+
respectReducedMotion?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Checkpoints: when it returns true for the frame playback just reached,
|
|
74
|
+
* the built-in timer pauses there and reports `held` until `release()` —
|
|
75
|
+
* "stop and predict what happens next". Manual stepping and seeks are never
|
|
76
|
+
* held (the reader keeps control). Ignored when a custom `engine` drives
|
|
77
|
+
* playback. Each checkpoint holds once per playthrough.
|
|
78
|
+
*/
|
|
79
|
+
holdAt?: (frame: number) => boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Follow the live edge of a growing timeline (a stream, a log, a game's
|
|
82
|
+
* move history). When frames are appended while the reader is live
|
|
83
|
+
* (playing, or parked on the last frame), playback continues through them
|
|
84
|
+
* at the normal pace. A reader who scrubbed back or paused mid-trace stays
|
|
85
|
+
* put — `live` turns false and `goLive()` returns them. With `autoPlay`, a
|
|
86
|
+
* player that mounts empty starts on the first frames. Checkpoints still
|
|
87
|
+
* hold; a custom `engine` owns its own timing and ignores this.
|
|
88
|
+
*/
|
|
89
|
+
follow?: boolean;
|
|
65
90
|
}
|
|
66
91
|
interface StepPlayer {
|
|
67
92
|
/** Total frames in the current timeline. */
|
|
@@ -80,6 +105,28 @@ interface StepPlayer {
|
|
|
80
105
|
* not set it — `toggle()` at `ended` replays from frame 0.
|
|
81
106
|
*/
|
|
82
107
|
ended: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* The checkpoint frame playback is paused on (see `holdAt`), or null.
|
|
110
|
+
* Cleared by `release()` or any cursor movement.
|
|
111
|
+
*/
|
|
112
|
+
held: number | null;
|
|
113
|
+
/** Release the current checkpoint and resume playing. No-op when not held. */
|
|
114
|
+
release: () => void;
|
|
115
|
+
/**
|
|
116
|
+
* Move the cursor without touching playback state: playing stays playing,
|
|
117
|
+
* completion and checkpoints are kept. For re-anchoring after the timeline
|
|
118
|
+
* changes underneath (`useTrace` with keyed steps) — user-facing moves
|
|
119
|
+
* should use `seek`.
|
|
120
|
+
*/
|
|
121
|
+
jump: (frame: number) => void;
|
|
122
|
+
/**
|
|
123
|
+
* With `follow`: the reader is at the live edge (playing toward it, parked
|
|
124
|
+
* on the last frame, or waiting to start), so appended frames will play.
|
|
125
|
+
* Always false without `follow`.
|
|
126
|
+
*/
|
|
127
|
+
live: boolean;
|
|
128
|
+
/** Move to the last frame (paused, so new frames play from there with `follow`). */
|
|
129
|
+
goLive: () => void;
|
|
83
130
|
/** Jump to frame 0 and play. */
|
|
84
131
|
start: () => void;
|
|
85
132
|
/** Clear the player (frame = -1, paused). */
|
|
@@ -94,7 +141,358 @@ interface StepPlayer {
|
|
|
94
141
|
setPlaying: (playing: boolean) => void;
|
|
95
142
|
setSpeed: (speed: number) => void;
|
|
96
143
|
}
|
|
97
|
-
declare function useStepPlayer({ total, baseDelayMs, autoPlay, engine, syncHash, onEnd }: StepPlayerOptions): StepPlayer;
|
|
144
|
+
declare function useStepPlayer({ total, baseDelayMs, autoPlay, engine, syncHash, onEnd, respectReducedMotion, holdAt, follow, }: StepPlayerOptions): StepPlayer;
|
|
145
|
+
/** Value of `key` in the hash, or null when absent. */
|
|
146
|
+
declare function readHashParam(hash: string, key: string): string | null;
|
|
147
|
+
/** The hash with `key` set to `value` — replaced in place, else appended. */
|
|
148
|
+
declare function writeHashParam(hash: string, key: string, value: string): string;
|
|
149
|
+
|
|
150
|
+
interface TraceOptions<S extends StepwiseStep> extends Omit<StepPlayerOptions, "total" | "holdAt"> {
|
|
151
|
+
/**
|
|
152
|
+
* Checkpoints (see `StepPlayerOptions.holdAt`), with the step at hand:
|
|
153
|
+
* `holdAt: (_, step) => step?.kind === "compare"`. Always sees the current
|
|
154
|
+
* trace, also after it's recomputed.
|
|
155
|
+
*/
|
|
156
|
+
holdAt?: (frame: number, step: S | undefined) => boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Step identity used to keep the playhead in place when `steps` changes
|
|
159
|
+
* (see `reanchor`). Defaults to each step's `key` field. Traces without
|
|
160
|
+
* keys keep the old behavior: the same index, clamped.
|
|
161
|
+
*/
|
|
162
|
+
keyOf?: (step: S, index: number) => string | undefined;
|
|
163
|
+
}
|
|
164
|
+
interface TracePlayer<S extends StepwiseStep> {
|
|
165
|
+
/** The underlying player — pass it to PlayerBar. */
|
|
166
|
+
player: StepPlayer;
|
|
167
|
+
/**
|
|
168
|
+
* The frame to render. Never undefined for a non-empty trace: before the
|
|
169
|
+
* player starts (`player.index === -1`) this is frame 0, not the last one.
|
|
170
|
+
*/
|
|
171
|
+
step: S | undefined;
|
|
172
|
+
/** Index of `step` in the trace (0 before start; -1 for an empty trace). */
|
|
173
|
+
frame: number;
|
|
174
|
+
/** Stable `toneAt` for PlayerBar / StepScrubber. */
|
|
175
|
+
toneAt: (i: number) => StepTone;
|
|
176
|
+
/** Stable `titleAt` for PlayerBar / StepScrubber. */
|
|
177
|
+
titleAt: (i: number) => string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A player bound to a concrete trace: the steps array is the timeline, and
|
|
181
|
+
* the hook resolves the frame to render (frame 0 before start — invariant
|
|
182
|
+
* "index -1 means not started"), plus scrubber accessors that read the
|
|
183
|
+
* latest steps through a ref. Replaces the per-page
|
|
184
|
+
* `player.index >= 0 ? … : 0` boilerplate.
|
|
185
|
+
*
|
|
186
|
+
* const trace = useTrace(STEPS, { autoPlay: true });
|
|
187
|
+
* <Viz arr={trace.step.arr} />
|
|
188
|
+
* <PlayerBar player={trace.player} toneAt={trace.toneAt} titleAt={trace.titleAt} />
|
|
189
|
+
*
|
|
190
|
+
* `steps` may change (new input, new seed — see `useSimulation`). Keyed
|
|
191
|
+
* steps re-anchor: the playhead stays on the same logical step, and playback
|
|
192
|
+
* state is untouched.
|
|
193
|
+
*/
|
|
194
|
+
declare function useTrace<S extends StepwiseStep>(steps: readonly S[], opts?: TraceOptions<S>): TracePlayer<S>;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Where should the playhead go when the trace changes underneath it?
|
|
198
|
+
*
|
|
199
|
+
* Walk back from the old position to the most recent step whose key still
|
|
200
|
+
* exists in the new trace, and land on that step there. So editing an input
|
|
201
|
+
* keeps you on "the same moment" of the algorithm when it still happens, and
|
|
202
|
+
* otherwise on the last moment both runs share — never somewhere unrelated.
|
|
203
|
+
* With no shared key at all, the playhead returns to frame 0.
|
|
204
|
+
*
|
|
205
|
+
* Pure: `useTrace` calls it during render, and it's exported for custom
|
|
206
|
+
* players. Keys come from `keyOf`, by default each step's `key` field; a
|
|
207
|
+
* step without a key never matches.
|
|
208
|
+
*/
|
|
209
|
+
declare function reanchor<A, B>(oldSteps: readonly A[], oldIndex: number, newSteps: readonly B[], keyOf: (step: A | B, index: number) => string | undefined): number;
|
|
210
|
+
|
|
211
|
+
/** Deterministic pseudo-random source (mulberry32). */
|
|
212
|
+
interface Rng {
|
|
213
|
+
/** The seed this generator was created from (numbers pass through; strings are hashed). */
|
|
214
|
+
readonly seed: number;
|
|
215
|
+
/** Uniform float in [0, 1). */
|
|
216
|
+
next(): number;
|
|
217
|
+
/** Uniform integer in [min, max], both inclusive. */
|
|
218
|
+
int(min: number, max: number): number;
|
|
219
|
+
/** A uniformly chosen element. Throws on an empty array. */
|
|
220
|
+
pick<T>(items: readonly T[]): T;
|
|
221
|
+
/** A shuffled copy (Fisher-Yates); the input is not modified. */
|
|
222
|
+
shuffle<T>(items: readonly T[]): T[];
|
|
223
|
+
}
|
|
224
|
+
declare function createRng(seed?: number | string): Rng;
|
|
225
|
+
interface SimulationContext {
|
|
226
|
+
/** Seeded randomness. Use it instead of Math.random to keep runs replayable. */
|
|
227
|
+
rng: Rng;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A simulation turns an input into a trace. Write it as a generator that
|
|
231
|
+
* yields one step per moment worth showing (or return an array):
|
|
232
|
+
*
|
|
233
|
+
* function* bubble(input: number[]): Generator<SortStep> {
|
|
234
|
+
* const arr = [...input];
|
|
235
|
+
* for (let pass = 0; …) {
|
|
236
|
+
* yield { key: `p${pass}:j${j}`, tone: "neutral", label: "compare", arr: [...arr] };
|
|
237
|
+
* }
|
|
238
|
+
* }
|
|
239
|
+
*
|
|
240
|
+
* Yield snapshots (copies), not live references: every step is kept.
|
|
241
|
+
* Give steps a `key` so the playhead stays put when the input changes.
|
|
242
|
+
*/
|
|
243
|
+
type Simulation<Input, S extends StepwiseStep> = (input: Input, ctx: SimulationContext) => Iterable<S>;
|
|
244
|
+
interface SimulationRunOptions {
|
|
245
|
+
/** Seed for `ctx.rng`. Default 0. */
|
|
246
|
+
seed?: number | string;
|
|
247
|
+
/**
|
|
248
|
+
* Safety cap: stop collecting after this many steps and report
|
|
249
|
+
* `truncated` (a non-terminating simulation shouldn't hang the page).
|
|
250
|
+
* Default 10 000.
|
|
251
|
+
*/
|
|
252
|
+
maxSteps?: number;
|
|
253
|
+
}
|
|
254
|
+
interface SimulationResult<S> {
|
|
255
|
+
steps: S[];
|
|
256
|
+
/** True when `maxSteps` cut the run short. */
|
|
257
|
+
truncated: boolean;
|
|
258
|
+
/** What the simulation threw, if it did; `steps` holds what it yielded before. */
|
|
259
|
+
error: unknown;
|
|
260
|
+
/** The numeric seed actually used (strings are hashed). */
|
|
261
|
+
seed: number;
|
|
262
|
+
}
|
|
263
|
+
/** Run a simulation to completion (or the cap). Pure; safe at module scope and on the server. */
|
|
264
|
+
declare function runSimulation<Input, S extends StepwiseStep>(simulation: Simulation<Input, S>, input: Input, opts?: SimulationRunOptions): SimulationResult<S>;
|
|
265
|
+
|
|
266
|
+
interface SimulationOptions<S extends StepwiseStep> extends TraceOptions<S>, SimulationRunOptions {
|
|
267
|
+
}
|
|
268
|
+
interface SimulationPlayer<S extends StepwiseStep> extends TracePlayer<S> {
|
|
269
|
+
/** The computed trace. */
|
|
270
|
+
steps: readonly S[];
|
|
271
|
+
/** True when `maxSteps` cut the run short. */
|
|
272
|
+
truncated: boolean;
|
|
273
|
+
/** What the simulation threw, if anything (the trace holds the steps before it). */
|
|
274
|
+
error: unknown;
|
|
275
|
+
/** The numeric seed used for this run. */
|
|
276
|
+
seed: number;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* A live trace: run `simulation` on `input` and play the result. Change the
|
|
280
|
+
* input (or the seed) and the trace is recomputed; keyed steps keep the
|
|
281
|
+
* playhead on the same logical moment, and a playing player keeps playing.
|
|
282
|
+
*
|
|
283
|
+
* const [input, setInput] = useState([5, 2, 8, 1]);
|
|
284
|
+
* const sim = useSimulation(bubbleSort, input, { autoPlay: true, syncHash: "frame" });
|
|
285
|
+
* <Bars arr={sim.step!.arr} />
|
|
286
|
+
* <PlayerBar player={sim.player} toneAt={sim.toneAt} titleAt={sim.titleAt} />
|
|
287
|
+
*
|
|
288
|
+
* The trace is recomputed when `simulation`, `input`, `seed` or `maxSteps`
|
|
289
|
+
* change identity: define the simulation at module scope, and keep object
|
|
290
|
+
* inputs in state (or memoize them) rather than rebuilding them per render.
|
|
291
|
+
*/
|
|
292
|
+
declare function useSimulation<Input, S extends StepwiseStep>(simulation: Simulation<Input, S>, input: Input, opts?: SimulationOptions<S>): SimulationPlayer<S>;
|
|
293
|
+
|
|
294
|
+
interface TraceBuilder<Data extends object> {
|
|
295
|
+
/** Append one frame. Returns the builder, so calls chain. */
|
|
296
|
+
step(step: StepwiseStep<Data>): TraceBuilder<Data>;
|
|
297
|
+
/** Frames appended so far. */
|
|
298
|
+
readonly length: number;
|
|
299
|
+
/** The most recently appended frame (handy for "carry the state forward"). */
|
|
300
|
+
last(): StepwiseStep<Data> | undefined;
|
|
301
|
+
/** Snapshot the trace. A fresh array each call — the builder stays usable. */
|
|
302
|
+
build(): StepwiseStep<Data>[];
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Typed trace builder. Authors run their algorithm at module scope and record
|
|
306
|
+
* frames as they go — no IIFE, no pre-declared `const STEPS = []` that a
|
|
307
|
+
* component might read before it is filled (invariant 6):
|
|
308
|
+
*
|
|
309
|
+
* const t = createTrace<{ arr: number[] }>();
|
|
310
|
+
* for (…) t.step({ tone: "write", label: "swap", arr: [...arr] });
|
|
311
|
+
* export const STEPS = t.build();
|
|
312
|
+
*
|
|
313
|
+
* Pure data — no React, safe on the server.
|
|
314
|
+
*/
|
|
315
|
+
declare function createTrace<Data extends object = Record<never, never>>(): TraceBuilder<Data>;
|
|
316
|
+
|
|
317
|
+
/** Named traces to play side by side: `{ bubble: stepsA, insertion: stepsB }`. */
|
|
318
|
+
type Tracks = Record<string, readonly StepwiseStep<object>[]>;
|
|
319
|
+
/**
|
|
320
|
+
* Sync-point key for a step. Steps with the same key in two or more tracks
|
|
321
|
+
* land on the same aligned frame; `null`/`undefined` marks a free step. The
|
|
322
|
+
* n-th occurrence of a key in one track pairs with the n-th in another.
|
|
323
|
+
*/
|
|
324
|
+
type TrackKeyFn<T extends Tracks = Tracks> = (step: T[keyof T][number], index: number, track: keyof T & string) => string | null | undefined;
|
|
325
|
+
/** One track's slot in one aligned frame. */
|
|
326
|
+
interface AlignedCell {
|
|
327
|
+
/** Index into the track's own steps; -1 only for an empty track. */
|
|
328
|
+
index: number;
|
|
329
|
+
/**
|
|
330
|
+
* The track did not advance on this frame: it is holding its last frame
|
|
331
|
+
* (finished, or waiting for a sync point), or waiting at frame 0 for its
|
|
332
|
+
* first sync point.
|
|
333
|
+
*/
|
|
334
|
+
held: boolean;
|
|
335
|
+
}
|
|
336
|
+
type AlignedRow<K extends string = string> = Record<K, AlignedCell>;
|
|
337
|
+
/**
|
|
338
|
+
* Align several traces into one timeline. Pure — use it directly for server
|
|
339
|
+
* rendering or tests; `useSyncedTracks` memoizes it per track identity.
|
|
340
|
+
*
|
|
341
|
+
* - No `key` (hold): frame i shows step i of every track; a shorter track
|
|
342
|
+
* holds its last frame once it runs out. Length = the longest track.
|
|
343
|
+
* - With `key`: tracks advance together frame by frame, but a track whose
|
|
344
|
+
* next step is a sync point holds until every other track that shares the
|
|
345
|
+
* point has reached it, so matching steps share a frame. Keys must occur
|
|
346
|
+
* in the same relative order across tracks; where they conflict, that
|
|
347
|
+
* frame degrades to lockstep instead of deadlocking.
|
|
348
|
+
*/
|
|
349
|
+
declare function alignTracks<T extends Tracks>(tracks: T, key?: TrackKeyFn<T>): AlignedRow<keyof T & string>[];
|
|
350
|
+
interface TrackView<S> {
|
|
351
|
+
/** The step to render for this track (undefined only for an empty track). */
|
|
352
|
+
step: S | undefined;
|
|
353
|
+
/** Index into the track's own steps; -1 for an empty track. */
|
|
354
|
+
index: number;
|
|
355
|
+
/** The track did not advance on this frame (see `AlignedCell.held`). */
|
|
356
|
+
held: boolean;
|
|
357
|
+
/** The track is on its final step. */
|
|
358
|
+
done: boolean;
|
|
359
|
+
/** Length of the track's own trace. */
|
|
360
|
+
length: number;
|
|
361
|
+
}
|
|
362
|
+
interface SyncedTracksOptions<T extends Tracks = Tracks> extends Omit<StepPlayerOptions, "total"> {
|
|
363
|
+
/**
|
|
364
|
+
* Sync-point alignment (see `alignTracks`). Omit to hold shorter tracks on
|
|
365
|
+
* their last frame. Read when the tracks change — pass the same tracks to
|
|
366
|
+
* keep the alignment memoized; an inline function is fine.
|
|
367
|
+
*/
|
|
368
|
+
key?: TrackKeyFn<T>;
|
|
369
|
+
}
|
|
370
|
+
interface SyncedTracks<T extends Tracks> {
|
|
371
|
+
/** One player over the aligned timeline — pass it to PlayerBar. */
|
|
372
|
+
player: StepPlayer;
|
|
373
|
+
/** Aligned frame to render: 0 before the player starts, -1 when all tracks are empty. */
|
|
374
|
+
frame: number;
|
|
375
|
+
/** Number of aligned frames. */
|
|
376
|
+
total: number;
|
|
377
|
+
/** Per-track view of `frame`. */
|
|
378
|
+
tracks: {
|
|
379
|
+
[K in keyof T]: TrackView<T[K][number]>;
|
|
380
|
+
};
|
|
381
|
+
/** The full alignment table (stable while the tracks are). */
|
|
382
|
+
rows: readonly AlignedRow<keyof T & string>[];
|
|
383
|
+
/** Stable scrubber accessors: the most severe tone among tracks that moved. */
|
|
384
|
+
toneAt: (i: number) => StepTone;
|
|
385
|
+
/** Stable scrubber accessor: `track: label` for every track that moved. */
|
|
386
|
+
titleAt: (i: number) => string;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Comparison mode: several traces of the same input (two engines, two
|
|
390
|
+
* algorithms) under ONE player. The cursor walks the aligned timeline from
|
|
391
|
+
* `alignTracks`; each track resolves its own step, so payload types stay
|
|
392
|
+
* per-track. Like `useTrace`, frame 0 renders before playback starts.
|
|
393
|
+
*
|
|
394
|
+
* const cmp = useSyncedTracks({ bubble: BUBBLE, insertion: INSERTION }, { autoPlay: true });
|
|
395
|
+
* <Lane step={cmp.tracks.bubble.step} held={cmp.tracks.bubble.held} />
|
|
396
|
+
* <PlayerBar player={cmp.player} toneAt={cmp.toneAt} titleAt={cmp.titleAt} />
|
|
397
|
+
*/
|
|
398
|
+
declare function useSyncedTracks<T extends Tracks>(tracks: T, opts?: SyncedTracksOptions<T>): SyncedTracks<T>;
|
|
399
|
+
|
|
400
|
+
interface LessonStreamOptions {
|
|
401
|
+
/** Lesson to open on (deep link). Clamped. Default 0. */
|
|
402
|
+
seed?: number;
|
|
403
|
+
/** Lessons mounted on each side of the seed initially. Default 1. */
|
|
404
|
+
radius?: number;
|
|
405
|
+
/** Load the next lesson when the last mounted one's bottom is within this many px below the viewport. Default 600. */
|
|
406
|
+
approachBelow?: number;
|
|
407
|
+
/** Load the previous lesson when the first mounted one's top is below `-approachAbove`. Default 300. */
|
|
408
|
+
approachAbove?: number;
|
|
409
|
+
/** Scroll the seed lesson into view before first paint (seed > 0). Default true. */
|
|
410
|
+
scrollToSeed?: boolean;
|
|
411
|
+
/**
|
|
412
|
+
* Fired when the scroll-tracked active lesson changes (not on mount) — sync
|
|
413
|
+
* the URL / document title here. Read through a ref; inline is fine.
|
|
414
|
+
*/
|
|
415
|
+
onActiveChange?: (index: number) => void;
|
|
416
|
+
}
|
|
417
|
+
interface LessonStream {
|
|
418
|
+
/** Lesson containing the viewport center (the seed until the first measure). */
|
|
419
|
+
active: number;
|
|
420
|
+
/** The seed lesson — SSR'd visible; every other block should fade in. */
|
|
421
|
+
seed: number;
|
|
422
|
+
/** Indices to render, in order (the mounted window). */
|
|
423
|
+
mounted: number[];
|
|
424
|
+
/** The window reaches the last lesson — render an end-of-stream footer. */
|
|
425
|
+
complete: boolean;
|
|
426
|
+
/** Stable callback ref for lesson `i`'s block element (cached per index). */
|
|
427
|
+
refFor: (index: number) => (el: HTMLElement | null) => void;
|
|
428
|
+
/** Mount through lesson `i` (if needed) and scroll to it. */
|
|
429
|
+
goTo: (index: number) => void;
|
|
430
|
+
next: () => void;
|
|
431
|
+
prev: () => void;
|
|
432
|
+
reducedMotion: boolean;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Continuous reader for a sequence of lessons (a notebook): renders a growing
|
|
436
|
+
* window of lessons around the seed instead of all of them, and tracks the
|
|
437
|
+
* active one by viewport-center containment (invariant 1).
|
|
438
|
+
*
|
|
439
|
+
* - The lesson after the window mounts as the reader approaches its place;
|
|
440
|
+
* the one before mounts near the top, and a layout effect compensates the
|
|
441
|
+
* scroll so prepended content never shoves the reader down.
|
|
442
|
+
* - Navigation (`goTo`) expansions are NOT compensated — their scrollIntoView
|
|
443
|
+
* is absolute, and compensating them displaced the landing.
|
|
444
|
+
* - The cursor is a useSteps core, so it composes with ChapterNav
|
|
445
|
+
* (`active` + `goTo`) and LessonPager (`prev`/`next`).
|
|
446
|
+
*
|
|
447
|
+
* Headless: the consumer renders `mounted.map(i => <Block ref={refFor(i)} />)`
|
|
448
|
+
* and owns the content; pair blocks with `useFadeIn(i === seed)`.
|
|
449
|
+
*/
|
|
450
|
+
declare function useLessonStream(count: number, opts?: LessonStreamOptions): LessonStream;
|
|
451
|
+
/**
|
|
452
|
+
* Mount fade for streamed blocks: `false` on the first client render, `true`
|
|
453
|
+
* a frame later (immediately with reduced motion). Pass `initiallyVisible`
|
|
454
|
+
* for server-rendered blocks (the stream's seed) so they never flash.
|
|
455
|
+
*/
|
|
456
|
+
declare function useFadeIn(initiallyVisible?: boolean): boolean;
|
|
457
|
+
|
|
458
|
+
interface EvaluatorOptions {
|
|
459
|
+
/**
|
|
460
|
+
* "auto" (default) re-judges whenever `ctx` changes: a gate that opens the
|
|
461
|
+
* moment the reader does the thing. "manual" judges only on `check()`: a
|
|
462
|
+
* quiz with a Check button; changing `ctx` afterwards clears the verdict.
|
|
463
|
+
*/
|
|
464
|
+
mode?: "auto" | "manual";
|
|
465
|
+
}
|
|
466
|
+
interface EvaluatorState {
|
|
467
|
+
/** "idle" until the first verdict (manual mode, or after `reset`). */
|
|
468
|
+
status: VerdictStatus | "idle";
|
|
469
|
+
feedback: ReactNode;
|
|
470
|
+
/** status === "pass". */
|
|
471
|
+
passed: boolean;
|
|
472
|
+
/** How many times `check()` ran, for "show a hint after two tries". */
|
|
473
|
+
attempts: number;
|
|
474
|
+
/** What the evaluator threw (the verdict is then "fail"). */
|
|
475
|
+
error: unknown;
|
|
476
|
+
/** Judge the current context now. Resolves with the verdict, or null if superseded. */
|
|
477
|
+
check: () => Promise<Verdict | null>;
|
|
478
|
+
/** Back to "idle", attempts to 0, any running check abandoned. */
|
|
479
|
+
reset: () => void;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Run an evaluator against a context and keep its verdict as state. Async
|
|
483
|
+
* evaluators report "pending" while they run; a newer check (or a context
|
|
484
|
+
* change) aborts the older one and drops its late result, so the verdict
|
|
485
|
+
* always describes the latest context. `check` and `reset` are
|
|
486
|
+
* referentially stable.
|
|
487
|
+
*
|
|
488
|
+
* const answer = useEvaluator(
|
|
489
|
+
* (guess: string | null) => (guess === truth ? pass("Right!") : fail("Look at the pivot again.")),
|
|
490
|
+
* guess,
|
|
491
|
+
* { mode: "manual" }
|
|
492
|
+
* );
|
|
493
|
+
* <button onClick={answer.check}>Check</button> {answer.feedback}
|
|
494
|
+
*/
|
|
495
|
+
declare function useEvaluator<Ctx>(evaluator: Evaluator<Ctx>, ctx: Ctx, opts?: EvaluatorOptions): EvaluatorState;
|
|
98
496
|
|
|
99
497
|
interface GuidedTour {
|
|
100
498
|
/** Active step index, or null when the tour is not running. */
|
|
@@ -107,6 +505,11 @@ interface GuidedTour {
|
|
|
107
505
|
* the waitHint) so the user knows what is being waited on.
|
|
108
506
|
*/
|
|
109
507
|
blocked: boolean;
|
|
508
|
+
/**
|
|
509
|
+
* The active step's gate feedback, when its gate returns a `Verdict` with
|
|
510
|
+
* one ("Stage at least one file"). Pass it to `TourCard`'s `feedback`.
|
|
511
|
+
*/
|
|
512
|
+
feedback: ReactNode;
|
|
110
513
|
start: () => void;
|
|
111
514
|
end: () => void;
|
|
112
515
|
next: () => void;
|
|
@@ -124,19 +527,6 @@ interface GuidedTourOptions {
|
|
|
124
527
|
*/
|
|
125
528
|
driver?: TourDriver;
|
|
126
529
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Tour state whose steps are effects over app state: `onEnter` fires whenever
|
|
129
|
-
* the active step changes, reading the latest `steps` via ref so closures stay
|
|
130
|
-
* fresh without re-triggering the effect on every render. Composed on
|
|
131
|
-
* useSteps — a tour is a step sequence whose `next` past the end exits.
|
|
132
|
-
*
|
|
133
|
-
* On top of the linear cursor, steps may declare `when` (skip on arrival),
|
|
134
|
-
* `gate` (block `next` until true), and `next` (edge override by step id) —
|
|
135
|
-
* a small state machine where states are steps, edges are `next` functions,
|
|
136
|
-
* and guards are gates. The cursor itself stays a cursor: skipped steps are
|
|
137
|
-
* never landed on, and skipped/gated behavior is evaluated at transition
|
|
138
|
-
* time, not on every render.
|
|
139
|
-
*/
|
|
140
530
|
declare function useGuidedTour(steps: TourStepInput[], opts?: GuidedTourOptions): GuidedTour;
|
|
141
531
|
|
|
142
532
|
interface FlatChapter {
|
|
@@ -350,7 +740,7 @@ declare function StepScrubber({ count, index, toneAt, titleAt, onSeek, emptyHint
|
|
|
350
740
|
*/
|
|
351
741
|
groupOf?: (i: number) => number;
|
|
352
742
|
}): react.JSX.Element;
|
|
353
|
-
declare function PlayerBar({ player, toneAt, titleAt, leading, onEmpty, emptyHint, className, theme, groupOf, labels, }: {
|
|
743
|
+
declare function PlayerBar({ player, toneAt, titleAt, leading, onEmpty, emptyHint, className, theme, groupOf, labels, live, }: {
|
|
354
744
|
player: {
|
|
355
745
|
total: number;
|
|
356
746
|
index: number;
|
|
@@ -360,10 +750,18 @@ declare function PlayerBar({ player, toneAt, titleAt, leading, onEmpty, emptyHin
|
|
|
360
750
|
prev: () => void;
|
|
361
751
|
toggle: (onEmpty?: () => void) => void;
|
|
362
752
|
seek: (frame: number) => void;
|
|
753
|
+
live?: boolean;
|
|
754
|
+
goLive?: () => void;
|
|
363
755
|
};
|
|
364
756
|
toneAt?: (i: number) => StepTone;
|
|
365
757
|
titleAt?: (i: number) => string;
|
|
366
758
|
leading?: ReactNode;
|
|
759
|
+
/**
|
|
760
|
+
* Show the live-edge badge for a following player (`follow` option):
|
|
761
|
+
* "Live" at the edge, "Go live" with the frame count when the reader is
|
|
762
|
+
* behind.
|
|
763
|
+
*/
|
|
764
|
+
live?: boolean;
|
|
367
765
|
/** Fired when the user plays/steps with no timeline loaded. */
|
|
368
766
|
onEmpty?: () => void;
|
|
369
767
|
emptyHint?: string;
|
|
@@ -380,6 +778,8 @@ declare function PlayerBar({ player, toneAt, titleAt, leading, onEmpty, emptyHin
|
|
|
380
778
|
pause?: string;
|
|
381
779
|
replay?: string;
|
|
382
780
|
next?: string;
|
|
781
|
+
live?: string;
|
|
782
|
+
goLive?: string;
|
|
383
783
|
};
|
|
384
784
|
}): react.JSX.Element;
|
|
385
785
|
declare function ChapterNav({ chapters, active, onNavigate, hint, className, theme, }: {
|
|
@@ -410,7 +810,7 @@ declare function TourSpotlight({ target, theme, }: {
|
|
|
410
810
|
target: string | null;
|
|
411
811
|
theme?: StepwiseTheme;
|
|
412
812
|
}): react.JSX.Element | null;
|
|
413
|
-
declare function TourCard({ step, index, total, onNext, onBack, onEnd, labels, className, theme, nextDisabled, }: {
|
|
813
|
+
declare function TourCard({ step, index, total, onNext, onBack, onEnd, labels, className, theme, nextDisabled, feedback, }: {
|
|
414
814
|
step: TourStepInput;
|
|
415
815
|
index: number;
|
|
416
816
|
total: number;
|
|
@@ -428,6 +828,8 @@ declare function TourCard({ step, index, total, onNext, onBack, onEnd, labels, c
|
|
|
428
828
|
theme?: StepwiseTheme;
|
|
429
829
|
/** Disable the primary (Next/Finish) button — pair with tour.blocked. */
|
|
430
830
|
nextDisabled?: boolean;
|
|
831
|
+
/** Gate feedback ("Stage a file first") — pair with tour.feedback. Announced politely. */
|
|
832
|
+
feedback?: ReactNode;
|
|
431
833
|
}): react.JSX.Element;
|
|
432
834
|
declare function ShortcutsDialog({ open, onClose, items, title, className, theme, }: {
|
|
433
835
|
open: boolean;
|
|
@@ -438,4 +840,4 @@ declare function ShortcutsDialog({ open, onClose, items, title, className, theme
|
|
|
438
840
|
theme?: StepwiseTheme;
|
|
439
841
|
}): react.ReactPortal | null;
|
|
440
842
|
|
|
441
|
-
export { Chapter, ChapterNav, type ChaptersApi, CodeBlock, type Discoveries, type DiscoveriesOptions, type DiscoveryStore, type FlatChapter, type GlobalKeysOptions, GlossaryAppendix, type GlossaryEntry, GlossaryProvider, type GuidedTour, type GuidedTourOptions, type KeyMap, LessonPager, PlayerBar, PlayerEngine, ShortcutItem, ShortcutsDialog, type StepPlayer, type StepPlayerOptions, StepScrubber, StepTone, type StepsCore, type StepwiseTheme, TONE_CHIP_CLASS, TONE_SEGMENT_CLASS, Term, TourCard, TourDriver, TourSpotlight, TourStepInput, type UseStepsOptions, cookieStore, defaultTheme, flattenChapters, localStorageStore, memoryStore, useChapters, useDiscoveries, useFocusTrap, useGlobalKeys, useGuidedTour, usePrefersReducedMotion, useStepPlayer, useSteps };
|
|
843
|
+
export { type AlignedCell, type AlignedRow, Chapter, ChapterNav, type ChaptersApi, CodeBlock, type Discoveries, type DiscoveriesOptions, type DiscoveryStore, Evaluator, type EvaluatorOptions, type EvaluatorState, type FlatChapter, type GlobalKeysOptions, GlossaryAppendix, type GlossaryEntry, GlossaryProvider, type GuidedTour, type GuidedTourOptions, type KeyMap, LessonPager, type LessonStream, type LessonStreamOptions, PlayerBar, PlayerEngine, type Rng, ShortcutItem, ShortcutsDialog, type Simulation, type SimulationContext, type SimulationOptions, type SimulationPlayer, type SimulationResult, type SimulationRunOptions, type StepPlayer, type StepPlayerOptions, StepScrubber, StepTone, type StepsCore, StepwiseStep, type StepwiseTheme, type SyncedTracks, type SyncedTracksOptions, TONE_CHIP_CLASS, TONE_SEGMENT_CLASS, Term, TourCard, TourDriver, TourSpotlight, TourStepInput, type TraceBuilder, type TraceOptions, type TracePlayer, type TrackKeyFn, type TrackView, type Tracks, type UseStepsOptions, Verdict, VerdictStatus, alignTracks, cookieStore, createRng, createTrace, defaultTheme, flattenChapters, localStorageStore, memoryStore, readHashParam, reanchor, runSimulation, useChapters, useDiscoveries, useEvaluator, useFadeIn, useFocusTrap, useGlobalKeys, useGuidedTour, useLessonStream, usePrefersReducedMotion, useSimulation, useStepPlayer, useSteps, useSyncedTracks, useTrace, writeHashParam };
|