kuinetic 0.1.2 → 0.1.4

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 (31) hide show
  1. package/README.md +3 -3
  2. package/dist/esm/{chunk-WPU33HDL.mjs → chunk-7WMNPIOZ.mjs} +1724 -240
  3. package/dist/esm/{chunk-LWS4OSLX.mjs → chunk-JT4PZL3A.mjs} +50 -7
  4. package/dist/esm/{chunk-5EVG7CIY.mjs → chunk-TJDDIQRG.mjs} +42 -16
  5. package/dist/esm/core/index.mjs +2 -2
  6. package/dist/esm/effects/index.mjs +2 -2
  7. package/dist/esm/index.mjs +3 -3
  8. package/dist/kuinetic.all.js +1813 -262
  9. package/dist/kuinetic.css +1491 -94
  10. package/dist/kuinetic.js +1812 -261
  11. package/dist/types/core/instances.d.ts +34 -3
  12. package/dist/types/core/path-morph.d.ts +19 -0
  13. package/dist/types/core/style-plan.d.ts +6 -1
  14. package/dist/types/core/types.d.ts +99 -1
  15. package/dist/types/effects/catalog/background-media.d.ts +152 -0
  16. package/dist/types/effects/catalog/index.d.ts +0 -6
  17. package/dist/types/effects/catalog/media-shared.d.ts +124 -0
  18. package/dist/types/effects/catalog/media.d.ts +4 -0
  19. package/dist/types/effects/catalog/numbers-shared.d.ts +5 -1
  20. package/dist/types/effects/catalog/shared.d.ts +2 -3
  21. package/dist/types/effects/catalog/subtree-capture.d.ts +23 -0
  22. package/dist/types/effects/catalog/text-shared.d.ts +11 -2
  23. package/dist/types/effects/gestures/index.d.ts +0 -1
  24. package/dist/types/effects/layout/index.d.ts +0 -2
  25. package/dist/types/effects/scroll-mechanics/index.d.ts +0 -4
  26. package/dist/types/effects/scroll-mechanics/scroll-spy.d.ts +26 -0
  27. package/dist/types/effects/scroll-mechanics/tracker.d.ts +61 -3
  28. package/dist/types/effects/shared.d.ts +40 -0
  29. package/dist/types/effects/step-marking.d.ts +99 -0
  30. package/dist/types/effects/three-d/index.d.ts +0 -7
  31. package/package.json +9 -2
@@ -23,7 +23,7 @@ import type { Cleanup, EffectInstance } from './types.js';
23
23
  * @complexity O(a) per call in the number of running animations; O(1) space.
24
24
  * @overallScore 100
25
25
  */
26
- export declare function createCssInstance(el: Element, ledger: StyleLedger, animationNames: readonly string[]): EffectInstance;
26
+ export declare function createCssInstance(el: Element, ledger: StyleLedger, animationNames: readonly string[], scrubbed?: boolean): EffectInstance;
27
27
  /**
28
28
  * What a *finite* JS primitive's setup returns instead of a bare teardown.
29
29
  *
@@ -43,8 +43,34 @@ export interface TimedSetup {
43
43
  /** Jump to the end state, for `EffectInstance.finish()`. */
44
44
  finish?: () => void;
45
45
  }
46
- /** A deferred setup's return: a teardown, or a teardown plus the effect's own completion. */
47
- export type SetupResult = Cleanup | TimedSetup;
46
+ /**
47
+ * What a setup returns when its effect genuinely never ends — a pin, a scroll progress track, a
48
+ * media scrub, a drag handler.
49
+ *
50
+ * This exists because a bare `Cleanup` is ambiguous, and the ambiguity matters. Ten setups in the
51
+ * catalog return one meaning *"there was nothing to do"* — no words to cycle, no fine pointer to
52
+ * follow, a stage that could not be built — and for those, "no completion pending" is the honest
53
+ * report. A pin returns the same shape meaning *"this will still be running an hour from now"*.
54
+ * Reading the two as one thing is what made the fix for D9 regress the other ten; saying which you
55
+ * are is cheap and the only thing that actually distinguishes them.
56
+ */
57
+ export interface ContinuousSetup {
58
+ cleanup: Cleanup;
59
+ continuous: true;
60
+ }
61
+ /**
62
+ * Mark a teardown as belonging to an effect that never ends.
63
+ *
64
+ * Wrap only the return that means the effect actually started something perpetual — leave an early
65
+ * "nothing to do" bail-out as a bare `Cleanup`, which is what keeps the two distinguishable.
66
+ *
67
+ * @param cleanup - The effect's own teardown.
68
+ * @complexity O(1) time and space.
69
+ * @overallScore 100
70
+ */
71
+ export declare function continuousSetup(cleanup: Cleanup): ContinuousSetup;
72
+ /** A deferred setup's return: a teardown, optionally with a completion or a never-ends marker. */
73
+ export type SetupResult = Cleanup | TimedSetup | ContinuousSetup;
48
74
  /**
49
75
  * Turn setup-that-also-starts into a properly gated instance.
50
76
  *
@@ -85,6 +111,11 @@ export interface JsInstanceHooks {
85
111
  * `finished` after `activate()`, never before.
86
112
  */
