@sphynxoid/stepwise 0.2.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 CHANGED
@@ -1,6 +1,10 @@
1
1
  # @sphynxoid/stepwise
2
2
 
3
- A kit for interactive, narrated, step-driven visualizations — one primitive (`useSteps`) at every scale.
3
+ **Interactive explainers for React: step-through players, guided tours, scroll-driven lessons and glossary tooltips, all on one cursor.**
4
+
5
+ **[Live demos](https://stepwise.thesphynx.net)** · [API reference](https://stepwise.thesphynx.net/docs) · [StackBlitz starter](https://stackblitz.com/github/TheSphynxoid/stepwise/tree/master/examples/vite-starter) · [GitHub](https://github.com/TheSphynxoid/stepwise)
6
+
7
+ <img src="https://raw.githubusercontent.com/TheSphynxoid/stepwise/master/docs/assets/hero.gif" alt="A quicksort trace playing in a stepwise PlayerBar" width="516">
4
8
 
5
9
  Frames in a trace, chapters in a lesson, lessons in a notebook, and tour steps are the same thing at the core: a clamped cursor over an ordered sequence. Timers, tour effects, scroll tracking, and third-party presenters are policies and adapters layered on top.
6
10
 
@@ -45,7 +49,7 @@ Tokens: `--stepwise-background`, `-foreground`, `-card`, `-popover`, `-primary`,
45
49
  **2. Tailwind + shadcn/ui — copy the components in with the shadcn CLI:**
46
50
 
47
51
  ```bash
48
- npx shadcn@latest add https://stepwise-demo.vercel.app/r/player-bar.json
52
+ npx shadcn@latest add https://stepwise.thesphynx.net/r/player-bar.json
49
53
  ```
50
54
 
51
55
  Items: `player-bar`, `code-block`, `tour`, `glossary`, `chapter-nav`, `lesson-pager`, `shortcuts-dialog` — or `stepwise` for all of them. The presentation source lands in `components/stepwise/` (plus `lib/stepwise-theme.ts`), where Tailwind scans it automatically; hooks and types stay imported from this package. Colors come from your shadcn tokens.
@@ -86,6 +90,66 @@ export function Demo() {
86
90
 
87
91
  `useTrace` binds a player to a typed trace: `step` is the frame to render (frame 0 before playback starts), and `toneAt` / `titleAt` feed the scrubber. Under it, `useStepPlayer` gives you a timer-driven playhead with play/pause/seek, speed control, and optional `#frame=2` deep links — or hand it an `engine` to drive the playhead from GSAP or scroll instead. Two end-state flavors: `atEnd` (cursor parked on the last frame, seeks included) and `ended` (playback ran to the finish, via the `onEnd` callback option) — clearing on any cursor movement, so a completion summary knows the demo actually finished. `autoPlay` respects `prefers-reduced-motion` (lands on frame 0, paused; opt out with `respectReducedMotion: false`), and `syncHash` writes only its own key, so it coexists with anchors and other players' keys.
88
92
 
93
+ ## Live simulations
94
+
95
+ A trace doesn't have to be recorded by hand. Write the algorithm as a generator, give each step a `key` naming its moment, and `useSimulation` reruns it whenever the input changes, keeping the playhead on the same moment (mid-playback too):
96
+
97
+ ```tsx
98
+ import { useSimulation, type Simulation, type StepwiseStep } from "@sphynxoid/stepwise";
99
+
100
+ type Frame = StepwiseStep<{ arr: number[] }>;
101
+
102
+ const shuffleThenSort: Simulation<number[], Frame> = function* (input, { rng }) {
103
+ const a = rng.shuffle(input); // seeded: same input + seed → same trace
104
+ yield { key: "shuffled", tone: "neutral", label: "shuffled", arr: a };
105
+ for (let i = 1; i < a.length; i++) {
106
+ for (let j = i; j > 0 && a[j - 1] > a[j]; j--) {
107
+ [a[j - 1], a[j]] = [a[j], a[j - 1]];
108
+ yield { key: `swap ${i}.${j}`, tone: "write", label: `swap at ${j}`, arr: [...a] };
109
+ }
110
+ }
111
+ };
112
+
113
+ const sim = useSimulation(shuffleThenSort, input, { seed: 7, autoPlay: true, syncHash: "frame" });
114
+ // sim.step, sim.player, sim.toneAt, sim.titleAt: like useTrace, plus sim.steps / truncated / error
115
+ ```
116
+
117
+ How re-anchoring decides: walk back from the old position to the latest step whose key exists in the new trace, and land there (`reanchor` is exported for custom players). `runSimulation` is the pure, server-safe runner, with a `maxSteps` cap so a runaway generator can't hang the page.
118
+
119
+ ## Following a live timeline
120
+
121
+ When frames arrive while the reader watches (a log, a stream, an agent's steps, a game's moves), add `follow`: a reader at the edge keeps playing into new frames at the normal pace, and one who scrubbed back to inspect something stays there until they choose to return.
122
+
123
+ ```tsx
124
+ const trace = useTrace(events, { autoPlay: true, follow: true }); // events grows over time
125
+ // trace.player.live: at the edge? trace.player.goLive(): back to it
126
+ <PlayerBar player={trace.player} toneAt={trace.toneAt} titleAt={trace.titleAt} live />
127
+ ```
128
+
129
+ `PlayerBar live` shows "Live" at the edge and "Go live +N" when the reader is behind. Checkpoints still hold, and with reduced motion nothing starts on its own.
130
+
131
+ ## Checkpoints and evaluators
132
+
133
+ Pause autoplay where the reader should think, then grade their answer:
134
+
135
+ ```tsx
136
+ const sim = useSimulation(sort, input, {
137
+ autoPlay: true,
138
+ holdAt: (_, step) => step?.kind === "compare", // autoplay stops here; player.held is set
139
+ });
140
+
141
+ const answer = useEvaluator(
142
+ (guess: "swap" | "keep" | null) =>
143
+ guess === truth ? pass("Right!") : fail("Compare the two highlighted bars."),
144
+ guess,
145
+ { mode: "manual" } // judge on check(); "auto" judges every change
146
+ );
147
+ // <button onClick={answer.check}>Check</button> {answer.feedback}
148
+ // {answer.passed && <button onClick={sim.player.release}>Continue</button>}
149
+ ```
150
+
151
+ An `Evaluator` returns a boolean, a `Verdict` (`pass(feedback)`, `fail(feedback)`, `pending()`), or a promise of either; async checks report `pending`, and stale results are dropped. Tour gates accept verdicts too: anything but pass blocks Next, and `tour.feedback` (→ `TourCard feedback`) tells the reader what's missing.
152
+
89
153
  ## The three scales
90
154
 
91
155
  | Scale | Hook | Presentation |
@@ -1,4 +1,4 @@
1
- import { T as TourDriver } from '../types-IT_9L8On.js';
1
+ import { T as TourDriver } from '../types-CNMxISXe.js';
2
2
  import 'react';
3
3
 
4
4
  interface DriverJsTourOptions {
@@ -1,4 +1,4 @@
1
- import { P as PlayerEngine } from '../types-IT_9L8On.js';
1
+ import { P as PlayerEngine } from '../types-CNMxISXe.js';
2
2
  import 'react';
3
3
 
4
4
  interface GsapEngineOptions {
@@ -1,4 +1,4 @@
1
- import { P as PlayerEngine } from '../types-IT_9L8On.js';
1
+ import { P as PlayerEngine } from '../types-CNMxISXe.js';
2
2
  import 'react';
3
3
 
4
4
  interface ScrollEngineOptions {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as PlayerEngine, S as StepwiseStep, a as StepTone, b as TourStepInput, T as TourDriver, C as Chapter, c as ShortcutItem } from './types-IT_9L8On.js';
2
- export { d as PlayerEngineApi, e as PlayerEngineHandle, f as TourContext } from './types-IT_9L8On.js';
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
 
@@ -69,6 +69,24 @@ interface StepPlayerOptions {
69
69
  * user presses play. Default true.
70
70
  */
71
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;
72
90
  }
73
91
  interface StepPlayer {
74
92
  /** Total frames in the current timeline. */
@@ -87,6 +105,28 @@ interface StepPlayer {
87
105
  * not set it — `toggle()` at `ended` replays from frame 0.
88
106
  */
89
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;
90
130
  /** Jump to frame 0 and play. */
91
131
  start: () => void;
92
132
  /** Clear the player (frame = -1, paused). */
@@ -101,8 +141,26 @@ interface StepPlayer {
101
141
  setPlaying: (playing: boolean) => void;
102
142
  setSpeed: (speed: number) => void;
103
143
  }
104
- declare function useStepPlayer({ total, baseDelayMs, autoPlay, engine, syncHash, onEnd, respectReducedMotion, }: 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;
105
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
+ }
106
164
  interface TracePlayer<S extends StepwiseStep> {
107
165
  /** The underlying player — pass it to PlayerBar. */
108
166
  player: StepPlayer;
@@ -128,8 +186,110 @@ interface TracePlayer<S extends StepwiseStep> {
128
186
  * const trace = useTrace(STEPS, { autoPlay: true });
129
187
  * <Viz arr={trace.step.arr} />
130
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.
131
193
  */
132
- declare function useTrace<S extends StepwiseStep>(steps: readonly S[], opts?: Omit<StepPlayerOptions, "total">): TracePlayer<S>;
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>;
133
293
 
134
294
  interface TraceBuilder<Data extends object> {
135
295
  /** Append one frame. Returns the builder, so calls chain. */
@@ -295,6 +455,45 @@ declare function useLessonStream(count: number, opts?: LessonStreamOptions): Les
295
455
  */
296
456
  declare function useFadeIn(initiallyVisible?: boolean): boolean;
297
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;
496
+
298
497
  interface GuidedTour {
299
498
  /** Active step index, or null when the tour is not running. */
300
499
  index: number | null;
@@ -306,6 +505,11 @@ interface GuidedTour {
306
505
  * the waitHint) so the user knows what is being waited on.
307
506
  */
308
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;
309
513
  start: () => void;
310
514
  end: () => void;
311
515
  next: () => void;
@@ -323,19 +527,6 @@ interface GuidedTourOptions {
323
527
  */
324
528
  driver?: TourDriver;
325
529
  }
326
- /**
327
- * Tour state whose steps are effects over app state: `onEnter` fires whenever
328
- * the active step changes, reading the latest `steps` via ref so closures stay
329
- * fresh without re-triggering the effect on every render. Composed on
330
- * useSteps — a tour is a step sequence whose `next` past the end exits.
331
- *
332
- * On top of the linear cursor, steps may declare `when` (skip on arrival),
333
- * `gate` (block `next` until true), and `next` (edge override by step id) —
334
- * a small state machine where states are steps, edges are `next` functions,
335
- * and guards are gates. The cursor itself stays a cursor: skipped steps are
336
- * never landed on, and skipped/gated behavior is evaluated at transition
337
- * time, not on every render.
338
- */
339
530
  declare function useGuidedTour(steps: TourStepInput[], opts?: GuidedTourOptions): GuidedTour;
340
531
 
341
532
  interface FlatChapter {
@@ -549,7 +740,7 @@ declare function StepScrubber({ count, index, toneAt, titleAt, onSeek, emptyHint
549
740
  */
550
741
  groupOf?: (i: number) => number;
551
742
  }): react.JSX.Element;
552
- 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, }: {
553
744
  player: {
554
745
  total: number;
555
746
  index: number;
@@ -559,10 +750,18 @@ declare function PlayerBar({ player, toneAt, titleAt, leading, onEmpty, emptyHin
559
750
  prev: () => void;
560
751
  toggle: (onEmpty?: () => void) => void;
561
752
  seek: (frame: number) => void;
753
+ live?: boolean;
754
+ goLive?: () => void;
562
755
  };
563
756
  toneAt?: (i: number) => StepTone;
564
757
  titleAt?: (i: number) => string;
565
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;
566
765
  /** Fired when the user plays/steps with no timeline loaded. */
567
766
  onEmpty?: () => void;
568
767
  emptyHint?: string;
@@ -579,6 +778,8 @@ declare function PlayerBar({ player, toneAt, titleAt, leading, onEmpty, emptyHin
579
778
  pause?: string;
580
779
  replay?: string;
581
780
  next?: string;
781
+ live?: string;
782
+ goLive?: string;
582
783
  };
583
784
  }): react.JSX.Element;
584
785
  declare function ChapterNav({ chapters, active, onNavigate, hint, className, theme, }: {
@@ -609,7 +810,7 @@ declare function TourSpotlight({ target, theme, }: {
609
810
  target: string | null;
610
811
  theme?: StepwiseTheme;
611
812
  }): react.JSX.Element | null;
612
- 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, }: {
613
814
  step: TourStepInput;
614
815
  index: number;
615
816
  total: number;
@@ -627,6 +828,8 @@ declare function TourCard({ step, index, total, onNext, onBack, onEnd, labels, c
627
828
  theme?: StepwiseTheme;
628
829
  /** Disable the primary (Next/Finish) button — pair with tour.blocked. */
629
830
  nextDisabled?: boolean;
831
+ /** Gate feedback ("Stage a file first") — pair with tour.feedback. Announced politely. */
832
+ feedback?: ReactNode;
630
833
  }): react.JSX.Element;
631
834
  declare function ShortcutsDialog({ open, onClose, items, title, className, theme, }: {
632
835
  open: boolean;
@@ -637,4 +840,4 @@ declare function ShortcutsDialog({ open, onClose, items, title, className, theme
637
840
  theme?: StepwiseTheme;
638
841
  }): react.ReactPortal | null;
639
842
 
640
- export { type AlignedCell, type AlignedRow, 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, type LessonStream, type LessonStreamOptions, PlayerBar, PlayerEngine, ShortcutItem, ShortcutsDialog, 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 TracePlayer, type TrackKeyFn, type TrackView, type Tracks, type UseStepsOptions, alignTracks, cookieStore, createTrace, defaultTheme, flattenChapters, localStorageStore, memoryStore, useChapters, useDiscoveries, useFadeIn, useFocusTrap, useGlobalKeys, useGuidedTour, useLessonStream, usePrefersReducedMotion, useStepPlayer, useSteps, useSyncedTracks, useTrace };
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 };