kuinetic 0.1.3 → 0.2.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 (125) hide show
  1. package/README.md +93 -10
  2. package/dist/esm/advanced/index.mjs +4727 -0
  3. package/dist/esm/chunk-AEQGC2KM.mjs +189 -0
  4. package/dist/esm/chunk-MQSNMYRP.mjs +9332 -0
  5. package/dist/esm/chunk-RVTYN6NZ.mjs +3446 -0
  6. package/dist/esm/chunk-U43BX5O2.mjs +2984 -0
  7. package/dist/esm/core/index.mjs +16 -9
  8. package/dist/esm/effects/index.mjs +19 -4
  9. package/dist/esm/index.mjs +17 -10
  10. package/dist/kuinetic.advanced.js +5070 -0
  11. package/dist/kuinetic.advanced.min.js +749 -0
  12. package/dist/kuinetic.all.js +15218 -5595
  13. package/dist/kuinetic.all.min.js +2 -0
  14. package/dist/kuinetic.css +2900 -360
  15. package/dist/kuinetic.js +15217 -5593
  16. package/dist/kuinetic.min.css +1 -0
  17. package/dist/kuinetic.min.js +7 -0
  18. package/dist/types/3d/angles.d.ts +30 -0
  19. package/dist/types/3d/flattening.d.ts +49 -0
  20. package/dist/types/3d/index.d.ts +36 -0
  21. package/dist/types/3d/model-3d.d.ts +115 -0
  22. package/dist/types/3d/register.d.ts +26 -0
  23. package/dist/types/3d/webgl.d.ts +24 -0
  24. package/dist/types/advanced/audio.d.ts +322 -0
  25. package/dist/types/advanced/base.d.ts +233 -0
  26. package/dist/types/advanced/camera-3d.d.ts +236 -0
  27. package/dist/types/advanced/fluid-cursor.d.ts +149 -0
  28. package/dist/types/advanced/gl-utils.d.ts +52 -0
  29. package/dist/types/advanced/glsl.d.ts +60 -0
  30. package/dist/types/advanced/index.d.ts +23 -0
  31. package/dist/types/advanced/particles.d.ts +108 -0
  32. package/dist/types/advanced/scenes.d.ts +207 -0
  33. package/dist/types/advanced/shaders.d.ts +972 -0
  34. package/dist/types/browser/boot.d.ts +72 -0
  35. package/dist/types/core/activation.d.ts +227 -2
  36. package/dist/types/core/animator.d.ts +446 -11
  37. package/dist/types/core/attrs.d.ts +9 -0
  38. package/dist/types/core/breakpoints.d.ts +260 -0
  39. package/dist/types/core/bundles.d.ts +71 -0
  40. package/dist/types/core/callback.d.ts +64 -0
  41. package/dist/types/core/capabilities.d.ts +47 -0
  42. package/dist/types/core/channels.d.ts +120 -2
  43. package/dist/types/core/cloak-selectors.d.ts +47 -0
  44. package/dist/types/core/compile.d.ts +207 -2
  45. package/dist/types/core/control.d.ts +218 -0
  46. package/dist/types/core/declarations.d.ts +128 -0
  47. package/dist/types/core/derived/aggregate.d.ts +43 -0
  48. package/dist/types/core/derived/book.d.ts +30 -0
  49. package/dist/types/core/derived/diagnostics.d.ts +51 -0
  50. package/dist/types/core/derived/group-gate.d.ts +62 -0
  51. package/dist/types/core/derived/install.d.ts +110 -0
  52. package/dist/types/core/derived/late-matches.d.ts +19 -0
  53. package/dist/types/core/derived/types.d.ts +108 -0
  54. package/dist/types/core/easing.d.ts +57 -0
  55. package/dist/types/core/element-config.d.ts +47 -6
  56. package/dist/types/core/element-size.d.ts +23 -0
  57. package/dist/types/core/event-sources.d.ts +39 -0
  58. package/dist/types/core/flip.d.ts +34 -1
  59. package/dist/types/core/gesture.d.ts +12 -0
  60. package/dist/types/core/host-facts.d.ts +155 -0
  61. package/dist/types/core/index.d.ts +7 -3
  62. package/dist/types/core/owned-styles.d.ts +129 -2
  63. package/dist/types/core/params.d.ts +48 -0
  64. package/dist/types/core/parse.d.ts +28 -1
  65. package/dist/types/core/register-into.d.ts +29 -0
  66. package/dist/types/core/registry.d.ts +10 -3
  67. package/dist/types/core/repeat.d.ts +91 -0
  68. package/dist/types/core/sequence.d.ts +126 -0
  69. package/dist/types/core/stagger-config.d.ts +199 -0
  70. package/dist/types/core/stagger-keys.d.ts +63 -0
  71. package/dist/types/core/stagger.d.ts +187 -6
  72. package/dist/types/core/style-plan.d.ts +21 -16
  73. package/dist/types/core/target.d.ts +128 -0
  74. package/dist/types/core/threshold-reachability.d.ts +45 -0
  75. package/dist/types/core/time-scale.d.ts +9 -0
  76. package/dist/types/core/toggle-actions.d.ts +141 -0
  77. package/dist/types/core/travel.d.ts +59 -0
  78. package/dist/types/core/types.d.ts +705 -13
  79. package/dist/types/core/unquoted-selectors.d.ts +53 -0
  80. package/dist/types/effects/carousel/drag.d.ts +80 -0
  81. package/dist/types/effects/carousel/index.d.ts +149 -0
  82. package/dist/types/effects/catalog/background-media.d.ts +152 -0
  83. package/dist/types/effects/catalog/discrete.d.ts +13 -0
  84. package/dist/types/effects/catalog/interaction-proximity.d.ts +24 -0
  85. package/dist/types/effects/catalog/interaction-reveal.d.ts +248 -0
  86. package/dist/types/effects/catalog/interaction-shared.d.ts +42 -0
  87. package/dist/types/effects/catalog/interaction-states.d.ts +68 -0
  88. package/dist/types/effects/catalog/materials.d.ts +67 -0
  89. package/dist/types/effects/catalog/media-shared.d.ts +3 -2
  90. package/dist/types/effects/catalog/numbers-shared.d.ts +8 -5
  91. package/dist/types/effects/catalog/shared.d.ts +2 -2
  92. package/dist/types/effects/catalog/text-shared.d.ts +92 -10
  93. package/dist/types/effects/catalog/transforms.d.ts +13 -0
  94. package/dist/types/effects/catalog/view-transitions.d.ts +13 -0
  95. package/dist/types/effects/forms/primitives.d.ts +15 -4
  96. package/dist/types/effects/gestures/index.d.ts +53 -0
  97. package/dist/types/effects/index.d.ts +5 -0
  98. package/dist/types/effects/layout/presets.d.ts +35 -0
  99. package/dist/types/effects/motion-path/index.d.ts +35 -0
  100. package/dist/types/effects/scroll-mechanics/params.d.ts +23 -0
  101. package/dist/types/effects/scroll-mechanics/presets.d.ts +51 -0
  102. package/dist/types/effects/scroll-mechanics/section-index.d.ts +47 -0
  103. package/dist/types/effects/shared.d.ts +162 -2
  104. package/dist/types/effects/step-index.d.ts +121 -0
  105. package/dist/types/effects/step-marking.d.ts +41 -33
  106. package/dist/types/effects/svg/icon-parts.d.ts +23 -0
  107. package/dist/types/effects/three-d/flip-parts.d.ts +46 -0
  108. package/dist/types/effects/tween/index.d.ts +57 -0
  109. package/dist/types/effects/tween/properties.d.ts +107 -0
  110. package/dist/types/effects/tween/waypoints.d.ts +106 -0
  111. package/dist/types/showcase/compare.d.ts +4 -0
  112. package/dist/types/showcase/device-frame.d.ts +3 -0
  113. package/dist/types/showcase/hotspots.d.ts +3 -0
  114. package/dist/types/showcase/index.d.ts +30 -0
  115. package/dist/types/showcase/lightbox.d.ts +3 -0
  116. package/dist/types/showcase/media-source.d.ts +13 -0
  117. package/dist/types/showcase/modal-shell.d.ts +22 -0
  118. package/dist/types/showcase/scroll-story.d.ts +4 -0
  119. package/dist/types/showcase/shared.d.ts +47 -0
  120. package/dist/types/showcase/slideshow.d.ts +3 -0
  121. package/dist/types/showcase/slow-mo.d.ts +5 -0
  122. package/package.json +14 -7
  123. package/dist/esm/chunk-5DON3UFQ.mjs +0 -4380
  124. package/dist/esm/chunk-JT4PZL3A.mjs +0 -773
  125. package/dist/esm/chunk-QUVFODSQ.mjs +0 -1278
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Viewport gates — the `above:` / `below:` keys in the `data-kui` grammar.
3
+ *
4
+ * The gap this closes: there was no way to say "fade-up on desktop, nothing on mobile". `matchMedia`
5
+ * appeared exactly once in the whole source (`capabilities.ts`, for `prefers-reduced-motion`), and
6
+ * GSAP's answer — `gsap.matchMedia()` — is a runtime that also has to tear an animation down when a
7
+ * breakpoint changes underneath it.
8
+ *
9
+ * **A breakpoint is a media query, so the gate is compiled into CSS rather than evaluated in JS.**
10
+ * `gatedAnimationName` wraps a track's keyframe ident in a `var()` whose custom property is
11
+ * declared `none` outside the gate and guaranteed-invalid inside it (see the `kui.tokens` block in
12
+ * `base.css`). The browser re-resolves that on every resize with no script running, so there is
13
+ * nothing to observe, nothing to tear down, and nothing to get out of sync — the whole class of
14
+ * problem `gsap.matchMedia()` exists to manage simply does not arise. The cost is that the
15
+ * vocabulary has to be *closed*: a static stylesheet cannot hold a width the author invents, so
16
+ * `above:900px` is refused and only the five names below are accepted.
17
+ *
18
+ * ## Why these five names
19
+ *
20
+ * They are Tailwind v4's default scale, unchanged. This repo already builds `demo/tailwind.css`
21
+ * from its own `tailwindcss` dependency and every demo page authors against `sm`/`md`/`lg`, so
22
+ * adopting that scale means the library is matching a scale the project already uses rather than
23
+ * inventing a sixth one it would then have to own forever. `rem` rather than `px` for the reason
24
+ * Tailwind moved: the breakpoints then track the visitor's root font size, so a reader who has
25
+ * scaled their text up crosses into the "narrow" treatment at the point their layout actually
26
+ * reflows.
27
+ *
28
+ * ## Why `above` is inclusive and `below` is not
29
+ *
30
+ * `above:md` is `width >= md` and `below:md` is `width < md`, so the pair tiles the axis with
31
+ * neither a gap nor an overlap: `data-kui="fade-up below:md, parallax above:md"` runs exactly one
32
+ * of the two at every possible viewport width, including at exactly 768px. Splitting the boundary
33
+ * any other way makes one width either run both effects or run none, which is the classic
34
+ * `min-width`/`max-width` off-by-one — and is why this does *not* spell itself `min:`/`max:`, since
35
+ * CSS's own `max-width` is inclusive and would reintroduce it.
36
+ *
37
+ * ## Why `min-width` and not the range syntax
38
+ *
39
+ * `(width >= 48rem)` is Media Queries 4 and reads better, but a browser that does not parse it
40
+ * drops the whole block — which here means `--kui-above-md` keeps its `none` default and every
41
+ * `above:` gate on the page is silently off forever. `(min-width: 48rem)` has no such cliff. The
42
+ * shipped stylesheet and this module must agree exactly, so both use the same spelling.
43
+ */
44
+ /**
45
+ * The scale. Ordered narrowest to widest — `breakpointRank` depends on the declaration order, and
46
+ * `parse.ts` uses that ordering to refuse a band that can never match.
47
+ */
48
+ export declare const BREAKPOINTS: {
49
+ readonly sm: "40rem";
50
+ readonly md: "48rem";
51
+ readonly lg: "64rem";
52
+ readonly xl: "80rem";
53
+ readonly '2xl': "96rem";
54
+ };
55
+ export type Breakpoint = keyof typeof BREAKPOINTS;
56
+ /** In scale order, for `breakpointRank` and for naming the accepted values in a diagnostic. */
57
+ export declare const BREAKPOINT_NAMES: Breakpoint[];
58
+ /**
59
+ * A segment's condition, lifted onto the spec by `parse.ts`. Two independent axes:
60
+ *
61
+ * - `above`/`below` — the **viewport** condition, `data-kui="fade-up above:md"`. Compiled into
62
+ * `:root`-scoped custom properties in `base.css` (see `gatedAnimationName`), so the browser
63
+ * re-decides on every resize with no script involved.
64
+ * - `wide`/`narrow` — the **container** condition, `data-kui="fade-up wide:md"`, decided against
65
+ * the element's nearest `data-kui-container` ancestor rather than the viewport. Neither axis
66
+ * subsumes the other: `above:md` answers "is the window at least 768px", `wide:md` answers "is
67
+ * the box I sit in at least 768px", and a component reused at more than one width on the same
68
+ * page needs the second question. `wide`/`narrow` compile the same way `above`/`below` do
69
+ * (`gatedAnimationName` wraps both axes), with one deliberate asymmetry in their CSS
70
+ * defaults — see the `@container` block in `base.css` for why.
71
+ *
72
+ * All four keys may be present at once, each pair forming a band the same way: `above:md
73
+ * below:xl` runs only between the two, and `wide:md narrow:xl` does the same against the
74
+ * container. That falls out of the CSS for free — the `var()`s nest arbitrarily deep — so bands
75
+ * are supported rather than refused on either axis.
76
+ */
77
+ export interface EffectGate {
78
+ above?: Breakpoint;
79
+ below?: Breakpoint;
80
+ wide?: Breakpoint;
81
+ narrow?: Breakpoint;
82
+ }
83
+ export type GateDirection = keyof EffectGate;
84
+ /**
85
+ * The other direction on this one's axis — `above` pairs with `below`, `wide` pairs with `narrow`.
86
+ * Used wherever a check has to reason about a *band* rather than a single direction, so a new axis
87
+ * only ever needs an entry in {@link GATE_AXES} rather than a new branch at every call site.
88
+ *
89
+ * @complexity O(1) time (two axes, checked in a fixed order); O(1) space.
90
+ * @overallScore 100
91
+ */
92
+ export declare function axisOf(direction: GateDirection): readonly [GateDirection, GateDirection];
93
+ /**
94
+ * Whether a token names a breakpoint on the scale.
95
+ *
96
+ * `Object.hasOwn`, not `value in BREAKPOINTS` and not a truthiness test on the lookup: the value
97
+ * comes straight off an author-written attribute, so `above:constructor` and `above:__proto__`
98
+ * both resolve to an inherited value on `Object.prototype` and would be accepted as breakpoints.
99
+ * The same trap `parse.ts` documents on its `HOISTS` table.
100
+ *
101
+ * @complexity O(1) time and space.
102
+ * @overallScore 100
103
+ */
104
+ export declare function isBreakpoint(value: string): value is Breakpoint;
105
+ /**
106
+ * Position on the scale, narrowest first.
107
+ *
108
+ * @complexity O(b) time in the scale's length — five; O(1) space.
109
+ * @overallScore 100
110
+ */
111
+ export declare function breakpointRank(name: Breakpoint): number;
112
+ /**
113
+ * The custom property one direction/breakpoint pair is switched by. Shared by both axes — `above`/
114
+ * `below` name a `@media`-declared switch, `wide`/`narrow` name an `@container`-declared one — the
115
+ * spelling is identical either way, so one function serves both rather than a `containerProperty`
116
+ * duplicate of it.
117
+ *
118
+ * This name is a contract with `src/css/base.css`, which declares all twenty (ten per axis).
119
+ * Changing the spelling here without changing it there compiles a `var()` that resolves to
120
+ * nothing, and a `var()` that resolves to nothing falls through to its fallback — so every gate
121
+ * would silently be *on* everywhere, which is the failure a reader would be least likely to
122
+ * notice. `test/breakpoints.test.ts` asserts the two agree.
123
+ *
124
+ * @complexity O(1) time and space.
125
+ * @overallScore 100
126
+ */
127
+ export declare function gateProperty(direction: GateDirection, breakpoint: Breakpoint): string;
128
+ /**
129
+ * The media query one breakpoint is switched at. Deliberately only ever `min-width`: the `below`
130
+ * half is the *negation* of this same query rather than a `max-width` of its own, so the two halves
131
+ * cannot drift apart at the boundary. See the header note on inclusivity.
132
+ *
133
+ * @complexity O(1) time and space.
134
+ * @overallScore 100
135
+ */
136
+ export declare function breakpointQuery(breakpoint: Breakpoint): string;
137
+ /**
138
+ * A track's `animation-name` value, gated.
139
+ *
140
+ * `kui-in-up` becomes `var(--kui-above-md, kui-in-up)`. Outside the gate that property holds
141
+ * `none`, so the track is a named-nothing entry in the composed `animation-name` list: its
142
+ * duration, delay, easing and fill-mode entries all still sit at the same index and are simply
143
+ * ignored, which is what keeps a *composed* element's parallel longhand lists aligned. Inside the
144
+ * gate the property is the guaranteed-invalid value and `var()` falls through to the real ident.
145
+ *
146
+ * A gated-off track leaves nothing behind: with no animation there is no `animation-fill-mode:
147
+ * both` holding a from-state, so the element paints at its ordinary rest state. That is the
148
+ * property that makes "nothing on mobile" safe — the zero-area presets that have historically
149
+ * stranded elements did so through a from-state that never got released, and there is no
150
+ * from-state here to release.
151
+ *
152
+ * A band nests the two, and the order matters only for readability: `above` outermost reads as
153
+ * "not wide enough → off; otherwise, too wide → off; otherwise run". `wide`/`narrow` nest the same
154
+ * way, innermost, so all four conditions the author wrote — viewport band and container band
155
+ * together — AND together through the same chain of `var()` fallbacks: any one of them resolving
156
+ * to `none` neutralises the whole expression, regardless of which axis it came from.
157
+ *
158
+ * @complexity O(1) time and space.
159
+ * @overallScore 100
160
+ */
161
+ export declare function gatedAnimationName(keyframes: string, gate: EffectGate | undefined): string;
162
+ /**
163
+ * Whether a gate is currently satisfied — the JavaScript mirror of the CSS above.
164
+ *
165
+ * Needed *only* for JavaScript-rendered segments. A `css-keyframes` segment is gated by the
166
+ * compiled `var()` and never consults this; a JS-rendered one emits no `animation-name` at all, so
167
+ * there is nothing for a stylesheet to neutralise and the decision has to be made here. That is
168
+ * the whole of the JS fallback, and it is a stated limit rather than a hidden one: gating is free
169
+ * for the large majority of the catalog that compiles to keyframes, and costs one `matchMedia`
170
+ * read plus one `change` listener for the rest.
171
+ *
172
+ * **Fails open.** No `matchMedia` — a server render, a bare Node runtime, jsdom, which has none —
173
+ * means the effect runs. The alternative is an environment with no viewport deciding that every
174
+ * gated effect is off, which is a silent no-op, and "never a silent no-op" is the one promise the
175
+ * grammar makes everywhere else.
176
+ *
177
+ * Reads only the viewport axis (`above`/`below`). The container axis (`wide`/`narrow`) has no
178
+ * equivalent here on purpose — there is no `matchContainer()`, and building one is a
179
+ * `ResizeObserver` per container plus a re-entrancy-safe notify path for one attribute. A
180
+ * JavaScript-rendered primitive refuses a container gate at compile time instead of faking it (see
181
+ * `refuseContainerGate` in `compile.ts`), so a gate reaching this function has already had `wide`/
182
+ * `narrow` stripped if it ever had them — there is nothing left for this function to ignore.
183
+ *
184
+ * @param gate - The segment's condition; `undefined` is an ungated segment and always matches.
185
+ * @param win - Window to query. Injected rather than reached for, exactly as
186
+ * `effects/catalog/interaction-shared.ts` injects it for its `(hover: hover)` probe.
187
+ * @complexity O(1) time and space — at most two media queries.
188
+ * @overallScore 100
189
+ */
190
+ export declare function gateMatches(gate: EffectGate | undefined, win: Window | undefined): boolean;
191
+ /**
192
+ * Whether two gates can ever be satisfied at the same time, on both axes at once.
193
+ *
194
+ * Read by `channels.ts`: two effects that are never live together cannot collide on a channel, so
195
+ * `fade-up below:md, parallax-y above:md` composes even though both own `translate`. Without this
196
+ * the compiler would refuse that pair — the exact list the gate exists to make expressible — and
197
+ * silently drop its second half at every width. `wide:md` and `above:md` are independent
198
+ * conditions — a wide container inside a narrow viewport is ordinary layout — so overlap requires
199
+ * agreement on **both** axes: two gates that are disjoint on either one alone can never be live
200
+ * together, however the other axis compares.
201
+ *
202
+ * Each direction is a half-open interval over the scale's *ranks*, which is exact because the only
203
+ * boundaries that exist are the five breakpoints themselves: `above:X`/`wide:X` starts at `rank(X)`
204
+ * and an absent upper bound starts below the narrowest name; `below:Y`/`narrow:Y` ends at `rank(Y)`
205
+ * and an absent lower bound runs past the widest. Half-open is what makes the boundary come out
206
+ * right — `[below:md)` ends exactly where `[above:md)` begins, so the two touch without
207
+ * overlapping, which is the same property that makes a complementary pair tile an axis.
208
+ *
209
+ * @complexity O(b) time in the scale's length; O(1) space.
210
+ * @overallScore 100
211
+ */
212
+ export declare function gatesOverlap(a: EffectGate | undefined, b: EffectGate | undefined): boolean;
213
+ /**
214
+ * Breakpoints a set of gates depends on, deduplicated. Empty for an ungated list.
215
+ *
216
+ * Only ever called on `EffectGate`s already filtered to JavaScript-rendered effects
217
+ * (`Animator.applyViewportGates`), and those never carry `wide`/`narrow` — `refuseContainerGate`
218
+ * in `compile.ts` strips both before a gate reaches `plan.jsEffects`. Reading only `above`/`below`
219
+ * here is therefore not a gap, it mirrors `gateMatches`'s own scope.
220
+ *
221
+ * @complexity O(g) time in the gate count; O(b) space, bounded by the scale's five names.
222
+ * @overallScore 100
223
+ */
224
+ export declare function breakpointsIn(gates: readonly EffectGate[]): Breakpoint[];
225
+ export interface GateWatcher {
226
+ /**
227
+ * Re-notify about `el` whenever one of `breakpoints` is crossed. An empty list unwatches, so a
228
+ * caller can pass whatever it computed without branching first.
229
+ */
230
+ watch(el: Element, breakpoints: readonly Breakpoint[]): void;
231
+ unwatch(el: Element): void;
232
+ destroy(): void;
233
+ }
234
+ /**
235
+ * Watch the breakpoints a page's JavaScript-rendered gates actually name.
236
+ *
237
+ * Only JS-rendered gates ever reach here (see `gateMatches`), so on a page whose gates are all on
238
+ * CSS effects — the common case — nothing is ever watched and no listener is ever bound. The cost
239
+ * of this feature is genuinely zero until a `pin-section below:md` asks for it.
240
+ *
241
+ * The reaction to a crossing is deliberately "tell the caller about the element" rather than
242
+ * anything cleverer: the animator already has exact teardown (`release`) and exact reinstall
243
+ * (`process`), both used on every attribute edit, so a breakpoint change reuses the path that is
244
+ * already exercised rather than inventing a second, partial one. That is also what makes the
245
+ * transition correct at the boundary — a JS effect that was running is destroyed through its own
246
+ * `EffectInstance.destroy()` and its style writes unwound by the ledger before the new plan is
247
+ * installed, so nothing is left half-applied.
248
+ *
249
+ * A single `Map` of element → breakpoints rather than a reverse index per breakpoint: the map only
250
+ * ever holds elements with a gated JS effect, so a linear scan on a crossing is a handful of
251
+ * entries, and a second index would be more state to keep consistent than the scan costs.
252
+ *
253
+ * @param win - Window to bind against; `undefined`, or one with no `matchMedia`, yields a watcher
254
+ * that accepts every call and does nothing. Install-time gating still applies in that case; only
255
+ * live re-evaluation is dropped.
256
+ * @param onChange - Called with each affected element when a watched breakpoint is crossed.
257
+ * @complexity O(1) per `watch`/`unwatch`; O(w) per crossing in the number of watched elements.
258
+ * @overallScore 100
259
+ */
260
+ export declare function createGateWatcher(win: Window | undefined, onChange: (el: Element) => void): GateWatcher;
@@ -0,0 +1,71 @@
1
+ import type { Registry } from './registry.js';
2
+ import type { Reporter } from './reporter.js';
3
+ import type { ParsedValue } from './types.js';
4
+ /**
5
+ * Author-defined bundles for one animator root.
6
+ *
7
+ * A class rather than a closure for the same reason {@link Registry} is one: it is a table with a
8
+ * handful of operations on it, and the operations read better named than nested.
9
+ */
10
+ export declare class BundleTable {
11
+ private readonly registry;
12
+ private readonly bundles;
13
+ /**
14
+ * @param registry - Consulted for name collisions only; a bundle never enters it.
15
+ */
16
+ constructor(registry: Registry);
17
+ /**
18
+ * Register every definition in a subtree, including the subtree root itself.
19
+ *
20
+ * @returns How many *new* names were registered, so the caller can decide whether anything it
21
+ * already compiled deserves a second chance.
22
+ * @complexity O(n) time in the subtree's element count; O(d) space in definition count.
23
+ * @overallScore 100
24
+ */
25
+ collect(root: ParentNode, reporter: Reporter): number;
26
+ /**
27
+ * Rewrite bundle references into the segments they stand for.
28
+ *
29
+ * @returns A new `ParsedValue`; the argument is returned untouched when nothing is defined.
30
+ * @complexity O(s) time in the total segment count across the bundles reached; O(s) space.
31
+ * @overallScore 100
32
+ */
33
+ expand(parsed: ParsedValue): ParsedValue;
34
+ /**
35
+ * Register one definition element.
36
+ *
37
+ * @returns Whether this call added a name the table did not have.
38
+ * @complexity O(n) time in the body's length; O(e) space in its segment count.
39
+ * @overallScore 100
40
+ */
41
+ private define;
42
+ /**
43
+ * Refuse a name that cannot work, with the reason.
44
+ *
45
+ * @returns The diagnostic, or `undefined` when the name is usable.
46
+ * @complexity O(n) time in the name's length; O(1) space.
47
+ * @overallScore 100
48
+ */
49
+ private nameProblem;
50
+ /**
51
+ * Expand one comma list, recursing through bundles that name other bundles.
52
+ *
53
+ * @param trail - Bundle names currently being expanded, innermost last. Both the cycle guard and
54
+ * its diagnostic read it, so it is a list rather than a set.
55
+ * @complexity O(s) time in the total segment count across the bundles reached; O(s) space.
56
+ * @overallScore 100
57
+ */
58
+ private expandList;
59
+ /**
60
+ * Name an unregistered effect that is one typo away from a bundle.
61
+ *
62
+ * `compile.ts` already reports every unknown name, with a suggestion drawn from the catalog — a
63
+ * misspelled *bundle* is invisible in that list, so this adds the one fact that message cannot
64
+ * carry. Only for names the registry does not know, or a real effect one edit away from a badly
65
+ * chosen bundle name would be reported as a mistake.
66
+ *
67
+ * @complexity O(b·n) time in bundle count and name length; O(n) space.
68
+ * @overallScore 100
69
+ */
70
+ private noteNearMiss;
71
+ }
@@ -0,0 +1,64 @@
1
+ import type { Reporter } from './reporter.js';
2
+ /**
3
+ * `func:` — a named global function, called when the element's effects finish.
4
+ *
5
+ * ## What this is, and what it is not
6
+ *
7
+ * It is **sugar for one line of DOM**, and it is implemented as literally that line:
8
+ *
9
+ * ```js
10
+ * el.addEventListener('kui:finish', window[name])
11
+ * ```
12
+ *
13
+ * Nothing here invents a new moment for a callback to fire at. `animator.ts` already dispatches
14
+ * `kui:finish` at exactly one place, and this module registers an ordinary listener for it — so
15
+ * `func:` cannot drift out of step with the event, because it *is* the event. That also means the
16
+ * function receives the same `LifecycleEvent` a hand-written listener receives, with `this` bound to
17
+ * the same element, so moving code between the two spellings is a copy-paste rather than a rewrite.
18
+ *
19
+ * It is **not** the recommended API. `addEventListener('kui:finish', fn)` works with bundlers and ES
20
+ * modules, allows several listeners on one element, and needs no global — and every project with a
21
+ * build step should use it. `func:` exists for the no-build site: a page with a `<script>` block, an
22
+ * attribute, and no module graph to hang a listener off. That audience is real, and telling them to
23
+ * "just import the library and add a listener" is telling them to adopt a toolchain to run one
24
+ * function.
25
+ *
26
+ * ## The one risk an integrator has to own
27
+ *
28
+ * `func:` turns an attribute value into a function call, so **whoever controls the attribute
29
+ * controls which global function runs**. On a hand-authored page that is the author, and there is no
30
+ * gap. On a page where `data-kui` is assembled from a CMS field, a URL parameter, or any other input
31
+ * an end user can influence, it is *them* — and a value they choose can invoke any function the page
32
+ * happens to have put on `window`, with the animated element as `this`.
33
+ *
34
+ * The lookup below is deliberately as narrow as it can be while still doing the job: an own-property
35
+ * read on the element's own window, a `typeof === 'function'` check before calling, no dotted path
36
+ * traversal, and no `eval`/`Function` anywhere — so the value can *name* a function but can never
37
+ * *be* one. That closes code injection. It does not, and cannot, close "call any function by name",
38
+ * because that is the feature. If `data-kui` on your site is ever built from untrusted input, use
39
+ * the event API for the callback and keep `func:` out of the template.
40
+ */
41
+ /** Everything `bindCallback` needs, grouped so the call site reads as one request. */
42
+ export interface CallbackRequest {
43
+ /** The host element. The one lifecycle events are dispatched on, and the callback's `this`. */
44
+ el: Element;
45
+ /** The authored `func:` value — a bare global name, unvalidated by `parse.ts`. */
46
+ name: string;
47
+ /** Diagnostic sink. Silent by default, exactly as it is for an unknown effect name. */
48
+ reporter: Reporter;
49
+ /** The instance's teardown signal, so the listener detaches with everything else. */
50
+ signal: AbortSignal;
51
+ }
52
+ /**
53
+ * Register an author's named global as a `kui:finish` listener on one element.
54
+ *
55
+ * The name is resolved **at fire time, not here**, and that is deliberate: a no-build page routinely
56
+ * defines its functions in a `<script>` that runs after the library has scanned, and resolving
57
+ * eagerly would reject every one of those with a warning that was wrong by the time it printed.
58
+ * The cost is that a typo stays silent until the animation completes — at which point it warns,
59
+ * naming the value, which is the same deal every other diagnostic in the grammar offers.
60
+ *
61
+ * @complexity O(1) time and space to register; O(1) per event beyond the author's own function.
62
+ * @overallScore 100
63
+ */
64
+ export declare function bindCallback(request: CallbackRequest): void;
@@ -14,6 +14,53 @@ export interface Capabilities {
14
14
  viewTransitions: boolean;
15
15
  intersectionObserver: boolean;
16
16
  reducedMotion: boolean;
17
+ /**
18
+ * CSS Motion Path — `offset-path`, and with it `offset-distance`/`offset-rotate`/`offset-anchor`.
19
+ *
20
+ * Probed with a `path()` value specifically, not with `offset-path` alone: Chrome shipped
21
+ * `offset-path: path()` years before it accepted a `<basic-shape>` there, so "does this property
22
+ * parse" and "does this property parse the thing we emit" are different questions, in the same
23
+ * way `view()` and `scroll()` are two probes above rather than one.
24
+ */
25
+ motionPath: boolean;
17
26
  }
