Files

319 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.