kuinetic 0.1.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/esm/chunk-LWS4OSLX.mjs +730 -0
  4. package/dist/esm/chunk-QZIJ7WZI.mjs +3312 -0
  5. package/dist/esm/chunk-R3TGDJKA.mjs +1264 -0
  6. package/dist/esm/core/index.mjs +34 -0
  7. package/dist/esm/effects/index.mjs +27 -0
  8. package/dist/esm/index.mjs +52 -0
  9. package/dist/kuinetic.all.js +5291 -0
  10. package/dist/kuinetic.css +2338 -0
  11. package/dist/kuinetic.js +5281 -0
  12. package/dist/types/core/activation.d.ts +21 -0
  13. package/dist/types/core/animator.d.ts +216 -0
  14. package/dist/types/core/attrs.d.ts +15 -0
  15. package/dist/types/core/capabilities.d.ts +19 -0
  16. package/dist/types/core/channels.d.ts +25 -0
  17. package/dist/types/core/compile.d.ts +49 -0
  18. package/dist/types/core/dom-watcher.d.ts +35 -0
  19. package/dist/types/core/effect-context.d.ts +49 -0
  20. package/dist/types/core/element-config.d.ts +52 -0
  21. package/dist/types/core/flip.d.ts +80 -0
  22. package/dist/types/core/gesture.d.ts +99 -0
  23. package/dist/types/core/index.d.ts +14 -0
  24. package/dist/types/core/instances.d.ts +98 -0
  25. package/dist/types/core/js-effect-preparer.d.ts +46 -0
  26. package/dist/types/core/js-params.d.ts +124 -0
  27. package/dist/types/core/owned-styles.d.ts +44 -0
  28. package/dist/types/core/params.d.ts +65 -0
  29. package/dist/types/core/parse.d.ts +22 -0
  30. package/dist/types/core/path-morph.d.ts +83 -0
  31. package/dist/types/core/play.d.ts +48 -0
  32. package/dist/types/core/registry.d.ts +38 -0
  33. package/dist/types/core/reporter.d.ts +35 -0
  34. package/dist/types/core/scroll-scheduler.d.ts +153 -0
  35. package/dist/types/core/spring.d.ts +83 -0
  36. package/dist/types/core/stagger.d.ts +19 -0
  37. package/dist/types/core/style-plan.d.ts +56 -0
  38. package/dist/types/core/types.d.ts +239 -0
  39. package/dist/types/effects/catalog/ambient.d.ts +18 -0
  40. package/dist/types/effects/catalog/core.d.ts +19 -0
  41. package/dist/types/effects/catalog/feedback.d.ts +21 -0
  42. package/dist/types/effects/catalog/index.d.ts +21 -0
  43. package/dist/types/effects/catalog/interaction-shared.d.ts +56 -0
  44. package/dist/types/effects/catalog/interaction.d.ts +22 -0
  45. package/dist/types/effects/catalog/media.d.ts +13 -0
  46. package/dist/types/effects/catalog/numbers-shared.d.ts +103 -0
  47. package/dist/types/effects/catalog/numbers.d.ts +31 -0
  48. package/dist/types/effects/catalog/shared.d.ts +3 -0
  49. package/dist/types/effects/catalog/text-shared.d.ts +180 -0
  50. package/dist/types/effects/catalog/text.d.ts +17 -0
  51. package/dist/types/effects/forms/index.d.ts +14 -0
  52. package/dist/types/effects/forms/primitives.d.ts +32 -0
  53. package/dist/types/effects/gestures/index.d.ts +19 -0
  54. package/dist/types/effects/gestures/primitives.d.ts +2 -0
  55. package/dist/types/effects/index.d.ts +19 -0
  56. package/dist/types/effects/layout/index.d.ts +12 -0
  57. package/dist/types/effects/layout/presets.d.ts +9 -0
  58. package/dist/types/effects/layout/primitives.d.ts +2 -0
  59. package/dist/types/effects/navigation/index.d.ts +30 -0
  60. package/dist/types/effects/scroll-mechanics/index.d.ts +14 -0
  61. package/dist/types/effects/scroll-mechanics/presets.d.ts +6 -0
  62. package/dist/types/effects/scroll-mechanics/primitives.d.ts +2 -0
  63. package/dist/types/effects/scroll-mechanics/tracker.d.ts +55 -0
  64. package/dist/types/effects/shared.d.ts +26 -0
  65. package/dist/types/effects/svg/index.d.ts +13 -0
  66. package/dist/types/effects/three-d/index.d.ts +20 -0
  67. package/dist/types/index.d.ts +16 -0
  68. package/package.json +78 -0