27
+ /**
28
+ * Build a `Capabilities` from only the fields a caller actually has an opinion about.
29
+ *
30
+ * `Capabilities` is a closed record of required booleans, so before this existed every object
31
+ * literal constructing one had to name every field. Adding `motionPath` was therefore O(number of
32
+ * construction sites) rather than O(1), and it broke three test harnesses in a single merge — none
33
+ * of which had any interest in motion paths, and all of which failed to compile purely for not
34
+ * mentioning one. Spreading a complete default makes a new field additive everywhere instead.
35
+ *
36
+ * Deliberately *not* used by `detect()`, even though `detect()` is a construction site too. Its
37
+ * literal is the detection logic: keeping it exhaustive is what makes the compiler demand a real
38
+ * probe for each new capability. Route it through here and adding a field would type-check the
39
+ * moment it was given a default, and ship as permanently `false` in every browser — the feature
40
+ * dead in production, with no test able to notice. So the two defaults differ on purpose: a
41
+ * caller stating a fixed environment gets "absent unless said otherwise", and production gets no
42
+ * constant default at all, because the honest answer there is always "go and probe it". Adding a
43
+ * capability still means editing one file — this one — in two adjacent places the compiler names.
44
+ *
45
+ * There is deliberately no `allCapabilities()` counterpart. A harness that wants a fully modern
46
+ * browser lists its `true`s, because the alternative is a test opting in to every capability
47
+ * invented after it was written.
48
+ *
49
+ * @param overrides - Fields this caller has actually decided; everything else comes back absent.
50
+ * @returns A fully-populated capability record.
51
+ * @complexity O(c) time and space in the number of capability fields — fixed and single-digit.
52
+ * @overallScore 100
53
+ */
54
+ export declare function defaultCapabilities(overrides?: Partial<Capabilities>): Capabilities;
18
55
  export declare function detect(force?: boolean): Capabilities;
