397 lines
10 KiB
Markdown
397 lines
10 KiB
Markdown
# Core-v2 integration patterns
|
||
|
||
## Contents
|
||
|
||
1. [Install and bootstrap](#install-and-bootstrap)
|
||
2. [Mental model](#mental-model)
|
||
3. [Actions and retained history](#actions-and-retained-history)
|
||
4. [Gesture builder](#gesture-builder)
|
||
5. [Custom choreography](#custom-choreography)
|
||
6. [Connected partial presentations](#connected-partial-presentations)
|
||
7. [Nested scenes](#nested-scenes)
|
||
8. [Constraints and verification](#constraints-and-verification)
|
||
|
||
## Install and bootstrap
|
||
|
||
Install the Git package. Pin a commit or tag for reproducible applications:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```ts
|
||
// 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:
|
||
|
||
```vue
|
||
<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:
|
||
|
||
```text
|
||
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 `OriginNavigationIntent` for a gesture
|
||
builder.
|
||
- With choreography they return a complete `OriginAction` for `begin()` or
|
||
`perform()`.
|
||
|
||
### Forward
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
const origin = useOrigin();
|
||
|
||
origin.nodeKey;
|
||
origin.context.value.view;
|
||
origin.context.value.previous;
|
||
origin.context.value.history;
|
||
origin.context.value.canGoBack;
|
||
```
|
||
|
||
### Replace
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```vue
|
||
<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:
|
||
|
||
1. Optional `.from`: pointer-down eligibility.
|
||
2. `.to`: direction recognition and axis lock.
|
||
3. Optional `.complete`: release policy.
|
||
4. `.navigate`: destination/history intent.
|
||
5. `.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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```vue
|
||
<OriginGesture :gesture="goBack">
|
||
<article>...</article>
|
||
</OriginGesture>
|
||
```
|
||
|
||
## Custom choreography
|
||
|
||
Effects are recomputed often. Keep them pure:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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 `persistAtRest` only 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:
|
||
|
||
```ts
|
||
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.
|
||
- `KeepAlive` is not the scene cache and should not wrap scene navigation.
|
||
- Reduced-motion preference settles operations immediately.
|
||
|
||
Verify scene diagnostics and DOM behavior:
|
||
|
||
```ts
|
||
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.
|