Increase complexity of test demo
This commit is contained in:
@@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import OriginGestureSurface from "./components/OriginGestureSurface.vue";
|
||||
import OriginScene from "./components/OriginScene.vue";
|
||||
import { gesture, useOriginGesture } from "./gesture";
|
||||
import { back, defineOriginChoreography, forward } from "./motion";
|
||||
import { back, defineOriginChoreography, forward, replace } from "./motion";
|
||||
import { createOriginScene, originView } from "./scene";
|
||||
import type {
|
||||
OriginGestureBinding,
|
||||
@@ -109,6 +109,11 @@ describe("gesture builder", () => {
|
||||
placement: "under",
|
||||
history: "back",
|
||||
});
|
||||
expect(replace(target)).toEqual({
|
||||
target,
|
||||
placement: "above",
|
||||
history: "replace",
|
||||
});
|
||||
expect(forward(target, testMotion)).toMatchObject({
|
||||
target,
|
||||
choreography: testMotion,
|
||||
@@ -118,6 +123,11 @@ describe("gesture builder", () => {
|
||||
choreography: testMotion,
|
||||
history: "back",
|
||||
});
|
||||
expect(replace(target, testMotion)).toMatchObject({
|
||||
target,
|
||||
choreography: testMotion,
|
||||
history: "replace",
|
||||
});
|
||||
});
|
||||
|
||||
it("treats a chain beginning at .to as an immutable anywhere gesture", () => {
|
||||
|
||||
@@ -56,7 +56,8 @@ export interface OriginNavigationActionOptions {
|
||||
/**
|
||||
* Target stacking relationship during the operation.
|
||||
*
|
||||
* @defaultValue `"above"` for {@link forward}, `"under"` for {@link back}
|
||||
* @defaultValue `"above"` for {@link forward} and {@link replace}, `"under"`
|
||||
* for {@link back}
|
||||
*/
|
||||
placement?: OriginPlacement;
|
||||
}
|
||||
@@ -135,6 +136,50 @@ export function forward(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a retained-history replacement intent or complete action.
|
||||
*
|
||||
* The target inherits the origin's previous mounted entry instead of retaining
|
||||
* the origin itself. A commit unmounts the origin and activates the target, so
|
||||
* a later {@link back} skips the replaced entry. A cancellation removes the
|
||||
* proposed target and restores the origin unchanged.
|
||||
*
|
||||
* Omit choreography when declaring `.navigate()` inside a gesture builder;
|
||||
* provide choreography when passing the result directly to `begin()` or
|
||||
* `perform()`.
|
||||
*
|
||||
* @param target - View recipe created for the replacement.
|
||||
* @param options - Stacking options for an animation-free navigation intent.
|
||||
* @returns An animation-free replacement intent for use with a gesture builder.
|
||||
*/
|
||||
export function replace(
|
||||
target: OriginView,
|
||||
options?: OriginNavigationActionOptions,
|
||||
): OriginNavigationIntent;
|
||||
export function replace(
|
||||
target: OriginView,
|
||||
choreography: OriginChoreography,
|
||||
options?: OriginNavigationActionOptions,
|
||||
): OriginAction;
|
||||
export function replace(
|
||||
target: OriginView,
|
||||
choreographyOrOptions:
|
||||
OriginChoreography | OriginNavigationActionOptions = {},
|
||||
options: OriginNavigationActionOptions = {},
|
||||
): OriginAction | OriginNavigationIntent {
|
||||
if (isChoreography(choreographyOrOptions)) {
|
||||
return originAction(target, choreographyOrOptions, {
|
||||
placement: options.placement ?? "above",
|
||||
history: "replace",
|
||||
});
|
||||
}
|
||||
return {
|
||||
target,
|
||||
placement: choreographyOrOptions.placement ?? "above",
|
||||
history: "replace",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a retained-history back intent or complete action.
|
||||
*
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
back,
|
||||
defineOriginChoreography,
|
||||
forward,
|
||||
replace,
|
||||
under,
|
||||
} from "./motion";
|
||||
import { createOriginScene, originView } from "./scene";
|
||||
@@ -40,6 +41,23 @@ const layeredMotion = defineOriginChoreography({
|
||||
}),
|
||||
});
|
||||
|
||||
const partialDrawerOpen = defineOriginChoreography({
|
||||
name: "test-partial-drawer-open",
|
||||
persistAtRest: true,
|
||||
effects: ({ progress }) => ({
|
||||
source: { transform: `translateX(${progress * 66.6667}%)` },
|
||||
target: { transform: `translateX(${(progress - 1) * 66.6667}%)` },
|
||||
}),
|
||||
});
|
||||
|
||||
const partialDrawerClose = defineOriginChoreography({
|
||||
name: "test-partial-drawer-close",
|
||||
effects: ({ progress }) => ({
|
||||
source: { transform: `translateX(${-progress * 66.6667}%)` },
|
||||
target: { transform: `translateX(${(1 - progress) * 66.6667}%)` },
|
||||
}),
|
||||
});
|
||||
|
||||
describe("origin-relative scene graph", () => {
|
||||
it("composes X→Y and Y→Z as independent transform layers", async () => {
|
||||
const x = originView(component("X"), undefined, { key: "x" });
|
||||
@@ -290,6 +308,215 @@ describe("origin-relative scene graph", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("replaces the current entry and makes back skip the removed instance", async () => {
|
||||
let paymentUnmounts = 0;
|
||||
const home = originView(component("Home"), undefined, { key: "home" });
|
||||
const Payment = defineComponent({
|
||||
name: "Payment",
|
||||
setup() {
|
||||
onUnmounted(() => {
|
||||
paymentUnmounts += 1;
|
||||
});
|
||||
return () => h("div", "Payment");
|
||||
},
|
||||
});
|
||||
const payment = originView(Payment, undefined, { key: "payment" });
|
||||
const confirmation = originView(component("Confirmation"), undefined, {
|
||||
key: "confirmation",
|
||||
});
|
||||
const scene = createOriginScene({ initial: home });
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const app = createApp({
|
||||
render: () => h(OriginScene, { scene }),
|
||||
});
|
||||
mountedApps.push(app);
|
||||
app.mount(host);
|
||||
await nextTick();
|
||||
const homeKey = scene.nodes.value[0]!.key;
|
||||
|
||||
const openPayment = await scene.begin(
|
||||
homeKey,
|
||||
forward(payment, layeredMotion),
|
||||
);
|
||||
await openPayment.finish({ commit: true, animate: false });
|
||||
|
||||
const confirmPayment = await scene.begin(
|
||||
openPayment.targetKey,
|
||||
replace(confirmation, layeredMotion),
|
||||
);
|
||||
await confirmPayment.finish({ commit: true, animate: false });
|
||||
await nextTick();
|
||||
|
||||
expect(
|
||||
scene.nodes.value.map((node) => [node.view.name, node.state]),
|
||||
).toEqual([
|
||||
["Home", "parked"],
|
||||
["Confirmation", "active"],
|
||||
]);
|
||||
expect(paymentUnmounts).toBe(1);
|
||||
|
||||
const confirmationContext = scene.contextFor(confirmPayment.targetKey);
|
||||
expect(confirmationContext.previous).toBe(home);
|
||||
expect(confirmationContext.history).toEqual([home]);
|
||||
|
||||
const returnHome = await scene.begin(
|
||||
confirmPayment.targetKey,
|
||||
back(layeredMotion),
|
||||
);
|
||||
expect(returnHome.targetKey).toBe(homeKey);
|
||||
await returnHome.finish({ commit: true, animate: false });
|
||||
expect(scene.nodes.value.map((node) => node.view.name)).toEqual(["Home"]);
|
||||
});
|
||||
|
||||
it("restores the current entry when an interactive replace is cancelled", async () => {
|
||||
const home = originView(component("Home"), undefined, { key: "home" });
|
||||
const payment = originView(component("Payment"), undefined, {
|
||||
key: "payment",
|
||||
});
|
||||
const confirmation = originView(component("Confirmation"), undefined, {
|
||||
key: "confirmation",
|
||||
});
|
||||
const scene = createOriginScene({ initial: home });
|
||||
const homeKey = scene.nodes.value[0]!.key;
|
||||
const openPayment = await scene.begin(
|
||||
homeKey,
|
||||
forward(payment, layeredMotion),
|
||||
);
|
||||
await openPayment.finish({ commit: true, animate: false });
|
||||
|
||||
const confirmPayment = await scene.begin(
|
||||
openPayment.targetKey,
|
||||
replace(confirmation, layeredMotion),
|
||||
);
|
||||
await confirmPayment.cancel({ animate: false });
|
||||
|
||||
expect(
|
||||
scene.nodes.value.map((node) => [node.view.name, node.state]),
|
||||
).toEqual([
|
||||
["Home", "parked"],
|
||||
["Payment", "active"],
|
||||
]);
|
||||
expect(scene.contextFor(openPayment.targetKey).history).toEqual([home]);
|
||||
});
|
||||
|
||||
it("can replace an initial root without manufacturing history", async () => {
|
||||
const scene = createOriginScene({
|
||||
initial: originView(component("Welcome"), undefined, { key: "welcome" }),
|
||||
});
|
||||
const welcomeKey = scene.nodes.value[0]!.key;
|
||||
const replacement = await scene.begin(
|
||||
welcomeKey,
|
||||
replace(
|
||||
originView(component("Signed in"), undefined, { key: "signed-in" }),
|
||||
layeredMotion,
|
||||
),
|
||||
);
|
||||
await replacement.finish({ commit: true, animate: false });
|
||||
|
||||
expect(scene.roots.value).toEqual([replacement.targetKey]);
|
||||
expect(scene.nodes.value.map((node) => node.view.name)).toEqual([
|
||||
"Signed in",
|
||||
]);
|
||||
expect(scene.contextFor(replacement.targetKey)).toMatchObject({
|
||||
canGoBack: false,
|
||||
history: [],
|
||||
previous: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("can replace a target while its incoming push is still settling", async () => {
|
||||
const home = originView(component("Home"), undefined, { key: "home" });
|
||||
const payment = originView(component("Payment"), undefined, {
|
||||
key: "payment",
|
||||
});
|
||||
const confirmation = originView(component("Confirmation"), undefined, {
|
||||
key: "confirmation",
|
||||
});
|
||||
const scene = createOriginScene({ initial: home });
|
||||
const homeKey = scene.nodes.value[0]!.key;
|
||||
const openPayment = await scene.begin(
|
||||
homeKey,
|
||||
forward(payment, layeredMotion),
|
||||
);
|
||||
openPayment.update(0.8, 1);
|
||||
|
||||
const paymentSettlement = openPayment.finish({ commit: true });
|
||||
const confirmPayment = await scene.begin(
|
||||
openPayment.targetKey,
|
||||
replace(confirmation, layeredMotion),
|
||||
);
|
||||
await confirmPayment.finish({ commit: true, animate: false });
|
||||
await paymentSettlement;
|
||||
|
||||
expect(scene.operations.value).toHaveLength(0);
|
||||
expect(
|
||||
scene.nodes.value.map((node) => [node.view.name, node.state]),
|
||||
).toEqual([
|
||||
["Home", "parked"],
|
||||
["Confirmation", "active"],
|
||||
]);
|
||||
expect(scene.contextFor(confirmPayment.targetKey).history).toEqual([home]);
|
||||
});
|
||||
|
||||
it("keeps a partial push connected at rest and restores it after cancelled back", async () => {
|
||||
const page = originView(component("Page"), undefined, { key: "page" });
|
||||
const drawer = originView(component("Drawer"), undefined, {
|
||||
key: "drawer",
|
||||
});
|
||||
const scene = createOriginScene({ initial: page });
|
||||
const pageKey = scene.nodes.value[0]!.key;
|
||||
|
||||
const openDrawer = await scene.begin(
|
||||
pageKey,
|
||||
forward(drawer, partialDrawerOpen),
|
||||
);
|
||||
await openDrawer.finish({ commit: true, animate: false });
|
||||
|
||||
expect(scene.operations.value).toHaveLength(0);
|
||||
expect(scene.roots.value).toEqual([pageKey]);
|
||||
expect(
|
||||
scene.nodes.value.map((node) => [node.view.name, node.state]),
|
||||
).toEqual([
|
||||
["Page", "exposed"],
|
||||
["Drawer", "active"],
|
||||
]);
|
||||
expect(scene.styleForNode(pageKey).transform).toBe("translateX(66.6667%)");
|
||||
expect(scene.styleForNode(pageKey).pointerEvents).toBe("none");
|
||||
expect(scene.styleForNode(openDrawer.targetKey).transform).toBe(
|
||||
"translateX(0%)",
|
||||
);
|
||||
|
||||
const closeDrawer = await scene.begin(
|
||||
openDrawer.targetKey,
|
||||
back(partialDrawerClose),
|
||||
);
|
||||
expect(scene.styleForNode(pageKey).transform).toBe("translateX(66.6667%)");
|
||||
await closeDrawer.cancel({ animate: false });
|
||||
|
||||
expect(scene.operations.value).toHaveLength(0);
|
||||
expect(scene.roots.value).toEqual([pageKey]);
|
||||
expect(
|
||||
scene.nodes.value.map((node) => [node.view.name, node.state]),
|
||||
).toEqual([
|
||||
["Page", "exposed"],
|
||||
["Drawer", "active"],
|
||||
]);
|
||||
expect(scene.styleForNode(pageKey).transform).toBe("translateX(66.6667%)");
|
||||
|
||||
const committedClose = await scene.begin(
|
||||
openDrawer.targetKey,
|
||||
back(partialDrawerClose),
|
||||
);
|
||||
await committedClose.finish({ commit: true, animate: false });
|
||||
|
||||
expect(scene.nodes.value.map((node) => [node.key, node.state])).toEqual([
|
||||
[pageKey, "active"],
|
||||
]);
|
||||
expect(scene.roots.value).toEqual([pageKey]);
|
||||
expect(scene.styleForNode(pageKey).transform).toBe("none");
|
||||
});
|
||||
|
||||
it("hands an immediate back gesture from a settling push to the same instances", async () => {
|
||||
const x = originView(component("X"), undefined, { key: "x" });
|
||||
const y = originView(component("Y"), undefined, { key: "y" });
|
||||
|
||||
@@ -155,6 +155,12 @@ export function createOriginScene(
|
||||
): OriginScene {
|
||||
const nodes = shallowReactive(new Map<string, SceneNodeState>());
|
||||
const operations = shallowReactive(new Map<number, MutableOriginOperation>());
|
||||
/*
|
||||
* A close gesture temporarily removes a connected resting edge to avoid a
|
||||
* visual cycle (page→drawer→page). If that close is cancelled, this map lets
|
||||
* us restore the exact same edge and component instances.
|
||||
*/
|
||||
const suspendedPresentations = new Map<number, MutableOriginOperation>();
|
||||
const roots = shallowRef<string[]>([]);
|
||||
const elements = new Map<string, HTMLElement>();
|
||||
let container: HTMLElement | null = null;
|
||||
@@ -222,6 +228,10 @@ export function createOriginScene(
|
||||
switch (action.history ?? "push") {
|
||||
case "push":
|
||||
return origin.key;
|
||||
case "replace":
|
||||
// Skip the origin in the retained chain. The old entry remains mounted
|
||||
// until commit so an interactive replacement can still be cancelled.
|
||||
return origin.previousNodeKey;
|
||||
case "back":
|
||||
return origin.previousNodeKey;
|
||||
}
|
||||
@@ -275,9 +285,7 @@ export function createOriginScene(
|
||||
|
||||
function visualEffects(nodeKey: string) {
|
||||
const result = inheritedEffects(nodeKey);
|
||||
const outgoing = [...operations.values()].find(
|
||||
(operation) => operation.originKey === nodeKey,
|
||||
);
|
||||
const outgoing = visualOutgoingFor(nodeKey);
|
||||
if (!outgoing) return result;
|
||||
const effects = effectSetFor(outgoing);
|
||||
return [
|
||||
@@ -287,6 +295,12 @@ export function createOriginScene(
|
||||
];
|
||||
}
|
||||
|
||||
function visualOutgoingFor(nodeKey: string) {
|
||||
return [...operations.values()].find(
|
||||
(operation) => operation.originKey === nodeKey,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse independent effect layers into one host style. Geometry remains
|
||||
* composable; arbitrary CSS properties use normal local-last precedence.
|
||||
@@ -350,13 +364,15 @@ export function createOriginScene(
|
||||
|
||||
function outgoingFor(nodeKey: string) {
|
||||
return [...operations.values()].find(
|
||||
(operation) => operation.originKey === nodeKey,
|
||||
(operation) =>
|
||||
operation.originKey === nodeKey && operation.phase !== "finished",
|
||||
);
|
||||
}
|
||||
|
||||
function isNodeInteractive(nodeKey: string) {
|
||||
const node = nodes.get(nodeKey);
|
||||
if (!node || node.state === "parked") return false;
|
||||
if (!node || node.state === "parked" || node.state === "exposed")
|
||||
return false;
|
||||
|
||||
// A target being cancelled is already scheduled to disappear. A source
|
||||
// settling toward commit has ceded new interactions to the retained scene
|
||||
@@ -407,9 +423,12 @@ export function createOriginScene(
|
||||
}
|
||||
|
||||
function refreshVisibleState(node: SceneNodeState) {
|
||||
if (node.state === "parked") return;
|
||||
if (node.state === "parked" || node.state === "exposed") return;
|
||||
const incoming = node.incomingOperationId
|
||||
? operations.get(node.incomingOperationId)
|
||||
: undefined;
|
||||
node.state =
|
||||
node.incomingOperationId || outgoingFor(node.key)
|
||||
(incoming && incoming.phase !== "finished") || outgoingFor(node.key)
|
||||
? "transitioning"
|
||||
: "active";
|
||||
}
|
||||
@@ -437,8 +456,9 @@ export function createOriginScene(
|
||||
/**
|
||||
* Finalize a committed operation.
|
||||
*
|
||||
* Push keeps the origin mounted and parks it. Back reuses the existing
|
||||
* previous node and removes only the entry being popped.
|
||||
* A normal push parks its origin and collapses the temporary visual edge.
|
||||
* A connected push keeps the edge at progress 1 and leaves the origin
|
||||
* exposed but inert. Replace and back remove the origin.
|
||||
*/
|
||||
function commitOperation(id: number) {
|
||||
const operation = operations.get(id);
|
||||
@@ -447,15 +467,28 @@ export function createOriginScene(
|
||||
const target = nodes.get(operation.targetKey);
|
||||
if (!origin || !target) return false;
|
||||
|
||||
if (operation.history === "push" && operation.choreography.persistAtRest) {
|
||||
operation.progress = 1;
|
||||
operation.velocity = 0;
|
||||
operation.intent = "commit";
|
||||
operation.phase = "finished";
|
||||
origin.state = "exposed";
|
||||
target.state = "active";
|
||||
refreshVisibleState(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
spliceVisualTarget(origin, target);
|
||||
operation.phase = "finished";
|
||||
operations.delete(operation.id);
|
||||
suspendedPresentations.delete(operation.id);
|
||||
|
||||
if (operation.history === "push") {
|
||||
origin.incomingOperationId = undefined;
|
||||
origin.state = "parked";
|
||||
} else {
|
||||
// A committed back pops only the current retained history entry.
|
||||
// Back pops the current entry; replace discards it in favor of the new
|
||||
// target. Both preserve every earlier retained instance.
|
||||
removeNode(origin.key);
|
||||
}
|
||||
|
||||
@@ -469,11 +502,22 @@ export function createOriginScene(
|
||||
if (!operation) return;
|
||||
const origin = nodes.get(operation.originKey);
|
||||
const target = nodes.get(operation.entryTargetKey);
|
||||
const suspendedPresentation = suspendedPresentations.get(id);
|
||||
operation.phase = "finished";
|
||||
operations.delete(id);
|
||||
suspendedPresentations.delete(id);
|
||||
|
||||
if (operation.history === "back") {
|
||||
if (target) {
|
||||
if (suspendedPresentation && origin && target) {
|
||||
/*
|
||||
* Put the retained page back into its former parent/root position, then
|
||||
* reconnect the settled page→drawer edge. Both nodes keep their VNodes.
|
||||
*/
|
||||
spliceVisualTarget(origin, target);
|
||||
operations.set(suspendedPresentation.id, suspendedPresentation);
|
||||
origin.incomingOperationId = suspendedPresentation.id;
|
||||
target.state = "exposed";
|
||||
} else if (target) {
|
||||
target.incomingOperationId = undefined;
|
||||
target.state = "parked";
|
||||
}
|
||||
@@ -539,15 +583,15 @@ export function createOriginScene(
|
||||
) {
|
||||
const operation = operations.get(id);
|
||||
if (!operation) return false;
|
||||
if (operation.phase === "finished") return operation.intent === "commit";
|
||||
|
||||
const threshold = operation.choreography.commitThreshold ?? 0.36;
|
||||
// const velocityThreshold = operation.choreography.commitVelocity ?? 0.9;
|
||||
const shouldCommit =
|
||||
options.commit ??
|
||||
(operation.progress >= threshold ||
|
||||
(operation.progress >= 0.00 &&
|
||||
operation.velocity >= 0.3));
|
||||
// operation.velocity >= velocityThreshold));
|
||||
(operation.progress >= 0.0 && operation.velocity >= 0.3));
|
||||
// operation.velocity >= velocityThreshold));
|
||||
|
||||
// The outcome is known synchronously at release. The target can therefore
|
||||
// originate another gesture while this operation is only visually settling.
|
||||
@@ -568,8 +612,10 @@ export function createOriginScene(
|
||||
const origin = nodes.get(originKey);
|
||||
if (!origin)
|
||||
throw new Error(`Cannot animate from missing origin "${originKey}".`);
|
||||
if (origin.state === "parked")
|
||||
throw new Error(`Cannot animate from parked origin "${originKey}".`);
|
||||
if (origin.state === "parked" || origin.state === "exposed")
|
||||
throw new Error(
|
||||
`Cannot animate from ${origin.state} origin "${originKey}".`,
|
||||
);
|
||||
if (outgoingFor(originKey))
|
||||
throw new Error(
|
||||
`Origin "${originKey}" already has an outgoing operation. ` +
|
||||
@@ -577,7 +623,12 @@ export function createOriginScene(
|
||||
);
|
||||
|
||||
const history = action.history ?? "push";
|
||||
if (action.choreography.persistAtRest && history !== "push")
|
||||
throw new Error(
|
||||
"persistAtRest choreographies require retained-history push navigation.",
|
||||
);
|
||||
let target: SceneNodeState;
|
||||
let suspendedPresentation: MutableOriginOperation | undefined;
|
||||
|
||||
if (history === "back") {
|
||||
const previousKey = origin.previousNodeKey;
|
||||
@@ -606,7 +657,22 @@ export function createOriginScene(
|
||||
throw new Error(
|
||||
"Cannot go back through an undecided forward operation.",
|
||||
);
|
||||
commitOperation(incoming.id);
|
||||
if (incoming.phase !== "finished") commitOperation(incoming.id);
|
||||
|
||||
const connected = operations.get(incoming.id);
|
||||
if (
|
||||
connected?.phase === "finished" &&
|
||||
connected.choreography.persistAtRest
|
||||
) {
|
||||
/*
|
||||
* Re-root A→page→drawer as A→drawer before adding drawer→page.
|
||||
* The reverse choreography starts at the same visual endpoints.
|
||||
*/
|
||||
operations.delete(connected.id);
|
||||
spliceVisualTarget(retainedTarget, origin);
|
||||
retainedTarget.incomingOperationId = undefined;
|
||||
suspendedPresentation = connected;
|
||||
}
|
||||
}
|
||||
|
||||
if (outgoingFor(retainedTarget.key))
|
||||
@@ -645,6 +711,8 @@ export function createOriginScene(
|
||||
originRect: elementRect(elements.get(originKey)),
|
||||
});
|
||||
operations.set(operation.id, operation);
|
||||
if (suspendedPresentation)
|
||||
suspendedPresentations.set(operation.id, suspendedPresentation);
|
||||
|
||||
// Let Vue mount the target before measuring it. Gesture composables buffer
|
||||
// pointer progress while this short preparation step is pending.
|
||||
@@ -682,7 +750,10 @@ export function createOriginScene(
|
||||
})) as readonly OriginSceneNode[],
|
||||
),
|
||||
operations: computed(
|
||||
() => [...operations.values()] as readonly OriginOperation[],
|
||||
() =>
|
||||
[...operations.values()].filter(
|
||||
(operation) => operation.phase !== "finished",
|
||||
) as readonly OriginOperation[],
|
||||
),
|
||||
roots,
|
||||
contextFor,
|
||||
|
||||
@@ -48,9 +48,11 @@ export type OriginOperationIntent = "undecided" | "commit" | "cancel";
|
||||
* Controls how an operation participates in retained instance history.
|
||||
*
|
||||
* - `push`: create a target whose previous entry is the mounted origin.
|
||||
* - `replace`: create a target that inherits the origin's previous entry, then
|
||||
* remove the origin on commit.
|
||||
* - `back`: reuse the mounted previous entry and pop the origin on commit.
|
||||
*/
|
||||
export type OriginHistoryMode = "push" | "back";
|
||||
export type OriginHistoryMode = "push" | "replace" | "back";
|
||||
|
||||
/**
|
||||
* The target's stacking relationship to its origin while an operation exists.
|
||||
@@ -62,9 +64,12 @@ export type OriginPlacement = "above" | "under";
|
||||
*
|
||||
* - `active`: currently exposed for normal interaction.
|
||||
* - `transitioning`: participating in at least one live operation edge.
|
||||
* - `exposed`: retained and visually presented behind/beside the active entry,
|
||||
* but inert.
|
||||
* - `parked`: retained in history but visually hidden and inert.
|
||||
*/
|
||||
export type OriginSceneNodeState = "active" | "transitioning" | "parked";
|
||||
export type OriginSceneNodeState =
|
||||
"active" | "transitioning" | "exposed" | "parked";
|
||||
|
||||
/** A rectangle measured in viewport CSS pixels. */
|
||||
export interface OriginRect {
|
||||
@@ -172,13 +177,27 @@ export interface OriginChoreography {
|
||||
* @defaultValue `0.9`
|
||||
*/
|
||||
readonly commitVelocity?: number;
|
||||
/**
|
||||
* Keep this choreography's progress-`1` effects connected after a committed
|
||||
* push instead of collapsing the edge and hiding its source.
|
||||
*
|
||||
* This is intended for partial presentations such as drawers and inspectors
|
||||
* where part of the retained source remains visible beside the target. The
|
||||
* source remains mounted, visible, and inert. A back operation temporarily
|
||||
* suspends the resting edge; cancelling back restores it.
|
||||
*
|
||||
* Only retained-history `push` actions support connected resting effects.
|
||||
*
|
||||
* @defaultValue `false`
|
||||
*/
|
||||
readonly persistAtRest?: boolean;
|
||||
}
|
||||
|
||||
/** A complete request to create and animate a target view from an origin. */
|
||||
export interface OriginAction {
|
||||
/**
|
||||
* Recipe for a newly created target. A back action omits this because the
|
||||
* scene resolves its already-mounted previous entry.
|
||||
* Recipe for a newly created push or replacement target. A back action omits
|
||||
* this because the scene resolves its already-mounted previous entry.
|
||||
*/
|
||||
readonly target?: OriginView;
|
||||
/** Visual relationship applied to the source, target, and shared frame. */
|
||||
@@ -206,8 +225,8 @@ export interface OriginAction {
|
||||
*/
|
||||
export interface OriginNavigationIntent {
|
||||
/**
|
||||
* Recipe for a newly created target. Back navigation omits this because the
|
||||
* scene resolves the retained previous instance.
|
||||
* Recipe for a newly created push or replacement target. Back navigation
|
||||
* omits this because the scene resolves the retained previous instance.
|
||||
*/
|
||||
readonly target?: OriginView;
|
||||
/** Target stacking relationship while the gesture operation is visible. */
|
||||
@@ -234,7 +253,11 @@ export interface OriginSceneNode {
|
||||
readonly previousNodeKey?: string;
|
||||
/** Current visibility/lifecycle role of this mounted instance. */
|
||||
readonly state: OriginSceneNodeState;
|
||||
/** Live operation currently positioning this node as its target. */
|
||||
/**
|
||||
* Operation edge positioning this node as its target. This may identify a
|
||||
* connected resting edge that is intentionally absent from live-operation
|
||||
* diagnostics.
|
||||
*/
|
||||
readonly incomingOperationId?: number;
|
||||
}
|
||||
|
||||
@@ -341,8 +364,8 @@ export interface OriginScene {
|
||||
* Create a forward target or reveal a retained back target, then return
|
||||
* manual control of its operation.
|
||||
*
|
||||
* @throws If the origin is missing, parked, or already owns an outgoing
|
||||
* operation.
|
||||
* @throws If the origin is missing, parked/exposed and inert, or already owns
|
||||
* an outgoing operation.
|
||||
*/
|
||||
begin(
|
||||
originKey: string,
|
||||
|
||||
Reference in New Issue
Block a user