19
56
  export declare function resetCapabilities(): void;
57
+ /**
58
+ * Diagnostics for composed channels this environment cannot render.
59
+ *
60
+ * @param channels - Channels the compiled effects write to (`CompiledPlan.channels`).
61
+ * @param capabilities - The environment's detected capabilities.
62
+ * @returns One message per unsupported channel; empty in a browser that supports them all.
63
+ * @complexity O(r) time in the requirement table's length — single digits; O(1) space per message.
64
+ * @overallScore 100
65
+ */
66
+ export declare function unsupportedChannelWarnings(channels: readonly string[], capabilities: Capabilities): string[];
@@ -1,4 +1,5 @@
1
- import type { Channel } from './types.js';
1
+ import type { EffectGate } from './breakpoints.js';
2
+ import type { Channel, DeliveryMechanism, EffectPhase } from './types.js';
2
3
  /**
3
4
  * Composition safety.
4
5
  *
@@ -9,17 +10,134 @@ import type { Channel } from './types.js';
9
10
  *
10
11
  * Hence channels. An effect declares the property groups it owns; a comma list is only safe to
11
12
  * compile when those sets are pairwise disjoint. See docs/design.md §4.
13
+ *
14
+ * "Pairwise disjoint" gained a second dimension when viewport gates arrived: two effects that can
15
+ * never be live at the same viewport width cannot collide however many channels they share. That
16
+ * is not a nicety — `fade-up below:md, parallax-y above:md` is the flagship case the gate exists
17
+ * for, both halves own `translate`, and without the width check the compiler would refuse the pair
18
+ * and drop the second half at every width.
19
+ *
20
+ * And a third: *when* each effect holds the channel. Disjointness in space and time are the same
21
+ * kind of exemption — `fade-up, lift` share `translate` and share every width, but an entrance and
22
+ * a hover state are not fighting over the property, they are taking turns with it. See
23
+ * {@link EffectPhase} for why that is a declared fact rather than a derived one, and
24
+ * {@link independentPhases} for which pairs qualify.
12
25
  */
