Add experiments folder. Add usage.md and SKILL.md. Fix Sheet interactions and add dynamic sheet mode.

This commit is contained in:
2026-07-22 07:04:39 +00:00
parent bfe364c57d
commit 6aed7606ad
48 changed files with 5454 additions and 84 deletions

View File

@@ -0,0 +1,204 @@
---
name: integrate-native-vue-router
description: Integrate, migrate, configure, or debug Native Vue Router in new or existing Vue 3 applications. Use when adding @native-vue-router/core, converting a Vue Router app to native route surfaces, defining parent or sibling topology, adding gesture links and modal/sheet navigation, handling cached-view lifecycle, composing Vue built-ins with NativeRouterView, creating custom presentations, or wiring Capacitor and Electron adapters.
---
# Integrate Native Vue Router
Implement Native Vue Router as a visual transaction layer around Vue Router.
Keep Vue Router authoritative for committed URLs, history, guards, redirects, and
route matching.
Read [references/integration-reference.md](references/integration-reference.md)
before changing a project. Treat it as the API baseline for version `0.1.x`. If
the installed package version or local source differs, inspect that version's
`package.json`, exported declarations, and source before editing.
## Choose the adoption path
Determine whether the target is:
- A new Vue application: establish the native route shell and topology while
creating the router.
- An existing Vue Router application: preserve its history mode, routes,
guards, redirects, nested outlets, state, and deep links while migrating the
visual root and selected navigation calls.
- A host integration: add the core first, then add Capacitor or Electron as an
adapter at the application boundary.
- A targeted enhancement: adopt imperative animated navigation first and defer
predictive gestures or sibling paging.
Do not widen the task into a framework upgrade without user authorization. If
the project is below Vue 3.5 or Vue Router 5, report the compatibility gap and
the exact upgrade it requires before changing dependencies.
## Inspect the target
Before editing, identify:
1. Package manager, workspace layout, Vue version, and Vue Router version.
2. The router creation file, history implementation, route records, guards,
redirects, scroll behavior, and lazy components.
3. The app entry and plugin installation order.
4. Every root and nested `RouterView`, plus wrappers such as `KeepAlive`,
`Transition`, and `Suspense`.
5. Calls to `router.push`, `router.replace`, `router.back`, `RouterLink`, tab
controls, modal routing, and bespoke swipe handlers.
6. Route-local side effects in setup/mount hooks, including analytics,
subscriptions, polling, media, and mutations.
7. Deep-linkable child routes, ordered peer routes, and routes whose local state
can or cannot be evicted.
8. Horizontal gesture owners such as carousels, maps, editors, and canvases.
9. Browser/PWA, Capacitor, Electron, SSR, and accessibility requirements.
Summarize the route topology before implementation when it is non-trivial.
## Install and bootstrap
Add `@native-vue-router/core` and its stylesheet. Add preset or platform packages
only when needed. Match the project's package manager and formatting style.
Create exactly one native runtime for one authoritative root route surface:
1. Create the Vue Router normally.
2. Pass that router to `createNativeRouter`.
3. Call `app.use(router)` before `app.use(nativeRouter)`.
4. Wait for `router.isReady()` when the application already does so or relies on
deterministic initial rendering.
5. Replace the root visual `RouterView` with `NativeRouterView`.
6. Wrap it in `NativeNavigator` only when predictive Back or sibling paging is
required.
Retain nested `RouterView` components inside route components. Do not add nested
independent native runtimes without an explicit ownership design.
## Model route topology
Add the smallest route metadata needed for the requested behavior:
- Use `presentation` for a route's default visual treatment.
- Add `parent` to directly addressable child or presented routes so a cold start
has a predictive Back destination.
- Add numeric `siblingOrder` to peer routes whose direction must be stable.
- Choose `siblingHistory: "replace"` for tabs and other peers that should not
grow Back history; use `"push"` only when Back should revisit peer selection.
- Use `cache: false` for routes that must be destroyed when inactive and
`cache: "pin"` only for deliberately retained views.
- Use `gesture: false` to disable navigator gesture handling for a route.
Preserve params, query, and hash in dynamic `parent` functions whenever they are
part of the logical parent location. Do not infer parentage from the mounted
cache or route registration order.
`navigator` and `siblingGroup` are reserved labels in `0.1.x`; do not claim that
they automatically create navigator ownership. Pass sibling locations to
`NativeNavigator` explicitly.
## Migrate navigation intentionally
Use native runtime methods for navigation that needs preview-driven motion:
- `push` or `NativeLink` for forward stack navigation.
- `replace` for no-growth replacement.
- `sibling` for ordered peers.
- `pop` for Back.
- `present` and `dismiss` for modal or sheet routes.
- `NativeSheet` for safe-area-contained, content-height, or snapping sheet
surfaces.
- `NativeGestureLink` for component-originated horizontal dragging.
- `NativeDismissGesture` for downward dismissal.
Leave raw Vue Router navigation in place when it is an intentional redirect,
non-animated control flow, or external integration. The runtime reconciles such
navigation, but it cannot preview it before commit.
Preserve standard link semantics. Prefer `NativeLink` when a true anchor is
needed. If using `NativeGestureLink`, select a semantic `as` element and retain
keyboard activation and accessible naming.
## Make preview mounts safe
Assume a destination can run setup and mount before its route guard approves the
navigation, and can then unmount without becoming current.
Move committed-screen side effects from unconditional setup/mount code to
`useNativeViewActiveEffect`, `onNativeViewActivate`, or explicit user actions.
Use `useNativeViewVisibleEffect` only for work needed while the route is active
or participating in a transition. Keep durable data in a store or persistence
layer because the bounded native cache may evict component instances.
Do not replace the native multi-route cache with a single Vue `KeepAlive`.
`KeepAlive`, `Transition`, `Teleport`, and `Suspense` may still be composed
inside the `NativeRouterView` slot. Gate teleported overlays on native view
visibility because teleported DOM is outside the inactive layer's `inert` and
`aria-hidden` boundary.
For partial sheets, use `NativeSheet` rather than styling a route component with
an arbitrary viewport height. Use no breakpoints for content height, or
fractional breakpoints for snap points. Keep `presentation: "sheet"` in route
metadata so direct entries retain safe-area and underlay behavior.
Preserve the built-in scroll body unless replacing its gesture arbitration:
normal content scrolling owns interior positions, while top/down and bottom/up
overscroll started at a boundary hands off to sheet resizing. Keep one owner for
the complete physical gesture; never reinterpret accumulated content-scroll
distance as sheet movement after an edge is reached or direction reverses.
Use scoped `useRoute()` or Options API `$route` inside route trees. During a
preview, do not substitute `router.currentRoute`: it intentionally remains the
committed source location until navigation succeeds.
## Add gesture ownership safely
Wrap only the intended route surface in `NativeNavigator`. Give the container a
definite height and import core CSS.
Mark nested horizontal interaction regions with
`data-native-gesture="ignore"`. Confirm vertical scrolling, controls, text
selection, RTL behavior, reduced motion, keyboard navigation, focus, and screen
reader isolation after adding gestures.
Do not promise deterministic browser-edge ownership. Recommend Capacitor when a
product requires native-level suppression of host Back gestures.
## Add host adapters at the boundary
For Capacitor, install the adapter in `createNativeRouter` and configure hardware
Back, haptics, deep-link mapping, background cache trimming, and root exit. Do
not assume Universal Links or App Links are configured by JavaScript alone.
For Electron, call `disableElectronHistoryGestures(app.commandLine)` before
`app.whenReady()` in the main process. Expose only the renderer callbacks needed
by `createElectronRendererAdapter` through a secure preload. Preserve hash or
custom-protocol history behavior used by packaged applications.
Keep host detection and host APIs out of route components unless the product
experience genuinely differs.
## Verify the integration
Run the target project's formatter, type checker, unit tests, build, and relevant
end-to-end tests. Add tests or manual verification for the changed behavior:
- Direct entry and reload on deep child URLs.
- Params, query, hash, redirects, and nested route injection.
- Both routes remain live during a held gesture while the URL stays unchanged.
- Gesture cancellation preserves the source URL and state.
- Accepted and rejected guards settle correctly.
- Browser Back/Forward and cold-start parent Back.
- Sibling direction and replace/push history behavior.
- Cached state, eviction, unload, and active/visible effects.
- Vue built-ins, provide/inject, Options API `$route`, and lifecycle hooks used
by the application.
- Teleported overlay cleanup and inactive-route focus isolation.
- Gesture conflicts, RTL, reduced motion, keyboard behavior, and target hosts.
Use `runtime.entries`, `activeKey`, `transaction`, `canGoBack`, `cacheStats`, and
`onDiagnostic` for focused diagnostics. Use the profiler only when measuring
frame behavior; it is opt-in and should be stopped/disposed after capture.
## Report the result
State which routes and navigation paths became native-aware, which raw Vue Router
paths remain intentionally unchanged, what lifecycle work moved, and what was
verified. Call out unresolved host limitations, missing parent topology, or
dependency incompatibility explicitly.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Integrate Native Vue Router"
short_description: "Add native navigation to Vue Router apps"
default_prompt: "Use $integrate-native-vue-router to integrate Native Vue Router into this Vue project."

