522 lines
16 KiB
Markdown
522 lines
16 KiB
Markdown
# 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.
|