Add experiments folder. Add usage.md and SKILL.md. Fix Sheet interactions and add dynamic sheet mode.
This commit is contained in:
809
usage.md
Normal file
809
usage.md
Normal file
@@ -0,0 +1,809 @@
|
||||
# Native Vue Router usage guide
|
||||
|
||||
Native Vue Router adds gesture-driven, interruptible navigation to Vue 3 while
|
||||
leaving Vue Router responsible for route matching, URLs, redirects, guards, and
|
||||
browser history. It renders a provisional destination beside the current route,
|
||||
lets a pointer gesture control the transition, and commits the Vue Router
|
||||
navigation only when the gesture completes.
|
||||
|
||||
The current support contract is Vue 3.5+, Vue Router 5, and client-side DOM
|
||||
rendering. SSR hydration of a live native view stack is not yet a complete
|
||||
feature.
|
||||
|
||||
## Packages
|
||||
|
||||
| Package | Purpose |
|
||||
| ---------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `@native-vue-router/core` | Runtime, route surfaces, gestures, caching, lifecycle APIs, and profiler |
|
||||
| `@native-vue-router/preset-native` | Native-looking back and tab controls, safe-area CSS, and motion tokens |
|
||||
| `@native-vue-router/capacitor` | Hardware Back, deep links, app lifecycle, root exit, and haptics |
|
||||
| `@native-vue-router/electron` | Host back/forward integration and Chromium history-gesture suppression |
|
||||
|
||||
## Install
|
||||
|
||||
Install the core package alongside its peer dependencies:
|
||||
|
||||
```bash
|
||||
npm install vue@^3.5 vue-router@^5 @native-vue-router/core
|
||||
```
|
||||
|
||||
Add optional packages only when the application uses them:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
When consuming this repository directly, build the packages first and install
|
||||
the required package directories or packed tarballs into the target project:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build:packages
|
||||
npm pack --workspace @native-vue-router/core
|
||||
```
|
||||
|
||||
Import the core stylesheet once from the application entry point. Import the
|
||||
preset stylesheet as well when using its controls:
|
||||
|
||||
```ts
|
||||
import "@native-vue-router/core/style.css";
|
||||
import "@native-vue-router/preset-native/style.css"; // optional
|
||||
```
|
||||
|
||||
The elements containing the navigator must have a definite height. A typical
|
||||
full-screen application uses:
|
||||
|
||||
```css
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Minimal setup
|
||||
|
||||
Create the Vue Router first, then create and install the native runtime. Install
|
||||
Vue Router before Native Vue Router so route injection and Options API `$route`
|
||||
scoping are configured correctly.
|
||||
|
||||
```ts
|
||||
// src/main.ts
|
||||
import { createApp } from "vue";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import { createNativeRouter } from "@native-vue-router/core";
|
||||
import App from "./App.vue";
|
||||
import HomeView from "./views/HomeView.vue";
|
||||
import ProductView from "./views/ProductView.vue";
|
||||
import "@native-vue-router/core/style.css";
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: "/", name: "home", component: HomeView },
|
||||
{
|
||||
path: "/products/:id",
|
||||
name: "product",
|
||||
component: ProductView,
|
||||
meta: {
|
||||
native: {
|
||||
presentation: "push",
|
||||
parent: "/",
|
||||
gesture: "edge",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export const nativeRouter = createNativeRouter({
|
||||
router,
|
||||
cache: { maxInactive: 4 },
|
||||
});
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(nativeRouter);
|
||||
|
||||
await router.isReady();
|
||||
app.mount("#app");
|
||||
```
|
||||
|
||||
Replace the application-level `<RouterView>` with `<NativeRouterView>` and wrap
|
||||
the navigation surface in `<NativeNavigator>` to enable predictive Back.
|
||||
|
||||
```vue
|
||||
<!-- src/App.vue -->
|
||||
<script setup lang="ts">
|
||||
import { NativeNavigator, NativeRouterView } from "@native-vue-router/core";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NativeNavigator>
|
||||
<NativeRouterView />
|
||||
</NativeNavigator>
|
||||
</template>
|
||||
```
|
||||
|
||||
Nested `<RouterView>` components inside route components continue to work. Use
|
||||
one application-level `NativeRouterView` for one native runtime; independent
|
||||
nested native navigators are not currently a complete feature.
|
||||
|
||||
## Route metadata
|
||||
|
||||
Declare presentation, topology, history, cache, and gesture policy next to each
|
||||
route:
|
||||
|
||||
```ts
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/inbox",
|
||||
name: "inbox",
|
||||
component: () => import("./views/InboxView.vue"),
|
||||
meta: {
|
||||
native: {
|
||||
siblingGroup: "primary",
|
||||
siblingOrder: 0,
|
||||
siblingHistory: "replace",
|
||||
cache: "pin",
|
||||
gesture: "full",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/chat/:id",
|
||||
name: "chat",
|
||||
component: () => import("./views/ChatView.vue"),
|
||||
meta: {
|
||||
native: {
|
||||
presentation: "push",
|
||||
parent: "/inbox",
|
||||
gesture: "edge",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/chat/:id/details",
|
||||
name: "chat-details",
|
||||
component: () => import("./views/ChatDetailsView.vue"),
|
||||
meta: {
|
||||
native: {
|
||||
parent: (route) => ({
|
||||
name: "chat",
|
||||
params: { id: route.params.id },
|
||||
query: route.query,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
| Option | Meaning |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| `presentation` | `push`, `reveal`, `slide`, `fade`, `modal`, `sheet`, `none`, or a registered custom name |
|
||||
| `transition` | Compatibility alias for `presentation`; prefer `presentation` in new code |
|
||||
| `parent` | Logical Back target for a cold-start/deep-linked route; may be a location or a function of the current route |
|
||||
| `siblingOrder` | Numeric visual order used to derive sibling direction |
|
||||
| `siblingHistory` | `replace` keeps peer selections out of Back history; `push` makes them Back destinations |
|
||||
| `cache` | `false` unmounts when inactive, `true` uses normal retention, and `pin` exempts the view from ordinary LRU trimming |
|
||||
| `gesture` | `false` disables navigator gestures for the route; `edge` and `full` describe intended policy |
|
||||
| `navigator`, `siblingGroup` | Reserved topology labels; the current navigator still receives its sibling list explicitly |
|
||||
|
||||
Declare `parent` for detail, settings, modal, and other routes that should have a
|
||||
predictive destination when opened directly. It is a logical product
|
||||
relationship, not proof that a matching browser history entry exists.
|
||||
|
||||
## Navigate
|
||||
|
||||
Use `useNativeRouter()` inside `setup()`:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useNativeRouter } from "@native-vue-router/core";
|
||||
|
||||
const native = useNativeRouter();
|
||||
|
||||
async function openProduct(id: string) {
|
||||
const committed = await native.push({
|
||||
name: "product",
|
||||
params: { id },
|
||||
query: { source: "featured" },
|
||||
hash: "#summary",
|
||||
});
|
||||
|
||||
if (committed) {
|
||||
// Vue Router accepted the navigation.
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
The same runtime is available as `this.$nativeRouter` in Options API
|
||||
components.
|
||||
|
||||
| Method | Use |
|
||||
| ---------------------------- | --------------------------------------------------------------------- |
|
||||
| `push(to, options?)` | Add an animated history entry |
|
||||
| `replace(to, options?)` | Replace the current history entry |
|
||||
| `sibling(to, options?)` | Move between ordered peer routes |
|
||||
| `pop()` | Navigate to the previous native history entry or declared parent |
|
||||
| `present(to, presentation?)` | Present a route, using `modal` by default |
|
||||
| `dismiss()` | Return from a presented route |
|
||||
| `preload(to)` | Resolve and load a lazy route without mounting or committing it |
|
||||
| `unload(to)` | Unmount inactive instances of one location and return the count |
|
||||
| `trimCache(options?)` | Unmount inactive cached views while retaining lightweight descriptors |
|
||||
|
||||
Navigation options can override `presentation`, `replace`, `direction`, and
|
||||
`sourceRect`. The navigation methods return `true` when Vue Router accepts the
|
||||
commit and `false` for a no-op, cancellation, or rejected navigation.
|
||||
|
||||
Use normal `router.push()` for redirects or flows that intentionally do not need
|
||||
a native preview. The runtime reconciles external Vue Router navigations, but
|
||||
they do not receive the same preview-driven transition.
|
||||
|
||||
## Links and gesture components
|
||||
|
||||
### NativeLink
|
||||
|
||||
`NativeLink` renders a real anchor, resolves its `href`, preserves modified-click
|
||||
behavior, and routes an ordinary primary click through the native runtime.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { NativeLink } from "@native-vue-router/core";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NativeLink
|
||||
:to="{ name: 'product', params: { id: '42' } }"
|
||||
presentation="push"
|
||||
class="product-link"
|
||||
>
|
||||
Product 42
|
||||
</NativeLink>
|
||||
</template>
|
||||
```
|
||||
|
||||
Its navigation props are `to`, `replace`, and `presentation`; other attributes
|
||||
are passed to the anchor.
|
||||
|
||||
### NativeGestureLink
|
||||
|
||||
`NativeGestureLink` lets a horizontal drag on a component reveal its destination.
|
||||
It accepts `to`, `presentation` (default `reveal`), `replace`, `direction`
|
||||
(`left`, `right`, or `any`), and `as` (default `div`).
|
||||
|
||||
```vue
|
||||
<NativeGestureLink
|
||||
as="article"
|
||||
:to="{ name: 'product', params: { id: product.id } }"
|
||||
presentation="reveal"
|
||||
direction="left"
|
||||
>
|
||||
<ProductCard :product="product" />
|
||||
</NativeGestureLink>
|
||||
```
|
||||
|
||||
Choose a semantic `as` element and provide keyboard behavior when the result is
|
||||
interactive. A normal click also invokes native `push()`.
|
||||
|
||||
### NativeNavigator
|
||||
|
||||
Pass ordered peer locations to enable full-surface horizontal sibling paging.
|
||||
The leading edge remains reserved for Back when `canGoBack` is true.
|
||||
|
||||
```vue
|
||||
<NativeNavigator
|
||||
:siblings="['/inbox', '/stories', '/profile']"
|
||||
:edge-width="28"
|
||||
>
|
||||
<NativeRouterView />
|
||||
</NativeNavigator>
|
||||
```
|
||||
|
||||
Inputs, editable content, links, buttons, and elements carrying
|
||||
`data-native-gesture="ignore"` are excluded from automatic gesture recognition.
|
||||
Use the explicit attribute for carousels, maps, editors, canvases, or other
|
||||
regions that own horizontal input.
|
||||
|
||||
### NativeDismissGesture
|
||||
|
||||
Wrap a custom full-height modal surface to make a downward drag dismiss it:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { NativeDismissGesture, useNativeRouter } from "@native-vue-router/core";
|
||||
|
||||
const native = useNativeRouter();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NativeDismissGesture as="main" class="sheet">
|
||||
<button type="button" @click="native.dismiss()">Close</button>
|
||||
<!-- sheet content -->
|
||||
</NativeDismissGesture>
|
||||
</template>
|
||||
```
|
||||
|
||||
### NativeSheet
|
||||
|
||||
Use `NativeSheet` inside a route whose presentation is `sheet`. It keeps the
|
||||
surface below the device's safe top inset, leaves the previous route visible but
|
||||
inert beneath a backdrop, and includes a drag handle.
|
||||
|
||||
With no breakpoints, the surface grows to its content and is capped at the
|
||||
available device height:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { NativeSheet } from "@native-vue-router/core";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NativeSheet aria-label="Filters">
|
||||
<FilterForm />
|
||||
</NativeSheet>
|
||||
</template>
|
||||
```
|
||||
|
||||
Supply fractional breakpoints to create snap points. Fractions are measured
|
||||
against the route height after the safe top inset has been reserved:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { NativeSheet } from "@native-vue-router/core";
|
||||
|
||||
const breakpoint = ref(0.55);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NativeSheet
|
||||
v-model="breakpoint"
|
||||
:breakpoints="[0.3, 0.55, 1]"
|
||||
:initial-breakpoint="0.55"
|
||||
aria-label="Choose a location"
|
||||
@breakpoint-change="savePreferredSheetSize"
|
||||
>
|
||||
<LocationPicker />
|
||||
</NativeSheet>
|
||||
</template>
|
||||
```
|
||||
|
||||
Users drag the handle or use Arrow Up/Down, Home, and End while it is focused.
|
||||
Dragging below the smallest point dismisses the route. The backdrop and Escape
|
||||
also dismiss by default.
|
||||
|
||||
The scrollable sheet body participates in the same gesture automatically. At an
|
||||
interior scroll position, the content scrolls normally. When the content is at
|
||||
the top, pulling down hands the gesture to the sheet so it can move to a lower
|
||||
point or dismiss. When the content is at the bottom, pushing upward grows the
|
||||
sheet to its next point. Mouse/pen dragging, touch input, and thresholded
|
||||
trackpad/wheel overscroll follow the same boundary rules.
|
||||
|
||||
Gesture ownership is chosen from the initial directional intent and remains
|
||||
locked until release. A gesture that starts while content exists in that
|
||||
direction stays a content gesture even if it reaches an edge or reverses. Lift
|
||||
and begin a new gesture at the edge to resize the sheet. This prevents content
|
||||
and sheet movement from overlapping and prevents previously scrolled distance
|
||||
from becoming a sheet-height jump.
|
||||
|
||||
| Prop | Meaning |
|
||||
| ------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `breakpoints` | Unique fractions greater than `0` and at most `1`; an empty list enables content height |
|
||||
| `initialBreakpoint` | Initial fraction, snapped to the nearest declared point |
|
||||
| `modelValue` | Current fractional point for `v-model` |
|
||||
| `dismissible` | Enables sheet-triggered drag and keyboard dismissal; default `true` |
|
||||
| `backdropDismiss` | Lets a backdrop click dismiss; default `true` |
|
||||
| `showHandle` | Renders the built-in drag/keyboard handle; default `true` |
|
||||
| `ariaLabel` | Accessible dialog label; default `Sheet` |
|
||||
|
||||
Use the `handle` slot to replace the visual handle without replacing its input
|
||||
behavior. Theme the surface with `--nvr-sheet-background`,
|
||||
`--nvr-sheet-backdrop`, `--nvr-sheet-radius`, and `--nvr-sheet-top-gap`.
|
||||
|
||||
For a route opened with `native.present(to, "sheet")`, the runtime remembers the
|
||||
sheet presentation after commit. Defining `meta.native.presentation: "sheet"`
|
||||
as well makes direct URL entry and raw Vue Router navigation use the same
|
||||
contained layout.
|
||||
|
||||
During presentation and dismissal, `NativeSheet` moves its actual surface by
|
||||
that surface's height rather than translating a transparent viewport-sized
|
||||
route wrapper. The source route remains continuously scaled as the sheet's
|
||||
underlay, avoiding a geometry jump when the route transaction commits.
|
||||
|
||||
## Tabs and back controls
|
||||
|
||||
The optional native preset supplies a Back button and tab bar:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NativeBackButton,
|
||||
NativeTabBar,
|
||||
type NativeTabItem,
|
||||
} from "@native-vue-router/preset-native";
|
||||
|
||||
const tabs: NativeTabItem[] = [
|
||||
{ label: "Inbox", to: "/inbox", icon: "◉" },
|
||||
{ label: "Stories", to: "/stories", icon: "◎" },
|
||||
{
|
||||
label: "Profile",
|
||||
to: "/profile",
|
||||
icon: "◇",
|
||||
activeWhen: (route) => route.path.startsWith("/profile"),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header><NativeBackButton label="Back" /></header>
|
||||
<NativeTabBar :items="tabs" />
|
||||
</template>
|
||||
```
|
||||
|
||||
The preset tab bar uses replace-style sibling navigation. Build a product-specific
|
||||
control with `native.sibling()` when tabs need different history semantics.
|
||||
|
||||
## Route params, query, hash, and injected route state
|
||||
|
||||
Pass any normal `RouteLocationRaw` to native navigation methods and components.
|
||||
`useRoute()`, `useRouter()`, and Options API `this.$route` work within active and
|
||||
preview route trees. During a held gesture, the destination subtree sees its own
|
||||
params, query, hash, matched records, and metadata even though
|
||||
`router.currentRoute` still points at the committed source route.
|
||||
|
||||
This distinction is intentional:
|
||||
|
||||
- Read `useRoute()` or `$route` inside a route component for that surface's
|
||||
scoped route.
|
||||
- Read `router.currentRoute` only when the application needs the globally
|
||||
committed route.
|
||||
- Expect the two values to differ while a preview is visible.
|
||||
|
||||
Normal Vue `provide()` and `inject()` work across the route surface. App-level
|
||||
provides, plugin provides, and values provided by route components remain
|
||||
available to descendants.
|
||||
|
||||
## Lifecycle and cached views
|
||||
|
||||
Native Vue Router can keep inactive route component trees mounted. Vue's normal
|
||||
mount, update, and unmount hooks continue to describe component lifetime, but
|
||||
being mounted does not mean the route is the current screen.
|
||||
|
||||
Use the native lifecycle for route visibility and activity:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
onNativeViewActivate,
|
||||
onNativeViewDeactivate,
|
||||
onNativeViewEvict,
|
||||
useNativeViewActiveEffect,
|
||||
useNativeViewLifecycle,
|
||||
useNativeViewVisibleEffect,
|
||||
} from "@native-vue-router/core";
|
||||
|
||||
const view = useNativeViewLifecycle();
|
||||
|
||||
useNativeViewActiveEffect(() => {
|
||||
const controller = new AbortController();
|
||||
startPolling({ signal: controller.signal });
|
||||
return () => controller.abort();
|
||||
});
|
||||
|
||||
useNativeViewVisibleEffect(() => {
|
||||
const stop = startAnimationNeededDuringTransitions();
|
||||
return stop;
|
||||
});
|
||||
|
||||
onNativeViewActivate(() => resumeMedia());
|
||||
onNativeViewDeactivate(() => pauseMedia());
|
||||
onNativeViewEvict((reason) => saveDraft(view.route.value, reason));
|
||||
</script>
|
||||
```
|
||||
|
||||
`isActive` means Vue Router has made the route authoritative. `isVisible` is
|
||||
also true for either side of an interactive transition and for a sheet's visual
|
||||
underlay. An underlay remains inert and is not active. `isCached` identifies a
|
||||
mounted inactive view; `isPreview` identifies an uncommitted destination.
|
||||
|
||||
Use active effects for polling, subscriptions, media, analytics, and work that
|
||||
should run only on the current route. Use visible effects for rendering work
|
||||
needed while the route is on screen during a transition. Put durable state in a
|
||||
store or persistence layer because cache eviction unmounts component-local
|
||||
state.
|
||||
|
||||
The default cache limit is four inactive, non-pinned views. History descriptors
|
||||
remain after a component tree is evicted and are remounted if navigation reaches
|
||||
them later.
|
||||
|
||||
## Vue built-in components
|
||||
|
||||
`NativeRouterView` exposes `Component`, `route`, and `entry` through its default
|
||||
slot, so normal Vue wrappers can be used inside each native route layer:
|
||||
|
||||
```vue
|
||||
<NativeRouterView v-slot="{ Component, route }">
|
||||
<Suspense>
|
||||
<Transition name="route-content" mode="out-in">
|
||||
<KeepAlive :max="3">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
|
||||
<template #fallback>
|
||||
<RouteSkeleton />
|
||||
</template>
|
||||
</Suspense>
|
||||
</NativeRouterView>
|
||||
```
|
||||
|
||||
These components retain their normal Vue meaning:
|
||||
|
||||
- `<KeepAlive>` caches components selected within that route layer. It is not a
|
||||
replacement for the native multi-route cache, and native route changes alone
|
||||
do not imply Vue `onActivated()` or `onDeactivated()`.
|
||||
- `<Transition>` animates changes inside a layer. Native presentations animate
|
||||
the route layers themselves.
|
||||
- `<Suspense>` may show a fallback while an async preview component resolves.
|
||||
- `<Teleport>` can move DOM outside the layer. Because teleported DOM is outside
|
||||
the layer's `inert` and `aria-hidden` boundary, close or hide overlays whenever
|
||||
the owning native view is not visible.
|
||||
|
||||
A visibility-safe teleported overlay looks like this:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useNativeViewLifecycle } from "@native-vue-router/core";
|
||||
|
||||
const open = ref(false);
|
||||
const view = useNativeViewLifecycle();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button type="button" @click="open = true">Open overlay</button>
|
||||
<Teleport to="body">
|
||||
<MyOverlay v-if="open && view.isVisible.value" @close="open = false" />
|
||||
</Teleport>
|
||||
</template>
|
||||
```
|
||||
|
||||
Options API lifecycle hooks (`beforeCreate`, `created`, `beforeMount`,
|
||||
`mounted`, `beforeUpdate`, `updated`, `beforeUnmount`, and `unmounted`) and the
|
||||
corresponding Composition API hooks keep their standard Vue behavior.
|
||||
|
||||
## Guards, redirects, and preview side effects
|
||||
|
||||
Forward navigation resolves and loads the route component before commit so the
|
||||
user can drag a live destination. Vue Router guards run when the runtime commits
|
||||
the real `push()`, `replace()`, or Back operation. A rejected guard removes the
|
||||
preview and restores the source route; a redirect is reconciled to the route Vue
|
||||
Router accepts.
|
||||
|
||||
Consequently, a preview component may execute `setup()` and mount before a guard
|
||||
allows entry, then unmount without ever becoming active. Avoid irreversible work
|
||||
such as analytics events, mutations, purchases, or permanent subscriptions in
|
||||
unconditional setup/mount code. Tie committed-screen behavior to
|
||||
`useNativeViewActiveEffect()` or an explicit committed application action.
|
||||
|
||||
## Custom presentations
|
||||
|
||||
Register a presentation at runtime or pass it in `createNativeRouter()`:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createNativeRouter,
|
||||
definePresentation,
|
||||
} from "@native-vue-router/core";
|
||||
|
||||
const scaleFade = definePresentation({
|
||||
name: "scale-fade",
|
||||
axis: "x",
|
||||
layerStyle({ role, progress }) {
|
||||
return role === "to"
|
||||
? {
|
||||
opacity: progress,
|
||||
transform: `scale(${0.94 + progress * 0.06})`,
|
||||
}
|
||||
: { opacity: 1 - progress * 0.25 };
|
||||
},
|
||||
});
|
||||
|
||||
const nativeRouter = createNativeRouter({
|
||||
router,
|
||||
presentations: [scaleFade],
|
||||
});
|
||||
|
||||
// This is also valid later:
|
||||
nativeRouter.registerPresentation(scaleFade);
|
||||
```
|
||||
|
||||
Reference the registered name from route metadata or a navigation option.
|
||||
Presentation functions should derive compositor-friendly styles from progress;
|
||||
they must not mutate history or application state.
|
||||
|
||||
Advanced interactions can call `beginInteractive()`, `updateInteractive()`,
|
||||
`finishInteractive()`, and `cancelInteractive()` directly. Progress is normalized
|
||||
from `0` to `1`, and velocity is normalized route progress per second. Cancel the
|
||||
transaction when the owning component unmounts and ignore stale async results by
|
||||
checking the returned transaction ID.
|
||||
|
||||
## Platform adapters
|
||||
|
||||
### Capacitor
|
||||
|
||||
```ts
|
||||
import { createCapacitorAdapter } from "@native-vue-router/capacitor";
|
||||
|
||||
const nativeRouter = createNativeRouter({
|
||||
router,
|
||||
platform: createCapacitorAdapter({
|
||||
haptics: true,
|
||||
exitAtRoot: true,
|
||||
trimCacheOnPause: true,
|
||||
deepLinkPath: (url) => `${url.pathname}${url.search}${url.hash}`,
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
The adapter handles hardware Back, launch/app URLs, pause cancellation, cache
|
||||
trimming, optional haptics, and optional exit at the root. Configure Universal
|
||||
Links/App Links in the native project separately.
|
||||
|
||||
### Electron
|
||||
|
||||
Disable Chromium's competing overscroll navigation in the main process before
|
||||
`app.whenReady()`:
|
||||
|
||||
```ts
|
||||
import { app } from "electron";
|
||||
import { disableElectronHistoryGestures } from "@native-vue-router/electron";
|
||||
|
||||
disableElectronHistoryGestures(app.commandLine);
|
||||
```
|
||||
|
||||
Install the renderer adapter after exposing the documented `window.nativeVueHost`
|
||||
back/forward/memory-pressure bridge from a secure preload:
|
||||
|
||||
```ts
|
||||
import { createElectronRendererAdapter } from "@native-vue-router/electron";
|
||||
|
||||
const nativeRouter = createNativeRouter({
|
||||
router,
|
||||
platform: createElectronRendererAdapter(),
|
||||
});
|
||||
```
|
||||
|
||||
Use hash history for packaged `file:` applications unless the Electron host
|
||||
serves navigation URLs through an application protocol.
|
||||
|
||||
### Browser and PWA
|
||||
|
||||
The core works in normal browser tabs, but a browser may reserve an edge gesture
|
||||
before page JavaScript can claim it. An installed iOS PWA can improve gesture
|
||||
ownership with an early non-passive edge guard, but web content cannot change
|
||||
`WKWebView.allowsBackForwardNavigationGestures`. Use Capacitor when deterministic
|
||||
native-level ownership is required.
|
||||
|
||||
## Add to an existing Vue Router application
|
||||
|
||||
Adopt the library incrementally:
|
||||
|
||||
1. Confirm Vue 3.5+ and Vue Router 5, then install core and its CSS.
|
||||
2. Create the native runtime from the existing router and install it after
|
||||
`app.use(router)`.
|
||||
3. Replace only the root visual outlet with `NativeRouterView`; leave nested
|
||||
router views in route components intact.
|
||||
4. Change navigation that needs native motion from `router.push()`/`RouterLink`
|
||||
to runtime methods or `NativeLink`. Keep ordinary Vue Router calls where no
|
||||
native transition is wanted.
|
||||
5. Add `parent` metadata to deep-linkable child routes.
|
||||
6. Identify peer routes such as tabs, assign `siblingOrder`, choose
|
||||
`siblingHistory`, and pass their locations to `NativeNavigator`.
|
||||
7. Move active-screen side effects from unconditional mount hooks into native
|
||||
active or visible effects.
|
||||
8. Mark nested horizontal controls with `data-native-gesture="ignore"` and add
|
||||
component gesture links only where the product intends them.
|
||||
9. Choose cache policy per route and move durable state out of component-local
|
||||
memory.
|
||||
10. Exercise URLs, redirects, rejected guards, direct deep links, browser Back
|
||||
and Forward, held/cancelled gestures, reduced motion, and keyboard/focus
|
||||
behavior before broad rollout.
|
||||
|
||||
For a lower-risk migration, start with button-driven `push()`, `pop()`, and
|
||||
`present()`. Add predictive Back, siblings, and component-originated gestures
|
||||
after the route topology and lifecycle behavior are verified.
|
||||
|
||||
## Diagnostics and profiling
|
||||
|
||||
Inspect the runtime's reactive state while integrating:
|
||||
|
||||
```ts
|
||||
const native = useNativeRouter();
|
||||
|
||||
watchEffect(() => {
|
||||
console.table(native.cacheStats.value);
|
||||
console.log(native.transaction.value);
|
||||
});
|
||||
|
||||
const stop = native.onDiagnostic((event) => {
|
||||
console.debug("native-navigation", event);
|
||||
});
|
||||
```
|
||||
|
||||
The opt-in profiler records frame cadence and timing-safe navigation events:
|
||||
|
||||
```ts
|
||||
import { createNativeNavigationProfiler } from "@native-vue-router/core";
|
||||
|
||||
const profiler = createNativeNavigationProfiler(nativeRouter, {
|
||||
metadata: { build: import.meta.env.VITE_BUILD_ID },
|
||||
});
|
||||
|
||||
profiler.start();
|
||||
// Reproduce the navigation.
|
||||
const report = profiler.stop();
|
||||
const json = profiler.toJSON(report);
|
||||
profiler.dispose();
|
||||
```
|
||||
|
||||
Route params, query values, and application state are omitted from profiler
|
||||
route labels.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**The route surface is blank or has zero height.** Give `html`, `body`, `#app`,
|
||||
and the application shell a definite height, and import the core stylesheet.
|
||||
|
||||
**`useNativeRouter()` says the plugin is not installed.** Create one runtime and
|
||||
call `app.use(nativeRouter)` before mounting the app.
|
||||
|
||||
**`useNativeViewLifecycle()` throws.** Call it only from a component rendered
|
||||
inside `NativeRouterView`.
|
||||
|
||||
**A route mounts even though a guard rejects it.** This is preview behavior, not
|
||||
a committed navigation. Move irreversible work to an active effect.
|
||||
|
||||
**Back has no visual destination after a direct deep link.** Add a `parent`
|
||||
location or parent function to that route's native metadata.
|
||||
|
||||
**A tab appears in browser Back history.** Set `siblingHistory: "replace"` and
|
||||
use `native.sibling()` or the preset tab bar.
|
||||
|
||||
**A carousel or editor fights the route gesture.** Put
|
||||
`data-native-gesture="ignore"` on the region that owns the input.
|
||||
|
||||
**A teleported dialog remains interactive from an inactive cached route.** Gate
|
||||
the teleport content on `useNativeViewLifecycle().isVisible` and close it on
|
||||
hide/deactivate when appropriate.
|
||||
|
||||
**Local state disappears.** The cache is bounded and can be trimmed by a host.
|
||||
Use `cache: "pin"` sparingly or store durable state outside the route component.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [How the preview and commit model works](docs/how-it-works.md)
|
||||
- [Architecture reference](docs/architecture.md)
|
||||
- [Engineering constraints and trade-offs](docs/challenges-and-tradeoffs.md)
|
||||
- [Scalability and extension points](docs/principles-and-scalability.md)
|
||||
- [Platform integration](docs/platforms.md)
|
||||
- [Interactive demo](apps/demo)
|
||||
|
||||
## AI agent skill
|
||||
|
||||
This repository includes a portable integration skill at
|
||||
[`skills/integrate-native-vue-router`](skills/integrate-native-vue-router).
|
||||
Copy that complete directory into the skills location recognized by the agent
|
||||
(for Codex, normally `~/.codex/skills/`) and invoke it as
|
||||
`$integrate-native-vue-router`. Keep `SKILL.md`, `agents/openai.yaml`, and the
|
||||
`references` directory together so the integration workflow retains its API and
|
||||
verification reference.
|
||||
Reference in New Issue
Block a user