solid-drift 0.2.0 → 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.
Files changed (54) hide show
  1. package/README.md +933 -10
  2. package/dist/ai.d.ts +207 -0
  3. package/dist/ai.js +570 -0
  4. package/dist/animate.d.ts +2 -2
  5. package/dist/animate.js +2 -2
  6. package/dist/cartoon.d.ts +190 -0
  7. package/dist/cartoon.js +334 -0
  8. package/dist/color.d.ts +53 -0
  9. package/dist/color.js +391 -0
  10. package/dist/directive.d.ts +5 -5
  11. package/dist/directive.js +15 -7
  12. package/dist/easing.d.ts +22 -1
  13. package/dist/easing.js +49 -1
  14. package/dist/flip.d.ts +44 -0
  15. package/dist/flip.js +108 -0
  16. package/dist/horizontal.d.ts +107 -0
  17. package/dist/horizontal.js +208 -0
  18. package/dist/index.d.ts +20 -3
  19. package/dist/index.js +17 -3
  20. package/dist/inview.d.ts +4 -4
  21. package/dist/inview.js +5 -5
  22. package/dist/motion.d.ts +404 -0
  23. package/dist/motion.js +761 -0
  24. package/dist/physics.d.ts +146 -0
  25. package/dist/physics.js +352 -0
  26. package/dist/pointer.d.ts +76 -0
  27. package/dist/pointer.js +123 -0
  28. package/dist/reduced-motion.d.ts +3 -3
  29. package/dist/reduced-motion.js +5 -4
  30. package/dist/scroll.d.ts +3 -3
  31. package/dist/scroll.js +5 -5
  32. package/dist/scrollfx.d.ts +156 -0
  33. package/dist/scrollfx.js +148 -0
  34. package/dist/scrub.d.ts +51 -0
  35. package/dist/scrub.js +67 -0
  36. package/dist/spring.d.ts +39 -4
  37. package/dist/spring.js +26 -7
  38. package/dist/stagger.d.ts +4 -4
  39. package/dist/stagger.js +4 -4
  40. package/dist/text.d.ts +27 -0
  41. package/dist/text.js +84 -0
  42. package/dist/timeline.d.ts +45 -0
  43. package/dist/timeline.js +93 -0
  44. package/dist/trail.d.ts +27 -0
  45. package/dist/trail.js +75 -0
  46. package/dist/tween.d.ts +3 -3
  47. package/dist/tween.js +3 -3
  48. package/dist/typography.d.ts +241 -0
  49. package/dist/typography.js +812 -0
  50. package/dist/velocity.d.ts +44 -0
  51. package/dist/velocity.js +88 -0
  52. package/dist/web3.d.ts +213 -0
  53. package/dist/web3.js +640 -0
  54. package/package.json +1 -1
