10 KiB
Core-v2 integration patterns
Contents
- Install and bootstrap
- Mental model
- Actions and retained history
- Gesture builder
- Custom choreography
- Connected partial presentations
- Nested scenes
- Constraints and verification
Install and bootstrap
Install the Git package. Pin a commit or tag for reproducible applications:
npm install "git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git#<commit-or-tag>"
The unpinned default-branch form is useful during active development:
npm install git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git
The installed package name is @native-vue-router/core-v2. It has one peer
dependency: Vue ^3.5.0. It does not depend on Vue Router.
Create the scene:
// src/scene.ts
import { createOriginScene, originView } from "@native-vue-router/core-v2";
import HomeView from "./views/HomeView.vue";
export const scene = createOriginScene({
initial: originView(
HomeView,
{ accountId: "42" },
{ key: "home", name: "Home" },
),
});
Render it:
<script setup lang="ts">
import { OriginScene } from "@native-vue-router/core-v2";
import "@native-vue-router/core-v2/style.css";
import { scene } from "./scene";
</script>
<template>
<main class="app-shell">
<OriginScene :scene="scene" />
</main>
</template>
<style>
html,
body,
#app,
.app-shell {
width: 100%;
height: 100%;
margin: 0;
}
</style>
The scene needs a definite width and height. Its view hosts are stable, absolutely positioned siblings.
Mental model
OriginView: immutable recipe containing a Vue component, props, and optional diagnostic key/name.- Scene node: one mounted instance created from a recipe.
- Operation edge: temporary source→target relationship with progress, velocity, choreography, and commit/cancel outcome.
- Origin: the mounted component that handled the event. There is no global current view.
- Retained history: linked mounted instances, independent of URLs.
A newly created target can originate another operation while its incoming operation settles. Visual transforms compose by origin frame:
visual(Y) = X→Y.target × Y→Z.source
visual(Z) = X→Y.target × Y→Z.target
Actions and retained history
Helpers are overloaded:
- Without choreography they return
OriginNavigationIntentfor a gesture builder. - With choreography they return a complete
OriginActionforbegin()orperform().
Forward
const profileView = () =>
originView(
ProfileView,
{ userId: "7" },
{ key: "profile-7", name: "Profile" },
);
await origin.perform(forward(profileView(), slideLeft));
Commit parks but retains the origin. Back later restores that exact instance, including local state and DOM scroll position.
Back
if (origin.context.value.canGoBack) {
await origin.perform(back(slideRight));
}
Back has no target recipe. It resolves previousNodeKey, animates the retained
instance, and unmounts the current entry only on commit.
Useful context:
const origin = useOrigin();
origin.nodeKey;
origin.context.value.view;
origin.context.value.previous;
origin.context.value.history;
origin.context.value.canGoBack;
Replace
await origin.perform(
replace(
originView(
OrderConfirmationView,
{ orderId: "NVO-2048" },
{ key: "confirmation", name: "Confirmation" },
),
slideLeft,
),
);
Replace creates a target linked to the current origin's predecessor. Commit unmounts the current entry; cancellation removes the proposed replacement and restores the current entry. Back from the replacement skips the removed view.
Placement
above() and under() select stacking, not direction or history:
above(originView(DialogView), fade);
under(originView(BackdropView), reveal);
Prefer forward, back, and replace when expressing history semantics.
Gesture builder
Declare an anywhere forward gesture and responsive edge Back:
<script setup lang="ts">
import {
OriginGestureSurface,
back,
forward,
gesture,
originView,
slideLeft,
slideRight,
useOrigin,
} from "@native-vue-router/core-v2";
import DetailsView from "./DetailsView.vue";
const origin = useOrigin();
const detailsView = () =>
originView(DetailsView, undefined, {
key: "details",
name: "Details",
});
const openDetails = gesture.to
.left()
.navigate(() => forward(detailsView()))
.animate(slideLeft);
const goBack = gesture.from
.left("clamp(24px, 8%, 64px)")
.to.right()
.navigate((context) => (context.canGoBack ? back() : null))
.animate(slideRight);
const gestures = [openDetails, goBack] as const;
</script>
<template>
<OriginGestureSurface as="main" :gestures="gestures">
<button
type="button"
data-origin-gesture="ignore"
@click="origin.perform(forward(detailsView(), slideLeft))"
>
Open details
</button>
</OriginGestureSurface>
</template>
Builder stages:
- Optional
.from: pointer-down eligibility. .to: direction recognition and axis lock.- Optional
.complete: release policy. .navigate: destination/history intent..animate: visual choreography.
Starting at .to permits pointer-down anywhere. Edge distances accept CSS
pixels, percentages, calc(), and clamp(). Use .from.when(context => ...)
for arbitrary shapes, safe areas, or exclusion regions.
Set explicit completion policy for product-specific velocity behavior:
const open = gesture.to
.left()
.complete(
({ progress, velocity }) =>
progress >= 0.38 || (progress >= 0.06 && velocity >= 0.85),
)
.navigate(() => forward(detailsView()))
.animate(slideLeft);
Use OriginGesture when only one definition owns the host:
<OriginGesture :gesture="goBack">
<article>...</article>
</OriginGesture>
Custom choreography
Effects are recomputed often. Keep them pure:
import { defineOriginChoreography } from "@native-vue-router/core-v2";
const percent = (value: number) => `${value * 100}%`;
export const focusCard = defineOriginChoreography({
name: "focus-card",
commitThreshold: 0.36,
effects: ({ progress }) => ({
frame: {
transform: `translateY(${-Math.sin(progress * Math.PI) * 8}px)`,
},
source: {
transform: `scale(${1 - progress * 0.08})`,
opacity: 1 - progress * 0.45,
},
target: {
transform: `translateY(${percent((1 - progress) * 0.1)}) scale(${
0.82 + progress * 0.18
})`,
opacity: progress,
style: {
borderRadius: `${(1 - progress) * 28}px`,
},
},
}),
});
frame affects both sides and descendants. Transforms concatenate, opacities
multiply, and other style properties use local-last precedence.
Choreography context includes progress, velocity, phase, intent, measured origin/target rectangles, and scene viewport.
Connected partial presentations
Normal forward commit removes its operation edge and parks the retained source.
Set persistAtRest: true when progress-1 effects must remain connected:
export const openDrawer = defineOriginChoreography({
name: "open-two-thirds-drawer",
persistAtRest: true,
effects: ({ progress }) => ({
source: {
transform: `translateX(${progress * 66.6667}%)`,
},
target: {
transform: `translateX(${(progress - 1) * 66.6667}%)`,
},
}),
});
export const closeDrawer = defineOriginChoreography({
name: "close-two-thirds-drawer",
effects: ({ progress }) => ({
source: {
transform: `translateX(${-progress * 66.6667}%)`,
},
target: {
transform: `translateX(${(1 - progress) * 66.6667}%)`,
},
}),
});
Open with a retained push:
forward(originView(DrawerView), openDrawer);
The source becomes exposed: mounted and visible, but inert and
aria-hidden. The drawer is active. Back temporarily suspends the settled
opening edge and uses the close choreography. A cancelled Back restores the
opening relationship; committed Back removes the drawer.
Requirements:
- Use
persistAtRestonly with push history. - Make close progress 0 visually identical to open progress 1.
- Put a transparent interactive region in the target if tapping the exposed source area should dismiss; the source itself is intentionally inert.
- Do not treat settled connected edges as live animations in diagnostics.
Nested scenes
Create another scene when an embedded component needs independent history:
const carouselScene = createOriginScene({
initial: originView(CarouselSlide, { index: 0 }),
});
Render <OriginScene :scene="carouselScene" /> inside the parent view. The
nearest injected node scope owns useOrigin() and gestures.
Current parent/child ownership is selected at pointer-down. Give the child a
non-overlapping .from region when the parent must retain fallback behavior.
Do not assume a child can decline after capture and automatically hand the same
pointer sequence to its parent.
Constraints and verification
- No URL, deep-link, route-param, or browser-history model is included. Model data as props/state and bridge host Back separately.
- No global active view exists. The component receiving the event is the origin.
- Pushed entries remain mounted until Back pops them. There is no eviction policy yet.
- Parked/exposed entries are inert, but their Vue watchers, timers, and effects continue running.
- One origin owns at most one outgoing operation; a target may originate the next operation once its incoming intent commits.
- Gesture recognition handles one primary pointer and one axis.
- Nested scenes have separate history and measurements.
KeepAliveis not the scene cache and should not wrap scene navigation.- Reduced-motion preference settles operations immediately.
Verify scene diagnostics and DOM behavior:
scene.nodes.value;
scene.operations.value;
scene.contextFor(nodeKey);
scene.styleForNode(nodeKey);
Test committed and cancelled operations, component mount/unmount counts, retained DOM identity, scroll position, rapid chained operations, replace history, connected presentation cancellation, responsive edge regions, focus, inert/ARIA behavior, and production bundling.