solid-drift 0.7.1 → 0.8.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
@@ -723,6 +723,8 @@ onMount(() => kinetic.play());
723
723
 
724
724
  Returns `{ play, stop, replay, status }`. `play()` resolves when the last unit arrives. Under reduced motion every unit jumps to its final state, so the text is fully readable.
725
725
 
726
+ `from` also accepts `variance` (0 to 1, default 0) and `seed`. Each unit jitters `y`, `blur`, `scale`, and `rotate` around the `from` values by up to `variance`, so a headline feels hand-set instead of mechanical. The jitter uses a seeded PRNG, so the same `seed` renders the exact same layout on every run (stable across SSR and replays). `variance: 0` keeps the classic uniform behavior.
727
+
726
728
  ### `createScenePlayer(scenes)`
727
729
 
728
730
  Scene orchestrator for showreels and launch films: an ordered list of scenes, each with a `duration` and `onEnter`/`onExit` hooks. `scene()` tells your view which scene is live; the hooks trigger each scene's choreography (a `createKineticType`, a camera move, a color shift).
@@ -803,7 +805,203 @@ const off = beat.onBeat((b) => {
803
805
  beat.start();
804
806
  ```
805
807
 
806
- Returns `{ beat, bar, phase, onBeat, start, stop, status }`. `onBeat` returns an unsubscribe function. Beats are timing, not motion, so the clock keeps ticking under reduced motion (your callbacks decide what that means visually).
808
+ Returns `{ beat, bar, beatsPerBar, phase, onBeat, start, stop, status }`. `onBeat` returns an unsubscribe function. Beats are timing, not motion, so the clock keeps ticking under reduced motion (your callbacks decide what that means visually).
809
+
810
+ ### `createShowreel(scenes)`
811
+
812
+ A guided showreel recipe on top of `createScenePlayer`: scenes carry a `kind` label (`"title"`, `"camera"`, `"color"`, `"cut"`, `"custom"`) so the reel reads like a shot list. It returns the full scene player controls, so `play`, `pause`, `next`, `prev`, and `goTo` all work unchanged.
813
+
814
+ ```ts
815
+ const reel = createShowreel([
816
+ { kind: "title", duration: 1200, onEnter: () => titleCard.play() },
817
+ { kind: "camera", duration: 2000, onEnter: () => dolly.play() },
818
+ { kind: "color", duration: 1500, onEnter: () => finale.play() },
819
+ { kind: "cut", duration: 400, onEnter: () => wipe.play() },
820
+ ]);
821
+ await reel.play(); // resolves after the last scene
822
+ ```
823
+
824
+ Showreel recipe: combine `createKineticType` (title cards), `createCamera` (dolly moves), `createColorShift` (finale grade), `createTransition` (match cuts), `createBeat` (rhythm), and `createShowreel` (the shot list). Under reduced motion `play()` jumps straight to the last scene.
825
+
826
+ ### `createBeatCuts(beat, player, options?)`
827
+
828
+ Beat-synced scene cuts: advances the player every N beats through the beat clock's `onBeat`. The default interval is the beat clock's `beatsPerBar`, so a cut lands on every downbeat. Returns a cleanup function that unsubscribes the cut listener.
829
+
830
+ ```ts
831
+ const beat = createBeat({ bpm: 128, beatsPerBar: 4 });
832
+ const stopCuts = createBeatCuts(beat, player); // cut every bar
833
+ // const stopCuts = createBeatCuts(beat, player, { every: 8 }); // every 2 bars
834
+ beat.start();
835
+ await player.play();
836
+ stopCuts();
837
+ ```
838
+
839
+ Cuts only fire while the player is running, so pausing the reel pauses the cuts too. Beat timing is not motion, so cuts keep firing under reduced motion (pair with a reduced-motion-safe `onEnter` if the cut itself animates).
840
+
841
+ ### `createStreamReveal(ref, options?)`
842
+
843
+ Streaming text for chat and agent UIs: push characters as they arrive and each batch reveals with the kinetic treatment (rise, deblur, settle). Batches flush on a short cadence, or early when the queue grows past `maxBatch`, so fast streams never fall behind.
844
+
845
+ ```tsx
846
+ let out!: HTMLDivElement;
847
+ const stream = createStreamReveal(() => out, { batchMs: 120, maxBatch: 24 });
848
+ const res = await fetch("/chat", { method: "POST", body: q });
849
+ const reader = res.body!.getReader();
850
+ const decoder = new TextDecoder();
851
+ for (;;) {
852
+ const { done, value } = await reader.read();
853
+ if (done) break;
854
+ stream.push(decoder.decode(value, { stream: true }));
855
+ }
856
+ await stream.complete(); // flush the tail, then done
857
+ <div ref={out} aria-live="polite" />
858
+ ```
859
+
860
+ Returns `{ push, complete, reset, status, pending }`. `status()` is `"idle"`, `"streaming"`, or `"done"`; `pending()` counts queued characters. Under reduced motion pushed text appears immediately with no per-unit animation; on the server `push` is a no-op and `status()` is `"done"`.
861
+
862
+ ### `createAgentState(options?)`
863
+
864
+ A tiny state machine for agent UIs: `idle`, `thinking`, `streaming`, `tool`, `done`, `error`, with legal-transition gating and enter/exit hooks. Pure signals, no DOM, so it works on the server and in tests.
865
+
866
+ ```ts
867
+ const agent = createAgentState({
868
+ allowed: [
869
+ { from: "idle", to: "thinking" },
870
+ { from: "thinking", to: "streaming" },
871
+ { from: "streaming", to: "tool-call" },
872
+ { from: "tool-call", to: "streaming" },
873
+ { from: "streaming", to: "done" },
874
+ ],
875
+ onEnter: (s) => console.log("agent:", s),
876
+ });
877
+ agent.set("thinking");
878
+ agent.state(); // "thinking"
879
+ agent.prev(); // "idle"
880
+ ```
881
+
882
+ Agent UI recipe: `thinking` pairs with `createWobble` on typing dots, `streaming` drives `createStreamReveal`, `tool-call` overlays a `createTransition`, and `done`/`error` tint a status pill with `createColorShift`. Returns `{ state, prev, set, reset, is }`. Illegal moves are ignored. State is logic, not motion, so it behaves identically under reduced motion.
883
+
884
+ ### `parseDriftSpec(input)`
885
+
886
+ Validates a DriftSpec, the JSON motion spec a coding agent can generate: `{ version: 1, scenes: [{ primitive, target?, options?, duration? }] }`. Supported primitives: `kineticType`, `streamReveal`, `camera`, `colorShift`, `transition`, `beat`. Throws a `DriftSpecError` naming the exact path (`scenes[0].options.duration`) on the first problem.
887
+
888
+ ```ts
889
+ import { parseDriftSpec } from "solid-drift";
890
+
891
+ const spec = parseDriftSpec({
892
+ version: 1,
893
+ scenes: [
894
+ {
895
+ primitive: "kineticType",
896
+ target: "title",
897
+ options: { duration: 600, stagger: 40, from: { y: 40, variance: 0.5, seed: 7 } },
898
+ },
899
+ {
900
+ primitive: "colorShift",
901
+ options: {
902
+ stops: [
903
+ { at: 0, color: "#0a1220" },
904
+ { at: 1, color: "#d9a441" },
905
+ ],
906
+ duration: 1200,
907
+ },
908
+ duration: 1200, // step budget: move on even if the shift is still running
909
+ },
910
+ ],
911
+ });
912
+ ```
913
+
914
+ Prompt hint for generating specs: "Return ONLY a JSON DriftSpec: `{ version: 1, scenes: [...] }`. Each scene is `{ primitive, target?, options?, duration? }`. `primitive` is one of kineticType, streamReveal, camera, colorShift, transition, beat. `target` is a key into the refs map I provide. `options` match that primitive's options exactly. Keep scenes short; put a `duration` budget on any scene that should not block." Validation is pure and runs anywhere, including the server.
915
+
916
+ ### `createSpecPlayer(spec, refs)`
917
+
918
+ Plays a validated DriftSpec: each scene runs its primitive against the matching ref from the `refs` map (`{ title: () => el }`), then the player advances. A scene `duration` acts as a budget, so a long ambient loop never stalls the reel.
919
+
920
+ ```ts
921
+ const player = createSpecPlayer(spec, { title: () => titleEl });
922
+ await player.play(); // scenes in order, ends "done"
923
+ player.stop(); // halt mid-reel; the play() promise resolves
924
+ ```
925
+
926
+ Returns `{ scene, status, play, stop }`. `scene()` is the live scene index (-1 before the first play). Under reduced motion `play()` applies every scene's final state instantly and ends `"done"`.
927
+
928
+ ### `createTxLifecycle(options?)`
929
+
930
+ Transaction lifecycle for onchain UI, with zero wallet dependencies: feed it wagmi/viem-style state through an accessor (the adapter pattern) and it maps that to `idle`, `signing`, `pending`, `confirming`, `success`, `failed`. `progress()` springs between 0, 0.25, 0.5, 0.75, 1 so progress rings glide instead of jumping.
931
+
932
+ ```ts
933
+ // Adapter: your wagmi/viem state in, tx state out.
934
+ const [chain] = createSignal({ status: "idle" as const, confirmations: 0 });
935
+ const tx = createTxLifecycle({
936
+ source: () => ({
937
+ status: chain().status, // "idle" | "pending" | "success" | "error"
938
+ confirmations: chain().confirmations,
939
+ }),
940
+ requiredConfirmations: 2,
941
+ onEnter: (s) => console.log("tx:", s),
942
+ });
943
+ tx.set("signing"); // manual: the moment the wallet prompt opens
944
+ ```
945
+
946
+ Standard mapping: `set("signing")` when the wallet prompt opens, source `pending` (hash received) maps to `"pending"`, confirmations reaching the threshold promote to `"confirming"`, source `success`/`error` map to `"success"`/`"failed"`. Omit `source` for a fully manual lifecycle. Returns `{ state, set, reset, progress }`. On the server it stays `"idle"` with `progress()` 0; under reduced motion state changes apply instantly and `progress()` jumps to its target.
947
+
948
+ ### `createTicker(source, ref, options?)`
949
+
950
+ A price ticker with rolling digits: each digit rolls vertically on change, the whole figure flashes green/red on up/down moves, and rapid source updates batch into one render per frame. `display()` always holds the formatted string, so SSR and tests read the price without DOM.
951
+
952
+ ```tsx
953
+ const [price] = createSignal(64218.5);
954
+ const ticker = createTicker(price, () => priceEl, {
955
+ decimals: 2,
956
+ locale: "en-US",
957
+ upColor: "#16a34a",
958
+ downColor: "#dc2626",
959
+ flashMs: 600,
960
+ });
961
+ <div>
962
+ <span ref={priceEl} aria-label={`Price ${ticker.display()}`} />
963
+ </div>
964
+ ```
965
+
966
+ Returns `{ display, direction }`. `direction()` is `"up"`, `"down"`, or `"flat"`. Formatting uses `Intl.NumberFormat` with the given locale. Under reduced motion the text swaps instantly with no rolling digits and no color flash; on the server only `display()` and `direction()` work.
967
+
968
+ ### `createMintReveal(ref, options?)`
969
+
970
+ An NFT mint reveal: an anticipation shake winds up, the card flips on rotateY, `onFlip` fires at the midpoint (edge-on, so the face swap is invisible), and squash-and-stretch sells the landing. `reset()` returns the card to idle.
971
+
972
+ ```tsx
973
+ let card!: HTMLDivElement;
974
+ const reveal = createMintReveal(() => card, {
975
+ shakeDuration: 500,
976
+ flipDuration: 700,
977
+ squash: true,
978
+ onFlip: () => setFace("revealed"), // swap the artwork mid-flip
979
+ });
980
+ <button onClick={() => reveal.play()}>Reveal</button>
981
+ <div ref={card} style={{ "transform-style": "preserve-3d" }} />
982
+ ```
983
+
984
+ Returns `{ play, reset, status }`. `status()` walks `"idle"`, `"anticipating"`, `"flipping"`, `"revealed"`. Under reduced motion (and on the server) `play()` applies the revealed state immediately and still calls `onFlip`.
985
+
986
+ ### `createConnectButton(ref, options?)`
987
+
988
+ Wallet connect button micro-interactions: magnetic pull toward the pointer, a press scale, an animated check overlay for copy-address feedback, and a chain pulse ring. Pointer handling is global (presses that start inside still count if released outside), and everything cleans up on unmount.
989
+
990
+ ```tsx
991
+ let btn!: HTMLButtonElement;
992
+ const connect = createConnectButton(() => btn, { strength: 0.35 });
993
+ <button
994
+ ref={btn}
995
+ onClick={() => {
996
+ navigator.clipboard.writeText(address);
997
+ connect.copyTick(); // check overlay pops, then fades
998
+ }}
999
+ >
1000
+ {address}
1001
+ </button>
1002
+ ```
1003
+
1004
+ Returns `{ copyTick, chainPulse, status }`. `status()` is `"idle"`, `"ticking"` (check visible), or `"pulsing"` (ring expanding). Call `chainPulse()` after a successful connection or network switch. Under reduced motion there is no magnetic pull or scale; `copyTick()` and `chainPulse()` still show their overlays statically.
807
1005
 
808
1006
  ### Easings
809
1007
 
package/dist/ai.d.ts ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Streaming-text and agent-state family: motion primitives for
3
+ * conversational interfaces.
4
+ *
5
+ * Token streams arrive in bursts, so `createStreamReveal` batches them
6
+ * into a readable cadence before animating each unit's entrance.
7
+ * `createAgentState` is a pure-signal state machine for agent
8
+ * status, kept free of motion so each state can pair with any
9
+ * primitive. `parseDriftSpec` and `createSpecPlayer` let generated
10
+ * JSON choreography be validated and rendered deterministically.
11
+ *
12
+ * All primitives are signal-native, SSR-safe, dependency-free, and
13
+ * define sensible static behavior under `prefers-reduced-motion`.
14
+ */
15
+ import { type Accessor } from "solid-js";
16
+ import { type Easing, type EasingName } from "./easing.js";
17
+ import { type KineticTypeFrom } from "./motion.js";
18
+ type MaybeElement = () => Element | null | undefined;
19
+ export type StreamRevealStatus = "idle" | "streaming" | "done";
20
+ export interface StreamRevealOptions {
21
+ /** Split into "chars" or "words". Default "chars". */
22
+ unit?: "chars" | "words";
23
+ /** Flush the batch queue on this cadence, in ms. Default 120. */
24
+ batchMs?: number;
25
+ /** Flush early when the queue grows past this many chars. Default 24. */
26
+ maxBatch?: number;
27
+ /** Entrance duration per unit, in ms. Default 450. */
28
+ duration?: number;
29
+ /** Milliseconds between unit starts. Default 30. */
30
+ stagger?: number;
31
+ /** Starting transform/opacity/blur for each unit. */
32
+ from?: KineticTypeFrom;
33
+ /** Easing for each unit's entrance. Default "easeOutExpo". */
34
+ easing?: Easing | EasingName;
35
+ }
36
+ export interface StreamRevealControls {
37
+ /** Append raw stream tokens; they are batched internally. */
38
+ push: (chunk: string) => void;
39
+ /** Flush remaining units and settle. */
40
+ complete: () => void;
41
+ /** Clear the text and return to idle. */
42
+ reset: () => void;
43
+ /** Reactive status: "idle" | "streaming" | "done". */
44
+ status: Accessor<StreamRevealStatus>;
45
+ /** Chars waiting in the batch queue. */
46
+ pending: Accessor<number>;
47
+ }
48
+ /**
49
+ * Smooth irregular token cadence into readable animated text.
50
+ *
51
+ * Stream tokens arrive in bursts, a flood, then silence. `push()`
52
+ * only appends to an internal queue; one rAF task flushes the queue
53
+ * on a steady cadence (or early when it overflows) and drives every
54
+ * unit's entrance, so a 40-token burst reads as a calm typed line
55
+ * instead of a flicker.
56
+ *
57
+ * SSR-safe: `push` is a no-op on the server, `status()` is "done",
58
+ * and the host renders the full text statically. Under reduced motion
59
+ * batching and entrances are skipped: text appears as it is pushed.
60
+ *
61
+ * ```tsx
62
+ * let out!: HTMLDivElement
63
+ * const stream = createStreamReveal(() => out, { unit: "chars" })
64
+ * const reader = response.body.getReader()
65
+ * onMount(async () => {
66
+ * for (;;) {
67
+ * const { done, value } = await reader.read()
68
+ * if (done) break
69
+ * stream.push(decoder.decode(value))
70
+ * }
71
+ * stream.complete()
72
+ * })
73
+ * <div ref={out} aria-live="polite" />
74
+ * ```
75
+ */
76
+ export declare function createStreamReveal(ref: MaybeElement, options?: StreamRevealOptions): StreamRevealControls;
77
+ /** Lifecycle states of a conversational agent turn. */
78
+ export type AgentState = "idle" | "thinking" | "streaming" | "tool-call" | "done" | "error";
79
+ /** One legal state change. */
80
+ export interface AgentStateTransition {
81
+ from: AgentState;
82
+ to: AgentState;
83
+ }
84
+ export interface AgentStateOptions {
85
+ /** Starting state. Default "idle". */
86
+ initial?: AgentState;
87
+ /** Legal transitions. Omit to allow every transition. */
88
+ allowed?: AgentStateTransition[];
89
+ /** Called after entering a state, with the previous state. */
90
+ onEnter?: (state: AgentState, prev: AgentState) => void;
91
+ /** Called before leaving a state, with the next state. */
92
+ onExit?: (state: AgentState, next: AgentState) => void;
93
+ }
94
+ export interface AgentStateControls {
95
+ /** Current state. */
96
+ state: Accessor<AgentState>;
97
+ /** Previous state. */
98
+ prev: Accessor<AgentState>;
99
+ /** Move to the next state. Illegal transitions are ignored. */
100
+ set: (next: AgentState) => void;
101
+ /** Return to the initial state. */
102
+ reset: () => void;
103
+ /** Convenience for class bindings: `agent.is("thinking")`. */
104
+ is: (s: AgentState) => boolean;
105
+ }
106
+ /**
107
+ * A pure-signal state machine for agent UI.
108
+ *
109
+ * Motion is deliberately not built in: pair each state with a recipe
110
+ * instead, so the machine stays transparent, testable, and SSR-safe.
111
+ * A typical pairing is `createWobble` on typing dots while "thinking",
112
+ * `createStreamReveal` while "streaming", `createTransition` for the
113
+ * "tool-call" overlay, and `createColorShift` on the status pill for
114
+ * "done"/"error". Each of those degrades on its own under reduced
115
+ * motion.
116
+ *
117
+ * SSR-safe by construction: signals only, no DOM, no clock.
118
+ *
119
+ * ```ts
120
+ * const agent = createAgentState()
121
+ * agent.set("thinking")
122
+ * agent.state() // "thinking"
123
+ * agent.is("streaming") // false
124
+ * ```
125
+ */
126
+ export declare function createAgentState(options?: AgentStateOptions): AgentStateControls;
127
+ /** Primitives a drift spec can choreograph. */
128
+ export type DriftSpecPrimitive = "kineticType" | "camera" | "colorShift" | "transition" | "beat" | "streamReveal";
129
+ /** One choreographed step. */
130
+ export interface DriftSpecStep {
131
+ /** Which primitive renders this step. */
132
+ primitive: DriftSpecPrimitive;
133
+ /**
134
+ * Key into the refs record given to `createSpecPlayer`. Required
135
+ * for primitives that render into an element ("kineticType" and
136
+ * "streamReveal").
137
+ */
138
+ target?: string;
139
+ /**
140
+ * Primitive options. Validated against each primitive's minimal
141
+ * shape. "camera" reads `keyframes`, "colorShift" reads `stops`,
142
+ * "streamReveal" reads `text` here.
143
+ */
144
+ options?: Record<string, unknown>;
145
+ /** Step budget in milliseconds, for timed players. */
146
+ duration?: number;
147
+ }
148
+ /** A deterministic motion choreography, usually generated as JSON. */
149
+ export interface DriftSpec {
150
+ version: 1;
151
+ scenes: DriftSpecStep[];
152
+ }
153
+ /**
154
+ * Thrown by `parseDriftSpec`. `path` pinpoints the invalid value
155
+ * (for example "scenes[2].options.duration") so a generator can
156
+ * repair the spec without guessing.
157
+ */
158
+ export declare class DriftSpecError extends Error {
159
+ /** Dot/bracket path to the invalid value, "" for the root. */
160
+ path: string;
161
+ constructor(path: string, message: string);
162
+ }
163
+ /**
164
+ * Validate unknown input against the DriftSpec schema and return the
165
+ * typed spec. Throws `DriftSpecError` with a precise `path` on the
166
+ * first invalid value.
167
+ *
168
+ * Pure validation, no DOM: safe to run on the server.
169
+ *
170
+ * ```ts
171
+ * const spec = parseDriftSpec(JSON.parse(raw))
172
+ * const player = createSpecPlayer(spec, { title: () => titleEl })
173
+ * await player.play()
174
+ * ```
175
+ */
176
+ export declare function parseDriftSpec(input: unknown): DriftSpec;
177
+ export type SpecPlayerStatus = "idle" | "running" | "done";
178
+ export interface SpecPlayerControls {
179
+ /** Play every scene in order. Resolves after the last scene. */
180
+ play: () => Promise<void>;
181
+ /** Stop mid-spec. The play() promise resolves. */
182
+ stop: () => void;
183
+ /** Reactive status: "idle" | "running" | "done". */
184
+ status: Accessor<SpecPlayerStatus>;
185
+ /** Current scene index, or -1 before the first play(). */
186
+ scene: Accessor<number>;
187
+ }
188
+ /**
189
+ * Render a validated DriftSpec: each scene's primitive plays in
190
+ * order against the element refs the host supplies. `duration` on a
191
+ * step caps that step's budget.
192
+ *
193
+ * SSR-safe: `play()` is a no-op on the server. Under reduced motion
194
+ * `play()` jumps straight to the last scene (the clean final frame),
195
+ * matching `createScenePlayer`.
196
+ *
197
+ * ```ts
198
+ * const player = createSpecPlayer(spec, {
199
+ * title: () => titleEl,
200
+ * body: () => bodyEl,
201
+ * })
202
+ * player.scene() // 0, 1, ... as the spec plays
203
+ * await player.play()
204
+ * ```
205
+ */
206
+ export declare function createSpecPlayer(spec: DriftSpec, refs: Record<string, MaybeElement>): SpecPlayerControls;
207
+ export {};