View File

@@ -0,0 +1,521 @@
# Native Vue Router integration reference
Use this reference for `@native-vue-router/*` version `0.1.x`. Inspect installed
declarations or local source when integrating another version.
## Support and ownership
- Requires Vue `^3.5.0` and Vue Router `^5.0.0`.
- Targets client-side DOM navigation. SSR/live-stack hydration is incomplete.
- Vue Router owns committed routes, URLs, history, guards, redirects, and lazy
route matching.
- Native Vue Router owns preview surfaces, mounted-view caching, gesture
progress, presentations, and transaction state.
- Preview trees receive scoped Vue Router route injection. `useRoute()` and
Options API `$route` identify that surface; `router.currentRoute` identifies
the globally committed route.
- The URL does not change during a forward preview. Vue Router navigation and
guards run when the gesture or imperative transaction commits.
## Packages and styles
```bash
npm install vue@^3.5 vue-router@^5 @native-vue-router/core
```
Optional packages:
```bash
npm install @native-vue-router/preset-native
npm install @native-vue-router/capacitor @capacitor/app @capacitor/core @capacitor/haptics
npm install @native-vue-router/electron
```
Import styles explicitly:
```ts
import "@native-vue-router/core/style.css";
import "@native-vue-router/preset-native/style.css"; // when used
```
Give the route shell a definite height. The core `.nvr-navigator` and
`.nvr-router-view` elements use `height: 100%`.
## Bootstrap template
```ts
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { createNativeRouter } from "@native-vue-router/core";
import App from "./App.vue";
import "@native-vue-router/core/style.css";
const router = createRouter({
history: createWebHistory(),
routes,
});
const nativeRouter = createNativeRouter({
router,
cache: { maxInactive: 4 },
// edgeWidth: 28,
// platform,
// presentations: [customPresentation],
});
const app = createApp(App);
app.use(router);
app.use(nativeRouter);
await router.isReady();
app.mount("#app");
```
```vue
<script setup lang="ts">
import { NativeNavigator, NativeRouterView } from "@native-vue-router/core";
const siblings = ["/inbox", "/stories", "/profile"];
</script>
<template>
<NativeNavigator :siblings="siblings" :edge-width="28">
<NativeRouterView />
</NativeNavigator>
</template>
```
Install Vue Router before the native plugin. Retain ordinary nested
`RouterView`s within route components.
## Route metadata
```ts
interface NativeRouteOptions {
navigator?: string;
presentation?:
"push" | "reveal" | "slide" | "fade" | "modal" | "sheet" | "none" | string;
transition?: string;
parent?: RouteLocationRaw | ((route) => RouteLocationRaw);
siblingGroup?: string;
siblingOrder?: number;
siblingHistory?: "push" | "replace";
cache?: boolean | "pin";
gesture?: boolean | "edge" | "full";
}
```
- Prefer `presentation`; `transition` is a compatibility alias.
- `parent` supplies a synthetic predictive Back destination if no warm native
history predecessor exists.
- `siblingOrder` determines direction for programmatic sibling navigation.
- `siblingHistory: "replace"` is the usual tab behavior.
- `cache: false` prevents inactive retention. `cache: "pin"` avoids ordinary LRU
trimming and is excluded from default `trimCache()`.
- `gesture: false` is enforced by `NativeNavigator`. In `0.1.x`, finer
`edge`/`full` policy is mainly structural.
- `navigator` and `siblingGroup` are reserved; pass siblings to the navigator
explicitly.
Example with dynamic parent state:
```ts
{
path: "/chat/:id/details",
name: "chat-details",
component: () => import("./ChatDetailsView.vue"),
meta: {
native: {
presentation: "push",
parent: (route) => ({
name: "chat",
params: { id: route.params.id },
query: route.query,
hash: route.hash,
}),
gesture: "edge",
},
},
}
```
## Core components
### `NativeRouterView`
Renders every mounted native entry as a sibling layer. Its default slot exposes:
```ts
{
Component: Component | undefined;
route: RouteLocationNormalizedLoaded;
entry: NativeViewEntry;
}
```
Without a slot, it renders the matched route component. Inactive layers are
hidden, `inert`, and `aria-hidden`.
### `NativeNavigator`
Props:
- `siblings: RouteLocationRaw[]` (default `[]`)
- `edgeWidth: number` (default `28`)
Owns leading-edge predictive Back and optional full-surface paging between the
listed siblings. The list is visual order. Route `siblingOrder` also makes
programmatic direction deterministic.
### `NativeLink`
Props:
- `to: RouteLocationRaw` (required)
- `replace: boolean`
- `presentation: NativePresentationName`
Renders an anchor with a resolved `href`. Primary unmodified clicks use native
`push` or `replace`; other attributes pass through.
### `NativeGestureLink`
Props:
- `to: RouteLocationRaw` (required)
- `presentation` (default `"reveal"`)
- `replace: boolean`
- `direction: "left" | "right" | "any"` (default `"any"`)
- `as: string` (default `"div"`)
Owns horizontal component-originated navigation. A click uses native `push`.
Choose semantic markup and add accessible keyboard behavior as needed.
### `NativeDismissGesture`
Prop `as` defaults to `div`. Owns downward vertical dismissal for a modal or
sheet surface.
### `NativeSheet`
Use inside a route with `meta.native.presentation: "sheet"`. The sheet route is
contained below `--nvr-safe-top` plus `--nvr-sheet-top-gap`, and its previous
route remains visible but inert as an underlay.
Props:
- `breakpoints: number[]` (default `[]`): normalized fractions in `(0, 1]`.
Empty means content height capped at available height.
- `initialBreakpoint?: number`: starts at the nearest declared fraction.
- `modelValue?: number`: current fraction for `v-model`.
- `dismissible: boolean` (default `true`).
- `backdropDismiss: boolean` (default `true`).
- `showHandle: boolean` (default `true`).
- `ariaLabel: string` (default `"Sheet"`).
Events are `update:modelValue`, `breakpoint-change`, and `dismiss`. The named
`handle` slot replaces only the handle visual. The built-in handle supports
pointer dragging, Escape, Arrow Up/Down, Home, and End.
The body keeps native scrolling while it has content in the requested
direction. At the top, downward overscroll shrinks or dismisses the sheet. At
the bottom, upward overscroll expands it. The handoff supports touch,
mouse/pen dragging, and thresholded wheel/trackpad input. A snapping
`NativeSheet` animates its own measured surface, while the route-sized wrapper
stays fixed and the previous route retains its underlay scale across commit.
Ownership locks after the gesture's initial directional intent. If content owns
the gesture, reaching an edge or reversing does not transfer that same gesture
to the sheet; release and start at the edge to resize. Never implement a
mid-gesture reinterpretation using distance accumulated while content was
scrolling.
```vue
<script setup lang="ts">
import { ref } from "vue";
import { NativeSheet } from "@native-vue-router/core";
const point = ref(0.55);
</script>
<template>
<NativeSheet
v-model="point"
:breakpoints="[0.3, 0.55, 1]"
:initial-breakpoint="0.55"
aria-label="Filters"
>
<FilterForm />
</NativeSheet>
</template>
```
Theme with `--nvr-sheet-background`, `--nvr-sheet-backdrop`,
`--nvr-sheet-radius`, and `--nvr-sheet-top-gap`. Prefer `NativeSheet` to
`NativeDismissGesture` for partial or snapping route sheets.
### Gesture exclusions
The recognizers ignore ordinary form/editable controls and elements within:
```html
<div data-native-gesture="ignore">...</div>
```
Use this for maps, carousels, canvases, code editors, and custom horizontal
controls.
## Runtime API
Create/inject:
- `createNativeRouter(options): NativeRouterRuntime`
- `useNativeRouter(): NativeRouterRuntime`
- Options API: `this.$nativeRouter`
Reactive readonly state:
- `router`
- `entries`
- `activeKey`
- `transaction`
- `canGoBack`
- `cacheStats`
Navigation:
```ts
push(to, options?): Promise<boolean>
replace(to, options?): Promise<boolean>
sibling(to, options?): Promise<boolean>
pop(): Promise<boolean>
present(to, presentation = "modal"): Promise<boolean>
dismiss(): Promise<boolean>
preload(to): Promise<RouteLocationNormalizedLoaded>
```
`NativeNavigationOptions` supports:
```ts
{
presentation?: NativePresentationName;
replace?: boolean;
direction?: "forward" | "back" | "up" | "down";
sourceRect?: {
top: number;
left: number;
width: number;
height: number;
viewportWidth: number;
viewportHeight: number;
};
}
```
Interactive driver:
```ts
beginInteractive(kind, to?, options?): Promise<number | null>
updateInteractive(progress, velocity?): void
finishInteractive(forceCommit?): Promise<boolean>
cancelInteractive(): Promise<void>
```
Kinds are `push`, `pop`, `sibling`, `present`, and `dismiss`. Progress is clamped
to `0...1`. Velocity is normalized route progress per second. The default commit
rule is progress at least `0.36`, or progress at least `0.08` with velocity at
least `1.1`.
Cache/diagnostics/extension:
```ts
unload(to): number
trimCache({ includePinned?, reason? }?): void
onDiagnostic(listener): () => void
registerPresentation(definition): void
presentationFor(name): NativePresentationDefinition | undefined
dispose(): void
```
Call `dispose()` if the owning application lifecycle tears the runtime down
without a page unload.
## Native view lifecycle
Available APIs:
```ts
useNativeViewLifecycle();
onNativeViewActivate(hook);
onNativeViewDeactivate(hook);
onNativeViewShow(hook);
onNativeViewHide(hook);
onNativeViewEvict(hook);
useNativeViewActiveEffect(effect);
useNativeViewVisibleEffect(effect);
```
Lifecycle state:
- `route`: scoped route for the view.
- `status`: `active`, `inactive`, `preview`, or `evicted`.
- `role`: `active`, `inactive`, `underlay`, `from`, or `to`.
- `isActive`: authoritative committed route.
- `isVisible`: active, one side of a live transition, or a visible sheet
underlay. Underlays remain inert.
- `isPreview`: destination not yet committed.
- `isCached`: mounted inactive route.
- `evictionReason`: `cache-disabled`, `cache-limit`,
`navigation-rejected`, `popped`, `manual`, `trimmed`, or `memory-pressure`.
Effects may return cleanup functions. Active effects suit polling,
subscriptions, media, and committed-screen analytics. Visible effects suit work
needed while the surface participates in a transition.
Do not treat Vue component mount as route commitment. A preview can mount before
guards run and unmount after rejection or gesture cancellation.
## Vue compatibility
Within a native route surface:
- Vue Composition and Options API lifecycle hooks retain normal component
semantics.
- App/plugin/ancestor `provide` and `inject` work normally.
- `useRoute`, `useRouter`, and Options API `$route` are supported.
- Params, query, hash, metadata, and matched records are scoped to each preview
or committed surface.
- Nested `RouterView` is supported.
- `KeepAlive`, `Transition`, `Teleport`, and `Suspense` may be used inside the
`NativeRouterView` slot.
Important distinctions:
- Native route caching is separate from `KeepAlive`. A cached native route stays
mounted, so native inactivity alone does not trigger Vue `onDeactivated`.
- Native presentations animate route layers; a Vue `Transition` handles changes
within one layer.
- Teleported DOM escapes the route layer's `inert`/`aria-hidden` isolation. Gate
it with `isVisible` and close it when the owning view hides where appropriate.
- Async route components are loaded before preview. A `Suspense` inside the route
can still show fallback UI for async descendants.
Slot composition example:
```vue
<NativeRouterView v-slot="{ Component, route }">
<Suspense>
<Transition name="content" mode="out-in">
<KeepAlive :max="3">
<component :is="Component" :key="route.fullPath" />
</KeepAlive>
</Transition>
<template #fallback><RouteSkeleton /></template>
</Suspense>
</NativeRouterView>
```
## Custom presentation
```ts
import { definePresentation } from "@native-vue-router/core";
const scaleFade = definePresentation({
name: "scale-fade",
axis: "x",
layerStyle({ role, progress, direction, sourceRect }) {
return role === "to"
? { opacity: progress, transform: `scale(${0.94 + progress * 0.06})` }
: { opacity: 1 - progress * 0.25 };
},
});
```
Register through `createNativeRouter({ presentations: [...] })` or
`runtime.registerPresentation()`. Keep layer styles compositor-friendly and do
not perform application or history mutations in a style function.
## Preset-native API
- `NativeBackButton` prop: `label` (default `"Back"`). Calls `native.pop()`.
- `NativeTabBar` prop: required `items: NativeTabItem[]`. It uses replace-style
native sibling navigation.
- `NativeTabItem`: `{ label, to, icon?, activeWhen? }`.
- `detectNativePlatform()`: `"ios" | "android" | "desktop"`.
- `nativeMotionTokens`: edge widths and commit thresholds for those three
platform labels. Core physics are not route-configurable from these tokens in
`0.1.x`.
## Capacitor adapter
```ts
createCapacitorAdapter({
exitAtRoot?: boolean; // default true behavior
haptics?: boolean; // default enabled on native
trimCacheOnPause?: boolean; // default true
deepLinkPath?: (url: URL) => string;
})
```
It handles Android hardware Back, App URL open/launch URLs, interactive
cancellation on pause, optional inactive-cache trimming, root exit, and haptics.
Native link association/configuration remains an application responsibility.
## Electron adapter
Main process, before `app.whenReady()`:
```ts
disableElectronHistoryGestures(app.commandLine);
```
Renderer:
```ts
createNativeRouter({
router,
platform: createElectronRendererAdapter(),
});
```
The preload may expose:
```ts
window.nativeVueHost = {
onBack(callback): () => void,
onForward?(callback): () => void,
onMemoryPressure?(callback): () => void,
};
```
Use a secure context bridge; do not enable Node integration merely for the
adapter. Packaged `file:` apps commonly require hash history.
## Verification matrix
At minimum verify:
| Area | Checks |
| ----------- | --------------------------------------------------------------------------------- |
| Boot | Initial route renders after `router.isReady`; CSS and height are correct |
| Route state | Params, query, hash, `useRoute`, `$route`, nested routes, provide/inject |
| Preview | Source and destination coexist; URL stays on source; cancellation removes preview |
| Guards | Acceptance, rejection, and redirect reconcile to Vue Router's result |
| Back | Warm Back, browser Back/Forward, direct-entry parent, no unrelated cached target |
| Siblings | Direction follows order; chosen push/replace history behavior is correct |
| Cache | Local warm state, `cache: false`, pin, LRU, unload, trim, eviction cleanup |
| Lifecycle | Active/visible effects stop and resume at the correct boundaries |
| Vue | KeepAlive, Transition, Teleport, Suspense, component hook ordering |
| Input | Edge Back, component drag, vertical scroll, ignored regions, rapid interruption |
| A11y | Inactive focus isolation, semantic controls, keyboard access, reduced motion |
| Hosts | PWA limits, Electron commands/history, Capacitor Back/deep links/pause |
## Known boundaries
- One runtime coordinates one authoritative visual transaction at a time.
- Preview setup can run before navigation approval.
- The mounted cache is bounded; route-local state can be lost after eviction.
- Cold-start predictive Back requires explicit parent topology.
- Browser content cannot guarantee ownership of operating-system/browser edge
gestures.
- Independent nested navigation controllers and SSR stack hydration need an
explicit design beyond the current `0.1.x` implementation.