319 lines
10 KiB
Markdown
319 lines
10 KiB
Markdown
# 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](./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:
|
||
|
||
```bash
|
||
npm install git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git
|
||
```
|
||
|
||
Pin applications to a commit or tag when reproducibility matters:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```sh
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```vue
|
||
<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:
|
||
|
||
```vue
|
||
<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:
|
||
|
||
```vue
|
||
<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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
gesture.to
|
||
.left()
|
||
.navigate(() => replace(originView(OrderConfirmationView)))
|
||
.animate(slideLeft);
|
||
```
|
||
|
||
## Custom choreography
|
||
|
||
A choreography returns independent effects for its source, target, and their
|
||
shared frame:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```ts
|
||
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 `OriginScene` components have independent history and measurements.
|
||
Child recognizers currently claim propagation at pointer-down, so yielding a
|
||
region to a parent requires an explicit `.from` policy.
|
||
- 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.
|