V2: Origin based animations, Gesture Builder, New Demo, non-url-based-routing. Massive improvements.
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
OriginGesture,
|
||||
OriginScene,
|
||||
back,
|
||||
createOriginScene,
|
||||
gesture,
|
||||
originView,
|
||||
useOrigin,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import { unfocusPortal } from "../motions";
|
||||
import NestedSceneSlide from "./NestedSceneSlide.vue";
|
||||
|
||||
type NestedSceneKind = "cooperative" | "vertical" | "conflict";
|
||||
|
||||
interface SlideCopy {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
accent: string;
|
||||
}
|
||||
|
||||
const slideSets: Record<NestedSceneKind, readonly SlideCopy[]> = {
|
||||
cooperative: [
|
||||
{
|
||||
eyebrow: "Cooperative carousel",
|
||||
title: "A scene inside a page",
|
||||
description:
|
||||
"Swipe left away from the glowing pass-through rail. The target mounts only inside this clipped carousel.",
|
||||
accent: "#76dcff",
|
||||
},
|
||||
{
|
||||
eyebrow: "Retained nested history",
|
||||
title: "Slide one is still mounted",
|
||||
description:
|
||||
"Increment local state, move again, then return. The same nested Vue instances are revealed.",
|
||||
accent: "#a78bfa",
|
||||
},
|
||||
{
|
||||
eyebrow: "Independent operation graph",
|
||||
title: "Three carousel instances",
|
||||
description:
|
||||
"This history belongs to the carousel scene. The outer page remains one mounted application entry.",
|
||||
accent: "#65f2b1",
|
||||
},
|
||||
],
|
||||
vertical: [
|
||||
{
|
||||
eyebrow: "Vertical nested deck",
|
||||
title: "Pull upward inside this card",
|
||||
description:
|
||||
"The deck uses vertical progress, rotation, and a shared sideways frame arc.",
|
||||
accent: "#ffbd70",
|
||||
},
|
||||
{
|
||||
eyebrow: "Orthogonal choreography",
|
||||
title: "The page still owns its left rail",
|
||||
description:
|
||||
"Pull down to reveal the retained card, or begin on its pass-through rail and swipe right to leave the lab.",
|
||||
accent: "#ff7f9f",
|
||||
},
|
||||
{
|
||||
eyebrow: "Nested composition",
|
||||
title: "Frames remain local",
|
||||
description:
|
||||
"Transforms compose exactly like the full-screen scene, but measurements come from this smaller viewport.",
|
||||
accent: "#ffe079",
|
||||
},
|
||||
],
|
||||
conflict: [
|
||||
{
|
||||
eyebrow: "Intentional conflict",
|
||||
title: "This child claims every pointer-down",
|
||||
description:
|
||||
"Try the page's rightward back gesture over this card. The child sees it first, even though nested back is unavailable.",
|
||||
accent: "#ff5f78",
|
||||
},
|
||||
{
|
||||
eyebrow: "Child-owned back",
|
||||
title: "Now rightward swipe works here",
|
||||
description:
|
||||
"After moving forward, the exact same gesture can resolve nested back, producing the cube reversal.",
|
||||
accent: "#ff8d4d",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function nestedView(kind: NestedSceneKind, index: number) {
|
||||
const slides = slideSets[kind];
|
||||
const copy = slides[index]!;
|
||||
return originView(
|
||||
NestedSceneSlide,
|
||||
{
|
||||
kind,
|
||||
index,
|
||||
total: slides.length,
|
||||
...copy,
|
||||
next:
|
||||
index + 1 < slides.length
|
||||
? () => nestedView(kind, index + 1)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
key: `${kind}-slide-${index + 1}`,
|
||||
name: `${kind} slide ${index + 1}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function nestedScene(kind: NestedSceneKind) {
|
||||
return createOriginScene({ initial: nestedView(kind, 0) });
|
||||
}
|
||||
|
||||
interface NestedCase {
|
||||
kind: NestedSceneKind;
|
||||
status: "supported" | "supported-with-policy" | "known-conflict";
|
||||
title: string;
|
||||
summary: string;
|
||||
expected: string;
|
||||
issue: string;
|
||||
scene: ReturnType<typeof createOriginScene>;
|
||||
}
|
||||
|
||||
const cases: readonly NestedCase[] = [
|
||||
{
|
||||
kind: "cooperative",
|
||||
status: "supported-with-policy",
|
||||
title: "Reserved-edge carousel",
|
||||
summary:
|
||||
"Horizontal nested history with a start predicate that leaves a clamp(22px, 12%, 52px) rail for its parent.",
|
||||
expected:
|
||||
"Swipe left or right beyond the rail; swipe right from the card's rail to trigger parent back.",
|
||||
issue:
|
||||
"The parent/child agreement is spatial policy written by the developer, not automatic arbitration.",
|
||||
scene: nestedScene("cooperative"),
|
||||
},
|
||||
{
|
||||
kind: "vertical",
|
||||
status: "supported",
|
||||
title: "Vertical component deck",
|
||||
summary:
|
||||
"A second nested scene uses upward/downward gestures and an entirely different frame animation.",
|
||||
expected:
|
||||
"Swipe up for the next card, down for retained back, and swipe right from its rail for parent back.",
|
||||
issue:
|
||||
"Its touch-action prevents page scrolling while a touch begins over the deck, which is appropriate here but must be intentional.",
|
||||
scene: nestedScene("vertical"),
|
||||
},
|
||||
{
|
||||
kind: "conflict",
|
||||
status: "known-conflict",
|
||||
title: "Greedy child recognizer",
|
||||
summary:
|
||||
"The cube carousel deliberately uses `.to.left()` and `.to.right()` without a `.from` constraint.",
|
||||
expected: "Nested forward/back works and remains clipped to the card.",
|
||||
issue:
|
||||
"On slide one, parent back fails everywhere over this card: the child stops pointer-down propagation before `.navigate()` returns null.",
|
||||
scene: nestedScene("conflict"),
|
||||
},
|
||||
];
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance("Nested scenes");
|
||||
|
||||
const pageBackGesture = gesture.to
|
||||
.right()
|
||||
.complete(
|
||||
({ distance, bounds, progress, velocity }) =>
|
||||
distance >= bounds.width * 0.08 || progress >= 0.22 || velocity >= 0.65,
|
||||
)
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(unfocusPortal);
|
||||
|
||||
function backWithButton() {
|
||||
if (origin.context.value.canGoBack) void origin.perform(back(unfocusPortal));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGesture class="lab-page nested-scenes-view" :gesture="pageBackGesture">
|
||||
<button
|
||||
class="view-back"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
@click="backWithButton"
|
||||
>
|
||||
← Labs
|
||||
</button>
|
||||
|
||||
<header class="nested-scenes-header">
|
||||
<div>
|
||||
<div class="eyebrow">Lab 07 · Nested origin scenes</div>
|
||||
<h1>Components can own smaller worlds.</h1>
|
||||
<p>
|
||||
Each card below contains a completely independent
|
||||
<code>OriginScene</code>. Its views, history, gestures, measurements,
|
||||
and animations stay inside that card while this page remains the outer
|
||||
origin.
|
||||
</p>
|
||||
</div>
|
||||
<div class="nested-page-instance">
|
||||
<span>Outer page instance</span>
|
||||
<strong>{{ identity.instance }}</strong>
|
||||
<small>rightward 8% → page back</small>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="nested-case-grid">
|
||||
<section
|
||||
v-for="item in cases"
|
||||
:key="item.kind"
|
||||
class="nested-case"
|
||||
:class="`nested-case--${item.kind}`"
|
||||
>
|
||||
<header>
|
||||
<span class="nested-case__status" :class="item.status">
|
||||
{{ item.status.replaceAll("-", " ") }}
|
||||
</span>
|
||||
<h2>{{ item.title }}</h2>
|
||||
<p>{{ item.summary }}</p>
|
||||
</header>
|
||||
|
||||
<div class="nested-stage-shell">
|
||||
<OriginScene class="nested-stage" :scene="item.scene" />
|
||||
<div class="nested-stage-edge" aria-hidden="true">
|
||||
{{
|
||||
item.kind === "conflict"
|
||||
? "parent blocked"
|
||||
: "parent pass-through"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nested-case__diagnostics">
|
||||
<span>
|
||||
{{ item.scene.nodes.value.length }} nested node{{
|
||||
item.scene.nodes.value.length === 1 ? "" : "s"
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{ item.scene.operations.value.length }} live edge{{
|
||||
item.scene.operations.value.length === 1 ? "" : "s"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Expected</dt>
|
||||
<dd>{{ item.expected }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>
|
||||
{{ item.kind === "conflict" ? "Predicted failure" : "Caveat" }}
|
||||
</dt>
|
||||
<dd>{{ item.issue }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="nested-scenes-footer">
|
||||
<strong>Ownership today is decided at pointer-down.</strong>
|
||||
<span>
|
||||
A future gesture arena could delay that decision and let a parent claim
|
||||
the sequence when a child declines navigation.
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
<div class="nested-page-gesture">
|
||||
Parent gesture: swipe right outside a child, or from a cooperative rail
|
||||
</div>
|
||||
</OriginGesture>
|
||||
</template>
|
||||
Reference in New Issue
Block a user