Add experiments folder. Add usage.md and SKILL.md. Fix Sheet interactions and add dynamic sheet mode.
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user