@sphynxoid/stepwise 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,127 +1,150 @@
1
- # @sphynxoid/stepwise
2
-
3
- A kit for interactive, narrated, step-driven visualizations — one primitive (`useSteps`) at every scale.
4
-
5
- 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
-
7
- - **Headless core.** The hooks never render; the styled components are optional and theme-injectable.
8
- - **One dependency by default.** The root barrel needs only React. GSAP and driver.js integrations are opt-in via adapter subpaths.
9
-
10
- ## Install
11
-
12
- ```bash
13
- npm install @sphynxoid/stepwise
14
- ```
15
-
16
- Optional adapters (install only what you use):
17
-
18
- ```bash
19
- npm install gsap # @sphynxoid/stepwise/adapters/gsap
20
- npm install driver.js # @sphynxoid/stepwise/adapters/driver-js
21
- # @sphynxoid/stepwise/adapters/scroll is dependency-free
22
- ```
23
-
24
- ## Required: Tailwind CSS setup
25
-
26
- The styled components ship **className strings**. Tailwind v4's automatic
27
- content detection skips `node_modules`, so **no library class gets generated**
28
- unless you register the source. Without this, every library element renders
29
- completely unstyled.
30
-
31
- ```css
32
- @import "tailwindcss";
33
- @source "../node_modules/@sphynxoid/stepwise/src";
34
- ```
35
-
36
- Components are also fully usable without Tailwind if you bring your own
37
- presentation — the hooks are headless.
38
-
39
- ## Quick start — a narrated player
40
-
41
- ```tsx
42
- "use client";
43
- import { useStepPlayer, PlayerBar, CodeBlock, type StepTone } from "@sphynxoid/stepwise";
44
-
45
- const frames: { tone: StepTone; label: string; code: string[] }[] = [
46
- { tone: "write", label: "Initialize", code: ["let i = 0;"] },
47
- { tone: "neutral", label: "Loop", code: ["while (i < n) { … }"] },
48
- { tone: "good", label: "Done", code: ["return arr;"] },
49
- ];
50
-
51
- export function Demo() {
52
- const player = useStepPlayer({ total: frames.length, autoPlay: true, syncHash: "frame" });
53
- const frame = frames[Math.max(0, player.index)] ?? frames[0];
54
-
55
- return (
56
- <>
57
- <CodeBlock lines={frame.code} />
58
- <PlayerBar player={player} toneAt={(i) => frames[i]?.tone ?? "neutral"} titleAt={(i) => frames[i]?.label ?? ""} />
59
- </>
60
- );
61
- }
62
- ```
63
-
64
- `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.
65
-
66
- ## The three scales
67
-
68
- | Scale | Hook | Presentation |
69
- |---|---|---|
70
- | **Player** — auto-played frames in a trace | `useStepPlayer` (+ `PlayerEngine` adapters) | `PlayerBar`, `StepScrubber`, `CodeBlock` |
71
- | **Tour** — steps that mutate app state via `onEnter` | `useGuidedTour` (+ `TourDriver` adapters) | `TourSpotlight`, `TourCard` |
72
- | **Chapters** — scroll-tracked sections | `useChapters`, `flattenChapters` | `ChapterNav`, `LessonPager` |
73
-
74
- All three compose the same `useSteps` cursor, so movement, clamping, and boundary flags behave identically everywhere.
75
-
76
- Tours are small state machines: a step may declare `when` (skip on arrival when false), `gate` (block `next()` — and expose `blocked` — until the user actually does the thing), and `next` (edge override to another step's `id`, enabling branches and loops). `gotoId(id)` navigates by identity; programmatic navigation never trips gates.
77
-
78
- ## Show-once tutorials (led discovery)
79
-
80
- Pair `useDiscoveries` with `useGuidedTour` to auto-play a tour the first time a user encounters a feature:
81
-
82
- ```tsx
83
- const { discovered, ready, markDiscovered } = useDiscoveries();
84
- const tour = useGuidedTour(steps);
85
-
86
- // when this build carries an undiscovered feature, play its tour once
87
- useEffect(() => {
88
- if (ready && !tour.active && !discovered.has("search:v1")) tour.start();
89
- }, [ready, tour.active, discovered]);
90
-
91
- // mark on completion (or explicit dismiss) — never on mere trigger
92
- useEffect(() => {
93
- if (launched && !tour.active) markDiscovered("search:v1");
94
- }, [tour.active]);
95
- ```
96
-
97
- The ledger is a set of opaque ids behind a `DiscoveryStore` seam: `localStorageStore` (default), `cookieStore` (when SSR markup must know), `memoryStore` (tests), or your own store backed by an API for cross-device discovery.
98
-
99
- ## Supporting pieces
100
-
101
- - `useGlobalKeys` — global keyboard bindings with a form-control guard
102
- - `useFocusTrap` — dialog/tour focus containment
103
- - `usePrefersReducedMotion` — motion preference gate
104
- - `useDiscoveries` (+ `localStorageStore` / `cookieStore` / `memoryStore`) — persisted discovery ledger with a swappable storage seam
105
- - `GlossaryProvider` / `Term` / `GlossaryAppendix` — recursive, depth-capped term tooltips with an auto-collecting appendix
106
- - `ShortcutsDialog` — keyboard shortcut reference
107
- - `defaultTheme` / `StepwiseTheme` — the default styling, overridable per component via a `theme` prop (swap in any design system)
108
-
109
- ## Adapters
110
-
111
- Import adapters from their subpaths — never from the root barrel:
112
-
113
- ```ts
114
- import { createGsapPlayerEngine } from "@sphynxoid/stepwise/adapters/gsap";
115
- import { createDriverJsTourDriver } from "@sphynxoid/stepwise/adapters/driver-js";
116
- import { createScrollPlayerEngine } from "@sphynxoid/stepwise/adapters/scroll";
117
- ```
118
-
119
- An adapter plugs into a tiny port (`PlayerEngine` / `TourDriver`), so you can write your own for Shepherd, Framer Motion, or anything else without touching the kit.
120
-
121
- ## React
122
-
123
- Supports React 18 and 19. All hooks are SSR-safe (`"use client"` modules, no window access at module evaluation) and every public hook returns referentially stable callbacks.
124
-
125
- ## License
126
-
127
- MIT
1
+ # @sphynxoid/stepwise
2
+
3
+ A kit for interactive, narrated, step-driven visualizations — one primitive (`useSteps`) at every scale.
4
+
5
+ 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
+
7
+ - **Headless core.** The hooks never render; the styled components are optional and theme-injectable.
8
+ - **One dependency by default.** The root barrel needs only React. GSAP and driver.js integrations are opt-in via adapter subpaths.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @sphynxoid/stepwise
14
+ ```
15
+
16
+ Optional adapters (install only what you use):
17
+
18
+ ```bash
19
+ npm install gsap # @sphynxoid/stepwise/adapters/gsap
20
+ npm install driver.js # @sphynxoid/stepwise/adapters/driver-js
21
+ # @sphynxoid/stepwise/adapters/scroll is dependency-free
22
+ ```
23
+
24
+ ## Styling — pick one
25
+
26
+ The hooks are headless. The styled components (`PlayerBar`, `TourCard`, `Term`, …) ship Tailwind class strings, and there are three ways to get them styled:
27
+
28
+ **1. No Tailwind — import the precompiled stylesheet** (once, at your app entry):
29
+
30
+ ```ts
31
+ import "@sphynxoid/stepwise/styles.css";
32
+ ```
33
+
34
+ It contains exactly the CSS the components use, no global reset (a small preflight is scoped to the components' `[data-stepwise]` roots), and light/dark defaults that follow `prefers-color-scheme` or a `.dark` / `[data-theme="dark"]` switch. Re-color it with CSS variables:
35
+
36
+ ```css
37
+ :root {
38
+ --stepwise-primary: #7c3aed; /* play button, active segment, rings */
39
+ --stepwise-accent: #0ea5e9; /* "accent" tone, links, wait hints */
40
+ }
41
+ ```
42
+
43
+ Tokens: `--stepwise-background`, `-foreground`, `-card`, `-popover`, `-primary`, `-primary-foreground`, `-accent`, `-destructive`, `-ring`, `-shade` (overlay/code-block tint).
44
+
45
+ **2. Tailwind + shadcn/ui — copy the components in with the shadcn CLI:**
46
+
47
+ ```bash
48
+ npx shadcn@latest add https://stepwise-demo.vercel.app/r/player-bar.json
49
+ ```
50
+
51
+ 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.
52
+
53
+ **3. Tailwind, using the components from npm** — register the package so Tailwind generates its classes (v4 skips `node_modules`):
54
+
55
+ ```css
56
+ @import "tailwindcss";
57
+ @source "../node_modules/@sphynxoid/stepwise/dist";
58
+ ```
59
+
60
+ Token contract, Tailwind v3 setup and the "everything renders unstyled" checklist: [TAILWIND-SETUP.md](https://github.com/TheSphynxoid/stepwise/blob/master/docs/TAILWIND-SETUP.md).
61
+
62
+ ## Quick start — a narrated player
63
+
64
+ ```tsx
65
+ "use client";
66
+ import { useTrace, PlayerBar, CodeBlock, type StepwiseStep } from "@sphynxoid/stepwise";
67
+
68
+ // Your domain payload rides along with the presentation fields.
69
+ const frames: StepwiseStep<{ code: string[] }>[] = [
70
+ { tone: "write", label: "Initialize", code: ["let i = 0;"] },
71
+ { tone: "neutral", label: "Loop", code: ["while (i < n) { … }"] },
72
+ { tone: "good", label: "Done", code: ["return arr;"] },
73
+ ];
74
+
75
+ export function Demo() {
76
+ const trace = useTrace(frames, { autoPlay: true, syncHash: "frame" });
77
+
78
+ return (
79
+ <>
80
+ <CodeBlock lines={trace.step!.code} />
81
+ <PlayerBar player={trace.player} toneAt={trace.toneAt} titleAt={trace.titleAt} />
82
+ </>
83
+ );
84
+ }
85
+ ```
86
+
87
+ `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
+
89
+ ## The three scales
90
+
91
+ | Scale | Hook | Presentation |
92
+ |---|---|---|
93
+ | **Player** — auto-played frames in a trace | `useStepPlayer` (+ `PlayerEngine` adapters) | `PlayerBar`, `StepScrubber`, `CodeBlock` |
94
+ | **Tour** — steps that mutate app state via `onEnter` | `useGuidedTour` (+ `TourDriver` adapters) | `TourSpotlight`, `TourCard` |
95
+ | **Chapters** — scroll-tracked sections | `useChapters`, `flattenChapters` | `ChapterNav`, `LessonPager` |
96
+
97
+ All three compose the same `useSteps` cursor, so movement, clamping, and boundary flags behave identically everywhere.
98
+
99
+ Tours are small state machines: a step may declare `when` (skip on arrival when false), `gate` (block `next()` — and expose `blocked` — until the user actually does the thing), and `next` (edge override to another step's `id`, enabling branches and loops). `gotoId(id)` navigates by identity; programmatic navigation never trips gates.
100
+
101
+ ## Show-once tutorials (led discovery)
102
+
103
+ Pair `useDiscoveries` with `useGuidedTour` to auto-play a tour the first time a user encounters a feature:
104
+
105
+ ```tsx
106
+ const { discovered, ready, markDiscovered } = useDiscoveries();
107
+ const tour = useGuidedTour(steps);
108
+
109
+ // when this build carries an undiscovered feature, play its tour once
110
+ useEffect(() => {
111
+ if (ready && !tour.active && !discovered.has("search:v1")) tour.start();
112
+ }, [ready, tour.active, discovered]);
113
+
114
+ // mark on completion (or explicit dismiss) — never on mere trigger
115
+ useEffect(() => {
116
+ if (launched && !tour.active) markDiscovered("search:v1");
117
+ }, [tour.active]);
118
+ ```
119
+
120
+ The ledger is a set of opaque ids behind a `DiscoveryStore` seam: `localStorageStore` (default), `cookieStore` (when SSR markup must know), `memoryStore` (tests), or your own store backed by an API for cross-device discovery.
121
+
122
+ ## Supporting pieces
123
+
124
+ - `useGlobalKeys` — global keyboard bindings with a form-control guard
125
+ - `useFocusTrap` — dialog/tour focus containment
126
+ - `usePrefersReducedMotion` — motion preference gate
127
+ - `useDiscoveries` (+ `localStorageStore` / `cookieStore` / `memoryStore`) — persisted discovery ledger with a swappable storage seam
128
+ - `GlossaryProvider` / `Term` / `GlossaryAppendix` — recursive, depth-capped term tooltips with an auto-collecting appendix
129
+ - `ShortcutsDialog` — keyboard shortcut reference
130
+ - `defaultTheme` / `StepwiseTheme` — the default styling, overridable per component via a `theme` prop (swap in any design system)
131
+
132
+ ## Adapters
133
+
134
+ Import adapters from their subpaths — never from the root barrel:
135
+
136
+ ```ts
137
+ import { createGsapPlayerEngine } from "@sphynxoid/stepwise/adapters/gsap";
138
+ import { createDriverJsTourDriver } from "@sphynxoid/stepwise/adapters/driver-js";
139
+ import { createScrollPlayerEngine } from "@sphynxoid/stepwise/adapters/scroll";
140
+ ```
141
+
142
+ An adapter plugs into a tiny port (`PlayerEngine` / `TourDriver`), so you can write your own for Shepherd, Framer Motion, or anything else without touching the kit.
143
+
144
+ ## React
145
+
146
+ Supports React 18 and 19. All hooks are SSR-safe (`"use client"` modules, no window access at module evaluation) and every public hook returns referentially stable callbacks.
147
+
148
+ ## License
149
+
150
+ MIT
@@ -1,4 +1,4 @@
1
- import { T as TourDriver } from '../types-CPf9ZD_Z.js';
1
+ import { T as TourDriver } from '../types-IT_9L8On.js';
2
2
  import 'react';
3
3
 
4
4
  interface DriverJsTourOptions {
@@ -1,4 +1,4 @@
1
- import { P as PlayerEngine } from '../types-CPf9ZD_Z.js';
1
+ import { P as PlayerEngine } from '../types-IT_9L8On.js';
2
2
  import 'react';
3
3
 
4
4
  interface GsapEngineOptions {
@@ -1,4 +1,4 @@
1
- import { P as PlayerEngine } from '../types-CPf9ZD_Z.js';
1
+ import { P as PlayerEngine } from '../types-IT_9L8On.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, a as TourStepInput, T as TourDriver, C as Chapter, S as StepTone, b as ShortcutItem } from './types-CPf9ZD_Z.js';
2
- export { c as PlayerEngineApi, d as PlayerEngineHandle, e as StepwiseStep, f as TourContext } from './types-CPf9ZD_Z.js';
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';
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,12 @@ 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;
65
72
  }
66
73
  interface StepPlayer {
67
74
  /** Total frames in the current timeline. */
@@ -94,7 +101,199 @@ interface StepPlayer {
94
101
  setPlaying: (playing: boolean) => void;
95
102
  setSpeed: (speed: number) => void;
96
103
  }
97
- declare function useStepPlayer({ total, baseDelayMs, autoPlay, engine, syncHash, onEnd }: StepPlayerOptions): StepPlayer;
104
+ declare function useStepPlayer({ total, baseDelayMs, autoPlay, engine, syncHash, onEnd, respectReducedMotion, }: StepPlayerOptions): StepPlayer;
105
+
106
+ interface TracePlayer<S extends StepwiseStep> {
107
+ /** The underlying player — pass it to PlayerBar. */
108
+ player: StepPlayer;
109
+ /**
110
+ * The frame to render. Never undefined for a non-empty trace: before the
111
+ * player starts (`player.index === -1`) this is frame 0, not the last one.
112
+ */
113
+ step: S | undefined;
114
+ /** Index of `step` in the trace (0 before start; -1 for an empty trace). */
115
+ frame: number;
116
+ /** Stable `toneAt` for PlayerBar / StepScrubber. */
117
+ toneAt: (i: number) => StepTone;
118
+ /** Stable `titleAt` for PlayerBar / StepScrubber. */
119
+ titleAt: (i: number) => string;
120
+ }
121
+ /**
122
+ * A player bound to a concrete trace: the steps array is the timeline, and
123
+ * the hook resolves the frame to render (frame 0 before start — invariant
124
+ * "index -1 means not started"), plus scrubber accessors that read the
125
+ * latest steps through a ref. Replaces the per-page
126
+ * `player.index >= 0 ? … : 0` boilerplate.
127
+ *
128
+ * const trace = useTrace(STEPS, { autoPlay: true });
129
+ * <Viz arr={trace.step.arr} />
130
+ * <PlayerBar player={trace.player} toneAt={trace.toneAt} titleAt={trace.titleAt} />
131
+ */
132
+ declare function useTrace<S extends StepwiseStep>(steps: readonly S[], opts?: Omit<StepPlayerOptions, "total">): TracePlayer<S>;
133
+
134
+ interface TraceBuilder<Data extends object> {
135
+ /** Append one frame. Returns the builder, so calls chain. */
136
+ step(step: StepwiseStep<Data>): TraceBuilder<Data>;
137
+ /** Frames appended so far. */
138
+ readonly length: number;
139
+ /** The most recently appended frame (handy for "carry the state forward"). */
140
+ last(): StepwiseStep<Data> | undefined;
141
+ /** Snapshot the trace. A fresh array each call — the builder stays usable. */
142
+ build(): StepwiseStep<Data>[];
143
+ }
144
+ /**
145
+ * Typed trace builder. Authors run their algorithm at module scope and record
146
+ * frames as they go — no IIFE, no pre-declared `const STEPS = []` that a
147
+ * component might read before it is filled (invariant 6):
148
+ *
149
+ * const t = createTrace<{ arr: number[] }>();
150
+ * for (…) t.step({ tone: "write", label: "swap", arr: [...arr] });
151
+ * export const STEPS = t.build();
152
+ *
153
+ * Pure data — no React, safe on the server.
154
+ */
155
+ declare function createTrace<Data extends object = Record<never, never>>(): TraceBuilder<Data>;
156
+
157
+ /** Named traces to play side by side: `{ bubble: stepsA, insertion: stepsB }`. */
158
+ type Tracks = Record<string, readonly StepwiseStep<object>[]>;
159
+ /**
160
+ * Sync-point key for a step. Steps with the same key in two or more tracks
161
+ * land on the same aligned frame; `null`/`undefined` marks a free step. The
162
+ * n-th occurrence of a key in one track pairs with the n-th in another.
163
+ */
164
+ type TrackKeyFn<T extends Tracks = Tracks> = (step: T[keyof T][number], index: number, track: keyof T & string) => string | null | undefined;
165
+ /** One track's slot in one aligned frame. */
166
+ interface AlignedCell {
167
+ /** Index into the track's own steps; -1 only for an empty track. */
168
+ index: number;
169
+ /**
170
+ * The track did not advance on this frame: it is holding its last frame
171
+ * (finished, or waiting for a sync point), or waiting at frame 0 for its
172
+ * first sync point.
173
+ */
174
+ held: boolean;
175
+ }
176
+ type AlignedRow<K extends string = string> = Record<K, AlignedCell>;
177
+ /**
178
+ * Align several traces into one timeline. Pure — use it directly for server
179
+ * rendering or tests; `useSyncedTracks` memoizes it per track identity.
180
+ *
181
+ * - No `key` (hold): frame i shows step i of every track; a shorter track
182
+ * holds its last frame once it runs out. Length = the longest track.
183
+ * - With `key`: tracks advance together frame by frame, but a track whose
184
+ * next step is a sync point holds until every other track that shares the
185
+ * point has reached it, so matching steps share a frame. Keys must occur
186
+ * in the same relative order across tracks; where they conflict, that
187
+ * frame degrades to lockstep instead of deadlocking.
188
+ */
189
+ declare function alignTracks<T extends Tracks>(tracks: T, key?: TrackKeyFn<T>): AlignedRow<keyof T & string>[];
190
+ interface TrackView<S> {
191
+ /** The step to render for this track (undefined only for an empty track). */
192
+ step: S | undefined;
193
+ /** Index into the track's own steps; -1 for an empty track. */
194
+ index: number;
195
+ /** The track did not advance on this frame (see `AlignedCell.held`). */
196
+ held: boolean;
197
+ /** The track is on its final step. */
198
+ done: boolean;
199
+ /** Length of the track's own trace. */
200
+ length: number;
201
+ }
202
+ interface SyncedTracksOptions<T extends Tracks = Tracks> extends Omit<StepPlayerOptions, "total"> {
203
+ /**
204
+ * Sync-point alignment (see `alignTracks`). Omit to hold shorter tracks on
205
+ * their last frame. Read when the tracks change — pass the same tracks to
206
+ * keep the alignment memoized; an inline function is fine.
207
+ */
208
+ key?: TrackKeyFn<T>;
209
+ }
210
+ interface SyncedTracks<T extends Tracks> {
211
+ /** One player over the aligned timeline — pass it to PlayerBar. */
212
+ player: StepPlayer;
213
+ /** Aligned frame to render: 0 before the player starts, -1 when all tracks are empty. */
214
+ frame: number;
215
+ /** Number of aligned frames. */
216
+ total: number;
217
+ /** Per-track view of `frame`. */
218
+ tracks: {
219
+ [K in keyof T]: TrackView<T[K][number]>;
220
+ };
221
+ /** The full alignment table (stable while the tracks are). */
222
+ rows: readonly AlignedRow<keyof T & string>[];
223
+ /** Stable scrubber accessors: the most severe tone among tracks that moved. */
224
+ toneAt: (i: number) => StepTone;
225
+ /** Stable scrubber accessor: `track: label` for every track that moved. */
226
+ titleAt: (i: number) => string;
227
+ }
228
+ /**
229
+ * Comparison mode: several traces of the same input (two engines, two
230
+ * algorithms) under ONE player. The cursor walks the aligned timeline from
231
+ * `alignTracks`; each track resolves its own step, so payload types stay
232
+ * per-track. Like `useTrace`, frame 0 renders before playback starts.
233
+ *
234
+ * const cmp = useSyncedTracks({ bubble: BUBBLE, insertion: INSERTION }, { autoPlay: true });
235
+ * <Lane step={cmp.tracks.bubble.step} held={cmp.tracks.bubble.held} />
236
+ * <PlayerBar player={cmp.player} toneAt={cmp.toneAt} titleAt={cmp.titleAt} />
237
+ */
238
+ declare function useSyncedTracks<T extends Tracks>(tracks: T, opts?: SyncedTracksOptions<T>): SyncedTracks<T>;
239
+
240
+ interface LessonStreamOptions {
241
+ /** Lesson to open on (deep link). Clamped. Default 0. */
242
+ seed?: number;
243
+ /** Lessons mounted on each side of the seed initially. Default 1. */
244
+ radius?: number;
245
+ /** Load the next lesson when the last mounted one's bottom is within this many px below the viewport. Default 600. */
246
+ approachBelow?: number;
247
+ /** Load the previous lesson when the first mounted one's top is below `-approachAbove`. Default 300. */
248
+ approachAbove?: number;
249
+ /** Scroll the seed lesson into view before first paint (seed > 0). Default true. */
250
+ scrollToSeed?: boolean;
251
+ /**
252
+ * Fired when the scroll-tracked active lesson changes (not on mount) — sync
253
+ * the URL / document title here. Read through a ref; inline is fine.
254
+ */
255
+ onActiveChange?: (index: number) => void;
256
+ }
257
+ interface LessonStream {
258
+ /** Lesson containing the viewport center (the seed until the first measure). */
259
+ active: number;
260
+ /** The seed lesson — SSR'd visible; every other block should fade in. */
261
+ seed: number;
262
+ /** Indices to render, in order (the mounted window). */
263
+ mounted: number[];
264
+ /** The window reaches the last lesson — render an end-of-stream footer. */
265
+ complete: boolean;
266
+ /** Stable callback ref for lesson `i`'s block element (cached per index). */
267
+ refFor: (index: number) => (el: HTMLElement | null) => void;
268
+ /** Mount through lesson `i` (if needed) and scroll to it. */
269
+ goTo: (index: number) => void;
270
+ next: () => void;
271
+ prev: () => void;
272
+ reducedMotion: boolean;
273
+ }
274
+ /**
275
+ * Continuous reader for a sequence of lessons (a notebook): renders a growing
276
+ * window of lessons around the seed instead of all of them, and tracks the
277
+ * active one by viewport-center containment (invariant 1).
278
+ *
279
+ * - The lesson after the window mounts as the reader approaches its place;
280
+ * the one before mounts near the top, and a layout effect compensates the
281
+ * scroll so prepended content never shoves the reader down.
282
+ * - Navigation (`goTo`) expansions are NOT compensated — their scrollIntoView
283
+ * is absolute, and compensating them displaced the landing.
284
+ * - The cursor is a useSteps core, so it composes with ChapterNav
285
+ * (`active` + `goTo`) and LessonPager (`prev`/`next`).
286
+ *
287
+ * Headless: the consumer renders `mounted.map(i => <Block ref={refFor(i)} />)`
288
+ * and owns the content; pair blocks with `useFadeIn(i === seed)`.
289
+ */
290
+ declare function useLessonStream(count: number, opts?: LessonStreamOptions): LessonStream;
291
+ /**
292
+ * Mount fade for streamed blocks: `false` on the first client render, `true`
293
+ * a frame later (immediately with reduced motion). Pass `initiallyVisible`
294
+ * for server-rendered blocks (the stream's seed) so they never flash.
295
+ */
296
+ declare function useFadeIn(initiallyVisible?: boolean): boolean;
98
297
 
99
298
  interface GuidedTour {
100
299
  /** Active step index, or null when the tour is not running. */
@@ -438,4 +637,4 @@ declare function ShortcutsDialog({ open, onClose, items, title, className, theme
438
637
  theme?: StepwiseTheme;
439
638
  }): react.ReactPortal | null;
440
639
 
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 };
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 };