@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 +150 -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 +204 -5
- package/dist/index.js +535 -148
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2 -0
- package/dist/{types-CPf9ZD_Z.d.ts → types-IT_9L8On.d.ts} +8 -3
- package/package.json +101 -86
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
|
-
##
|
|
25
|
-
|
|
26
|
-
The styled components ship
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
package/dist/adapters/gsap.d.ts
CHANGED
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, 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 };
|