13
26
  export interface ChannelClaim {
14
27
  name: string;
15
28
  channels: Channel[];
29
+ /** Viewport condition, if the segment carries one. Absent means "live at every width". */
30
+ gate?: EffectGate;
31
+ /**
32
+ * When this segment holds its channels, if that is known. Absent means unphased — the preset
33
+ * declares no phase and none could be derived — and an unphased claim collides with everything,
34
+ * itself included, which is exactly how every claim behaved before this field existed.
35
+ */
36
+ phase?: EffectPhase;
37
+ /**
38
+ * Whether the caller can put `animation-composition: add` on whatever this segment compiles to.
39
+ * Asked of the caller rather than worked out here because the answer is about the *renderer* —
40
+ * how many keyframe blocks the segment produces, and whether it produces any at all — which is
41
+ * `compile.ts`'s knowledge. What is additive about a *channel* stays here, in
42
+ * {@link CHANNEL_COMPOSITION}.
43
+ */
44
+ additive?: boolean;
45
+ /**
46
+ * How this segment's motion reaches the element, when that is something composition has to act on
47
+ * — see `types.ts`'s {@link DeliveryMechanism}. Read only by {@link deliveryClobbers}, never by
48
+ * {@link findConflicts}: it describes a fight over a *property* rather than over a channel.
49
+ */
50
+ delivery?: DeliveryMechanism;
51
+ /**
52
+ * Whether this segment writes `animation-name` to `element.style`. Asked of the caller for the
53
+ * same reason {@link additive} is — it is a fact about the renderer, which is `compile.ts`'s
54
+ * knowledge, not this module's.
55
+ */
56
+ inlineAnimation?: boolean;
16
57
  }
