Compare commits

1 Commits

Author SHA1 Message Date
435ac9a899 Add core-v2 skill and make installable 2026-07-31 08:10:01 +00:00
8 changed files with 1005 additions and 57 deletions

View File

@@ -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.
## 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
- Interactive edge pop that can be held indefinitely at any progress.

404
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,36 @@
{
"name": "native-vue-router-workspace",
"name": "@native-vue-router/core-v2",
"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",
"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": [
"packages/*",
"apps/*"
@@ -20,12 +48,16 @@
"preview": "vite preview",
"electron": "npm run build && electron apps/electron/main.mjs",
"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/android": "^8.0.0",
"@capacitor/cli": "^8.0.0",
"@capacitor/core": "^8.0.0",
"@capacitor/haptics": "^8.0.0",
"@capacitor/ios": "^8.0.0",
"@capacitor/splash-screen": "^8.0.0",
"@capacitor/status-bar": "^8.0.0",
"@lucide/vue": "^1.24.0",
@@ -33,27 +65,22 @@
"@native-vue-router/core": "0.1.0",
"@native-vue-router/electron": "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",
"@tailwindcss/vite": "^4.3.2",
"@types/node": "^24.13.3",
"@vitejs/plugin-vue": "^6.0.7",
"@vue/tsconfig": "^0.9.1",
"electron": "^43.1.1",
"happy-dom": "^20.10.6",
"prettier": "^3.9.6",
"tailwindcss": "^4.3.2",
"tw-animate-css": "^1.4.0",
"typescript": "~6.0.2",
"vite": "^8.1.1",
"vite-plugin-pwa": "^1.1.0",
"vitest": "^3.2.4",
"vue": "^3.5.39",
"vue-router": "^5.0.6",
"vue-tsc": "^3.3.5"
}
}

View File

@@ -7,6 +7,29 @@ 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,

View File

@@ -3,6 +3,11 @@
"version": "0.1.0-experimental.0",
"type": "module",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git",
"directory": "packages/core-v2"
},
"files": [
"dist",
"API.md",

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

View File

@@ -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."

View File

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