Rewrite the core-v2 internals without changing the public API or runtime behavior. The four large files (types, scene, motion, gesture) become layered modules with one responsibility each: - shared/: geometry and environment helpers - view/: originView recipes - choreography/: effect types, presets, and composeEffects compositing - actions/: forward/back/replace/above/under via one overload resolver - scene/: SceneGraph storage, transitions (history rules), compositor (styles and interactivity), spring (pure stepper + rAF driver), and createOriginScene, which only wires them and owns async timing - composables/: useOrigin and the node-scope injection key - gesture/: builder, definition->policy normalization, start admission, DirectionalTracker, and a GestureRecognizer session state machine replacing fifteen loose closure variables - components/: renderers and gesture hosts, plus their prop types Verification (packages/core-v2/verify): differential fuzzing drives main and this branch with identical random operation sequences and pointer streams and requires identical scene state, styles, contexts, callbacks, and promise results at every step. It caught every planted mutation that applied. Benchmarks are at parity. Also fix a pre-existing test that called the defineProps macro inside a runtime defineComponent, and correct API.md's description of the flick commit threshold to match the runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Faarh2Ts6StJPcz2mRiHiR
339 lines
12 KiB
Markdown
339 lines
12 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.
|
||
|
||
## Source layout
|
||
|
||
The package is layered; each layer depends only on the ones above it.
|
||
|
||
| Directory | Responsibility |
|
||
| --------------- | ------------------------------------------------------------------------------------------------------- |
|
||
| `shared/` | Geometry and environment helpers |
|
||
| `view/` | `originView()` component recipes |
|
||
| `choreography/` | Effect types, `defineOriginChoreography()`, presets, and `composeEffects()` style compositing |
|
||
| `actions/` | `forward()`, `back()`, `replace()`, `above()`, `under()`, `originAction()` |
|
||
| `scene/` | `graph.ts` storage, `transitions.ts` history rules, `compositor.ts` styles, `spring.ts` settling motion |
|
||
| `composables/` | `useOrigin()` and the node-scope injection key |
|
||
| `gesture/` | Builder, definition→policy normalization, start admission, pointer tracking, and the recognizer |
|
||
| `components/` | `OriginScene`, `OriginGesture`, `OriginGestureSurface`, and the internal node host |
|
||
|
||
`createOriginScene()` wires the scene modules together: the graph holds nodes
|
||
and operation edges, transitions rewrite it synchronously when operations
|
||
begin, commit, or cancel, and the scene adds the asynchronous parts (waiting
|
||
for Vue to mount a target, measuring, and running the settling spring).
|
||
|
||
## 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.
|