17
58
  export interface Conflict {
18
59
  channel: Channel;
19
60
  effects: [string, string];
20
61
  }
21
- /** Every pair of effects claiming the same channel. Empty array means the list composes. */
62
+ /**
63
+ * Which channels may be composed by *adding* two animations' output rather than replacing it, and
64
+ * which must stay one-writer-only.
65
+ *
66
+ * Deliberately an allowlist with one member. `animation-composition: add` looks like a general
67
+ * answer to this whole problem and is not: addition in Web Animations L1 is defined per property
68
+ * type, and a property with no additive definition — anything discrete, and every value CSS
69
+ * resolves as a keyword — falls back toward replacement *silently*. Applying `add` across the
70
+ * board would convert a loud, detectable conflict into a quiet browser-dependent last-writer
71
+ * result, which is strictly worse than the drop it was meant to fix. The channel set includes
72
+ * `clip`, `background`, `text`, display-like channels and, through `Channel`'s open `string` arm,
73
+ * whatever a third-party primitive registers — none of which anyone has tested.
74
+ *
75
+ * So: `translate` is here because a translation is a vector with a well-defined sum and a neutral
76
+ * `0px 0px 0px`, and nothing else is here until someone measures it in a browser and adds it.
77
+ */
78
+ export declare const CHANNEL_COMPOSITION: Partial<Record<Channel, 'add'>>;
79
+ /** Whether every channel a claim writes can survive being made additive. */
80
+ export declare function additiveChannels(channels: readonly Channel[]): boolean;
81
+ /**
82
+ * Rescue a surviving clash by summing the claims that caused it instead of letting one replace the
83
+ * other.
84
+ *
85
+ * The last resort, and deliberately the narrowest of the three exemptions the detector offers — a
86
+ * gate says the two are never live together, a phase says they take turns, and this one admits they
87
+ * genuinely overlap and asks the browser to blend them.
88
+ *
89
+ * All-or-nothing over the whole contested set. A partly-additive list is the worst outcome
90
+ * available: the author is told nothing, and one of the two effects is quietly replaced anyway.
91
+ *
92
+ * @returns Indices of the claims to compile additively, or `undefined` when the rescue does not
93
+ * apply and the caller should refuse the list.
94
+ * @complexity O(n * c) time in claims and their channels; O(n + c) space.
95
+ * @overallScore 100
96
+ */
97
+ export declare function additiveResolution(claims: ChannelClaim[], conflicts: Conflict[]): Set<number> | undefined;
98
+ /**
99
+ * Every pair of effects claiming the same channel at widths where both can be live, in phases where
100
+ * both can hold it. Empty array means the list composes.
101
+ *
102
+ * Each channel keeps a *list* of prior claimants rather than a single owner, and a claim is
103
+ * reported against the first one it actually overlaps. With no gates in play that first one is
104
+ * always the first claim, so the output is exactly what a single-owner map produced. With gates it
105
+ * is the difference between catching a real collision and inventing one: in `fade-up below:md,
106
+ * parallax-y above:md, slide-up above:md` the genuine clash is the second pair, and a map holding
107
+ * only `fade-up` would have compared both later effects against a segment neither can coexist with
108
+ * and waved the list through.
109
+ *
110
+ * @complexity O(n^2) worst case in same-channel claimants; a comma list is a handful of segments.
111
+ * @overallScore 100
112
+ */
22
113
  export declare function findConflicts(claims: ChannelClaim[]): Conflict[];
