Add core-v2 skill and make installable
This commit is contained in:
49
README.md
49
README.md
@@ -4,6 +4,55 @@ Native Vue Router is a gesture-first navigation runtime for Vue 3 and Vue Router
|
|||||||
|
|
||||||
The repository includes a reusable headless core, a platform-adaptive visual preset, Capacitor and Electron adapters, and one messaging demo delivered as a PWA and through native hosts.
|
The repository includes a reusable headless core, a platform-adaptive visual preset, Capacitor and Electron adapters, and one messaging demo delivered as a PWA and through native hosts.
|
||||||
|
|
||||||
|
## Install the routeless core-v2 experiment
|
||||||
|
|
||||||
|
The repository root is an installable facade for
|
||||||
|
`@native-vue-router/core-v2`. npm builds the package from source when it installs
|
||||||
|
the Git dependency:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Import it by its package name and include the compositor stylesheet:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {
|
||||||
|
OriginScene,
|
||||||
|
createOriginScene,
|
||||||
|
gesture,
|
||||||
|
originView,
|
||||||
|
} from "@native-vue-router/core-v2";
|
||||||
|
import "@native-vue-router/core-v2/style.css";
|
||||||
|
```
|
||||||
|
|
||||||
|
Pin production applications to a commit or tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install "git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git#<commit-or-tag>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Core-v2 is routeless and does not depend on Vue Router. Its usage guide and API
|
||||||
|
are in [packages/core-v2/README.md](packages/core-v2/README.md) and
|
||||||
|
[packages/core-v2/API.md](packages/core-v2/API.md).
|
||||||
|
|
||||||
|
### Codex skill
|
||||||
|
|
||||||
|
The Git package also includes
|
||||||
|
[`build-with-native-vue-router-v2`](skills/build-with-native-vue-router-v2/SKILL.md),
|
||||||
|
a Codex skill for creating and integrating routeless core-v2 applications.
|
||||||
|
After installing the dependency, copy the skill into Codex's skill directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
|
||||||
|
cp -R \
|
||||||
|
node_modules/@native-vue-router/core-v2/skills/build-with-native-vue-router-v2 \
|
||||||
|
"${CODEX_HOME:-$HOME/.codex}/skills/"
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart or reload Codex if necessary, then invoke it explicitly with
|
||||||
|
`$build-with-native-vue-router-v2`.
|
||||||
|
|
||||||
## What works
|
## What works
|
||||||
|
|
||||||
- Interactive edge pop that can be held indefinitely at any progress.
|
- Interactive edge pop that can be held indefinitely at any progress.
|
||||||
|
|||||||
404
package-lock.json
generated
404
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
57
package.json
57
package.json
@@ -1,8 +1,36 @@
|
|||||||
{
|
{
|
||||||
"name": "native-vue-router-workspace",
|
"name": "@native-vue-router/core-v2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0-experimental.0",
|
||||||
|
"description": "Routeless, component-owned scene transitions and gesture recognition for Vue 3.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"packages/core-v2/dist",
|
||||||
|
"packages/core-v2/API.md",
|
||||||
|
"packages/core-v2/README.md",
|
||||||
|
"skills/build-with-native-vue-router-v2"
|
||||||
|
],
|
||||||
|
"main": "./packages/core-v2/dist/index.js",
|
||||||
|
"types": "./packages/core-v2/dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./packages/core-v2/dist/index.d.ts",
|
||||||
|
"import": "./packages/core-v2/dist/index.js"
|
||||||
|
},
|
||||||
|
"./style.css": "./packages/core-v2/dist/style.css",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"sideEffects": [
|
||||||
|
"./packages/core-v2/dist/style.css"
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*",
|
"packages/*",
|
||||||
"apps/*"
|
"apps/*"
|
||||||
@@ -20,12 +48,16 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"electron": "npm run build && electron apps/electron/main.mjs",
|
"electron": "npm run build && electron apps/electron/main.mjs",
|
||||||
"cap:sync": "npm run build && npm --prefix apps/capacitor exec cap sync",
|
"cap:sync": "npm run build && npm --prefix apps/capacitor exec cap sync",
|
||||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\""
|
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
|
||||||
|
"prepare": "npm run build --workspace packages/core-v2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/app": "^8.0.0",
|
"@capacitor/app": "^8.0.0",
|
||||||
|
"@capacitor/android": "^8.0.0",
|
||||||
|
"@capacitor/cli": "^8.0.0",
|
||||||
"@capacitor/core": "^8.0.0",
|
"@capacitor/core": "^8.0.0",
|
||||||
"@capacitor/haptics": "^8.0.0",
|
"@capacitor/haptics": "^8.0.0",
|
||||||
|
"@capacitor/ios": "^8.0.0",
|
||||||
"@capacitor/splash-screen": "^8.0.0",
|
"@capacitor/splash-screen": "^8.0.0",
|
||||||
"@capacitor/status-bar": "^8.0.0",
|
"@capacitor/status-bar": "^8.0.0",
|
||||||
"@lucide/vue": "^1.24.0",
|
"@lucide/vue": "^1.24.0",
|
||||||
@@ -33,27 +65,22 @@
|
|||||||
"@native-vue-router/core": "0.1.0",
|
"@native-vue-router/core": "0.1.0",
|
||||||
"@native-vue-router/electron": "0.1.0",
|
"@native-vue-router/electron": "0.1.0",
|
||||||
"@native-vue-router/preset-native": "0.1.0",
|
"@native-vue-router/preset-native": "0.1.0",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
|
||||||
"tailwindcss": "^4.3.2",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
|
||||||
"vite-plugin-pwa": "^1.1.0",
|
|
||||||
"vue": "^3.5.39",
|
|
||||||
"vue-router": "^5.0.6"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@capacitor/android": "^8.0.0",
|
|
||||||
"@capacitor/cli": "^8.0.0",
|
|
||||||
"@capacitor/ios": "^8.0.0",
|
|
||||||
"@playwright/test": "^1.55.0",
|
"@playwright/test": "^1.55.0",
|
||||||
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@types/node": "^24.13.3",
|
"@types/node": "^24.13.3",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.7",
|
||||||
"@vue/tsconfig": "^0.9.1",
|
"@vue/tsconfig": "^0.9.1",
|
||||||
"electron": "^43.1.1",
|
"electron": "^43.1.1",
|
||||||
"happy-dom": "^20.10.6",
|
"happy-dom": "^20.10.6",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
|
"tailwindcss": "^4.3.2",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"vite": "^8.1.1",
|
"vite": "^8.1.1",
|
||||||
|
"vite-plugin-pwa": "^1.1.0",
|
||||||
"vitest": "^3.2.4",
|
"vitest": "^3.2.4",
|
||||||
|
"vue": "^3.5.39",
|
||||||
|
"vue-router": "^5.0.6",
|
||||||
"vue-tsc": "^3.3.5"
|
"vue-tsc": "^3.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,29 @@ render through `RouterView`.
|
|||||||
The complete function, component, option, type, gesture-edge, and choreography
|
The complete function, component, option, type, gesture-edge, and choreography
|
||||||
reference is in [API.md](./API.md).
|
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:
|
The primitive is:
|
||||||
|
|
||||||
> A mounted component can originate a routine that creates another component,
|
> A mounted component can originate a routine that creates another component,
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
"version": "0.1.0-experimental.0",
|
"version": "0.1.0-experimental.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git",
|
||||||
|
"directory": "packages/core-v2"
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"API.md",
|
"API.md",
|
||||||
|
|||||||
124
skills/build-with-native-vue-router-v2/SKILL.md
Normal file
124
skills/build-with-native-vue-router-v2/SKILL.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
name: build-with-native-vue-router-v2
|
||||||
|
description: Build, integrate, migrate, extend, or debug Vue 3 applications using the routeless @native-vue-router/core-v2 scene, history, gesture-builder, and choreography APIs. Use for new or existing apps that need component-owned navigation, retained Vue instances, forward/back/replace flows, custom interactive transitions, nested scenes or carousels, connected partial drawers, sheets, dialogs, or native-app-like gesture ownership without Vue Router.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Build with Native Vue Router v2
|
||||||
|
|
||||||
|
Treat core-v2 as a routeless scene and gesture compositor. Views create other
|
||||||
|
views from their own mounted origin; there is no route table, URL coordinator,
|
||||||
|
`RouterView`, or globally current route.
|
||||||
|
|
||||||
|
Read [references/core-v2-patterns.md](references/core-v2-patterns.md) completely
|
||||||
|
before implementing or reviewing an integration. Treat that reference as the
|
||||||
|
baseline for `0.1.0-experimental.0`. When an installed commit differs, inspect
|
||||||
|
its exported declarations and package docs before relying on the reference.
|
||||||
|
|
||||||
|
## Inspect the target
|
||||||
|
|
||||||
|
Identify:
|
||||||
|
|
||||||
|
1. Package manager, Vue version, TypeScript settings, and application entry.
|
||||||
|
2. Existing navigation, gesture, animation, and component-state ownership.
|
||||||
|
3. Which mounted view should originate each destination and gesture.
|
||||||
|
4. Which entries must remain mounted for state or scroll restoration.
|
||||||
|
5. Which flows push, replace, go back, or remain partially connected.
|
||||||
|
6. Nested gesture owners such as carousels, maps, drawers, and editors.
|
||||||
|
7. Browser/PWA, Electron, Capacitor, reduced-motion, and accessibility needs.
|
||||||
|
|
||||||
|
Do not remove Vue Router from an existing app unless the user explicitly
|
||||||
|
authorizes that migration. Core-v2 can first own an isolated application
|
||||||
|
surface or nested feature. Do not mix core-v2 concepts with the legacy
|
||||||
|
`@native-vue-router/core` runtime.
|
||||||
|
|
||||||
|
## Install and establish the scene
|
||||||
|
|
||||||
|
Require Vue 3.5 or newer. Install from the Git repository, preferably pinned to
|
||||||
|
a commit or tag:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install "git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git#<commit-or-tag>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Import `@native-vue-router/core-v2/style.css` exactly once. Create an
|
||||||
|
`OriginScene` from an `originView()` recipe and render one `OriginScene`
|
||||||
|
component at the owning boundary.
|
||||||
|
|
||||||
|
Keep application data in normal Vue state, stores, or props. An `OriginView` is
|
||||||
|
a construction recipe; a scene node is the particular mounted instance.
|
||||||
|
|
||||||
|
## Declare interactions at their origins
|
||||||
|
|
||||||
|
Define available gestures and target factories inside the page or component
|
||||||
|
that owns the interaction:
|
||||||
|
|
||||||
|
- Use `forward()` to push a newly mounted history entry.
|
||||||
|
- Use `back()` to reveal the exact retained previous instance and pop the
|
||||||
|
current entry on commit.
|
||||||
|
- Use `replace()` for one-way completion flows that must skip the current entry
|
||||||
|
on future Back.
|
||||||
|
- Use `above()` and `under()` only for stacking; they do not imply direction.
|
||||||
|
- Use `origin.perform(action)` for buttons and commands.
|
||||||
|
- Use an intent without choreography in `.navigate()`, then attach visual
|
||||||
|
behavior with `.animate()`.
|
||||||
|
|
||||||
|
Prefer `OriginGesture` for one gesture and `OriginGestureSurface` for several.
|
||||||
|
The surface only forwards pointer events; all recognition, navigation, and
|
||||||
|
animation policy must remain in the owning component.
|
||||||
|
|
||||||
|
Start a builder at `.to` for an anywhere gesture. Add `.from.left()`,
|
||||||
|
`.from.right()`, `.from.top()`, `.from.bottom()`, or `.from.when()` only when
|
||||||
|
pointer-down eligibility is constrained. Use CSS percentages or `clamp()` for
|
||||||
|
responsive edge regions. Mark controls and nested interaction regions with
|
||||||
|
`data-origin-gesture="ignore"` when the parent recognizer must yield.
|
||||||
|
|
||||||
|
## Design choreography deliberately
|
||||||
|
|
||||||
|
Keep `effects()` deterministic and side-effect free. Compose geometry through
|
||||||
|
`frame`, `source`, and `target`; do not mutate application state from an
|
||||||
|
animation callback.
|
||||||
|
|
||||||
|
Use separate reciprocal opening and closing choreographies when their source
|
||||||
|
and target roles differ. Set `persistAtRest: true` only for a pushed partial
|
||||||
|
presentation whose progress-1 relationship must remain visible, such as a
|
||||||
|
two-thirds drawer. Ensure its back choreography begins at exactly the same
|
||||||
|
visual endpoints.
|
||||||
|
|
||||||
|
Use explicit `.complete(({ progress, velocity }) => ...)` policy when product
|
||||||
|
requirements depend on velocity. Avoid assuming a library default is the
|
||||||
|
application's UX policy.
|
||||||
|
|
||||||
|
## Preserve instance semantics
|
||||||
|
|
||||||
|
Every committed push retains the previous Vue instance until it is popped by
|
||||||
|
Back. Do not add `<KeepAlive>` around scene views or build a second cache.
|
||||||
|
`replace()` unmounts the replaced entry only after commit. A cancelled
|
||||||
|
operation restores its origin.
|
||||||
|
|
||||||
|
Parked and visually exposed retained entries are inert. Their normal Vue
|
||||||
|
effects and timers still run, so page components must pause expensive work
|
||||||
|
using ordinary app state when needed.
|
||||||
|
|
||||||
|
Create a nested `OriginScene` when a carousel, deck, or embedded flow needs its
|
||||||
|
own local component history. Define clear pointer-down regions between parent
|
||||||
|
and child recognizers; automatic gesture-arena arbitration is not available.
|
||||||
|
|
||||||
|
## Verify the result
|
||||||
|
|
||||||
|
Run the target formatter, type checker, tests, and production build. Exercise:
|
||||||
|
|
||||||
|
- Programmatic and gesture-driven push, Back, and cancellation.
|
||||||
|
- Replace followed by Back to prove the replaced entry cannot return.
|
||||||
|
- Rapid chained gestures while earlier springs are settling.
|
||||||
|
- Component identity, form state, and nested scroll restoration.
|
||||||
|
- Edge sizing at desktop and mobile widths.
|
||||||
|
- Nested gesture conflicts and ignored controls.
|
||||||
|
- Connected partial presentation open, cancelled close, and committed close.
|
||||||
|
- Reduced motion, keyboard controls, focus isolation, and screen-reader
|
||||||
|
isolation.
|
||||||
|
- Browser console errors and scene diagnostics during held gestures.
|
||||||
|
|
||||||
|
Report the component ownership model, history behavior, gestures and
|
||||||
|
choreographies added, retained-instance implications, and verification results.
|
||||||
|
Call out URL/hardware-back integration as application-host work rather than
|
||||||
|
claiming core-v2 provides it.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Build with Native Vue Router v2"
|
||||||
|
short_description: "Build routeless Vue apps with core-v2"
|
||||||
|
default_prompt: "Use $build-with-native-vue-router-v2 to build this Vue app with routeless, component-owned navigation."
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user