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.
- package/README.md +93 -10
- package/dist/esm/advanced/index.mjs +4727 -0
- package/dist/esm/chunk-AEQGC2KM.mjs +189 -0
- package/dist/esm/chunk-MQSNMYRP.mjs +9332 -0
- package/dist/esm/chunk-RVTYN6NZ.mjs +3446 -0
- package/dist/esm/chunk-U43BX5O2.mjs +2984 -0
- package/dist/esm/core/index.mjs +16 -9
- package/dist/esm/effects/index.mjs +19 -4
- package/dist/esm/index.mjs +17 -10
- package/dist/kuinetic.advanced.js +5070 -0
- package/dist/kuinetic.advanced.min.js +749 -0
- package/dist/kuinetic.all.js +15218 -5595
- package/dist/kuinetic.all.min.js +2 -0
- package/dist/kuinetic.css +2900 -360
- package/dist/kuinetic.js +15217 -5593
- package/dist/kuinetic.min.css +1 -0
- package/dist/kuinetic.min.js +7 -0
- package/dist/types/3d/angles.d.ts +30 -0
- package/dist/types/3d/flattening.d.ts +49 -0
- package/dist/types/3d/index.d.ts +36 -0
- package/dist/types/3d/model-3d.d.ts +115 -0
- package/dist/types/3d/register.d.ts +26 -0
- package/dist/types/3d/webgl.d.ts +24 -0
- package/dist/types/advanced/audio.d.ts +322 -0
- package/dist/types/advanced/base.d.ts +233 -0
- package/dist/types/advanced/camera-3d.d.ts +236 -0
- package/dist/types/advanced/fluid-cursor.d.ts +149 -0
- package/dist/types/advanced/gl-utils.d.ts +52 -0
- package/dist/types/advanced/glsl.d.ts +60 -0
- package/dist/types/advanced/index.d.ts +23 -0
- package/dist/types/advanced/particles.d.ts +108 -0
- package/dist/types/advanced/scenes.d.ts +207 -0
- package/dist/types/advanced/shaders.d.ts +972 -0
- package/dist/types/browser/boot.d.ts +72 -0
- package/dist/types/core/activation.d.ts +227 -2
- package/dist/types/core/animator.d.ts +446 -11
- package/dist/types/core/attrs.d.ts +9 -0
- package/dist/types/core/breakpoints.d.ts +260 -0
- package/dist/types/core/bundles.d.ts +71 -0
- package/dist/types/core/callback.d.ts +64 -0
- package/dist/types/core/capabilities.d.ts +47 -0
- package/dist/types/core/channels.d.ts +120 -2
- package/dist/types/core/cloak-selectors.d.ts +47 -0
- package/dist/types/core/compile.d.ts +207 -2
- package/dist/types/core/control.d.ts +218 -0
- package/dist/types/core/declarations.d.ts +128 -0
- package/dist/types/core/derived/aggregate.d.ts +43 -0
- package/dist/types/core/derived/book.d.ts +30 -0
- package/dist/types/core/derived/diagnostics.d.ts +51 -0
- package/dist/types/core/derived/group-gate.d.ts +62 -0
- package/dist/types/core/derived/install.d.ts +110 -0
- package/dist/types/core/derived/late-matches.d.ts +19 -0
- package/dist/types/core/derived/types.d.ts +108 -0
- package/dist/types/core/easing.d.ts +57 -0
- package/dist/types/core/element-config.d.ts +47 -6
- package/dist/types/core/element-size.d.ts +23 -0
- package/dist/types/core/event-sources.d.ts +39 -0
- package/dist/types/core/flip.d.ts +34 -1
- package/dist/types/core/gesture.d.ts +12 -0
- package/dist/types/core/host-facts.d.ts +155 -0
- package/dist/types/core/index.d.ts +7 -3
- package/dist/types/core/owned-styles.d.ts +129 -2
- package/dist/types/core/params.d.ts +48 -0
- package/dist/types/core/parse.d.ts +28 -1
- package/dist/types/core/register-into.d.ts +29 -0
- package/dist/types/core/registry.d.ts +10 -3
- package/dist/types/core/repeat.d.ts +91 -0
- package/dist/types/core/sequence.d.ts +126 -0
- package/dist/types/core/stagger-config.d.ts +199 -0
- package/dist/types/core/stagger-keys.d.ts +63 -0
- package/dist/types/core/stagger.d.ts +187 -6
- package/dist/types/core/style-plan.d.ts +21 -16
- package/dist/types/core/target.d.ts +128 -0
- package/dist/types/core/threshold-reachability.d.ts +45 -0
- package/dist/types/core/time-scale.d.ts +9 -0
- package/dist/types/core/toggle-actions.d.ts +141 -0
- package/dist/types/core/travel.d.ts +59 -0
- package/dist/types/core/types.d.ts +705 -13
- package/dist/types/core/unquoted-selectors.d.ts +53 -0
- package/dist/types/effects/carousel/drag.d.ts +80 -0
- package/dist/types/effects/carousel/index.d.ts +149 -0
- package/dist/types/effects/catalog/background-media.d.ts +152 -0
- package/dist/types/effects/catalog/discrete.d.ts +13 -0
- package/dist/types/effects/catalog/interaction-proximity.d.ts +24 -0
- package/dist/types/effects/catalog/interaction-reveal.d.ts +248 -0
- package/dist/types/effects/catalog/interaction-shared.d.ts +42 -0
- package/dist/types/effects/catalog/interaction-states.d.ts +68 -0
- package/dist/types/effects/catalog/materials.d.ts +67 -0
- package/dist/types/effects/catalog/media-shared.d.ts +3 -2
- package/dist/types/effects/catalog/numbers-shared.d.ts +8 -5
- package/dist/types/effects/catalog/shared.d.ts +2 -2
- package/dist/types/effects/catalog/text-shared.d.ts +92 -10
- package/dist/types/effects/catalog/transforms.d.ts +13 -0
- package/dist/types/effects/catalog/view-transitions.d.ts +13 -0
- package/dist/types/effects/forms/primitives.d.ts +15 -4
- package/dist/types/effects/gestures/index.d.ts +53 -0
- package/dist/types/effects/index.d.ts +5 -0
- package/dist/types/effects/layout/presets.d.ts +35 -0
- package/dist/types/effects/motion-path/index.d.ts +35 -0
- package/dist/types/effects/scroll-mechanics/params.d.ts +23 -0
- package/dist/types/effects/scroll-mechanics/presets.d.ts +51 -0
- package/dist/types/effects/scroll-mechanics/section-index.d.ts +47 -0
- package/dist/types/effects/shared.d.ts +162 -2
- package/dist/types/effects/step-index.d.ts +121 -0
- package/dist/types/effects/step-marking.d.ts +41 -33
- package/dist/types/effects/svg/icon-parts.d.ts +23 -0
- package/dist/types/effects/three-d/flip-parts.d.ts +46 -0
- package/dist/types/effects/tween/index.d.ts +57 -0
- package/dist/types/effects/tween/properties.d.ts +107 -0
- package/dist/types/effects/tween/waypoints.d.ts +106 -0
- package/dist/types/showcase/compare.d.ts +4 -0
- package/dist/types/showcase/device-frame.d.ts +3 -0
- package/dist/types/showcase/hotspots.d.ts +3 -0
- package/dist/types/showcase/index.d.ts +30 -0
- package/dist/types/showcase/lightbox.d.ts +3 -0
- package/dist/types/showcase/media-source.d.ts +13 -0
- package/dist/types/showcase/modal-shell.d.ts +22 -0
- package/dist/types/showcase/scroll-story.d.ts +4 -0
- package/dist/types/showcase/shared.d.ts +47 -0
- package/dist/types/showcase/slideshow.d.ts +3 -0
- package/dist/types/showcase/slow-mo.d.ts +5 -0
- package/package.json +14 -7
- package/dist/esm/chunk-5DON3UFQ.mjs +0 -4380
- package/dist/esm/chunk-JT4PZL3A.mjs +0 -773
- package/dist/esm/chunk-QUVFODSQ.mjs +0 -1278
|
@@ -0,0 +1,3446 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ATTR,
|
|
3
|
+
KUI_EVENT,
|
|
4
|
+
Registry,
|
|
5
|
+
SEGMENT_HOIST_KEYS,
|
|
6
|
+
applyToggleVerb,
|
|
7
|
+
assignOnce,
|
|
8
|
+
breakpointsIn,
|
|
9
|
+
control,
|
|
10
|
+
createActivationBinder,
|
|
11
|
+
createCssInstance,
|
|
12
|
+
createGateWatcher,
|
|
13
|
+
createRootResolver,
|
|
14
|
+
createScrollScheduler,
|
|
15
|
+
cssEasingValue,
|
|
16
|
+
directionValue,
|
|
17
|
+
emitLifecycle,
|
|
18
|
+
gateMatches,
|
|
19
|
+
gatedAnimationName,
|
|
20
|
+
gatesOverlap,
|
|
21
|
+
isInfiniteRepeat,
|
|
22
|
+
isOneShot,
|
|
23
|
+
isSafeCssValue,
|
|
24
|
+
parse,
|
|
25
|
+
parseActivationAttribute,
|
|
26
|
+
parseToggleActions,
|
|
27
|
+
play,
|
|
28
|
+
playbackExpression,
|
|
29
|
+
queryScoped,
|
|
30
|
+
readEffectParams,
|
|
31
|
+
readEffectTiming,
|
|
32
|
+
repeatCount,
|
|
33
|
+
resolveActivationSpec,
|
|
34
|
+
resolveParams,
|
|
35
|
+
resolvePlayback,
|
|
36
|
+
selectorBreadth,
|
|
37
|
+
splitTopLevel,
|
|
38
|
+
startKindOf,
|
|
39
|
+
suggest,
|
|
40
|
+
timingProperty,
|
|
41
|
+
toMilliseconds,
|
|
42
|
+
warnAboutActivation,
|
|
43
|
+
warnAboutToggleActions
|
|
44
|
+
} from "./chunk-U43BX5O2.mjs";
|
|
45
|
+
import {
|
|
46
|
+
createLedgerSet
|
|
47
|
+
} from "./chunk-AEQGC2KM.mjs";
|
|
48
|
+
|
|
49
|
+
// src/core/bundles.ts
|
|
50
|
+
var HOIST_KEYS = ["activation", "timeline", "threshold", "rm", "func"];
|
|
51
|
+
var HOIST_LABELS = {
|
|
52
|
+
activation: "on",
|
|
53
|
+
timeline: "timeline",
|
|
54
|
+
threshold: "threshold",
|
|
55
|
+
rm: "rm",
|
|
56
|
+
func: "func"
|
|
57
|
+
};
|
|
58
|
+
var BundleTable = class {
|
|
59
|
+
/**
|
|
60
|
+
* @param registry - Consulted for name collisions only; a bundle never enters it.
|
|
61
|
+
*/
|
|
62
|
+
constructor(registry) {
|
|
63
|
+
this.registry = registry;
|
|
64
|
+
}
|
|
65
|
+
bundles = /* @__PURE__ */ new Map();
|
|
66
|
+
/**
|
|
67
|
+
* Register every definition in a subtree, including the subtree root itself.
|
|
68
|
+
*
|
|
69
|
+
* @returns How many *new* names were registered, so the caller can decide whether anything it
|
|
70
|
+
* already compiled deserves a second chance.
|
|
71
|
+
* @complexity O(n) time in the subtree's element count; O(d) space in definition count.
|
|
72
|
+
* @overallScore 100
|
|
73
|
+
*/
|
|
74
|
+
collect(root, reporter) {
|
|
75
|
+
let added = 0;
|
|
76
|
+
for (const el of definitionsIn(root)) {
|
|
77
|
+
if (this.define(el, reporter)) added++;
|
|
78
|
+
}
|
|
79
|
+
return added;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Rewrite bundle references into the segments they stand for.
|
|
83
|
+
*
|
|
84
|
+
* @returns A new `ParsedValue`; the argument is returned untouched when nothing is defined.
|
|
85
|
+
* @complexity O(s) time in the total segment count across the bundles reached; O(s) space.
|
|
86
|
+
* @overallScore 100
|
|
87
|
+
*/
|
|
88
|
+
expand(parsed) {
|
|
89
|
+
if (this.bundles.size === 0) return parsed;
|
|
90
|
+
const sink = { warnings: [...parsed.warnings], hoists: {} };
|
|
91
|
+
const specs = this.expandList(parsed.specs, [], sink);
|
|
92
|
+
const result = { ...parsed, specs, warnings: sink.warnings };
|
|
93
|
+
for (const key of HOIST_KEYS) absorbHoist(result, sink.hoists, key);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Register one definition element.
|
|
98
|
+
*
|
|
99
|
+
* @returns Whether this call added a name the table did not have.
|
|
100
|
+
* @complexity O(n) time in the body's length; O(e) space in its segment count.
|
|
101
|
+
* @overallScore 100
|
|
102
|
+
*/
|
|
103
|
+
define(el, reporter) {
|
|
104
|
+
const name = el.getAttribute(ATTR.define).trim();
|
|
105
|
+
const source = el.getAttribute(ATTR.source) ?? "";
|
|
106
|
+
const problem = this.nameProblem(name);
|
|
107
|
+
if (problem) {
|
|
108
|
+
reporter.warn(problem, el);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const existing = this.bundles.get(name);
|
|
112
|
+
if (existing) {
|
|
113
|
+
if (existing.source !== source) {
|
|
114
|
+
reporter.warn(`bundle "${name}" is already defined \u2014 the first definition wins`, el);
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (el.localName !== "template") {
|
|
119
|
+
reporter.warn(
|
|
120
|
+
`bundle "${name}" is defined on <${el.localName}> \u2014 a definition is data, so it is never animated; use <template>`,
|
|
121
|
+
el
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
this.bundles.set(name, readBundle(name, source, el, reporter));
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Refuse a name that cannot work, with the reason.
|
|
129
|
+
*
|
|
130
|
+
* @returns The diagnostic, or `undefined` when the name is usable.
|
|
131
|
+
* @complexity O(n) time in the name's length; O(1) space.
|
|
132
|
+
* @overallScore 100
|
|
133
|
+
*/
|
|
134
|
+
nameProblem(name) {
|
|
135
|
+
if (!name) return `${ATTR.define} needs a name \u2014 an empty one defines nothing`;
|
|
136
|
+
if (/[\s,:()"']/.test(name)) {
|
|
137
|
+
return `bundle name "${name}" cannot contain whitespace, a comma, a colon, parentheses or quotes \u2014 those are grammar, so a name carrying them could never be written in ${ATTR.source}`;
|
|
138
|
+
}
|
|
139
|
+
if (this.registry.has(name)) {
|
|
140
|
+
return `bundle "${name}" is already the name of a registered effect \u2014 the effect wins and this definition is ignored`;
|
|
141
|
+
}
|
|
142
|
+
return void 0;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Expand one comma list, recursing through bundles that name other bundles.
|
|
146
|
+
*
|
|
147
|
+
* @param trail - Bundle names currently being expanded, innermost last. Both the cycle guard and
|
|
148
|
+
* its diagnostic read it, so it is a list rather than a set.
|
|
149
|
+
* @complexity O(s) time in the total segment count across the bundles reached; O(s) space.
|
|
150
|
+
* @overallScore 100
|
|
151
|
+
*/
|
|
152
|
+
expandList(specs, trail, sink) {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const spec of specs) {
|
|
155
|
+
const bundle = this.bundles.get(spec.name);
|
|
156
|
+
if (!bundle) {
|
|
157
|
+
out.push(spec);
|
|
158
|
+
this.noteNearMiss(spec.name, sink.warnings);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (trail.includes(spec.name)) {
|
|
162
|
+
sink.warnings.push(cycleWarning(trail, spec.name));
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
takeHoists(bundle, spec.name, sink);
|
|
166
|
+
const inner = this.expandList(bundle.specs, [...trail, spec.name], sink);
|
|
167
|
+
warnOnFlattenedOffsets(spec, inner, sink.warnings);
|
|
168
|
+
for (const member of inner) out.push(overlay(member, spec));
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Name an unregistered effect that is one typo away from a bundle.
|
|
174
|
+
*
|
|
175
|
+
* `compile.ts` already reports every unknown name, with a suggestion drawn from the catalog — a
|
|
176
|
+
* misspelled *bundle* is invisible in that list, so this adds the one fact that message cannot
|
|
177
|
+
* carry. Only for names the registry does not know, or a real effect one edit away from a badly
|
|
178
|
+
* chosen bundle name would be reported as a mistake.
|
|
179
|
+
*
|
|
180
|
+
* @complexity O(b·n) time in bundle count and name length; O(n) space.
|
|
181
|
+
* @overallScore 100
|
|
182
|
+
*/
|
|
183
|
+
noteNearMiss(name, warnings) {
|
|
184
|
+
if (this.registry.has(name)) return;
|
|
185
|
+
const hit = suggest(name, [...this.bundles.keys()]);
|
|
186
|
+
if (hit) {
|
|
187
|
+
warnings.push(`"${name}" is not a registered effect \u2014 did you mean the bundle "${hit}"?`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
function readBundle(name, source, el, reporter) {
|
|
192
|
+
const parsed = parse(source);
|
|
193
|
+
for (const warning of parsed.warnings) reporter.warn(`in bundle "${name}": ${warning}`, el);
|
|
194
|
+
if (parsed.specs.length === 0) {
|
|
195
|
+
reporter.warn(`bundle "${name}" names no effect \u2014 elements that use it animate nothing`, el);
|
|
196
|
+
}
|
|
197
|
+
const group = ["cascade", "spread", "order", "cols", "along"].filter((key) => parsed[key] !== void 0).map((key) => `"${key}:"`).join(" and ");
|
|
198
|
+
if (group) {
|
|
199
|
+
reporter.warn(
|
|
200
|
+
`bundle "${name}" declares ${group} \u2014 a stagger group is read from the group element's own attribute, which never sees a bundle, so it is dropped; write it on the group element itself`,
|
|
201
|
+
el
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return { source, specs: parsed.specs, hoists: parsed };
|
|
205
|
+
}
|
|
206
|
+
function takeHoists(bundle, name, sink) {
|
|
207
|
+
for (const key of HOIST_KEYS) {
|
|
208
|
+
if (absorbHoist(sink.hoists, bundle.hoists, key) !== "clash") continue;
|
|
209
|
+
const label = HOIST_LABELS[key];
|
|
210
|
+
const kept = String(sink.hoists[key]);
|
|
211
|
+
sink.warnings.push(
|
|
212
|
+
`bundle "${name}" sets "${label}:${String(bundle.hoists[key])}", which conflicts with "${label}:${kept}" from an earlier bundle \u2014 the first wins`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function cycleWarning(trail, name) {
|
|
217
|
+
const path = [...trail, name].join(" \u2192 ");
|
|
218
|
+
return `bundle "${name}" is defined in terms of itself (${path}) \u2014 the reference is dropped`;
|
|
219
|
+
}
|
|
220
|
+
function definitionsIn(root) {
|
|
221
|
+
const selector = `[${ATTR.define}]`;
|
|
222
|
+
const found = [...root.querySelectorAll(selector)];
|
|
223
|
+
const self = root.nodeType === 1 ? root : null;
|
|
224
|
+
if (self?.matches(selector)) found.unshift(self);
|
|
225
|
+
return found;
|
|
226
|
+
}
|
|
227
|
+
function absorbHoist(into, from, key) {
|
|
228
|
+
const value = from[key];
|
|
229
|
+
if (value === void 0) return "kept";
|
|
230
|
+
const current = into[key];
|
|
231
|
+
if (current === void 0) {
|
|
232
|
+
into[key] = value;
|
|
233
|
+
return "taken";
|
|
234
|
+
}
|
|
235
|
+
return current === value ? "kept" : "clash";
|
|
236
|
+
}
|
|
237
|
+
function overlay(member, ref) {
|
|
238
|
+
return {
|
|
239
|
+
...member,
|
|
240
|
+
duration: ref.duration ?? member.duration,
|
|
241
|
+
delay: ref.delay ?? member.delay,
|
|
242
|
+
easing: ref.easing ?? member.easing,
|
|
243
|
+
at: ref.at ?? member.at,
|
|
244
|
+
gate: ref.gate ?? member.gate,
|
|
245
|
+
repeat: ref.repeat ?? member.repeat,
|
|
246
|
+
yoyo: ref.yoyo ?? member.yoyo,
|
|
247
|
+
params: { ...member.params, ...ref.params },
|
|
248
|
+
hoists: overlayHoists(member.hoists, ref.hoists)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function overlayHoists(member, ref) {
|
|
252
|
+
if (!member && !ref) return void 0;
|
|
253
|
+
const merged = { ...member };
|
|
254
|
+
for (const key of SEGMENT_HOIST_KEYS) {
|
|
255
|
+
const value = ref?.[key];
|
|
256
|
+
if (value !== void 0) merged[key] = value;
|
|
257
|
+
}
|
|
258
|
+
return merged;
|
|
259
|
+
}
|
|
260
|
+
function warnOnFlattenedOffsets(ref, inner, warnings) {
|
|
261
|
+
if (ref.at === void 0 || !inner.some((member) => member.at !== void 0)) return;
|
|
262
|
+
warnings.push(
|
|
263
|
+
`"at:${ref.at}" on "${ref.name}" overrides the offsets the bundle sets on its own segments \u2014 they now all start at the same position`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/core/callback.ts
|
|
268
|
+
function bindCallback(request) {
|
|
269
|
+
const { el, name, reporter, signal } = request;
|
|
270
|
+
const handler = (event) => {
|
|
271
|
+
if (event.target !== el) return;
|
|
272
|
+
const fn = resolveGlobalFunction(el, name);
|
|
273
|
+
if (!fn) {
|
|
274
|
+
reporter.warn(
|
|
275
|
+
`func:${name} \u2014 no global function named "${name}". A function declaration (\`function ${name}() {}\`) or an explicit \`window.${name} = \u2026\` creates one; \`const\` and \`let\` deliberately do not, and neither does a bundled or \`type="module"\` script.`,
|
|
276
|
+
el
|
|
277
|
+
);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
Reflect.apply(fn, el, [event]);
|
|
281
|
+
};
|
|
282
|
+
el.addEventListener(KUI_EVENT.finish, handler);
|
|
283
|
+
signal.addEventListener("abort", () => el.removeEventListener(KUI_EVENT.finish, handler));
|
|
284
|
+
}
|
|
285
|
+
function resolveGlobalFunction(el, name) {
|
|
286
|
+
const scope = el.ownerDocument?.defaultView ?? globalThis;
|
|
287
|
+
if (!Object.hasOwn(scope, name)) return void 0;
|
|
288
|
+
const candidate = scope[name];
|
|
289
|
+
return typeof candidate === "function" ? candidate : void 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/core/capabilities.ts
|
|
293
|
+
function supports(property, value) {
|
|
294
|
+
if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return false;
|
|
295
|
+
try {
|
|
296
|
+
return CSS.supports(property, value);
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
var cached;
|
|
302
|
+
function detect(force = false) {
|
|
303
|
+
if (cached && !force) return cached;
|
|
304
|
+
cached = {
|
|
305
|
+
viewTimeline: supports("animation-timeline", "view()"),
|
|
306
|
+
scrollTimeline: supports("animation-timeline", "scroll()"),
|
|
307
|
+
animationRange: supports("animation-range", "entry 0% cover 30%"),
|
|
308
|
+
// `translate`/`rotate`/`scale` as independent properties is what makes the channel model
|
|
309
|
+
// possible at all — under the `transform` shorthand every effect would collide.
|
|
310
|
+
individualTransforms: supports("translate", "0 10px") && supports("scale", "1.1"),
|
|
311
|
+
scrollTimelineName: supports("scroll-timeline-name", "--x"),
|
|
312
|
+
viewTransitions: typeof document !== "undefined" && "startViewTransition" in document,
|
|
313
|
+
intersectionObserver: typeof IntersectionObserver !== "undefined",
|
|
314
|
+
reducedMotion: typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches,
|
|
315
|
+
motionPath: supports("offset-path", 'path("M 0 0")')
|
|
316
|
+
};
|
|
317
|
+
return cached;
|
|
318
|
+
}
|
|
319
|
+
var CHANNEL_REQUIREMENTS = [
|
|
320
|
+
{
|
|
321
|
+
channel: "offset",
|
|
322
|
+
capability: "motionPath",
|
|
323
|
+
message: "this browser does not support CSS Motion Path (offset-path), so the element will not travel \u2014 the animation still runs and completes, it just has no path to follow"
|
|
324
|
+
}
|
|
325
|
+
];
|
|
326
|
+
function unsupportedChannelWarnings(channels, capabilities) {
|
|
327
|
+
return CHANNEL_REQUIREMENTS.filter(
|
|
328
|
+
(entry) => channels.includes(entry.channel) && !capabilities[entry.capability]
|
|
329
|
+
).map((entry) => entry.message);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/core/channels.ts
|
|
333
|
+
var INDEPENDENT_PHASES = /* @__PURE__ */ new Set(["entrance|state", "exit|state"]);
|
|
334
|
+
function phasePair(a, b) {
|
|
335
|
+
return a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
336
|
+
}
|
|
337
|
+
function independentPhases(a, b) {
|
|
338
|
+
if (!a.phase || !b.phase) return false;
|
|
339
|
+
return INDEPENDENT_PHASES.has(phasePair(a.phase, b.phase));
|
|
340
|
+
}
|
|
341
|
+
var CHANNEL_COMPOSITION = {
|
|
342
|
+
translate: "add"
|
|
343
|
+
};
|
|
344
|
+
function additiveChannels(channels) {
|
|
345
|
+
return channels.length > 0 && channels.every((channel) => CHANNEL_COMPOSITION[channel] === "add");
|
|
346
|
+
}
|
|
347
|
+
function additiveResolution(claims, conflicts) {
|
|
348
|
+
if (conflicts.some((conflict) => conflict.effects[0] === conflict.effects[1])) return void 0;
|
|
349
|
+
const contested = new Set(conflicts.map((conflict) => conflict.channel));
|
|
350
|
+
const involved = /* @__PURE__ */ new Set();
|
|
351
|
+
claims.forEach((claim2, index) => {
|
|
352
|
+
if (claim2.channels.some((channel) => contested.has(channel))) involved.add(index);
|
|
353
|
+
});
|
|
354
|
+
for (const index of involved) if (!claims[index].additive) return void 0;
|
|
355
|
+
return involved;
|
|
356
|
+
}
|
|
357
|
+
function findConflicts(claims) {
|
|
358
|
+
const conflicts = [];
|
|
359
|
+
const seen = /* @__PURE__ */ new Map();
|
|
360
|
+
for (const claim2 of claims) {
|
|
361
|
+
for (const channel of claim2.channels) {
|
|
362
|
+
const claimants = seen.get(channel) ?? [];
|
|
363
|
+
const clash = claimants.find(
|
|
364
|
+
// Same effect name twice in one list claims the channel against itself; still a conflict,
|
|
365
|
+
// and a more likely author typo than a deliberate choice. Phase cannot exempt that pair
|
|
366
|
+
// either, because a phase is only ever independent of a *different* one.
|
|
367
|
+
(other) => gatesOverlap(other.gate, claim2.gate) && !independentPhases(other, claim2)
|
|
368
|
+
);
|
|
369
|
+
if (clash) conflicts.push({ channel, effects: [clash.name, claim2.name] });
|
|
370
|
+
claimants.push(claim2);
|
|
371
|
+
seen.set(channel, claimants);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return conflicts;
|
|
375
|
+
}
|
|
376
|
+
function deliveryClobbers(claims) {
|
|
377
|
+
const stylesheet = claims.filter((claim2) => claim2.delivery === "stylesheet-animation");
|
|
378
|
+
if (stylesheet.length === 0) return [];
|
|
379
|
+
const inline = claims.find((claim2) => claim2.inlineAnimation && !claim2.delivery);
|
|
380
|
+
if (!inline) return [];
|
|
381
|
+
return stylesheet.map(
|
|
382
|
+
(claim2) => `"${inline.name}" writes an inline animation, which outranks the stylesheet rule "${claim2.name}" delivers its motion from`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
function describeConflicts(conflicts) {
|
|
386
|
+
return conflicts.map((c) => `"${c.effects[0]}" and "${c.effects[1]}" both animate ${c.channel}`).join("; ");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/core/sequence.ts
|
|
390
|
+
var DURATION_FALLBACK = "600ms";
|
|
391
|
+
var DELAY_FALLBACK = "0ms";
|
|
392
|
+
function durationExpression(duration, primitiveId) {
|
|
393
|
+
return duration ?? `var(${timingProperty(primitiveId, "duration")}, ${DURATION_FALLBACK})`;
|
|
394
|
+
}
|
|
395
|
+
function delayExpression(delay, primitiveId) {
|
|
396
|
+
return delay ?? `var(${timingProperty(primitiveId, "delay")}, ${DELAY_FALLBACK})`;
|
|
397
|
+
}
|
|
398
|
+
var AT_ANCHOR = /^(with|after)/i;
|
|
399
|
+
var AT_OFFSET = /^([+-])(\d+(?:\.\d+)?|\.\d+)(ms|s)$/i;
|
|
400
|
+
var BARE_TIME = /^(?:\d+(?:\.\d+)?|\.\d+)(?:ms|s)$/i;
|
|
401
|
+
var PROGRESS_DRIVEN = /* @__PURE__ */ new Set(["view", "scroll"]);
|
|
402
|
+
function parsePosition(raw) {
|
|
403
|
+
const value = raw.trim();
|
|
404
|
+
const named = AT_ANCHOR.exec(value);
|
|
405
|
+
const anchor = named?.[1]?.toLowerCase() === "with" ? "start" : "end";
|
|
406
|
+
const offset = named ? value.slice(named[0].length) : value;
|
|
407
|
+
if (offset === "") {
|
|
408
|
+
if (!named) return { ok: false, reason: refusalReason(value) };
|
|
409
|
+
return { ok: true, position: { anchor, offsetMs: 0, term: "" } };
|
|
410
|
+
}
|
|
411
|
+
const parts = AT_OFFSET.exec(offset);
|
|
412
|
+
if (!parts) return { ok: false, reason: refusalReason(value) };
|
|
413
|
+
const [, sign, amount, unit] = parts;
|
|
414
|
+
const time = `${amount}${unit}`;
|
|
415
|
+
const magnitude = toMilliseconds(time, 0);
|
|
416
|
+
return {
|
|
417
|
+
ok: true,
|
|
418
|
+
position: {
|
|
419
|
+
anchor,
|
|
420
|
+
offsetMs: sign === "-" ? -magnitude : magnitude,
|
|
421
|
+
// Spaces around the operator are required by `calc()`, not decoration: `calc(600ms -200ms)`
|
|
422
|
+
// is a syntax error, and a browser drops the whole declaration rather than reporting it.
|
|
423
|
+
term: ` ${sign} ${time}`
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function refusalReason(value) {
|
|
428
|
+
if (BARE_TIME.test(value)) {
|
|
429
|
+
return `at:"${value}" is an absolute position, which is only another spelling of delay:${value} \u2014 write at:+${value} to start that long after the previous effect ends, at:-${value} to overlap it by that much, or at:with to start alongside it`;
|
|
430
|
+
}
|
|
431
|
+
return `at:"${value}" is not a position \u2014 expected at:with, at:after, or a signed time such as at:-200ms, at:+100ms or at:with+150ms`;
|
|
432
|
+
}
|
|
433
|
+
function resolveSequence(members, timeline, warn) {
|
|
434
|
+
const chain = { members, steps: [], warn };
|
|
435
|
+
let warnedAboutTimeline = false;
|
|
436
|
+
for (const [index, member] of members.entries()) {
|
|
437
|
+
if (member.at === void 0) {
|
|
438
|
+
chain.steps.push(unsequenced(member));
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (!warnedAboutTimeline && PROGRESS_DRIVEN.has(timeline)) {
|
|
442
|
+
warnedAboutTimeline = true;
|
|
443
|
+
warn(timelineRefusal(timeline));
|
|
444
|
+
}
|
|
445
|
+
chain.steps.push(place(chain, index));
|
|
446
|
+
}
|
|
447
|
+
return chain.steps;
|
|
448
|
+
}
|
|
449
|
+
function unsequenced(member) {
|
|
450
|
+
return {
|
|
451
|
+
delayExpr: delayExpression(member.delay, member.primitiveId),
|
|
452
|
+
// Falls back to the same `0ms` the compiled `var()` does. An unreadable delay can only reach
|
|
453
|
+
// here from a direct caller — `compile` screens its candidates with `isReadableTime`, and a
|
|
454
|
+
// positional delay was already matched against the parser's own time pattern.
|
|
455
|
+
delayMs: timeMs(member.delay ?? member.cascadeDelay ?? DELAY_FALLBACK) ?? 0,
|
|
456
|
+
sequenced: false
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function place(chain, index) {
|
|
460
|
+
const member = chain.members[index];
|
|
461
|
+
const own = unsequenced(member);
|
|
462
|
+
const parsed = parsePosition(member.at);
|
|
463
|
+
if (!parsed.ok) {
|
|
464
|
+
chain.warn(`"${member.name}": ${parsed.reason}`);
|
|
465
|
+
return own;
|
|
466
|
+
}
|
|
467
|
+
const refused = refusalFor(chain, index);
|
|
468
|
+
if (refused) {
|
|
469
|
+
chain.warn(refused);
|
|
470
|
+
return own;
|
|
471
|
+
}
|
|
472
|
+
if (member.delay !== void 0) {
|
|
473
|
+
chain.warn(
|
|
474
|
+
`"${member.name}" has both a positional delay (${member.delay}) and at:"${member.at}" \u2014 the delay is ignored, because at: positions the effect itself`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return follow(chain, index, parsed.position, own);
|
|
478
|
+
}
|
|
479
|
+
function refusalFor(chain, index) {
|
|
480
|
+
const member = chain.members[index];
|
|
481
|
+
if (index === 0) {
|
|
482
|
+
return `"${member.name}" is the first effect in the list, so at:"${member.at}" has nothing to be relative to \u2014 using its own delay instead`;
|
|
483
|
+
}
|
|
484
|
+
if (!member.positionable) {
|
|
485
|
+
return `"${member.name}" is rendered in JavaScript and declares no "delay", so at:"${member.at}" cannot position it \u2014 it starts with the rest of the list`;
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
function follow(chain, index, position, own) {
|
|
490
|
+
const previous = chain.members[index - 1];
|
|
491
|
+
const anchor = chain.steps[index - 1];
|
|
492
|
+
if (position.anchor === "start") {
|
|
493
|
+
return {
|
|
494
|
+
delayExpr: `${anchor.delayExpr}${position.term}`,
|
|
495
|
+
delayMs: anchor.delayMs + position.offsetMs,
|
|
496
|
+
sequenced: true
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (isInfiniteRepeat(previous.repeat)) return refuseEndless(chain, index, own);
|
|
500
|
+
const durationMs = timeMs(previous.duration ?? previous.cascadeDuration ?? "");
|
|
501
|
+
if (durationMs === void 0) {
|
|
502
|
+
return refuseUnmeasurable(chain, index, own);
|
|
503
|
+
}
|
|
504
|
+
const played = playbackExpression(
|
|
505
|
+
durationExpression(previous.duration, previous.primitiveId),
|
|
506
|
+
previous.repeat
|
|
507
|
+
);
|
|
508
|
+
return {
|
|
509
|
+
delayExpr: `${anchor.delayExpr} + ${played}${position.term}`,
|
|
510
|
+
delayMs: anchor.delayMs + durationMs * (repeatCount(previous.repeat) ?? 1) + position.offsetMs,
|
|
511
|
+
sequenced: true
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function refuseUnmeasurable(chain, index, own) {
|
|
515
|
+
const member = chain.members[index];
|
|
516
|
+
const previous = chain.members[index - 1];
|
|
517
|
+
chain.warn(
|
|
518
|
+
`cannot start "${member.name}" relative to the end of "${previous.name}": "${previous.name}" has no readable duration, so it has no end to measure from \u2014 give it an explicit duration, or use at:with to start alongside it instead`
|
|
519
|
+
);
|
|
520
|
+
return own;
|
|
521
|
+
}
|
|
522
|
+
function refuseEndless(chain, index, own) {
|
|
523
|
+
const member = chain.members[index];
|
|
524
|
+
const previous = chain.members[index - 1];
|
|
525
|
+
chain.warn(
|
|
526
|
+
`cannot start "${member.name}" relative to the end of "${previous.name}": "${previous.name}" has repeat:infinite, so it never ends \u2014 use at:with to start alongside it, or give it a finite repeat count`
|
|
527
|
+
);
|
|
528
|
+
return own;
|
|
529
|
+
}
|
|
530
|
+
function timelineRefusal(timeline) {
|
|
531
|
+
return `at: compiles to an animation-delay, and a "${timeline}" timeline is driven by scroll position rather than a clock, so it ignores one \u2014 position a scroll-driven effect with a range instead (the trailing tokens of data-kui-timeline, e.g. data-kui-timeline="${timeline} entry 0% cover 60%"). The delay still applies where the browser has no scroll-driven animations and the effect degrades to a clock.`;
|
|
532
|
+
}
|
|
533
|
+
function timeMs(value) {
|
|
534
|
+
const ms = toMilliseconds(value, Number.NaN);
|
|
535
|
+
return Number.isFinite(ms) ? ms : void 0;
|
|
536
|
+
}
|
|
537
|
+
function isReadableTime(value) {
|
|
538
|
+
return timeMs(value) !== void 0;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/core/declarations.ts
|
|
542
|
+
function emptyTracks() {
|
|
543
|
+
return {
|
|
544
|
+
names: [],
|
|
545
|
+
keyframes: [],
|
|
546
|
+
durations: [],
|
|
547
|
+
delays: [],
|
|
548
|
+
ends: [],
|
|
549
|
+
easings: [],
|
|
550
|
+
iterationCounts: [],
|
|
551
|
+
directions: [],
|
|
552
|
+
compositions: []
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function keyframesFor(entry) {
|
|
556
|
+
const { preset } = entry.resolved;
|
|
557
|
+
return entry.variant?.keyframes ?? [preset.keyframes ?? `kui-${preset.name}`];
|
|
558
|
+
}
|
|
559
|
+
function iterationCountProperty(presetName) {
|
|
560
|
+
return `--kui-fx-${presetName}-iterations`;
|
|
561
|
+
}
|
|
562
|
+
function pushTrack(tracks, entry, step) {
|
|
563
|
+
const { spec, resolved } = entry;
|
|
564
|
+
const id = resolved.primitive.id;
|
|
565
|
+
const duration = durationExpression(spec.duration, id);
|
|
566
|
+
const delay = staggerDelay(step.delayExpr);
|
|
567
|
+
const end = `${step.delayExpr} + ${playbackExpression(duration, spec.repeat)}`;
|
|
568
|
+
const easing = easingValue(spec.easing, id);
|
|
569
|
+
const iterations = spec.repeat ?? `var(${iterationCountProperty(resolved.preset.name)}, 1)`;
|
|
570
|
+
const direction = directionValue(spec.yoyo);
|
|
571
|
+
const composition = entry.composite ?? "replace";
|
|
572
|
+
for (const name of keyframesFor(entry)) {
|
|
573
|
+
tracks.names.push(gatedAnimationName(name, spec.gate));
|
|
574
|
+
tracks.keyframes.push(name);
|
|
575
|
+
tracks.durations.push(duration);
|
|
576
|
+
tracks.delays.push(delay);
|
|
577
|
+
tracks.ends.push(end);
|
|
578
|
+
tracks.easings.push(easing);
|
|
579
|
+
tracks.iterationCounts.push(iterations);
|
|
580
|
+
tracks.directions.push(direction);
|
|
581
|
+
tracks.compositions.push(composition);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function pushTransitions(segments, owners, entry, warnings) {
|
|
585
|
+
const { spec, resolved } = entry;
|
|
586
|
+
const { preset, primitive } = resolved;
|
|
587
|
+
for (const segment of preset.transitions ?? []) {
|
|
588
|
+
const previousOwner = owners.get(segment.property);
|
|
589
|
+
if (previousOwner !== void 0 && previousOwner !== preset.name) {
|
|
590
|
+
warnings.push(
|
|
591
|
+
`"${previousOwner}" and "${preset.name}" both transition ${segment.property} \u2014 "${preset.name}" wins (last in the list)`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
owners.set(segment.property, preset.name);
|
|
595
|
+
const duration = segment.duration ?? durationExpression(spec.duration, primitive.id);
|
|
596
|
+
const easing = segment.easing ?? easingValue(spec.easing, primitive.id);
|
|
597
|
+
const delay = `var(--kui-tx-delay-${preset.name}, 0ms)`;
|
|
598
|
+
segments.push(`${segment.property} ${duration} ${easing} ${delay}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function declarationsFor(tracks, timeline) {
|
|
602
|
+
if (tracks.names.length === 0) return {};
|
|
603
|
+
const delays = timeline === "pin" ? pinnedDelays(tracks) : tracks.delays.map(wrapped);
|
|
604
|
+
const declarations = {
|
|
605
|
+
"animation-name": tracks.names.join(", "),
|
|
606
|
+
"animation-duration": tracks.durations.join(", "),
|
|
607
|
+
"animation-delay": delays.join(", "),
|
|
608
|
+
"animation-timing-function": tracks.easings.join(", "),
|
|
609
|
+
"animation-iteration-count": tracks.iterationCounts.join(", "),
|
|
610
|
+
"animation-fill-mode": tracks.names.map(() => "both").join(", ")
|
|
611
|
+
};
|
|
612
|
+
if (tracks.directions.some((value) => value !== "normal")) {
|
|
613
|
+
declarations["animation-direction"] = tracks.directions.join(", ");
|
|
614
|
+
}
|
|
615
|
+
if (tracks.compositions.some((value) => value !== "replace")) {
|
|
616
|
+
declarations["animation-composition"] = tracks.compositions.join(", ");
|
|
617
|
+
}
|
|
618
|
+
return declarations;
|
|
619
|
+
}
|
|
620
|
+
function staggerDelay(base) {
|
|
621
|
+
return `${base} + var(--kui-i, 0) * var(--kui-stagger, 0ms)`;
|
|
622
|
+
}
|
|
623
|
+
function wrapped(sum) {
|
|
624
|
+
return `calc(${sum})`;
|
|
625
|
+
}
|
|
626
|
+
function pinnedDelays(tracks) {
|
|
627
|
+
const ends = [...new Set(tracks.ends)];
|
|
628
|
+
const span = ends.length === 1 ? ends[0] : `max(${ends.join(", ")})`;
|
|
629
|
+
const head = `${span} + (var(--kui-stagger-count, 1) - 1) * var(--kui-stagger, 0ms)`;
|
|
630
|
+
return tracks.delays.map((delay) => wrapped(`${delay} - var(--kui-progress, 0) * (${head})`));
|
|
631
|
+
}
|
|
632
|
+
function easingValue(easing, primitiveId) {
|
|
633
|
+
if (!easing) return `var(${timingProperty(primitiveId, "ease")}, ease-out)`;
|
|
634
|
+
return cssEasingValue(easing);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/core/element-config.ts
|
|
638
|
+
var TIMELINES = /* @__PURE__ */ new Set(["time", "view", "scroll", "pointer", "pin"]);
|
|
639
|
+
function readAttributes(el) {
|
|
640
|
+
return {
|
|
641
|
+
source: el.getAttribute(ATTR.source) ?? "",
|
|
642
|
+
on: el.getAttribute(ATTR.on),
|
|
643
|
+
timeline: el.getAttribute(ATTR.timeline),
|
|
644
|
+
threshold: el.getAttribute(ATTR.threshold)
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function resolveConfig(attributes, parsed) {
|
|
648
|
+
const rawTimeline = parsed.timeline ?? attributes.timeline ?? "time";
|
|
649
|
+
const [head = "time", ...rest] = rawTimeline.trim().split(/\s+/);
|
|
650
|
+
const longhand = parseActivationAttribute(attributes.on);
|
|
651
|
+
const authored = parsed.activation ?? longhand.activation;
|
|
652
|
+
return {
|
|
653
|
+
activation: authored ?? "enter",
|
|
654
|
+
activationAuthored: authored !== void 0,
|
|
655
|
+
...sourceFromLonghand({ parsed, longhand }),
|
|
656
|
+
// Re-parsed rather than threaded down, and its warnings dropped: `parse.ts` has already
|
|
657
|
+
// reported every one of them against this same element through `validateToggleActions`, so
|
|
658
|
+
// forwarding them would double each diagnostic. There is no longhand attribute to merge with —
|
|
659
|
+
// `actions:` refines `on:` and has only the inline spelling.
|
|
660
|
+
...parsed.actions === void 0 ? {} : { actions: parseToggleActions(parsed.actions) },
|
|
661
|
+
timeline: TIMELINES.has(head) ? head : "time",
|
|
662
|
+
range: rest.join(" "),
|
|
663
|
+
threshold: parsed.threshold ?? attributes.threshold ?? "0%"
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function resolveTimelineHead(raw, fallback) {
|
|
667
|
+
if (raw === void 0) return fallback;
|
|
668
|
+
const [head] = raw.trim().split(/\s+/);
|
|
669
|
+
return TIMELINES.has(head) ? head : fallback;
|
|
670
|
+
}
|
|
671
|
+
function resolveGroupConfig(base, hoists) {
|
|
672
|
+
if (!hoists) return base;
|
|
673
|
+
const authored = hoists.activation;
|
|
674
|
+
const config = {
|
|
675
|
+
...base,
|
|
676
|
+
activation: authored ?? base.activation,
|
|
677
|
+
activationAuthored: authored !== void 0 || base.activationAuthored
|
|
678
|
+
};
|
|
679
|
+
if (hoists.actions !== void 0) config.actions = parseToggleActions(hoists.actions);
|
|
680
|
+
if (hoists.timeline !== void 0) {
|
|
681
|
+
const [head = "time", ...rest] = hoists.timeline.trim().split(/\s+/);
|
|
682
|
+
config.timeline = TIMELINES.has(head) ? head : base.timeline;
|
|
683
|
+
config.range = rest.join(" ");
|
|
684
|
+
}
|
|
685
|
+
if (hoists.threshold !== void 0) config.threshold = hoists.threshold;
|
|
686
|
+
return config;
|
|
687
|
+
}
|
|
688
|
+
function sourceFromLonghand({
|
|
689
|
+
parsed,
|
|
690
|
+
longhand
|
|
691
|
+
}) {
|
|
692
|
+
if (parsed.activation !== void 0 || longhand.from === void 0) return {};
|
|
693
|
+
return { activationSource: longhand.from };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/core/host-facts.ts
|
|
697
|
+
var RM_RANK = { shorten: 0, crossfade: 1, disable: 2 };
|
|
698
|
+
function resolveDefaultActivation(claims) {
|
|
699
|
+
const declared = claims.map((claim2) => claim2.defaultActivation);
|
|
700
|
+
const firstWins = declared.find((value) => value !== void 0);
|
|
701
|
+
const entrance = claims.findIndex((claim2) => claim2.phase === "entrance");
|
|
702
|
+
if (firstWins === void 0 || entrance === -1) return firstWins;
|
|
703
|
+
return declared[entrance] ?? "enter";
|
|
704
|
+
}
|
|
705
|
+
function mergeHostFacts(targets) {
|
|
706
|
+
let reducedMotion = "shorten";
|
|
707
|
+
for (const { plan } of targets) reducedMotion = strictestPolicy(reducedMotion, plan.reducedMotion);
|
|
708
|
+
for (const { plan } of targets) plan.reducedMotion = reducedMotion;
|
|
709
|
+
}
|
|
710
|
+
function resolvedPolicy(declared, authored, warnings) {
|
|
711
|
+
if (authored === void 0) return declared;
|
|
712
|
+
if (RM_RANK[authored] < RM_RANK[declared]) {
|
|
713
|
+
warnings.push(
|
|
714
|
+
`"rm:${authored}" is weaker than the "${declared}" these effects declare \u2014 keeping "${declared}" (rm: may only strengthen the reduced-motion policy)`
|
|
715
|
+
);
|
|
716
|
+
return declared;
|
|
717
|
+
}
|
|
718
|
+
return authored;
|
|
719
|
+
}
|
|
720
|
+
function intersect(accumulated, supported) {
|
|
721
|
+
if (!accumulated) return [...supported];
|
|
722
|
+
return accumulated.filter((value) => supported.includes(value));
|
|
723
|
+
}
|
|
724
|
+
function strictestPolicy(a, b) {
|
|
725
|
+
return RM_RANK[b] > RM_RANK[a] ? b : a;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/core/unquoted-selectors.ts
|
|
729
|
+
var SELECTOR_START = /^[.#[*:]/;
|
|
730
|
+
var SELECTOR_COMBINATOR = /[>~+]/;
|
|
731
|
+
function looksLikeSelectorFragment(name) {
|
|
732
|
+
return SELECTOR_START.test(name) || SELECTOR_COMBINATOR.test(name);
|
|
733
|
+
}
|
|
734
|
+
function findUnquotedSelectors(specs, registry) {
|
|
735
|
+
const found = [];
|
|
736
|
+
for (const [i, spec] of specs.entries()) {
|
|
737
|
+
const target = specs[i - 1]?.params.target;
|
|
738
|
+
if (!target || registry.has(spec.name) || !looksLikeSelectorFragment(spec.name)) continue;
|
|
739
|
+
found.push({ previous: target, segment: spec.name });
|
|
740
|
+
}
|
|
741
|
+
return found;
|
|
742
|
+
}
|
|
743
|
+
function unquotedSelectorWarning(found) {
|
|
744
|
+
return `target:${found.previous} looks like it continues into "${found.segment}", but a comma always starts a new effect segment \u2014 a target: selector containing a comma must be quoted, e.g. target:'${found.previous}, ${found.segment}'`;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/core/compile.ts
|
|
748
|
+
function channelsFor(entry) {
|
|
749
|
+
const declared = entry.resolved.primitive.channels;
|
|
750
|
+
if (!entry.variant?.channels) return declared;
|
|
751
|
+
return [...declared, ...entry.variant.channels];
|
|
752
|
+
}
|
|
753
|
+
function authoredParams(entry) {
|
|
754
|
+
return entry.variant?.params ?? entry.spec.params;
|
|
755
|
+
}
|
|
756
|
+
function schemaFor(entry) {
|
|
757
|
+
const extra = entry.variant?.schema;
|
|
758
|
+
if (!extra) return entry.resolved.primitive.parameters;
|
|
759
|
+
return { ...entry.resolved.primitive.parameters, ...extra };
|
|
760
|
+
}
|
|
761
|
+
function compileTargets(parsed, registry, timeline) {
|
|
762
|
+
const warnings = [...parsed.warnings];
|
|
763
|
+
const unquoted = findUnquotedSelectors(parsed.specs, registry);
|
|
764
|
+
for (const found of unquoted) warnings.push(unquotedSelectorWarning(found));
|
|
765
|
+
const suspects = unquoted.length > 0 ? { suspectSelectors: unquoted.map((found) => found.segment) } : {};
|
|
766
|
+
const { entries, unknown } = resolveEntries(parsed.specs, registry, warnings);
|
|
767
|
+
if (entries.length === 0) {
|
|
768
|
+
return {
|
|
769
|
+
targets: [{ selector: "", scope: "self", specs: [], plan: emptyPlan(unknown, []) }],
|
|
770
|
+
warnings,
|
|
771
|
+
...suspects
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const sanitized = entries.map(
|
|
775
|
+
(entry) => refusePlayback(refuseContainerGate(entry, warnings), timeline, warnings)
|
|
776
|
+
);
|
|
777
|
+
const steps = resolveSequence(sanitized.map(memberFor), timeline, (m) => warnings.push(m));
|
|
778
|
+
const sequenced = sanitized.map((entry, index) => ({ ...entry, step: steps[index] }));
|
|
779
|
+
warnCrossGroupSequencing(sequenced, parsed.activation, warnings);
|
|
780
|
+
const targets = partitionByTarget(sequenced).map(({ selector, scope, entries: group }) => {
|
|
781
|
+
const groupWarnings = [];
|
|
782
|
+
const composed = resolveComposition(group, registry, groupWarnings);
|
|
783
|
+
const groupHoists = mergeGroupHoists(group, groupWarnings);
|
|
784
|
+
const groupTimeline = resolveTimelineHead(groupHoists?.timeline, timeline);
|
|
785
|
+
const plan = buildPlan(composed, groupTimeline, unknown, groupWarnings);
|
|
786
|
+
plan.defaultActivation = resolveDefaultActivation(
|
|
787
|
+
composed.map((entry) => ({
|
|
788
|
+
phase: phaseOf(entry),
|
|
789
|
+
defaultActivation: entry.resolved.primitive.defaultActivation
|
|
790
|
+
}))
|
|
791
|
+
);
|
|
792
|
+
return { selector, scope, hoists: groupHoists, specs: group.map((entry) => entry.spec), plan };
|
|
793
|
+
});
|
|
794
|
+
mergeHostFacts(targets);
|
|
795
|
+
const reducedMotion = resolvedPolicy(targets[0].plan.reducedMotion, parsed.rm, warnings);
|
|
796
|
+
for (const target of targets) target.plan.reducedMotion = reducedMotion;
|
|
797
|
+
return { targets, warnings, ...suspects };
|
|
798
|
+
}
|
|
799
|
+
function scopeHoists(parsed, registry) {
|
|
800
|
+
if (!parsed.specs.some((spec) => spec.hoists)) return parsed;
|
|
801
|
+
const warnings = [...parsed.warnings];
|
|
802
|
+
const folded = { ...parsed, warnings };
|
|
803
|
+
const specs = parsed.specs.map((spec) => {
|
|
804
|
+
if (!spec.hoists || !staysOnHost(spec, registry)) return spec;
|
|
805
|
+
for (const key of SEGMENT_HOIST_KEYS) {
|
|
806
|
+
const value = spec.hoists[key];
|
|
807
|
+
if (value === void 0) continue;
|
|
808
|
+
assignOnce(folded, key, value, HOIST_LABELS2[key]);
|
|
809
|
+
}
|
|
810
|
+
const rest = { ...spec };
|
|
811
|
+
delete rest.hoists;
|
|
812
|
+
return rest;
|
|
813
|
+
});
|
|
814
|
+
return { ...folded, specs };
|
|
815
|
+
}
|
|
816
|
+
function staysOnHost(spec, registry) {
|
|
817
|
+
const resolved = registry.resolve(spec.name);
|
|
818
|
+
if (!resolved) return true;
|
|
819
|
+
return Object.hasOwn(resolved.primitive.parameters, "target");
|
|
820
|
+
}
|
|
821
|
+
var HOIST_LABELS2 = {
|
|
822
|
+
activation: "activations",
|
|
823
|
+
actions: "crossing actions",
|
|
824
|
+
timeline: "timelines",
|
|
825
|
+
threshold: "thresholds",
|
|
826
|
+
cascade: "stagger steps",
|
|
827
|
+
spread: "stagger budgets",
|
|
828
|
+
order: "stagger orders",
|
|
829
|
+
cols: "stagger column counts",
|
|
830
|
+
along: "stagger axes"
|
|
831
|
+
};
|
|
832
|
+
function mergeGroupHoists(entries, warnings) {
|
|
833
|
+
let merged;
|
|
834
|
+
for (const entry of entries) {
|
|
835
|
+
if (!entry.spec.hoists) continue;
|
|
836
|
+
merged = absorbGroupHoists(merged ?? {}, entry.spec.hoists, warnings);
|
|
837
|
+
}
|
|
838
|
+
return merged;
|
|
839
|
+
}
|
|
840
|
+
function absorbGroupHoists(merged, hoists, warnings) {
|
|
841
|
+
for (const key of SEGMENT_HOIST_KEYS) {
|
|
842
|
+
const value = hoists[key];
|
|
843
|
+
if (value === void 0) continue;
|
|
844
|
+
const current = merged[key];
|
|
845
|
+
if (current === void 0) merged[key] = value;
|
|
846
|
+
else if (current !== value) {
|
|
847
|
+
warnings.push(
|
|
848
|
+
`conflicting ${HOIST_LABELS2[key]} "${current}" and "${value}" in one target: group \u2014 the first wins`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return merged;
|
|
853
|
+
}
|
|
854
|
+
function warnCrossGroupSequencing(sequenced, elementActivation, warnings) {
|
|
855
|
+
for (let i = 1; i < sequenced.length; i++) {
|
|
856
|
+
const entry = sequenced[i];
|
|
857
|
+
if (!entry.step.sequenced) continue;
|
|
858
|
+
const previous = sequenced[i - 1];
|
|
859
|
+
if (groupKeyOf(entry) === groupKeyOf(previous)) continue;
|
|
860
|
+
const ownTrigger = entry.spec.hoists?.activation ?? elementActivation;
|
|
861
|
+
const previousTrigger = previous.spec.hoists?.activation ?? elementActivation;
|
|
862
|
+
if (ownTrigger === previousTrigger) continue;
|
|
863
|
+
warnings.push(
|
|
864
|
+
`"${entry.spec.name}"'s at:"${entry.spec.at}" is relative to "${previous.spec.name}", which targets a different element and may start on a different trigger \u2014 the offset is relative to each one's own start, not a shared clock`
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function groupKeyOf(entry) {
|
|
869
|
+
return `${entry.scope ?? "self"} ${entry.target ?? ""}`;
|
|
870
|
+
}
|
|
871
|
+
function partitionByTarget(entries) {
|
|
872
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
873
|
+
const order = [];
|
|
874
|
+
for (const entry of entries) {
|
|
875
|
+
const selector = entry.target ?? "";
|
|
876
|
+
const scope = entry.scope ?? "self";
|
|
877
|
+
const key = `${scope} ${selector}`;
|
|
878
|
+
let group = byKey.get(key);
|
|
879
|
+
if (!group) {
|
|
880
|
+
group = { selector, scope, entries: [] };
|
|
881
|
+
byKey.set(key, group);
|
|
882
|
+
order.push(group);
|
|
883
|
+
}
|
|
884
|
+
group.entries.push(entry);
|
|
885
|
+
}
|
|
886
|
+
const hostIndex = order.findIndex((group) => group.selector === "");
|
|
887
|
+
if (hostIndex > 0) {
|
|
888
|
+
const [host] = order.splice(hostIndex, 1);
|
|
889
|
+
order.unshift(host);
|
|
890
|
+
}
|
|
891
|
+
return order;
|
|
892
|
+
}
|
|
893
|
+
function emptyPlan(unknown, warnings) {
|
|
894
|
+
return {
|
|
895
|
+
fxNames: [],
|
|
896
|
+
vars: {},
|
|
897
|
+
declarations: {},
|
|
898
|
+
keyframeNames: [],
|
|
899
|
+
jsEffects: [],
|
|
900
|
+
unknown,
|
|
901
|
+
reducedMotion: "shorten",
|
|
902
|
+
supportedActivations: [],
|
|
903
|
+
supportedTimelines: [],
|
|
904
|
+
channels: [],
|
|
905
|
+
warnings
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function resolveEntries(specs, registry, warnings) {
|
|
909
|
+
const entries = [];
|
|
910
|
+
const unknown = [];
|
|
911
|
+
for (const spec of specs) {
|
|
912
|
+
const resolved = registry.resolve(spec.name);
|
|
913
|
+
if (!resolved) {
|
|
914
|
+
unknown.push(spec.name);
|
|
915
|
+
warnUnknownEffect(spec.name, registry, warnings);
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
entries.push(entryFor(spec, resolved, warnings));
|
|
919
|
+
}
|
|
920
|
+
return { entries, unknown };
|
|
921
|
+
}
|
|
922
|
+
function entryFor(spec, resolved, warnings) {
|
|
923
|
+
const lifted = liftTarget(spec, resolved, warnings);
|
|
924
|
+
const variant = resolved.primitive.variantFor?.(lifted.spec, (m) => warnings.push(m));
|
|
925
|
+
const entry = variant ? { spec: lifted.spec, resolved, variant } : { spec: lifted.spec, resolved };
|
|
926
|
+
if (lifted.target !== void 0) {
|
|
927
|
+
entry.target = lifted.target;
|
|
928
|
+
entry.scope = lifted.scope;
|
|
929
|
+
}
|
|
930
|
+
return entry;
|
|
931
|
+
}
|
|
932
|
+
function warnUnknownEffect(name, registry, warnings) {
|
|
933
|
+
const hint = suggest(name, registry.names());
|
|
934
|
+
const suffix = hint ? ` \u2014 did you mean "${hint}"?` : "";
|
|
935
|
+
warnings.push(`unknown effect "${name}"${suffix}`);
|
|
936
|
+
}
|
|
937
|
+
function liftTarget(spec, resolved, warnings) {
|
|
938
|
+
if (Object.hasOwn(resolved.primitive.parameters, "target")) return { spec };
|
|
939
|
+
const target = spec.params.target;
|
|
940
|
+
if (!target) return { spec };
|
|
941
|
+
const rest = { ...spec.params };
|
|
942
|
+
const authoredScope = rest.scope;
|
|
943
|
+
delete rest.target;
|
|
944
|
+
delete rest.scope;
|
|
945
|
+
const stripped = { ...spec, params: rest };
|
|
946
|
+
if (resolved.preset.requiresOwnSubtree) {
|
|
947
|
+
warnings.push(
|
|
948
|
+
`"${resolved.preset.name}" cannot be retargeted \u2014 its CSS reaches past the animated element itself, so "target:${target}" is dropped and it stays on the host`
|
|
949
|
+
);
|
|
950
|
+
return { spec: stripped };
|
|
951
|
+
}
|
|
952
|
+
const scope = authoredScope === "page" ? "page" : "self";
|
|
953
|
+
return { spec: stripped, target, scope };
|
|
954
|
+
}
|
|
955
|
+
function refuseContainerGate(entry, warnings) {
|
|
956
|
+
const { spec, resolved } = entry;
|
|
957
|
+
if (resolved.primitive.renderer === "css-keyframes") return entry;
|
|
958
|
+
if (!spec.gate?.wide && !spec.gate?.narrow) return entry;
|
|
959
|
+
warnings.push(
|
|
960
|
+
`"${spec.name}" ignores "wide:"/"narrow:" \u2014 container gates are not supported on JavaScript-rendered effects yet, so it runs unconditionally`
|
|
961
|
+
);
|
|
962
|
+
const { above, below } = spec.gate;
|
|
963
|
+
const gate = above || below ? { above, below } : void 0;
|
|
964
|
+
return { ...entry, spec: { ...spec, gate } };
|
|
965
|
+
}
|
|
966
|
+
function refusePlayback(entry, timeline, warnings) {
|
|
967
|
+
const { spec, resolved } = entry;
|
|
968
|
+
if (spec.repeat === void 0 && spec.yoyo === void 0) return entry;
|
|
969
|
+
const { repeat, yoyo, warnings: raised } = resolvePlayback({
|
|
970
|
+
name: resolved.preset.name,
|
|
971
|
+
renderer: resolved.primitive.renderer,
|
|
972
|
+
cloak: resolved.preset.cloak,
|
|
973
|
+
timeline,
|
|
974
|
+
repeat: spec.repeat,
|
|
975
|
+
yoyo: spec.yoyo
|
|
976
|
+
});
|
|
977
|
+
warnings.push(...raised);
|
|
978
|
+
return { ...entry, spec: { ...spec, repeat, yoyo } };
|
|
979
|
+
}
|
|
980
|
+
function phaseOf(entry) {
|
|
981
|
+
if (entry.spec.repeat === "infinite") return "idle";
|
|
982
|
+
const { preset } = entry.resolved;
|
|
983
|
+
if (preset.phase) return preset.phase;
|
|
984
|
+
if (preset.transitions?.length) return "state";
|
|
985
|
+
return void 0;
|
|
986
|
+
}
|
|
987
|
+
function additivelyComposable(entry) {
|
|
988
|
+
if (entry.resolved.primitive.renderer !== "css-keyframes") return false;
|
|
989
|
+
if (entry.variant?.keyframes) return false;
|
|
990
|
+
return additiveChannels(channelsFor(entry));
|
|
991
|
+
}
|
|
992
|
+
function resolveComposition(entries, registry, warnings) {
|
|
993
|
+
if (entries.length <= 1) return entries;
|
|
994
|
+
const claims = entries.map((e) => ({
|
|
995
|
+
name: e.spec.name,
|
|
996
|
+
channels: channelsFor(e),
|
|
997
|
+
gate: e.spec.gate,
|
|
998
|
+
phase: phaseOf(e),
|
|
999
|
+
additive: additivelyComposable(e),
|
|
1000
|
+
delivery: e.resolved.preset.delivery,
|
|
1001
|
+
// "Writes `animation-name` inline" needs no declaration of its own: `buildPlan` sends exactly
|
|
1002
|
+
// the `css-keyframes` entries to `pushTrack`, and `declarations.ts` emits the longhands from
|
|
1003
|
+
// the tracks. Only the losing side of a delivery clobber has to say what it is.
|
|
1004
|
+
inlineAnimation: e.resolved.primitive.renderer === "css-keyframes"
|
|
1005
|
+
}));
|
|
1006
|
+
const conflicts = findConflicts(claims);
|
|
1007
|
+
const clobbers = deliveryClobbers(claims);
|
|
1008
|
+
if (conflicts.length === 0 && clobbers.length === 0) return entries;
|
|
1009
|
+
if (clobbers.length === 0) {
|
|
1010
|
+
const additive = additiveResolution(claims, conflicts);
|
|
1011
|
+
if (additive) return entries.map((e, i) => additive.has(i) ? { ...e, composite: "add" } : e);
|
|
1012
|
+
}
|
|
1013
|
+
const combo = registry.findCombo(entries.map((e) => e.spec.name));
|
|
1014
|
+
const remedy = combo ? `Use the "${combo.preset.name}" effect instead.` : "Apply them to nested elements, or register a combined effect.";
|
|
1015
|
+
const kept = entries[0].spec.name;
|
|
1016
|
+
const dropped = entries.slice(1).map((entry) => `"${entry.spec.name}"`);
|
|
1017
|
+
const loss = `Dropped ${dropped.join(", ")} \u2014 only "${kept}" will run.`;
|
|
1018
|
+
const diagnosis = [...conflicts.length > 0 ? [describeConflicts(conflicts)] : [], ...clobbers];
|
|
1019
|
+
warnings.push(`cannot compose: ${diagnosis.join("; ")}. ${loss} ${remedy}`);
|
|
1020
|
+
return [entries[0]];
|
|
1021
|
+
}
|
|
1022
|
+
function buildPlan(entries, timeline, unknown, warnings) {
|
|
1023
|
+
const plan = emptyPlan(unknown, warnings);
|
|
1024
|
+
const tracks = emptyTracks();
|
|
1025
|
+
const channels = /* @__PURE__ */ new Set();
|
|
1026
|
+
const transitionSegments = [];
|
|
1027
|
+
const transitionOwners = /* @__PURE__ */ new Map();
|
|
1028
|
+
let activations;
|
|
1029
|
+
let timelines;
|
|
1030
|
+
for (const entry of entries) {
|
|
1031
|
+
const { preset, primitive } = entry.resolved;
|
|
1032
|
+
const step = entry.step;
|
|
1033
|
+
plan.fxNames.push(preset.name);
|
|
1034
|
+
plan.reducedMotion = strictestPolicy(plan.reducedMotion, primitive.reducedMotion);
|
|
1035
|
+
activations = intersect(activations, primitive.supportedActivations);
|
|
1036
|
+
timelines = intersect(timelines, primitive.supportedTimelines);
|
|
1037
|
+
for (const channel of channelsFor(entry)) channels.add(channel);
|
|
1038
|
+
warnUnsupportedTimeline(preset.name, primitive.supportedTimelines, timeline, warnings);
|
|
1039
|
+
Object.assign(
|
|
1040
|
+
plan.vars,
|
|
1041
|
+
resolveParams(authoredParams(entry), schemaFor(entry), (m) => warnings.push(m))
|
|
1042
|
+
);
|
|
1043
|
+
if (primitive.renderer === "css-keyframes") pushTrack(tracks, entry, step);
|
|
1044
|
+
else plan.jsEffects.push(positioned(entry, step));
|
|
1045
|
+
pushTransitions(transitionSegments, transitionOwners, entry, warnings);
|
|
1046
|
+
}
|
|
1047
|
+
Object.assign(plan.declarations, declarationsFor(tracks, timeline));
|
|
1048
|
+
plan.keyframeNames = tracks.keyframes;
|
|
1049
|
+
if (transitionSegments.length > 0) plan.transition = transitionSegments.join(", ");
|
|
1050
|
+
plan.supportedActivations = activations;
|
|
1051
|
+
plan.supportedTimelines = timelines;
|
|
1052
|
+
plan.channels = [...channels];
|
|
1053
|
+
return plan;
|
|
1054
|
+
}
|
|
1055
|
+
function memberFor(entry) {
|
|
1056
|
+
const { spec, resolved } = entry;
|
|
1057
|
+
const { preset, primitive } = resolved;
|
|
1058
|
+
const authored = authoredParams(entry);
|
|
1059
|
+
return {
|
|
1060
|
+
name: preset.name,
|
|
1061
|
+
primitiveId: primitive.id,
|
|
1062
|
+
at: spec.at,
|
|
1063
|
+
delay: spec.delay,
|
|
1064
|
+
duration: spec.duration,
|
|
1065
|
+
cascadeDelay: cascadeValue(authored, preset, primitive.parameters, "delay"),
|
|
1066
|
+
cascadeDuration: cascadeValue(authored, preset, primitive.parameters, "duration"),
|
|
1067
|
+
repeat: spec.repeat,
|
|
1068
|
+
// A `css-keyframes` segment is always positionable — it compiles to an `animation-delay` and
|
|
1069
|
+
// the browser honours it. A JavaScript-rendered one is positionable only if it declares the
|
|
1070
|
+
// parameter, which is the single compile-time signal that it reads a delay at all.
|
|
1071
|
+
positionable: primitive.renderer === "css-keyframes" || Object.hasOwn(primitive.parameters, "delay")
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function cascadeValue(authored, preset, schema, name) {
|
|
1075
|
+
const candidates = [authored[name], preset.params?.[name], schema[name]?.default];
|
|
1076
|
+
return candidates.find((value) => value !== void 0 && isReadableTime(value));
|
|
1077
|
+
}
|
|
1078
|
+
function positioned(entry, step) {
|
|
1079
|
+
if (!step.sequenced) return entry;
|
|
1080
|
+
return { ...entry, sequencedDelayMs: step.delayMs };
|
|
1081
|
+
}
|
|
1082
|
+
function warnUnsupportedTimeline(name, supported, timeline, warnings) {
|
|
1083
|
+
if (supported.includes(timeline)) return;
|
|
1084
|
+
warnings.push(
|
|
1085
|
+
`"${name}" does not support timeline "${timeline}" (supports: ${supported.join(", ")})`
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/core/derived/book.ts
|
|
1090
|
+
function createDerivedBook() {
|
|
1091
|
+
return {
|
|
1092
|
+
derived: /* @__PURE__ */ new WeakMap(),
|
|
1093
|
+
hosts: /* @__PURE__ */ new WeakMap(),
|
|
1094
|
+
groups: /* @__PURE__ */ new WeakMap(),
|
|
1095
|
+
contexts: /* @__PURE__ */ new WeakMap(),
|
|
1096
|
+
unions: /* @__PURE__ */ new WeakMap(),
|
|
1097
|
+
cleanups: /* @__PURE__ */ new WeakMap()
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
function derivedOf(book, el) {
|
|
1101
|
+
return [...book.derived.get(el) ?? []];
|
|
1102
|
+
}
|
|
1103
|
+
function hostOf(book, el) {
|
|
1104
|
+
return book.hosts.get(el);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/core/derived/aggregate.ts
|
|
1108
|
+
function isForwardRunning(state) {
|
|
1109
|
+
return state.status === "running" && state.direction !== "reverse";
|
|
1110
|
+
}
|
|
1111
|
+
function isRunning(state) {
|
|
1112
|
+
return state.status === "running";
|
|
1113
|
+
}
|
|
1114
|
+
function isFailed(state) {
|
|
1115
|
+
return state.status === "failed";
|
|
1116
|
+
}
|
|
1117
|
+
function isSettled(state) {
|
|
1118
|
+
return state.status === "finished" || state.status === "failed";
|
|
1119
|
+
}
|
|
1120
|
+
function isReady(state) {
|
|
1121
|
+
return state.status === "ready";
|
|
1122
|
+
}
|
|
1123
|
+
function matchStates(port, host) {
|
|
1124
|
+
const states = [];
|
|
1125
|
+
for (const match of derivedOf(port.book, host)) {
|
|
1126
|
+
const state = port.stateOf(match);
|
|
1127
|
+
if (state) states.push(state);
|
|
1128
|
+
}
|
|
1129
|
+
return states;
|
|
1130
|
+
}
|
|
1131
|
+
function applyRunning(port, host, hostState, states) {
|
|
1132
|
+
if (hostState.status === "running") return;
|
|
1133
|
+
port.writeStatus(host, hostState, "running");
|
|
1134
|
+
if (states.some(isForwardRunning)) port.emit(host, hostState, KUI_EVENT.start, "activated");
|
|
1135
|
+
}
|
|
1136
|
+
function applyFinished(port, host, hostState, states) {
|
|
1137
|
+
if (hostState.status === "finished") return;
|
|
1138
|
+
const wasRunning = hostState.status === "running";
|
|
1139
|
+
port.writeStatus(host, hostState, "finished");
|
|
1140
|
+
if (states.every((state) => state.cancelled === true)) return;
|
|
1141
|
+
port.emit(host, hostState, KUI_EVENT.finish, wasRunning ? "complete" : "reduced-motion");
|
|
1142
|
+
}
|
|
1143
|
+
function syncAggregate(port, host) {
|
|
1144
|
+
const hostState = port.stateOf(host);
|
|
1145
|
+
if (!hostState || !hostState.aggregate) return;
|
|
1146
|
+
const states = matchStates(port, host);
|
|
1147
|
+
if (states.length === 0) return;
|
|
1148
|
+
if (states.some(isRunning)) {
|
|
1149
|
+
applyRunning(port, host, hostState, states);
|
|
1150
|
+
} else if (states.every(isFailed)) {
|
|
1151
|
+
port.writeStatus(host, hostState, "failed");
|
|
1152
|
+
} else if (states.every(isSettled)) {
|
|
1153
|
+
applyFinished(port, host, hostState, states);
|
|
1154
|
+
} else if (states.every(isReady)) {
|
|
1155
|
+
port.writeStatus(host, hostState, "ready");
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
function unionKey(targets) {
|
|
1159
|
+
return targets.map((target) => `${target.scope} ${target.selector}`).sort((a, b) => a.localeCompare(b)).join("");
|
|
1160
|
+
}
|
|
1161
|
+
function compileUnion(port, context, targets) {
|
|
1162
|
+
const key = unionKey(targets);
|
|
1163
|
+
const cache = port.book.unions.get(context.host);
|
|
1164
|
+
const cached2 = cache?.get(key);
|
|
1165
|
+
if (cached2) return cached2;
|
|
1166
|
+
const first = targets[0];
|
|
1167
|
+
const compiled = compileTargets(
|
|
1168
|
+
{ specs: targets.flatMap((target) => target.specs), warnings: [] },
|
|
1169
|
+
port.registry,
|
|
1170
|
+
context.timeline
|
|
1171
|
+
);
|
|
1172
|
+
const union = compiled.targets[0];
|
|
1173
|
+
union.plan.reducedMotion = first.plan.reducedMotion;
|
|
1174
|
+
const win = context.host.ownerDocument?.defaultView ?? void 0;
|
|
1175
|
+
union.plan.jsEffects = union.plan.jsEffects.filter((entry) => gateMatches(entry.spec.gate, win));
|
|
1176
|
+
const known = new Set(targets.flatMap((target) => target.plan.warnings));
|
|
1177
|
+
for (const warning of union.plan.warnings) {
|
|
1178
|
+
if (!known.has(warning)) port.reporter.warn(warning, context.host);
|
|
1179
|
+
}
|
|
1180
|
+
const result = { ...union, selector: first.selector, scope: first.scope };
|
|
1181
|
+
const unions = cache ?? /* @__PURE__ */ new Map();
|
|
1182
|
+
unions.set(key, result);
|
|
1183
|
+
if (!cache) port.book.unions.set(context.host, unions);
|
|
1184
|
+
return result;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/core/style-plan.ts
|
|
1188
|
+
function planStyles(input) {
|
|
1189
|
+
const { plan, config, capabilities, respectReducedMotion } = input;
|
|
1190
|
+
const reduce = respectReducedMotion && capabilities.reducedMotion;
|
|
1191
|
+
const { scrubbed, useNativeTimeline } = timelineMode(plan, config, capabilities);
|
|
1192
|
+
const properties = { ...plan.vars, ...plan.declarations };
|
|
1193
|
+
Object.assign(properties, transitionProperty(plan));
|
|
1194
|
+
Object.assign(properties, timelineProperties(config, capabilities, useNativeTimeline));
|
|
1195
|
+
const hasCssAnimation = Object.keys(plan.declarations).length > 0;
|
|
1196
|
+
const elementHasCssAnimation = input.elementHasCssAnimation ?? hasCssAnimation;
|
|
1197
|
+
const gate = resolveGate({
|
|
1198
|
+
useNativeTimeline,
|
|
1199
|
+
scrubbed: scrubbed && elementHasCssAnimation,
|
|
1200
|
+
reduce,
|
|
1201
|
+
activation: config.activation,
|
|
1202
|
+
// JS effects are gated too. They emit no `animation` declaration, so only the play-state
|
|
1203
|
+
// write is skipped — the activation itself still has to be bound, or `on:enter` and
|
|
1204
|
+
// `on:click` would silently do nothing for every pinned, dragged, or morphing element.
|
|
1205
|
+
hasWork: elementHasCssAnimation || plan.jsEffects.length > 0,
|
|
1206
|
+
hasCssAnimation,
|
|
1207
|
+
// A browser lacking standalone translate/rotate/scale support silently ignores any
|
|
1208
|
+
// `@keyframes` step written in those properties — the animation never visibly completes. An
|
|
1209
|
+
// effect deferred on that promise would sit paused (or, for an entrance reveal, invisible)
|
|
1210
|
+
// forever, so it must reach its final state immediately instead, the same fail-open rule
|
|
1211
|
+
// already applied under reduced motion.
|
|
1212
|
+
unsupportedTransform: needsIndividualTransforms(plan.channels, capabilities)
|
|
1213
|
+
});
|
|
1214
|
+
if (gate === "deferred" || gate === "scrubbed") properties["animation-play-state"] = "paused";
|
|
1215
|
+
return {
|
|
1216
|
+
properties,
|
|
1217
|
+
attributes: {
|
|
1218
|
+
[ATTR.normalized]: plan.fxNames.join(" "),
|
|
1219
|
+
[ATTR.rm]: plan.reducedMotion,
|
|
1220
|
+
[ATTR.state]: "ready"
|
|
1221
|
+
},
|
|
1222
|
+
gate,
|
|
1223
|
+
activation: gate === "deferred" ? effectiveActivation(config) : null
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
function timelineMode(plan, config, capabilities) {
|
|
1227
|
+
const scrubbed = config.timeline === "pin" && plan.supportedTimelines.includes("pin");
|
|
1228
|
+
const useNativeTimeline = !scrubbed && supportsTimeline(config.timeline, capabilities) && plan.supportedTimelines.includes(config.timeline);
|
|
1229
|
+
return { scrubbed, useNativeTimeline };
|
|
1230
|
+
}
|
|
1231
|
+
function transitionProperty(plan) {
|
|
1232
|
+
return plan.transition ? { "--kui-transition": plan.transition } : {};
|
|
1233
|
+
}
|
|
1234
|
+
function timelineProperties(config, capabilities, useNativeTimeline) {
|
|
1235
|
+
if (!useNativeTimeline) return {};
|
|
1236
|
+
const properties = {
|
|
1237
|
+
"animation-timeline": config.timeline === "scroll" ? "scroll()" : "view()"
|
|
1238
|
+
};
|
|
1239
|
+
if (capabilities.animationRange) {
|
|
1240
|
+
properties["animation-range"] = config.range || (config.timeline === "scroll" ? "0% 100%" : "entry 0% cover 60%");
|
|
1241
|
+
}
|
|
1242
|
+
return properties;
|
|
1243
|
+
}
|
|
1244
|
+
function resolveGate(input) {
|
|
1245
|
+
if (input.useNativeTimeline) return "native-timeline";
|
|
1246
|
+
if (input.scrubbed) return "scrubbed";
|
|
1247
|
+
if (!input.hasWork) return "immediate";
|
|
1248
|
+
if (input.reduce || startKindOf(input.activation) === "immediate" || input.unsupportedTransform) {
|
|
1249
|
+
return "immediate";
|
|
1250
|
+
}
|
|
1251
|
+
return "deferred";
|
|
1252
|
+
}
|
|
1253
|
+
function needsIndividualTransforms(channels, capabilities) {
|
|
1254
|
+
if (capabilities.individualTransforms) return false;
|
|
1255
|
+
return channels.some((c) => c === "translate" || c === "rotate" || c === "scale");
|
|
1256
|
+
}
|
|
1257
|
+
function supportsTimeline(timeline, capabilities) {
|
|
1258
|
+
if (timeline === "view") return capabilities.viewTimeline;
|
|
1259
|
+
if (timeline === "scroll") return capabilities.scrollTimeline;
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
function effectiveActivation(config) {
|
|
1263
|
+
const observed = "enter";
|
|
1264
|
+
if (config.timeline !== "time" && startKindOf(config.activation) === "manual") return observed;
|
|
1265
|
+
return config.activation;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/core/stagger-keys.ts
|
|
1269
|
+
var GROUP_KEY_NAMES = ["cascade", "spread", "order", "cols", "along"];
|
|
1270
|
+
function collectKeys(lookup) {
|
|
1271
|
+
const keys = {};
|
|
1272
|
+
for (const name of GROUP_KEY_NAMES) {
|
|
1273
|
+
const value = lookup(name);
|
|
1274
|
+
if (value !== void 0) keys[name] = value;
|
|
1275
|
+
}
|
|
1276
|
+
return Object.keys(keys).length > 0 ? keys : void 0;
|
|
1277
|
+
}
|
|
1278
|
+
function elementStaggerKeys(parsed) {
|
|
1279
|
+
return collectKeys((name) => parsed[name]);
|
|
1280
|
+
}
|
|
1281
|
+
function staggerKeysFor(scoped, elementWide) {
|
|
1282
|
+
return collectKeys((name) => scoped?.[name] ?? elementWide?.[name]);
|
|
1283
|
+
}
|
|
1284
|
+
function isGroupedKeys(keys) {
|
|
1285
|
+
return keys?.cascade !== void 0 || keys?.spread !== void 0 || keys?.order !== void 0;
|
|
1286
|
+
}
|
|
1287
|
+
var TARGET_GROUP_HOSTS = /* @__PURE__ */ new WeakMap();
|
|
1288
|
+
function claimTargetGroupHost(host) {
|
|
1289
|
+
TARGET_GROUP_HOSTS.set(host, (TARGET_GROUP_HOSTS.get(host) ?? 0) + 1);
|
|
1290
|
+
let released = false;
|
|
1291
|
+
return () => {
|
|
1292
|
+
if (released) return;
|
|
1293
|
+
released = true;
|
|
1294
|
+
const remaining = TARGET_GROUP_HOSTS.get(host) - 1;
|
|
1295
|
+
if (remaining > 0) TARGET_GROUP_HOSTS.set(host, remaining);
|
|
1296
|
+
else TARGET_GROUP_HOSTS.delete(host);
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
function isTargetGroupHost(el) {
|
|
1300
|
+
return TARGET_GROUP_HOSTS.has(el);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// src/core/derived/group-gate.ts
|
|
1304
|
+
function groupGateOwner(port, context, target) {
|
|
1305
|
+
const keys = staggerKeysFor(target.hoists, elementStaggerKeys(context.parsed));
|
|
1306
|
+
return isGroupedKeys(keys) ? context.host : void 0;
|
|
1307
|
+
}
|
|
1308
|
+
function skipsOwnBinding(state) {
|
|
1309
|
+
return state.gateOwner !== void 0;
|
|
1310
|
+
}
|
|
1311
|
+
function activateGroup(port, group, oneShot) {
|
|
1312
|
+
for (const member of group.members) {
|
|
1313
|
+
if (oneShot && port.stateOf(member)?.status !== "ready") continue;
|
|
1314
|
+
port.activateOne(member);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function deactivateGroup(port, group) {
|
|
1318
|
+
for (const member of group.members) port.deactivateOne(member);
|
|
1319
|
+
}
|
|
1320
|
+
function registerCleanup(port, host, cleanup) {
|
|
1321
|
+
const cleanups = port.book.cleanups.get(host) ?? [];
|
|
1322
|
+
cleanups.push(cleanup);
|
|
1323
|
+
port.book.cleanups.set(host, cleanups);
|
|
1324
|
+
}
|
|
1325
|
+
function bindOneGroup(port, host, group) {
|
|
1326
|
+
const stylePlan = planStyles({
|
|
1327
|
+
plan: group.target.plan,
|
|
1328
|
+
config: group.config,
|
|
1329
|
+
capabilities: port.capabilities,
|
|
1330
|
+
respectReducedMotion: port.respectReducedMotion
|
|
1331
|
+
});
|
|
1332
|
+
if (stylePlan.gate !== "deferred") return;
|
|
1333
|
+
const activation = stylePlan.activation;
|
|
1334
|
+
const oneShot = isOneShot(resolveActivationSpec(activation));
|
|
1335
|
+
const release = port.binder.bind(host, activation, {
|
|
1336
|
+
threshold: group.config.threshold,
|
|
1337
|
+
from: group.config.activationSource,
|
|
1338
|
+
activate: () => activateGroup(port, group, oneShot),
|
|
1339
|
+
deactivate: () => deactivateGroup(port, group)
|
|
1340
|
+
});
|
|
1341
|
+
registerCleanup(port, host, release);
|
|
1342
|
+
}
|
|
1343
|
+
function bindGroups(port, host) {
|
|
1344
|
+
const groups = port.book.groups.get(host);
|
|
1345
|
+
if (groups === void 0) return;
|
|
1346
|
+
for (const group of groups) {
|
|
1347
|
+
if (group.gateOwner === host) bindOneGroup(port, host, group);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// src/core/stagger-config.ts
|
|
1352
|
+
var FROM_KEYWORDS = /* @__PURE__ */ new Set(["start", "end", "center", "edges", "random"]);
|
|
1353
|
+
var ORDER_KEYS = /* @__PURE__ */ new Set(["from", "order"]);
|
|
1354
|
+
var SPREAD_KEY = "spread";
|
|
1355
|
+
var COLS_KEY = "cols";
|
|
1356
|
+
var AXIS_KEYS = /* @__PURE__ */ new Set(["axis", "along"]);
|
|
1357
|
+
var AXIS_VALUES = /* @__PURE__ */ new Set(["x", "y"]);
|
|
1358
|
+
var ORIGIN_RE = /^(\d+(?:\.\d+)?|\.\d+)\/(\d+(?:\.\d+)?|\.\d+)$/;
|
|
1359
|
+
var COLS_RE = /^\d{1,9}$/;
|
|
1360
|
+
var PAIR_RE = /^([a-zA-Z-]+):(.*)$/;
|
|
1361
|
+
var INDEX_RE = /^-?\d{1,9}$/;
|
|
1362
|
+
function parseStaggerTokens(value, warnings) {
|
|
1363
|
+
const config = { from: "start" };
|
|
1364
|
+
const seen = { from: false, spread: false, cols: false, along: false };
|
|
1365
|
+
for (const token of splitTopLevel(value, " ", warnings)) {
|
|
1366
|
+
const pair = PAIR_RE.exec(token);
|
|
1367
|
+
if (pair) applyStaggerPair(pair[1], pair[2], { config, seen, warnings });
|
|
1368
|
+
else config.step = keepFirstStep(config.step, token, warnings);
|
|
1369
|
+
}
|
|
1370
|
+
return { config, sawFrom: seen.from };
|
|
1371
|
+
}
|
|
1372
|
+
function applyStaggerPair(key, raw, state) {
|
|
1373
|
+
const slot = slotFor(key);
|
|
1374
|
+
if (slot === void 0) {
|
|
1375
|
+
state.warnings.push(
|
|
1376
|
+
`unrecognised key "${key}" in data-kui-stagger \u2014 expected a time step, "spread:", "cols:", "along:", "from:" or "order:"`
|
|
1377
|
+
);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (claim(slot, raw, state, key)) ASSIGN[slot](raw, state, key);
|
|
1381
|
+
}
|
|
1382
|
+
function slotFor(key) {
|
|
1383
|
+
if (key === SPREAD_KEY) return "spread";
|
|
1384
|
+
if (key === COLS_KEY) return "cols";
|
|
1385
|
+
if (AXIS_KEYS.has(key)) return "along";
|
|
1386
|
+
if (ORDER_KEYS.has(key)) return "from";
|
|
1387
|
+
return void 0;
|
|
1388
|
+
}
|
|
1389
|
+
var ASSIGN = {
|
|
1390
|
+
spread: (raw, state) => {
|
|
1391
|
+
state.config.spread = raw.trim();
|
|
1392
|
+
},
|
|
1393
|
+
cols: (raw, state) => {
|
|
1394
|
+
state.config.cols = parseCols(raw, state.warnings);
|
|
1395
|
+
},
|
|
1396
|
+
along: (raw, state, key) => {
|
|
1397
|
+
state.config.along = parseAxis(raw, state.warnings, key);
|
|
1398
|
+
},
|
|
1399
|
+
from: (raw, state, key) => {
|
|
1400
|
+
state.config.from = parseFrom(raw, state.warnings, key);
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
function claim(slot, raw, state, spelling) {
|
|
1404
|
+
if (state.seen[slot]) {
|
|
1405
|
+
state.warnings.push(`duplicate "${spelling}:" in data-kui-stagger \u2014 "${raw}" ignored`);
|
|
1406
|
+
return false;
|
|
1407
|
+
}
|
|
1408
|
+
state.seen[slot] = true;
|
|
1409
|
+
return true;
|
|
1410
|
+
}
|
|
1411
|
+
function parseCols(raw, warnings) {
|
|
1412
|
+
const value = raw.trim();
|
|
1413
|
+
if (value === "auto") return "auto";
|
|
1414
|
+
if (COLS_RE.test(value) && Number(value) > 0) return Number(value);
|
|
1415
|
+
warnings.push(`unrecognised "cols:${raw}" \u2014 expected a column count or "auto"`);
|
|
1416
|
+
return void 0;
|
|
1417
|
+
}
|
|
1418
|
+
function parseAxis(raw, warnings, key) {
|
|
1419
|
+
const value = raw.trim();
|
|
1420
|
+
if (AXIS_VALUES.has(value)) return value;
|
|
1421
|
+
warnings.push(`unrecognised "${key}:${raw}" \u2014 expected x or y`);
|
|
1422
|
+
return void 0;
|
|
1423
|
+
}
|
|
1424
|
+
function resolveStaggerConfig(attribute, source, warnings = []) {
|
|
1425
|
+
const inline = inlineGroupKeys(source);
|
|
1426
|
+
if (attribute === null && inline === void 0) return void 0;
|
|
1427
|
+
return resolveStaggerConfigFrom(attribute, inline, warnings);
|
|
1428
|
+
}
|
|
1429
|
+
function resolveStaggerConfigFrom(attribute, inline, warnings = []) {
|
|
1430
|
+
if (attribute === null && inline === void 0) return void 0;
|
|
1431
|
+
const { config: longhand, sawFrom } = parseStaggerTokens(attribute ?? "", warnings);
|
|
1432
|
+
return oneStepMode(screenStep(mergeInline(longhand, sawFrom, inline ?? {}, warnings), warnings), warnings);
|
|
1433
|
+
}
|
|
1434
|
+
function oneStepMode(config, warnings) {
|
|
1435
|
+
if (config.spread === void 0 || config.step === void 0) return config;
|
|
1436
|
+
warnings.push(
|
|
1437
|
+
`stagger step "${config.step}" is ignored \u2014 "spread:${config.spread}" already budgets the whole group, and the step is what it divides out`
|
|
1438
|
+
);
|
|
1439
|
+
const budgeted = { ...config };
|
|
1440
|
+
delete budgeted.step;
|
|
1441
|
+
return budgeted;
|
|
1442
|
+
}
|
|
1443
|
+
function inlineGroupKeys(source) {
|
|
1444
|
+
if (!hasGroupKey(source)) return void 0;
|
|
1445
|
+
const { cascade, spread, order, cols, along } = parse(source);
|
|
1446
|
+
const keys = { cascade, spread, order, cols, along };
|
|
1447
|
+
return Object.values(keys).some((value) => value !== void 0) ? keys : void 0;
|
|
1448
|
+
}
|
|
1449
|
+
function screenStep(config, warnings) {
|
|
1450
|
+
const screened = { ...config };
|
|
1451
|
+
if (!keepValue("step", config.step, warnings)) delete screened.step;
|
|
1452
|
+
if (!keepValue("spread", config.spread, warnings)) delete screened.spread;
|
|
1453
|
+
return screened;
|
|
1454
|
+
}
|
|
1455
|
+
function keepValue(label, value, warnings) {
|
|
1456
|
+
if (value === void 0) return false;
|
|
1457
|
+
if (isSafeCssValue(value)) return true;
|
|
1458
|
+
warnings.push(`stagger ${label} "${value}" contains disallowed CSS syntax \u2014 ignored`);
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
function mergeInline(longhand, sawFrom, inline, warnings) {
|
|
1462
|
+
const config = { ...longhand };
|
|
1463
|
+
if (inline.cascade !== void 0) {
|
|
1464
|
+
warnOverride("stagger step", longhand.step, inline.cascade, warnings);
|
|
1465
|
+
config.step = inline.cascade;
|
|
1466
|
+
}
|
|
1467
|
+
if (inline.spread !== void 0) {
|
|
1468
|
+
warnOverride("stagger budget", longhand.spread, inline.spread, warnings);
|
|
1469
|
+
config.spread = inline.spread;
|
|
1470
|
+
}
|
|
1471
|
+
mergeInlineGrid(config, longhand, inline, warnings);
|
|
1472
|
+
if (inline.order !== void 0) {
|
|
1473
|
+
const order = parseFrom(inline.order, warnings, "order");
|
|
1474
|
+
warnOverride("stagger order", sawFrom ? longhand.from : void 0, order, warnings);
|
|
1475
|
+
config.from = order;
|
|
1476
|
+
}
|
|
1477
|
+
return config;
|
|
1478
|
+
}
|
|
1479
|
+
function mergeInlineGrid(config, longhand, inline, warnings) {
|
|
1480
|
+
if (inline.cols !== void 0) {
|
|
1481
|
+
const cols = parseCols(inline.cols, warnings);
|
|
1482
|
+
warnOverride("stagger columns", longhand.cols, cols, warnings);
|
|
1483
|
+
if (cols !== void 0) config.cols = cols;
|
|
1484
|
+
}
|
|
1485
|
+
if (inline.along !== void 0) {
|
|
1486
|
+
const along = parseAxis(inline.along, warnings, "along");
|
|
1487
|
+
warnOverride("stagger axis", longhand.along, along, warnings);
|
|
1488
|
+
if (along !== void 0) config.along = along;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
function hasGroupKey(source) {
|
|
1492
|
+
return GROUP_KEY_PREFIXES.some((prefix) => source.includes(prefix));
|
|
1493
|
+
}
|
|
1494
|
+
var GROUP_KEY_PREFIXES = [
|
|
1495
|
+
"cascade:",
|
|
1496
|
+
"spread:",
|
|
1497
|
+
"order:",
|
|
1498
|
+
"cols:",
|
|
1499
|
+
"along:"
|
|
1500
|
+
];
|
|
1501
|
+
function warnOverride(label, previous, next, warnings) {
|
|
1502
|
+
if (previous === void 0 || next === void 0 || previous === next) return;
|
|
1503
|
+
warnings.push(
|
|
1504
|
+
`conflicting ${label}: data-kui-stagger says "${String(previous)}", data-kui says "${String(next)}" \u2014 data-kui wins`
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
function keepFirstStep(current, token, warnings) {
|
|
1508
|
+
if (current === void 0) return token;
|
|
1509
|
+
warnings.push(`extra token "${token}" in data-kui-stagger (expected one time step)`);
|
|
1510
|
+
return current;
|
|
1511
|
+
}
|
|
1512
|
+
function parseFrom(raw, warnings, key = "from") {
|
|
1513
|
+
const value = raw.trim();
|
|
1514
|
+
if (FROM_KEYWORDS.has(value)) return value;
|
|
1515
|
+
if (INDEX_RE.test(value)) return Number(value);
|
|
1516
|
+
const origin = ORIGIN_RE.exec(value);
|
|
1517
|
+
if (origin) return { x: clamp01(Number(origin[1]), value, warnings), y: clamp01(Number(origin[2]), value, warnings) };
|
|
1518
|
+
warnings.push(
|
|
1519
|
+
`unrecognised "${key}:${raw}" \u2014 expected start, end, center, edges, random, a child index, or an "x/y" point in a grid`
|
|
1520
|
+
);
|
|
1521
|
+
return "start";
|
|
1522
|
+
}
|
|
1523
|
+
function clamp01(value, source, warnings) {
|
|
1524
|
+
if (value >= 0 && value <= 1) return value;
|
|
1525
|
+
warnings.push(`stagger order "${source}" is outside the grid (0 to 1 on each axis) \u2014 clamped`);
|
|
1526
|
+
return Math.min(Math.max(value, 0), 1);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// src/core/stagger.ts
|
|
1530
|
+
var RANK_PRECISION = 1e3;
|
|
1531
|
+
var RANDOM_SALT = 2654435761;
|
|
1532
|
+
var COUNT_SALT = 2246822507;
|
|
1533
|
+
var MIX_A = 569420461;
|
|
1534
|
+
var MIX_B = 1935289751;
|
|
1535
|
+
function resolveStep(config, maxRank) {
|
|
1536
|
+
if (config.spread === void 0) return config.step;
|
|
1537
|
+
if (maxRank <= 0) return "0ms";
|
|
1538
|
+
return `calc((${config.spread}) / ${String(maxRank)})`;
|
|
1539
|
+
}
|
|
1540
|
+
function staggerRanks(count, from, warnings = [], layout) {
|
|
1541
|
+
if (count <= 0) return [];
|
|
1542
|
+
if (from === "random") return randomRanks(count);
|
|
1543
|
+
if (layout) return gridRanks(count, from, layout, warnings);
|
|
1544
|
+
if (typeof from === "object") {
|
|
1545
|
+
warnings.push(
|
|
1546
|
+
`stagger order "${String(from.x)}/${String(from.y)}" is a point in a grid, but this group has no "cols:" \u2014 add one, or name an edge with start/end/center/edges`
|
|
1547
|
+
);
|
|
1548
|
+
return staggerRanks(count, "start", warnings);
|
|
1549
|
+
}
|
|
1550
|
+
const last = count - 1;
|
|
1551
|
+
if (from === "edges") {
|
|
1552
|
+
return Array.from({ length: count }, (_, index) => Math.min(index, last - index));
|
|
1553
|
+
}
|
|
1554
|
+
const origin = originOf(from, last, warnings);
|
|
1555
|
+
return Array.from({ length: count }, (_, index) => Math.floor(Math.abs(index - origin)));
|
|
1556
|
+
}
|
|
1557
|
+
function originOf(from, last, warnings) {
|
|
1558
|
+
if (from === "start") return 0;
|
|
1559
|
+
if (from === "end") return last;
|
|
1560
|
+
if (from === "center") return last / 2;
|
|
1561
|
+
return clampIndex(from, last, warnings);
|
|
1562
|
+
}
|
|
1563
|
+
function clampIndex(index, last, warnings) {
|
|
1564
|
+
if (index < 0 || index > last) {
|
|
1565
|
+
warnings.push(`stagger order "${String(index)}" is outside the group (0 to ${String(last)}) \u2014 clamped`);
|
|
1566
|
+
}
|
|
1567
|
+
return Math.min(Math.max(index, 0), last);
|
|
1568
|
+
}
|
|
1569
|
+
function gridRanks(count, from, layout, warnings) {
|
|
1570
|
+
const cols = Math.min(Math.max(1, layout.cols), count);
|
|
1571
|
+
const rows = Math.ceil(count / cols);
|
|
1572
|
+
if (from === "edges") {
|
|
1573
|
+
const inward = gridRanks(count, "center", layout, warnings);
|
|
1574
|
+
let furthest = 0;
|
|
1575
|
+
for (const rank of inward) furthest = Math.max(furthest, rank);
|
|
1576
|
+
return inward.map((rank) => round(furthest - rank));
|
|
1577
|
+
}
|
|
1578
|
+
const origin = gridOrigin(from, { cols, rows, last: count - 1 }, warnings);
|
|
1579
|
+
return Array.from({ length: count }, (_, index) => {
|
|
1580
|
+
const dx = index % cols - origin.x;
|
|
1581
|
+
const dy = Math.floor(index / cols) - origin.y;
|
|
1582
|
+
if (layout.along === "x") return round(Math.abs(dx));
|
|
1583
|
+
if (layout.along === "y") return round(Math.abs(dy));
|
|
1584
|
+
return round(Math.hypot(dx, dy));
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
function gridOrigin(from, grid, warnings) {
|
|
1588
|
+
const { cols, rows, last } = grid;
|
|
1589
|
+
if (typeof from === "object") return { x: from.x * (cols - 1), y: from.y * (rows - 1) };
|
|
1590
|
+
if (from === "start") return { x: 0, y: 0 };
|
|
1591
|
+
if (from === "end") return { x: cols - 1, y: rows - 1 };
|
|
1592
|
+
if (from === "center") return { x: (cols - 1) / 2, y: (rows - 1) / 2 };
|
|
1593
|
+
const index = clampIndex(from, last, warnings);
|
|
1594
|
+
return { x: index % cols, y: Math.floor(index / cols) };
|
|
1595
|
+
}
|
|
1596
|
+
function round(value) {
|
|
1597
|
+
return Math.round(value * RANK_PRECISION) / RANK_PRECISION;
|
|
1598
|
+
}
|
|
1599
|
+
function randomRanks(count) {
|
|
1600
|
+
const order = Array.from({ length: count }, (_, index) => index);
|
|
1601
|
+
order.sort((a, b) => scatterKey(a, count) - scatterKey(b, count));
|
|
1602
|
+
const ranks = new Array(count);
|
|
1603
|
+
for (const [rank, index] of order.entries()) ranks[index] = rank;
|
|
1604
|
+
return ranks;
|
|
1605
|
+
}
|
|
1606
|
+
function scatterKey(index, count) {
|
|
1607
|
+
let hash = (Math.imul(index, RANDOM_SALT) ^ Math.imul(count, COUNT_SALT)) >>> 0;
|
|
1608
|
+
hash ^= hash >>> 16;
|
|
1609
|
+
hash = Math.imul(hash, MIX_A) >>> 0;
|
|
1610
|
+
hash ^= hash >>> 15;
|
|
1611
|
+
hash = Math.imul(hash, MIX_B) >>> 0;
|
|
1612
|
+
hash ^= hash >>> 15;
|
|
1613
|
+
return hash >>> 0;
|
|
1614
|
+
}
|
|
1615
|
+
function indexStaggerGroup(group, reporter) {
|
|
1616
|
+
const warnings = [];
|
|
1617
|
+
const ledgers = reopenLedger(group);
|
|
1618
|
+
const config = resolveStaggerConfig(
|
|
1619
|
+
group.getAttribute(ATTR.stagger),
|
|
1620
|
+
group.getAttribute(ATTR.source) ?? "",
|
|
1621
|
+
warnings
|
|
1622
|
+
) ?? { from: "start" };
|
|
1623
|
+
const children = animatedChildren(group);
|
|
1624
|
+
const ranks = staggerRanks(children.length, config.from, warnings, resolveLayout(config, children, warnings));
|
|
1625
|
+
let maxRank = 0;
|
|
1626
|
+
for (const [index, child] of children.entries()) {
|
|
1627
|
+
const rank = ranks[index];
|
|
1628
|
+
ledgers.style(child).set("--kui-i", String(rank));
|
|
1629
|
+
GROUP_OF_CHILD.set(child, group);
|
|
1630
|
+
if (rank > maxRank) maxRank = rank;
|
|
1631
|
+
}
|
|
1632
|
+
const step = resolveStep(config, maxRank);
|
|
1633
|
+
if (step) ledgers.style(group).set("--kui-stagger", step);
|
|
1634
|
+
ledgers.style(group).set("--kui-stagger-count", String(round(maxRank + 1)));
|
|
1635
|
+
for (const warning of warnings) reporter?.warn(warning, group);
|
|
1636
|
+
}
|
|
1637
|
+
var GROUP_LEDGERS = /* @__PURE__ */ new WeakMap();
|
|
1638
|
+
var GROUP_OF_CHILD = /* @__PURE__ */ new WeakMap();
|
|
1639
|
+
function restageAfterRemoval(removed, reporter) {
|
|
1640
|
+
const groups = /* @__PURE__ */ new Set();
|
|
1641
|
+
for (const el of removed) {
|
|
1642
|
+
const group = GROUP_OF_CHILD.get(el);
|
|
1643
|
+
if (group) groups.add(group);
|
|
1644
|
+
}
|
|
1645
|
+
for (const group of groups) indexStaggerGroup(group, reporter);
|
|
1646
|
+
}
|
|
1647
|
+
function reopenLedger(group) {
|
|
1648
|
+
GROUP_LEDGERS.get(group)?.restore();
|
|
1649
|
+
const ledgers = createLedgerSet(group);
|
|
1650
|
+
GROUP_LEDGERS.set(group, ledgers);
|
|
1651
|
+
return ledgers;
|
|
1652
|
+
}
|
|
1653
|
+
function releaseStaggerGroup(group) {
|
|
1654
|
+
GROUP_LEDGERS.get(group)?.restore();
|
|
1655
|
+
GROUP_LEDGERS.delete(group);
|
|
1656
|
+
}
|
|
1657
|
+
function releaseStagger(root) {
|
|
1658
|
+
const selector = `[${ATTR.stagger}], [${ATTR.source}]`;
|
|
1659
|
+
if (root instanceof Element && root.matches(selector)) releaseStaggerGroup(root);
|
|
1660
|
+
for (const el of root.querySelectorAll(selector)) releaseStaggerGroup(el);
|
|
1661
|
+
}
|
|
1662
|
+
function restageAround(el, reporter) {
|
|
1663
|
+
restageOne(el, reporter);
|
|
1664
|
+
const parent = el.parentElement;
|
|
1665
|
+
if (parent) restageOne(parent, reporter);
|
|
1666
|
+
}
|
|
1667
|
+
function restageOne(el, reporter) {
|
|
1668
|
+
if (declaresGroup(el)) indexStaggerGroup(el, reporter);
|
|
1669
|
+
else releaseStaggerGroup(el);
|
|
1670
|
+
}
|
|
1671
|
+
function indexTargetGroup(request) {
|
|
1672
|
+
const { host, matches, styleOf, keys, reporter } = request;
|
|
1673
|
+
const warnings = [];
|
|
1674
|
+
const attribute = host.getAttribute(ATTR.stagger);
|
|
1675
|
+
const config = (keys ? resolveStaggerConfigFrom(attribute, keys, warnings) : resolveStaggerConfig(attribute, host.getAttribute(ATTR.source) ?? "", warnings)) ?? { from: "start" };
|
|
1676
|
+
const maxRank = rankBuckets(bucketByParent(matches), config, styleOf, warnings);
|
|
1677
|
+
const step = resolveStep(config, maxRank);
|
|
1678
|
+
if (step) styleOf(host).set("--kui-stagger", step);
|
|
1679
|
+
styleOf(host).set("--kui-stagger-count", String(round(maxRank + 1)));
|
|
1680
|
+
for (const warning of warnings) reporter?.warn(warning, host);
|
|
1681
|
+
}
|
|
1682
|
+
function bucketByParent(matches) {
|
|
1683
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
1684
|
+
for (const match of matches) {
|
|
1685
|
+
const siblings = byParent.get(match.parentElement);
|
|
1686
|
+
if (siblings) siblings.push(match);
|
|
1687
|
+
else byParent.set(match.parentElement, [match]);
|
|
1688
|
+
}
|
|
1689
|
+
return byParent;
|
|
1690
|
+
}
|
|
1691
|
+
function rankBuckets(byParent, config, styleOf, warnings) {
|
|
1692
|
+
let maxRank = 0;
|
|
1693
|
+
for (const siblings of byParent.values()) {
|
|
1694
|
+
const layout = resolveLayout(config, siblings, warnings);
|
|
1695
|
+
const ranks = staggerRanks(siblings.length, config.from, warnings, layout);
|
|
1696
|
+
for (const [index, match] of siblings.entries()) {
|
|
1697
|
+
const rank = ranks[index];
|
|
1698
|
+
styleOf(match).set("--kui-i", String(rank));
|
|
1699
|
+
if (rank > maxRank) maxRank = rank;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
return maxRank;
|
|
1703
|
+
}
|
|
1704
|
+
function resolveLayout(config, members, warnings) {
|
|
1705
|
+
if (config.cols === void 0) {
|
|
1706
|
+
if (config.along !== void 0) {
|
|
1707
|
+
warnings.push(
|
|
1708
|
+
`"along:${config.along}" names an axis of a grid this group has not declared \u2014 add "cols:" (a column count, or "auto")`
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
return void 0;
|
|
1712
|
+
}
|
|
1713
|
+
const cols = config.cols === "auto" ? measureColumns(members, warnings) : config.cols;
|
|
1714
|
+
if (cols === void 0) return void 0;
|
|
1715
|
+
return config.along === void 0 ? { cols } : { cols, along: config.along };
|
|
1716
|
+
}
|
|
1717
|
+
function measureColumns(members, warnings) {
|
|
1718
|
+
if (members.length < 2) return 1;
|
|
1719
|
+
const lefts = [];
|
|
1720
|
+
let laidOut = false;
|
|
1721
|
+
for (const member of members) {
|
|
1722
|
+
const box = member.getBoundingClientRect();
|
|
1723
|
+
if (box.width !== 0 || box.height !== 0) laidOut = true;
|
|
1724
|
+
lefts.push(box.left);
|
|
1725
|
+
}
|
|
1726
|
+
if (!laidOut) {
|
|
1727
|
+
warnings.push(
|
|
1728
|
+
`"cols:auto" could not measure this group \u2014 none of its children have been laid out yet (display:none, or detached), so the stagger falls back to DOM order`
|
|
1729
|
+
);
|
|
1730
|
+
return void 0;
|
|
1731
|
+
}
|
|
1732
|
+
const direction = Math.sign(lefts[1] - lefts[0]);
|
|
1733
|
+
if (direction === 0) return 1;
|
|
1734
|
+
let cols = 1;
|
|
1735
|
+
for (let index = 1; index < lefts.length; index++) {
|
|
1736
|
+
if (Math.sign(lefts[index] - lefts[index - 1]) !== direction) break;
|
|
1737
|
+
cols = index + 1;
|
|
1738
|
+
}
|
|
1739
|
+
return cols;
|
|
1740
|
+
}
|
|
1741
|
+
function animatedChildren(group) {
|
|
1742
|
+
const children = [];
|
|
1743
|
+
for (const child of group.children) {
|
|
1744
|
+
if (child.hasAttribute(ATTR.source)) children.push(child);
|
|
1745
|
+
}
|
|
1746
|
+
return children;
|
|
1747
|
+
}
|
|
1748
|
+
function applyStagger(root, reporter) {
|
|
1749
|
+
const selector = `[${ATTR.stagger}], [${ATTR.source}]`;
|
|
1750
|
+
if (root instanceof Element && root.matches(selector)) restageOne(root, reporter);
|
|
1751
|
+
for (const group of root.querySelectorAll(selector)) restageOne(group, reporter);
|
|
1752
|
+
}
|
|
1753
|
+
function declaresGroup(el) {
|
|
1754
|
+
if (el.hasAttribute(ATTR.define)) return false;
|
|
1755
|
+
if (isTargetGroupHost(el)) return false;
|
|
1756
|
+
if (el.hasAttribute(ATTR.stagger)) return true;
|
|
1757
|
+
return resolveStaggerConfig(null, el.getAttribute(ATTR.source) ?? "") !== void 0;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/core/derived/diagnostics.ts
|
|
1761
|
+
var ANCESTOR_WALK_BOUND = 64;
|
|
1762
|
+
function describe(el) {
|
|
1763
|
+
return el.id ? `${el.tagName.toLowerCase()}#${el.id}` : el.tagName.toLowerCase();
|
|
1764
|
+
}
|
|
1765
|
+
function anyEstablishes3d(registry, names) {
|
|
1766
|
+
return names.some((name) => registry.resolve(name)?.preset.establishes3d === true);
|
|
1767
|
+
}
|
|
1768
|
+
function liveEstablishes3d(port, el) {
|
|
1769
|
+
const state = port.stateOf(el);
|
|
1770
|
+
if (!state || state.aggregate) return false;
|
|
1771
|
+
return anyEstablishes3d(port.registry, state.fxNames);
|
|
1772
|
+
}
|
|
1773
|
+
function find3dNeighbor(port, host, match) {
|
|
1774
|
+
if (liveEstablishes3d(port, host)) return host;
|
|
1775
|
+
let ancestor = match.parentElement;
|
|
1776
|
+
let depth = 0;
|
|
1777
|
+
while (ancestor && depth < ANCESTOR_WALK_BOUND) {
|
|
1778
|
+
if (ancestor !== host && liveEstablishes3d(port, ancestor)) return ancestor;
|
|
1779
|
+
ancestor = ancestor.parentElement;
|
|
1780
|
+
depth++;
|
|
1781
|
+
}
|
|
1782
|
+
return void 0;
|
|
1783
|
+
}
|
|
1784
|
+
function markUnmatched(host, selectors, attributes) {
|
|
1785
|
+
if (selectors.length === 0) {
|
|
1786
|
+
host.removeAttribute(ATTR.unmatched);
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
const value = selectors.join(", ");
|
|
1790
|
+
if (attributes) attributes.set(ATTR.unmatched, value);
|
|
1791
|
+
else host.setAttribute(ATTR.unmatched, value);
|
|
1792
|
+
}
|
|
1793
|
+
function warnNested3d(port, host, match, target) {
|
|
1794
|
+
if (!anyEstablishes3d(port.registry, target.plan.fxNames)) return;
|
|
1795
|
+
const neighbor = find3dNeighbor(port, host, match);
|
|
1796
|
+
if (!neighbor) return;
|
|
1797
|
+
const where = neighbor === host ? "its target: host" : "an ancestor";
|
|
1798
|
+
port.reporter.warn(
|
|
1799
|
+
`<${describe(match)}> establishes a 3D rendering context nested inside ${where} <${describe(neighbor)}>, which also runs a 3D-establishing effect \u2014 preserve-3d/perspective contexts do not compose when nested, so move one of the two 3D effects off its descendant`,
|
|
1800
|
+
match
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
function warnPageScopeCloak(port, host, target) {
|
|
1804
|
+
if (target.scope !== "page") return;
|
|
1805
|
+
if (!target.plan.fxNames.some((name) => port.registry.resolve(name)?.preset.cloak === true)) return;
|
|
1806
|
+
if (!host.ownerDocument.documentElement.hasAttribute(ATTR.cloak)) return;
|
|
1807
|
+
port.reporter.warn(
|
|
1808
|
+
`target "${target.selector}" (scope:page) on <${describe(host)}> carries a cloaked entrance \u2014 the pre-JS cloak selector only hides descendants of the element it is authored on, so a scope:page match outside <${describe(host)}>'s own subtree will flash unstyled before this library runs; use scope:self, or drop the cloaked preset from this group`,
|
|
1809
|
+
host
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// src/core/derived/install.ts
|
|
1814
|
+
var FORMER_HOST = /* @__PURE__ */ new WeakMap();
|
|
1815
|
+
function rememberFormerHost(book, match, host) {
|
|
1816
|
+
let table = FORMER_HOST.get(book);
|
|
1817
|
+
if (!table) {
|
|
1818
|
+
table = /* @__PURE__ */ new WeakMap();
|
|
1819
|
+
FORMER_HOST.set(book, table);
|
|
1820
|
+
}
|
|
1821
|
+
table.set(match, host);
|
|
1822
|
+
}
|
|
1823
|
+
function formerHostOf(book, match) {
|
|
1824
|
+
return FORMER_HOST.get(book)?.get(match);
|
|
1825
|
+
}
|
|
1826
|
+
function targetKey(target) {
|
|
1827
|
+
return `${target.scope} ${target.selector}`;
|
|
1828
|
+
}
|
|
1829
|
+
function matchKey(targets) {
|
|
1830
|
+
return targets.length === 1 ? targetKey(targets[0]) : targets.map(targetKey).sort((a, b) => a.localeCompare(b)).join("");
|
|
1831
|
+
}
|
|
1832
|
+
function groupConfigFor(port, context, target, cache) {
|
|
1833
|
+
const cached2 = cache.get(target);
|
|
1834
|
+
if (cached2) return cached2;
|
|
1835
|
+
const config = { ...resolveGroupConfig(context.baseConfig, target.hoists) };
|
|
1836
|
+
config.activation = port.resolveActivation(context.host, config, target.plan);
|
|
1837
|
+
cache.set(target, config);
|
|
1838
|
+
return config;
|
|
1839
|
+
}
|
|
1840
|
+
function resolveGroups(port, context, targets, cache) {
|
|
1841
|
+
const live = [];
|
|
1842
|
+
const resolved = [];
|
|
1843
|
+
const unmatched = [];
|
|
1844
|
+
for (const target of targets) {
|
|
1845
|
+
if (target.selector === "") continue;
|
|
1846
|
+
const matches = port.resolveGroupMatches(context.host, target);
|
|
1847
|
+
if (matches.length === 0) unmatched.push(target.selector);
|
|
1848
|
+
resolved.push({ target, matches });
|
|
1849
|
+
live.push({
|
|
1850
|
+
target,
|
|
1851
|
+
config: groupConfigFor(port, context, target, cache),
|
|
1852
|
+
gateOwner: groupGateOwner(port, context, target),
|
|
1853
|
+
members: []
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
return { live, resolved, unmatched };
|
|
1857
|
+
}
|
|
1858
|
+
function buildContext(request) {
|
|
1859
|
+
const baseConfig = request.baseConfig;
|
|
1860
|
+
return {
|
|
1861
|
+
host: request.el,
|
|
1862
|
+
fingerprint: request.fingerprint,
|
|
1863
|
+
parsed: request.parsed,
|
|
1864
|
+
baseConfig,
|
|
1865
|
+
timeline: baseConfig.timeline
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
function installHostGroup(port, request, own) {
|
|
1869
|
+
if (own) {
|
|
1870
|
+
port.install({ ...request, document: { targets: [own], warnings: [] } });
|
|
1871
|
+
} else {
|
|
1872
|
+
port.installAggregate(request);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
function registerCleanup2(port, host, cleanup) {
|
|
1876
|
+
const cleanups = port.book.cleanups.get(host) ?? [];
|
|
1877
|
+
cleanups.push(cleanup);
|
|
1878
|
+
port.book.cleanups.set(host, cleanups);
|
|
1879
|
+
}
|
|
1880
|
+
function installedTargetFor(port, context, targets) {
|
|
1881
|
+
return targets.length === 1 ? targets[0] : compileUnion(port, context, targets);
|
|
1882
|
+
}
|
|
1883
|
+
function finalizeInstall(port, context, input) {
|
|
1884
|
+
const host = context.host;
|
|
1885
|
+
const hostState = port.stateOf(host);
|
|
1886
|
+
markUnmatched(host, [...input.unmatched, ...input.suspects ?? []], hostState.attributes);
|
|
1887
|
+
registerCleanup2(port, host, claimTargetGroupHost(host));
|
|
1888
|
+
restageTargets(port, host);
|
|
1889
|
+
bindGroups(port, host);
|
|
1890
|
+
for (const [match, targets] of input.claims) {
|
|
1891
|
+
warnNested3d(port, host, match, installedTargetFor(port, context, targets));
|
|
1892
|
+
}
|
|
1893
|
+
for (const group of input.resolved) warnPageScopeCloak(port, host, group.target);
|
|
1894
|
+
syncAggregate(port, host);
|
|
1895
|
+
}
|
|
1896
|
+
function installWithTargets(port, request) {
|
|
1897
|
+
const context = buildContext(request);
|
|
1898
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1899
|
+
const { live, resolved, unmatched } = resolveGroups(port, context, request.document.targets, cache);
|
|
1900
|
+
port.book.contexts.set(context.host, context);
|
|
1901
|
+
port.book.groups.set(context.host, live);
|
|
1902
|
+
const claims = claimMatches(port, context.host, resolved);
|
|
1903
|
+
const own = request.document.targets.find((target) => target.selector === "");
|
|
1904
|
+
if (!own && claims.size === 0) {
|
|
1905
|
+
context.host.setAttribute(ATTR.state, "failed");
|
|
1906
|
+
markUnmatched(context.host, [...unmatched, ...request.document.suspectSelectors ?? []]);
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
installDerivedMatches(port, context, claims, cache);
|
|
1910
|
+
installHostGroup(port, request, own);
|
|
1911
|
+
finalizeInstall(port, context, { claims, resolved, unmatched, suspects: request.document.suspectSelectors });
|
|
1912
|
+
}
|
|
1913
|
+
function releaseMatch(port, host, match) {
|
|
1914
|
+
const { book } = port;
|
|
1915
|
+
rememberFormerHost(book, match, host);
|
|
1916
|
+
book.hosts.delete(match);
|
|
1917
|
+
book.derived.get(host)?.delete(match);
|
|
1918
|
+
for (const group of book.groups.get(host) ?? []) {
|
|
1919
|
+
const index = group.members.indexOf(match);
|
|
1920
|
+
if (index !== -1) group.members.splice(index, 1);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
function releaseHost(port, host) {
|
|
1924
|
+
const { book } = port;
|
|
1925
|
+
for (const match of [...book.derived.get(host) ?? []]) port.release(match);
|
|
1926
|
+
for (const cleanup of book.cleanups.get(host) ?? []) cleanup();
|
|
1927
|
+
book.derived.delete(host);
|
|
1928
|
+
book.groups.delete(host);
|
|
1929
|
+
book.contexts.delete(host);
|
|
1930
|
+
book.unions.delete(host);
|
|
1931
|
+
book.cleanups.delete(host);
|
|
1932
|
+
}
|
|
1933
|
+
function releaseDerived(port, el) {
|
|
1934
|
+
const host = port.book.hosts.get(el);
|
|
1935
|
+
if (host) {
|
|
1936
|
+
releaseMatch(port, host, el);
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
if (port.book.derived.has(el)) releaseHost(port, el);
|
|
1940
|
+
}
|
|
1941
|
+
function restageAfterTreeRelease(port, removed) {
|
|
1942
|
+
const formerHosts = /* @__PURE__ */ new Set();
|
|
1943
|
+
for (const el of removed) {
|
|
1944
|
+
const host = formerHostOf(port.book, el);
|
|
1945
|
+
if (host) formerHosts.add(host);
|
|
1946
|
+
}
|
|
1947
|
+
for (const host of formerHosts) {
|
|
1948
|
+
if (!port.stateOf(host)) continue;
|
|
1949
|
+
restageTargets(port, host);
|
|
1950
|
+
syncAggregate(port, host);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
var SKIP_LABEL = {
|
|
1954
|
+
host: "is the target: host itself",
|
|
1955
|
+
authored: "already carries its own data-kui and animates itself",
|
|
1956
|
+
claimed: "is already claimed by another target: host"
|
|
1957
|
+
};
|
|
1958
|
+
function rejectionReason(port, host, match) {
|
|
1959
|
+
if (match === host) return "host";
|
|
1960
|
+
if (match.hasAttribute(ATTR.source)) return "authored";
|
|
1961
|
+
const claimedBy = port.book.hosts.get(match);
|
|
1962
|
+
if (claimedBy !== void 0 && claimedBy !== host) return "claimed";
|
|
1963
|
+
return void 0;
|
|
1964
|
+
}
|
|
1965
|
+
function claimMatch(claims, target, match) {
|
|
1966
|
+
const existing = claims.get(match);
|
|
1967
|
+
if (existing) existing.push(target);
|
|
1968
|
+
else claims.set(match, [target]);
|
|
1969
|
+
}
|
|
1970
|
+
function reportSkips(port, host, target, skipped) {
|
|
1971
|
+
const names = target.plan.fxNames.join(", ");
|
|
1972
|
+
for (const [reason, count] of skipped) {
|
|
1973
|
+
const plural = count === 1 ? "match" : "matches";
|
|
1974
|
+
port.reporter.warn(
|
|
1975
|
+
`target "${target.selector}" (${names}) skipped ${count} ${plural} that ${SKIP_LABEL[reason]}`,
|
|
1976
|
+
host
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
function claimMatches(port, host, groups) {
|
|
1981
|
+
const claims = /* @__PURE__ */ new Map();
|
|
1982
|
+
for (const group of groups) {
|
|
1983
|
+
const skipped = /* @__PURE__ */ new Map();
|
|
1984
|
+
for (const match of group.matches) {
|
|
1985
|
+
const reason = rejectionReason(port, host, match);
|
|
1986
|
+
if (reason) skipped.set(reason, (skipped.get(reason) ?? 0) + 1);
|
|
1987
|
+
else claimMatch(claims, group.target, match);
|
|
1988
|
+
}
|
|
1989
|
+
reportSkips(port, host, group.target, skipped);
|
|
1990
|
+
}
|
|
1991
|
+
return claims;
|
|
1992
|
+
}
|
|
1993
|
+
function resolveClaim(port, context, entry, cache) {
|
|
1994
|
+
const { match, targets } = entry;
|
|
1995
|
+
const target = installedTargetFor(port, context, targets);
|
|
1996
|
+
return { match, targets, target, config: groupConfigFor(port, context, target, cache) };
|
|
1997
|
+
}
|
|
1998
|
+
function matchGateOwner(port, context, targets) {
|
|
1999
|
+
const grouped = targets.some((target) => groupGateOwner(port, context, target) !== void 0);
|
|
2000
|
+
return grouped ? context.host : void 0;
|
|
2001
|
+
}
|
|
2002
|
+
function recordClaim(port, context, claim2) {
|
|
2003
|
+
const { book } = port;
|
|
2004
|
+
book.hosts.set(claim2.match, context.host);
|
|
2005
|
+
const derived = book.derived.get(context.host) ?? /* @__PURE__ */ new Set();
|
|
2006
|
+
derived.add(claim2.match);
|
|
2007
|
+
book.derived.set(context.host, derived);
|
|
2008
|
+
const live = book.groups.get(context.host) ?? [];
|
|
2009
|
+
for (const target of claim2.targets) {
|
|
2010
|
+
live.find((group) => group.target === target)?.members.push(claim2.match);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
function installClaim(port, context, claim2) {
|
|
2014
|
+
port.install({
|
|
2015
|
+
el: claim2.match,
|
|
2016
|
+
fingerprint: `${context.fingerprint}\0${matchKey(claim2.targets)}`,
|
|
2017
|
+
parsed: { specs: claim2.target.specs, warnings: [] },
|
|
2018
|
+
config: claim2.config,
|
|
2019
|
+
document: { targets: [{ ...claim2.target, selector: "", scope: "self" }], warnings: [] },
|
|
2020
|
+
host: context.host,
|
|
2021
|
+
gateOwner: matchGateOwner(port, context, claim2.targets)
|
|
2022
|
+
});
|
|
2023
|
+
recordClaim(port, context, claim2);
|
|
2024
|
+
}
|
|
2025
|
+
function installDerivedMatches(port, context, claims, cache = /* @__PURE__ */ new Map()) {
|
|
2026
|
+
const installed = [];
|
|
2027
|
+
for (const [match, targets] of claims) {
|
|
2028
|
+
const claim2 = resolveClaim(port, context, { match, targets }, cache);
|
|
2029
|
+
installClaim(port, context, claim2);
|
|
2030
|
+
installed.push(claim2.match);
|
|
2031
|
+
}
|
|
2032
|
+
return installed;
|
|
2033
|
+
}
|
|
2034
|
+
function restageTargets(port, host) {
|
|
2035
|
+
const context = port.book.contexts.get(host);
|
|
2036
|
+
const live = port.book.groups.get(host);
|
|
2037
|
+
const hostState = port.stateOf(host);
|
|
2038
|
+
if (!context || !live || !hostState) return;
|
|
2039
|
+
const elementWide = elementStaggerKeys(context.parsed);
|
|
2040
|
+
const styleOf = (el) => el === host ? hostState.ledgers.style(host) : port.stateOf(el).ledgers.style(el);
|
|
2041
|
+
for (const group of live) {
|
|
2042
|
+
if (group.members.length === 0) continue;
|
|
2043
|
+
indexTargetGroup({
|
|
2044
|
+
host,
|
|
2045
|
+
matches: group.members,
|
|
2046
|
+
styleOf,
|
|
2047
|
+
keys: staggerKeysFor(group.target.hoists, elementWide),
|
|
2048
|
+
reporter: port.reporter
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// src/core/derived/late-matches.ts
|
|
2054
|
+
var ANCESTOR_WALK_BOUND2 = 64;
|
|
2055
|
+
function newMatchesFor(node, host, target) {
|
|
2056
|
+
const candidates = [...node.querySelectorAll(target.selector)];
|
|
2057
|
+
if (node.matches(target.selector)) candidates.unshift(node);
|
|
2058
|
+
return candidates.filter((el) => host.contains(el) && el !== host && !el.hasAttribute(ATTR.part));
|
|
2059
|
+
}
|
|
2060
|
+
function resolveNewGroups(node, host, groups) {
|
|
2061
|
+
const resolved = [];
|
|
2062
|
+
const before = /* @__PURE__ */ new Map();
|
|
2063
|
+
for (const group of groups) {
|
|
2064
|
+
before.set(group, group.members.length);
|
|
2065
|
+
if (group.target.scope !== "self") continue;
|
|
2066
|
+
const matches = newMatchesFor(node, host, group.target);
|
|
2067
|
+
if (matches.length > 0) resolved.push({ target: group.target, matches });
|
|
2068
|
+
}
|
|
2069
|
+
return { resolved, before };
|
|
2070
|
+
}
|
|
2071
|
+
function insertionIndex(members, anchor) {
|
|
2072
|
+
const index = members.findIndex((existing) => Boolean(existing.compareDocumentPosition(anchor) & Node.DOCUMENT_POSITION_PRECEDING));
|
|
2073
|
+
return index === -1 ? members.length : index;
|
|
2074
|
+
}
|
|
2075
|
+
function hasAlreadyFired(port, group, added) {
|
|
2076
|
+
return group.members.some((member) => !added.includes(member) && port.stateOf(member)?.status !== "ready");
|
|
2077
|
+
}
|
|
2078
|
+
function activateIfAlreadyFired(port, group, added) {
|
|
2079
|
+
if (group.gateOwner === void 0) return;
|
|
2080
|
+
if (hasAlreadyFired(port, group, added)) for (const member of added) port.activateOne(member);
|
|
2081
|
+
}
|
|
2082
|
+
function reindexGroup(port, group, before) {
|
|
2083
|
+
if (group.members.length === before) return;
|
|
2084
|
+
const added = group.members.splice(before);
|
|
2085
|
+
group.members.splice(insertionIndex(group.members, added[0]), 0, ...added);
|
|
2086
|
+
activateIfAlreadyFired(port, group, added);
|
|
2087
|
+
}
|
|
2088
|
+
function adoptIntoHost(port, node, host) {
|
|
2089
|
+
const groups = port.book.groups.get(host);
|
|
2090
|
+
const context = port.book.contexts.get(host);
|
|
2091
|
+
if (!groups || !context) return;
|
|
2092
|
+
const { resolved, before } = resolveNewGroups(node, host, groups);
|
|
2093
|
+
if (resolved.length === 0) return;
|
|
2094
|
+
const claims = claimMatches(port, host, resolved);
|
|
2095
|
+
if (claims.size === 0) return;
|
|
2096
|
+
installDerivedMatches(port, context, claims);
|
|
2097
|
+
for (const group of groups) reindexGroup(port, group, before.get(group));
|
|
2098
|
+
restageTargets(port, host);
|
|
2099
|
+
}
|
|
2100
|
+
function adoptLateMatches(port, node) {
|
|
2101
|
+
if (node.hasAttribute(ATTR.part)) return;
|
|
2102
|
+
let ancestor = node.parentElement;
|
|
2103
|
+
let depth = 0;
|
|
2104
|
+
while (ancestor && depth < ANCESTOR_WALK_BOUND2) {
|
|
2105
|
+
adoptIntoHost(port, node, ancestor);
|
|
2106
|
+
ancestor = ancestor.parentElement;
|
|
2107
|
+
depth++;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// src/core/dom-watcher.ts
|
|
2112
|
+
var WORK_BUDGET = 100;
|
|
2113
|
+
function createDomWatcher(options) {
|
|
2114
|
+
const { root, onElementAdded, onElementRemoved, onAttributeChanged } = options;
|
|
2115
|
+
const schedule = options.schedule ?? scheduleFrame;
|
|
2116
|
+
const createObserver = options.createObserver ?? defaultObserverFactory();
|
|
2117
|
+
const added = /* @__PURE__ */ new Set();
|
|
2118
|
+
const removed = /* @__PURE__ */ new Set();
|
|
2119
|
+
const changed = /* @__PURE__ */ new Set();
|
|
2120
|
+
let observer;
|
|
2121
|
+
let scheduled = false;
|
|
2122
|
+
let destroyed = false;
|
|
2123
|
+
function collect(record) {
|
|
2124
|
+
if (record.type === "attributes") {
|
|
2125
|
+
const target = asElement(record.target);
|
|
2126
|
+
if (target) changed.add(target);
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
for (const node of record.addedNodes) queueRoot(added, asElement(node));
|
|
2130
|
+
for (const node of record.removedNodes) queueRoot(removed, asElement(node));
|
|
2131
|
+
}
|
|
2132
|
+
function queueWork(records) {
|
|
2133
|
+
for (const record of records) collect(record);
|
|
2134
|
+
if (scheduled) return;
|
|
2135
|
+
scheduled = true;
|
|
2136
|
+
schedule(flush);
|
|
2137
|
+
}
|
|
2138
|
+
const install = whileAttached(root, onElementAdded);
|
|
2139
|
+
const recompile = whileAttached(root, onAttributeChanged);
|
|
2140
|
+
function flush() {
|
|
2141
|
+
scheduled = false;
|
|
2142
|
+
if (destroyed) return;
|
|
2143
|
+
let remaining = WORK_BUDGET;
|
|
2144
|
+
remaining = drain(removed, onElementRemoved, remaining);
|
|
2145
|
+
remaining = drain(added, install, remaining);
|
|
2146
|
+
drain(changed, recompile, remaining);
|
|
2147
|
+
if (added.size + removed.size + changed.size > 0) {
|
|
2148
|
+
scheduled = true;
|
|
2149
|
+
schedule(flush);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return {
|
|
2153
|
+
watch() {
|
|
2154
|
+
if (!createObserver) return;
|
|
2155
|
+
destroyed = false;
|
|
2156
|
+
observer = createObserver(queueWork);
|
|
2157
|
+
observer.observe(root, {
|
|
2158
|
+
subtree: true,
|
|
2159
|
+
childList: true,
|
|
2160
|
+
attributes: true,
|
|
2161
|
+
// `ATTR.stagger` is watched for the same reason `ATTR.source` is: both spell a stagger
|
|
2162
|
+
// group, and an edit to either has to re-rank the group. Its absence here meant editing
|
|
2163
|
+
// `data-kui-stagger` produced no record at all — not a missed handler, no mutation.
|
|
2164
|
+
attributeFilter: [ATTR.source, ATTR.on, ATTR.timeline, ATTR.threshold, ATTR.stagger]
|
|
2165
|
+
});
|
|
2166
|
+
},
|
|
2167
|
+
destroy() {
|
|
2168
|
+
destroyed = true;
|
|
2169
|
+
observer?.disconnect();
|
|
2170
|
+
added.clear();
|
|
2171
|
+
removed.clear();
|
|
2172
|
+
changed.clear();
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
function whileAttached(root, work) {
|
|
2177
|
+
return (el) => {
|
|
2178
|
+
if (root.contains(el)) work(el);
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
function queueRoot(roots, candidate) {
|
|
2182
|
+
if (!candidate) return;
|
|
2183
|
+
for (const root of roots) {
|
|
2184
|
+
if (root.contains(candidate)) return;
|
|
2185
|
+
if (candidate.contains(root)) roots.delete(root);
|
|
2186
|
+
}
|
|
2187
|
+
roots.add(candidate);
|
|
2188
|
+
}
|
|
2189
|
+
function drain(roots, callback, budget) {
|
|
2190
|
+
for (const root of roots) {
|
|
2191
|
+
if (budget === 0) break;
|
|
2192
|
+
roots.delete(root);
|
|
2193
|
+
callback(root);
|
|
2194
|
+
budget--;
|
|
2195
|
+
}
|
|
2196
|
+
return budget;
|
|
2197
|
+
}
|
|
2198
|
+
function asElement(node) {
|
|
2199
|
+
return node.nodeType === 1 ? node : null;
|
|
2200
|
+
}
|
|
2201
|
+
function scheduleFrame(callback) {
|
|
2202
|
+
const requestFrame = globalThis.requestAnimationFrame;
|
|
2203
|
+
if (typeof requestFrame === "function") requestFrame(() => callback());
|
|
2204
|
+
else queueMicrotask(callback);
|
|
2205
|
+
}
|
|
2206
|
+
function defaultObserverFactory() {
|
|
2207
|
+
if (typeof MutationObserver === "undefined") return void 0;
|
|
2208
|
+
return (callback) => new MutationObserver(callback);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// src/core/js-effect-preparer.ts
|
|
2212
|
+
function sequencedTiming(entry, warn) {
|
|
2213
|
+
const timing = readEffectTiming(entry.spec, warn);
|
|
2214
|
+
if (entry.sequencedDelayMs !== void 0) timing.delayMs = entry.sequencedDelayMs;
|
|
2215
|
+
return timing;
|
|
2216
|
+
}
|
|
2217
|
+
function createJsEffectPreparer(options) {
|
|
2218
|
+
const { scheduler, rootResolver, capabilities, reporter, respectReducedMotion } = options;
|
|
2219
|
+
function contextFor(el, signal, ledger) {
|
|
2220
|
+
const doc = el.ownerDocument;
|
|
2221
|
+
return {
|
|
2222
|
+
doc,
|
|
2223
|
+
win: doc.defaultView ?? globalThis,
|
|
2224
|
+
scheduler,
|
|
2225
|
+
rootFor: rootResolver,
|
|
2226
|
+
capabilities,
|
|
2227
|
+
invalidate: () => scheduler.invalidate(),
|
|
2228
|
+
warn: (message) => reporter.warn(message, el),
|
|
2229
|
+
reducedMotion: respectReducedMotion && capabilities.reducedMotion,
|
|
2230
|
+
signal,
|
|
2231
|
+
style: ledger
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
return {
|
|
2235
|
+
prepare({ el, plan, signal, ledger }) {
|
|
2236
|
+
const instances = [];
|
|
2237
|
+
if (plan.jsEffects.length === 0) return instances;
|
|
2238
|
+
const ctx = contextFor(el, signal, ledger);
|
|
2239
|
+
for (const entry of plan.jsEffects) {
|
|
2240
|
+
const { spec, resolved } = entry;
|
|
2241
|
+
const prepare = resolved.primitive.prepare;
|
|
2242
|
+
if (!prepare) continue;
|
|
2243
|
+
const warn = (message) => reporter.warn(message, el);
|
|
2244
|
+
const params = readEffectParams(
|
|
2245
|
+
{ ...resolved.preset.params, ...authoredParams(entry) },
|
|
2246
|
+
resolved.primitive.parameters,
|
|
2247
|
+
warn,
|
|
2248
|
+
sequencedTiming(entry, warn)
|
|
2249
|
+
);
|
|
2250
|
+
try {
|
|
2251
|
+
instances.push(prepare(el, params, ctx));
|
|
2252
|
+
} catch (error) {
|
|
2253
|
+
reporter.warn(`"${spec.name}" failed to initialise: ${String(error)}`, el);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return instances;
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// src/core/reporter.ts
|
|
2262
|
+
function consoleReporter() {
|
|
2263
|
+
return {
|
|
2264
|
+
warn(message, subject) {
|
|
2265
|
+
if (subject === void 0) console.warn(`[kuinetic] ${message}`);
|
|
2266
|
+
else console.warn(`[kuinetic] ${message}`, subject);
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
function silentReporter() {
|
|
2271
|
+
return { warn() {
|
|
2272
|
+
} };
|
|
2273
|
+
}
|
|
2274
|
+
function collectingReporter() {
|
|
2275
|
+
const messages = [];
|
|
2276
|
+
return {
|
|
2277
|
+
messages,
|
|
2278
|
+
warn(message) {
|
|
2279
|
+
messages.push(message);
|
|
2280
|
+
}
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// src/core/animator.ts
|
|
2285
|
+
var CLOAK_WATCHDOG_MS = 3e3;
|
|
2286
|
+
function fingerprintOf(attributes) {
|
|
2287
|
+
return [attributes.source, attributes.on, attributes.timeline, attributes.threshold].join("\0");
|
|
2288
|
+
}
|
|
2289
|
+
function isElementNode(node) {
|
|
2290
|
+
return typeof Element !== "undefined" && node instanceof Element;
|
|
2291
|
+
}
|
|
2292
|
+
var Animator = class {
|
|
2293
|
+
registry;
|
|
2294
|
+
capabilities;
|
|
2295
|
+
/**
|
|
2296
|
+
* Public alongside `registry` and `capabilities`, and for the same reason: `control()` is a free
|
|
2297
|
+
* function in its own module (mirroring `play()`), and an author-facing diagnostic it emits has
|
|
2298
|
+
* to reach the same sink every other diagnostic on this animator does. Routing it through a
|
|
2299
|
+
* second, private channel would mean `consoleReporter()` silenced half the library's warnings.
|
|
2300
|
+
*/
|
|
2301
|
+
reporter;
|
|
2302
|
+
root;
|
|
2303
|
+
/**
|
|
2304
|
+
* Author-defined bundles found in this animator's root. Per-animator, not per-module: two
|
|
2305
|
+
* animators on two roots must not see each other's definitions.
|
|
2306
|
+
*/
|
|
2307
|
+
bundles;
|
|
2308
|
+
binder;
|
|
2309
|
+
scheduler;
|
|
2310
|
+
rootResolver;
|
|
2311
|
+
jsEffectPreparer;
|
|
2312
|
+
respectReducedMotion;
|
|
2313
|
+
shouldObserve;
|
|
2314
|
+
/** Runtime truth. Attributes are for CSS and debugging; they make a poor state machine. */
|
|
2315
|
+
states = /* @__PURE__ */ new WeakMap();
|
|
2316
|
+
/** Iterable lifecycle index; the WeakMap remains the fast state lookup. */
|
|
2317
|
+
liveElements = /* @__PURE__ */ new Set();
|
|
2318
|
+
/**
|
|
2319
|
+
* Which run is current for one element's `InstanceState`, so a completion that resolves after
|
|
2320
|
+
* being superseded can tell it is stale even when `status` and `direction` do not say so.
|
|
2321
|
+
*
|
|
2322
|
+
* `state.direction` cannot serve as that identifier on its own — it only ever holds `'forward'`
|
|
2323
|
+
* or `'reverse'`, so an element whose activation flips back and forth (a hover-driven card flip,
|
|
2324
|
+
* `pointerenter`/`pointerleave` firing repeatedly) revisits the same direction many times over
|
|
2325
|
+
* its life. A cancelled run's completion settling late would then match a much later run headed
|
|
2326
|
+
* the same way, by coincidence rather than by being the same run. Keyed on `InstanceState` rather
|
|
2327
|
+
* than `Element` so a `release()`-then-reinstall — a fresh state object — starts with no
|
|
2328
|
+
* history, the same reason `settleWhen` already re-checks `this.states.get(el) !== state`.
|
|
2329
|
+
*/
|
|
2330
|
+
currentRun = /* @__PURE__ */ new WeakMap();
|
|
2331
|
+
/**
|
|
2332
|
+
* Whether the element's current run has a settle gate that will eventually write its final
|
|
2333
|
+
* status, or whether nothing is ever going to report on it.
|
|
2334
|
+
*
|
|
2335
|
+
* `settleWhen` declines to arm a gate when every instance it was handed is `continuous` — see
|
|
2336
|
+
* the reasoning there, which is right: a pin that is still pinned should read `running`. But
|
|
2337
|
+
* "nobody will ever write this status" was left as an unrecorded fact known only inside that
|
|
2338
|
+
* one early return, and `cancel()` had no way to ask. So a cancelled pin kept `status =
|
|
2339
|
+
* 'running'` for the rest of the element's life and `activate()`'s own `running` guard locked it
|
|
2340
|
+
* out of every future activation. Measured on `<div data-kui="pin-section">`, the documented
|
|
2341
|
+
* form: cancel left `data-kui-state="running"`, and the next `activate()` emitted no `kui:start`
|
|
2342
|
+
* at all.
|
|
2343
|
+
*
|
|
2344
|
+
* Recorded here rather than re-derived in `cancel()` from `state.instances`, because the two
|
|
2345
|
+
* would not be asking the same question. `settleWhen` decides on the instances that actually
|
|
2346
|
+
* *started* this run; an instance whose setup threw is excluded there, and by cancel time it is
|
|
2347
|
+
* indistinguishable from a timed one (`continuous` reads from the setup result, which a failed
|
|
2348
|
+
* or torn-down instance no longer has). The gate's own answer is the only one that matches the
|
|
2349
|
+
* gate's own behaviour.
|
|
2350
|
+
*
|
|
2351
|
+
* Keyed on `InstanceState` for the same reason `currentRun` is: a `release()`-then-reinstall
|
|
2352
|
+
* mints a fresh state object, which must start with no history.
|
|
2353
|
+
*/
|
|
2354
|
+
settleArmed = /* @__PURE__ */ new WeakMap();
|
|
2355
|
+
/** Built lazily by `watch()` when not injected, so nothing observes until `start()` needs it. */
|
|
2356
|
+
domWatcher;
|
|
2357
|
+
/**
|
|
2358
|
+
* Built lazily by `watchGates()`, and only for an element carrying a viewport gate on a
|
|
2359
|
+
* JavaScript-rendered effect — see `applyViewportGates` for why that is the only case that needs
|
|
2360
|
+
* one. Stays `undefined` on a page whose gates are all on CSS-rendered effects, which binds no
|
|
2361
|
+
* `MediaQueryList` listener anywhere.
|
|
2362
|
+
*/
|
|
2363
|
+
gateWatcher;
|
|
2364
|
+
started = false;
|
|
2365
|
+
/** This animator's derived-host bookkeeping — see `DerivedBook`. */
|
|
2366
|
+
book = createDerivedBook();
|
|
2367
|
+
/** The narrow window the `derived/*` modules get onto this animator — see `AnimatorPort`. */
|
|
2368
|
+
port;
|
|
2369
|
+
constructor(options = {}) {
|
|
2370
|
+
const resolved = resolveCollaborators(options);
|
|
2371
|
+
this.registry = resolved.registry;
|
|
2372
|
+
this.bundles = new BundleTable(resolved.registry);
|
|
2373
|
+
this.capabilities = resolved.capabilities;
|
|
2374
|
+
this.root = resolved.root;
|
|
2375
|
+
this.reporter = resolved.reporter;
|
|
2376
|
+
this.binder = resolved.binder;
|
|
2377
|
+
this.scheduler = resolved.scheduler;
|
|
2378
|
+
this.rootResolver = resolved.rootResolver;
|
|
2379
|
+
this.jsEffectPreparer = resolved.jsEffectPreparer;
|
|
2380
|
+
this.domWatcher = resolved.domWatcher;
|
|
2381
|
+
this.respectReducedMotion = resolved.respectReducedMotion;
|
|
2382
|
+
this.shouldObserve = resolved.shouldObserve;
|
|
2383
|
+
this.port = {
|
|
2384
|
+
registry: this.registry,
|
|
2385
|
+
reporter: this.reporter,
|
|
2386
|
+
binder: this.binder,
|
|
2387
|
+
capabilities: this.capabilities,
|
|
2388
|
+
respectReducedMotion: this.respectReducedMotion,
|
|
2389
|
+
book: this.book,
|
|
2390
|
+
stateOf: (el) => this.states.get(el),
|
|
2391
|
+
install: (request) => this.install(request),
|
|
2392
|
+
installAggregate: (request) => this.installAggregate(request),
|
|
2393
|
+
resolveGroupMatches: (host, target) => this.resolveGroupMatches(host, target),
|
|
2394
|
+
resolveActivation: (el, config, plan) => this.resolveActivation(el, config, plan),
|
|
2395
|
+
release: (el) => this.release(el),
|
|
2396
|
+
activateOne: (el) => this.activateOne(el),
|
|
2397
|
+
deactivateOne: (el) => this.deactivate(el),
|
|
2398
|
+
writeStatus: (el, state, next) => this.writeStatus(el, state, next),
|
|
2399
|
+
emit: (el, state, type, reason) => this.emit(el, state, type, reason)
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* Explicit entry point. Importing the library never touches the document, which keeps SSR,
|
|
2404
|
+
* hydration, and tests deterministic.
|
|
2405
|
+
*
|
|
2406
|
+
* @complexity O(n) time in the number of elements scanned.
|
|
2407
|
+
* @overallScore 100
|
|
2408
|
+
*/
|
|
2409
|
+
start() {
|
|
2410
|
+
if (this.started) return this;
|
|
2411
|
+
this.started = true;
|
|
2412
|
+
const watchdog = globalThis.setTimeout(() => this.uncloak(), CLOAK_WATCHDOG_MS);
|
|
2413
|
+
try {
|
|
2414
|
+
this.scan(this.root);
|
|
2415
|
+
if (this.shouldObserve) this.watch();
|
|
2416
|
+
} finally {
|
|
2417
|
+
globalThis.clearTimeout(watchdog);
|
|
2418
|
+
this.uncloak();
|
|
2419
|
+
}
|
|
2420
|
+
return this;
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* Process every unprocessed element in a subtree, including the root.
|
|
2424
|
+
*
|
|
2425
|
+
* `querySelectorAll` excludes the root, but an inserted subtree very often carries the
|
|
2426
|
+
* attribute on its top node — skipping it silently drops those animations.
|
|
2427
|
+
*
|
|
2428
|
+
* Two passes, and the order is the feature. Every `data-kui-define` in the subtree is registered
|
|
2429
|
+
* before any element in it is compiled, so a `<template>` at the bottom of the document is
|
|
2430
|
+
* already known to the card at the top that names it. Making forward references work by
|
|
2431
|
+
* construction rather than by handling is the point — see `core/bundles.ts`.
|
|
2432
|
+
*
|
|
2433
|
+
* @param root - Subtree to scan. Defaults to the animator's root.
|
|
2434
|
+
* @complexity O(n) time in the subtree size; O(1) extra space.
|
|
2435
|
+
* @overallScore 100
|
|
2436
|
+
*/
|
|
2437
|
+
scan(root = this.root) {
|
|
2438
|
+
if (!root) return this;
|
|
2439
|
+
const defined = this.bundles.collect(root, this.reporter);
|
|
2440
|
+
const selector = `[${ATTR.source}]`;
|
|
2441
|
+
if (isElementNode(root) && root.matches(selector)) this.process(root);
|
|
2442
|
+
for (const el of root.querySelectorAll(selector)) this.process(el);
|
|
2443
|
+
applyStagger(root, this.reporter);
|
|
2444
|
+
if (defined > 0 && root !== this.root) this.retryPending();
|
|
2445
|
+
return this;
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Recompile every element that stalled on a name the library did not know.
|
|
2449
|
+
*
|
|
2450
|
+
* `pending` is stamped for any unresolved name, not only an undefined bundle, so this can retry
|
|
2451
|
+
* an element that will fail again. That is deliberate: the alternative is an index of which
|
|
2452
|
+
* element is waiting on which name, and the cost of being wrong here is one wasted compile on a
|
|
2453
|
+
* page that already has a warning about it.
|
|
2454
|
+
*
|
|
2455
|
+
* @complexity O(p) time in the number of pending elements.
|
|
2456
|
+
* @overallScore 100
|
|
2457
|
+
*/
|
|
2458
|
+
retryPending() {
|
|
2459
|
+
for (const el of this.root.querySelectorAll(`[${ATTR.state}="pending"]`)) this.process(el);
|
|
2460
|
+
}
|
|
2461
|
+
/**
|
|
2462
|
+
* Compile and install one element's effects, recompiling if its attribute changed.
|
|
2463
|
+
*
|
|
2464
|
+
* @param el - Element carrying `data-kui`.
|
|
2465
|
+
* @complexity O(e) time in the number of composed effects; O(e) space for the plan.
|
|
2466
|
+
* @overallScore 100
|
|
2467
|
+
*/
|
|
2468
|
+
process(el) {
|
|
2469
|
+
if (el.hasAttribute(ATTR.define)) return;
|
|
2470
|
+
const attributes = readAttributes(el);
|
|
2471
|
+
const fingerprint = fingerprintOf(attributes);
|
|
2472
|
+
const existing = this.states.get(el);
|
|
2473
|
+
if (existing?.fingerprint === fingerprint) return;
|
|
2474
|
+
if (existing) this.release(el);
|
|
2475
|
+
const parsed = scopeHoists(this.bundles.expand(parse(attributes.source)), this.registry);
|
|
2476
|
+
const baseConfig = resolveConfig(attributes, parsed);
|
|
2477
|
+
const config = { ...baseConfig };
|
|
2478
|
+
const document2 = compileTargets(parsed, this.registry, baseConfig.timeline);
|
|
2479
|
+
const facts = document2.targets[0].plan;
|
|
2480
|
+
this.applyViewportGates(el, document2);
|
|
2481
|
+
config.activation = this.resolveActivation(el, baseConfig, facts);
|
|
2482
|
+
this.reportCompiled(el, document2);
|
|
2483
|
+
if (document2.targets.every((target) => target.plan.fxNames.length === 0)) {
|
|
2484
|
+
el.setAttribute(ATTR.state, facts.unknown.length > 0 ? "pending" : "failed");
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
this.install({ el, fingerprint, parsed, config, baseConfig, document: document2 });
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* Report everything a finished compile has to say about one element.
|
|
2491
|
+
*
|
|
2492
|
+
* Three sources, one place. The third is separate from the plan's own warnings because `compile`
|
|
2493
|
+
* is pure and environment-free by design — it is handed a registry and a timeline, never the
|
|
2494
|
+
* browser it is running in. "This browser cannot render that channel" is only answerable here,
|
|
2495
|
+
* where the detected capabilities live, and `targets[0]`'s channel union is the merged one, so it
|
|
2496
|
+
* runs once per element rather than once per `target:` group.
|
|
2497
|
+
*
|
|
2498
|
+
* @complexity O(w) time in the total warning count; O(1) space.
|
|
2499
|
+
* @overallScore 100
|
|
2500
|
+
*/
|
|
2501
|
+
reportCompiled(el, document2) {
|
|
2502
|
+
for (const warning of document2.warnings) this.reporter.warn(warning, el);
|
|
2503
|
+
for (const target of document2.targets) {
|
|
2504
|
+
for (const warning of target.plan.warnings) this.reporter.warn(warning, el);
|
|
2505
|
+
}
|
|
2506
|
+
const channels = [...new Set(document2.targets.flatMap((target) => target.plan.channels))];
|
|
2507
|
+
for (const warning of unsupportedChannelWarnings(channels, this.capabilities)) {
|
|
2508
|
+
this.reporter.warn(warning, el);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Apply a compiled plan and bind its activation.
|
|
2513
|
+
*
|
|
2514
|
+
* @complexity O(e) time in composed effects; O(e) space for retained cleanups.
|
|
2515
|
+
* @overallScore 100
|
|
2516
|
+
*/
|
|
2517
|
+
/**
|
|
2518
|
+
* Choose the activation, letting a primitive's preference fill in only when the author named
|
|
2519
|
+
* none, and reporting whatever looks wrong about one the author did name.
|
|
2520
|
+
*
|
|
2521
|
+
* Declared capability metadata was previously never checked anywhere, which made
|
|
2522
|
+
* `supportedActivations` documentation rather than a contract. The checks themselves live in
|
|
2523
|
+
* `activation.ts`'s diagnostics half — they grew a good deal when the list opened, and they
|
|
2524
|
+
* decide nothing, so keeping them here would have made this class the place where "which
|
|
2525
|
+
* activation" and "is that a real event" were the same paragraph.
|
|
2526
|
+
*
|
|
2527
|
+
* @complexity O(a) time in supported activations; O(1) space.
|
|
2528
|
+
* @overallScore 100
|
|
2529
|
+
*/
|
|
2530
|
+
resolveActivation(el, config, plan) {
|
|
2531
|
+
if (!config.activationAuthored) return plan.defaultActivation ?? config.activation;
|
|
2532
|
+
warnAboutActivation({
|
|
2533
|
+
el,
|
|
2534
|
+
spec: resolveActivationSpec(config.activation),
|
|
2535
|
+
supported: plan.supportedActivations,
|
|
2536
|
+
reporter: this.reporter
|
|
2537
|
+
});
|
|
2538
|
+
return config.activation;
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* Resolve the viewport gates (`above:` / `below:`) on this element's JavaScript-rendered effects.
|
|
2542
|
+
*
|
|
2543
|
+
* CSS-rendered segments are deliberately absent from this method. Their gate is compiled into the
|
|
2544
|
+
* `animation-name` declaration as a `var()` the browser re-resolves on every resize with no
|
|
2545
|
+
* script involved (see `core/breakpoints.ts`), which is the entire reason this feature does not
|
|
2546
|
+
* need the teardown machinery `gsap.matchMedia()` is built around. A JavaScript-rendered effect
|
|
2547
|
+
* has no `animation-name` for a stylesheet to neutralise, so it is the one case that has to be
|
|
2548
|
+
* decided here — and, having been decided once, has to be re-decided when the viewport crosses
|
|
2549
|
+
* the breakpoint it was decided at.
|
|
2550
|
+
*
|
|
2551
|
+
* @complexity O(e) time in JS-rendered effects; O(b) space, bounded by the scale's five names.
|
|
2552
|
+
* @overallScore 100
|
|
2553
|
+
*/
|
|
2554
|
+
applyViewportGates(el, document2) {
|
|
2555
|
+
const gates = document2.targets.flatMap((target) => target.plan.jsEffects).map((entry) => entry.spec.gate).filter((gate) => gate !== void 0);
|
|
2556
|
+
if (gates.length === 0) {
|
|
2557
|
+
this.gateWatcher?.unwatch(el);
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
const win = el.ownerDocument.defaultView ?? void 0;
|
|
2561
|
+
for (const target of document2.targets) {
|
|
2562
|
+
target.plan.jsEffects = target.plan.jsEffects.filter((entry) => gateMatches(entry.spec.gate, win));
|
|
2563
|
+
}
|
|
2564
|
+
this.watchGates(el, win, breakpointsIn(gates));
|
|
2565
|
+
}
|
|
2566
|
+
/**
|
|
2567
|
+
* Arm live re-evaluation for one element, building the watcher on first use.
|
|
2568
|
+
*
|
|
2569
|
+
* Lazy so that a page with no JavaScript-rendered gate — which is most pages, since the catalog
|
|
2570
|
+
* is overwhelmingly `css-keyframes` — binds no `MediaQueryList` listener at all.
|
|
2571
|
+
*
|
|
2572
|
+
* @complexity O(b) time in the element's breakpoints; O(1) amortised space.
|
|
2573
|
+
* @overallScore 100
|
|
2574
|
+
*/
|
|
2575
|
+
watchGates(el, win, breakpoints) {
|
|
2576
|
+
this.gateWatcher ??= createGateWatcher(win, (target) => this.regate(target));
|
|
2577
|
+
this.gateWatcher.watch(el, breakpoints);
|
|
2578
|
+
}
|
|
2579
|
+
/**
|
|
2580
|
+
* Rebuild one element because the viewport crossed a breakpoint its JS effects depend on.
|
|
2581
|
+
*
|
|
2582
|
+
* `release` then `process`, rather than a partial update, and rather than `process` alone:
|
|
2583
|
+
* `process` short-circuits on an unchanged fingerprint, and the fingerprint is a hash of
|
|
2584
|
+
* *attributes*, which have not changed — the viewport has. Releasing first is also what makes the
|
|
2585
|
+
* transition safe mid-animation: it aborts the activation binding, runs each JS effect's own
|
|
2586
|
+
* `destroy()`, and unwinds both ledgers, so the element is back at the author's own markup before
|
|
2587
|
+
* the new plan touches it. That is the same path an attribute edit takes, so there is one
|
|
2588
|
+
* teardown implementation rather than two.
|
|
2589
|
+
*
|
|
2590
|
+
* There is deliberately no "is this element still live?" guard. Every path that stops tracking an
|
|
2591
|
+
* element goes through `release`, and `release` unwatches — so an element that reaches here is
|
|
2592
|
+
* one the watcher still holds, which is one `release` has not run on, which is one that is live.
|
|
2593
|
+
* A guard would be unreachable code pretending to be caution, the same call `positioned()` in
|
|
2594
|
+
* `compile.ts` makes about its own missing branch.
|
|
2595
|
+
*
|
|
2596
|
+
* @complexity O(e) time in the element's composed effects; O(1) space.
|
|
2597
|
+
* @overallScore 100
|
|
2598
|
+
*/
|
|
2599
|
+
regate(el) {
|
|
2600
|
+
this.release(el);
|
|
2601
|
+
this.process(el);
|
|
2602
|
+
}
|
|
2603
|
+
/** Route a document with any non-empty-selector group through the derived-host path; `true`
|
|
2604
|
+
* when it did (a derived install's own `document` never carries one, so this cannot recurse). */
|
|
2605
|
+
installedAsHost(request) {
|
|
2606
|
+
if (!request.document.targets.some((t) => t.selector !== "")) return false;
|
|
2607
|
+
installWithTargets(this.port, request);
|
|
2608
|
+
return true;
|
|
2609
|
+
}
|
|
2610
|
+
install(request) {
|
|
2611
|
+
if (this.installedAsHost(request)) return;
|
|
2612
|
+
const { el, fingerprint, parsed, config, document: document2 } = request;
|
|
2613
|
+
const groups = document2.targets.map((target) => ({ target, matches: this.resolveGroupMatches(el, target) })).filter((group) => group.matches.length > 0);
|
|
2614
|
+
const ledgers = createLedgerSet(el);
|
|
2615
|
+
const ledger = ledgers.style(el);
|
|
2616
|
+
const attributes = ledgers.attributes(el);
|
|
2617
|
+
const controller = new AbortController();
|
|
2618
|
+
this.bindAuthorCallback(el, parsed, controller.signal);
|
|
2619
|
+
const elementHasCssAnimation = document2.targets.some(
|
|
2620
|
+
(target) => Object.keys(target.plan.declarations).length > 0
|
|
2621
|
+
);
|
|
2622
|
+
const elementStylePlan = planStyles({
|
|
2623
|
+
plan: groups[0].target.plan,
|
|
2624
|
+
config,
|
|
2625
|
+
capabilities: this.capabilities,
|
|
2626
|
+
respectReducedMotion: this.respectReducedMotion,
|
|
2627
|
+
elementHasCssAnimation
|
|
2628
|
+
});
|
|
2629
|
+
const state = {
|
|
2630
|
+
fingerprint,
|
|
2631
|
+
specs: parsed.specs,
|
|
2632
|
+
activation: config.activation,
|
|
2633
|
+
timeline: config.timeline,
|
|
2634
|
+
fxNames: document2.targets.flatMap((target) => target.plan.fxNames),
|
|
2635
|
+
jsEffectNames: document2.targets.flatMap(
|
|
2636
|
+
(target) => target.plan.jsEffects.map((entry) => entry.spec.name)
|
|
2637
|
+
),
|
|
2638
|
+
progressDriven: elementStylePlan.gate === "scrubbed" || elementStylePlan.gate === "native-timeline",
|
|
2639
|
+
instances: [],
|
|
2640
|
+
ledger,
|
|
2641
|
+
attributes,
|
|
2642
|
+
ledgers,
|
|
2643
|
+
controller,
|
|
2644
|
+
status: "ready"
|
|
2645
|
+
};
|
|
2646
|
+
if (request.host) state.host = request.host;
|
|
2647
|
+
if (request.gateOwner) state.gateOwner = request.gateOwner;
|
|
2648
|
+
this.states.set(el, state);
|
|
2649
|
+
this.liveElements.add(el);
|
|
2650
|
+
attributes.set(ATTR.state, "ready");
|
|
2651
|
+
const context = {
|
|
2652
|
+
el,
|
|
2653
|
+
state,
|
|
2654
|
+
ledgers,
|
|
2655
|
+
config,
|
|
2656
|
+
signal: controller.signal,
|
|
2657
|
+
elementHasCssAnimation
|
|
2658
|
+
};
|
|
2659
|
+
for (const group of groups) this.installGroup(group, context);
|
|
2660
|
+
this.openGate({ el, state, stylePlan: elementStylePlan, config, plan: groups[0].target.plan });
|
|
2661
|
+
}
|
|
2662
|
+
/**
|
|
2663
|
+
* Install a zero-instance aggregate host: a `target:` host with no own (empty-selector) group,
|
|
2664
|
+
* whose status mirrors its derived matches' instead of running any effect of its own
|
|
2665
|
+
* (`syncAggregate`). Called by 2a's `installWithTargets` after every claimed match has already
|
|
2666
|
+
* been installed, which is what D-E's stamping order requires — the host's own `data-kui-state`
|
|
2667
|
+
* must never appear before its matches'.
|
|
2668
|
+
*
|
|
2669
|
+
* @complexity O(g) time in the document's group count; O(1) space.
|
|
2670
|
+
* @overallScore 100
|
|
2671
|
+
*/
|
|
2672
|
+
installAggregate(request) {
|
|
2673
|
+
const { el, fingerprint, parsed, config, document: document2 } = request;
|
|
2674
|
+
const ledgers = createLedgerSet(el);
|
|
2675
|
+
const controller = new AbortController();
|
|
2676
|
+
this.bindAuthorCallback(el, parsed, controller.signal);
|
|
2677
|
+
const state = {
|
|
2678
|
+
fingerprint,
|
|
2679
|
+
specs: parsed.specs,
|
|
2680
|
+
activation: config.activation,
|
|
2681
|
+
timeline: config.timeline,
|
|
2682
|
+
fxNames: document2.targets.flatMap((target) => target.plan.fxNames),
|
|
2683
|
+
jsEffectNames: [],
|
|
2684
|
+
progressDriven: false,
|
|
2685
|
+
instances: [],
|
|
2686
|
+
ledger: ledgers.style(el),
|
|
2687
|
+
attributes: ledgers.attributes(el),
|
|
2688
|
+
ledgers,
|
|
2689
|
+
controller,
|
|
2690
|
+
status: "ready",
|
|
2691
|
+
aggregate: true
|
|
2692
|
+
};
|
|
2693
|
+
this.states.set(el, state);
|
|
2694
|
+
this.liveElements.add(el);
|
|
2695
|
+
state.attributes.set(ATTR.state, "ready");
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Apply one `target:` group's compiled plan to every element its selector resolved to.
|
|
2699
|
+
*
|
|
2700
|
+
* Its own `planStyles` call, not the element-wide one `install` already made: `declarations` are
|
|
2701
|
+
* per group, so `fade-up target:h2, pin` writes different properties to the `h2` than to the host
|
|
2702
|
+
* even though both share one gate, one activation and one reduced-motion policy (which is exactly
|
|
2703
|
+
* what `elementHasCssAnimation` carries in from the caller).
|
|
2704
|
+
*
|
|
2705
|
+
* `group.target.selector` is always `''` here — `installedAsHost` diverts any document carrying a
|
|
2706
|
+
* real `target:` group to `installWithTargets` before `install` (this method's only caller) ever
|
|
2707
|
+
* runs, so `matches` is always the host's own single-element `[el]` (target:-everywhere reconcile
|
|
2708
|
+
* R-8 removed the in-place relocation path this method used to also handle, along with the
|
|
2709
|
+
* `indexTargetGroup` call it made only for a real group — `restageTargets`, in
|
|
2710
|
+
* `core/derived/install.ts`, is the one that indexes a real target group's stagger keys now).
|
|
2711
|
+
*
|
|
2712
|
+
* @complexity O(m * p) time in the group's matches and the plan's properties; O(p) space.
|
|
2713
|
+
* @overallScore 100
|
|
2714
|
+
*/
|
|
2715
|
+
installGroup(group, context) {
|
|
2716
|
+
const { target, matches } = group;
|
|
2717
|
+
const stylePlan = planStyles({
|
|
2718
|
+
plan: target.plan,
|
|
2719
|
+
config: context.config,
|
|
2720
|
+
capabilities: this.capabilities,
|
|
2721
|
+
respectReducedMotion: this.respectReducedMotion,
|
|
2722
|
+
elementHasCssAnimation: context.elementHasCssAnimation
|
|
2723
|
+
});
|
|
2724
|
+
const hasCssAnimation = Object.keys(stylePlan.properties).some(
|
|
2725
|
+
(property) => property.startsWith("animation-")
|
|
2726
|
+
);
|
|
2727
|
+
const writes = { target, stylePlan, hasCssAnimation };
|
|
2728
|
+
for (const match of matches) this.installMatch(match, writes, context);
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Write one group's plan onto one of its matched elements, and register the instances that plan
|
|
2732
|
+
* produced there.
|
|
2733
|
+
*
|
|
2734
|
+
* Every write goes through `context.ledgers`, never `match.style`/`match.setAttribute` directly:
|
|
2735
|
+
* under `scope:page` a match need not be a descendant of the host at all, so the ledger set is
|
|
2736
|
+
* the only thing that knows to unwind it on `release()`.
|
|
2737
|
+
*
|
|
2738
|
+
* @complexity O(p) time in the plan's property count; O(1) space beyond the instances pushed.
|
|
2739
|
+
* @overallScore 100
|
|
2740
|
+
*/
|
|
2741
|
+
installMatch(match, writes, context) {
|
|
2742
|
+
const { target, stylePlan, hasCssAnimation } = writes;
|
|
2743
|
+
const { state, ledgers } = context;
|
|
2744
|
+
const matchLedger = ledgers.style(match);
|
|
2745
|
+
const matchAttributes = ledgers.attributes(match);
|
|
2746
|
+
for (const [property, value] of Object.entries(stylePlan.properties)) {
|
|
2747
|
+
matchLedger.set(property, value);
|
|
2748
|
+
}
|
|
2749
|
+
matchAttributes.set(ATTR.normalized, stylePlan.attributes[ATTR.normalized]);
|
|
2750
|
+
matchAttributes.set(ATTR.rm, stylePlan.attributes[ATTR.rm]);
|
|
2751
|
+
matchLedger.claim("animation-play-state");
|
|
2752
|
+
if (hasCssAnimation) {
|
|
2753
|
+
const scrubbed = stylePlan.gate === "scrubbed";
|
|
2754
|
+
state.instances.push(
|
|
2755
|
+
createCssInstance(match, matchLedger, target.plan.keyframeNames, scrubbed)
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
state.instances.push(
|
|
2759
|
+
...this.jsEffectPreparer.prepare({
|
|
2760
|
+
el: match,
|
|
2761
|
+
plan: target.plan,
|
|
2762
|
+
signal: context.signal,
|
|
2763
|
+
ledger: matchLedger
|
|
2764
|
+
})
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
/**
|
|
2768
|
+
* Wire an authored `func:` to this element's completion.
|
|
2769
|
+
*
|
|
2770
|
+
* Registered as an ordinary `kui:finish` listener rather than called from a dispatch site, so it
|
|
2771
|
+
* can never fire at a moment the event does not — the key is sugar for that listener, and is
|
|
2772
|
+
* implemented as literally that listener. It detaches with every other binding on
|
|
2773
|
+
* `controller.abort()`.
|
|
2774
|
+
*
|
|
2775
|
+
* Called from `install` *before* `openGate`, and the order is load-bearing: the one `kui:finish`
|
|
2776
|
+
* an element can receive during install is the synchronous `reduced-motion` one, and the visitor
|
|
2777
|
+
* who asked for reduced motion is the last one whose chained step should silently be dropped.
|
|
2778
|
+
*
|
|
2779
|
+
* See `callback.ts` for the lookup rules, and for why this key must never be built from
|
|
2780
|
+
* untrusted input.
|
|
2781
|
+
*
|
|
2782
|
+
* @complexity O(1) time and space.
|
|
2783
|
+
* @overallScore 100
|
|
2784
|
+
*/
|
|
2785
|
+
bindAuthorCallback(el, parsed, signal) {
|
|
2786
|
+
if (parsed.func === void 0) return;
|
|
2787
|
+
bindCallback({ el, name: parsed.func, reporter: this.reporter, signal });
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Validate and resolve one `CompiledTarget`'s selector against the live document.
|
|
2791
|
+
*
|
|
2792
|
+
* `resolveTarget`'s document-wide/invalid rejection — the same rule the six built-in
|
|
2793
|
+
* `target:`-declaring primitives apply inside their own `prepare` — has to run again here for
|
|
2794
|
+
* every *other* primitive: a CSS-rendered retargeted effect never calls `prepare` at all, so
|
|
2795
|
+
* compile-to-install time is the only point at which this element has a real `Document` to
|
|
2796
|
+
* validate the selector against. `compileTargets` itself is pure and DOM-free by design and
|
|
2797
|
+
* cannot do this check.
|
|
2798
|
+
*
|
|
2799
|
+
* @param el - The host. The search root under `'self'`, and the only "match" for the host group
|
|
2800
|
+
* (`target.selector === ''`), which is never queried at all.
|
|
2801
|
+
* @returns The matches, in document order. Empty when the selector was invalid, too broad, or
|
|
2802
|
+
* simply matched nothing — every case already warned by name.
|
|
2803
|
+
* @complexity O(1) for the host group; O(n) in document size otherwise, the query itself being
|
|
2804
|
+
* the DOM's.
|
|
2805
|
+
* @overallScore 100
|
|
2806
|
+
*/
|
|
2807
|
+
resolveGroupMatches(el, target) {
|
|
2808
|
+
if (target.selector === "") return [el];
|
|
2809
|
+
const doc = el.ownerDocument;
|
|
2810
|
+
const names = target.plan.fxNames.join(", ");
|
|
2811
|
+
const breadth = selectorBreadth(target.selector, doc);
|
|
2812
|
+
if (breadth !== "ok") {
|
|
2813
|
+
const reason = breadth === "invalid" ? "is not a valid selector" : "matches the whole document";
|
|
2814
|
+
this.reporter.warn(`target "${target.selector}" (${names}) ${reason} and will be ignored`, el);
|
|
2815
|
+
return [];
|
|
2816
|
+
}
|
|
2817
|
+
const matches = queryScoped(el, { doc }, target.selector, target.scope);
|
|
2818
|
+
if (matches.length === 0) {
|
|
2819
|
+
this.reporter.warn(`target "${target.selector}" (${names}) matched nothing`, el);
|
|
2820
|
+
}
|
|
2821
|
+
return matches;
|
|
2822
|
+
}
|
|
2823
|
+
/**
|
|
2824
|
+
* Decide whether, and when, the effects on this element are allowed to start.
|
|
2825
|
+
*
|
|
2826
|
+
* The single place any effect begins. Routing both renderers through it is what makes
|
|
2827
|
+
* `on:enter`, `on:click`, `manual`, and `reducedMotion: 'disable'` mean the same thing for a
|
|
2828
|
+
* pinned section as for a fade — previously JS effects started during `prepare` and honoured
|
|
2829
|
+
* none of them.
|
|
2830
|
+
*
|
|
2831
|
+
* @complexity O(n) time in the number of instances; O(1) space.
|
|
2832
|
+
* @overallScore 100
|
|
2833
|
+
*/
|
|
2834
|
+
openGate(request) {
|
|
2835
|
+
const { el, state, stylePlan, config, plan } = request;
|
|
2836
|
+
if (this.reducedMotionDisables(plan)) {
|
|
2837
|
+
this.writeStatus(el, state, "finished");
|
|
2838
|
+
this.emit(el, state, KUI_EVENT.finish, "reduced-motion");
|
|
2839
|
+
this.syncHost(state);
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
const actions = config.actions;
|
|
2843
|
+
if (actions) {
|
|
2844
|
+
this.warnAboutCrossings(el, state, stylePlan.activation ?? config.activation, actions);
|
|
2845
|
+
}
|
|
2846
|
+
if (stylePlan.gate !== "deferred") {
|
|
2847
|
+
this.activateOne(el);
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
if (skipsOwnBinding(state)) return;
|
|
2851
|
+
const activation = stylePlan.activation;
|
|
2852
|
+
const releaseBinding = this.binder.bind(el, activation, {
|
|
2853
|
+
threshold: config.threshold,
|
|
2854
|
+
from: config.activationSource,
|
|
2855
|
+
activate: () => this.activateOne(el),
|
|
2856
|
+
deactivate: () => this.deactivate(el),
|
|
2857
|
+
// Only when the author asked for the four-way reading. Absent, the binder keeps its two-way
|
|
2858
|
+
// delivery and its one-shot release, which is what every piece of existing markup depends on.
|
|
2859
|
+
...actions ? { cross: (crossing) => this.applyCrossing(el, crossing, actions) } : {}
|
|
2860
|
+
});
|
|
2861
|
+
let released = false;
|
|
2862
|
+
const releaseOnce = () => {
|
|
2863
|
+
if (released) return;
|
|
2864
|
+
released = true;
|
|
2865
|
+
releaseBinding();
|
|
2866
|
+
};
|
|
2867
|
+
state.controller.signal.addEventListener("abort", releaseOnce);
|
|
2868
|
+
if (!actions && isOneShot(resolveActivationSpec(activation))) {
|
|
2869
|
+
state.releaseActivation = releaseOnce;
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Whether the visitor's own reduced-motion preference disables this plan outright, rather than
|
|
2874
|
+
* merely shortening or crossfading it.
|
|
2875
|
+
*
|
|
2876
|
+
* Pulled out of `openGate` so that method's own branching stays about *what to do*, not about
|
|
2877
|
+
* the two-part question of whether the policy applies at all.
|
|
2878
|
+
*
|
|
2879
|
+
* @complexity O(1) time and space.
|
|
2880
|
+
* @overallScore 100
|
|
2881
|
+
*/
|
|
2882
|
+
reducedMotionDisables(plan) {
|
|
2883
|
+
return this.respectReducedMotion && this.capabilities.reducedMotion && plan.reducedMotion === "disable";
|
|
2884
|
+
}
|
|
2885
|
+
/**
|
|
2886
|
+
* Report every way an authored `actions:` cannot do what it says, once, at bind time.
|
|
2887
|
+
*
|
|
2888
|
+
* At bind time rather than per crossing, and that is the whole reason this is a separate method:
|
|
2889
|
+
* a crossing fires on every scroll pass, and a diagnostic repeated on each one is a diagnostic
|
|
2890
|
+
* nobody reads. It is the same discipline `control.ts` applies — warn once at construction, even
|
|
2891
|
+
* for a caller who only meant to read.
|
|
2892
|
+
*
|
|
2893
|
+
* @complexity O(1) time and space.
|
|
2894
|
+
* @overallScore 100
|
|
2895
|
+
*/
|
|
2896
|
+
warnAboutCrossings(el, state, activation, actions) {
|
|
2897
|
+
const spec = resolveActivationSpec(activation);
|
|
2898
|
+
const problems = warnAboutToggleActions({
|
|
2899
|
+
actions,
|
|
2900
|
+
observed: spec.start.kind === "observed" || spec.end?.kind === "observed",
|
|
2901
|
+
activation: String(activation),
|
|
2902
|
+
jsEffectNames: state.jsEffectNames,
|
|
2903
|
+
progressDriven: state.progressDriven
|
|
2904
|
+
});
|
|
2905
|
+
for (const problem of problems) this.reporter.warn(problem, el);
|
|
2906
|
+
}
|
|
2907
|
+
/**
|
|
2908
|
+
* Do whatever this element's `actions:` names for the crossing that just happened.
|
|
2909
|
+
*
|
|
2910
|
+
* A pure dispatch, and deliberately nothing more. `toggle-actions.ts` owns what each verb means,
|
|
2911
|
+
* `activation.ts` owns which crossing it was, and this method's only contribution is the two
|
|
2912
|
+
* things neither of them can reach: the element's own directional transitions, and its instances'
|
|
2913
|
+
* playheads. If anything resembling animation logic ever appears here, it is in the wrong file.
|
|
2914
|
+
*
|
|
2915
|
+
* @complexity O(n) time in the element's instances; O(n) space.
|
|
2916
|
+
* @overallScore 100
|
|
2917
|
+
*/
|
|
2918
|
+
applyCrossing(el, crossing, actions) {
|
|
2919
|
+
const state = this.states.get(el);
|
|
2920
|
+
if (!state) return;
|
|
2921
|
+
applyToggleVerb(actions[crossing], {
|
|
2922
|
+
// `ready` is the only status that has not started; `finished` and `failed` both have, and a
|
|
2923
|
+
// `resume` on either of those is a resume rather than a fresh activation.
|
|
2924
|
+
started: state.status !== "ready",
|
|
2925
|
+
controls: state.instances.map((instance) => instance.control).filter((control2) => control2 !== void 0),
|
|
2926
|
+
activate: () => this.activateOne(el),
|
|
2927
|
+
reverse: () => this.reverseOne(el)
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
/**
|
|
2931
|
+
* Start a deferred animation.
|
|
2932
|
+
*
|
|
2933
|
+
* A JS-rendered effect's real setup work is postponed until this call — `deferPrepare` in
|
|
2934
|
+
* `instances.ts` only wires up an inert instance during `prepare` — so a broken primitive (a bad
|
|
2935
|
+
* selector, a malformed param) first throws here, not while the plan was being built. `scan()`
|
|
2936
|
+
* reaches this synchronously for every `on:load` element, inside the very loop that processes
|
|
2937
|
+
* every other element on the page; an uncaught throw here previously unwound that loop and
|
|
2938
|
+
* silently orphaned every element after the broken one — the same blast radius the `__proto__`
|
|
2939
|
+
* scan-crash fix closed for a different door. Each instance is isolated so one effect's failure
|
|
2940
|
+
* can neither strand a sibling effect on the same element nor abort the rest of the scan.
|
|
2941
|
+
*
|
|
2942
|
+
* @complexity O(n) time in composed instances; O(1) space.
|
|
2943
|
+
* @overallScore 100
|
|
2944
|
+
*/
|
|
2945
|
+
activate(el) {
|
|
2946
|
+
this.activateOne(el);
|
|
2947
|
+
for (const match of this.derivedOf(el)) this.activateOne(match);
|
|
2948
|
+
}
|
|
2949
|
+
/** The per-element half of {@link activate} — see that method's own doc comment. */
|
|
2950
|
+
activateOne(el) {
|
|
2951
|
+
const state = this.states.get(el);
|
|
2952
|
+
if (!state) return;
|
|
2953
|
+
if (state.aggregate) return;
|
|
2954
|
+
if (state.status === "running" && state.direction === "reverse") {
|
|
2955
|
+
this.turnAround(el, state);
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
if (state.status === "running") return;
|
|
2959
|
+
state.releaseActivation?.();
|
|
2960
|
+
state.releaseActivation = void 0;
|
|
2961
|
+
this.writeStatus(el, state, "running");
|
|
2962
|
+
state.direction = "forward";
|
|
2963
|
+
const run = this.beginRun(state);
|
|
2964
|
+
const started = state.instances.filter((instance) => this.startInstance(instance, el));
|
|
2965
|
+
if (started.length === 0 && state.instances.length > 0) {
|
|
2966
|
+
this.writeStatus(el, state, "failed");
|
|
2967
|
+
this.syncHost(state);
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
this.emit(el, state, KUI_EVENT.start, "activated");
|
|
2971
|
+
this.syncHost(state);
|
|
2972
|
+
this.settleWhen({ el, state, run }, started, "finished");
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Play an element's effects back out.
|
|
2976
|
+
*
|
|
2977
|
+
* The exit half of a paired activation — `data-kui-on="pointerenter/pointerleave"`,
|
|
2978
|
+
* `data-kui-on="enter/leave"` — routed through one method for the same reason `activate` is: it
|
|
2979
|
+
* is the single place an effect ever runs backwards, so what an exit means does not have to be
|
|
2980
|
+
* re-decided per activation.
|
|
2981
|
+
*
|
|
2982
|
+
* **CSS-rendered effects reverse; JS-rendered ones do not, and say so.** A CSS effect has a real
|
|
2983
|
+
* `Animation` handle whose playback rate can simply be negated, landing on the from-state that
|
|
2984
|
+
* `animation-fill-mode: both` is already holding. A JS-rendered effect has no playhead at all —
|
|
2985
|
+
* `getAnimations()` returns `[]` for it (see `play.ts`) — and there is no honest general shim
|
|
2986
|
+
* for "half a `split-flap`, backwards". Rather than invent one that misbehaves differently per
|
|
2987
|
+
* primitive, an instance that cannot reverse is named in a warning; the author learns their
|
|
2988
|
+
* pointerleave does nothing *here* instead of discovering it in a browser. On an element
|
|
2989
|
+
* composing both renderers the CSS half still reverses and the warning names what did not.
|
|
2990
|
+
*
|
|
2991
|
+
* @param el - Element whose effects should play out.
|
|
2992
|
+
* @complexity O(n) time in the number of instances; O(1) space.
|
|
2993
|
+
* @overallScore 100
|
|
2994
|
+
*/
|
|
2995
|
+
deactivate(el) {
|
|
2996
|
+
const state = this.states.get(el);
|
|
2997
|
+
if (!state) return;
|
|
2998
|
+
if (state.status !== "running" && state.status !== "finished") return;
|
|
2999
|
+
if (state.direction === "reverse") return;
|
|
3000
|
+
const reversible = state.instances.filter(isDirectional);
|
|
3001
|
+
if (reversible.length < state.instances.length) {
|
|
3002
|
+
this.reporter.warn(
|
|
3003
|
+
"effect cannot play backwards, so the exit half of this activation does nothing for it (JS-rendered effects have no playhead \u2014 see EffectInstance.reverse)",
|
|
3004
|
+
el
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
this.reverseOne(el);
|
|
3008
|
+
}
|
|
3009
|
+
/**
|
|
3010
|
+
* Turn an element's playhead around and settle it back at the from-state it started from.
|
|
3011
|
+
*
|
|
3012
|
+
* The single owner of `state.direction === 'reverse'`, and it exists as its own method because
|
|
3013
|
+
* for a while there were two owners and neither knew about the other. `deactivate()` — the exit
|
|
3014
|
+
* half of a paired activation — flipped the direction and re-armed the settle gate;
|
|
3015
|
+
* `control(el).reverse()` reached straight past the state machine into the raw `Animation`
|
|
3016
|
+
* handles through `InstanceControl.reverse`. Both moved the same playheads, only one of them said
|
|
3017
|
+
* so, and an animator that believed a reversing element was still travelling forwards got three
|
|
3018
|
+
* things wrong at once: a later `deactivate()` sailed through its "an exit already in flight must
|
|
3019
|
+
* not restart" guard and began a *second* reverse; `activate()` could never turn the playhead
|
|
3020
|
+
* around, because it looks for `'reverse'` and never saw it; and the forward `settleWhen` left
|
|
3021
|
+
* over from the entrance stayed pending until it stamped `data-kui-state="finished"` onto an
|
|
3022
|
+
* element sitting at its from-state.
|
|
3023
|
+
*
|
|
3024
|
+
* So the transition lives here and both entry points call it. What `deactivate()` keeps is only
|
|
3025
|
+
* what is true of an *activation* exit specifically. A programmatic reverse is not one — an
|
|
3026
|
+
* author calling `control().reverse()` has not had a pointer leave anything — so it inherits
|
|
3027
|
+
* none of that, and in particular is not told that the effects it cannot reach make "the exit
|
|
3028
|
+
* half of this activation" do nothing. `control.ts` has already named those for it.
|
|
3029
|
+
*
|
|
3030
|
+
* Resuming forward travel is `activate()`'s job, not a second method here: an element left in
|
|
3031
|
+
* `'reverse'` is turned around by `turnAround`, which is reachable from every route that
|
|
3032
|
+
* activates — a pointer coming back, `play()`, `kui.activate()`.
|
|
3033
|
+
*
|
|
3034
|
+
* @param el - Element whose effects should run backwards. Unknown elements are ignored, exactly
|
|
3035
|
+
* as they are by `activate` and `deactivate`.
|
|
3036
|
+
* @complexity O(n) time in the number of instances; O(1) space.
|
|
3037
|
+
* @overallScore 100
|
|
3038
|
+
*/
|
|
3039
|
+
reverseFrom(el) {
|
|
3040
|
+
this.reverseOne(el);
|
|
3041
|
+
for (const match of this.derivedOf(el)) this.reverseOne(match);
|
|
3042
|
+
}
|
|
3043
|
+
/** The per-element half of {@link reverseFrom} — see that method's own doc comment. */
|
|
3044
|
+
reverseOne(el) {
|
|
3045
|
+
const state = this.states.get(el);
|
|
3046
|
+
if (!state) return;
|
|
3047
|
+
if (state.status !== "running" && state.status !== "finished") return;
|
|
3048
|
+
if (state.direction === "reverse") return;
|
|
3049
|
+
const reversible = state.instances.filter(isDirectional);
|
|
3050
|
+
if (reversible.length === 0) return;
|
|
3051
|
+
this.writeStatus(el, state, "running");
|
|
3052
|
+
state.direction = "reverse";
|
|
3053
|
+
this.syncHost(state);
|
|
3054
|
+
const run = this.beginRun(state);
|
|
3055
|
+
for (const instance of reversible) instance.reverse();
|
|
3056
|
+
this.settleWhen({ el, state, run }, reversible, "ready");
|
|
3057
|
+
}
|
|
3058
|
+
/**
|
|
3059
|
+
* Resume forward playback on an element whose exit is still in flight.
|
|
3060
|
+
*
|
|
3061
|
+
* Separate from `activate`'s main path because none of that path applies: the instances are
|
|
3062
|
+
* already started, the activation is already spent, and the state is already `running`. All that
|
|
3063
|
+
* changes is the direction of travel.
|
|
3064
|
+
*
|
|
3065
|
+
* @complexity O(n) time in the number of instances; O(1) space.
|
|
3066
|
+
* @overallScore 100
|
|
3067
|
+
*/
|
|
3068
|
+
turnAround(el, state) {
|
|
3069
|
+
const playable = state.instances.filter(isDirectional);
|
|
3070
|
+
state.direction = "forward";
|
|
3071
|
+
const run = this.beginRun(state);
|
|
3072
|
+
for (const instance of playable) instance.play();
|
|
3073
|
+
this.settleWhen({ el, state, run }, playable, "finished");
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Write an element's final state once the instances driving it have finished.
|
|
3077
|
+
*
|
|
3078
|
+
* The reported state has to become truthful eventually, or `data-kui-state` codifies a lie that
|
|
3079
|
+
* tests then assert against. It also has to stay truthful when a run is superseded mid-flight: a
|
|
3080
|
+
* reverse that is turned around leaves its own `finished` promise pending, and letting that
|
|
3081
|
+
* promise write `ready` over the forward run that replaced it is how an element ends up claiming
|
|
3082
|
+
* it is back at its from-state while visibly finishing its entrance. Capturing the direction at
|
|
3083
|
+
* the time of the call and re-checking it on resolution is what makes the stale promise a no-op
|
|
3084
|
+
* in the ordinary case — turned-around and reversed-again runs, which is why that comment and
|
|
3085
|
+
* check are kept even though `run` below also covers them.
|
|
3086
|
+
*
|
|
3087
|
+
* `run` is the check `direction` cannot be: `direction` only ever holds two values, so a run
|
|
3088
|
+
* that flips back and forth several times (a hover-driven card flip, `pointerenter`/
|
|
3089
|
+
* `pointerleave` firing repeatedly) revisits the same direction repeatedly, and a stale run's
|
|
3090
|
+
* belated completion could otherwise be mistaken for a much later one simply travelling the same
|
|
3091
|
+
* way. `beginRun` mints one identity per run; only the run that is still current when its own
|
|
3092
|
+
* promise resolves gets to report anything.
|
|
3093
|
+
*
|
|
3094
|
+
* @param target - The element, its state, and this run's identity — see {@link SettleTarget}.
|
|
3095
|
+
* @param instances - The instances this particular run started; a run only waits on its own.
|
|
3096
|
+
* @param next - State to report when they are all done.
|
|
3097
|
+
* @complexity O(n) time in the instances; O(n) space for the pending promises.
|
|
3098
|
+
* @overallScore 100
|
|
3099
|
+
*/
|
|
3100
|
+
settleWhen(target, instances, next) {
|
|
3101
|
+
const { el, state, run } = target;
|
|
3102
|
+
const timed = instances.filter((instance) => !instance.continuous);
|
|
3103
|
+
if (timed.length === 0 && instances.length > 0) {
|
|
3104
|
+
this.settleArmed.set(state, false);
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
this.settleArmed.set(state, true);
|
|
3108
|
+
const direction = state.direction;
|
|
3109
|
+
void Promise.all(timed.map((instance) => instance.finished)).then(() => {
|
|
3110
|
+
if (this.states.get(el) !== state || state.status !== "running") return;
|
|
3111
|
+
if (state.direction !== direction) return;
|
|
3112
|
+
if (this.currentRun.get(state) !== run) return;
|
|
3113
|
+
this.writeStatus(el, state, next);
|
|
3114
|
+
if (!state.cancelled) {
|
|
3115
|
+
if (next === "finished") this.emit(el, state, KUI_EVENT.finish, "complete");
|
|
3116
|
+
else this.emit(el, state, KUI_EVENT.reverseFinish, "reversed");
|
|
3117
|
+
}
|
|
3118
|
+
this.syncHost(state);
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Mark the start of a new run — a forward activation, a reversal, or a turnaround out of one —
|
|
3123
|
+
* and clear the mark a previous run's cancellation left behind.
|
|
3124
|
+
*
|
|
3125
|
+
* The two things a fresh run does are bound together deliberately: `cancelled` describes *this*
|
|
3126
|
+
* run, not the element for life, so every place that begins one must also disown whatever the
|
|
3127
|
+
* run before it was marked with, or that run's cancellation would silently apply to a completion
|
|
3128
|
+
* it had nothing to do with — see `InstanceState.cancelled`'s own doc comment for the contract
|
|
3129
|
+
* this keeps. Doing both here, in one call, is what stops a caller resetting one without the
|
|
3130
|
+
* other; three call sites each remembering to write two lines in the right order was exactly
|
|
3131
|
+
* this bug's shape the first time.
|
|
3132
|
+
*
|
|
3133
|
+
* @returns This run's identity, to be threaded through to the `settleWhen` call that will
|
|
3134
|
+
* eventually report on it.
|
|
3135
|
+
* @complexity O(1) time, O(1) space.
|
|
3136
|
+
* @overallScore 100
|
|
3137
|
+
*/
|
|
3138
|
+
beginRun(state) {
|
|
3139
|
+
const run = Symbol("kui-run");
|
|
3140
|
+
this.currentRun.set(state, run);
|
|
3141
|
+
state.cancelled = false;
|
|
3142
|
+
return run;
|
|
3143
|
+
}
|
|
3144
|
+
/**
|
|
3145
|
+
* Dispatch one lifecycle event for an element, filling in the identity every listener needs.
|
|
3146
|
+
*
|
|
3147
|
+
* Centralised here rather than inside each instance because the animator is the only place that
|
|
3148
|
+
* knows the *element's* lifecycle — an element composing three effects starts once and finishes
|
|
3149
|
+
* once, not three times, and only this class sees all three instances at the same moment.
|
|
3150
|
+
*
|
|
3151
|
+
* @complexity O(n) time in listeners on the propagation path; O(1) space.
|
|
3152
|
+
* @overallScore 100
|
|
3153
|
+
*/
|
|
3154
|
+
emit(el, state, type, reason) {
|
|
3155
|
+
emitLifecycle(el, type, {
|
|
3156
|
+
effects: state.fxNames,
|
|
3157
|
+
activation: state.activation,
|
|
3158
|
+
timeline: state.timeline,
|
|
3159
|
+
reason,
|
|
3160
|
+
...state.host ? { host: state.host } : {}
|
|
3161
|
+
});
|
|
3162
|
+
}
|
|
3163
|
+
/**
|
|
3164
|
+
* Write one element's status, through its own ledger, with no aggregate sync.
|
|
3165
|
+
*
|
|
3166
|
+
* The one place `status`/`data-kui-state` are written together, so every call site that used to
|
|
3167
|
+
* write both fields by hand now shares one line to drift out of sync in. `el` is unused today —
|
|
3168
|
+
* kept because 2b's tests and a future per-element hook both key on it, and because every other
|
|
3169
|
+
* write in this file takes the element it writes to as its first argument.
|
|
3170
|
+
*
|
|
3171
|
+
* @complexity O(1) time and space.
|
|
3172
|
+
* @overallScore 100
|
|
3173
|
+
*/
|
|
3174
|
+
writeStatus(el, state, next) {
|
|
3175
|
+
state.status = next;
|
|
3176
|
+
state.attributes.set(ATTR.state, next);
|
|
3177
|
+
}
|
|
3178
|
+
/**
|
|
3179
|
+
* Let a derived state's host re-derive its aggregate status (D-A).
|
|
3180
|
+
*
|
|
3181
|
+
* Called after the derived element's own event, so listeners see the match's event before the
|
|
3182
|
+
* host's — the same order a nested DOM structure would deliver them in were the host merely
|
|
3183
|
+
* listening for its children's events instead of deriving its own status from them.
|
|
3184
|
+
*
|
|
3185
|
+
* @complexity O(1) time and space, delegating to {@link syncAggregate}.
|
|
3186
|
+
* @overallScore 100
|
|
3187
|
+
*/
|
|
3188
|
+
syncHost(state) {
|
|
3189
|
+
if (state.host) syncAggregate(this.port, state.host);
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
3192
|
+
* Stop one element's effects where they are, leaving it mid-animation.
|
|
3193
|
+
*
|
|
3194
|
+
* The counterpart to `activate()`, and the reason it exists rather than callers reaching into
|
|
3195
|
+
* `stateOf(el).instances` themselves (which is what `play()`'s handle used to do): cancellation
|
|
3196
|
+
* has an observable consequence — `kui:cancel`, and the suppression of the `kui:finish` that
|
|
3197
|
+
* would otherwise follow it — and that consequence has to be applied wherever cancellation
|
|
3198
|
+
* happens, not only where it happens to be convenient.
|
|
3199
|
+
*
|
|
3200
|
+
* @param el - Element whose effects should stop.
|
|
3201
|
+
* @complexity O(n) time in the element's instances; O(1) space.
|
|
3202
|
+
* @overallScore 100
|
|
3203
|
+
*/
|
|
3204
|
+
cancel(el) {
|
|
3205
|
+
this.cancelOne(el);
|
|
3206
|
+
for (const match of this.derivedOf(el)) this.cancelOne(match);
|
|
3207
|
+
}
|
|
3208
|
+
/** The per-element half of {@link cancel} — see that method's own doc comment. */
|
|
3209
|
+
cancelOne(el) {
|
|
3210
|
+
const state = this.states.get(el);
|
|
3211
|
+
if (!state) return;
|
|
3212
|
+
const wasRunning = state.status === "running";
|
|
3213
|
+
state.cancelled = true;
|
|
3214
|
+
for (const instance of state.instances) runQuietly(() => instance.cancel());
|
|
3215
|
+
if (wasRunning && this.settleArmed.get(state) === false) {
|
|
3216
|
+
this.writeStatus(el, state, "finished");
|
|
3217
|
+
this.syncHost(state);
|
|
3218
|
+
}
|
|
3219
|
+
if (wasRunning) this.emit(el, state, KUI_EVENT.cancel, "cancelled");
|
|
3220
|
+
}
|
|
3221
|
+
/**
|
|
3222
|
+
* Runtime control over a selection's playheads — pause, resume, reverse, seek, re-speed.
|
|
3223
|
+
*
|
|
3224
|
+
* Selection-shaped rather than single-element, so it matches `play()` and so one call covers a
|
|
3225
|
+
* staggered group. `control.ts` builds a per-element handle underneath and the returned handle
|
|
3226
|
+
* composes them.
|
|
3227
|
+
*
|
|
3228
|
+
* @param target - Selector, element, or iterable of elements.
|
|
3229
|
+
* @complexity O(n) time in selected elements and their instances; O(n) space.
|
|
3230
|
+
* @overallScore 100
|
|
3231
|
+
*/
|
|
3232
|
+
control(target) {
|
|
3233
|
+
return control({ animator: this, root: this.root, target });
|
|
3234
|
+
}
|
|
3235
|
+
/**
|
|
3236
|
+
* Activate one instance, isolating a throw from its (possibly deferred) setup.
|
|
3237
|
+
*
|
|
3238
|
+
* @returns Whether the instance actually started — a failed instance is excluded from the
|
|
3239
|
+
* `finished` gate in `activate()`, since something that never started can never legitimately
|
|
3240
|
+
* finish (see `EffectInstance.finished`'s "resolves, never rejects" contract in `types.ts`).
|
|
3241
|
+
* @complexity O(1) time, O(1) space.
|
|
3242
|
+
* @overallScore 100
|
|
3243
|
+
*/
|
|
3244
|
+
startInstance(instance, el) {
|
|
3245
|
+
try {
|
|
3246
|
+
instance.activate();
|
|
3247
|
+
return true;
|
|
3248
|
+
} catch (error) {
|
|
3249
|
+
this.reporter.warn(`effect failed to activate: ${String(error)}`, el);
|
|
3250
|
+
return false;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Programmatic entry point. Accepts a selector, an Element, a NodeList, or any iterable, so
|
|
3255
|
+
* `getElementById`, `getElementsByClassName`, and `querySelectorAll` all work directly.
|
|
3256
|
+
*
|
|
3257
|
+
* @complexity O(n) time in selected elements.
|
|
3258
|
+
* @overallScore 100
|
|
3259
|
+
*/
|
|
3260
|
+
play(target, effect, options = {}) {
|
|
3261
|
+
return play({ animator: this, root: this.root, target, effect }, options);
|
|
3262
|
+
}
|
|
3263
|
+
/**
|
|
3264
|
+
* Remove the opt-in cloak so a stalled or failed initialisation can never leave a page hidden.
|
|
3265
|
+
*
|
|
3266
|
+
* @complexity O(1) time, O(1) space.
|
|
3267
|
+
* @overallScore 100
|
|
3268
|
+
*/
|
|
3269
|
+
uncloak() {
|
|
3270
|
+
const doc = isElementNode(this.root) ? this.root.ownerDocument : this.root;
|
|
3271
|
+
doc?.documentElement?.removeAttribute(ATTR.cloak);
|
|
3272
|
+
}
|
|
3273
|
+
stateOf(el) {
|
|
3274
|
+
return this.states.get(el);
|
|
3275
|
+
}
|
|
3276
|
+
/** The derived matches a host's `target:` groups installed, in document order. `[]` otherwise. */
|
|
3277
|
+
derivedOf(el) {
|
|
3278
|
+
return derivedOf(this.book, el);
|
|
3279
|
+
}
|
|
3280
|
+
/** The host that installed a derived match, else `undefined`. */
|
|
3281
|
+
hostOf(el) {
|
|
3282
|
+
return hostOf(this.book, el);
|
|
3283
|
+
}
|
|
3284
|
+
/**
|
|
3285
|
+
* Tear an element's effects down so the next `process()` reinstalls from scratch.
|
|
3286
|
+
*
|
|
3287
|
+
* Needed for replay: `process()` short-circuits when the configuration fingerprint is
|
|
3288
|
+
* unchanged, so playing the same effect twice was previously a no-op.
|
|
3289
|
+
*
|
|
3290
|
+
* @complexity O(c) time in retained instances; O(1) space.
|
|
3291
|
+
* @overallScore 100
|
|
3292
|
+
*/
|
|
3293
|
+
reset(el) {
|
|
3294
|
+
this.release(el);
|
|
3295
|
+
}
|
|
3296
|
+
/**
|
|
3297
|
+
* Tear down one element's effects and clear its library-owned attributes.
|
|
3298
|
+
*
|
|
3299
|
+
* @complexity O(c) time in retained cleanups; O(1) extra space.
|
|
3300
|
+
* @overallScore 100
|
|
3301
|
+
*/
|
|
3302
|
+
release(el) {
|
|
3303
|
+
const state = this.states.get(el);
|
|
3304
|
+
if (!state) return;
|
|
3305
|
+
releaseDerived(this.port, el);
|
|
3306
|
+
const wasRunning = state.status === "running";
|
|
3307
|
+
state.cancelled = true;
|
|
3308
|
+
state.controller.abort();
|
|
3309
|
+
for (const instance of state.instances) runQuietly(() => instance.destroy());
|
|
3310
|
+
this.states.delete(el);
|
|
3311
|
+
this.liveElements.delete(el);
|
|
3312
|
+
this.gateWatcher?.unwatch(el);
|
|
3313
|
+
state.ledgers.restore();
|
|
3314
|
+
if (wasRunning) this.emit(el, state, KUI_EVENT.cancel, "reset");
|
|
3315
|
+
}
|
|
3316
|
+
/**
|
|
3317
|
+
* Tear down every tracked element inside a removed subtree.
|
|
3318
|
+
*
|
|
3319
|
+
* Scoped to `node`'s own descendants rather than re-scanning `liveElements` against the whole
|
|
3320
|
+
* page, so a removal event costs O(removed subtree), not O(every animated element alive
|
|
3321
|
+
* anywhere) — `dom-watcher.ts` can queue up to 100 removed roots per frame, and `liveElements`
|
|
3322
|
+
* only shrinks on release, so it stays large on a scroll-reveal-heavy page.
|
|
3323
|
+
*
|
|
3324
|
+
* Membership is checked against `liveElements` (the ground truth) rather than re-querying
|
|
3325
|
+
* `[${ATTR.source}]` the way `scan()` does: `dom-watcher.ts`'s `flush()` drains removed roots
|
|
3326
|
+
* before attribute-change roots, so if calling code strips `data-kui` and removes the element in
|
|
3327
|
+
* the same tick, a selector-based query would already miss it here and leak its teardown.
|
|
3328
|
+
*
|
|
3329
|
+
* `node` is typed `Element`, not `ParentNode`: `dom-watcher.ts`'s `onElementRemoved` — this
|
|
3330
|
+
* method's only caller — is itself typed `(el: Element) => void`, so there is no runtime case
|
|
3331
|
+
* where `node` is a `Document`/`DocumentFragment` to guard against.
|
|
3332
|
+
*
|
|
3333
|
+
* `restageAfterRemoval` runs over every candidate visited, not only the ones actually released:
|
|
3334
|
+
* a removed stagger member is exactly the case this closes, and whether it was "live" by
|
|
3335
|
+
* `liveElements`' definition is a question `GROUP_OF_CHILD` (in `stagger.ts`) answers on its own
|
|
3336
|
+
* terms. Without it, removing one item from a staggered group left every later sibling holding
|
|
3337
|
+
* the rank an index computed for a group one element larger — the stale-after-*edit* half of the
|
|
3338
|
+
* same defect `3d57ff7` already closed for an attribute change, left open for a removal.
|
|
3339
|
+
*
|
|
3340
|
+
* @complexity O(s) time in the removed subtree's element count; O(1) per candidate via the
|
|
3341
|
+
* `liveElements` Set lookup, plus stagger re-ranking bounded by the surviving groups' own sizes.
|
|
3342
|
+
* @overallScore 100
|
|
3343
|
+
*/
|
|
3344
|
+
releaseTree(node) {
|
|
3345
|
+
const candidates = [node, ...node.querySelectorAll("*")];
|
|
3346
|
+
for (const el of candidates) {
|
|
3347
|
+
if (this.liveElements.has(el)) this.release(el);
|
|
3348
|
+
}
|
|
3349
|
+
restageAfterRemoval(candidates, this.reporter);
|
|
3350
|
+
restageAfterTreeRelease(this.port, candidates);
|
|
3351
|
+
}
|
|
3352
|
+
destroy() {
|
|
3353
|
+
this.domWatcher?.destroy();
|
|
3354
|
+
releaseStagger(this.root);
|
|
3355
|
+
for (const el of [...this.liveElements]) this.release(el);
|
|
3356
|
+
this.gateWatcher?.destroy();
|
|
3357
|
+
this.binder.destroy();
|
|
3358
|
+
this.scheduler.destroy();
|
|
3359
|
+
this.started = false;
|
|
3360
|
+
}
|
|
3361
|
+
/**
|
|
3362
|
+
* Start watching for DOM insertions, removals, and attribute changes.
|
|
3363
|
+
*
|
|
3364
|
+
* Attribute changes recompile in place; insertions scan; removals tear down so listeners and
|
|
3365
|
+
* observers do not outlive their elements.
|
|
3366
|
+
*
|
|
3367
|
+
* @complexity O(1) time and space to build and start; the watcher's own callback runs O(n) time
|
|
3368
|
+
* in the nodes one mutation record carries.
|
|
3369
|
+
* @overallScore 100
|
|
3370
|
+
*/
|
|
3371
|
+
watch() {
|
|
3372
|
+
this.domWatcher ??= createDomWatcher({
|
|
3373
|
+
root: this.root,
|
|
3374
|
+
onElementAdded: (el) => {
|
|
3375
|
+
this.scan(el);
|
|
3376
|
+
adoptLateMatches(this.port, el);
|
|
3377
|
+
},
|
|
3378
|
+
onElementRemoved: (el) => this.releaseTree(el),
|
|
3379
|
+
onAttributeChanged: (el) => {
|
|
3380
|
+
this.process(el);
|
|
3381
|
+
restageAround(el, this.reporter);
|
|
3382
|
+
}
|
|
3383
|
+
});
|
|
3384
|
+
this.domWatcher.watch();
|
|
3385
|
+
}
|
|
3386
|
+
};
|
|
3387
|
+
function createAnimator(options = {}) {
|
|
3388
|
+
return new Animator(options);
|
|
3389
|
+
}
|
|
3390
|
+
function resolveCollaborators(options) {
|
|
3391
|
+
const root = options.root ?? globalThis.document;
|
|
3392
|
+
const capabilities = options.capabilities ?? detect();
|
|
3393
|
+
const reporter = options.reporter ?? silentReporter();
|
|
3394
|
+
const scheduler = options.scheduler ?? createScrollScheduler();
|
|
3395
|
+
const rootResolver = options.rootResolver ?? defaultRootResolver(root);
|
|
3396
|
+
const respectReducedMotion = (options.reducedMotion ?? "respect") === "respect";
|
|
3397
|
+
return {
|
|
3398
|
+
registry: options.registry ?? new Registry(),
|
|
3399
|
+
capabilities,
|
|
3400
|
+
root,
|
|
3401
|
+
reporter,
|
|
3402
|
+
binder: options.binder ?? createActivationBinder({ reporter }),
|
|
3403
|
+
scheduler,
|
|
3404
|
+
rootResolver,
|
|
3405
|
+
jsEffectPreparer: resolveJsEffectPreparer(options.jsEffectPreparer, {
|
|
3406
|
+
scheduler,
|
|
3407
|
+
rootResolver,
|
|
3408
|
+
capabilities,
|
|
3409
|
+
reporter,
|
|
3410
|
+
respectReducedMotion
|
|
3411
|
+
}),
|
|
3412
|
+
// Not defaulted here (unlike the other collaborators above): building the real watcher needs
|
|
3413
|
+
// `this.scan`/`this.process`/`this.releaseTree`, which don't exist yet inside this free
|
|
3414
|
+
// function. `Animator.watch()` builds it lazily instead, so nothing observes — and no
|
|
3415
|
+
// `MutationObserver` is ever constructed — unless `shouldObserve` is true and `start()` runs.
|
|
3416
|
+
domWatcher: options.domWatcher,
|
|
3417
|
+
respectReducedMotion,
|
|
3418
|
+
shouldObserve: options.observe ?? false
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
function resolveJsEffectPreparer(provided, deps) {
|
|
3422
|
+
return provided ?? createJsEffectPreparer(deps);
|
|
3423
|
+
}
|
|
3424
|
+
function defaultRootResolver(root) {
|
|
3425
|
+
const doc = isElementNode(root) ? root.ownerDocument : root;
|
|
3426
|
+
const win = doc?.defaultView ?? globalThis;
|
|
3427
|
+
return createRootResolver({ win });
|
|
3428
|
+
}
|
|
3429
|
+
function isDirectional(instance) {
|
|
3430
|
+
return instance.play !== void 0 && instance.reverse !== void 0;
|
|
3431
|
+
}
|
|
3432
|
+
function runQuietly(cleanup) {
|
|
3433
|
+
try {
|
|
3434
|
+
cleanup();
|
|
3435
|
+
} catch {
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
export {
|
|
3440
|
+
detect,
|
|
3441
|
+
consoleReporter,
|
|
3442
|
+
silentReporter,
|
|
3443
|
+
collectingReporter,
|
|
3444
|
+
Animator,
|
|
3445
|
+
createAnimator
|
|
3446
|
+
};
|