114
+ /**
115
+ * Clashes over *how* each segment's motion is delivered, which {@link findConflicts} cannot see.
116
+ *
117
+ * That function asks which property groups two segments write, and for two compiled animation
118
+ * tracks it is the right question. It is the wrong one here. A preset whose motion is
119
+ * `[data-kui-fx~='icon-spin']:hover { animation: … }` in `src/css/interaction.css` loses the
120
+ * `animation` property to *any* composed neighbour that writes `animation-name` inline, because an
121
+ * inline declaration outranks an author rule outright. It does not matter that `icon-spin` writes
122
+ * `rotate` and `fade-up` writes `opacity`/`translate` — the two never shared a channel, the
123
+ * detector correctly reported nothing, and `fade-up, icon-spin` composed clean with the hover
124
+ * silently dead. 548 such pairs, measured, and pre-existing rather than introduced by the phase
125
+ * axis: see `types.ts`'s {@link DeliveryMechanism} and the 2026-09-08 catalog review.
126
+ *
127
+ * There is no rescue and deliberately none offered. A gate does not help, unlike in
128
+ * {@link findConflicts}: `gatedAnimationName` compiles an out-of-band track to
129
+ * `animation-name: var(--kui-above-md, kui-in-up)` and `base.css` declares that property `none`
130
+ * outside the gate — `none` is still an inline value, still outranks the stylesheet rule, and
131
+ * leaves the hover just as dead below the breakpoint as above it. Neither does
132
+ * {@link additiveResolution}: `animation-composition` blends two tracks that both run, and the
133
+ * whole failure here is a rule that never becomes active.
134
+ *
135
+ * @returns One sentence per clobbered claim, in the shape {@link describeConflicts} produces, or an
136
+ * empty array when nothing in the list writes an inline animation.
137
+ * @complexity O(n) time in the claim count; O(n) space.
138
+ * @overallScore 100
139
+ */
140
+ export declare function deliveryClobbers(claims: ChannelClaim[]): string[];
23
141
  export declare function describeConflicts(conflicts: Conflict[]): string;