package/dist/ai.js ADDED
@@ -0,0 +1,570 @@
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 { createSignal, onCleanup, } from "solid-js";
16
+ import { animate } from "./animate.js";
17
+ import { resolveEasing } from "./easing.js";
18
+ import { now, schedule } from "./engine.js";
19
+ import { createBeat, createCamera, createColorShift, createKineticType, createTransition, } from "./motion.js";
20
+ import { prefersReducedMotion } from "./reduced-motion.js";
21
+ import { appendUnits } from "./text.js";
22
+ function clamp01(v) {
23
+ return v < 0 ? 0 : v > 1 ? 1 : v;
24
+ }
25
+ /** Round to 2 decimals for style values. */
26
+ function fmt(v) {
27
+ return String(Math.round(v * 100) / 100);
28
+ }
29
+ /**
30
+ * Smooth irregular token cadence into readable animated text.
31
+ *
32
+ * Stream tokens arrive in bursts, a flood, then silence. `push()`
33
+ * only appends to an internal queue; one rAF task flushes the queue
34
+ * on a steady cadence (or early when it overflows) and drives every
35
+ * unit's entrance, so a 40-token burst reads as a calm typed line
36
+ * instead of a flicker.
37
+ *
38
+ * SSR-safe: `push` is a no-op on the server, `status()` is "done",
39
+ * and the host renders the full text statically. Under reduced motion
40
+ * batching and entrances are skipped: text appears as it is pushed.
41
+ *
42
+ * ```tsx
43
+ * let out!: HTMLDivElement
44
+ * const stream = createStreamReveal(() => out, { unit: "chars" })
45
+ * const reader = response.body.getReader()
46
+ * onMount(async () => {
47
+ * for (;;) {
48
+ * const { done, value } = await reader.read()
49
+ * if (done) break
50
+ * stream.push(decoder.decode(value))
51
+ * }
52
+ * stream.complete()
53
+ * })
54
+ * <div ref={out} aria-live="polite" />
55
+ * ```
56
+ */
57
+ export function createStreamReveal(ref, options = {}) {
58
+ const { unit = "chars", batchMs = 120, maxBatch = 24, duration = 450, stagger = 30, easing: easingOpt = "easeOutExpo", } = options;
59
+ const from = {
60
+ y: options.from?.y ?? 28,
61
+ blur: options.from?.blur ?? 10,
62
+ scale: options.from?.scale ?? 0.85,
63
+ opacity: options.from?.opacity ?? 0,
64
+ rotate: options.from?.rotate ?? 0,
65
+ };
66
+ const easing = resolveEasing(easingOpt);
67
+ const server = typeof window === "undefined";
68
+ const [status, setStatus] = createSignal(server ? "done" : "idle");
69
+ const [pending, setPending] = createSignal(0);
70
+ let queue = "";
71
+ let units = [];
72
+ let unitIndex = 0;
73
+ let cancel = null;
74
+ let lastFlush = 0;
75
+ let settling = false;
76
+ const applyEntrance = (el, e) => {
77
+ const t = 1 - e;
78
+ el.style.transform =
79
+ `translateY(${fmt(from.y * t)}px) scale(${fmt(from.scale + (1 - from.scale) * e)}) rotate(${fmt(from.rotate * t)}deg)`;
80
+ const blur = from.blur * t;
81
+ el.style.filter = blur > 0.05 ? `blur(${fmt(blur)}px)` : "none";
82
+ el.style.opacity = fmt(from.opacity + (1 - from.opacity) * e);
83
+ };
84
+ const flush = (t) => {
85
+ const el = ref();
86
+ if (!el || queue.length === 0) {
87
+ queue = "";
88
+ setPending(0);
89
+ return;
90
+ }
91
+ const batch = queue;
92
+ queue = "";
93
+ setPending(0);
94
+ for (const span of appendUnits(el, batch, unit)) {
95
+ units.push({ el: span, startAt: t + unitIndex * stagger });
96
+ unitIndex++;
97
+ }
98
+ };
99
+ const tick = (t) => {
100
+ if (queue.length > 0 &&
101
+ (queue.length >= maxBatch || t - lastFlush >= batchMs)) {
102
+ flush(t);
103
+ lastFlush = t;
104
+ }
105
+ let alive = false;
106
+ units = units.filter((u) => {
107
+ const local = clamp01((t - u.startAt) / duration);
108
+ applyEntrance(u.el, easing(local));
109
+ if (local < 1)
110
+ alive = true;
111
+ return local < 1;
112
+ });
113
+ if (queue.length > 0)
114
+ alive = true;
115
+ if (!alive) {
116
+ cancel = null;
117
+ if (settling) {
118
+ settling = false;
119
+ setStatus("done");
120
+ }
121
+ }
122
+ return alive;
123
+ };
124
+ const ensureLoop = () => {
125
+ if (cancel || server)
126
+ return;
127
+ lastFlush = now();
128
+ cancel = schedule(tick);
129
+ };
130
+ const push = (chunk) => {
131
+ if (server || chunk.length === 0)
132
+ return;
133
+ if (prefersReducedMotion()) {
134
+ // Accessibility: skip batching and entrances, show text at once.
135
+ const el = ref();
136
+ if (el) {
137
+ for (const span of appendUnits(el, chunk, unit)) {
138
+ void span;
139
+ }
140
+ }
141
+ setPending(0);
142
+ setStatus("streaming");
143
+ return;
144
+ }
145
+ queue += chunk;
146
+ setPending(queue.length);
147
+ setStatus("streaming");
148
+ ensureLoop();
149
+ };
150
+ const complete = () => {
151
+ if (server) {
152
+ setStatus("done");
153
+ return;
154
+ }
155
+ if (prefersReducedMotion()) {
156
+ queue = "";
157
+ setPending(0);
158
+ setStatus("done");
159
+ return;
160
+ }
161
+ flush(now());
162
+ if (units.length === 0 && queue.length === 0) {
163
+ setStatus("done");
164
+ return;
165
+ }
166
+ settling = true;
167
+ ensureLoop();
168
+ };
169
+ const reset = () => {
170
+ cancel?.();
171
+ cancel = null;
172
+ queue = "";
173
+ units = [];
174
+ unitIndex = 0;
175
+ settling = false;
176
+ setPending(0);
177
+ if (!server) {
178
+ const el = ref();
179
+ el?.removeAttribute?.("aria-label");
180
+ if (el)
181
+ el.textContent = "";
182
+ setStatus("idle");
183
+ }
184
+ };
185
+ onCleanup(() => {
186
+ cancel?.();
187
+ cancel = null;
188
+ });
189
+ return { push, complete, reset, status, pending };
190
+ }
191
+ /**
192
+ * A pure-signal state machine for agent UI.
193
+ *
194
+ * Motion is deliberately not built in: pair each state with a recipe
195
+ * instead, so the machine stays transparent, testable, and SSR-safe.
196
+ * A typical pairing is `createWobble` on typing dots while "thinking",
197
+ * `createStreamReveal` while "streaming", `createTransition` for the
198
+ * "tool-call" overlay, and `createColorShift` on the status pill for
199
+ * "done"/"error". Each of those degrades on its own under reduced
200
+ * motion.
201
+ *
202
+ * SSR-safe by construction: signals only, no DOM, no clock.
203
+ *
204
+ * ```ts
205
+ * const agent = createAgentState()
206
+ * agent.set("thinking")
207
+ * agent.state() // "thinking"
208
+ * agent.is("streaming") // false
209
+ * ```
210
+ */
211
+ export function createAgentState(options = {}) {
212
+ const { initial = "idle", allowed, onEnter, onExit } = options;
213
+ const [state, setState] = createSignal(initial);
214
+ const [prev, setPrev] = createSignal(initial);
215
+ const legal = (from, to) => {
216
+ if (!allowed)
217
+ return true;
218
+ return allowed.some((t) => t.from === from && t.to === to);
219
+ };
220
+ const move = (next) => {
221
+ const current = state();
222
+ if (next === current || !legal(current, next))
223
+ return;
224
+ onExit?.(current, next);
225
+ setPrev(current);
226
+ setState(next);
227
+ onEnter?.(next, current);
228
+ };
229
+ const reset = () => {
230
+ move(initial);
231
+ };
232
+ return {
233
+ state,
234
+ prev,
235
+ set: move,
236
+ reset,
237
+ is: (s) => state() === s,
238
+ };
239
+ }
240
+ /**
241
+ * Thrown by `parseDriftSpec`. `path` pinpoints the invalid value
242
+ * (for example "scenes[2].options.duration") so a generator can
243
+ * repair the spec without guessing.
244
+ */
245
+ export class DriftSpecError extends Error {
246
+ constructor(path, message) {
247
+ super(path
248
+ ? `Invalid drift spec at "${path}": ${message}`
249
+ : `Invalid drift spec: ${message}`);
250
+ this.name = "DriftSpecError";
251
+ this.path = path;
252
+ }
253
+ }
254
+ const PRIMITIVES = [
255
+ "kineticType",
256
+ "camera",
257
+ "colorShift",
258
+ "transition",
259
+ "beat",
260
+ "streamReveal",
261
+ ];
262
+ /** Primitives that render into an element and need a target key. */
263
+ const DOM_PRIMITIVES = [
264
+ "kineticType",
265
+ "streamReveal",
266
+ ];
267
+ function isRecord(v) {
268
+ return typeof v === "object" && v !== null && !Array.isArray(v);
269
+ }
270
+ function assertPositiveNumber(value, path) {
271
+ if (typeof value !== "number" || !(value > 0)) {
272
+ throw new DriftSpecError(path, "expected a positive number");
273
+ }
274
+ }
275
+ function checkOptions(primitive, options, base) {
276
+ const at = (key) => `${base}.options.${key}`;
277
+ switch (primitive) {
278
+ case "kineticType":
279
+ if (options.duration !== undefined) {
280
+ assertPositiveNumber(options.duration, at("duration"));
281
+ }
282
+ if (options.stagger !== undefined) {
283
+ assertPositiveNumber(options.stagger, at("stagger"));
284
+ }
285
+ break;
286
+ case "camera":
287
+ if (options.keyframes !== undefined &&
288
+ !Array.isArray(options.keyframes)) {
289
+ throw new DriftSpecError(at("keyframes"), "expected an array");
290
+ }
291
+ break;
292
+ case "colorShift":
293
+ if (options.stops !== undefined && !Array.isArray(options.stops)) {
294
+ throw new DriftSpecError(at("stops"), "expected an array");
295
+ }
296
+ if (options.duration !== undefined) {
297
+ assertPositiveNumber(options.duration, at("duration"));
298
+ }
299
+ break;
300
+ case "transition":
301
+ if (options.type !== undefined &&
302
+ !["cut", "fade", "slide", "wipe"].includes(options.type)) {
303
+ throw new DriftSpecError(at("type"), 'expected one of "cut", "fade", "slide", "wipe"');
304
+ }
305
+ if (options.duration !== undefined) {
306
+ assertPositiveNumber(options.duration, at("duration"));
307
+ }
308
+ break;
309
+ case "beat":
310
+ if (options.bpm !== undefined) {
311
+ assertPositiveNumber(options.bpm, at("bpm"));
312
+ }
313
+ break;
314
+ case "streamReveal":
315
+ if (options.text !== undefined && typeof options.text !== "string") {
316
+ throw new DriftSpecError(at("text"), "expected a string");
317
+ }
318
+ break;
319
+ }
320
+ }
321
+ /**
322
+ * Validate unknown input against the DriftSpec schema and return the
323
+ * typed spec. Throws `DriftSpecError` with a precise `path` on the
324
+ * first invalid value.
325
+ *
326
+ * Pure validation, no DOM: safe to run on the server.
327
+ *
328
+ * ```ts
329
+ * const spec = parseDriftSpec(JSON.parse(raw))
330
+ * const player = createSpecPlayer(spec, { title: () => titleEl })
331
+ * await player.play()
332
+ * ```
333
+ */
334
+ export function parseDriftSpec(input) {
335
+ if (!isRecord(input)) {
336
+ throw new DriftSpecError("", "expected an object");
337
+ }
338
+ if (input.version !== 1) {
339
+ throw new DriftSpecError("version", "expected 1");
340
+ }
341
+ if (!Array.isArray(input.scenes) || input.scenes.length === 0) {
342
+ throw new DriftSpecError("scenes", "expected a non-empty array");
343
+ }
344
+ const scenes = input.scenes.map((raw, i) => {
345
+ const base = `scenes[${i}]`;
346
+ if (!isRecord(raw)) {
347
+ throw new DriftSpecError(base, "expected an object");
348
+ }
349
+ if (typeof raw.primitive !== "string" ||
350
+ !PRIMITIVES.includes(raw.primitive)) {
351
+ throw new DriftSpecError(`${base}.primitive`, `expected one of ${PRIMITIVES.join(", ")}`);
352
+ }
353
+ const primitive = raw.primitive;
354
+ let target;
355
+ if (raw.target !== undefined) {
356
+ if (typeof raw.target !== "string" || raw.target.length === 0) {
357
+ throw new DriftSpecError(`${base}.target`, "expected a non-empty string key");
358
+ }
359
+ target = raw.target;
360
+ }
361
+ else if (DOM_PRIMITIVES.includes(primitive)) {
362
+ throw new DriftSpecError(`${base}.target`, `"${primitive}" needs a target key into the refs record`);
363
+ }
364
+ let specOptions;
365
+ if (raw.options !== undefined) {
366
+ if (!isRecord(raw.options)) {
367
+ throw new DriftSpecError(`${base}.options`, "expected an object");
368
+ }
369
+ specOptions = raw.options;
370
+ checkOptions(primitive, specOptions, base);
371
+ }
372
+ let duration;
373
+ if (raw.duration !== undefined) {
374
+ assertPositiveNumber(raw.duration, `${base}.duration`);
375
+ duration = raw.duration;
376
+ }
377
+ return { primitive, target, options: specOptions, duration };
378
+ });
379
+ return { version: 1, scenes };
380
+ }
381
+ const noopHandle = () => ({
382
+ promise: Promise.resolve(),
383
+ stop: () => { },
384
+ });
385
+ /** Resolve when `isDone()` turns true, on the shared clock. */
386
+ function whenDone(isDone) {
387
+ let cancel = null;
388
+ let resolveFn;
389
+ const promise = new Promise((resolve) => {
390
+ resolveFn = resolve;
391
+ if (isDone()) {
392
+ resolve();
393
+ return;
394
+ }
395
+ cancel = schedule(() => {
396
+ if (isDone()) {
397
+ resolve();
398
+ return false;
399
+ }
400
+ return true;
401
+ });
402
+ });
403
+ return {
404
+ promise,
405
+ stop: () => {
406
+ cancel?.();
407
+ cancel = null;
408
+ resolveFn();
409
+ },
410
+ };
411
+ }
412
+ const stepPlayers = {
413
+ kineticType: (target, options) => {
414
+ const c = createKineticType(target ?? (() => null), options);
415
+ return { promise: c.play(), stop: c.stop };
416
+ },
417
+ streamReveal: (target, options) => {
418
+ const c = createStreamReveal(target ?? (() => null), options);
419
+ const text = typeof options.text === "string" ? options.text : "";
420
+ c.push(text);
421
+ c.complete();
422
+ return whenDone(() => c.status() === "done");
423
+ },
424
+ camera: (target, options, budget) => {
425
+ const el = target?.();
426
+ const keyframes = options.keyframes ?? [];
427
+ if (!el || keyframes.length === 0)
428
+ return noopHandle();
429
+ const [p, setP] = createSignal(0);
430
+ const cam = createCamera(keyframes, { progress: p });
431
+ const ctl = animate(0, 1, {
432
+ duration: Math.max(budget ?? 1200, 1),
433
+ easing: "linear",
434
+ onUpdate: (v) => {
435
+ setP(v);
436
+ el.style.transform = cam();
437
+ },
438
+ });
439
+ return { promise: ctl.finished, stop: ctl.stop };
440
+ },
441
+ colorShift: (target, options, budget) => {
442
+ const el = target?.();
443
+ const stops = options.stops ?? [];
444
+ if (!el || stops.length === 0)
445
+ return noopHandle();
446
+ const c = createColorShift(stops, {
447
+ ...options,
448
+ duration: Math.max(budget ?? 1200, 1),
449
+ });
450
+ const playPromise = c.play();
451
+ const apply = schedule(() => {
452
+ el.style.color = c.color();
453
+ return c.status() === "running";
454
+ });
455
+ const settle = () => {
456
+ apply();
457
+ el.style.color = c.color();
458
+ };
459
+ return {
460
+ promise: playPromise.then(settle),
461
+ stop: () => {
462
+ c.stop();
463
+ settle();
464
+ },
465
+ };
466
+ },
467
+ transition: (_target, options) => {
468
+ const c = createTransition(options);
469
+ return { promise: c.play(), stop: c.stop };
470
+ },
471
+ beat: (_target, options, budget) => {
472
+ const c = createBeat(options);
473
+ c.start();
474
+ const timer = animate(0, 1, { duration: Math.max(budget ?? 2000, 1) });
475
+ const stopAll = () => {
476
+ timer.stop();
477
+ c.stop();
478
+ };
479
+ return { promise: timer.finished.then(() => c.stop()), stop: stopAll };
480
+ },
481
+ };
482
+ /**
483
+ * Render a validated DriftSpec: each scene's primitive plays in
484
+ * order against the element refs the host supplies. `duration` on a
485
+ * step caps that step's budget.
486
+ *
487
+ * SSR-safe: `play()` is a no-op on the server. Under reduced motion
488
+ * `play()` jumps straight to the last scene (the clean final frame),
489
+ * matching `createScenePlayer`.
490
+ *
491
+ * ```ts
492
+ * const player = createSpecPlayer(spec, {
493
+ * title: () => titleEl,
494
+ * body: () => bodyEl,
495
+ * })
496
+ * player.scene() // 0, 1, ... as the spec plays
497
+ * await player.play()
498
+ * ```
499
+ */
500
+ export function createSpecPlayer(spec, refs) {
501
+ const [status, setStatus] = createSignal("idle");
502
+ const [scene, setScene] = createSignal(-1);
503
+ let currentStop = null;
504
+ let resolvePlay = null;
505
+ let stopped = false;
506
+ const runStep = async (step) => {
507
+ const target = step.target === undefined ? undefined : refs[step.target];
508
+ const handle = stepPlayers[step.primitive](target, step.options ?? {}, step.duration);
509
+ currentStop = handle.stop;
510
+ if (step.duration === undefined) {
511
+ await handle.promise;
512
+ }
513
+ else {
514
+ // The duration is a budget: the step ends when the primitive
515
+ // finishes or the budget runs out, whichever comes first.
516
+ const timer = animate(0, 1, { duration: Math.max(step.duration, 0.001) });
517
+ const stopStep = currentStop;
518
+ currentStop = () => {
519
+ stopStep();
520
+ timer.stop();
521
+ };
522
+ await Promise.race([handle.promise, timer.finished]);
523
+ timer.stop();
524
+ }
525
+ currentStop = null;
526
+ };
527
+ const play = () => {
528
+ if (spec.scenes.length === 0) {
529
+ setScene(-1);
530
+ setStatus("done");
531
+ return Promise.resolve();
532
+ }
533
+ if (prefersReducedMotion() || typeof window === "undefined") {
534
+ setScene(spec.scenes.length - 1);
535
+ setStatus("done");
536
+ return Promise.resolve();
537
+ }
538
+ // A superseded play() must not leave its caller hanging.
539
+ resolvePlay?.();
540
+ resolvePlay = null;
541
+ stopped = false;
542
+ setStatus("running");
543
+ return new Promise((resolve) => {
544
+ resolvePlay = resolve;
545
+ void (async () => {
546
+ for (let i = 0; i < spec.scenes.length; i++) {
547
+ if (stopped)
548
+ break;
549
+ setScene(i);
550
+ await runStep(spec.scenes[i]);
551
+ }
552
+ currentStop = null;
553
+ const done = resolvePlay;
554
+ resolvePlay = null;
555
+ setStatus(stopped ? "idle" : "done");
556
+ done?.();
557
+ })();
558
+ });
559
+ };
560
+ const stop = () => {
561
+ if (status() !== "running")
562
+ return;
563
+ stopped = true;
564
+ currentStop?.();
565
+ currentStop = null;
566
+ // The async loop observes `stopped`, breaks, and resolves play().
567
+ };
568
+ onCleanup(stop);
569
+ return { play, stop, status, scene };
570
+ }
package/dist/animate.d.ts CHANGED
@@ -25,8 +25,8 @@ export interface AnimationControls {
25
25
  * duration: 500,
26
26
  * easing: "easeOutExpo",
27
27
  * onUpdate: (v) => el.style.opacity = String(v / 100),
28
- * });
29
- * await ctl.finished;
28
+ * })
29
+ * await ctl.finished
30
30
  * ```
31
31
  */
32
32
  export declare function animate(from: number, to: number, options?: AnimateOptions): AnimationControls;
package/dist/animate.js CHANGED
@@ -9,8 +9,8 @@ import { prefersReducedMotion } from "./reduced-motion.js";
9
9
  * duration: 500,
10
10
  * easing: "easeOutExpo",
11
11
  * onUpdate: (v) => el.style.opacity = String(v / 100),
12
- * });
13
- * await ctl.finished;
12
+ * })
13
+ * await ctl.finished
14
14
  * ```
15
15
  */
16
16
  export function animate(from, to, options = {}) {