@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 CHANGED
@@ -1,127 +1,214 @@
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
+ **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">
8
+
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.
10
+
11
+ - **Headless core.** The hooks never render; the styled components are optional and theme-injectable.
12
+ - **One dependency by default.** The root barrel needs only React. GSAP and driver.js integrations are opt-in via adapter subpaths.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @sphynxoid/stepwise
18
+ ```
19
+
20
+ Optional adapters (install only what you use):
21
+
22
+ ```bash
23
+ npm install gsap # @sphynxoid/stepwise/adapters/gsap
24
+ npm install driver.js # @sphynxoid/stepwise/adapters/driver-js
25
+ # @sphynxoid/stepwise/adapters/scroll is dependency-free
26
+ ```
27
+
28
+ ## Styling — pick one
29
+
30
+ The hooks are headless. The styled components (`PlayerBar`, `TourCard`, `Term`, …) ship Tailwind class strings, and there are three ways to get them styled:
31
+
32
+ **1. No Tailwind — import the precompiled stylesheet** (once, at your app entry):
33
+
34
+ ```ts
35
+ import "@sphynxoid/stepwise/styles.css";
36
+ ```
37
+
38
+ 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:
39
+
40
+ ```css
41
+ :root {
42
+ --stepwise-primary: #7c3aed; /* play button, active segment, rings */
43
+ --stepwise-accent: #0ea5e9; /* "accent" tone, links, wait hints */
44
+ }
45
+ ```
46
+
47
+ Tokens: `--stepwise-background`, `-foreground`, `-card`, `-popover`, `-primary`, `-primary-foreground`, `-accent`, `-destructive`, `-ring`, `-shade` (overlay/code-block tint).
48
+
49
+ **2. Tailwind + shadcn/ui — copy the components in with the shadcn CLI:**
50
+
51
+ ```bash
52
+ npx shadcn@latest add https://stepwise.thesphynx.net/r/player-bar.json
53
+ ```
54
+
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.
56
+
57
+ **3. Tailwind, using the components from npm** — register the package so Tailwind generates its classes (v4 skips `node_modules`):
58
+
59
+ ```css
60
+ @import "tailwindcss";
61
+ @source "../node_modules/@sphynxoid/stepwise/dist";
62
+ ```
63
+
64
+ 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).
65
+
66
+ ## Quick start — a narrated player
67
+
68
+ ```tsx
69
+ "use client";
70
+ import { useTrace, PlayerBar, CodeBlock, type StepwiseStep } from "@sphynxoid/stepwise";
71
+
72
+ // Your domain payload rides along with the presentation fields.
73
+ const frames: StepwiseStep<{ code: string[] }>[] = [
74
+ { tone: "write", label: "Initialize", code: ["let i = 0;"] },
75
+ { tone: "neutral", label: "Loop", code: ["while (i < n) { … }"] },
76
+ { tone: "good", label: "Done", code: ["return arr;"] },
77
+ ];
78
+
79
+ export function Demo() {
80
+ const trace = useTrace(frames, { autoPlay: true, syncHash: "frame" });
81
+
82
+ return (
83
+ <>
84
+ <CodeBlock lines={trace.step!.code} />
85
+ <PlayerBar player={trace.player} toneAt={trace.toneAt} titleAt={trace.titleAt} />
86
+ </>
87
+ );
88
+ }
89
+ ```
90
+
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.
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
+
153
+ ## The three scales
154
+
155
+ | Scale | Hook | Presentation |
156
+ |---|---|---|
157
+ | **Player** — auto-played frames in a trace | `useStepPlayer` (+ `PlayerEngine` adapters) | `PlayerBar`, `StepScrubber`, `CodeBlock` |
158
+ | **Tour** — steps that mutate app state via `onEnter` | `useGuidedTour` (+ `TourDriver` adapters) | `TourSpotlight`, `TourCard` |
159
+ | **Chapters** — scroll-tracked sections | `useChapters`, `flattenChapters` | `ChapterNav`, `LessonPager` |
160
+
161
+ All three compose the same `useSteps` cursor, so movement, clamping, and boundary flags behave identically everywhere.
162
+
163
+ 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.
164
+
165
+ ## Show-once tutorials (led discovery)
166
+
167
+ Pair `useDiscoveries` with `useGuidedTour` to auto-play a tour the first time a user encounters a feature:
168
+
169
+ ```tsx
170
+ const { discovered, ready, markDiscovered } = useDiscoveries();
171
+ const tour = useGuidedTour(steps);
172
+
173
+ // when this build carries an undiscovered feature, play its tour once
174
+ useEffect(() => {
175
+ if (ready && !tour.active && !discovered.has("search:v1")) tour.start();
176
+ }, [ready, tour.active, discovered]);
177
+
178
+ // mark on completion (or explicit dismiss) — never on mere trigger
179
+ useEffect(() => {
180
+ if (launched && !tour.active) markDiscovered("search:v1");
181
+ }, [tour.active]);
182
+ ```
183
+
184
+ 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.
185
+
186
+ ## Supporting pieces
187
+
188
+ - `useGlobalKeys` — global keyboard bindings with a form-control guard
189
+ - `useFocusTrap` — dialog/tour focus containment
190
+ - `usePrefersReducedMotion` — motion preference gate
191
+ - `useDiscoveries` (+ `localStorageStore` / `cookieStore` / `memoryStore`) — persisted discovery ledger with a swappable storage seam
192
+ - `GlossaryProvider` / `Term` / `GlossaryAppendix` — recursive, depth-capped term tooltips with an auto-collecting appendix
193
+ - `ShortcutsDialog` — keyboard shortcut reference
194
+ - `defaultTheme` / `StepwiseTheme` — the default styling, overridable per component via a `theme` prop (swap in any design system)
195
+
196
+ ## Adapters
197
+
198
+ Import adapters from their subpaths — never from the root barrel:
199
+
200
+ ```ts
201
+ import { createGsapPlayerEngine } from "@sphynxoid/stepwise/adapters/gsap";
202
+ import { createDriverJsTourDriver } from "@sphynxoid/stepwise/adapters/driver-js";
203
+ import { createScrollPlayerEngine } from "@sphynxoid/stepwise/adapters/scroll";
204
+ ```
205
+
206
+ 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.
207
+
208
+ ## React
209
+
210
+ 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.
211
+
212
+ ## License
213
+
214
+ MIT
@@ -1,4 +1,4 @@
1
- import { T as TourDriver } from '../types-CPf9ZD_Z.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-CPf9ZD_Z.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-CPf9ZD_Z.js';
1
+ import { P as PlayerEngine } from '../types-CNMxISXe.js';
2
2
  import 'react';
3
3
 
4
4
  interface ScrollEngineOptions {