87
113
  finished?(): Promise<void>;
114
+ /**
115
+ * Whether this effect never ends. Read on every access, for the same reason as `finished`: a
116
+ * deferred setup only learns which kind it is once it has run. See `EffectInstance.continuous`.
117
+ */
118
+ continuous?(): boolean;
88
119
  }
89
120
  /**
90
121
  * Build a JS-rendered instance from a primitive's hooks, filling in the optional operations.
@@ -21,8 +21,27 @@ export interface Cubic {
21
21
  c2: Point;
22
22
  to: Point;
23
23
  }
24
+ /**
25
+ * One subpath: a run of segments, plus whether the author closed it.
26
+ *
27
+ * A `d` string is not one contour — `M ... Z M ... Z` is a shape with a hole, and an icon with a
28
+ * counter (the inside of an `o`, `a`, `e`) is the common case. Flattening the runs together turns
29
+ * a hole into a detour line across the glyph, so the boundary has to survive parsing.
30
+ */
31
+ export interface Subpath {
32
+ segments: Cubic[];
33
+ /**
34
+ * True when the author wrote an explicit `Z`/`z`. Kept separate from the geometry because
35
+ * `closeSubpath` already materialises the closing side as a real segment — this flag is what
36
+ * decides whether `Z` is re-emitted, which is what `fill-rule` needs to punch the hole.
37
+ */
38
+ closed: boolean;
39
+ }
24
40
  export interface ParseResult {
41
+ /** Every segment in document order, with subpath boundaries flattened away. */
25
42
  segments: Cubic[];
43
+ /** The same segments grouped by subpath. This is the form morphing needs. */
44
+ subpaths: Subpath[];
26
45
  /** Present when the path could not be normalised; the caller should warn and not morph. */
27
46
  reason?: string;
28
47
  }
@@ -7,10 +7,15 @@ import type { Activation } from './types.js';
7
7
  * How the animation is started.
8
8
  *
9
9
  * - `native-timeline` — a scroll/view timeline drives progress; nothing to start.
10
+ * - `scrubbed` — `timeline: pin`. Held paused forever and seeked by `--kui-progress` through the
11
+ * compiled `animation-delay`. Distinct from `deferred`, which is also paused but is waiting to
12
+ * be *released* by an activation: a scrubbed animation must never be released, because
13
+ * `animation-play-state: running` hands it back to the document timeline and it would play
14
+ * forward in real time instead of tracking scroll.
10
15
  * - `immediate` — runs as soon as it is applied.
11
16
  * - `deferred` — held at its from-state until an activation fires.
12
17
  */
