V2: Origin based animations, Gesture Builder, New Demo, non-url-based-routing. Massive improvements.

This commit is contained in:
2026-07-25 05:57:26 +00:00
parent 5a514906eb
commit 55dad11b25
49 changed files with 8885 additions and 1 deletions
@@ -0,0 +1,166 @@
<script setup lang="ts">
import { ref } from "vue";
import {
back,
forward,
gesture,
OriginGestureSurface,
useOrigin,
type OriginChoreography,
type OriginGestureDirectedBuilder,
type OriginView,
} from "@native-vue-router/core-v2";
import InstanceCard from "../components/InstanceCard.vue";
import { useDemoInstance } from "../lab-state";
import {
nestedCarouselBack,
nestedCarouselForward,
nestedCubeBack,
nestedCubeForward,
nestedDeckBack,
nestedDeckForward,
} from "../motions";
type NestedSceneKind = "cooperative" | "vertical" | "conflict";
const props = defineProps<{
kind: NestedSceneKind;
index: number;
total: number;
eyebrow: string;
title: string;
description: string;
accent: string;
next?: () => OriginView;
}>();
const origin = useOrigin();
const identity = useDemoInstance(`Nested ${props.kind} ${props.index + 1}`);
const localLikes = ref(0);
const vertical = props.kind === "vertical";
const forwardMotion: OriginChoreography = vertical
? nestedDeckForward
: props.kind === "conflict"
? nestedCubeForward
: nestedCarouselForward;
const backMotion: OriginChoreography = vertical
? nestedDeckBack
: props.kind === "conflict"
? nestedCubeBack
: nestedCarouselBack;
/*
* Cooperative cases deliberately leave the outer page's first 44 pixels
* untouched. The conflict case starts anywhere, so its pointer-down handler
* stops propagation before it knows whether `back()` is available.
*/
function movementBuilder(
direction: "forward" | "back",
): OriginGestureDirectedBuilder {
const to = vertical
? direction === "forward"
? "up"
: "down"
: direction === "forward"
? "left"
: "right";
const start =
props.kind === "conflict"
? gesture.to
: gesture.from.when(({ point, bounds }) => {
const parentRail = Math.min(52, Math.max(22, bounds.width * 0.12));
return point.localX > parentRail;
}).to;
return start[to]();
}
const forwardGesture = movementBuilder("forward")
.navigate(() => {
const target = props.next?.();
return target ? forward(target) : null;
})
.animate(forwardMotion);
const backGesture = movementBuilder("back")
.navigate((context) => (context.canGoBack ? back() : null))
.animate(backMotion);
const gestures = [forwardGesture, backGesture] as const;
function nextWithButton() {
const target = props.next?.();
if (target) void origin.perform(forward(target, forwardMotion));
}
function backWithButton() {
if (origin.context.value.canGoBack) void origin.perform(back(backMotion));
}
</script>
<template>
<OriginGestureSurface
as="article"
class="nested-slide"
:class="`nested-slide--${kind}`"
:data-nested-kind="kind"
:data-nested-index="index"
:style="{ '--nested-accent': accent }"
:gestures="gestures"
>
<div class="nested-slide__orb" aria-hidden="true" />
<header>
<span>{{ eyebrow }}</span>
<strong>{{ index + 1 }} / {{ total }}</strong>
</header>
<div class="nested-slide__copy">
<h3>{{ title }}</h3>
<p>{{ description }}</p>
</div>
<div class="nested-slide__state">
<InstanceCard
label="Nested instance"
:instance="identity.instance"
:status="identity.status.value"
/>
<button
type="button"
data-origin-gesture="ignore"
@click="localLikes += 1"
>
Local state · {{ localLikes }}
</button>
</div>
<footer>
<button
type="button"
data-origin-gesture="ignore"
:disabled="!origin.canGoBack.value"
@click="backWithButton"
>
{{ vertical ? "↓ Previous" : "← Previous" }}
</button>
<div class="nested-slide__dots" aria-label="Slide position">
<span
v-for="dot in total"
:key="dot"
:class="{ active: dot === index + 1 }"
/>
</div>
<button
type="button"
data-origin-gesture="ignore"
:disabled="!next"
@click="nextWithButton"
>
{{ vertical ? "Next ↑" : "Next →" }}
</button>
</footer>
</OriginGestureSurface>
</template>