24
142
  /** Union of all claimed channels, for debugging and perf accounting. */
25
143
  export declare function claimedChannels(claims: ChannelClaim[]): Channel[];
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Owned by phase 9a — the pre-JS cloak selector strings, pulled out of
3
+ * `scripts/generate-preset-css.mjs` so the generator can grow the comma-form handling 9a needs
4
+ * (`cloak: true`/`cloak:'.a, .b'`-style selector lists, per the owner decision) without hand-editing
5
+ * the bundled generator script directly.
6
+ *
7
+ * `~=` matches a whole whitespace-separated token, but a `data-kui` value's segments are
8
+ * comma-separated, not space-separated — `data-kui="text-reveal-mask,\nfade-up"` puts the comma
9
+ * straight up against the previous word, so the actual whitespace token is `text-reveal-mask,`, not
10
+ * `text-reveal-mask`. An author can glue that comma to either side (or both, mid-list), so each name
11
+ * or gate token needs one `[data-kui~=…]` per comma form, grouped in a single `:is()` so the cloak
12
+ * still matches whichever form the author wrote. `:is()` takes the specificity of its most specific
13
+ * argument; every form here has identical specificity, so the grouped selector keeps HEAD's
14
+ * specificity exactly. A browser old enough to reject `:is()` drops the whole rule and fails open —
15
+ * not cloaking is exactly today's (pre-9a) behaviour, same fail-open the bare-comma case already had.
16
+ */
17
+ /**
18
+ * The four ways one `data-kui` token can sit relative to its neighbours' commas: alone, followed by
19
+ * a glued comma, preceded by one, or both (mid-list with no surrounding spaces).
20
+ *
21
+ * @param token - The preset name or gate token, as written in `data-kui`.
22
+ * @returns The four literal token forms, in `[data-kui~=…]` match order.
23
+ * @complexity O(1) time and space.
24
+ * @overallScore 100
25
+ */
26
+ export declare function cloakTokenForms(token: string): string[];
27
+ /**
28
+ * The pre-JS cloak selector for one preset name — matched while `<html data-kui-cloak>` is still
29
+ * present, so an element carrying that preset stays hidden until the animator's own `data-kui-*`
30
+ * state attribute lands and the ordinary ready-gate rules take over.
31
+ *
32
+ * @param name - The preset name, as written in `data-kui`.
33
+ * @returns One selector line (no trailing comma or newline).
34
+ * @complexity O(1) time and space.
35
+ * @overallScore 100
36
+ */
37
+ export declare function cloakSelector(name: string): string;
38
+ /**
39
+ * The gate-release selector for one gate token (e.g. `above:md`) — the selector half of
40
+ * `gateReleaseRules`'s per-token block, without the trailing ` {` a caller appends itself.
41
+ *
42
+ * @param token - The gate token, as written in `data-kui`.
43
+ * @returns One selector line (no trailing ` {` or newline).
44
+ * @complexity O(1) time and space.
45
+ * @overallScore 100
46
+ */
47
+ export declare function gateReleaseSelector(token: string): string;