phase 0.0.7 → 0.0.9
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 +312 -49
- package/dist/{scroll-DZvWyPQE.js → debounce-BD9K4TW4.js} +205 -6
- package/dist/debounce-BD9K4TW4.js.map +1 -0
- package/dist/{index-BRyL_Inx.d.ts → index-C10bmjm1.d.ts} +45 -2
- package/dist/index-C10bmjm1.d.ts.map +1 -0
- package/dist/index.d.ts +34 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/react.d.ts +70 -4
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +94 -4
- package/dist/react.js.map +1 -1
- package/package.json +4 -4
- package/dist/index-BRyL_Inx.d.ts.map +0 -1
- package/dist/scroll-DZvWyPQE.js.map +0 -1
package/README.md
CHANGED
|
@@ -35,6 +35,13 @@ Each guarantee is a [tested invariant](#guarantees), not an aspiration. Every ex
|
|
|
35
35
|
- [createLifecycle](#createlifecycle)
|
|
36
36
|
- [createScrollProgress](#createscrollprogress)
|
|
37
37
|
- [createScroll](#createscroll)
|
|
38
|
+
- [createThrottle](#createthrottle)
|
|
39
|
+
- [createDebounce](#createdebounce)
|
|
40
|
+
- [createRenderState](#createrenderstate)
|
|
41
|
+
- [createDevicePixelRatio](#createdevicepixelratio)
|
|
42
|
+
- [createMutation](#createmutation)
|
|
43
|
+
- [createPointer](#createpointer)
|
|
44
|
+
- [whenIdle](#whenidle)
|
|
38
45
|
- [prefersReducedMotion](#prefersreducedmotion)
|
|
39
46
|
- [Easing and math](#easing-and-math)
|
|
40
47
|
- [Choosing a primitive](#choosing-a-primitive)
|
|
@@ -46,7 +53,11 @@ Each guarantee is a [tested invariant](#guarantees), not an aspiration. Every ex
|
|
|
46
53
|
- [usePresence](#usepresence)
|
|
47
54
|
- [useScrollProgress](#usescrollprogress)
|
|
48
55
|
- [useScroll](#usescroll)
|
|
49
|
-
- [
|
|
56
|
+
- [useThrottledCallback](#usethrottledcallback)
|
|
57
|
+
- [useDebouncedCallback](#usedebouncedcallback)
|
|
58
|
+
- [useMutation](#usemutation)
|
|
59
|
+
- [usePointer](#usepointer)
|
|
60
|
+
- [Observation and utility hooks](#observation-and-utility-hooks)
|
|
50
61
|
- [React components](#react-components)
|
|
51
62
|
- [How animations work](#how-animations-work)
|
|
52
63
|
- [Presence](#presence)
|
|
@@ -54,7 +65,8 @@ Each guarantee is a [tested invariant](#guarantees), not an aspiration. Every ex
|
|
|
54
65
|
- [Swap](#swap)
|
|
55
66
|
- [Rendering](#rendering)
|
|
56
67
|
- [Defer](#defer)
|
|
57
|
-
- [WhenIdle](#whenidle)
|
|
68
|
+
- [WhenIdle](#whenidle-1)
|
|
69
|
+
- [useIdle](#useidle)
|
|
58
70
|
- [useWhenIdle](#usewhenidle)
|
|
59
71
|
- [useRenderState](#userenderstate)
|
|
60
72
|
- [Guarantees](#guarantees)
|
|
@@ -146,11 +158,11 @@ If a gap fails any criterion, phase closes it in the [skill](#agent-skill) (audi
|
|
|
146
158
|
|
|
147
159
|
## Entry points
|
|
148
160
|
|
|
149
|
-
| Import | Contents
|
|
150
|
-
| ------------- |
|
|
151
|
-
| `phase` |
|
|
152
|
-
| `phase/ease` | Easing functions and math utilities only
|
|
153
|
-
| `phase/react` | React hooks and components
|
|
161
|
+
| Import | Contents |
|
|
162
|
+
| ------------- | ------------------------------------------------------------------------------- |
|
|
163
|
+
| `phase` | Framework-agnostic timing, observation, lifecycle, scheduling, math, and errors |
|
|
164
|
+
| `phase/ease` | Easing functions and math utilities only |
|
|
165
|
+
| `phase/react` | React hooks and components |
|
|
154
166
|
|
|
155
167
|
Each entry point is independently tree-shakeable. Importing `phase/ease` in a server component pulls zero browser APIs.
|
|
156
168
|
|
|
@@ -398,6 +410,157 @@ scroll.stop();
|
|
|
398
410
|
|
|
399
411
|
The options type is `CreateScrollOptions` (`ScrollOptions` is a `lib.dom` global and must not be shadowed).
|
|
400
412
|
|
|
413
|
+
### createThrottle
|
|
414
|
+
|
|
415
|
+
Frame-aligned, visibility-aware throttle for event-driven work below frame rate (socket emits, worker messaging, expensive recompute). Leading calls fire synchronously; a pending trailing call fires with the latest value on the first animation frame at or past `interval`. Nothing is scheduled while the trigger is idle or the document is hidden.
|
|
416
|
+
|
|
417
|
+
> **Event-driven, not a loop.** This fires on the trigger and idles otherwise. To cap a continuous render loop, use `fps` on [`createLoop`](#createloop). To think in rates, `interval: 1000 / 20` reads as "at most 20 per second".
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import { createThrottle } from 'phase';
|
|
421
|
+
|
|
422
|
+
const throttle = createThrottle({
|
|
423
|
+
callback: (state) => socket.emit('cursor', state.x, state.y),
|
|
424
|
+
interval: 50,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const pointer = createPointer({ element, onPointer: throttle.call });
|
|
428
|
+
|
|
429
|
+
// throttle.flush() fires a pending trailing call now
|
|
430
|
+
// throttle.cancel() discards it and resets the window
|
|
431
|
+
// cleanup:
|
|
432
|
+
throttle.stop();
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
When the document hides, a pending call is flushed with the latest value (default) or dropped per `hidden`. Calls made while hidden are recorded but fire nothing until the document is visible again.
|
|
436
|
+
|
|
437
|
+
#### Throttle options
|
|
438
|
+
|
|
439
|
+
| Option | Type | Default | Description |
|
|
440
|
+
| ---------- | ----------------------------------- | --------- | --------------------------------------------- |
|
|
441
|
+
| `callback` | `(value: T) => void` | required | Called with the latest value passed to `call` |
|
|
442
|
+
| `interval` | `number` | required | Minimum ms between invocations |
|
|
443
|
+
| `edge` | `'leading' \| 'trailing' \| 'both'` | `'both'` | Which edges fire |
|
|
444
|
+
| `hidden` | `'flush' \| 'drop'` | `'flush'` | Pending-call policy when the document hides |
|
|
445
|
+
| `signal` | `AbortSignal` | — | Stops the throttle when aborted |
|
|
446
|
+
|
|
447
|
+
### createDebounce
|
|
448
|
+
|
|
449
|
+
Visibility-aware trailing debounce: fires the callback with the latest value once `wait` ms pass without a new call. No timer runs while the document is hidden; the quiet period restarts on return. Use it for work that should wait out a burst, like reallocating canvas buffers after a resize stream settles.
|
|
450
|
+
|
|
451
|
+
```ts
|
|
452
|
+
import { createDebounce } from 'phase';
|
|
453
|
+
|
|
454
|
+
const debounce = createDebounce({
|
|
455
|
+
callback: (size) => reallocateBuffers(size),
|
|
456
|
+
wait: 250,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
debounce.call({ width, height });
|
|
460
|
+
|
|
461
|
+
// cleanup:
|
|
462
|
+
debounce.stop();
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Same surface as `createThrottle`: `flush()`, `cancel()`, a synchronous `pending` read, and terminal `stop()`.
|
|
466
|
+
|
|
467
|
+
#### Debounce options
|
|
468
|
+
|
|
469
|
+
| Option | Type | Default | Description |
|
|
470
|
+
| ---------- | -------------------- | --------- | --------------------------------------------- |
|
|
471
|
+
| `callback` | `(value: T) => void` | required | Called with the latest value passed to `call` |
|
|
472
|
+
| `wait` | `number` | required | Quiet period in ms; each call restarts it |
|
|
473
|
+
| `hidden` | `'flush' \| 'drop'` | `'flush'` | Pending-call policy when the document hides |
|
|
474
|
+
| `signal` | `AbortSignal` | — | Stops the debounce when aborted |
|
|
475
|
+
|
|
476
|
+
### createRenderState
|
|
477
|
+
|
|
478
|
+
Reports whether the browser is rendering an element or skipping it under `content-visibility: auto`. Use it to pause raw work inside deferred content; `phase` loops already pause themselves.
|
|
479
|
+
|
|
480
|
+
```ts
|
|
481
|
+
import { createRenderState } from 'phase';
|
|
482
|
+
|
|
483
|
+
const renderState = createRenderState({
|
|
484
|
+
element: el,
|
|
485
|
+
onPhaseChange: (phase) => {
|
|
486
|
+
if (phase === 'skipped') clock.pause();
|
|
487
|
+
else clock.resume();
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
renderState.stop();
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
It listens to `contentvisibilityautostatechange`, the browser's actual paint decision, without changing layout.
|
|
495
|
+
|
|
496
|
+
### createDevicePixelRatio
|
|
497
|
+
|
|
498
|
+
Tracks `devicePixelRatio` changes through a shared media-query subscription. Use it for framework-free canvas, WebGL, or worker renderers that own their buffer sizing.
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
import { createDevicePixelRatio } from 'phase';
|
|
502
|
+
|
|
503
|
+
const dpr = createDevicePixelRatio({
|
|
504
|
+
onChange: (value) => renderer.setPixelRatio(Math.min(value, 2)),
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// dpr.dpr is always current
|
|
508
|
+
dpr.stop();
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
`useCanvas` handles DPR automatically; use this primitive only when you own the renderer.
|
|
512
|
+
|
|
513
|
+
### createMutation
|
|
514
|
+
|
|
515
|
+
A lifecycle-aware `MutationObserver`: records are coalesced into one callback per animation frame, observation pauses off-screen by default, and teardown is explicit.
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
import { createMutation } from 'phase';
|
|
519
|
+
|
|
520
|
+
const mutation = createMutation({
|
|
521
|
+
element: list,
|
|
522
|
+
mutation: { childList: true },
|
|
523
|
+
onMutations: (records) => syncItems(records),
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
mutation.stop();
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Reserve it for structural or narrow attribute changes. For dimensions, use ResizeObserver-backed `useSize`; reading layout inside `onMutations` still forces a reflow.
|
|
530
|
+
|
|
531
|
+
### createPointer
|
|
532
|
+
|
|
533
|
+
Tracks pointer position relative to an element, batching high-frequency events into one callback and one bounds read per animation frame. It pauses when the element is off-screen.
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
import { createPointer } from 'phase';
|
|
537
|
+
|
|
538
|
+
const pointer = createPointer({
|
|
539
|
+
element: surface,
|
|
540
|
+
onPointer: (state) => {
|
|
541
|
+
cursor.style.transform = `translate(${state.x}px, ${state.y}px)`;
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// pointer.state is always current
|
|
546
|
+
pointer.stop();
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Use CSS `:hover` for hover state and a gesture library for drag physics. This primitive is for continuous element-relative coordinates.
|
|
550
|
+
|
|
551
|
+
### whenIdle
|
|
552
|
+
|
|
553
|
+
Runs one callback when the browser is idle, with a timeout fallback for browsers without `requestIdleCallback`. The returned function cancels pending work.
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
import { whenIdle } from 'phase';
|
|
557
|
+
|
|
558
|
+
const cancel = whenIdle(() => warmCache(), { timeout: 2000 });
|
|
559
|
+
cancel();
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
In React, use `useWhenIdle` for effects, `useIdle` for a boolean, or `WhenIdle` to mount a subtree.
|
|
563
|
+
|
|
401
564
|
### prefersReducedMotion
|
|
402
565
|
|
|
403
566
|
Returns `true` when reduced motion is enabled at the OS level. Use it to gate expensive setup or dynamic imports.
|
|
@@ -455,19 +618,25 @@ Easing, interpolation, and your value range are three separate concerns. `phase`
|
|
|
455
618
|
|
|
456
619
|
## Choosing a primitive
|
|
457
620
|
|
|
458
|
-
| Need
|
|
459
|
-
|
|
|
460
|
-
| Check on-screen visibility
|
|
461
|
-
| Run a frame loop via `phase`
|
|
462
|
-
| Pause/resume your own loop (WebGL, three.js, Web Worker)
|
|
463
|
-
| Animate a single value in render output
|
|
464
|
-
| Animate mount/unmount transitions
|
|
465
|
-
| Skip painting off-screen content (keep in DOM)
|
|
466
|
-
| Defer non-critical UI until the browser is idle
|
|
467
|
-
| Run a side effect or prefetch when idle
|
|
468
|
-
| Pause non-`phase` work inside a `Defer` subtree
|
|
469
|
-
|
|
|
470
|
-
|
|
|
621
|
+
| Need | Use |
|
|
622
|
+
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
623
|
+
| Check on-screen visibility | `useSight` (visibility only) |
|
|
624
|
+
| Run a frame loop via `phase` | `useLoop` (DOM) / `useCanvas` (canvas) |
|
|
625
|
+
| Pause/resume your own loop (WebGL, three.js, Web Worker) | `useLifecycle` (active/paused signal) |
|
|
626
|
+
| Animate a single value in render output | `useTween` |
|
|
627
|
+
| Animate mount/unmount transitions | `Presence` / `Swap` / `WhenVisible` |
|
|
628
|
+
| Skip painting off-screen content (keep in DOM) | `Defer` |
|
|
629
|
+
| Defer non-critical UI until the browser is idle | `WhenIdle` / `useIdle` |
|
|
630
|
+
| Run a side effect or prefetch when idle | `useWhenIdle` |
|
|
631
|
+
| Pause non-`phase` work inside a `Defer` subtree | `useRenderState` |
|
|
632
|
+
| React to DOM mutations without synchronous callback storms | `useMutation` |
|
|
633
|
+
| Track element-relative pointer position without per-event layout reads | `usePointer` |
|
|
634
|
+
| Track DPR for a renderer you own | `useDevicePixelRatio` |
|
|
635
|
+
| Check reduced motion for non-`phase` work | `usePrefersReducedMotion` |
|
|
636
|
+
| Subscribe to scroll, size, or media values reactively | `useScrollProgress` / `useSize` / `useContainerQuery` / `useMediaQuery` |
|
|
637
|
+
| Scroll/size/visibility without re-renders? | Same hooks with a callback (`onProgress` / `onResize` / `onVisibilityChange`), read via ref |
|
|
638
|
+
| Rate-limit event-driven work (sockets, workers) | `useThrottledCallback` |
|
|
639
|
+
| Run once after a burst settles (resize, typing) | `useDebouncedCallback` |
|
|
471
640
|
|
|
472
641
|
**`useSight` vs `useLifecycle`:** `useSight` reports pure visibility (for lazy-mounting, analytics, `WhenVisible`). `useLifecycle` folds in reduced motion and a manual pause, so you can't accidentally animate for users who asked not to. If you're gating an animation, use `useLifecycle`. If you're gating content, use `useSight`.
|
|
473
642
|
|
|
@@ -668,17 +837,94 @@ function Carousel({ children }) {
|
|
|
668
837
|
|
|
669
838
|
Scrolling writes to the DOM directly with zero re-renders. Read the latest position on demand from `stateRef.current` (e.g. inside a `useLoop` tick), and call `measure()` after changing scrollable content.
|
|
670
839
|
|
|
671
|
-
###
|
|
840
|
+
### useThrottledCallback
|
|
841
|
+
|
|
842
|
+
Wraps `createThrottle` with React lifecycle management. Returns a stable-identity throttled function (with `flush()` and `cancel()` attached) that drops directly into any callback slot and always invokes the latest `callback`.
|
|
843
|
+
|
|
844
|
+
```tsx
|
|
845
|
+
import { usePointer, useThrottledCallback } from 'phase/react';
|
|
846
|
+
|
|
847
|
+
function LiveCursor() {
|
|
848
|
+
const emit = useThrottledCallback(
|
|
849
|
+
(s: PointerState) => socket.emit('cursor', { x: s.x, y: s.y }),
|
|
850
|
+
{ interval: 50 },
|
|
851
|
+
);
|
|
852
|
+
const { ref } = usePointer({ onPointer: emit });
|
|
853
|
+
return <div ref={ref} />;
|
|
854
|
+
}
|
|
855
|
+
```
|
|
672
856
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
857
|
+
Unmount and option changes discard a pending trailing call. When the final value must land, flush in your own cleanup: `useEffect(() => () => emit.flush(), [emit])`.
|
|
858
|
+
|
|
859
|
+
### useDebouncedCallback
|
|
860
|
+
|
|
861
|
+
Wraps `createDebounce` with React lifecycle management. Same shape as `useThrottledCallback`, but fires once `wait` ms pass without a new call.
|
|
862
|
+
|
|
863
|
+
```tsx
|
|
864
|
+
import { useSize, useDebouncedCallback } from 'phase/react';
|
|
865
|
+
|
|
866
|
+
function SimulationCanvas() {
|
|
867
|
+
const realloc = useDebouncedCallback(
|
|
868
|
+
(size: Size) => reallocateBuffers(size),
|
|
869
|
+
{ wait: 250 },
|
|
870
|
+
);
|
|
871
|
+
const { ref } = useSize({ onResize: realloc });
|
|
872
|
+
return <canvas ref={ref} />;
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
### useMutation
|
|
877
|
+
|
|
878
|
+
Wraps `createMutation` with ref management and automatic teardown. Mutation records stay imperative—delivered once per animation frame—while only infrequent `observing` / `paused` phase changes re-render.
|
|
879
|
+
|
|
880
|
+
```tsx
|
|
881
|
+
import { useMutation } from 'phase/react';
|
|
882
|
+
|
|
883
|
+
const { ref, phase } = useMutation({
|
|
884
|
+
mutation: { childList: true },
|
|
885
|
+
onMutations: (records) => syncItems(records),
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
return <ul ref={ref} data-observer-phase={phase} />;
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
Observation pauses off-screen by default. Set `visibility: 'ignore'` only for document-level coordination that must continue in the background.
|
|
892
|
+
|
|
893
|
+
### usePointer
|
|
894
|
+
|
|
895
|
+
Element-relative pointer tracking without per-event layout reads or per-frame React state. Position is delivered through `onPointer` and mirrored in `stateRef`; only enter/leave phase changes re-render.
|
|
896
|
+
|
|
897
|
+
```tsx
|
|
898
|
+
import { usePointer } from 'phase/react';
|
|
899
|
+
|
|
900
|
+
const { ref } = usePointer({
|
|
901
|
+
onPointer: ({ x, y, active }) => {
|
|
902
|
+
cursorRef.current?.style.setProperty(
|
|
903
|
+
'transform',
|
|
904
|
+
`translate(${x}px, ${y}px)`,
|
|
905
|
+
);
|
|
906
|
+
cursorRef.current?.toggleAttribute('data-active', active);
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
return <div ref={ref}>{children}</div>;
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
Use it for custom cursors, canvas interaction, and tooltips—not simple hover or drag gestures.
|
|
914
|
+
|
|
915
|
+
### Observation and utility hooks
|
|
916
|
+
|
|
917
|
+
| Hook | Purpose |
|
|
918
|
+
| ------------------------- | ------------------------------------------------------------------------------------- |
|
|
919
|
+
| `useSight` | Element visibility as a phase. Pass `onVisibilityChange` for zero-re-render mode |
|
|
920
|
+
| `useSize` | Element dimensions via shared ResizeObserver. Pass `onResize` for zero-re-render mode |
|
|
921
|
+
| `useContainerQuery` | Breakpoint matching against element width |
|
|
922
|
+
| `useScrollProgress` | Element visibility ratio (0–1). Pass `onProgress` for zero-re-render mode |
|
|
923
|
+
| `useMediaQuery` | CSS media query subscription (shared MQL pool) |
|
|
924
|
+
| `usePrefersReducedMotion` | Reactive reduced-motion preference for non-`phase` animation |
|
|
925
|
+
| `useDevicePixelRatio` | Reactive DPR for renderers outside `useCanvas` |
|
|
926
|
+
| `useSyncedRef` | Ref always in sync with latest value |
|
|
927
|
+
| `useStableCallback` | Stable-identity function that calls latest closure |
|
|
682
928
|
|
|
683
929
|
`useSight`, `useSize`, and `useScrollProgress` each support a transient mode: pass a callback (`onVisibilityChange`, `onResize`, `onProgress`) and the hook delivers updates via callback with zero re-renders. The reactive state field is omitted from the return type so accessing it is a compile-time error. An always-current ref (`phaseRef`, `sizeRef`, `progressRef`) is available in both modes.
|
|
684
930
|
|
|
@@ -860,6 +1106,19 @@ import { WhenIdle } from 'phase/react';
|
|
|
860
1106
|
|
|
861
1107
|
Idle never fires during SSR, so `WhenIdle` children are absent from server HTML. Reserve it for non-critical content. For content that must be crawlable, use `Defer`. Reduced motion is automatic: `data-enter="animate"` is not stamped when reduced motion is preferred.
|
|
862
1108
|
|
|
1109
|
+
### useIdle
|
|
1110
|
+
|
|
1111
|
+
Returns `false`, then flips to `true` once the browser is idle. Use it when the idle signal belongs in render; use `WhenIdle` for a wrapper or `useWhenIdle` for an effect.
|
|
1112
|
+
|
|
1113
|
+
```tsx
|
|
1114
|
+
import { useIdle } from 'phase/react';
|
|
1115
|
+
|
|
1116
|
+
const idle = useIdle({ timeout: 2000 });
|
|
1117
|
+
return idle ? <SecondaryPanel /> : <Skeleton />;
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
Like `WhenIdle`, idle-gated content is absent from server HTML and should be non-critical.
|
|
1121
|
+
|
|
863
1122
|
### useWhenIdle
|
|
864
1123
|
|
|
865
1124
|
Runs a callback once when the browser is idle after mount (the effect-shaped counterpart to `useIdle`). Use it for side effects (prefetching a chunk, warming a cache) rather than rendering. Cancels on unmount and always calls the latest callback.
|
|
@@ -974,45 +1233,49 @@ Minimal footprint is a core promise (see [Why phase](#why-phase)). Every export
|
|
|
974
1233
|
| ------------------------- | ----------------: |
|
|
975
1234
|
| **Core** | |
|
|
976
1235
|
| `createTicker` | 834 B |
|
|
977
|
-
| `createSight` |
|
|
978
|
-
| `createLifecycle` | 1.
|
|
1236
|
+
| `createSight` | 967 B |
|
|
1237
|
+
| `createLifecycle` | 1.48 kB |
|
|
979
1238
|
| `createLoop` | 2.59 kB |
|
|
980
|
-
| `createScrollProgress` |
|
|
1239
|
+
| `createScrollProgress` | 866 B |
|
|
981
1240
|
| `createRenderState` | 495 B |
|
|
982
1241
|
| `createDevicePixelRatio` | 544 B |
|
|
983
|
-
| `createMutation` | 1.
|
|
984
|
-
| `createPointer` | 1.
|
|
1242
|
+
| `createMutation` | 1.18 kB |
|
|
1243
|
+
| `createPointer` | 1.27 kB |
|
|
985
1244
|
| `createScroll` | 1.45 kB |
|
|
1245
|
+
| `createThrottle` | 657 B |
|
|
1246
|
+
| `createDebounce` | 559 B |
|
|
986
1247
|
| `whenIdle` | 409 B |
|
|
987
1248
|
| `prefersReducedMotion` | 101 B |
|
|
988
1249
|
| **Ease** | |
|
|
989
1250
|
| `ease (all)` | 210 B |
|
|
990
1251
|
| **React** | |
|
|
991
|
-
| `useLoop` | 2.
|
|
992
|
-
| `useLifecycle` | 1.
|
|
1252
|
+
| `useLoop` | 2.82 kB |
|
|
1253
|
+
| `useLifecycle` | 1.68 kB |
|
|
993
1254
|
| `useSight` | 1.18 kB |
|
|
994
1255
|
| `useCanvas` | 3.44 kB |
|
|
995
1256
|
| `useMutation` | 1.36 kB |
|
|
996
|
-
| `usePointer` | 1.
|
|
997
|
-
| `useScroll` | 1.
|
|
998
|
-
| `
|
|
999
|
-
| `
|
|
1000
|
-
| `
|
|
1001
|
-
| `
|
|
1002
|
-
| `
|
|
1003
|
-
| `
|
|
1257
|
+
| `usePointer` | 1.47 kB |
|
|
1258
|
+
| `useScroll` | 1.72 kB |
|
|
1259
|
+
| `useThrottledCallback` | 797 B |
|
|
1260
|
+
| `useDebouncedCallback` | 688 B |
|
|
1261
|
+
| `useTween` | 619 B |
|
|
1262
|
+
| `usePresence` | 591 B |
|
|
1263
|
+
| `useScrollProgress` | 997 B |
|
|
1264
|
+
| `useSize` | 378 B |
|
|
1265
|
+
| `useContainerQuery` | 384 B |
|
|
1266
|
+
| `useMediaQuery` | 246 B |
|
|
1004
1267
|
| `usePrefersReducedMotion` | 272 B |
|
|
1005
1268
|
| `useDevicePixelRatio` | 231 B |
|
|
1006
1269
|
| `useSyncedRef` | 22 B |
|
|
1007
1270
|
| `useStableCallback` | 39 B |
|
|
1008
|
-
| `Presence` |
|
|
1271
|
+
| `Presence` | 741 B |
|
|
1009
1272
|
| `WhenVisible` | 1.44 kB |
|
|
1010
|
-
| `WhenIdle` |
|
|
1273
|
+
| `WhenIdle` | 593 B |
|
|
1011
1274
|
| `Defer` | 86 B |
|
|
1012
1275
|
| `useIdle` | 435 B |
|
|
1013
|
-
| `useWhenIdle` |
|
|
1276
|
+
| `useWhenIdle` | 445 B |
|
|
1014
1277
|
| `useRenderState` | 527 B |
|
|
1015
|
-
| `Swap` | 1.
|
|
1278
|
+
| `Swap` | 1.13 kB |
|
|
1016
1279
|
|
|
1017
1280
|
<!-- SIZE-TABLE:END -->
|
|
1018
1281
|
|
|
@@ -243,10 +243,7 @@ function observeIntersection(options) {
|
|
|
243
243
|
* IO options are immutable after construction, so identical options can share.
|
|
244
244
|
*/
|
|
245
245
|
function getPoolKey(opts) {
|
|
246
|
-
|
|
247
|
-
const margin = opts.rootMargin ?? "0px";
|
|
248
|
-
const threshold = Array.isArray(opts.threshold) ? opts.threshold.join(",") : String(opts.threshold ?? 0);
|
|
249
|
-
return root + "|" + margin + "|" + threshold;
|
|
246
|
+
return `${opts.root ? "custom" : "null"}|${opts.rootMargin ?? "0px"}|${Array.isArray(opts.threshold) ? opts.threshold.join(",") : String(opts.threshold ?? 0)}`;
|
|
250
247
|
}
|
|
251
248
|
/** Return an existing pool entry for this key, or create and register a new one. */
|
|
252
249
|
function getOrCreatePoolEntry(key, options) {
|
|
@@ -1414,6 +1411,208 @@ function createScroll(options) {
|
|
|
1414
1411
|
};
|
|
1415
1412
|
}
|
|
1416
1413
|
//#endregion
|
|
1417
|
-
|
|
1414
|
+
//#region src/core/throttle/index.ts
|
|
1415
|
+
/**
|
|
1416
|
+
* Frame-aligned, visibility-aware throttle. Leading calls fire synchronously;
|
|
1417
|
+
* a pending trailing call rides a one-shot rAF chain and fires with the latest
|
|
1418
|
+
* value on the first frame at or past `interval`. While the document is hidden
|
|
1419
|
+
* nothing is scheduled: a pending call is flushed or dropped per `hidden`, and
|
|
1420
|
+
* new calls are recorded but deferred until the document is visible again.
|
|
1421
|
+
*
|
|
1422
|
+
* @remarks
|
|
1423
|
+
* `call` takes exactly one value and stores it by reference, so the hot path
|
|
1424
|
+
* never allocates. Trailing calls read the value at fire time.
|
|
1425
|
+
*/
|
|
1426
|
+
function createThrottle(options) {
|
|
1427
|
+
if (typeof document === "undefined") serverContextError("createThrottle");
|
|
1428
|
+
const { callback, interval, edge = "both", hidden = "flush", signal } = options;
|
|
1429
|
+
const leading = edge !== "trailing";
|
|
1430
|
+
const trailing = edge !== "leading";
|
|
1431
|
+
let stopped = false;
|
|
1432
|
+
let pending = false;
|
|
1433
|
+
let lastFire = 0;
|
|
1434
|
+
let rafId = 0;
|
|
1435
|
+
let documentVisible = !document.hidden;
|
|
1436
|
+
let latest = void 0;
|
|
1437
|
+
function fire(now) {
|
|
1438
|
+
lastFire = now;
|
|
1439
|
+
pending = false;
|
|
1440
|
+
callback(latest);
|
|
1441
|
+
}
|
|
1442
|
+
function cancelRaf() {
|
|
1443
|
+
if (rafId !== 0) {
|
|
1444
|
+
cancelAnimationFrame(rafId);
|
|
1445
|
+
rafId = 0;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
function tick() {
|
|
1449
|
+
rafId = 0;
|
|
1450
|
+
if (stopped || !pending) return;
|
|
1451
|
+
const now = performance.now();
|
|
1452
|
+
if (now - lastFire >= interval) fire(now);
|
|
1453
|
+
else rafId = requestAnimationFrame(tick);
|
|
1454
|
+
}
|
|
1455
|
+
function scheduleRaf() {
|
|
1456
|
+
if (rafId === 0) rafId = requestAnimationFrame(tick);
|
|
1457
|
+
}
|
|
1458
|
+
function call(value) {
|
|
1459
|
+
if (stopped) return;
|
|
1460
|
+
latest = value;
|
|
1461
|
+
if (!documentVisible) {
|
|
1462
|
+
pending = true;
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const now = performance.now();
|
|
1466
|
+
if (now - lastFire >= interval) {
|
|
1467
|
+
if (leading) {
|
|
1468
|
+
fire(now);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
lastFire = now;
|
|
1472
|
+
}
|
|
1473
|
+
if (trailing) {
|
|
1474
|
+
pending = true;
|
|
1475
|
+
scheduleRaf();
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function flush() {
|
|
1479
|
+
if (stopped || !pending) return;
|
|
1480
|
+
cancelRaf();
|
|
1481
|
+
fire(performance.now());
|
|
1482
|
+
}
|
|
1483
|
+
function cancel() {
|
|
1484
|
+
if (stopped) return;
|
|
1485
|
+
cancelRaf();
|
|
1486
|
+
pending = false;
|
|
1487
|
+
lastFire = 0;
|
|
1488
|
+
}
|
|
1489
|
+
function onVisibilityChange() {
|
|
1490
|
+
documentVisible = !document.hidden;
|
|
1491
|
+
if (stopped) return;
|
|
1492
|
+
if (!documentVisible) {
|
|
1493
|
+
cancelRaf();
|
|
1494
|
+
if (pending) if (hidden === "flush") fire(performance.now());
|
|
1495
|
+
else pending = false;
|
|
1496
|
+
} else if (pending) scheduleRaf();
|
|
1497
|
+
}
|
|
1498
|
+
function onPageShow(event) {
|
|
1499
|
+
if (!event.persisted) return;
|
|
1500
|
+
documentVisible = true;
|
|
1501
|
+
if (!stopped && pending) scheduleRaf();
|
|
1502
|
+
}
|
|
1503
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1504
|
+
window.addEventListener("pageshow", onPageShow);
|
|
1505
|
+
let unlinkAbort;
|
|
1506
|
+
function stop() {
|
|
1507
|
+
if (stopped) return;
|
|
1508
|
+
stopped = true;
|
|
1509
|
+
unlinkAbort?.();
|
|
1510
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
1511
|
+
window.removeEventListener("pageshow", onPageShow);
|
|
1512
|
+
cancelRaf();
|
|
1513
|
+
pending = false;
|
|
1514
|
+
}
|
|
1515
|
+
unlinkAbort = linkAbortSignal(signal, stop);
|
|
1516
|
+
return {
|
|
1517
|
+
call,
|
|
1518
|
+
flush,
|
|
1519
|
+
cancel,
|
|
1520
|
+
get pending() {
|
|
1521
|
+
return pending;
|
|
1522
|
+
},
|
|
1523
|
+
stop
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
//#endregion
|
|
1527
|
+
//#region src/core/debounce/index.ts
|
|
1528
|
+
/**
|
|
1529
|
+
* Visibility-aware trailing debounce. Fires the callback with the latest
|
|
1530
|
+
* value once `wait` milliseconds pass without a new call. While the document
|
|
1531
|
+
* is hidden no timer runs: a pending call is flushed or dropped per `hidden`,
|
|
1532
|
+
* and new calls are recorded but wait until the document is visible again,
|
|
1533
|
+
* when the quiet timer restarts.
|
|
1534
|
+
*/
|
|
1535
|
+
function createDebounce(options) {
|
|
1536
|
+
if (typeof document === "undefined") serverContextError("createDebounce");
|
|
1537
|
+
const { callback, wait, hidden = "flush", signal } = options;
|
|
1538
|
+
let stopped = false;
|
|
1539
|
+
let pending = false;
|
|
1540
|
+
let timer;
|
|
1541
|
+
let documentVisible = !document.hidden;
|
|
1542
|
+
let latest = void 0;
|
|
1543
|
+
function fire() {
|
|
1544
|
+
pending = false;
|
|
1545
|
+
callback(latest);
|
|
1546
|
+
}
|
|
1547
|
+
function clearTimer() {
|
|
1548
|
+
if (timer !== void 0) {
|
|
1549
|
+
clearTimeout(timer);
|
|
1550
|
+
timer = void 0;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
function onTimer() {
|
|
1554
|
+
timer = void 0;
|
|
1555
|
+
if (stopped || !pending) return;
|
|
1556
|
+
fire();
|
|
1557
|
+
}
|
|
1558
|
+
function startTimer() {
|
|
1559
|
+
clearTimer();
|
|
1560
|
+
timer = setTimeout(onTimer, wait);
|
|
1561
|
+
}
|
|
1562
|
+
function call(value) {
|
|
1563
|
+
if (stopped) return;
|
|
1564
|
+
latest = value;
|
|
1565
|
+
pending = true;
|
|
1566
|
+
if (documentVisible) startTimer();
|
|
1567
|
+
}
|
|
1568
|
+
function flush() {
|
|
1569
|
+
if (stopped || !pending) return;
|
|
1570
|
+
clearTimer();
|
|
1571
|
+
fire();
|
|
1572
|
+
}
|
|
1573
|
+
function cancel() {
|
|
1574
|
+
if (stopped) return;
|
|
1575
|
+
clearTimer();
|
|
1576
|
+
pending = false;
|
|
1577
|
+
}
|
|
1578
|
+
function onVisibilityChange() {
|
|
1579
|
+
documentVisible = !document.hidden;
|
|
1580
|
+
if (stopped) return;
|
|
1581
|
+
if (!documentVisible) {
|
|
1582
|
+
clearTimer();
|
|
1583
|
+
if (pending) if (hidden === "flush") fire();
|
|
1584
|
+
else pending = false;
|
|
1585
|
+
} else if (pending) startTimer();
|
|
1586
|
+
}
|
|
1587
|
+
function onPageShow(event) {
|
|
1588
|
+
if (!event.persisted) return;
|
|
1589
|
+
documentVisible = true;
|
|
1590
|
+
if (!stopped && pending) startTimer();
|
|
1591
|
+
}
|
|
1592
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1593
|
+
window.addEventListener("pageshow", onPageShow);
|
|
1594
|
+
let unlinkAbort;
|
|
1595
|
+
function stop() {
|
|
1596
|
+
if (stopped) return;
|
|
1597
|
+
stopped = true;
|
|
1598
|
+
unlinkAbort?.();
|
|
1599
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
1600
|
+
window.removeEventListener("pageshow", onPageShow);
|
|
1601
|
+
clearTimer();
|
|
1602
|
+
pending = false;
|
|
1603
|
+
}
|
|
1604
|
+
unlinkAbort = linkAbortSignal(signal, stop);
|
|
1605
|
+
return {
|
|
1606
|
+
call,
|
|
1607
|
+
flush,
|
|
1608
|
+
cancel,
|
|
1609
|
+
get pending() {
|
|
1610
|
+
return pending;
|
|
1611
|
+
},
|
|
1612
|
+
stop
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
//#endregion
|
|
1616
|
+
export { missingContextError as C, isPhaseError as S, linkAbortSignal as T, subscribeMediaQuery as _, createPointer as a, PhaseError as b, prefersReducedMotion as c, subscribeDpr as d, createRenderState as f, readMediaQuery as g, createLifecycle as h, observeResize as i, whenIdle as l, createLoop as m, createThrottle as n, createMutation as o, createScrollProgress as p, createScroll as r, REDUCED_MOTION_QUERY as s, createDebounce as t, readDpr as u, createSight as v, serverContextError as w, invalidDurationError as x, createTicker as y };
|
|
1418
1617
|
|
|
1419
|
-
//# sourceMappingURL=
|
|
1618
|
+
//# sourceMappingURL=debounce-BD9K4TW4.js.map
|