13
- export type Gate = 'native-timeline' | 'immediate' | 'deferred';
18
+ export type Gate = 'native-timeline' | 'scrubbed' | 'immediate' | 'deferred';
14
19
  export interface StylePlan {
15
20
  /** Custom properties and animation longhands to write, in application order. */
16
21
  properties: Record<string, string>;
@@ -29,6 +29,17 @@ export declare const CHANNEL: {
29
29
  readonly color: "color";
30
30
  readonly stroke: "stroke";
31
31
  readonly text: "text";
32
+ /**
33
+ * Skew is the one transform CSS never gave an independent property to — there is no `skew:`
34
+ * beside `translate:`/`rotate:`/`scale:`, so it can only be written through the `transform`
35
+ * shorthand. That makes it its own channel: anything writing `transform` clobbers the whole
36
+ * shorthand, so every primitive that does is on this channel, whatever the transform is for.
37
+ * `scroll-skew` is one member; `flip-face` (`effects/three-d`) and `flip-3d`
38
+ * (`effects/catalog/core`, the `flip-in-*`/`flip-out-*` family) are the other two — both need
39
+ * the `perspective()` transform *function* for an element to have depth on itself, which
40
+ * likewise only exists inside `transform`.
41
+ */
42
+ readonly skew: "skew";
32
43
  };
33
44
  /**
34
45
  * A CSS property group an effect writes to. Two effects may only be composed in one `data-kui`
@@ -51,7 +62,18 @@ export type Activation = 'load' | 'enter' | 'hover' | 'focus' | 'click' | 'manua
51
62
  * *reverse when the user scrolls back*, which a time-based reveal does not. These are different
52
63
  * animation models, not fallbacks for each other. See docs/design.md §5.
53
64
  */
54
- export type Timeline = 'time' | 'view' | 'scroll' | 'pointer';
65
+ /**
66
+ * `pin` is the odd one out and deliberately so. `view`/`scroll` map to a native CSS
67
+ * `animation-timeline`; `pin` has no native equivalent because CSS has no way to say "drive this
68
+ * from another element's published progress". A pinned element's `view()` timeline *stalls* — it
69
+ * stops travelling through the viewport, so its progress freezes for the whole hold, which is
70
+ * exactly the span an author wants to animate across. `pin` fills that hole: the animation is
71
+ * held paused and seeked with a negative `animation-delay` proportional to `--kui-progress`,
72
+ * which the scroll-mechanics primitives already publish every frame. Verified in Chrome:
73
+ * progress 0/0.25/0.5/0.75/1 renders -180/-135/-90/-45/0deg on a half-turn keyframe, re-seeks
74
+ * live on every variable change, and composes with `--kui-stagger`.
75
+ */
76
+ export type Timeline = 'time' | 'view' | 'scroll' | 'pointer' | 'pin';
55
77
  export type Renderer = 'css-keyframes' | 'waapi' | 'javascript';
56
78
  /** Governs the performance budget a primitive is held to in tests. */
57
79
  export type PerfClass = 'compositor' | 'paint' | 'layout' | 'continuous' | 'dom-transform';
@@ -106,6 +128,20 @@ export interface EffectInstance {
106
128
  finish(): void;
107
129
  /** Resolves when the effect completes. Resolves — never rejects — on cancel. */
108
130
  readonly finished: Promise<void>;
131
+ /**
132
+ * Whether this effect has no end — a pin, a scroll progress track, a media scrub.
133
+ *
134
+ * Such an effect keeps an already-resolved `finished` on purpose, so that composing it with a
135
+ * one-shot on the same element does not stop the one-shot ever reporting complete. That is
136
+ * right for composition and wrong for the element's own reported state: an element whose *only*
137
+ * effects are continuous would otherwise read `data-kui-state="finished"` from the first
138
+ * microtask onwards and never say anything else, which is a lie an author cannot style around.
139
+ *
140
+ * The animator therefore reads this to decide whether there is any completion to wait for at
141
+ * all. Read after `activate()`, never before — a deferred setup only learns which kind it is
142
+ * once it has actually run.
143
+ */
144
+ readonly continuous?: boolean;
109
145
  /** Release every listener, observer, subscription, and inserted node. */
110
146
  destroy(): void;
111
147
  }
@@ -182,6 +218,43 @@ export interface Primitive {
182
218
  defaultActivation?: Activation;
183
219
  perfClass: PerfClass;
184
220
  reducedMotion: ReducedMotionPolicy;
221
+ /**
222
+ * Whether this primitive, left alone to finish with nobody calling `reset()`/`destroy()`, ends
223
+ * with the element's markup indistinguishable from what the author wrote.
224
+ *
225
+ * There are two separate teardown contracts, and `test/browser/teardown-sweep.test.mjs` used to
226
+ * conflate them into one check:
227
+ *
228
+ * - **Interruption restores.** Calling `reset()` mid-effect puts the markup back byte-for-byte.
229
+ * This is unconditional and every primitive owes it — it is not gated by this field at all, and
230
+ * never will be, because a caller can `reset()` anything at any moment.
231
+ * - **Natural finish restores** (this field). The effect, left alone to run to completion, ends
232
+ * with the markup back as authored. This is `slat-assemble`'s whole point: it disassembles a
233
+ * picture into slats and reassembles it, and the slats are scaffolding that must be gone once
234
+ * the illusion lands — not just when someone happens to call `reset()`.
235
+ *
236
+ * **Defaults to `false` — a primitive has to opt in.** The tempting default is `true` ("assume it
237
+ * restores unless told otherwise"), on the theory that a forgotten declaration should never
238
+ * silently skip a check. That is the wrong default in this codebase specifically: the majority of
239
+ * the catalog is JS/CSS-driven effects that either never resolve `finished` at all (ambient
240
+ * loops, hover/pointer effects, anything `reducedMotion: 'disable'` marks as unboundable) or
241
+ * finish *by design* in a state that differs from authored markup — `data-kui-state="finished"`
242
+ * and the `--kui-*` custom properties `createCssInstance` leaves in place are the point, not a
243
+ * bug, and every `css-keyframes` primitive in the catalog would need an explicit opt-out under a
244
+ * `true` default. Defaulting to `true` would have meant "almost every primitive needs a
245
+ * non-default declaration," which is the sign from the design notes above that a default is
246
+ * wrong. `false` means "no claim is being made" for the ordinary case, and only the small set of
247
+ * primitives that build their own temporary DOM as scaffolding — `slat-assemble` today — assert
248
+ * this explicitly. The unconditional interruption check above is what actually closes the "a new
249
+ * scaffold-building primitive forgets to opt in" gap: `reset()` is still guaranteed to restore it
250
+ * even if nobody ever wrote `restoresOnFinish: true` for it.
251
+ *
252
+ * `test/browser/teardown-sweep.test.mjs` reads this to decide whether a primitive owes a
253
+ * natural-finish check at all, and its failure messages name which law was broken instead of the
254
+ * generic "leaves synthetic nodes behind" that previously misdescribed a `style=""` leak as extra
255
+ * DOM nodes for a whole debugging session.
256
+ */
257
+ restoresOnFinish?: boolean;
185
258
  /**
186
259
  * JS-side setup. Returns a lifecycle handle, **not** a teardown function.
187
260
  *
@@ -202,6 +275,25 @@ export interface Preset {
202
275
  params?: Record<string, string>;
203
276
  /** CSS `@keyframes` name this preset animates, when renderer is `css-keyframes`. */
204
277
  keyframes?: string;
278
+ /**
279
+ * This effect begins from a state the visitor must not see — invisible, displaced, unsplit, or
280
+ * un-assembled — so painting the element at its rest state before the runtime installs that
281
+ * from-state is a visible flash. `scripts/generate-preset-css.mjs` emits a pre-JS cloak rule for
282
+ * every preset that declares it; see the `kui.cloak` layer in `src/css/base.css`.
283
+ *
284
+ * **This has to be declared, not derived.** Both plausible signals were measured and both are
285
+ * wrong. Keying on "owns the opacity channel" cloaks `hamburger-to-x`, `checkbox-draw` and
286
+ * `dropdown-open` — stateful controls with no entrance — while missing `blur-in` and `bounce-in`.
287
+ * Keying on "supports the entrance timelines" cloaks `pin-section`, `scroll-spy` and
288
+ * `horizontal-scroll`, and hiding a pinned section at opacity 0 is precisely the failure the
289
+ * opt-in rule in `base.css` was written to avoid. Whether a name is an entrance with a
290
+ * from-state worth hiding is a fact only its author knows, which is why it sits here beside
291
+ * `params` rather than being inferred from either.
292
+ *
293
+ * Exits are deliberately excluded: `fade-out` starts at the rest state, so there is nothing to
294
+ * hide and cloaking it would blank an element that should be visible until it leaves.
295
+ */
296
+ cloak?: boolean;
205
297
  }
206
298
  export type ResolvedParams = Record<string, string>;
207
299
  /** One effect segment parsed out of a `data-kui` value. */
@@ -235,5 +327,11 @@ export interface InstanceState {
235
327
  attributes: AttributeLedger;
236
328
  /** Aborted on release, detaching bindings and primitive listeners. */
237
329
  controller: AbortController;
330
+ /**
331
+ * Releases a one-shot `enter` binding, set only for that activation and cleared once spent.
332
+ * Present so `Animator.activate` can disarm an observer a programmatic activation just made
333
+ * redundant; a toggle activation (`hover`/`focus`/`click`) deliberately leaves this undefined.
334
+ */
335
+ releaseActivation?: Cleanup;
238
336
  status: 'pending' | 'ready' | 'running' | 'finished' | 'failed';
239
337
  }
@@ -0,0 +1,152 @@
1
+ import type { PrepareContext } from '../../core/effect-context.js';
2
+ import type { Cleanup } from '../../core/types.js';
3
+ /**
4
+ * Whether an authored `src` names a video platform's web page instead of a media file.
5
+ *
6
+ * @complexity O(n) time in value length; O(1) space.
7
+ * @overallScore 100
8
+ */
9
+ export declare function isVideoPageUrl(value: string): boolean;
10
+ /**
11
+ * Named focal points, as `object-position` percentage pairs.
12
+ *
13
+ * A `cover` fit crops, and which part it crops away is the whole editorial decision — the classic
14
+ * failure is a portrait cropped through the subject's chin. Percentages rather than the equivalent
15
+ * CSS keywords (`top`, `left top`) because the pair is written into one property from two halves,
16
+ * and `50% 0%` composes where `center top` would need its own keyword-order rules.
17
+ */
18
+ declare const FOCAL_POINTS: {
19
+ readonly center: "50% 50%";
20
+ readonly top: "50% 0%";
21
+ readonly bottom: "50% 100%";
22
+ readonly left: "0% 50%";
23
+ readonly right: "100% 50%";
24
+ readonly 'top-left': "0% 0%";
25
+ readonly 'top-right': "100% 0%";
26
+ readonly 'bottom-left': "0% 100%";
27
+ readonly 'bottom-right': "100% 100%";
28
+ };
29
+ export type FocalPoint = keyof typeof FOCAL_POINTS;
30
+ /** The nine named focal points, in the order the parameter schema declares them. */
31
+ export declare const FOCAL_POINT_NAMES: FocalPoint[];
32
+ /**
33
+ * Resolve a focal point name to an `object-position` value.
34
+ *
35
+ * @complexity O(1) time and space.
36
+ * @overallScore 100
37
+ */
38
+ export declare function focalPosition(focus: string): string;
39
+ /** How a background clip decides when to run. */
40
+ export type AutoplayMode = 'in-view' | 'always' | 'never';
41
+ export interface BackgroundMediaOptions {
42
+ /** URL or path to the image or video, already screened by `mediaSource`. */
43
+ src: string;
44
+ /** Still shown before a video's first frame; `''` for none. */
45
+ poster: string;
46
+ /** `object-fit` for the layer. */
47
+ fit: 'cover' | 'contain';
48
+ /** `object-position` for the layer, already resolved from a named focal point. */
49
+ position: string;
50
+ /** Scrim colour painted over the media and under the author's children. */
51
+ overlay: string;
52
+ /** Scrim opacity, `0`–`1`. */
53
+ overlayOpacity: number;
54
+ /** When a clip plays. Ignored for an image. */
55
+ autoplay: AutoplayMode;
56
+ /** `playbackRate` for a clip. Ignored for an image. */
57
+ rate: number;
58
+ /** Whether a clip restarts when it ends. Ignored for an image. */
59
+ loop: boolean;
60
+ /** Suppresses autoplay, leaving the poster frame standing. */
61
+ reducedMotion: boolean;
62
+ }
63
+ export interface BackgroundLayer {
64
+ /** Stop observing and delete every node this effect added, leaving the author's own as written. */
65
+ remove: Cleanup;
66
+ }
67
+ /**
68
+ * Whether a scrim colour is actually asking for a scrim.
69
+ *
70
+ * `transparent` is the default so that the parameter stays a real `type: 'color'` — validated by
71
+ * the same screen every other colour goes through — rather than an empty string that no colour
72
+ * grammar accepts. It also happens to be the honest spelling of "no scrim", so an author who
73
+ * writes it explicitly gets exactly what the default gives them.
74
+ *
75
+ * @complexity O(1) time and space.
76
+ * @overallScore 100
77
+ */
78
+ export declare function paintsOverlay(options: BackgroundMediaOptions): boolean;
79
+ /**
80
+ * Whether an authored `src` names a video rather than an image.
81
+ *
82
+ * @param src - Author-supplied path, already accepted as same-origin.
83
+ * @returns `true` when the path's final extension is a known video container.
84
+ * @complexity O(n) time in path length; O(1) space.
85
+ * @overallScore 100
86
+ */
87
+ export declare function isVideoSource(src: string): boolean;
88
+ /**
89
+ * Screen an authored media URL before it can reach `src`/`poster`.
90
+ *
91
+ * **Cross-origin URLs are allowed here, unlike `media-scrub`'s frame pattern, and that is a
92
+ * deliberate divergence rather than an oversight.** `isSameOriginPath` (`core/params.ts`) refuses
93
+ * every absolute URL on the grounds that `data-kui` is not always written by the site owner — a CMS
94
+ * field, a comment — so an unconstrained `src` turns the visitor's browser into an
95
+ * attacker-directed request tool. That reasoning still stands, and it is why the scheme allowlist
96
+ * above is kept tight.
97
+ *
98
+ * It is not the right trade for *this* parameter, though. A background is the one media case where
99
+ * the file routinely does not live on the page's own origin — an S3 bucket, a CDN, an asset host —
100
+ * and `media-scrub`'s `src:` is materially more dangerous than this one for a second reason: it is
101
+ * a `{i}` *template* that fires one request per frame, so a two-hundred-frame sequence is a
102
+ * two-hundred-request channel where this is a single fixed fetch. A site that does accept untrusted
103
+ * `data-kui` should set a `Content-Security-Policy` with `img-src`/`media-src`, which is the
104
+ * control actually designed for this and the one thing a library cannot do on the consumer's
105
+ * behalf.
106
+ *
107
+ * @param authored - Author-supplied value; `''` when none was written.
108
+ * @param name - Parameter name, for the diagnostic.
109
+ * @returns The URL, or `''` when it was rejected or absent.
110
+ * @complexity O(n) time in value length; O(1) space.
111
+ * @overallScore 100
112
+ */
113
+ export declare function mediaSource(authored: string, name: string, ctx: PrepareContext): string;
114
+ /**
115
+ * Apply the authored playback policy to a clip.
116
+ *
117
+ * `autoplay:` is the opt-out the three real cases need. `in-view` (default) pairs the clip with the
118
+ * viewport. `always` is for a short hero clip that must never be caught mid-stall by a visibility
119
+ * heuristic. `never` installs the clip and leaves it on its poster, which is also what any mode
120
+ * degrades to under a reduced-motion preference.
121
+ *
122
+ * @returns A teardown that stops any observer and leaves the clip paused.
123
+ * @complexity O(1) time and space.
124
+ * @overallScore 100
125
+ */
126
+ export declare function startPlayback(video: HTMLVideoElement, win: Window, options: BackgroundMediaOptions): Cleanup;
127
+ /**
128
+ * Play a background clip while any part of it is on screen, and pause it the rest of the time.
129
+ *
130
+ * This is the primitive's own observer rather than the animator's `on:enter` gate, because the two
131
+ * answer different questions: `on:enter` fires once and stays fired, which is right for a reveal
132
+ * and wrong for a clip that should stop costing decode budget the moment it scrolls away.
133
+ *
134
+ * `threshold: 0` — any intersection at all — rather than the `0.25` a card-sized video wants.
135
+ * `intersectionRatio` is measured against the *target*, and this target is usually a full section:
136
+ * one taller than four viewports can never reach a ratio of 0.25 no matter where it is scrolled,
137
+ * so a fractional threshold is a clip that silently never plays.
138
+ *
139
+ * @returns A teardown that disconnects the observer and leaves the clip paused.
140
+ * @complexity O(1) time and space.
141
+ * @overallScore 100
142
+ */
143
+ export declare function autoplayInView(video: HTMLVideoElement, win: Window): Cleanup;
144
+ /**
145
+ * Create the backdrop layer and hand back the handle that deletes it again.
146
+ *
147
+ * @param el - The host element, which keeps every child the author wrote.
148
+ * @complexity O(1) time and space.
149
+ * @overallScore 100
150
+ */
151
+ export declare function installBackgroundMedia(el: Element, ctx: PrepareContext, options: BackgroundMediaOptions): BackgroundLayer;
152
+ export {};
@@ -1,10 +1,4 @@
1
1
  import type { Registry } from '../../core/registry.js';
2
- export { AMBIENT_PRESETS, AMBIENT_PRIMITIVES } from './ambient.js';
3
- export { FEEDBACK_PRESETS, FEEDBACK_PRIMITIVES } from './feedback.js';
4
- export { INTERACTION_PRESETS, INTERACTION_PRIMITIVES } from './interaction.js';
5
- export { MEDIA_PRESETS, MEDIA_PRIMITIVES } from './media.js';
6
- export { NUMBERS_PRESETS, NUMBERS_PRIMITIVES } from './numbers.js';
7
- export { TEXT_PRESETS, TEXT_PRIMITIVES } from './text.js';
8
2
  /**
9
3
  * Register the CSS-oriented catalog sections.
10
4
  *
@@ -0,0 +1,124 @@
1
+ import type { Cleanup, EffectParams } from '../../core/types.js';
2
+ /**
3
+ * DOM-surgery helpers behind `slat-assemble` (catalog section G).
4
+ *
5
+ * `text-shared.ts` is the template this follows: build a synthetic child tree once at activation,
6
+ * index each child with a plain custom property, and let CSS (`media.css`) own every frame from
7
+ * there — this module never animates anything itself, it only builds the tree and computes the
8
+ * per-child numbers CSS reads.
9
+ */
10
+ /** Which way the image is cut into strips. `angle:` generalises this to any degree. */
11
+ export type SlatAxis = 'vertical' | 'horizontal';
12
+ /** How the per-slat stagger order relates to a slat's geometric position. */
13
+ export type SlatFrom = 'alternate' | 'start' | 'end' | 'edges' | 'random-ish';
14
+ /**
15
+ * Map a slat's geometric position to its stagger rank.
16
+ *
17
+ * The rank is what `--kui-i` carries into `media.css`'s `animation-delay: calc(delay + i *
18
+ * stagger)` — the same formula `text.css` already uses for `.kui-split-item` — so `from:` only
19
+ * ever changes which number lands in that one slot, never the delay formula itself. Ties are
20
+ * fine: two slats sharing a rank simply animate on the same beat, which is a legitimate look
21
+ * (`edges`'s two starting slats are meant to move together), not a bug.
22
+ *
23
+ * - `start` — geometric order, left-to-right or top-to-bottom.
24
+ * - `end` — the reverse.
25
+ * - `edges` — outside-in: both ends rank 0, the middle ranks highest, so the seam closes last.
26
+ * - `alternate` (default) — a zig-zag across the whole run (0, N-1, 1, N-2, 2, …), so slats land
27
+ * from alternating ends instead of sweeping one direction.
28
+ * - `random-ish` — a deterministic scatter (see `GOLDEN_RATIO_CONJUGATE`), not real randomness.
29
+ *
30
+ * @complexity O(1) time and space.
31
+ * @overallScore 100
32
+ */
33
+ export declare function slatOrder(index: number, count: number, from: SlatFrom): number;
34
+ /**
35
+ * Resolve the authored `angle:` to degrees, falling back to whatever `axis:` names.
36
+ *
37
+ * `angle:` is the general parameter and `axis:` the two-keyword shorthand that predates it, so an
38
+ * authored angle wins and an absent one reads the axis. Both `45` and `45deg` are accepted, as are
39
+ * `turn` and `rad`, because an author who writes an angle should not have to remember which of the
40
+ * three this particular parameter takes.
41
+ *
42
+ * Normalised to `[0, 180)`: a band at 200° is the same set of bands as one at 20°, only numbered
43
+ * from the other end, and collapsing that here means the geometry below never has to think about
44
+ * sign or wrap.
45
+ *
46
+ * @complexity O(1) time and space.
47
+ * @overallScore 100
48
+ */
49
+ export declare function slatAngleDegrees(authored: string, axis: SlatAxis): number;
50
+ /**
51
+ * The unit vector each slat travels along in its from-state: the band's own long axis.
52
+ *
53
+ * At 0° this is `(0, 1)` — vertical columns sliding up and down, which is exactly what the two
54
+ * hand-written axis keyframes did before this generalised them. At 90° it is `(-1, 0)`, horizontal
55
+ * rows sliding sideways. Perpendicular travel is deliberately not offered: a band moving across its
56
+ * own width exposes the gap it left, and the point of the effect is strips that are *out of line*,
57
+ * not strips that are missing.
58
+ *
59
+ * @complexity O(1) time and space.
60
+ * @overallScore 100
61
+ */
62
+ export declare function slatTravelVector(angleDegrees: number): {
63
+ x: number;
64
+ y: number;
65
+ };
66
+ /**
67
+ * The `clip-path` for one band of an angled slice, in pixels against the stage's measured box.
68
+ *
69
+ * Pixels, not percentages, on purpose. A percentage polygon is resolved against each axis
70
+ * separately, so "45°" on a 3:4 card would paint at some other angle entirely — the author asked
71
+ * for a diagonal and would get a diagonal that changes as the card resizes. Measuring means the
72
+ * angle is the angle, at any aspect ratio, and the caller re-runs this whenever the box changes.
73
+ *
74
+ * The band is the region between two parallel lines perpendicular to `angleDegrees`, extended far
75
+ * enough sideways to cover the stage from any rotation; `overflow: clip` on the stage trims the
76
+ * overhang. Each boundary is pushed out by {@link BAND_OVERLAP_PX} for the same reason the axis
77
+ * version added a pixel to its width: `span / count` almost never lands on a whole pixel, and the
78
+ * remainder renders as a hairline of backdrop between neighbours once they have landed.
79
+ *
80
+ * @param index - Band number, `0` to `count - 1`, in geometric order along the cut normal.
81
+ * @param box - The stage's measured pixel size, re-read by the caller on every layout change.
82
+ * @complexity O(1) time and space.
83
+ * @overallScore 100
84
+ */
85
+ export declare function slatBandClip(index: number, count: number, angleDegrees: number, box: {
86
+ width: number;
87
+ height: number;
88
+ }): string;
89
+ export interface SlatStage {
90
+ /** The `aria-hidden` wrapper holding every slat, positioned over the source `<img>`. */
91
+ stage: HTMLElement;
92
+ /** One element per slat, in geometric order. */
93
+ slats: HTMLElement[];
94
+ /** Remove every generated node and stop watching the image's box. The source `<img>` was never modified, so nothing to restore on it. */
95
+ restore: Cleanup;
96
+ }
97
+ /** Validated build options for {@link installSlatStage}, grouped to keep its own signature small. */
98
+ export interface SlatBuildOptions {
99
+ count: number;
100
+ /** Resolved cut angle in degrees, `[0, 180)`. See {@link slatAngleDegrees}. */
101
+ angleDegrees: number;
102
+ from: SlatFrom;
103
+ fold: boolean;
104
+ }
105
+ export declare function installSlatStage(el: Element, doc: Document, win: Window, options: SlatBuildOptions): SlatStage | null;
106
+ /**
107
+ * Forward validated timing params onto the stage, so `media.css`'s `--kui-i * --kui-stagger`
108
+ * delay formula (the same shape `text-shared.ts`'s `applyStaggerVars` writes for split-text) can
109
+ * read them by inheritance into every `.kui-slat-item`.
110
+ *
111
+ * @complexity O(1) time and space.
112
+ * @overallScore 100
113
+ */
114
+ export declare function applySlatTimingVars(stage: HTMLElement, params: EffectParams): void;
115
+ /**
116
+ * Total time before every staggered slat has finished landing: the last slat's `--kui-i *
117
+ * --kui-stagger` delay, plus its own duration — `splitRevealFinishMs`'s reasoning, over slat
118
+ * count instead of grapheme/word/line count.
119
+ *
120
+ * @param count - Slat count; zero means nothing was ever built, so nothing is animating.
121
+ * @complexity O(1) time and space.
122
+ * @overallScore 100
123
+ */
124
+ export declare function slatAssembleFinishMs(params: EffectParams, count: number): number;
@@ -1,5 +1,9 @@
1
1
  import type { Preset, Primitive } from '../../core/types.js';
2
2
  import type { Registry } from '../../core/registry.js';
3
+ export declare const MEDIA_CSS_PRIMITIVES: Primitive[];
4
+ export declare const MEDIA_CSS_PRESETS: Preset[];
5
+ export declare const MEDIA_JS_PRIMITIVES: Primitive[];
6
+ export declare const MEDIA_JS_PRESETS: Preset[];
3
7
  export declare const MEDIA_PRIMITIVES: Primitive[];
4
8
  export declare const MEDIA_PRESETS: Preset[];
5
9
  /**
@@ -14,7 +14,11 @@ export interface CountLayers {
14
14
  * mid-tick — so a screen reader is told the final value and nothing in between.
15
15
  */
16
16
  srOnly: HTMLElement;
17
- /** Remove both layers, restoring the element to plain text of whatever the SR layer last held. */
17
+ /**
18
+ * Remove both layers, putting the authored children back as they were. Not the value the
19
+ * counter reached: that number is the library's, and teardown gives the element back to the
20
+ * author — including the `42` that was there as a no-JS fallback.
21
+ */
18
22
  restore: Cleanup;
19
23
  }
