Files

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