kuinetic 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/esm/chunk-LWS4OSLX.mjs +730 -0
- package/dist/esm/chunk-QZIJ7WZI.mjs +3312 -0
- package/dist/esm/chunk-R3TGDJKA.mjs +1264 -0
- package/dist/esm/core/index.mjs +34 -0
- package/dist/esm/effects/index.mjs +27 -0
- package/dist/esm/index.mjs +52 -0
- package/dist/kuinetic.all.js +5291 -0
- package/dist/kuinetic.css +2338 -0
- package/dist/kuinetic.js +5281 -0
- package/dist/types/core/activation.d.ts +21 -0
- package/dist/types/core/animator.d.ts +216 -0
- package/dist/types/core/attrs.d.ts +15 -0
- package/dist/types/core/capabilities.d.ts +19 -0
- package/dist/types/core/channels.d.ts +25 -0
- package/dist/types/core/compile.d.ts +49 -0
- package/dist/types/core/dom-watcher.d.ts +35 -0
- package/dist/types/core/effect-context.d.ts +49 -0
- package/dist/types/core/element-config.d.ts +52 -0
- package/dist/types/core/flip.d.ts +80 -0
- package/dist/types/core/gesture.d.ts +99 -0
- package/dist/types/core/index.d.ts +14 -0
- package/dist/types/core/instances.d.ts +98 -0
- package/dist/types/core/js-effect-preparer.d.ts +46 -0
- package/dist/types/core/js-params.d.ts +124 -0
- package/dist/types/core/owned-styles.d.ts +44 -0
- package/dist/types/core/params.d.ts +65 -0
- package/dist/types/core/parse.d.ts +22 -0
- package/dist/types/core/path-morph.d.ts +83 -0
- package/dist/types/core/play.d.ts +48 -0
- package/dist/types/core/registry.d.ts +38 -0
- package/dist/types/core/reporter.d.ts +35 -0
- package/dist/types/core/scroll-scheduler.d.ts +153 -0
- package/dist/types/core/spring.d.ts +83 -0
- package/dist/types/core/stagger.d.ts +19 -0
- package/dist/types/core/style-plan.d.ts +56 -0
- package/dist/types/core/types.d.ts +239 -0
- package/dist/types/effects/catalog/ambient.d.ts +18 -0
- package/dist/types/effects/catalog/core.d.ts +19 -0
- package/dist/types/effects/catalog/feedback.d.ts +21 -0
- package/dist/types/effects/catalog/index.d.ts +21 -0
- package/dist/types/effects/catalog/interaction-shared.d.ts +56 -0
- package/dist/types/effects/catalog/interaction.d.ts +22 -0
- package/dist/types/effects/catalog/media.d.ts +13 -0
- package/dist/types/effects/catalog/numbers-shared.d.ts +103 -0
- package/dist/types/effects/catalog/numbers.d.ts +31 -0
- package/dist/types/effects/catalog/shared.d.ts +3 -0
- package/dist/types/effects/catalog/text-shared.d.ts +180 -0
- package/dist/types/effects/catalog/text.d.ts +17 -0
- package/dist/types/effects/forms/index.d.ts +14 -0
- package/dist/types/effects/forms/primitives.d.ts +32 -0
- package/dist/types/effects/gestures/index.d.ts +19 -0
- package/dist/types/effects/gestures/primitives.d.ts +2 -0
- package/dist/types/effects/index.d.ts +19 -0
- package/dist/types/effects/layout/index.d.ts +12 -0
- package/dist/types/effects/layout/presets.d.ts +9 -0
- package/dist/types/effects/layout/primitives.d.ts +2 -0
- package/dist/types/effects/navigation/index.d.ts +30 -0
- package/dist/types/effects/scroll-mechanics/index.d.ts +14 -0
- package/dist/types/effects/scroll-mechanics/presets.d.ts +6 -0
- package/dist/types/effects/scroll-mechanics/primitives.d.ts +2 -0
- package/dist/types/effects/scroll-mechanics/tracker.d.ts +55 -0
- package/dist/types/effects/shared.d.ts +26 -0
- package/dist/types/effects/svg/index.d.ts +13 -0
- package/dist/types/effects/three-d/index.d.ts +20 -0
- package/dist/types/index.d.ts +16 -0
- package/package.json +78 -0
|
@@ -0,0 +1,1264 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Registry,
|
|
3
|
+
createAttributeLedger,
|
|
4
|
+
createCssInstance,
|
|
5
|
+
createRootResolver,
|
|
6
|
+
createScrollScheduler,
|
|
7
|
+
createStyleLedger,
|
|
8
|
+
readEffectParams,
|
|
9
|
+
readEffectTiming,
|
|
10
|
+
resolveParams,
|
|
11
|
+
suggest,
|
|
12
|
+
timingProperty
|
|
13
|
+
} from "./chunk-LWS4OSLX.mjs";
|
|
14
|
+
|
|
15
|
+
// src/core/attrs.ts
|
|
16
|
+
var ATTR = {
|
|
17
|
+
/** Authored. The rich grammar. */
|
|
18
|
+
source: "data-kui",
|
|
19
|
+
/** Library-owned and unstable: normalized effect names, for CSS hooks and debugging. */
|
|
20
|
+
normalized: "data-kui-fx",
|
|
21
|
+
state: "data-kui-state",
|
|
22
|
+
on: "data-kui-on",
|
|
23
|
+
timeline: "data-kui-timeline",
|
|
24
|
+
threshold: "data-kui-threshold",
|
|
25
|
+
stagger: "data-kui-stagger",
|
|
26
|
+
cloak: "data-kui-cloak",
|
|
27
|
+
/** Reduced-motion policy, stamped from the primitive so the CSS layer can act on it. */
|
|
28
|
+
rm: "data-kui-rm"
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/core/element-config.ts
|
|
32
|
+
var ACTIVATIONS = /* @__PURE__ */ new Set(["load", "enter", "hover", "focus", "click", "manual"]);
|
|
33
|
+
var TIMELINES = /* @__PURE__ */ new Set(["time", "view", "scroll", "pointer"]);
|
|
34
|
+
function readAttributes(el) {
|
|
35
|
+
return {
|
|
36
|
+
source: el.getAttribute(ATTR.source) ?? "",
|
|
37
|
+
on: el.getAttribute(ATTR.on),
|
|
38
|
+
timeline: el.getAttribute(ATTR.timeline),
|
|
39
|
+
threshold: el.getAttribute(ATTR.threshold)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function resolveConfig(attributes, parsed) {
|
|
43
|
+
const rawTimeline = parsed.timeline ?? attributes.timeline ?? "time";
|
|
44
|
+
const [head = "time", ...rest] = rawTimeline.trim().split(/\s+/);
|
|
45
|
+
const authored = parsed.activation ?? readActivation(attributes.on);
|
|
46
|
+
return {
|
|
47
|
+
activation: authored ?? "enter",
|
|
48
|
+
activationAuthored: authored !== void 0,
|
|
49
|
+
timeline: TIMELINES.has(head) ? head : "time",
|
|
50
|
+
range: rest.join(" "),
|
|
51
|
+
threshold: parsed.threshold ?? attributes.threshold ?? "0%"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function readActivation(attribute) {
|
|
55
|
+
return attribute && ACTIVATIONS.has(attribute) ? attribute : void 0;
|
|
56
|
+
}
|
|
57
|
+
function toThresholdRatio(raw) {
|
|
58
|
+
const value = Number.parseFloat(raw);
|
|
59
|
+
if (Number.isNaN(value)) return 0;
|
|
60
|
+
const ratio = raw.includes("%") ? value / 100 : value;
|
|
61
|
+
return Math.min(1, Math.max(0, ratio));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/core/activation.ts
|
|
65
|
+
var NOOP = () => {
|
|
66
|
+
};
|
|
67
|
+
var ACTIVATION_EVENTS = {
|
|
68
|
+
load: [],
|
|
69
|
+
enter: [],
|
|
70
|
+
manual: [],
|
|
71
|
+
hover: ["pointerenter", "focusin"],
|
|
72
|
+
focus: ["focusin"],
|
|
73
|
+
click: ["click"]
|
|
74
|
+
};
|
|
75
|
+
function createActivationBinder(options = {}) {
|
|
76
|
+
const observers = /* @__PURE__ */ new Map();
|
|
77
|
+
const callbacks = /* @__PURE__ */ new WeakMap();
|
|
78
|
+
const createObserver = options.createObserver ?? defaultObserverFactory();
|
|
79
|
+
function observerFor(threshold) {
|
|
80
|
+
if (!createObserver) return void 0;
|
|
81
|
+
const ratio = toThresholdRatio(threshold);
|
|
82
|
+
const key = String(ratio);
|
|
83
|
+
const existing = observers.get(key);
|
|
84
|
+
if (existing) return { key, shared: existing };
|
|
85
|
+
const observer = createObserver(
|
|
86
|
+
(entries) => {
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (!entry.isIntersecting) continue;
|
|
89
|
+
const binding = callbacks.get(entry.target);
|
|
90
|
+
binding?.activate();
|
|
91
|
+
binding?.release();
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{ threshold: ratio }
|
|
95
|
+
);
|
|
96
|
+
const shared = { observer, count: 0 };
|
|
97
|
+
observers.set(key, shared);
|
|
98
|
+
return { key, shared };
|
|
99
|
+
}
|
|
100
|
+
function bindObserved(el, threshold, onActivate) {
|
|
101
|
+
const binding = observerFor(threshold);
|
|
102
|
+
if (!binding) {
|
|
103
|
+
onActivate();
|
|
104
|
+
return NOOP;
|
|
105
|
+
}
|
|
106
|
+
const { key, shared } = binding;
|
|
107
|
+
let active = true;
|
|
108
|
+
const release = () => {
|
|
109
|
+
if (!active) return;
|
|
110
|
+
active = false;
|
|
111
|
+
callbacks.delete(el);
|
|
112
|
+
shared.observer.unobserve(el);
|
|
113
|
+
shared.count--;
|
|
114
|
+
if (shared.count > 0) return;
|
|
115
|
+
shared.observer.disconnect();
|
|
116
|
+
observers.delete(key);
|
|
117
|
+
};
|
|
118
|
+
callbacks.set(el, { activate: onActivate, release });
|
|
119
|
+
shared.count++;
|
|
120
|
+
shared.observer.observe(el);
|
|
121
|
+
return release;
|
|
122
|
+
}
|
|
123
|
+
function bindEvents(el, activation, onActivate) {
|
|
124
|
+
const types = ACTIVATION_EVENTS[activation];
|
|
125
|
+
if (types.length === 0) return NOOP;
|
|
126
|
+
const handler = () => onActivate();
|
|
127
|
+
for (const type of types) el.addEventListener(type, handler, { passive: true });
|
|
128
|
+
return () => {
|
|
129
|
+
for (const type of types) el.removeEventListener(type, handler);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
bind(el, activation, threshold, onActivate) {
|
|
134
|
+
if (activation === "load") {
|
|
135
|
+
onActivate();
|
|
136
|
+
return NOOP;
|
|
137
|
+
}
|
|
138
|
+
if (activation === "enter") return bindObserved(el, threshold, onActivate);
|
|
139
|
+
return bindEvents(el, activation, onActivate);
|
|
140
|
+
},
|
|
141
|
+
destroy() {
|
|
142
|
+
for (const { observer } of observers.values()) observer.disconnect();
|
|
143
|
+
observers.clear();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function defaultObserverFactory() {
|
|
148
|
+
if (typeof IntersectionObserver === "undefined") return void 0;
|
|
149
|
+
return (callback, init) => new IntersectionObserver(callback, init);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/core/capabilities.ts
|
|
153
|
+
function supports(property, value) {
|
|
154
|
+
if (typeof CSS === "undefined" || typeof CSS.supports !== "function") return false;
|
|
155
|
+
try {
|
|
156
|
+
return CSS.supports(property, value);
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
var cached;
|
|
162
|
+
function detect(force = false) {
|
|
163
|
+
if (cached && !force) return cached;
|
|
164
|
+
cached = {
|
|
165
|
+
viewTimeline: supports("animation-timeline", "view()"),
|
|
166
|
+
scrollTimeline: supports("animation-timeline", "scroll()"),
|
|
167
|
+
animationRange: supports("animation-range", "entry 0% cover 30%"),
|
|
168
|
+
// `translate`/`rotate`/`scale` as independent properties is what makes the channel model
|
|
169
|
+
// possible at all — under the `transform` shorthand every effect would collide.
|
|
170
|
+
individualTransforms: supports("translate", "0 10px") && supports("scale", "1.1"),
|
|
171
|
+
scrollTimelineName: supports("scroll-timeline-name", "--x"),
|
|
172
|
+
viewTransitions: typeof document !== "undefined" && "startViewTransition" in document,
|
|
173
|
+
intersectionObserver: typeof IntersectionObserver !== "undefined",
|
|
174
|
+
reducedMotion: typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
175
|
+
};
|
|
176
|
+
return cached;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/core/channels.ts
|
|
180
|
+
function findConflicts(claims) {
|
|
181
|
+
const conflicts = [];
|
|
182
|
+
const seen = /* @__PURE__ */ new Map();
|
|
183
|
+
for (const claim of claims) {
|
|
184
|
+
for (const channel of claim.channels) {
|
|
185
|
+
const owner = seen.get(channel);
|
|
186
|
+
if (owner === void 0) {
|
|
187
|
+
seen.set(channel, claim.name);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
conflicts.push({ channel, effects: [owner, claim.name] });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return conflicts;
|
|
194
|
+
}
|
|
195
|
+
function describeConflicts(conflicts) {
|
|
196
|
+
return conflicts.map((c) => `"${c.effects[0]}" and "${c.effects[1]}" both animate ${c.channel}`).join("; ");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/core/compile.ts
|
|
200
|
+
var NATIVE_EASINGS = /* @__PURE__ */ new Set([
|
|
201
|
+
"linear",
|
|
202
|
+
"ease",
|
|
203
|
+
"ease-in",
|
|
204
|
+
"ease-out",
|
|
205
|
+
"ease-in-out",
|
|
206
|
+
"step-start",
|
|
207
|
+
"step-end"
|
|
208
|
+
]);
|
|
209
|
+
var RM_RANK = { shorten: 0, crossfade: 1, disable: 2 };
|
|
210
|
+
function compile(parsed, registry, timeline) {
|
|
211
|
+
const warnings = [...parsed.warnings];
|
|
212
|
+
const { entries, unknown } = resolveEntries(parsed.specs, registry, warnings);
|
|
213
|
+
if (entries.length === 0) {
|
|
214
|
+
return emptyPlan(unknown, warnings);
|
|
215
|
+
}
|
|
216
|
+
const composed = resolveComposition(entries, registry, warnings);
|
|
217
|
+
return buildPlan(composed, timeline, unknown, warnings);
|
|
218
|
+
}
|
|
219
|
+
function emptyPlan(unknown, warnings) {
|
|
220
|
+
return {
|
|
221
|
+
fxNames: [],
|
|
222
|
+
vars: {},
|
|
223
|
+
declarations: {},
|
|
224
|
+
jsEffects: [],
|
|
225
|
+
unknown,
|
|
226
|
+
reducedMotion: "shorten",
|
|
227
|
+
supportedActivations: [],
|
|
228
|
+
supportedTimelines: [],
|
|
229
|
+
channels: [],
|
|
230
|
+
warnings
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function resolveEntries(specs, registry, warnings) {
|
|
234
|
+
const entries = [];
|
|
235
|
+
const unknown = [];
|
|
236
|
+
for (const spec of specs) {
|
|
237
|
+
const resolved = registry.resolve(spec.name);
|
|
238
|
+
if (resolved) {
|
|
239
|
+
entries.push({ spec, resolved });
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
unknown.push(spec.name);
|
|
243
|
+
const hint = suggest(spec.name, registry.names());
|
|
244
|
+
const suffix = hint ? ` \u2014 did you mean "${hint}"?` : "";
|
|
245
|
+
warnings.push(`unknown effect "${spec.name}"${suffix}`);
|
|
246
|
+
}
|
|
247
|
+
return { entries, unknown };
|
|
248
|
+
}
|
|
249
|
+
function resolveComposition(entries, registry, warnings) {
|
|
250
|
+
if (entries.length <= 1) return entries;
|
|
251
|
+
const conflicts = findConflicts(
|
|
252
|
+
entries.map((e) => ({ name: e.spec.name, channels: e.resolved.primitive.channels }))
|
|
253
|
+
);
|
|
254
|
+
if (conflicts.length === 0) return entries;
|
|
255
|
+
const combo = registry.findCombo(entries.map((e) => e.spec.name));
|
|
256
|
+
const remedy = combo ? `Use the "${combo.preset.name}" effect instead.` : "Apply them to nested elements, or register a combined effect.";
|
|
257
|
+
warnings.push(`cannot compose: ${describeConflicts(conflicts)}. ${remedy}`);
|
|
258
|
+
return [entries[0]];
|
|
259
|
+
}
|
|
260
|
+
function buildPlan(entries, timeline, unknown, warnings) {
|
|
261
|
+
const plan = emptyPlan(unknown, warnings);
|
|
262
|
+
const tracks = {
|
|
263
|
+
names: [],
|
|
264
|
+
durations: [],
|
|
265
|
+
delays: [],
|
|
266
|
+
easings: [],
|
|
267
|
+
iterationCounts: []
|
|
268
|
+
};
|
|
269
|
+
const channels = /* @__PURE__ */ new Set();
|
|
270
|
+
let activations;
|
|
271
|
+
let timelines;
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
const { preset, primitive } = entry.resolved;
|
|
274
|
+
plan.fxNames.push(preset.name);
|
|
275
|
+
plan.reducedMotion = strictestPolicy(plan.reducedMotion, primitive.reducedMotion);
|
|
276
|
+
plan.defaultActivation ??= primitive.defaultActivation;
|
|
277
|
+
activations = intersect(activations, primitive.supportedActivations);
|
|
278
|
+
timelines = intersect(timelines, primitive.supportedTimelines);
|
|
279
|
+
for (const channel of primitive.channels) channels.add(channel);
|
|
280
|
+
warnUnsupportedTimeline(preset.name, primitive.supportedTimelines, timeline, warnings);
|
|
281
|
+
Object.assign(
|
|
282
|
+
plan.vars,
|
|
283
|
+
resolveParams(entry.spec.params, primitive.parameters, (m) => warnings.push(m))
|
|
284
|
+
);
|
|
285
|
+
if (primitive.renderer === "css-keyframes") pushTrack(tracks, entry);
|
|
286
|
+
else plan.jsEffects.push(entry);
|
|
287
|
+
}
|
|
288
|
+
Object.assign(plan.declarations, declarationsFor(tracks));
|
|
289
|
+
plan.supportedActivations = activations;
|
|
290
|
+
plan.supportedTimelines = timelines;
|
|
291
|
+
plan.channels = [...channels];
|
|
292
|
+
return plan;
|
|
293
|
+
}
|
|
294
|
+
function intersect(accumulated, supported) {
|
|
295
|
+
if (!accumulated) return [...supported];
|
|
296
|
+
return accumulated.filter((value) => supported.includes(value));
|
|
297
|
+
}
|
|
298
|
+
function iterationCountProperty(presetName) {
|
|
299
|
+
return `--kui-fx-${presetName}-iterations`;
|
|
300
|
+
}
|
|
301
|
+
function pushTrack(tracks, entry) {
|
|
302
|
+
const { spec, resolved } = entry;
|
|
303
|
+
const id = resolved.primitive.id;
|
|
304
|
+
tracks.names.push(resolved.preset.keyframes ?? `kui-${resolved.preset.name}`);
|
|
305
|
+
tracks.durations.push(spec.duration ?? `var(${timingProperty(id, "duration")}, 600ms)`);
|
|
306
|
+
tracks.delays.push(staggerDelay(spec.delay, id));
|
|
307
|
+
tracks.easings.push(easingValue(spec.easing, id));
|
|
308
|
+
tracks.iterationCounts.push(`var(${iterationCountProperty(resolved.preset.name)}, 1)`);
|
|
309
|
+
}
|
|
310
|
+
function declarationsFor(tracks) {
|
|
311
|
+
if (tracks.names.length === 0) return {};
|
|
312
|
+
return {
|
|
313
|
+
"animation-name": tracks.names.join(", "),
|
|
314
|
+
"animation-duration": tracks.durations.join(", "),
|
|
315
|
+
"animation-delay": tracks.delays.join(", "),
|
|
316
|
+
"animation-timing-function": tracks.easings.join(", "),
|
|
317
|
+
"animation-iteration-count": tracks.iterationCounts.join(", "),
|
|
318
|
+
"animation-fill-mode": tracks.names.map(() => "both").join(", ")
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function strictestPolicy(a, b) {
|
|
322
|
+
return RM_RANK[b] > RM_RANK[a] ? b : a;
|
|
323
|
+
}
|
|
324
|
+
function warnUnsupportedTimeline(name, supported, timeline, warnings) {
|
|
325
|
+
if (supported.includes(timeline)) return;
|
|
326
|
+
warnings.push(
|
|
327
|
+
`"${name}" does not support timeline "${timeline}" (supports: ${supported.join(", ")})`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
function staggerDelay(delay, primitiveId) {
|
|
331
|
+
const base = delay ?? `var(${timingProperty(primitiveId, "delay")}, 0ms)`;
|
|
332
|
+
return `calc(${base} + var(--kui-i, 0) * var(--kui-stagger, 0ms))`;
|
|
333
|
+
}
|
|
334
|
+
function easingValue(easing, primitiveId) {
|
|
335
|
+
if (!easing) return `var(${timingProperty(primitiveId, "ease")}, ease-out)`;
|
|
336
|
+
if (NATIVE_EASINGS.has(easing)) return easing;
|
|
337
|
+
if (easing.includes("(")) return easing;
|
|
338
|
+
return `var(--kui-ease-${easing}, ease-out)`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/core/dom-watcher.ts
|
|
342
|
+
var WORK_BUDGET = 100;
|
|
343
|
+
function createDomWatcher(options) {
|
|
344
|
+
const { root, onElementAdded, onElementRemoved, onAttributeChanged } = options;
|
|
345
|
+
const schedule = options.schedule ?? scheduleFrame;
|
|
346
|
+
const createObserver = options.createObserver ?? defaultObserverFactory2();
|
|
347
|
+
const added = /* @__PURE__ */ new Set();
|
|
348
|
+
const removed = /* @__PURE__ */ new Set();
|
|
349
|
+
const changed = /* @__PURE__ */ new Set();
|
|
350
|
+
let observer;
|
|
351
|
+
let scheduled = false;
|
|
352
|
+
let destroyed = false;
|
|
353
|
+
function collect(record) {
|
|
354
|
+
if (record.type === "attributes") {
|
|
355
|
+
const target = asElement(record.target);
|
|
356
|
+
if (target) changed.add(target);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
for (const node of record.addedNodes) queueRoot(added, asElement(node));
|
|
360
|
+
for (const node of record.removedNodes) queueRoot(removed, asElement(node));
|
|
361
|
+
}
|
|
362
|
+
function queueWork(records) {
|
|
363
|
+
for (const record of records) collect(record);
|
|
364
|
+
if (scheduled) return;
|
|
365
|
+
scheduled = true;
|
|
366
|
+
schedule(flush);
|
|
367
|
+
}
|
|
368
|
+
function flush() {
|
|
369
|
+
scheduled = false;
|
|
370
|
+
if (destroyed) return;
|
|
371
|
+
let remaining = WORK_BUDGET;
|
|
372
|
+
remaining = drain(removed, onElementRemoved, remaining);
|
|
373
|
+
remaining = drain(added, onElementAdded, remaining);
|
|
374
|
+
drain(changed, onAttributeChanged, remaining);
|
|
375
|
+
if (added.size + removed.size + changed.size > 0) {
|
|
376
|
+
scheduled = true;
|
|
377
|
+
schedule(flush);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
watch() {
|
|
382
|
+
if (!createObserver) return;
|
|
383
|
+
destroyed = false;
|
|
384
|
+
observer = createObserver(queueWork);
|
|
385
|
+
observer.observe(root, {
|
|
386
|
+
subtree: true,
|
|
387
|
+
childList: true,
|
|
388
|
+
attributes: true,
|
|
389
|
+
attributeFilter: [ATTR.source, ATTR.on, ATTR.timeline, ATTR.threshold]
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
destroy() {
|
|
393
|
+
destroyed = true;
|
|
394
|
+
observer?.disconnect();
|
|
395
|
+
added.clear();
|
|
396
|
+
removed.clear();
|
|
397
|
+
changed.clear();
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function queueRoot(roots, candidate) {
|
|
402
|
+
if (!candidate) return;
|
|
403
|
+
for (const root of roots) {
|
|
404
|
+
if (root.contains(candidate)) return;
|
|
405
|
+
if (candidate.contains(root)) roots.delete(root);
|
|
406
|
+
}
|
|
407
|
+
roots.add(candidate);
|
|
408
|
+
}
|
|
409
|
+
function drain(roots, callback, budget) {
|
|
410
|
+
for (const root of roots) {
|
|
411
|
+
if (budget === 0) break;
|
|
412
|
+
roots.delete(root);
|
|
413
|
+
callback(root);
|
|
414
|
+
budget--;
|
|
415
|
+
}
|
|
416
|
+
return budget;
|
|
417
|
+
}
|
|
418
|
+
function asElement(node) {
|
|
419
|
+
return node.nodeType === 1 ? node : null;
|
|
420
|
+
}
|
|
421
|
+
function scheduleFrame(callback) {
|
|
422
|
+
const requestFrame = globalThis.requestAnimationFrame;
|
|
423
|
+
if (typeof requestFrame === "function") requestFrame(() => callback());
|
|
424
|
+
else queueMicrotask(callback);
|
|
425
|
+
}
|
|
426
|
+
function defaultObserverFactory2() {
|
|
427
|
+
if (typeof MutationObserver === "undefined") return void 0;
|
|
428
|
+
return (callback) => new MutationObserver(callback);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/core/js-effect-preparer.ts
|
|
432
|
+
function createJsEffectPreparer(options) {
|
|
433
|
+
const { scheduler, rootResolver, capabilities, reporter, respectReducedMotion } = options;
|
|
434
|
+
function contextFor(el, signal, ledger) {
|
|
435
|
+
const doc = el.ownerDocument;
|
|
436
|
+
return {
|
|
437
|
+
doc,
|
|
438
|
+
win: doc.defaultView ?? globalThis,
|
|
439
|
+
scheduler,
|
|
440
|
+
rootFor: rootResolver,
|
|
441
|
+
capabilities,
|
|
442
|
+
invalidate: () => scheduler.invalidate(),
|
|
443
|
+
warn: (message) => reporter.warn(message, el),
|
|
444
|
+
reducedMotion: respectReducedMotion && capabilities.reducedMotion,
|
|
445
|
+
signal,
|
|
446
|
+
style: ledger
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
prepare({ el, plan, signal, ledger }) {
|
|
451
|
+
const instances = [];
|
|
452
|
+
if (plan.jsEffects.length === 0) return instances;
|
|
453
|
+
const ctx = contextFor(el, signal, ledger);
|
|
454
|
+
for (const { spec, resolved } of plan.jsEffects) {
|
|
455
|
+
const prepare = resolved.primitive.prepare;
|
|
456
|
+
if (!prepare) continue;
|
|
457
|
+
const warn = (message) => reporter.warn(message, el);
|
|
458
|
+
const params = readEffectParams(
|
|
459
|
+
{ ...resolved.preset.params, ...spec.params },
|
|
460
|
+
resolved.primitive.parameters,
|
|
461
|
+
warn,
|
|
462
|
+
readEffectTiming(spec, warn)
|
|
463
|
+
);
|
|
464
|
+
try {
|
|
465
|
+
instances.push(prepare(el, params, ctx));
|
|
466
|
+
} catch (error) {
|
|
467
|
+
reporter.warn(`"${spec.name}" failed to initialise: ${String(error)}`, el);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return instances;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/core/parse.ts
|
|
476
|
+
var TIME_RE = /^-?(?:\d+(?:\.\d+)?|\.\d+)(?:ms|s)$/;
|
|
477
|
+
var EASING_FUNCTIONS = ["cubic-bezier(", "steps(", "linear("];
|
|
478
|
+
var ACTIVATIONS2 = /* @__PURE__ */ new Set([
|
|
479
|
+
"load",
|
|
480
|
+
"enter",
|
|
481
|
+
"hover",
|
|
482
|
+
"focus",
|
|
483
|
+
"click",
|
|
484
|
+
"manual"
|
|
485
|
+
]);
|
|
486
|
+
var EASING_KEYWORDS = /* @__PURE__ */ new Set([
|
|
487
|
+
"linear",
|
|
488
|
+
"ease",
|
|
489
|
+
"ease-in",
|
|
490
|
+
"ease-out",
|
|
491
|
+
"ease-in-out",
|
|
492
|
+
"step-start",
|
|
493
|
+
"step-end",
|
|
494
|
+
"expo-in",
|
|
495
|
+
"expo-out",
|
|
496
|
+
"expo-in-out",
|
|
497
|
+
"back-in",
|
|
498
|
+
"back-out",
|
|
499
|
+
"back-in-out",
|
|
500
|
+
"quart-out",
|
|
501
|
+
"circ-out",
|
|
502
|
+
"spring"
|
|
503
|
+
]);
|
|
504
|
+
function splitTopLevel(input, delimiter, warnings = []) {
|
|
505
|
+
const parts = [];
|
|
506
|
+
const scanner = { depth: 0, quote: null, escaped: false };
|
|
507
|
+
let buffer = "";
|
|
508
|
+
for (const char of input) {
|
|
509
|
+
if (isSeparator(char, delimiter, scanner)) {
|
|
510
|
+
if (buffer.trim()) parts.push(buffer.trim());
|
|
511
|
+
buffer = "";
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
buffer += char;
|
|
515
|
+
}
|
|
516
|
+
if (buffer.trim()) parts.push(buffer.trim());
|
|
517
|
+
if (scanner.quote) warnings.push(`unterminated ${scanner.quote} quote in "${input}"`);
|
|
518
|
+
else if (scanner.depth > 0) warnings.push(`unclosed "(" in "${input}"`);
|
|
519
|
+
return parts;
|
|
520
|
+
}
|
|
521
|
+
function isSeparator(char, delimiter, scanner) {
|
|
522
|
+
if (scanner.quote) return advanceQuote(char, scanner);
|
|
523
|
+
if (char === '"' || char === "'") {
|
|
524
|
+
scanner.quote = char;
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
if (char === "(") scanner.depth++;
|
|
528
|
+
else if (char === ")") scanner.depth = Math.max(0, scanner.depth - 1);
|
|
529
|
+
if (scanner.depth !== 0) return false;
|
|
530
|
+
return delimiter === " " ? /\s/.test(char) : char === delimiter;
|
|
531
|
+
}
|
|
532
|
+
function advanceQuote(char, scanner) {
|
|
533
|
+
if (scanner.escaped) scanner.escaped = false;
|
|
534
|
+
else if (char === "\\") scanner.escaped = true;
|
|
535
|
+
else if (char === scanner.quote) scanner.quote = null;
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
function splitPair(token) {
|
|
539
|
+
let depth = 0;
|
|
540
|
+
for (let i = 0; i < token.length; i++) {
|
|
541
|
+
const char = token[i];
|
|
542
|
+
if (char === "(") depth++;
|
|
543
|
+
else if (char === ")") depth = Math.max(0, depth - 1);
|
|
544
|
+
else if (char === ":" && depth === 0) {
|
|
545
|
+
const key = token.slice(0, i).trim();
|
|
546
|
+
const value = unquote(token.slice(i + 1).trim());
|
|
547
|
+
return key && value ? [key, value] : null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
function unquote(value) {
|
|
553
|
+
const first = value[0];
|
|
554
|
+
if ((first === '"' || first === "'") && value.endsWith(first) && value.length > 1) {
|
|
555
|
+
return value.slice(1, -1).replaceAll(`\\${first}`, first);
|
|
556
|
+
}
|
|
557
|
+
return value;
|
|
558
|
+
}
|
|
559
|
+
function classify(token) {
|
|
560
|
+
const pair = splitPair(token);
|
|
561
|
+
if (pair) return { kind: "pair", key: pair[0], value: pair[1] };
|
|
562
|
+
if (TIME_RE.test(token)) return { kind: "time", value: token };
|
|
563
|
+
if (isEasing(token)) return { kind: "easing", value: token };
|
|
564
|
+
return { kind: "unknown", value: token };
|
|
565
|
+
}
|
|
566
|
+
function isEasing(token) {
|
|
567
|
+
if (EASING_KEYWORDS.has(token)) return true;
|
|
568
|
+
return EASING_FUNCTIONS.some((fn) => token.startsWith(fn) && token.endsWith(")"));
|
|
569
|
+
}
|
|
570
|
+
function parse(input) {
|
|
571
|
+
const result = { specs: [], warnings: [] };
|
|
572
|
+
for (const segment of splitTopLevel(input ?? "", ",", result.warnings)) {
|
|
573
|
+
const spec = parseSegment(segment, result);
|
|
574
|
+
if (spec) result.specs.push(spec);
|
|
575
|
+
}
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
function parseSegment(segment, result) {
|
|
579
|
+
const tokens = splitTopLevel(segment, " ", result.warnings);
|
|
580
|
+
const name = tokens.shift();
|
|
581
|
+
if (splitPair(name)) {
|
|
582
|
+
result.warnings.push(`effect name expected, got "${name}"`);
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
const spec = { name, params: {} };
|
|
586
|
+
let timeCount = 0;
|
|
587
|
+
for (const raw of tokens) {
|
|
588
|
+
const token = classify(raw);
|
|
589
|
+
if (token.kind === "time") timeCount = applyTime(spec, token.value, timeCount, result.warnings);
|
|
590
|
+
else applyToken(token, spec, segment, result);
|
|
591
|
+
}
|
|
592
|
+
return spec;
|
|
593
|
+
}
|
|
594
|
+
function applyTime(spec, value, seen, warnings) {
|
|
595
|
+
if (seen === 0) spec.duration = value;
|
|
596
|
+
else if (seen === 1) spec.delay = value;
|
|
597
|
+
else warnings.push(`third time value "${value}" ignored (expected duration then delay)`);
|
|
598
|
+
return seen + 1;
|
|
599
|
+
}
|
|
600
|
+
function applyToken(token, spec, segment, result) {
|
|
601
|
+
if (token.kind === "easing") {
|
|
602
|
+
if (spec.easing) result.warnings.push(`duplicate easing "${token.value}" in "${segment}"`);
|
|
603
|
+
spec.easing = token.value;
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (token.kind === "unknown") {
|
|
607
|
+
result.warnings.push(
|
|
608
|
+
`unrecognised token "${token.value}" in "${segment}" \u2014 expected [duration] [delay] [easing] or key:value`
|
|
609
|
+
);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (Object.hasOwn(HOISTS, token.key)) {
|
|
613
|
+
HOISTS[token.key](result, token.value);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (token.key in spec.params) {
|
|
617
|
+
result.warnings.push(`duplicate parameter "${token.key}" in "${segment}"`);
|
|
618
|
+
}
|
|
619
|
+
spec.params[token.key] = token.value;
|
|
620
|
+
}
|
|
621
|
+
var HOISTS = {
|
|
622
|
+
on(result, value) {
|
|
623
|
+
if (!ACTIVATIONS2.has(value)) {
|
|
624
|
+
result.warnings.push(`unknown activation "${value}"`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
assignOnce(result, "activation", value, "activations");
|
|
628
|
+
},
|
|
629
|
+
timeline(result, value) {
|
|
630
|
+
assignOnce(result, "timeline", value, "timelines");
|
|
631
|
+
},
|
|
632
|
+
threshold(result, value) {
|
|
633
|
+
assignOnce(result, "threshold", value, "thresholds");
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
function assignOnce(result, key, value, label) {
|
|
637
|
+
const current = result[key];
|
|
638
|
+
if (current === void 0) {
|
|
639
|
+
result[key] = value;
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (current !== value) {
|
|
643
|
+
result.warnings.push(`conflicting ${label} "${String(current)}" and "${String(value)}"`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/core/play.ts
|
|
648
|
+
function resolveTargets(target, root) {
|
|
649
|
+
if (typeof target === "string") return [...root.querySelectorAll(target)];
|
|
650
|
+
if (target instanceof Element) return [target];
|
|
651
|
+
return [...target];
|
|
652
|
+
}
|
|
653
|
+
function time(value) {
|
|
654
|
+
if (value === void 0) return void 0;
|
|
655
|
+
return typeof value === "number" ? `${value}ms` : value;
|
|
656
|
+
}
|
|
657
|
+
var STRUCTURAL_RE = /[\s,()"']/;
|
|
658
|
+
function quoteIfNeeded(value) {
|
|
659
|
+
if (!STRUCTURAL_RE.test(value)) return value;
|
|
660
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
661
|
+
return `"${escaped}"`;
|
|
662
|
+
}
|
|
663
|
+
function hasTopLevelColon(value) {
|
|
664
|
+
let depth = 0;
|
|
665
|
+
for (const char of value) {
|
|
666
|
+
if (char === "(") depth++;
|
|
667
|
+
else if (char === ")") depth = Math.max(0, depth - 1);
|
|
668
|
+
else if (char === ":" && depth === 0) return true;
|
|
669
|
+
}
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
function assertBareToken(label, value) {
|
|
673
|
+
const warnings = [];
|
|
674
|
+
const bySpace = splitTopLevel(value, " ", warnings);
|
|
675
|
+
const byComma = splitTopLevel(value, ",", warnings);
|
|
676
|
+
const isSingleToken = bySpace.length === 1 && bySpace[0] === value;
|
|
677
|
+
const safe = warnings.length === 0 && isSingleToken && byComma.length === 1 && !hasTopLevelColon(value);
|
|
678
|
+
if (!safe) {
|
|
679
|
+
throw new Error(
|
|
680
|
+
`play(): ${label} "${value}" cannot be serialized \u2014 it contains a space, comma, colon, quote, or unbalanced parenthesis the parser cannot read back as one token`
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function toAttributeValue(effect, options = {}) {
|
|
685
|
+
assertBareToken("effect", effect);
|
|
686
|
+
const { duration, delay, ease, ...rest } = options;
|
|
687
|
+
delete rest.stagger;
|
|
688
|
+
const parts = [effect];
|
|
689
|
+
const resolvedDelay = time(delay);
|
|
690
|
+
const resolvedDuration = time(duration) ?? (resolvedDelay ? "0ms" : void 0);
|
|
691
|
+
if (resolvedDuration) {
|
|
692
|
+
assertBareToken("duration", resolvedDuration);
|
|
693
|
+
parts.push(resolvedDuration);
|
|
694
|
+
}
|
|
695
|
+
if (resolvedDelay) {
|
|
696
|
+
assertBareToken("delay", resolvedDelay);
|
|
697
|
+
parts.push(resolvedDelay);
|
|
698
|
+
}
|
|
699
|
+
if (ease) {
|
|
700
|
+
assertBareToken("easing", ease);
|
|
701
|
+
parts.push(ease);
|
|
702
|
+
}
|
|
703
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
704
|
+
if (value === void 0) continue;
|
|
705
|
+
assertBareToken("parameter name", key);
|
|
706
|
+
parts.push(`${key}:${quoteIfNeeded(String(value))}`);
|
|
707
|
+
}
|
|
708
|
+
return parts.join(" ");
|
|
709
|
+
}
|
|
710
|
+
function play(request, options = {}) {
|
|
711
|
+
const { animator, root, target, effect } = request;
|
|
712
|
+
const elements = resolveTargets(target, root);
|
|
713
|
+
const stagger = time(options.stagger);
|
|
714
|
+
const source = toAttributeValue(effect, options);
|
|
715
|
+
for (const [index, el] of elements.entries()) {
|
|
716
|
+
if (stagger) {
|
|
717
|
+
;
|
|
718
|
+
el.style.setProperty("--kui-stagger", stagger);
|
|
719
|
+
el.style.setProperty("--kui-i", String(index));
|
|
720
|
+
}
|
|
721
|
+
animator.reset(el);
|
|
722
|
+
if (!el.hasAttribute(ATTR.on)) el.setAttribute(ATTR.on, "manual");
|
|
723
|
+
el.setAttribute(ATTR.source, source);
|
|
724
|
+
animator.process(el);
|
|
725
|
+
animator.activate(el);
|
|
726
|
+
}
|
|
727
|
+
const instancesOf = (el) => animator.stateOf(el)?.instances ?? [];
|
|
728
|
+
const finished = Promise.all(
|
|
729
|
+
elements.flatMap((el) => instancesOf(el).map((instance) => instance.finished))
|
|
730
|
+
).then(() => void 0);
|
|
731
|
+
return {
|
|
732
|
+
elements,
|
|
733
|
+
finished,
|
|
734
|
+
cancel() {
|
|
735
|
+
for (const el of elements) for (const instance of instancesOf(el)) instance.cancel();
|
|
736
|
+
},
|
|
737
|
+
finish() {
|
|
738
|
+
for (const el of elements) for (const instance of instancesOf(el)) instance.finish();
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// src/core/reporter.ts
|
|
744
|
+
function consoleReporter() {
|
|
745
|
+
return {
|
|
746
|
+
warn(message, subject) {
|
|
747
|
+
if (subject === void 0) console.warn(`[kuinetic] ${message}`);
|
|
748
|
+
else console.warn(`[kuinetic] ${message}`, subject);
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function silentReporter() {
|
|
753
|
+
return { warn() {
|
|
754
|
+
} };
|
|
755
|
+
}
|
|
756
|
+
function collectingReporter() {
|
|
757
|
+
const messages = [];
|
|
758
|
+
return {
|
|
759
|
+
messages,
|
|
760
|
+
warn(message) {
|
|
761
|
+
messages.push(message);
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/core/stagger.ts
|
|
767
|
+
function indexStaggerGroup(group) {
|
|
768
|
+
const step = group.getAttribute(ATTR.stagger);
|
|
769
|
+
if (step) group.style.setProperty("--kui-stagger", step);
|
|
770
|
+
let index = 0;
|
|
771
|
+
for (const child of group.children) {
|
|
772
|
+
if (child.hasAttribute(ATTR.source)) {
|
|
773
|
+
;
|
|
774
|
+
child.style.setProperty("--kui-i", String(index));
|
|
775
|
+
index++;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function applyStagger(root) {
|
|
780
|
+
const selector = `[${ATTR.stagger}]`;
|
|
781
|
+
if (root instanceof Element && root.matches(selector)) indexStaggerGroup(root);
|
|
782
|
+
for (const group of root.querySelectorAll(selector)) indexStaggerGroup(group);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/core/style-plan.ts
|
|
786
|
+
function planStyles(input) {
|
|
787
|
+
const { plan, config, capabilities, respectReducedMotion } = input;
|
|
788
|
+
const reduce = respectReducedMotion && capabilities.reducedMotion;
|
|
789
|
+
const useNativeTimeline = supportsTimeline(config.timeline, capabilities) && plan.supportedTimelines.includes(config.timeline);
|
|
790
|
+
const properties = { ...plan.vars, ...plan.declarations };
|
|
791
|
+
Object.assign(properties, timelineProperties(config, capabilities, useNativeTimeline));
|
|
792
|
+
const hasCssAnimation = Object.keys(plan.declarations).length > 0;
|
|
793
|
+
const gate = resolveGate({
|
|
794
|
+
useNativeTimeline,
|
|
795
|
+
reduce,
|
|
796
|
+
activation: config.activation,
|
|
797
|
+
// JS effects are gated too. They emit no `animation` declaration, so only the play-state
|
|
798
|
+
// write is skipped — the activation itself still has to be bound, or `on:enter` and
|
|
799
|
+
// `on:click` would silently do nothing for every pinned, dragged, or morphing element.
|
|
800
|
+
hasWork: hasCssAnimation || plan.jsEffects.length > 0,
|
|
801
|
+
hasCssAnimation,
|
|
802
|
+
// A browser lacking standalone translate/rotate/scale support silently ignores any
|
|
803
|
+
// `@keyframes` step written in those properties — the animation never visibly completes. An
|
|
804
|
+
// effect deferred on that promise would sit paused (or, for an entrance reveal, invisible)
|
|
805
|
+
// forever, so it must reach its final state immediately instead, the same fail-open rule
|
|
806
|
+
// already applied under reduced motion.
|
|
807
|
+
unsupportedTransform: needsIndividualTransforms(plan.channels, capabilities)
|
|
808
|
+
});
|
|
809
|
+
if (gate === "deferred") properties["animation-play-state"] = "paused";
|
|
810
|
+
return {
|
|
811
|
+
properties,
|
|
812
|
+
attributes: {
|
|
813
|
+
[ATTR.normalized]: plan.fxNames.join(" "),
|
|
814
|
+
[ATTR.rm]: plan.reducedMotion,
|
|
815
|
+
[ATTR.state]: "ready"
|
|
816
|
+
},
|
|
817
|
+
gate,
|
|
818
|
+
activation: gate === "deferred" ? effectiveActivation(config) : null
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function timelineProperties(config, capabilities, useNativeTimeline) {
|
|
822
|
+
if (!useNativeTimeline) return {};
|
|
823
|
+
const properties = {
|
|
824
|
+
"animation-timeline": config.timeline === "scroll" ? "scroll()" : "view()"
|
|
825
|
+
};
|
|
826
|
+
if (capabilities.animationRange) {
|
|
827
|
+
properties["animation-range"] = config.range || "entry 0% cover 60%";
|
|
828
|
+
}
|
|
829
|
+
return properties;
|
|
830
|
+
}
|
|
831
|
+
function resolveGate(input) {
|
|
832
|
+
if (input.useNativeTimeline) return "native-timeline";
|
|
833
|
+
if (!input.hasWork) return "immediate";
|
|
834
|
+
if (input.reduce || input.activation === "load" || input.unsupportedTransform) return "immediate";
|
|
835
|
+
return "deferred";
|
|
836
|
+
}
|
|
837
|
+
function needsIndividualTransforms(channels, capabilities) {
|
|
838
|
+
if (capabilities.individualTransforms) return false;
|
|
839
|
+
return channels.some((c) => c === "translate" || c === "rotate" || c === "scale");
|
|
840
|
+
}
|
|
841
|
+
function supportsTimeline(timeline, capabilities) {
|
|
842
|
+
if (timeline === "view") return capabilities.viewTimeline;
|
|
843
|
+
if (timeline === "scroll") return capabilities.scrollTimeline;
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
function effectiveActivation(config) {
|
|
847
|
+
if (config.timeline !== "time" && config.activation === "manual") return "enter";
|
|
848
|
+
return config.activation;
|
|
849
|
+
}
|
|
850
|
+
function applyStylePlan(request) {
|
|
851
|
+
const { plan, ledger, attributes } = request;
|
|
852
|
+
for (const [property, value] of Object.entries(plan.properties)) ledger.set(property, value);
|
|
853
|
+
for (const [attribute, value] of Object.entries(plan.attributes)) attributes.set(attribute, value);
|
|
854
|
+
ledger.claim("animation-play-state");
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/core/animator.ts
|
|
858
|
+
var CLOAK_WATCHDOG_MS = 3e3;
|
|
859
|
+
function fingerprintOf(attributes) {
|
|
860
|
+
return [attributes.source, attributes.on, attributes.timeline, attributes.threshold].join("\0");
|
|
861
|
+
}
|
|
862
|
+
function isElementNode(node) {
|
|
863
|
+
return typeof Element !== "undefined" && node instanceof Element;
|
|
864
|
+
}
|
|
865
|
+
var Animator = class {
|
|
866
|
+
registry;
|
|
867
|
+
capabilities;
|
|
868
|
+
root;
|
|
869
|
+
reporter;
|
|
870
|
+
binder;
|
|
871
|
+
scheduler;
|
|
872
|
+
rootResolver;
|
|
873
|
+
jsEffectPreparer;
|
|
874
|
+
respectReducedMotion;
|
|
875
|
+
shouldObserve;
|
|
876
|
+
/** Runtime truth. Attributes are for CSS and debugging; they make a poor state machine. */
|
|
877
|
+
states = /* @__PURE__ */ new WeakMap();
|
|
878
|
+
/** Iterable lifecycle index; the WeakMap remains the fast state lookup. */
|
|
879
|
+
liveElements = /* @__PURE__ */ new Set();
|
|
880
|
+
/** Built lazily by `watch()` when not injected, so nothing observes until `start()` needs it. */
|
|
881
|
+
domWatcher;
|
|
882
|
+
started = false;
|
|
883
|
+
constructor(options = {}) {
|
|
884
|
+
const resolved = resolveCollaborators(options);
|
|
885
|
+
this.registry = resolved.registry;
|
|
886
|
+
this.capabilities = resolved.capabilities;
|
|
887
|
+
this.root = resolved.root;
|
|
888
|
+
this.reporter = resolved.reporter;
|
|
889
|
+
this.binder = resolved.binder;
|
|
890
|
+
this.scheduler = resolved.scheduler;
|
|
891
|
+
this.rootResolver = resolved.rootResolver;
|
|
892
|
+
this.jsEffectPreparer = resolved.jsEffectPreparer;
|
|
893
|
+
this.domWatcher = resolved.domWatcher;
|
|
894
|
+
this.respectReducedMotion = resolved.respectReducedMotion;
|
|
895
|
+
this.shouldObserve = resolved.shouldObserve;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Explicit entry point. Importing the library never touches the document, which keeps SSR,
|
|
899
|
+
* hydration, and tests deterministic.
|
|
900
|
+
*
|
|
901
|
+
* @complexity O(n) time in the number of elements scanned.
|
|
902
|
+
* @overallScore 100
|
|
903
|
+
*/
|
|
904
|
+
start() {
|
|
905
|
+
if (this.started) return this;
|
|
906
|
+
this.started = true;
|
|
907
|
+
const watchdog = globalThis.setTimeout(() => this.uncloak(), CLOAK_WATCHDOG_MS);
|
|
908
|
+
try {
|
|
909
|
+
this.scan(this.root);
|
|
910
|
+
if (this.shouldObserve) this.watch();
|
|
911
|
+
} finally {
|
|
912
|
+
globalThis.clearTimeout(watchdog);
|
|
913
|
+
this.uncloak();
|
|
914
|
+
}
|
|
915
|
+
return this;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Process every unprocessed element in a subtree, including the root.
|
|
919
|
+
*
|
|
920
|
+
* `querySelectorAll` excludes the root, but an inserted subtree very often carries the
|
|
921
|
+
* attribute on its top node — skipping it silently drops those animations.
|
|
922
|
+
*
|
|
923
|
+
* @param root - Subtree to scan. Defaults to the animator's root.
|
|
924
|
+
* @complexity O(n) time in the subtree size; O(1) extra space.
|
|
925
|
+
* @overallScore 100
|
|
926
|
+
*/
|
|
927
|
+
scan(root = this.root) {
|
|
928
|
+
if (!root) return this;
|
|
929
|
+
const selector = `[${ATTR.source}]`;
|
|
930
|
+
if (isElementNode(root) && root.matches(selector)) this.process(root);
|
|
931
|
+
for (const el of root.querySelectorAll(selector)) this.process(el);
|
|
932
|
+
applyStagger(root);
|
|
933
|
+
return this;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Compile and install one element's effects, recompiling if its attribute changed.
|
|
937
|
+
*
|
|
938
|
+
* @param el - Element carrying `data-kui`.
|
|
939
|
+
* @complexity O(e) time in the number of composed effects; O(e) space for the plan.
|
|
940
|
+
* @overallScore 100
|
|
941
|
+
*/
|
|
942
|
+
process(el) {
|
|
943
|
+
const attributes = readAttributes(el);
|
|
944
|
+
const fingerprint = fingerprintOf(attributes);
|
|
945
|
+
const existing = this.states.get(el);
|
|
946
|
+
if (existing?.fingerprint === fingerprint) return;
|
|
947
|
+
if (existing) this.release(el);
|
|
948
|
+
const parsed = parse(attributes.source);
|
|
949
|
+
const config = resolveConfig(attributes, parsed);
|
|
950
|
+
const plan = compile(parsed, this.registry, config.timeline);
|
|
951
|
+
config.activation = this.resolveActivation(el, config, plan);
|
|
952
|
+
for (const warning of plan.warnings) this.reporter.warn(warning, el);
|
|
953
|
+
if (plan.fxNames.length === 0) {
|
|
954
|
+
el.setAttribute(ATTR.state, plan.unknown.length > 0 ? "pending" : "failed");
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
this.install({ el, fingerprint, parsed, config, plan });
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Apply a compiled plan and bind its activation.
|
|
961
|
+
*
|
|
962
|
+
* @complexity O(e) time in composed effects; O(e) space for retained cleanups.
|
|
963
|
+
* @overallScore 100
|
|
964
|
+
*/
|
|
965
|
+
/**
|
|
966
|
+
* Choose the activation, letting a primitive's preference fill in only when the author named
|
|
967
|
+
* none, and warning when an authored activation is not one the effect supports.
|
|
968
|
+
*
|
|
969
|
+
* Declared capability metadata was previously never checked anywhere, which made
|
|
970
|
+
* `supportedActivations` documentation rather than a contract.
|
|
971
|
+
*
|
|
972
|
+
* @complexity O(a) time in supported activations; O(1) space.
|
|
973
|
+
* @overallScore 100
|
|
974
|
+
*/
|
|
975
|
+
resolveActivation(el, config, plan) {
|
|
976
|
+
if (!config.activationAuthored) return plan.defaultActivation ?? config.activation;
|
|
977
|
+
const supported = plan.supportedActivations;
|
|
978
|
+
if (supported.length > 0 && !supported.includes(config.activation)) {
|
|
979
|
+
this.reporter.warn(
|
|
980
|
+
`activation "${config.activation}" is not supported by this effect (supports: ${supported.join(", ")})`,
|
|
981
|
+
el
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
return config.activation;
|
|
985
|
+
}
|
|
986
|
+
install(request) {
|
|
987
|
+
const { el, fingerprint, parsed, config, plan } = request;
|
|
988
|
+
const stylePlan = planStyles({
|
|
989
|
+
plan,
|
|
990
|
+
config,
|
|
991
|
+
capabilities: this.capabilities,
|
|
992
|
+
respectReducedMotion: this.respectReducedMotion
|
|
993
|
+
});
|
|
994
|
+
const ledger = createStyleLedger(el);
|
|
995
|
+
const attributes = createAttributeLedger(el);
|
|
996
|
+
const controller = new AbortController();
|
|
997
|
+
applyStylePlan({ el, plan: stylePlan, ledger, attributes });
|
|
998
|
+
const state = {
|
|
999
|
+
fingerprint,
|
|
1000
|
+
specs: parsed.specs,
|
|
1001
|
+
activation: config.activation,
|
|
1002
|
+
timeline: config.timeline,
|
|
1003
|
+
instances: [],
|
|
1004
|
+
ledger,
|
|
1005
|
+
attributes,
|
|
1006
|
+
controller,
|
|
1007
|
+
status: "ready"
|
|
1008
|
+
};
|
|
1009
|
+
this.states.set(el, state);
|
|
1010
|
+
this.liveElements.add(el);
|
|
1011
|
+
if (Object.keys(stylePlan.properties).some((property) => property.startsWith("animation-"))) {
|
|
1012
|
+
const animationNames = (plan.declarations["animation-name"] ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
1013
|
+
state.instances.push(createCssInstance(el, ledger, animationNames));
|
|
1014
|
+
}
|
|
1015
|
+
state.instances.push(
|
|
1016
|
+
...this.jsEffectPreparer.prepare({ el, plan, signal: controller.signal, ledger })
|
|
1017
|
+
);
|
|
1018
|
+
this.openGate({ el, state, stylePlan, config, plan });
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Decide whether, and when, the effects on this element are allowed to start.
|
|
1022
|
+
*
|
|
1023
|
+
* The single place any effect begins. Routing both renderers through it is what makes
|
|
1024
|
+
* `on:enter`, `on:click`, `manual`, and `reducedMotion: 'disable'` mean the same thing for a
|
|
1025
|
+
* pinned section as for a fade — previously JS effects started during `prepare` and honoured
|
|
1026
|
+
* none of them.
|
|
1027
|
+
*
|
|
1028
|
+
* @complexity O(n) time in the number of instances; O(1) space.
|
|
1029
|
+
* @overallScore 100
|
|
1030
|
+
*/
|
|
1031
|
+
openGate(request) {
|
|
1032
|
+
const { el, state, stylePlan, config, plan } = request;
|
|
1033
|
+
const reduce = this.respectReducedMotion && this.capabilities.reducedMotion;
|
|
1034
|
+
if (reduce && plan.reducedMotion === "disable") {
|
|
1035
|
+
state.status = "finished";
|
|
1036
|
+
state.attributes.set(ATTR.state, "finished");
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (stylePlan.gate !== "deferred") {
|
|
1040
|
+
this.activate(el);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
state.controller.signal.addEventListener(
|
|
1044
|
+
"abort",
|
|
1045
|
+
this.binder.bind(el, stylePlan.activation, config.threshold, () => this.activate(el))
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Start a deferred animation.
|
|
1050
|
+
*
|
|
1051
|
+
* A JS-rendered effect's real setup work is postponed until this call — `deferPrepare` in
|
|
1052
|
+
* `instances.ts` only wires up an inert instance during `prepare` — so a broken primitive (a bad
|
|
1053
|
+
* selector, a malformed param) first throws here, not while the plan was being built. `scan()`
|
|
1054
|
+
* reaches this synchronously for every `on:load` element, inside the very loop that processes
|
|
1055
|
+
* every other element on the page; an uncaught throw here previously unwound that loop and
|
|
1056
|
+
* silently orphaned every element after the broken one — the same blast radius the `__proto__`
|
|
1057
|
+
* scan-crash fix closed for a different door. Each instance is isolated so one effect's failure
|
|
1058
|
+
* can neither strand a sibling effect on the same element nor abort the rest of the scan.
|
|
1059
|
+
*
|
|
1060
|
+
* @complexity O(n) time in composed instances; O(1) space.
|
|
1061
|
+
* @overallScore 100
|
|
1062
|
+
*/
|
|
1063
|
+
activate(el) {
|
|
1064
|
+
const state = this.states.get(el);
|
|
1065
|
+
if (!state || state.status === "running") return;
|
|
1066
|
+
state.status = "running";
|
|
1067
|
+
state.attributes.set(ATTR.state, "running");
|
|
1068
|
+
const started = state.instances.filter((instance) => this.startInstance(instance, el));
|
|
1069
|
+
if (started.length === 0 && state.instances.length > 0) {
|
|
1070
|
+
state.status = "failed";
|
|
1071
|
+
state.attributes.set(ATTR.state, "failed");
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
void Promise.all(started.map((instance) => instance.finished)).then(() => {
|
|
1075
|
+
if (this.states.get(el) !== state || state.status !== "running") return;
|
|
1076
|
+
state.status = "finished";
|
|
1077
|
+
state.attributes.set(ATTR.state, "finished");
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Activate one instance, isolating a throw from its (possibly deferred) setup.
|
|
1082
|
+
*
|
|
1083
|
+
* @returns Whether the instance actually started — a failed instance is excluded from the
|
|
1084
|
+
* `finished` gate in `activate()`, since something that never started can never legitimately
|
|
1085
|
+
* finish (see `EffectInstance.finished`'s "resolves, never rejects" contract in `types.ts`).
|
|
1086
|
+
* @complexity O(1) time, O(1) space.
|
|
1087
|
+
* @overallScore 100
|
|
1088
|
+
*/
|
|
1089
|
+
startInstance(instance, el) {
|
|
1090
|
+
try {
|
|
1091
|
+
instance.activate();
|
|
1092
|
+
return true;
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
this.reporter.warn(`effect failed to activate: ${String(error)}`, el);
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Programmatic entry point. Accepts a selector, an Element, a NodeList, or any iterable, so
|
|
1100
|
+
* `getElementById`, `getElementsByClassName`, and `querySelectorAll` all work directly.
|
|
1101
|
+
*
|
|
1102
|
+
* @complexity O(n) time in selected elements.
|
|
1103
|
+
* @overallScore 100
|
|
1104
|
+
*/
|
|
1105
|
+
play(target, effect, options = {}) {
|
|
1106
|
+
return play({ animator: this, root: this.root, target, effect }, options);
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Remove the opt-in cloak so a stalled or failed initialisation can never leave a page hidden.
|
|
1110
|
+
*
|
|
1111
|
+
* @complexity O(1) time, O(1) space.
|
|
1112
|
+
* @overallScore 100
|
|
1113
|
+
*/
|
|
1114
|
+
uncloak() {
|
|
1115
|
+
const doc = isElementNode(this.root) ? this.root.ownerDocument : this.root;
|
|
1116
|
+
doc?.documentElement?.removeAttribute(ATTR.cloak);
|
|
1117
|
+
}
|
|
1118
|
+
stateOf(el) {
|
|
1119
|
+
return this.states.get(el);
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Tear an element's effects down so the next `process()` reinstalls from scratch.
|
|
1123
|
+
*
|
|
1124
|
+
* Needed for replay: `process()` short-circuits when the configuration fingerprint is
|
|
1125
|
+
* unchanged, so playing the same effect twice was previously a no-op.
|
|
1126
|
+
*
|
|
1127
|
+
* @complexity O(c) time in retained instances; O(1) space.
|
|
1128
|
+
* @overallScore 100
|
|
1129
|
+
*/
|
|
1130
|
+
reset(el) {
|
|
1131
|
+
this.release(el);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Tear down one element's effects and clear its library-owned attributes.
|
|
1135
|
+
*
|
|
1136
|
+
* @complexity O(c) time in retained cleanups; O(1) extra space.
|
|
1137
|
+
* @overallScore 100
|
|
1138
|
+
*/
|
|
1139
|
+
release(el) {
|
|
1140
|
+
const state = this.states.get(el);
|
|
1141
|
+
if (!state) return;
|
|
1142
|
+
state.controller.abort();
|
|
1143
|
+
for (const instance of state.instances) runQuietly(() => instance.destroy());
|
|
1144
|
+
this.states.delete(el);
|
|
1145
|
+
this.liveElements.delete(el);
|
|
1146
|
+
state.ledger.restore();
|
|
1147
|
+
state.attributes.restore();
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Tear down every tracked element inside a removed subtree.
|
|
1151
|
+
*
|
|
1152
|
+
* Scoped to `node`'s own descendants rather than re-scanning `liveElements` against the whole
|
|
1153
|
+
* page, so a removal event costs O(removed subtree), not O(every animated element alive
|
|
1154
|
+
* anywhere) — `dom-watcher.ts` can queue up to 100 removed roots per frame, and `liveElements`
|
|
1155
|
+
* only shrinks on release, so it stays large on a scroll-reveal-heavy page.
|
|
1156
|
+
*
|
|
1157
|
+
* Membership is checked against `liveElements` (the ground truth) rather than re-querying
|
|
1158
|
+
* `[${ATTR.source}]` the way `scan()` does: `dom-watcher.ts`'s `flush()` drains removed roots
|
|
1159
|
+
* before attribute-change roots, so if calling code strips `data-kui` and removes the element in
|
|
1160
|
+
* the same tick, a selector-based query would already miss it here and leak its teardown.
|
|
1161
|
+
*
|
|
1162
|
+
* `node` is typed `Element`, not `ParentNode`: `dom-watcher.ts`'s `onElementRemoved` — this
|
|
1163
|
+
* method's only caller — is itself typed `(el: Element) => void`, so there is no runtime case
|
|
1164
|
+
* where `node` is a `Document`/`DocumentFragment` to guard against.
|
|
1165
|
+
*
|
|
1166
|
+
* @complexity O(s) time in the removed subtree's element count; O(1) per candidate via the
|
|
1167
|
+
* `liveElements` Set lookup.
|
|
1168
|
+
* @overallScore 100
|
|
1169
|
+
*/
|
|
1170
|
+
releaseTree(node) {
|
|
1171
|
+
if (this.liveElements.has(node)) this.release(node);
|
|
1172
|
+
for (const el of node.querySelectorAll("*")) {
|
|
1173
|
+
if (this.liveElements.has(el)) this.release(el);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
destroy() {
|
|
1177
|
+
this.domWatcher?.destroy();
|
|
1178
|
+
for (const el of [...this.liveElements]) this.release(el);
|
|
1179
|
+
this.binder.destroy();
|
|
1180
|
+
this.scheduler.destroy();
|
|
1181
|
+
this.started = false;
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Start watching for DOM insertions, removals, and attribute changes.
|
|
1185
|
+
*
|
|
1186
|
+
* Attribute changes recompile in place; insertions scan; removals tear down so listeners and
|
|
1187
|
+
* observers do not outlive their elements.
|
|
1188
|
+
*
|
|
1189
|
+
* @complexity O(1) time and space to build and start; the watcher's own callback runs O(n) time
|
|
1190
|
+
* in the nodes one mutation record carries.
|
|
1191
|
+
* @overallScore 100
|
|
1192
|
+
*/
|
|
1193
|
+
watch() {
|
|
1194
|
+
this.domWatcher ??= createDomWatcher({
|
|
1195
|
+
root: this.root,
|
|
1196
|
+
onElementAdded: (el) => this.scan(el),
|
|
1197
|
+
onElementRemoved: (el) => this.releaseTree(el),
|
|
1198
|
+
onAttributeChanged: (el) => this.process(el)
|
|
1199
|
+
});
|
|
1200
|
+
this.domWatcher.watch();
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
function createAnimator(options = {}) {
|
|
1204
|
+
return new Animator(options);
|
|
1205
|
+
}
|
|
1206
|
+
function resolveCollaborators(options) {
|
|
1207
|
+
const root = options.root ?? globalThis.document;
|
|
1208
|
+
const capabilities = options.capabilities ?? detect();
|
|
1209
|
+
const reporter = options.reporter ?? silentReporter();
|
|
1210
|
+
const scheduler = options.scheduler ?? createScrollScheduler();
|
|
1211
|
+
const rootResolver = options.rootResolver ?? defaultRootResolver(root);
|
|
1212
|
+
const respectReducedMotion = (options.reducedMotion ?? "respect") === "respect";
|
|
1213
|
+
return {
|
|
1214
|
+
registry: options.registry ?? new Registry(),
|
|
1215
|
+
capabilities,
|
|
1216
|
+
root,
|
|
1217
|
+
reporter,
|
|
1218
|
+
binder: options.binder ?? createActivationBinder(),
|
|
1219
|
+
scheduler,
|
|
1220
|
+
rootResolver,
|
|
1221
|
+
jsEffectPreparer: resolveJsEffectPreparer(options.jsEffectPreparer, {
|
|
1222
|
+
scheduler,
|
|
1223
|
+
rootResolver,
|
|
1224
|
+
capabilities,
|
|
1225
|
+
reporter,
|
|
1226
|
+
respectReducedMotion
|
|
1227
|
+
}),
|
|
1228
|
+
// Not defaulted here (unlike the other collaborators above): building the real watcher needs
|
|
1229
|
+
// `this.scan`/`this.process`/`this.releaseTree`, which don't exist yet inside this free
|
|
1230
|
+
// function. `Animator.watch()` builds it lazily instead, so nothing observes — and no
|
|
1231
|
+
// `MutationObserver` is ever constructed — unless `shouldObserve` is true and `start()` runs.
|
|
1232
|
+
domWatcher: options.domWatcher,
|
|
1233
|
+
respectReducedMotion,
|
|
1234
|
+
shouldObserve: options.observe ?? false
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
function resolveJsEffectPreparer(provided, deps) {
|
|
1238
|
+
return provided ?? createJsEffectPreparer(deps);
|
|
1239
|
+
}
|
|
1240
|
+
function defaultRootResolver(root) {
|
|
1241
|
+
const doc = isElementNode(root) ? root.ownerDocument : root;
|
|
1242
|
+
const win = doc?.defaultView ?? globalThis;
|
|
1243
|
+
return createRootResolver({ win });
|
|
1244
|
+
}
|
|
1245
|
+
function runQuietly(cleanup) {
|
|
1246
|
+
try {
|
|
1247
|
+
cleanup();
|
|
1248
|
+
} catch {
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
export {
|
|
1253
|
+
ATTR,
|
|
1254
|
+
createActivationBinder,
|
|
1255
|
+
detect,
|
|
1256
|
+
resolveTargets,
|
|
1257
|
+
toAttributeValue,
|
|
1258
|
+
play,
|
|
1259
|
+
consoleReporter,
|
|
1260
|
+
silentReporter,
|
|
1261
|
+
collectingReporter,
|
|
1262
|
+
Animator,
|
|
1263
|
+
createAnimator
|
|
1264
|
+
};
|