20
24
  /**
@@ -1,3 +1,2 @@
1
- /** Re-exported for same-directory catalog siblings; the canonical definition lives in `effects/shared.ts` — neutral ground for every CSS-keyframe category, not just `catalog/`. */
2
- export { cssPrimitive } from '../shared.js';
3
- export type { CssPrimitiveOptions } from '../shared.js';
1
+ /** Re-exported for same-directory catalog siblings; the canonical definitions live in `effects/shared.ts` — neutral ground for every effect category, not just `catalog/`. */
2
+ export { cssPrimitive, TIMELINE_AGNOSTIC, TRIGGER_DELAY_PARAM } from '../shared.js';
@@ -0,0 +1,23 @@
1
+ import type { Cleanup } from '../../core/types.js';
2
+ /**
3
+ * Snapshot an element's children so teardown can put back exactly what the author wrote.
4
+ *
5
+ * Every DOM-rewriting primitive in this folder used to restore by assigning `textContent`, which
6
+ * is faithful only when the authored content was plain text — the one case every restore test
7
+ * happened to author. Given `<p><strong>one</strong> two</p>` it flattened the `<strong>`; given
8
+ * `<span>42</span>` a counter wrote back the value it had counted to instead of the author's own
9
+ * content. Both are invisible on a page that never tears down, and both destroy author markup in
10
+ * any app that unmounts.
11
+ *
12
+ * The captured nodes are the live originals, merely detached by the caller's own overwrite.
13
+ * Restoring re-inserts those same node objects, so nested structure, attributes, and any listeners
14
+ * bound to them survive — none of which would come back from re-parsing a string.
15
+ *
16
+ * Capture before overwriting: this reads the children as they stand when it is called.
17
+ *
18
+ * @param el - Element whose children are about to be replaced.
19
+ * @returns A cleanup that discards whatever is there now and reinstates the captured children.
20
+ * @complexity O(n) time and space in child-node count.
21
+ * @overallScore 100
22
+ */
23
+ export declare function captureChildren(el: Element): Cleanup;
@@ -4,9 +4,12 @@ export type SplitUnit = 'chars' | 'words' | 'lines';
4
4
  export interface SplitLayers {
5
5
  /** `aria-hidden` container the caller populates with decorative markup or text. */
6
6
  decorative: HTMLElement;
7
- /** The element's text at the moment splitting began. */
7
+ /** The element's text at the moment splitting began, trimmed for display. */
8
8
  originalText: string;
9
- /** Remove both layers and put the original text back, selectable and unabridged. */
9
+ /**
10
+ * Remove both layers and put the authored children back exactly as they were — element nodes
11
+ * and untrimmed whitespace included, not the trimmed text `originalText` renders.
12
+ */
10
13
  restore: Cleanup;
11
14
  }
