BOC v1.0: RS256 auth, Ledger integration, Prometheus metrics, CI/CD, backup

This commit is contained in:
Bernt
2026-07-28 23:08:32 +00:00
parent 0b4f160af1
commit af874040ca
11541 changed files with 1654104 additions and 1103 deletions
+175
View File
@@ -0,0 +1,175 @@
import { noop } from 'motion-utils';
import { addToQueue } from './queue.mjs';
class ViewTransitionBuilder {
constructor(update, options = {}) {
this.currentSubject = "root";
this.targets = new Map();
/**
* Definitions that must be resolved to elements (and assigned a
* `view-transition-name`) rather than treated as pre-named layers.
*/
this.resolveDefs = new Set();
/**
* Per-subject crop override: `true` forces the crop (clip + object-fit:
* cover + animated corner radii) on, `false` forces it off. A subject with
* no entry uses the default - crop only a genuine morph (a layer present in
* both snapshots), so a fade-only enter/exit isn't clipped to nothing.
*/
this.cropOverride = new Map();
/**
* Subjects paired with a different new-snapshot target (the second `.add()`
* argument), so two distinct elements share one name and morph into each
* other - a shared-element transition.
*/
this.pairs = new Map();
/**
* A `view-transition-class` to apply to each subject's resolved elements,
* so authors can target the generated layers from CSS by class rather than
* the opaque generated name.
*/
this.classNames = new Map();
/**
* Subjects opted out of automatic group nesting via `.group(false)`. Their
* layer stays a flat top-level group (`view-transition-group: none`) instead
* of nesting under its DOM-ancestor layer - so it animates independently and
* escapes an ancestor's clip/transform (e.g. an element that lifts out of a
* card and flies across, which nesting would clip to the card).
*/
this.flatGroups = new Set();
this.notifyReady = noop;
this.notifyReject = noop;
this.readyPromise = new Promise((resolve, reject) => {
this.notifyReady = resolve;
this.notifyReject = reject;
});
this.update = update;
this.options = {
interrupt: "wait",
...options,
};
// Avoid an unhandled rejection when a failed transition has no
// `.then(_, reject)` handler attached (e.g. fire-and-forget).
this.readyPromise.catch(noop);
addToQueue(this);
}
/**
* Target elements resolved from a selector or Element, each assigned a
* `view-transition-name` automatically.
*
* Passing a second target pairs them: the first is resolved in the old
* snapshot and the second in the new, sharing one name so two *different*
* elements morph into each other (e.g. `.add(card, ".modal")`). Symmetric -
* pass them the other way round to morph back.
*/
add(subject, newSubject) {
this.currentSubject = subject;
this.resolveDefs.add(subject);
if (newSubject !== undefined)
this.pairs.set(subject, newSubject);
// Register the subject so it participates (and gets an automatic
// layout/morph animation) even without an explicit enter/exit/layout.
if (!this.targets.has(subject))
this.targets.set(subject, {});
return this;
}
/**
* Control this subject's crop (clip + `object-fit: cover` + animated
* corners). By default a subject auto-crops only when it actually morphs -
* present in both snapshots (a survivor, or an `.add(a, b)` pair). A
* fade-only enter/exit has no second box to crop against, so it's left to
* the browser default; in particular the `overflow: clip` a crop adds would
* otherwise clip a mis-sized enter/exit layer to nothing.
*
* `.crop(false)` forces the crop off (e.g. a text morph, where
* `object-fit: cover` clips glyphs as the box grows); `.crop(true)` forces
* it on for a non-morph the default wouldn't otherwise crop.
*/
crop(enabled = true) {
this.cropOverride.set(this.currentSubject, enabled);
return this;
}
/**
* By default a subject's layer nests under its nearest DOM-ancestor layer
* (`view-transition-group: contain`), so the ancestor's clip/transform/opacity
* apply to it through the transition - mirroring how the DOM actually paints,
* and letting a wrapper crop its child for the whole morph rather than only
* once the live DOM takes back over. (Needs a browser that supports nested
* view-transition groups; elsewhere it degrades to the flat default.)
*
* Call `.group(false)` to opt out: the layer stays flat and top-level, so it
* animates independently and escapes an ancestor's clip - e.g. an element
* that should lift out of a card and fly across, which nesting would clip.
*/
group(enabled = true) {
enabled
? this.flatGroups.delete(this.currentSubject)
: this.flatGroups.add(this.currentSubject);
return this;
}
/**
* Tag this subject's generated layers with a `view-transition-class`, so
* they can be targeted from CSS - `::view-transition-group(.name)`,
* `::view-transition-old/new(.name)`, `::view-transition-image-pair(.name)`
* - without the opaque generated `view-transition-name`. Because `.add()`
* can match many elements, a shared class targets them all at once (and,
* for a pair, both ends). The escape hatch for z-index / custom keyframes
* on a morph layer.
*/
class(name) {
this.classNames.set(this.currentSubject, name);
return this;
}
/**
* Set the transition for this subject's morph. The morph is enabled
* automatically by `.add()`; this just customises its timing (duration,
* easing, a `delay`/`stagger`, …). On the implicit `root` subject it also
* opts the page into the transition (the root crossfade).
*/
layout(options = {}) {
this.updateTarget("layout", {}, options);
return this;
}
enter(keyframes, options) {
this.updateTarget("enter", keyframes, options);
return this;
}
exit(keyframes, options) {
this.updateTarget("exit", keyframes, options);
return this;
}
/**
* Animate the new view directly, whether the element is appearing or
* persisting (unlike `.enter()`, which only fires for a pure newcomer).
* Pair with `.old()` for a crossfade or slide-through.
*/
new(keyframes, options) {
this.updateTarget("new", keyframes, options);
return this;
}
/**
* Animate the old view directly, whether the element is leaving or
* persisting (unlike `.exit()`, which only fires for a pure leaver).
*/
old(keyframes, options) {
this.updateTarget("old", keyframes, options);
return this;
}
updateTarget(target, keyframes, options = {}) {
const { currentSubject, targets } = this;
if (!targets.has(currentSubject)) {
targets.set(currentSubject, {});
}
const targetData = targets.get(currentSubject);
targetData[target] = { keyframes, options };
}
then(resolve, reject) {
return this.readyPromise.then(resolve, reject);
}
}
function animateView(update, options = {}) {
return new ViewTransitionBuilder(update, options);
}
export { ViewTransitionBuilder, animateView };
//# sourceMappingURL=index.mjs.map
File diff suppressed because one or more lines are too long
+59
View File
@@ -0,0 +1,59 @@
import { removeItem } from 'motion-utils';
import { microtask } from '../frameloop/microtask.mjs';
import { startViewAnimation } from './start.mjs';
let builders = [];
let current = null;
function next() {
current = null;
const [nextBuilder] = builders;
if (nextBuilder)
start(nextBuilder);
}
function start(builder) {
removeItem(builders, builder);
current = builder;
startViewAnimation(builder)
.then((animation) => {
builder.notifyReady(animation);
return animation.finished;
})
// A genuinely failed transition (a throwing update) rejects the
// builder; a skipped/interrupted one resolves with no animations (see
// start.ts). Either way, advance the queue - else later transitions hang.
.catch((error) => builder.notifyReject(error))
.finally(next);
}
function processQueue() {
/**
* Iterate backwards over the builders array. We can ignore the
* "wait" animations. If we have an interrupting animation in the
* queue then we need to batch all preceeding animations into it.
* Currently this only batches the update functions but will also
* need to batch the targets.
*/
for (let i = builders.length - 1; i >= 0; i--) {
const builder = builders[i];
const { interrupt } = builder.options;
if (interrupt === "immediate") {
const batchedUpdates = builders.slice(0, i + 1).map((b) => b.update);
const remaining = builders.slice(i + 1);
builder.update = () => {
batchedUpdates.forEach((update) => update());
};
// Put the current builder at the front, followed by any "wait" builders
builders = [builder, ...remaining];
break;
}
}
if (!current || builders[0]?.options.interrupt === "immediate") {
next();
}
}
function addToQueue(builder) {
builders.push(builder);
microtask.render(processQueue);
}
export { addToQueue };
//# sourceMappingURL=queue.mjs.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"queue.mjs","sources":["../../../src/view/queue.ts"],"sourcesContent":["import { removeItem } from \"motion-utils\"\nimport type { ViewTransitionBuilder } from \".\"\nimport { microtask } from \"../frameloop/microtask\"\nimport { startViewAnimation } from \"./start\"\n\nlet builders: ViewTransitionBuilder[] = []\n\nlet current: ViewTransitionBuilder | null = null\n\nfunction next() {\n current = null\n const [nextBuilder] = builders\n if (nextBuilder) start(nextBuilder)\n}\n\nfunction start(builder: ViewTransitionBuilder) {\n removeItem(builders, builder)\n current = builder\n startViewAnimation(builder)\n .then((animation) => {\n builder.notifyReady(animation)\n return animation.finished\n })\n // A genuinely failed transition (a throwing update) rejects the\n // builder; a skipped/interrupted one resolves with no animations (see\n // start.ts). Either way, advance the queue - else later transitions hang.\n .catch((error) => builder.notifyReject(error))\n .finally(next)\n}\n\nfunction processQueue() {\n /**\n * Iterate backwards over the builders array. We can ignore the\n * \"wait\" animations. If we have an interrupting animation in the\n * queue then we need to batch all preceeding animations into it.\n * Currently this only batches the update functions but will also\n * need to batch the targets.\n */\n for (let i = builders.length - 1; i >= 0; i--) {\n const builder = builders[i]\n const { interrupt } = builder.options\n\n if (interrupt === \"immediate\") {\n const batchedUpdates = builders.slice(0, i + 1).map((b) => b.update)\n const remaining = builders.slice(i + 1)\n\n builder.update = () => {\n batchedUpdates.forEach((update) => update())\n }\n\n // Put the current builder at the front, followed by any \"wait\" builders\n builders = [builder, ...remaining]\n\n break\n }\n }\n\n if (!current || builders[0]?.options.interrupt === \"immediate\") {\n next()\n }\n}\n\nexport function addToQueue(builder: ViewTransitionBuilder) {\n builders.push(builder)\n microtask.render(processQueue)\n}\n"],"names":[],"mappings":";;;;AAKA,IAAI,QAAQ,GAA4B,EAAE;AAE1C,IAAI,OAAO,GAAiC,IAAI;AAEhD,SAAS,IAAI,GAAA;IACT,OAAO,GAAG,IAAI;AACd,IAAA,MAAM,CAAC,WAAW,CAAC,GAAG,QAAQ;AAC9B,IAAA,IAAI,WAAW;QAAE,KAAK,CAAC,WAAW,CAAC;AACvC;AAEA,SAAS,KAAK,CAAC,OAA8B,EAAA;AACzC,IAAA,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC7B,OAAO,GAAG,OAAO;IACjB,kBAAkB,CAAC,OAAO;AACrB,SAAA,IAAI,CAAC,CAAC,SAAS,KAAI;AAChB,QAAA,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC;QAC9B,OAAO,SAAS,CAAC,QAAQ;AAC7B,IAAA,CAAC;;;;AAIA,SAAA,KAAK,CAAC,CAAC,KAAK,KAAK,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC;SAC5C,OAAO,CAAC,IAAI,CAAC;AACtB;AAEA,SAAS,YAAY,GAAA;AACjB;;;;;;AAMG;AACH,IAAA,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE;AAC3C,QAAA,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC;AAC3B,QAAA,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,OAAO;AAErC,QAAA,IAAI,SAAS,KAAK,WAAW,EAAE;YAC3B,MAAM,cAAc,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;YACpE,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC;AAEvC,YAAA,OAAO,CAAC,MAAM,GAAG,MAAK;gBAClB,cAAc,CAAC,OAAO,CAAC,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;AAChD,YAAA,CAAC;;AAGD,YAAA,QAAQ,GAAG,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC;YAElC;QACJ;IACJ;AAEA,IAAA,IAAI,CAAC,OAAO,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,SAAS,KAAK,WAAW,EAAE;AAC5D,QAAA,IAAI,EAAE;IACV;AACJ;AAEM,SAAU,UAAU,CAAC,OAA8B,EAAA;AACrD,IAAA,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;AACtB,IAAA,SAAS,CAAC,MAAM,CAAC,YAAY,CAAC;AAClC;;;;"}
+700
View File
@@ -0,0 +1,700 @@
import { warnOnce, secondsToMilliseconds } from 'motion-utils';
import { GroupAnimation } from '../animation/GroupAnimation.mjs';
import { NativeAnimation } from '../animation/NativeAnimation.mjs';
import { NativeAnimationWrapper } from '../animation/NativeAnimationWrapper.mjs';
import { getValueTransition } from '../animation/utils/get-value-transition.mjs';
import { mapEasingToNativeEasing } from '../animation/waapi/easing/map-easing.mjs';
import { applyGeneratorOptions } from '../animation/waapi/utils/apply-generator.mjs';
import { cornerRadiusProps } from '../utils/border-radius.mjs';
import { resolveElements } from '../utils/resolve-elements.mjs';
import { assignViewTransitionNames, releaseViewTransitionNames } from './utils/assign-names.mjs';
import { chooseLayerType } from './utils/choose-layer-type.mjs';
import { css } from './utils/css.mjs';
import { getViewAnimationLayerInfo } from './utils/get-layer-info.mjs';
import { getViewAnimations } from './utils/get-view-animations.mjs';
import { hasTarget } from './utils/has-target.mjs';
const definitionNames = ["layout", "enter", "exit", "new", "old"];
/**
* Whether a computed border-radius is square (every component zero). Splitting
* on whitespace handles two-value/elliptical radii like "0px 20px" - a leading
* `parseFloat` alone would misread the non-zero vertical radius as square.
*/
const isSquareRadius = (radius) => radius.split(" ").every((value) => parseFloat(value) === 0);
/**
* The `ViewTransitionTarget` buckets driving each generated layer type, in
* priority order - the inverse of `chooseLayerType`. The new view is driven by
* `new`/`enter`, the old by `old`/`exit`. `group-children`/`image-pair` have no
* bucket; they follow the default layout timing.
*/
const typeBuckets = {
group: ["layout"],
new: ["new", "enter"],
old: ["old", "exit"],
};
/**
* Default "absent" origin for a single-value keyframe, by pseudo type, so e.g.
* `enter({ scale: 1 })` grows in from 0.85 and `exit({ opacity: 0 })` fades
* from 1. `enter` prefers the matching `exit` value over these (see below).
*/
const ORIGIN_DEFAULTS = {
new: { opacity: 0, scale: 0.85 },
old: { opacity: 1, scale: 1 },
};
/**
* How much two box aspect ratios must differ before a morph is treated as
* aspect-changing (and so worth cropping). Matches the projection engine's
* `preserve-aspect` threshold, so small layout jitter doesn't trigger a crop.
*/
const ASPECT_TOLERANCE = 0.2;
function startViewAnimation(builder) {
const { update, targets, resolveDefs, cropOverride, pairs, classNames, flatGroups, options: defaultOptions, } = builder;
if (!document.startViewTransition) {
// An async IIFE (not `new Promise(async …)`) so a throwing/rejecting
// update rejects this promise rather than leaving it unsettled.
return (async () => {
await update();
return new GroupAnimation([]);
})();
}
/**
* Resolve any selector/Element targets to layer names, assigning a
* `view-transition-name` to each element as we go. We run this before the
* update (so the elements are captured in the old snapshot) and again
* after it (for the new snapshot). An element present in both keeps the
* same name and animates as a single `group` layer.
*/
const nameRegistry = new Map();
const assigned = [];
/**
* Elements we tagged with a `view-transition-class` (via `.class()`),
* tracked separately from `assigned` so cleanup removes the class without
* ever stripping an author's own inline `view-transition-name`.
*/
const classed = [];
/**
* Elements we set a `view-transition-group` on (for nesting), tracked for
* cleanup. `clipChildren` collects the names of nested parents that clip in
* the live layout, so their `::view-transition-group-children` is clipped
* through the transition - not just once the live DOM takes back over.
*/
const grouped = [];
const clipChildren = new Set();
const layerTargets = new Map();
const croppedNames = new Set();
/**
* Each layer's explicit `.crop(true | false)` override (by resolved name),
* so `finalizeCrop` can let an author's choice win over the morph default.
*/
const cropForName = new Map();
/**
* Each layer's stagger position (index + total) within its subject, per
* snapshot. Resolving against the snapshot the layer belongs to keeps
* stagger correct when `update()` replaces the matched elements, and lets
* us skip a layer that's absent from a snapshot (e.g. an exited element
* has no `new` pseudo-element).
*/
const layerStagger = new Map();
/**
* Names allocated for a paired subject in the old snapshot, replayed onto
* its new-snapshot target so both ends share a layer and morph.
*/
const pairNames = new Map();
/**
* The old (`from`) elements of each paired subject, so their names can be
* transferred off before the new (`to`) elements inherit them.
*/
const pairFrom = new Map();
const resolveLayers = (phase) => {
targets.forEach((target, definition) => {
const className = classNames.get(definition);
/**
* Nest each resolved layer under its DOM-ancestor layer by default
* (`contain`), so an ancestor's clip/transform/opacity reach it
* through the transition; `.group(false)` opts a subject out (`none`)
* to stay flat and escape. Skipped for root / pre-named layers, which
* aren't elements we resolve and style.
*/
const group = definition === "root" || !resolveDefs.has(definition)
? undefined
: flatGroups.has(definition)
? "none"
: "contain";
let names;
if (definition === "root" || !resolveDefs.has(definition)) {
names = [definition];
}
else if (pairs.has(definition)) {
/**
* Paired morph: name the old target in the old snapshot, then
* force the same name(s) onto the new target in the new one, so
* two different elements morph as a single layer.
*/
if (phase === "old") {
pairFrom.set(definition, resolveElements(definition));
names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed, group, grouped, clipChildren);
pairNames.set(definition, names);
}
else {
/**
* Transfer the name(s) off the `from` elements before the
* `to` elements inherit them. A `from` that survives into
* the new snapshot (e.g. hidden with `visibility: hidden`
* rather than removed) would otherwise keep the name and
* collide - "duplicate view-transition-name".
*/
for (const el of pairFrom.get(definition) ?? []) {
el.style?.removeProperty("view-transition-name");
/**
* Drop the old end from the registry too, so the new
* end alone supplies this name's `new` crop radii - we
* neither re-measure nor get ordered by a stale element.
*/
nameRegistry.delete(el);
}
names = assignViewTransitionNames(pairs.get(definition), nameRegistry, assigned, pairNames.get(definition), className, classed, group, grouped, clipChildren);
}
}
else {
names = assignViewTransitionNames(definition, nameRegistry, assigned, undefined, className, classed, group, grouped, clipChildren);
}
// Record any explicit `.crop(true | false)` per resolved name; the
// crop set itself is computed later by `finalizeCrop` (it needs both
// snapshots to know which morphs change aspect ratio).
const override = cropOverride.get(definition);
names.forEach((name, index) => {
/**
* If two subjects resolve to the same element, merge their
* definitions so neither subject's animations are dropped.
*/
const existing = layerTargets.get(name);
layerTargets.set(name, existing && existing !== target
? { ...existing, ...target }
: target);
if (override !== undefined)
cropForName.set(name, override);
const stagger = layerStagger.get(name) ?? {};
stagger[phase] = [index, names.length];
layerStagger.set(name, stagger);
});
});
};
/**
* The stagger index/total for a layer, resolved against the snapshot it
* belongs to. Returns index -1 when the layer is absent from that snapshot
* so the caller can skip a pseudo-element that doesn't exist.
*/
const staggerPosition = (name, type) => {
const stagger = layerStagger.get(name);
const position = type === "old"
? stagger?.old
: type === "new"
? stagger?.new
: // group / group-children / image-pair persist across both.
stagger?.new ?? stagger?.old;
return position ?? [-1, 1];
};
/**
* Merge default + per-layer transition options for a generated layer and
* resolve any stagger/delay function against this element's position. Used
* by both the morph-retiming and crop corner-radius passes.
*/
const resolveLayerTransition = (target, type, transitionName, index, total) => {
const transition = mergeTransition(getValueTransition(defaultOptions, transitionName), getValueTransition((layerOptions(target, type) ?? {}), transitionName));
if (typeof transition.delay === "function") {
transition.delay = transition.delay(index, total);
}
return transition;
};
/**
* Resolve a layer's group (`layout`) timing to plain WAAPI values: native
* ms `delay`/`duration` and a baked `ease`. The single source of group
* timing, shared by the generated-group retiming and the crop corner-radius
* pass so the rounded clip animates on exactly the box's timing. It returns
* no generator `type` (the WAAPI-only `NativeAnimation` rejects a string
* type) nor `repeat`/`times` (which the group's `updateTiming` ignores), so
* none of them can leak into the radius animation and desync it.
*/
const resolveGroupTiming = (name) => {
const [index, total] = staggerPosition(name, "group");
const transition = resolveLayerTransition(layerTargets.get(name), "group", "layout", index === -1 ? 0 : index, total);
transition.duration && (transition.duration = secondsToMilliseconds(transition.duration));
const { delay = 0, duration, ease } = applyGeneratorOptions(transition);
return { delay: secondsToMilliseconds(delay), duration, ease };
};
/**
* Each layer's measured box + corner radii per snapshot. The box lets
* `finalizeCrop` tell whether a morph's aspect ratio changed (the only case
* worth cropping); the radii let a cropped morph's group clip animate each
* corner from the old element's radius to the new element's, keeping it
* rounded where `overflow: clip` would otherwise square the corners.
*
* We never flatten the source for capture (a snapshot is a paint of the live
* DOM, so squaring an element just for its capture would flash one real
* square frame). For an aspect-changing morph `object-fit: cover` crops each
* snapshot's own baked corners off-screen mid-morph, so the animated clip is
* the only visible corner; a near-same-aspect forced crop (`.crop(true)`)
* can't hide the outgoing snapshot's silhouette, but the endpoints coincide.
*/
const cropMeasurements = new Map();
const measureLayers = (phase) => nameRegistry.forEach((name, element) => {
const el = element;
const rect = el.getBoundingClientRect?.();
if (rect && rect.height) {
const style = getComputedStyle(el);
const radii = {};
for (const corner of cornerRadiusProps) {
radii[corner] = style[corner];
}
const entry = cropMeasurements.get(name) ?? {};
entry[phase] = { width: rect.width, height: rect.height, radii };
cropMeasurements.set(name, entry);
}
});
/**
* With both snapshots measured, settle which layers crop. The default crops
* only a morph whose aspect ratio *changes* between snapshots - the one case
* where `object-fit: cover` does real work. A same-aspect morph or a
* fade-only layer is left uncropped: its corners scale naturally (no flash
* from squaring, no `overflow: clip` eating its shadow) and a backdrop can't
* be clipped to nothing. An explicit `.crop(true | false)` overrides either
* way. Runs after both snapshots are measured, since aspect needs both.
*/
const finalizeCrop = () => {
croppedNames.clear();
for (const name of layerStagger.keys()) {
if (name === "root")
continue;
// An explicit `.crop(true | false)` wins; otherwise crop a morph
// whose aspect ratio changed.
if (cropForName.get(name) ?? aspectChanged(name)) {
croppedNames.add(name);
}
}
};
/**
* Whether a layer is a morph whose box aspect ratio changed between
* snapshots (beyond a small tolerance). Fade-only layers (one snapshot) are
* never "changed".
*/
const aspectChanged = (name) => {
const box = cropMeasurements.get(name);
if (!box?.old || !box?.new || !box.old.height || !box.new.height) {
return false;
}
return (Math.abs(box.old.width / box.old.height -
box.new.width / box.new.height) > ASPECT_TOLERANCE);
};
/**
* Write the persistent view-transition CSS: suppress root capture when the
* root has no animations of its own; force linear timing (baked into the
* keyframes, so we can retime later via updateTiming); and clip +
* object-fit: cover every cropped morph (the UA default overflows on
* aspect-ratio change).
*
* `css.commit` replaces rather than appends, so we re-set the full rule set
* each call - the crop rules are only known after `finalizeCrop` runs in the
* update callback, so the second call writes them.
*/
const commitViewCSS = () => {
if (!hasTarget("root", targets)) {
css.set(":root", { "view-transition-name": "none" });
}
css.set("::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*)", { "animation-timing-function": "linear !important" });
croppedNames.forEach((name) => {
css.set(`::view-transition-group(${name})`, { overflow: "clip" });
css.set(`::view-transition-old(${name}), ::view-transition-new(${name})`, { width: "100%", height: "100%", "object-fit": "cover" });
});
/**
* Clip the nested children of any layer that clips in the live layout,
* so a wrapper crops its child for the whole morph (mirroring the DOM)
* rather than only at the live-DOM handoff. No-op on browsers without
* nested view-transition groups.
*/
clipChildren.forEach((name) => {
css.set(`::view-transition-group-children(${name})`, {
overflow: "clip",
});
});
css.commit(); // Write
};
const cleanup = () => {
releaseViewTransitionNames(assigned, classed, grouped);
css.remove(); // Write
};
const callback = async () => {
await update();
/**
* Re-resolve so elements created by the update are named for the new
* snapshot, then measure them. With both snapshots measured we can
* settle the crop set (aspect-changing morphs + forced).
*/
resolveLayers("new");
measureLayers("new");
finalizeCrop();
/**
* Re-commit the crop CSS unconditionally: `finalizeCrop` is computed
* here (after both snapshots are measured), so the clip rules must be
* (re)written to match the settled set.
*/
commitViewCSS();
};
let transition;
try {
resolveLayers("old");
/**
* Measure the old snapshot against the optimistic crop set (the new
* snapshot doesn't exist yet, so aspect change can't be known here;
* `finalizeCrop` settles it post-update).
*/
measureLayers("old");
commitViewCSS();
transition = document.startViewTransition(callback);
}
catch (error) {
/**
* The prelude writes inline names before the transition exists. If it
* throws (e.g. startViewTransition rejects in a bad UA state), release
* them so we neither leak DOM state nor stall the queue on a promise
* that never settles - hand back a rejection it can advance past.
*/
cleanup();
return Promise.reject(error);
}
transition.finished.finally(cleanup);
return new Promise((resolve, reject) => {
transition.ready
.then(() => {
const generatedViewAnimations = getViewAnimations();
const animations = [];
/**
* Create animations for each of our explicitly-defined subjects.
* `opacityAnimated` additionally tracks which `${name}:${type}`
* we faded, so we can keep the UA `plus-lighter` blend only for a
* genuine opacity crossfade (both sides fading) and drop it for a
* slide/transform, where additive compositing would flash bright.
*/
const explicitlyAnimated = new Set();
const opacityAnimated = new Set();
layerTargets.forEach((target, name) => {
const stagger = layerStagger.get(name);
/**
* Presence: `enter` only fires for a pure newcomer (a new
* view with no old), `exit` only for a pure leaver. A
* survivor (both) gets neither - it just morphs.
*/
const enterApplies = !!stagger?.new && !stagger?.old;
const exitApplies = !!stagger?.old && !stagger?.new;
for (const key of definitionNames) {
if (!target[key])
continue;
if (key === "enter" && !enterApplies)
continue;
if (key === "exit" && !exitApplies)
continue;
const type = chooseLayerType(key);
const [index, total] = staggerPosition(name, type);
// Skip a layer absent from its snapshot.
if (index === -1)
continue;
const { keyframes, options } = target[key];
for (let [valueName, valueKeyframes] of Object.entries(keyframes)) {
// Skip only missing values - `0` (e.g. opacity: 0)
// is valid and must reach the from-value inference.
if (valueKeyframes == null)
continue;
/**
* The view path hands keyframes straight to WAAPI,
* so Motion's `x`/`y` shorthands (compiled to
* `transform` only via the value pipeline) have no
* effect. Warn and skip - use `transform`/`translate`.
*/
if (valueName === "x" || valueName === "y") {
warnOnce(false, `animateView() animates view-transition layers with CSS properties; the "${valueName}" shorthand has no effect - use transform, e.g. { transform: "translateX(40px)" }.`);
continue;
}
/**
* enter/exit win over new/old on a shared property -
* skip it here when the gated bucket also defines it.
*/
if (key === "new" &&
enterApplies &&
target.enter?.keyframes[valueName] != null) {
continue;
}
if (key === "old" &&
exitApplies &&
target.exit?.keyframes[valueName] != null) {
continue;
}
const valueOptions = mergeTransition(getValueTransition(defaultOptions, valueName), getValueTransition(options, valueName));
/**
* Infer an origin for a single-value keyframe. An
* `enter` mirrors the matching `exit` value (a
* defined exit reverses into the enter for free);
* otherwise the per-type default (opacity 0/1, scale
* 0.85). No default -> left as-is (animates from the
* live value).
*
* `new`/`old` fire for survivors too, where only the
* opacity crossfade default applies - a transform
* default like scale 0.85 would pop a persisting
* element, so gate it on the layer actually
* entering/leaving.
*/
if (!Array.isArray(valueKeyframes)) {
const exitValue = key === "enter"
? target.exit?.keyframes[valueName]
: undefined;
const allowDefault = valueName === "opacity" ||
(type === "new" ? enterApplies : exitApplies);
const from = exitValue != null
? Array.isArray(exitValue)
? exitValue[exitValue.length - 1]
: exitValue
: allowDefault
? ORIGIN_DEFAULTS[type]?.[valueName]
: undefined;
if (from !== undefined) {
valueKeyframes = [from, valueKeyframes];
}
}
/**
* Resolve stagger function if provided, per element
* across this subject's resolved layers.
*/
if (typeof valueOptions.delay === "function") {
valueOptions.delay = valueOptions.delay(index, total);
}
valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
animations.push(new NativeAnimation({
...valueOptions,
element: document.documentElement,
name: valueName,
pseudoElement: `::view-transition-${type}(${name})`,
keyframes: valueKeyframes,
}));
explicitlyAnimated.add(`${name}:${type}`);
if (valueName === "opacity") {
opacityAnimated.add(`${name}:${type}`);
}
}
}
});
/**
* Handle browser generated animations
*/
for (const animation of generatedViewAnimations) {
if (animation.playState === "finished")
continue;
const { effect } = animation;
if (!effect || !(effect instanceof KeyframeEffect))
continue;
const { pseudoElement } = effect;
if (!pseudoElement)
continue;
const name = getViewAnimationLayerInfo(pseudoElement);
if (!name)
continue;
const targetDefinition = layerTargets.get(name.layer);
/**
* We built our own animation for this layer, so drop the
* browser-generated fade we're replacing. The UA
* `plus-lighter` blend is a *separate* generated animation on
* the same pseudo (it sets `mix-blend-mode` in its keyframes):
* keep it *only* for a true opacity crossfade - both sides
* fading - so a symmetric crossfade composites without
* darkening, but a slide/transform (where both layers stay
* opaque and overlap) doesn't flash bright from the addition.
*/
if (explicitlyAnimated.has(`${name.layer}:${name.type}`)) {
const isCrossfade = opacityAnimated.has(`${name.layer}:new`) &&
opacityAnimated.has(`${name.layer}:old`);
if (isCrossfade &&
effect
.getKeyframes()
.some((keyframe) => keyframe.mixBlendMode)) {
animations.push(new NativeAnimationWrapper(animation));
}
else {
animation.cancel();
}
continue;
}
/**
* Drop the orphaned half of the default crossfade. The UA
* fades old out and new in as a *pair*; if the opposing half
* was explicitly overridden with something other than an
* opacity fade (a clip or transform reveal), this side's
* default opacity fade has no partner - left to run it would
* dissolve what should be a static backdrop (e.g.
* `.new({ clipPath })` should reveal over a still old view,
* not fade the old out around the growing clip). Cancel it -
* and its `plus-lighter` sibling on the same pseudo, which
* would otherwise flash bright where the two opaque layers
* overlap. A genuine crossfade (the opposing side also fading
* opacity) keeps both halves and is handled above.
*/
const opposite = name.type === "old"
? "new"
: name.type === "new"
? "old"
: undefined;
if (opposite &&
explicitlyAnimated.has(`${name.layer}:${opposite}`) &&
!opacityAnimated.has(`${name.layer}:${opposite}`)) {
animation.cancel();
continue;
}
/**
* Otherwise retime the browser-generated animation to
* Motion's timing. This auto-enables the layout (group)
* morph for any resolved/named target, and applies the
* default timing to old/new layers we haven't explicitly
* overridden.
*
* group + group-children both follow the layout timing so
* the nesting container stays in sync with the morph.
*/
/**
* A survivor's old + new are the two halves of one
* `plus-lighter` crossfade. They must share identical timing
* (so their opacities stay mirrored and sum to 1 - else the
* additive blend flashes bright wherever both are partly
* visible) and fade linearly (the bounce belongs on the
* group's geometry, not the opacity). So time them as the
* group, rather than via their own - potentially staggered,
* or enter/exit-derived - old/new options.
*/
const stagger = layerStagger.get(name.layer);
const isMorphCrossfade = (name.type === "old" || name.type === "new") &&
!!stagger?.old &&
!!stagger?.new;
let timing;
if (name.type.startsWith("group")) {
// group + group-children follow the resolved group
// timing - the single source shared with the crop
// corner-radius pass below.
const { delay, duration, ease } = resolveGroupTiming(name.layer);
timing = {
delay,
duration,
easing: mapEasingToNativeEasing(ease, duration),
};
}
else {
const timingType = isMorphCrossfade ? "group" : name.type;
const [index, total] = staggerPosition(name.layer, timingType);
const transitionName = timingType === "group" ? "layout" : "";
let animationTransition = resolveLayerTransition(targetDefinition, timingType, transitionName, index === -1 ? 0 : index, total);
/**
* The crossfade should resolve at the spring's
* *perceptual* (visual) duration - the geometry can keep
* bouncing, but the opacity shouldn't drag through the
* settle. So capture `visualDuration` before
* `applyGeneratorOptions` replaces it with the full
* overshoot duration, and use it for the fade.
*/
const visualDuration = animationTransition.visualDuration;
animationTransition.duration && (animationTransition.duration = secondsToMilliseconds(animationTransition.duration));
animationTransition =
applyGeneratorOptions(animationTransition);
timing = {
delay: secondsToMilliseconds(animationTransition.delay ?? 0),
duration: isMorphCrossfade && visualDuration !== undefined
? secondsToMilliseconds(visualDuration)
: animationTransition.duration,
easing: isMorphCrossfade
? "linear"
: mapEasingToNativeEasing(animationTransition.ease, animationTransition.duration),
};
}
effect.updateTiming(timing);
animations.push(new NativeAnimationWrapper(animation));
}
/**
* Round each cropped layer's clip. Its `::view-transition-group`
* has `overflow: clip`, which would otherwise square the corners
* mid-morph; animate each corner from the old element's radius to
* the new element's so the crop stays rounded. Timed as the group
* (`layout`) so the radius tracks the morphing box.
*/
cropMeasurements.forEach((entry, name) => {
if (!croppedNames.has(name))
return;
// Reuse the group's resolved timing - native ms delay/
// duration + a baked ease, with no generator `type` or
// repeat/times to leak into (or throw inside) NativeAnimation.
const { delay, duration, ease } = resolveGroupTiming(name);
for (const corner of cornerRadiusProps) {
// `||` (not `??`) so an empty measurement falls back to
// the other snapshot rather than an invalid keyframe.
const from = entry.old?.radii[corner] ||
entry.new?.radii[corner] ||
"0px";
const to = entry.new?.radii[corner] ||
entry.old?.radii[corner] ||
"0px";
// Nothing to round if the corner is square at both ends.
if (isSquareRadius(from) && isSquareRadius(to))
continue;
animations.push(new NativeAnimation({
element: document.documentElement,
name: corner,
pseudoElement: `::view-transition-group(${name})`,
keyframes: [from, to],
delay,
duration,
ease,
}));
}
});
resolve(new GroupAnimation(animations));
})
.catch(() =>
/**
* `ready` rejects when the transition is skipped - no visual
* change, or superseded by an interrupting transition. The DOM
* update still applied, so settle with no animations rather than
* surfacing it as an error to an awaiting caller. A genuine
* failure in `update()` rejects `updateCallbackDone` (already
* settled by now), so propagate that instead.
*/
transition.updateCallbackDone.then(() => resolve(new GroupAnimation([])), reject));
});
}
/**
* The options that should time a given generated layer type, so a retimed
* group/old/new picks up any per-target transition the user provided. Checks
* the type's buckets in priority order (e.g. `new` before `enter`).
*/
function layerOptions(target, type) {
for (const bucket of typeBuckets[type] ?? []) {
const options = target?.[bucket]?.options;
if (options)
return options;
}
}
/**
* Merge a base transition (e.g. the default `options`) with a per-layer/value
* override. An explicit `duration` on the override must win over an inherited
* generator's own timing: a spring prefers `visualDuration`, and
* `spring.applyToOptions` overwrites `duration` with the computed settle time -
* so without this the override is silently discarded. Dropping the inherited
* `type`/`visualDuration` makes the layer a plain tween of that duration, unless
* it asked for its own generator `type`/`visualDuration`.
*/
function mergeTransition(base, override) {
const merged = { ...base, ...override };
if (override.duration !== undefined) {
if (override.visualDuration === undefined)
delete merged.visualDuration;
if (override.type === undefined)
delete merged.type;
}
return merged;
}
export { startViewAnimation };
//# sourceMappingURL=start.mjs.map
File diff suppressed because one or more lines are too long
+146
View File
@@ -0,0 +1,146 @@
import { resolveElements } from '../../utils/resolve-elements.mjs';
let nameCount = 0;
/**
* Generated names live in their own namespace so we can tell a name we own
* (and must clean up) from an author-defined one - and so a stale generated
* name left behind by an interrupted transition is re-owned, not mistaken for
* the author's and leaked.
*/
const generatedName = () => `motion-view-${nameCount++}`;
const isGeneratedName = (name) => name.startsWith("motion-view-");
/**
* Tag a captured element with a `view-transition-class` so authors can target
* its generated layer from CSS (e.g. `::view-transition-group(.hero)`) without
* the opaque generated name. Tracked in `classed` - separate from the generated
* names in `assigned` - so cleanup removes the class without ever stripping an
* author's own inline `view-transition-name`.
*/
function tagClass(element, className, classed) {
if (!className)
return;
element.style?.setProperty("view-transition-class", className);
classed.push(element);
}
/**
* Set the element's `view-transition-group` so its layer reconstructs the DOM
* hierarchy in the pseudo-tree (`contain`) - or stays flat (`none`). Tracked in
* `grouped` for cleanup. When the element clips (any non-`visible` overflow) its
* name is recorded in `clipChildren` so the caller can clip the nested children
* (`::view-transition-group-children(name)`), mirroring the live clip through
* the whole transition rather than only at the live-DOM handoff.
*
* Ignored by browsers without nested view-transition groups, where it harmlessly
* degrades to the flat default.
*/
function applyGroup(element, name, group, grouped, clipChildren) {
if (!group)
return;
element.style?.setProperty("view-transition-group", group);
grouped.push(element);
if (group !== "none" && clipChildren) {
const style = getComputedStyle(element);
if (style.overflowX !== "visible" || style.overflowY !== "visible") {
clipChildren.add(name);
}
}
}
/**
* Resolve a selector/Element to elements and ensure each one carries a
* `view-transition-name` we can target from script.
*
* Author-defined names are reused as-is. Elements that are unnamed (or use
* the browser's `auto`/`match-element`, whose generated name is not exposed
* to script) are given a unique generated name, set inline so it's captured,
* and tracked in `assigned` for later cleanup.
*
* `registry` maps each Element to its name so the same element keeps its name
* across both captures (before and after the update), which is what allows a
* persistent element to animate as a single `group` layer.
*/
function assignViewTransitionNames(definition, registry, assigned, forcedNames, className, classed = [], group, grouped = [], clipChildren) {
const elements = resolveElements(definition);
/**
* The new end of a paired morph: give each element the matching name from
* the old end (by index) so the two share one layer and morph. If the new
* end resolves to *more* elements than the old end named, the extras have no
* counterpart - give them a fresh name so they animate as newcomers rather
* than being silently left unnamed. We return the names actually assigned
* (sized to the resolved elements), not the raw `forcedNames`, so stagger
* totals and the layer set stay in step with what's on the page.
*/
if (forcedNames) {
return elements.map((element, i) => {
const existing = registry.get(element);
if (existing)
return existing;
const name = forcedNames[i] ?? generatedName();
element.style?.setProperty("view-transition-name", name);
assigned.push(element);
registry.set(element, name);
tagClass(element, className, classed);
applyGroup(element, name, group, grouped, clipChildren);
return name;
});
}
/**
* Read every current name up front, before assigning any. Interleaving the
* reads with the inline `setProperty` writes below would dirty styles
* between reads and force a style recalc per element; batching the reads
* keeps it to one. Elements already in the registry keep their name and
* need no read.
*/
const currentNames = elements.map((element) => registry.has(element)
? undefined
: getComputedStyle(element).getPropertyValue("view-transition-name"));
return elements.map((element, i) => {
const existing = registry.get(element);
if (existing)
return existing;
const current = currentNames[i];
let name;
if (current &&
current !== "none" &&
current !== "auto" &&
current !== "match-element" &&
!isGeneratedName(current)) {
/**
* The author already named this layer - target it as-is and leave
* it to them to clean up. `auto`/`match-element` are overridden
* because their generated name is not exposed to script, and a
* stale `motion-view-*` (e.g. left by an interrupted transition) is
* re-owned rather than adopted as an author name and leaked.
*/
name = current;
}
else {
name = generatedName();
element.style?.setProperty("view-transition-name", name);
assigned.push(element);
}
registry.set(element, name);
tagClass(element, className, classed);
applyGroup(element, name, group, grouped, clipChildren);
return name;
});
}
/**
* Remove the `view-transition-name`s we generated and the
* `view-transition-class`es we applied. Author-defined names are never touched
* (they're not in `assigned`). Safe to call more than once (e.g. on both a
* finished and an interrupted transition).
*/
function releaseViewTransitionNames(assigned, classed = [], grouped = []) {
for (const element of assigned) {
element.style?.removeProperty("view-transition-name");
}
for (const element of classed) {
element.style?.removeProperty("view-transition-class");
}
for (const element of grouped) {
element.style?.removeProperty("view-transition-group");
}
}
export { assignViewTransitionNames, releaseViewTransitionNames };
//# sourceMappingURL=assign-names.mjs.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
function chooseLayerType(valueName) {
if (valueName === "layout")
return "group";
if (valueName === "enter" || valueName === "new")
return "new";
return "old";
}
export { chooseLayerType };
//# sourceMappingURL=choose-layer-type.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"choose-layer-type.mjs","sources":["../../../../src/view/utils/choose-layer-type.ts"],"sourcesContent":["export function chooseLayerType(\n valueName: \"layout\" | \"enter\" | \"exit\" | \"new\" | \"old\"\n): \"group\" | \"old\" | \"new\" {\n if (valueName === \"layout\") return \"group\"\n if (valueName === \"enter\" || valueName === \"new\") return \"new\"\n return \"old\"\n}\n"],"names":[],"mappings":"AAAM,SAAU,eAAe,CAC3B,SAAsD,EAAA;IAEtD,IAAI,SAAS,KAAK,QAAQ;AAAE,QAAA,OAAO,OAAO;AAC1C,IAAA,IAAI,SAAS,KAAK,OAAO,IAAI,SAAS,KAAK,KAAK;AAAE,QAAA,OAAO,KAAK;AAC9D,IAAA,OAAO,KAAK;AAChB;;;;"}
+33
View File
@@ -0,0 +1,33 @@
let pendingRules = {};
let style = null;
const css = {
set: (selector, values) => {
pendingRules[selector] = values;
},
commit: () => {
if (!style) {
style = document.createElement("style");
style.id = "motion-view";
}
let cssText = "";
for (const selector in pendingRules) {
const rule = pendingRules[selector];
cssText += `${selector} {\n`;
for (const [property, value] of Object.entries(rule)) {
cssText += ` ${property}: ${value};\n`;
}
cssText += "}\n";
}
style.textContent = cssText;
document.head.appendChild(style);
pendingRules = {};
},
remove: () => {
if (style && style.parentElement) {
style.parentElement.removeChild(style);
}
},
};
export { css };
//# sourceMappingURL=css.mjs.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"css.mjs","sources":["../../../../src/view/utils/css.ts"],"sourcesContent":["let pendingRules: Record<string, Record<string, string>> = {}\n\nlet style: HTMLStyleElement | null = null\n\nexport const css = {\n set: (selector: string, values: Record<string, string>) => {\n pendingRules[selector] = values\n },\n\n commit: () => {\n if (!style) {\n style = document.createElement(\"style\")\n style.id = \"motion-view\"\n }\n\n let cssText = \"\"\n\n for (const selector in pendingRules) {\n const rule = pendingRules[selector]\n cssText += `${selector} {\\n`\n for (const [property, value] of Object.entries(rule)) {\n cssText += ` ${property}: ${value};\\n`\n }\n cssText += \"}\\n\"\n }\n\n style.textContent = cssText\n document.head.appendChild(style)\n\n pendingRules = {}\n },\n\n remove: () => {\n if (style && style.parentElement) {\n style.parentElement.removeChild(style)\n }\n },\n}\n"],"names":[],"mappings":"AAAA,IAAI,YAAY,GAA2C,EAAE;AAE7D,IAAI,KAAK,GAA4B,IAAI;AAElC,MAAM,GAAG,GAAG;AACf,IAAA,GAAG,EAAE,CAAC,QAAgB,EAAE,MAA8B,KAAI;AACtD,QAAA,YAAY,CAAC,QAAQ,CAAC,GAAG,MAAM;IACnC,CAAC;IAED,MAAM,EAAE,MAAK;QACT,IAAI,CAAC,KAAK,EAAE;AACR,YAAA,KAAK,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC;AACvC,YAAA,KAAK,CAAC,EAAE,GAAG,aAAa;QAC5B;QAEA,IAAI,OAAO,GAAG,EAAE;AAEhB,QAAA,KAAK,MAAM,QAAQ,IAAI,YAAY,EAAE;AACjC,YAAA,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC;AACnC,YAAA,OAAO,IAAI,CAAA,EAAG,QAAQ,CAAA,IAAA,CAAM;AAC5B,YAAA,KAAK,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;AAClD,gBAAA,OAAO,IAAI,CAAA,EAAA,EAAK,QAAQ,CAAA,EAAA,EAAK,KAAK,KAAK;YAC3C;YACA,OAAO,IAAI,KAAK;QACpB;AAEA,QAAA,KAAK,CAAC,WAAW,GAAG,OAAO;AAC3B,QAAA,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;QAEhC,YAAY,GAAG,EAAE;IACrB,CAAC;IAED,MAAM,EAAE,MAAK;AACT,QAAA,IAAI,KAAK,IAAI,KAAK,CAAC,aAAa,EAAE;AAC9B,YAAA,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,KAAK,CAAC;QAC1C;IACJ,CAAC;;;;;"}
+11
View File
@@ -0,0 +1,11 @@
function getViewAnimationLayerInfo(pseudoElement) {
const match = pseudoElement.match(
// `group-children` (nested transitions) before `group` so it wins.
/::view-transition-(old|new|group-children|group|image-pair)\((.*?)\)/);
if (!match)
return null;
return { layer: match[2], type: match[1] };
}
export { getViewAnimationLayerInfo };
//# sourceMappingURL=get-layer-info.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"get-layer-info.mjs","sources":["../../../../src/view/utils/get-layer-info.ts"],"sourcesContent":["export function getViewAnimationLayerInfo(pseudoElement: string) {\n const match = pseudoElement.match(\n // `group-children` (nested transitions) before `group` so it wins.\n /::view-transition-(old|new|group-children|group|image-pair)\\((.*?)\\)/\n )\n if (!match) return null\n\n return { layer: match[2], type: match[1] }\n}\n"],"names":[],"mappings":"AAAM,SAAU,yBAAyB,CAAC,aAAqB,EAAA;AAC3D,IAAA,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK;;AAE7B,IAAA,sEAAsE,CACzE;AACD,IAAA,IAAI,CAAC,KAAK;AAAE,QAAA,OAAO,IAAI;AAEvB,IAAA,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE;AAC9C;;;;"}
@@ -0,0 +1,11 @@
function getViewAnimations() {
return document.getAnimations().filter((animation) => {
const { effect } = animation;
return (!!effect &&
effect.target === document.documentElement &&
effect.pseudoElement?.startsWith("::view-transition"));
});
}
export { getViewAnimations };
//# sourceMappingURL=get-view-animations.mjs.map
@@ -0,0 +1 @@
{"version":3,"file":"get-view-animations.mjs","sources":["../../../../src/view/utils/get-view-animations.ts"],"sourcesContent":["export function getViewAnimations() {\n return document.getAnimations().filter((animation) => {\n const { effect } = animation\n return (\n !!effect &&\n effect.target === document.documentElement &&\n (effect as KeyframeEffect).pseudoElement?.startsWith(\n \"::view-transition\"\n )\n )\n })\n}\n"],"names":[],"mappings":"SAAgB,iBAAiB,GAAA;IAC7B,OAAO,QAAQ,CAAC,aAAa,EAAE,CAAC,MAAM,CAAC,CAAC,SAAS,KAAI;AACjD,QAAA,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS;QAC5B,QACI,CAAC,CAAC,MAAM;AACR,YAAA,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,eAAe;YACzC,MAAyB,CAAC,aAAa,EAAE,UAAU,CAChD,mBAAmB,CACtB;AAET,IAAA,CAAC,CAAC;AACN;;;;"}
+6
View File
@@ -0,0 +1,6 @@
function hasTarget(target, targets) {
return targets.has(target) && Object.keys(targets.get(target)).length > 0;
}
export { hasTarget };
//# sourceMappingURL=has-target.mjs.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"has-target.mjs","sources":["../../../../src/view/utils/has-target.ts"],"sourcesContent":["import { ViewTransitionTarget, ViewTransitionTargetDefinition } from \"../types\"\n\nexport function hasTarget(\n target: ViewTransitionTargetDefinition,\n targets: Map<ViewTransitionTargetDefinition, ViewTransitionTarget>\n) {\n return targets.has(target) && Object.keys(targets.get(target)!).length > 0\n}\n"],"names":[],"mappings":"AAEM,SAAU,SAAS,CACrB,MAAsC,EACtC,OAAkE,EAAA;IAElE,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AAC9E;;;;"}