Core v2: routeless origins
@native-vue-router/core-v2 is an experimental, Vue-only scene compositor. It
does not install Vue Router, resolve URLs, select a globally active route, or
render through RouterView.
The complete function, component, option, type, gesture-edge, and choreography reference is in API.md.
Install from Git
The repository root exposes this workspace package, runs its build during npm's Git-dependency preparation, and includes the generated JavaScript, declarations, and stylesheet:
npm install git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git
Pin applications to a commit or tag when reproducibility matters:
npm install "git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git#<commit-or-tag>"
Vue 3.5 or newer is the only peer dependency.
The Git-installed repository package includes a Codex integration skill at
skills/build-with-native-vue-router-v2. Copy that folder into
${CODEX_HOME:-$HOME/.codex}/skills and invoke it as
$build-with-native-vue-router-v2.
The primitive is:
A mounted component can originate a routine that creates another component, moves both components relative to the origin's coordinate frame, and retains the previous instance until a committed back operation pops the newer entry.
Run the experiment
From the workspace root:
npm run dev:v2
Open the printed URL to explore nine physical labs:
- a four-view chain that can keep four nodes and three edges live at once;
- one view with horizontal, vertical, and edge-only declarations;
- programmatic gallery navigation followed by gesture-owned traversal;
- a vertically presented media player with local interactive state;
- a chat that intentionally declares no back gesture;
- a predicate-gated downward gesture that drops a left-edge dialog;
- three nested scenes demonstrating cooperative carousels, vertical decks, and an intentional parent/child gesture conflict; and
- a checkout flow that replaces Payment Details with Confirmation and proves that back returns directly to the retained Hub instance; and
- a connected two-thirds drawer that keeps the translated source page visible in the exposed final third.
The expandable inspector reports mounted Vue instances, active operation edges, animation progress, and recent lifecycle events. In the chain lab, swipe rapidly through X → Y → Z → Ω to see all four components mounted while their independent frames are still moving.
An installable, offline-capable PWA build of the same experiment is hosted at https://v2.demo.native-router.harvmaster.com/.
Basic usage
Create a scene with a component recipe:
import { createOriginScene, originView } from "@native-vue-router/core-v2";
import "@native-vue-router/core-v2/style.css";
import HomeView from "./HomeView.vue";
export const scene = createOriginScene({
initial: originView(HomeView, { accountId: "42" }, { key: "home" }),
});
Render it:
<script setup lang="ts">
import { OriginScene } from "@native-vue-router/core-v2";
import { scene } from "./scene";
</script>
<template>
<OriginScene :scene="scene" />
</template>
Declare an interaction inside the component that should originate it:
<script setup lang="ts">
import {
OriginGesture,
forward,
gesture,
originView,
slideLeft,
} from "@native-vue-router/core-v2";
import ProfileView from "./ProfileView.vue";
const openProfile = gesture.to
.left()
.navigate(() =>
forward(originView(ProfileView, { userId: "7" }, { key: "profile-7" })),
)
.animate(slideLeft);
</script>
<template>
<OriginGesture :gesture="openProfile">
<main>Swipe this component left</main>
</OriginGesture>
</template>
Starting directly at .to.left() means pointer-down may occur anywhere on the
host. Add .from.left("clamp(24px, 8%, 64px)") before .to.right() for a
conventional proportional back edge, or .from.when(context => ...) for
arbitrary start policy. .complete() can override the choreography's release
thresholds.
There is no global navigation declaration. If this component should not
support that gesture, it simply does not render OriginGesture.
For an existing element where an additional wrapper is undesirable, use
useOriginGesture() and attach its four pointer handlers directly.
For several gestures on one page surface, keep the definitions in the page and pass them to the policy-neutral host:
<script setup lang="ts">
import { OriginGestureSurface } from "@native-vue-router/core-v2";
const gestures = [forwardGesture, backGesture] as const;
</script>
<template>
<OriginGestureSurface as="main" :gestures="gestures">
...
</OriginGestureSurface>
</template>
Going backward
Every pushed history entry remains mounted. Back resolves the already-mounted previous node and pops only the current entry after the operation commits:
import {
back,
gesture,
slideRight,
useOriginGesture,
} from "@native-vue-router/core-v2";
const goBack = useOriginGesture(
gesture.from
.left("max(24px, 6%)")
.to.right()
.navigate((context) => (context.canGoBack ? back() : null))
.animate(slideRight),
);
Parked entries are visually hidden, inert, and removed from pointer and accessibility interaction. Their Vue instances and DOM remain mounted, so component-local state and nested element scroll positions are preserved naturally. A cancelled back re-parks the previous target; a committed back unmounts the entry being left.
The application chooses whether this is exposed as a left-edge gesture, toolbar button, keyboard shortcut, Android hardware-back action, or not exposed at all.
Replacing the current entry
Use replace() for completed one-way flows such as Payment Details →
Confirmation:
import {
originView,
replace,
slideLeft,
useOrigin,
} from "@native-vue-router/core-v2";
import OrderConfirmationView from "./OrderConfirmationView.vue";
const origin = useOrigin();
function confirmOrder() {
return origin.perform(
replace(
originView(OrderConfirmationView, { orderId: "NVO-2048" }),
slideLeft,
),
);
}
The replacement inherits the current entry's mounted predecessor. It does not retain the entry being replaced, so back from Confirmation skips Payment Details. The mutation is atomic: a cancelled interactive replacement removes the proposed Confirmation and restores Payment Details unchanged.
Inside a gesture builder, omit choreography from the intent:
gesture.to
.left()
.navigate(() => replace(originView(OrderConfirmationView)))
.animate(slideLeft);
Custom choreography
A choreography returns independent effects for its source, target, and their shared frame:
import { defineOriginChoreography } from "@native-vue-router/core-v2";
export const zoomFromCard = defineOriginChoreography({
name: "zoom-from-card",
commitThreshold: 0.42,
effects: ({ progress, originRect, viewport }) => ({
source: {
transform: `scale(${1 - progress * 0.08})`,
opacity: 1 - progress * 0.4,
},
target: {
transform: `translateY(${(1 - progress) * viewport.height}px)`,
style: {
borderRadius: `${(1 - progress) * 24}px`,
},
},
}),
});
originRect, targetRect, and the scene viewport are measured after the
target mounts. The gesture may update progress interactively or a normal click
can call useOrigin().perform(action).
Transforms are concatenated from the oldest origin frame to the newest local
effect. Opacity is multiplied. Other properties in style use local-last
precedence. Consequently, if X→Y and Y→Z overlap:
Y transform = (X→Y target) × (Y→Z source)
Z transform = (X→Y target) × (Y→Z target)
For partial presentations that must keep both views visible after commit, set
persistAtRest: true on the opening choreography:
const openDrawer = defineOriginChoreography({
persistAtRest: true,
effects: ({ progress }) => ({
source: { transform: `translateX(${progress * 66.6667}%)` },
target: { transform: `translateX(${(progress - 1) * 66.6667}%)` },
}),
});
The retained source becomes visible-but-inert rather than parked. A reciprocal back choreography closes the target; a cancelled close restores the connected resting effects and both original Vue instances.
Why scene nodes are flat
The operation graph is not represented as Vue component ancestry. Every
component has one stable, keyed host directly under OriginScene.
If Y were physically moved from an X→Y wrapper to the scene root when an edge collapsed, Vue would unmount and recreate Y. Instead, v2 rewrites graph edges and recalculates Y's effect layers while every retained VNode stays in the same flat host.
“Y is Z's origin” is a coordinate and retained-history relationship, not Vue component ancestry.
Current experimental boundaries
- One operation creates one target. Chaining operations already permits any number of simultaneous scene nodes; multi-target routines are not yet exposed as a public builder.
- A node can originate one outgoing operation at a time. Its created target may immediately originate the next operation.
- The included pointer recognizer handles one primary pointer and one axis. Choreographies and scene operations are independent of it.
- Multiple recognizers can share a host and arbitrate by start policy and directional intent. A dedicated multi-pointer gesture arena is not exposed.
- Nested
OriginScenecomponents have independent history and measurements. Child recognizers currently claim propagation at pointer-down, so yielding a region to a parent requires an explicit.frompolicy. - Every pushed history entry remains mounted until back pops it. There is not yet an eviction policy, so applications should deliberately reset long-lived navigation contexts when that API is introduced.
- Parked instances remain mounted and ordinary Vue timers/watchers continue to run. Engine-specific park/resume lifecycle hooks are not exposed yet.
- Arbitrary CSS properties can be used, but only transforms and opacity have defined multi-operation composition rules at present.
These boundaries are explicit so the experiment can validate the origin primitive before compatibility conveniences become permanent architecture.