12
15
  /**
@@ -114,6 +117,12 @@ export declare function createStepRunner(win: Window, options: StepRunOptions):
114
117
  * wrapping the space itself, the same way `appendWordSpans` already leaves inter-word whitespace
115
118
  * unwrapped.
116
119
  *
120
+ * Per the CSS Text line-breaking spec, browsers insert a line-break opportunity between any two
121
+ * adjacent atomic inline-level boxes, even without whitespace between them — so a bare run of
122
+ * `.kui-split-item` spans could wrap mid-word. Each word's char-spans are nested inside a shared
123
+ * `.kui-split-word` wrapper (itself `display: inline-block`) so the only break opportunities left
124
+ * are between words, same as the source text.
125
+ *
117
126
  * @complexity O(n) time and space in grapheme count.
118
127
  * @overallScore 100
119
128
  */
@@ -1,6 +1,5 @@
1
1
  import type { Preset } from '../../core/types.js';
2
2
  import type { Registry } from '../../core/registry.js';
3
- export { GESTURE_PRIMITIVES } from './primitives.js';
4
3
  /**
5
4
  * Gesture names. Twelve names over four primitives.
6
5
  *
@@ -1,6 +1,4 @@
1
1
  import type { Registry } from '../../core/registry.js';
2
- export { LAYOUT_PRIMITIVES } from './primitives.js';
3
- export { LAYOUT_PRESETS } from './presets.js';
4
2
  /**
5
3
  * Register the layout/FLIP catalog.
6
4
  *
@@ -1,8 +1,4 @@
1
1
  import type { Registry } from '../../core/registry.js';
2
- export { SCROLL_PRIMITIVES } from './primitives.js';
3
- export { SCROLL_PRESETS } from './presets.js';
4
- export { domGeometry, progressFrom, trackProgress } from './tracker.js';
5
- export type { ElementGeometry, Measurer, TrackOptions } from './tracker.js';
6
2
  /**
7
3
  * Register the scroll-mechanics catalog.
8
4
  *