@@ -0,0 +1,21 @@
1
+ import type { Activation, Cleanup } from './types.js';
2
+ export interface ActivationBinder {
3
+ bind(el: Element, activation: Activation, threshold: string, onActivate: () => void): Cleanup;
4
+ destroy(): void;
5
+ }
6
+ export interface ActivationBinderOptions {
7
+ /** Injected so tests can supply a controllable observer and assert without layout. */
8
+ createObserver?: (callback: IntersectionObserverCallback, options: IntersectionObserverInit) => IntersectionObserver;
9
+ }
10
+ /**
11
+ * Bind activations to elements, sharing one IntersectionObserver per distinct threshold.
12
+ *
13
+ * One observer per threshold rather than per element is the difference between a constant number
14
+ * of observers and one per animated node on a long page.
15
+ *
16
+ * @param options - Optional observer factory; defaults to the global constructor when present.
17
+ * @returns A binder whose `bind` returns the teardown for that single binding.
18
+ * @complexity O(1) per bind; O(t) space in the number of distinct thresholds.
19
+ * @overallScore 100
20
+ */
21
+ export declare function createActivationBinder(options?: ActivationBinderOptions): ActivationBinder;
@@ -0,0 +1,216 @@
1
+ import type { ActivationBinder } from './activation.js';
2
+ import { ATTR } from './attrs.js';
3
+ import type { Capabilities } from './capabilities.js';
4
+ import type { DomWatcher } from './dom-watcher.js';
5
+ import type { JsEffectPreparer } from './js-effect-preparer.js';
6
+ import type { ScrollRoot, ScrollScheduler } from './scroll-scheduler.js';
7
+ import type { PlaybackHandle, PlayOptions, Target } from './play.js';
8
+ import { Registry } from './registry.js';
9
+ import type { Reporter } from './reporter.js';
10
+ import type { InstanceState } from './types.js';
11
+ export { ATTR };
12
+ export interface AnimatorOptions {
13
+ root?: ParentNode;
14
+ /** Effect catalog. Injected so a consumer can ship only the effects they use. */
15
+ registry?: Registry;
16
+ /** Environment capabilities. Injected so timeline and fallback paths are testable. */
17
+ capabilities?: Capabilities;
18
+ /** Diagnostic sink. Defaults to silent; pass `consoleReporter()` in development. */
19
+ reporter?: Reporter;
20
+ /** Activation strategy. Injected so tests can drive visibility without layout. */
21
+ binder?: ActivationBinder;
22
+ /** Shared scroll orchestration for JS-rendered effects. Injected for testability. */
23
+ scheduler?: ScrollScheduler;
24
+ /** Maps an element to the scroll root that moves it. Injected so nesting is fakeable. */
25
+ rootResolver?: (el: Element) => ScrollRoot;
26
+ /** Wires up JS-rendered effects' setup context. Injected so it is testable in isolation. */
27
+ jsEffectPreparer?: JsEffectPreparer;
28
+ /**
29
+ * Watches for DOM insertions, removals, and attribute changes. Injected for testability;
30
+ * defaults to a real `MutationObserver`-backed watcher, built lazily so nothing observes unless
31
+ * `observe: true` and `start()` actually run.
32
+ */
33
+ domWatcher?: DomWatcher;
34
+ /** `'respect'` honours prefers-reduced-motion. `'ignore'` is for demos and tests only. */
35
+ reducedMotion?: 'respect' | 'ignore';
36
+ /** Watch for DOM insertions and attribute changes. Off by default. */
37
+ observe?: boolean;
38
+ }
39
+ /**
40
+ * Scans a document for authored effects and installs them.
41
+ *
42
+ * Every collaborator — registry, capabilities, reporter, activation binder — is injected, so no
43
+ * behaviour depends on module state or globals. The class itself only orchestrates: parsing,
44
+ * compilation, and style decisions all live in pure modules it calls.
45
+ */
46
+ export declare class Animator {
47
+ readonly registry: Registry;
48
+ readonly capabilities: Capabilities;
49
+ private readonly root;
50
+ private readonly reporter;
51
+ private readonly binder;
52
+ private readonly scheduler;
53
+ private readonly rootResolver;
54
+ private readonly jsEffectPreparer;
55
+ private readonly respectReducedMotion;
56
+ private readonly shouldObserve;
57
+ /** Runtime truth. Attributes are for CSS and debugging; they make a poor state machine. */
58
+ private readonly states;
59
+ /** Iterable lifecycle index; the WeakMap remains the fast state lookup. */
60
+ private readonly liveElements;
61
+ /** Built lazily by `watch()` when not injected, so nothing observes until `start()` needs it. */
62
+ private domWatcher;
63
+ private started;
64
+ constructor(options?: AnimatorOptions);
65
+ /**
66
+ * Explicit entry point. Importing the library never touches the document, which keeps SSR,
67
+ * hydration, and tests deterministic.
68
+ *
69
+ * @complexity O(n) time in the number of elements scanned.
70
+ * @overallScore 100
71
+ */
72
+ start(): this;
73
+ /**
74
+ * Process every unprocessed element in a subtree, including the root.
75
+ *
76
+ * `querySelectorAll` excludes the root, but an inserted subtree very often carries the
77
+ * attribute on its top node — skipping it silently drops those animations.
78
+ *
79
+ * @param root - Subtree to scan. Defaults to the animator's root.
80
+ * @complexity O(n) time in the subtree size; O(1) extra space.
81
+ * @overallScore 100
82
+ */
83
+ scan(root?: ParentNode): this;
84
+ /**
85
+ * Compile and install one element's effects, recompiling if its attribute changed.
86
+ *
87
+ * @param el - Element carrying `data-kui`.
88
+ * @complexity O(e) time in the number of composed effects; O(e) space for the plan.
89
+ * @overallScore 100
90
+ */
91
+ process(el: Element): void;
92
+ /**
93
+ * Apply a compiled plan and bind its activation.
94
+ *
95
+ * @complexity O(e) time in composed effects; O(e) space for retained cleanups.
96
+ * @overallScore 100
97
+ */
98
+ /**
99
+ * Choose the activation, letting a primitive's preference fill in only when the author named
100
+ * none, and warning when an authored activation is not one the effect supports.
101
+ *
102
+ * Declared capability metadata was previously never checked anywhere, which made
103
+ * `supportedActivations` documentation rather than a contract.
104
+ *
105
+ * @complexity O(a) time in supported activations; O(1) space.
106
+ * @overallScore 100
107
+ */
108
+ private resolveActivation;
109
+ private install;
110
+ /**
111
+ * Decide whether, and when, the effects on this element are allowed to start.
112
+ *
113
+ * The single place any effect begins. Routing both renderers through it is what makes
114
+ * `on:enter`, `on:click`, `manual`, and `reducedMotion: 'disable'` mean the same thing for a
115
+ * pinned section as for a fade — previously JS effects started during `prepare` and honoured
116
+ * none of them.
117
+ *
118
+ * @complexity O(n) time in the number of instances; O(1) space.
119
+ * @overallScore 100
120
+ */
121
+ private openGate;
122
+ /**
123
+ * Start a deferred animation.
124
+ *
125
+ * A JS-rendered effect's real setup work is postponed until this call — `deferPrepare` in
126
+ * `instances.ts` only wires up an inert instance during `prepare` — so a broken primitive (a bad
127
+ * selector, a malformed param) first throws here, not while the plan was being built. `scan()`
128
+ * reaches this synchronously for every `on:load` element, inside the very loop that processes
129
+ * every other element on the page; an uncaught throw here previously unwound that loop and
130
+ * silently orphaned every element after the broken one — the same blast radius the `__proto__`
131
+ * scan-crash fix closed for a different door. Each instance is isolated so one effect's failure
132
+ * can neither strand a sibling effect on the same element nor abort the rest of the scan.
133
+ *
134
+ * @complexity O(n) time in composed instances; O(1) space.
135
+ * @overallScore 100
136
+ */
137
+ activate(el: Element): void;
138
+ /**
139
+ * Activate one instance, isolating a throw from its (possibly deferred) setup.
140
+ *
141
+ * @returns Whether the instance actually started — a failed instance is excluded from the
142
+ * `finished` gate in `activate()`, since something that never started can never legitimately
143
+ * finish (see `EffectInstance.finished`'s "resolves, never rejects" contract in `types.ts`).
144
+ * @complexity O(1) time, O(1) space.
145
+ * @overallScore 100
146
+ */
147
+ private startInstance;
148
+ /**
149
+ * Programmatic entry point. Accepts a selector, an Element, a NodeList, or any iterable, so
150
+ * `getElementById`, `getElementsByClassName`, and `querySelectorAll` all work directly.
151
+ *
152
+ * @complexity O(n) time in selected elements.
153
+ * @overallScore 100
154
+ */
155
+ play(target: Target, effect: string, options?: PlayOptions): PlaybackHandle;
156
+ /**
157
+ * Remove the opt-in cloak so a stalled or failed initialisation can never leave a page hidden.
158
+ *
159
+ * @complexity O(1) time, O(1) space.
160
+ * @overallScore 100
161
+ */
162
+ uncloak(): void;
163
+ stateOf(el: Element): InstanceState | undefined;
164
+ /**
165
+ * Tear an element's effects down so the next `process()` reinstalls from scratch.
166
+ *
167
+ * Needed for replay: `process()` short-circuits when the configuration fingerprint is
168
+ * unchanged, so playing the same effect twice was previously a no-op.
169
+ *
170
+ * @complexity O(c) time in retained instances; O(1) space.
171
+ * @overallScore 100
172
+ */
173
+ reset(el: Element): void;
174
+ /**
175
+ * Tear down one element's effects and clear its library-owned attributes.
176
+ *
177
+ * @complexity O(c) time in retained cleanups; O(1) extra space.
178
+ * @overallScore 100
179
+ */
180
+ private release;
181
+ /**
182
+ * Tear down every tracked element inside a removed subtree.
183
+ *
184
+ * Scoped to `node`'s own descendants rather than re-scanning `liveElements` against the whole
185
+ * page, so a removal event costs O(removed subtree), not O(every animated element alive
186
+ * anywhere) — `dom-watcher.ts` can queue up to 100 removed roots per frame, and `liveElements`
187
+ * only shrinks on release, so it stays large on a scroll-reveal-heavy page.
188
+ *
189
+ * Membership is checked against `liveElements` (the ground truth) rather than re-querying
190
+ * `[${ATTR.source}]` the way `scan()` does: `dom-watcher.ts`'s `flush()` drains removed roots
191
+ * before attribute-change roots, so if calling code strips `data-kui` and removes the element in
192
+ * the same tick, a selector-based query would already miss it here and leak its teardown.
193
+ *
194
+ * `node` is typed `Element`, not `ParentNode`: `dom-watcher.ts`'s `onElementRemoved` — this
195
+ * method's only caller — is itself typed `(el: Element) => void`, so there is no runtime case
196
+ * where `node` is a `Document`/`DocumentFragment` to guard against.
197
+ *
198
+ * @complexity O(s) time in the removed subtree's element count; O(1) per candidate via the
199
+ * `liveElements` Set lookup.
200
+ * @overallScore 100
201
+ */
202
+ private releaseTree;
203
+ destroy(): void;
204
+ /**
205
+ * Start watching for DOM insertions, removals, and attribute changes.
206
+ *
207
+ * Attribute changes recompile in place; insertions scan; removals tear down so listeners and
208
+ * observers do not outlive their elements.
209
+ *
210
+ * @complexity O(1) time and space to build and start; the watcher's own callback runs O(n) time
211
+ * in the nodes one mutation record carries.
212
+ * @overallScore 100
213
+ */
214
+ private watch;
215
+ }
216
+ export declare function createAnimator(options?: AnimatorOptions): Animator;
@@ -0,0 +1,15 @@
1
+ /** Attribute names, in one place so the namespace is a single rename before publish. */
2
+ export declare const ATTR: {
3
+ /** Authored. The rich grammar. */
4
+ readonly source: "data-kui";
5
+ /** Library-owned and unstable: normalized effect names, for CSS hooks and debugging. */
6
+ readonly normalized: "data-kui-fx";
7
+ readonly state: "data-kui-state";
8
+ readonly on: "data-kui-on";
9
+ readonly timeline: "data-kui-timeline";
10
+ readonly threshold: "data-kui-threshold";
11
+ readonly stagger: "data-kui-stagger";
12
+ readonly cloak: "data-kui-cloak";
13
+ /** Reduced-motion policy, stamped from the primitive so the CSS layer can act on it. */
14
+ readonly rm: "data-kui-rm";
15
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Per-feature detection.
3
+ *
4
+ * One global `CSS.supports('animation-timeline','view()')` check is not an abstraction boundary:
5
+ * `animation-range`, named timelines, and individual transform properties all ship separately.
6
+ * Each capability is probed independently and cached. See docs/design.md §6.
7
+ */
8
+ export interface Capabilities {
9
+ viewTimeline: boolean;
10
+ scrollTimeline: boolean;
11
+ animationRange: boolean;
12
+ individualTransforms: boolean;
13
+ scrollTimelineName: boolean;
14
+ viewTransitions: boolean;
15
+ intersectionObserver: boolean;
16
+ reducedMotion: boolean;
17
+ }
18
+ export declare function detect(force?: boolean): Capabilities;
19
+ export declare function resetCapabilities(): void;
@@ -0,0 +1,25 @@
1
+ import type { Channel } from './types.js';
2
+ /**
3
+ * Composition safety.
4
+ *
5
+ * Two CSS rules that each declare `animation` do NOT concatenate — the cascade discards one.
6
+ * So a comma list is compiled into a single declaration with parallel value lists. That fixes
7
+ * the cascade problem but not the *property* problem: two animations writing `opacity` replace
8
+ * rather than blend.
9
+ *
10
+ * Hence channels. An effect declares the property groups it owns; a comma list is only safe to
11
+ * compile when those sets are pairwise disjoint. See docs/design.md §4.
12
+ */
13
+ export interface ChannelClaim {
14
+ name: string;
15
+ channels: Channel[];
16
+ }
17
+ export interface Conflict {
18
+ channel: Channel;
19
+ effects: [string, string];
20
+ }
21
+ /** Every pair of effects claiming the same channel. Empty array means the list composes. */
22
+ export declare function findConflicts(claims: ChannelClaim[]): Conflict[];
23
+ export declare function describeConflicts(conflicts: Conflict[]): string;
24
+ /** Union of all claimed channels, for debugging and perf accounting. */
25
+ export declare function claimedChannels(claims: ChannelClaim[]): Channel[];
@@ -0,0 +1,49 @@
1
+ import type { Registry, ResolvedEffect } from './registry.js';
2
+ import type { Activation, Channel, EffectSpec, ParsedValue, ReducedMotionPolicy, Timeline } from './types.js';
3
+ export interface Entry {
4
+ spec: EffectSpec;
5
+ resolved: ResolvedEffect;
6
+ }
7
+ export interface CompiledPlan {
8
+ /** Effect names to stamp into `data-kui-fx` for CSS hooks and debugging. */
9
+ fxNames: string[];
10
+ /** Custom properties to write. Author overrides only — defaults stay in CSS `var()` fallbacks. */
11
+ vars: Record<string, string>;
12
+ /** Longhand animation declarations, compiled as parallel lists so effects compose. */
13
+ declarations: Record<string, string>;
14
+ /** Effects whose renderer needs JS setup. */
15
+ jsEffects: Entry[];
16
+ /** Names that are not registered. Must NOT be stamped, or the element is never rescanned. */
17
+ unknown: string[];
18
+ /** Strictest reduced-motion policy among the composed effects. */
19
+ reducedMotion: ReducedMotionPolicy;
20
+ /** Activation preferred by the composed primitives when the author named none. */
21
+ defaultActivation?: Activation;
22
+ /** Activations every composed primitive supports, for enforcement by the animator. */
23
+ supportedActivations: Activation[];
24
+ /**
25
+ * Timelines every composed primitive supports. Empty means none — `style-plan.ts` must not
26
+ * apply a native `view()`/`scroll()` timeline the author's effect doesn't declare support for,
27
+ * even when the browser itself is capable of one; `warnUnsupportedTimeline` only warns, it
28
+ * doesn't change what's compiled, so this is what actually stops the mismatch from being applied.
29
+ */
30
+ supportedTimelines: Timeline[];
31
+ /** Union of channels every composed effect writes to, so callers can react to what actually moves. */
32
+ channels: Channel[];
33
+ warnings: string[];
34
+ }
35
+ /**
36
+ * Turn a parsed `data-kui` value into the writes an element needs.
37
+ *
38
+ * Pure: same inputs always produce the same plan, and nothing is applied to the DOM here. That
39
+ * is what lets composition rules, parameter validation, and declaration output be asserted
40
+ * directly rather than through a rendered document.
41
+ *
42
+ * @param parsed - Output of `parse`.
43
+ * @param registry - Effect catalog to resolve names against.
44
+ * @param timeline - Element-scoped timeline, used to warn on unsupported combinations.
45
+ * @returns A plan describing custom properties, declarations, JS effects, and warnings.
46
+ * @complexity O(e * p) time in composed effects and their parameters; O(e) space.
47
+ * @overallScore 100
48
+ */
49
+ export declare function compile(parsed: ParsedValue, registry: Registry, timeline: Timeline): CompiledPlan;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Watches a subtree for authored-effect insertions, removals, and attribute changes.
3
+ *
4
+ * Extracted from `Animator` as an injected collaborator — same shape as `binder` or `scheduler` —
5
+ * owning its own `MutationObserver` lifecycle exactly as the class did inline: nothing observes
6
+ * until `watch()` is called, and `destroy()` disconnects whatever `watch()` started (or safely
7
+ * no-ops if it never ran).
8
+ */
9
+ export interface DomWatcher {
10
+ /** Start observing. No-op if `MutationObserver` is unavailable in this environment. */
11
+ watch(): void;
12
+ /** Stop observing. Safe to call even if `watch()` was never called. */
13
+ destroy(): void;
14
+ }
15
+ export interface DomWatcherOptions {
16
+ root: ParentNode;
17
+ /** An element was inserted into the watched subtree. */
18
+ onElementAdded(el: Element): void;
19
+ /** An element left the watched subtree. */
20
+ onElementRemoved(el: Element): void;
21
+ /** One of the watched authoring attributes changed on an existing element. */
22
+ onAttributeChanged(el: Element): void;
23
+ /** Injected frame queue used to budget mutation work. */
24
+ schedule?: (callback: () => void) => void;
25
+ /** Injected observer factory for realm isolation and deterministic tests. */
26
+ createObserver?: (callback: MutationCallback) => MutationObserver;
27
+ }
28
+ /**
29
+ * Build a `DomWatcher` closed over one animator's root and mutation callbacks.
30
+ *
31
+ * @complexity O(1) time and space to build. The mutation callback it installs runs O(n) time in
32
+ * the nodes one `MutationRecord` carries.
33
+ * @overallScore 100
34
+ */
35
+ export declare function createDomWatcher(options: DomWatcherOptions): DomWatcher;
@@ -0,0 +1,49 @@
1
+ import type { Capabilities } from './capabilities.js';
2
+ import type { StyleLedger } from './owned-styles.js';
3
+ import type { ScrollRoot, ScrollScheduler } from './scroll-scheduler.js';
4
+ /**
5
+ * What a JS-rendered primitive receives.
6
+ *
7
+ * v1 handed `prepare` only `{ doc, warn }`, which was enough for effects the browser drove and
8
+ * nothing else. Pinning, scrubbing, and FLIP all need the scroll position as a number, the
9
+ * environment's real capabilities, and a way to say "geometry moved" — and every one of those has
10
+ * to be injected rather than read off a global, or the primitive becomes untestable and each
11
+ * instance installs its own listeners.
12
+ *
13
+ * `params` are validated and defaulted (see `js-params.ts`). A primitive never sees a raw author
14
+ * string.
15
+ */
16
+ export interface PrepareContext {
17
+ doc: Document;
18
+ win: Window;
19
+ /** Shared frame source. One listener per scroll root, one rAF per dirtied frame. */
20
+ scheduler: ScrollScheduler;
21
+ /** The scroll root that actually moves this element — the window, or a nested scroller. */
22
+ rootFor(el: Element): ScrollRoot;
23
+ capabilities: Capabilities;
24
+ /**
25
+ * Bump the measurement epoch and schedule a frame. Call after a DOM change the scheduler
26
+ * cannot observe, such as inserting a pin spacer.
27
+ */
28
+ invalidate(): void;
29
+ warn(message: string): void;
30
+ /**
31
+ * Whether the user has asked for reduced motion.
32
+ *
33
+ * The animator refuses to activate any effect whose declared policy is `'disable'` when this is
34
+ * true, so a primitive never has to enforce that itself. It is exposed because effects that
35
+ * remain interactive under reduced motion — a drag still has to follow the finger — need to
36
+ * skip only their decorative parts.
37
+ */
38
+ reducedMotion: boolean;
39
+ /** Aborted on teardown, so a primitive can pass it straight to `addEventListener`. */
40
+ signal: AbortSignal;
41
+ /**
42
+ * Ledger for this element's inline style.
43
+ *
44
+ * Primitives must write through it rather than touching `element.style` directly. Several were
45
+ * previously *removing* properties on teardown — `translate`, `scroll-snap-align` — that the
46
+ * consumer had set themselves and the library never owned.
47
+ */
48
+ style: StyleLedger;
49
+ }
@@ -0,0 +1,52 @@
1
+ import type { Activation, ParsedValue, Timeline } from './types.js';
2
+ /** The subset of an element's attributes this module needs. Keeps resolution DOM-free. */
3
+ export interface ElementAttributes {
4
+ source: string;
5
+ on: string | null;
6
+ timeline: string | null;
7
+ threshold: string | null;
8
+ }
9
+ export interface ElementConfig {
10
+ activation: Activation;
11
+ /** Whether the author named an activation, so a primitive default may fill in when not. */
12
+ activationAuthored: boolean;
13
+ timeline: Timeline;
14
+ /** Trailing tokens of `data-kui-timeline`, e.g. `entry 0% cover 60%`. */
15
+ range: string;
16
+ threshold: string;
17
+ }
18
+ /**
19
+ * Read the attributes this module consumes off a live element.
20
+ *
21
+ * The only DOM-touching function here, so every downstream decision can be tested with a plain
22
+ * object instead of a rendered document.
23
+ *
24
+ * @param el - Element carrying the authored attributes.
25
+ * @returns A plain snapshot of the relevant attribute values.
26
+ * @complexity O(1) time, O(1) space.
27
+ * @overallScore 100
28
+ */
29
+ export declare function readAttributes(el: Element): ElementAttributes;
30
+ /**
31
+ * Resolve element-scoped activation and timeline settings.
32
+ *
33
+ * Activation and timeline are element-scoped rather than per-effect because one element has one
34
+ * activation; values written inline (`on:enter`) are a convenience that takes precedence over the
35
+ * longhand attribute.
36
+ *
37
+ * @param attributes - Snapshot from `readAttributes`.
38
+ * @param parsed - Result of parsing the `data-kui` value, whose hoisted keys win.
39
+ * @returns Fully defaulted configuration; unknown values fall back rather than throwing.
40
+ * @complexity O(k) time in the length of the timeline attribute; O(1) extra space.
41
+ * @overallScore 100
42
+ */
43
+ export declare function resolveConfig(attributes: ElementAttributes, parsed: ParsedValue): ElementConfig;
44
+ /**
45
+ * Convert an IntersectionObserver threshold from `"30%"` or `"0.3"` into a clamped ratio.
46
+ *
47
+ * @param raw - Authored threshold value.
48
+ * @returns A ratio in [0, 1]; unparseable input yields 0 rather than throwing.
49
+ * @complexity O(1) time, O(1) space.
50
+ * @overallScore 100
51
+ */
52
+ export declare function toThresholdRatio(raw: string): number;
@@ -0,0 +1,80 @@
1
+ import type { Cleanup } from './types.js';
2
+ /**
3
+ * FLIP — First, Last, Invert, Play.
4
+ *
5
+ * Measure where things are, let the layout change however it likes, measure again, then apply the
6
+ * inverse transform and animate it away. The layout change itself is never animated, which is why
7
+ * this works for reordering, filtering, sorting, and expanding: none of those are expressible as
8
+ * keyframes, but all of them are expressible as two measurements.
9
+ *
10
+ * Measurement and animation are both injected. Reading `getBoundingClientRect` directly would make
11
+ * every consumer untestable without real layout, and jsdom reports zeroes for everything.
12
+ */
13
+ export interface Box {
14
+ x: number;
15
+ y: number;
16
+ width: number;
17
+ height: number;
18
+ }
19
+ export interface FlipSnapshot {
20
+ boxes: Map<Element, Box>;
21
+ }
22
+ export interface FlipOptions {
23
+ durationMs?: number;
24
+ easing?: string;
25
+ /** Animate width/height differences as a scale. Off for elements with visible borders. */
26
+ scale?: boolean;
27
+ }
28
+ export interface FlipRun {
29
+ /** Elements that actually moved. Unmoved elements are skipped, not animated to identity. */
30
+ moved: Element[];
31
+ finished: Promise<void>;
32
+ cancel(): void;
33
+ }
34
+ export interface FlipDeps {
35
+ measure(el: Element): Box;
36
+ /** Returns `null` where the Web Animations API is unavailable; the move then applies instantly. */
37
+ animate(el: Element, keyframes: Keyframe[], options: KeyframeAnimationOptions): Animation | null;
38
+ }
39
+ export interface FlipEngine {
40
+ snapshot(elements: Iterable<Element>): FlipSnapshot;
41
+ play(before: FlipSnapshot, elements: Iterable<Element>, options?: FlipOptions): FlipRun;
42
+ }
43
+ /**
44
+ * Read an element's box from real layout.
45
+ *
46
+ * @complexity O(1) time, but forces layout — call once per batch, never per element in a loop.
47
+ * @overallScore 100
48
+ */
49
+ export declare function domMeasure(el: Element): Box;
50
+ /**
51
+ * Create a FLIP engine.
52
+ *
53
+ * @param deps - Measurement and animation sources; both default to the DOM implementations.
54
+ * @returns An engine exposing `snapshot` and `play`.
55
+ * @complexity O(n) per call in the number of elements; O(n) space for the snapshot.
56
+ * @overallScore 100
57
+ */
58
+ export declare function createFlipEngine(deps?: Partial<FlipDeps>): FlipEngine;
59
+ /**
60
+ * Watch a container and FLIP its children whenever its child list changes.
61
+ *
62
+ * This is what turns one engine into the whole layout category: reorder, filter, sort, shuffle,
63
+ * and grid↔list are all "the children moved", and none of them need to know why.
64
+ *
65
+ * @param container - Element whose children are animated.
66
+ * @param engine - FLIP engine to use.
67
+ * @param options - Timing and scaling.
68
+ * @param observe - MutationObserver factory, injected for tests.
69
+ * @returns Teardown that disconnects the observer.
70
+ * @complexity O(n) per mutation batch in the number of children.
71
+ * @overallScore 100
72
+ */
73
+ export declare function observeLayout(container: Element, engine: FlipEngine, options: FlipOptions, observe: (callback: () => void) => Cleanup): Cleanup;
74
+ /**
75
+ * MutationObserver-backed layout watcher.
76
+ *
77
+ * @complexity O(1) to install; callback cost is the caller's.
78
+ * @overallScore 100
79
+ */
80
+ export declare function mutationWatcher(container: Element): (callback: () => void) => Cleanup;
@@ -0,0 +1,99 @@
1
+ import type { Cleanup } from './types.js';
2
+ /**
3
+ * Pointer gesture recognition.
4
+ *
5
+ * Drag, swipe, and long-press are the same stream of pointer events read with different
6
+ * questions, so they share one recogniser. Velocity is the reason this cannot be a thin wrapper:
7
+ * a throw needs the speed of the last few milliseconds of movement, not the average over the
8
+ * whole gesture, and a single trailing sample is far too noisy to use.
9
+ *
10
+ * Every input is injected — the event target, the clock, and pointer capture — so the whole
11
+ * recogniser is drivable from tests with synthetic events and a fake clock.
12
+ */
13
+ export interface Sample {
14
+ x: number;
15
+ y: number;
16
+ time: number;
17
+ }
18
+ export interface GestureVector {
19
+ dx: number;
20
+ dy: number;
21
+ /** Pixels per second. */
22
+ vx: number;
23
+ vy: number;
24
+ }
25
+ export type Axis = 'x' | 'y' | 'both';
26
+ export type Direction = 'left' | 'right' | 'up' | 'down';
27
+ export interface GestureHandlers {
28
+ onStart?(sample: Sample): void;
29
+ onMove?(vector: GestureVector, sample: Sample): void;
30
+ onEnd?(vector: GestureVector, sample: Sample): void;
31
+ onSwipe?(direction: Direction, vector: GestureVector): void;
32
+ onLongPress?(sample: Sample): void;
33
+ }
34
+ export interface GestureOptions {
35
+ /** Movement below this many pixels is a tap, not a drag. */
36
+ threshold?: number;
37
+ axis?: Axis;
38
+ /** Minimum speed, px/s, for a movement to count as a swipe. */
39
+ swipeVelocity?: number;
40
+ /** Hold duration in ms before `onLongPress`. Zero disables it. */
41
+ longPressMs?: number;
42
+ }
43
+ export interface GestureDeps {
44
+ now(): number;
45
+ setTimer(callback: () => void, ms: number): number;
46
+ clearTimer(handle: number): void;
47
+ }
48
+ /**
49
+ * Estimate velocity from recent samples.
50
+ *
51
+ * Uses the oldest sample still inside the window rather than the previous frame: consecutive
52
+ * pointer events can be microseconds apart, and dividing by that produces enormous, meaningless
53
+ * speeds that throw an element off screen.
54
+ *
55
+ * @param samples - Recent positions, oldest first.
56
+ * @returns Pixels per second on both axes; zero when the window has no span.
57
+ * @complexity O(n) time in retained samples; O(1) space.
58
+ * @overallScore 100
59
+ */
60
+ export declare function velocityFrom(samples: Sample[]): {
61
+ vx: number;
62
+ vy: number;
63
+ };
64
+ /**
65
+ * Classify a release into a swipe direction.
66
+ *
67
+ * @returns The dominant direction, or `null` when neither axis is fast enough.
68
+ * @complexity O(1) time and space.
69
+ * @overallScore 100
70
+ */
71
+ export declare function swipeDirection(vector: GestureVector, minVelocity: number): Direction | null;
72
+ /**
73
+ * Recognise drag, swipe, and long-press on an element.
74
+ *
75
+ * @param el - Element to observe.
76
+ * @param handlers - Callbacks for each recognised phase.
77
+ * @param options - Threshold, axis lock, swipe velocity, long-press duration.
78
+ * @param deps - Clock and timer source, injected for tests.
79
+ * @returns Teardown removing every listener and pending timer.
80
+ * @complexity O(1) per pointer event; O(1) space (samples are capped).
81
+ * @overallScore 100
82
+ */
83
+ export declare function recognise(el: Element, handlers: GestureHandlers, options?: GestureOptions, deps?: GestureDeps): Cleanup;
84
+ export declare function defaultGestureDeps(): GestureDeps;
85
+ /**
86
+ * Apply rubber-band resistance past a boundary.
87
+ *
88
+ * Past the edge, movement is damped rather than blocked, so the surface still tracks the finger
89
+ * but signals that it has run out. Hard-clamping instead feels broken, which is why every native
90
+ * scroller does this.
91
+ *
92
+ * @param offset - Raw offset from the boundary.
93
+ * @param limit - Distance at which resistance approaches its maximum.
94
+ * @param tension - 0–1; lower resists harder.
95
+ * @returns The damped offset.
96
+ * @complexity O(1) time and space.
97
+ * @overallScore 100
98
+ */
99
+ export declare function rubberBand(offset: number, limit: number, tension?: number): number;