Claude caf2ad692a Restore native scrolling and reliable recognition; add feel tests
The previous round regressed the demo against main: dropped frames while
a finger drags, and swipes ignored when they start imperfectly.

Frame drops: hosts used touch-action: manipulation plus a blocking
touchmove listener, so Chrome waited for JavaScript on every touch move
(~30% of frames over 20ms in a drag, p95 28ms). Hosts again use per-axis
touch-action like main (pan-y for horizontal gestures, and so on), and
attach no touch listener.

Flaky recognition: an irreversible decision after 6px dropped swipes
with a wobbly or backward start. Recognition again measures from the
pointer-down point until the pointer lifts, as on main.

Scrolling beside gestures, natively:
- .from.scrollEdge() uses direction-specific touch-action while its
  scroller is at the edge (pan-x pan-down for drag-down at the top), the
  pull-to-refresh pattern from the Pointer Events spec. A passive scroll
  listener keeps it current; no touch handling blocks the browser.
- Where directional values are unsupported (Safari), scroll-edge
  gestures alone fall back to claiming touches at the browser's first
  cancelable touchmove.
- Hosts publish touch-action as --nvo-touch-action and the stylesheet
  applies it to descendants, so nested scroll containers keep it and
  horizontal swipes over lists are not cancelled. Ignored subtrees get
  touch-action: auto back.
- Bindings gain listeners and ref (spread on the host); the components
  use them.

Feel tests (npm run test:feel): Chromium with emulated touch checks
recognition on ten human-like traces, frame pacing budgets while
dragging (p95 <= 20ms, <= 5% over 20ms) for a swipe and a scroll-edge
dismiss, and the behaviour checks. Against the previous code they fail
on exactly the reported regressions; now 23/23 pass. On the demo, all
traces match main and drag frame pacing matches or beats it.

Commit thresholds are unchanged (0.25 distance, 0.2 flick).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Faarh2Ts6StJPcz2mRiHiR
2026-11-03 05:56:26 +00:00
2026-07-31 07:51:50 +00:00
2026-07-21 15:13:16 +10:00
2026-07-21 15:03:34 +10:00
2026-07-21 14:54:36 +10:00
2026-07-22 02:12:05 +00:00
2026-07-21 14:54:36 +10:00
2026-07-22 02:12:05 +00:00
2026-07-22 02:12:05 +00:00

Native Vue Router

Native Vue Router is a gesture-first navigation runtime for Vue 3 and Vue Router 5. It keeps Vue Router in charge of matching, URLs, guards, and history while rendering live route stacks that can be manipulated interactively.

The repository includes a reusable headless core, a platform-adaptive visual preset, Capacitor and Electron adapters, and one messaging demo delivered as a PWA and through native hosts.

Install the routeless core-v2 experiment

The repository root is an installable facade for @native-vue-router/core-v2. npm builds the package from source when it installs the Git dependency:

npm install git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git

Import it by its package name and include the compositor stylesheet:

import {
  OriginScene,
  createOriginScene,
  gesture,
  originView,
} from "@native-vue-router/core-v2";
import "@native-vue-router/core-v2/style.css";

Pin production applications to a commit or tag:

npm install "git+https://git.harvmaster.com/Harvmaster/Native-Router-Vue.git#<commit-or-tag>"

Core-v2 is routeless and does not depend on Vue Router. Its usage guide and API are in packages/core-v2/README.md and packages/core-v2/API.md.

Codex skill

The Git package also includes build-with-native-vue-router-v2, a Codex skill for creating and integrating routeless core-v2 applications. After installing the dependency, copy the skill into Codex's skill directory:

mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
cp -R \
  node_modules/@native-vue-router/core-v2/skills/build-with-native-vue-router-v2 \
  "${CODEX_HOME:-$HOME/.codex}/skills/"

Restart or reload Codex if necessary, then invoke it explicitly with $build-with-native-vue-router-v2.

What works

  • Interactive edge pop that can be held indefinitely at any progress.
  • Ordered horizontal route paging with replace-by-default history.
  • Component-originated route dragging with a live target route.
  • Interactive push, adjacent-page sibling slide, modal, safe-area-contained content/snap-point sheets with scroll-boundary handoff, fade, and application-defined presentations.
  • Concurrent from and to routes using only public Vue Router 5 APIs.
  • Guarded commits: previews do not alter the URL, and rejected navigation springs back.
  • Cold-start predictive back through declared parent routes.
  • Bounded live view caching, nested router views, focus isolation, RTL, and reduced motion.
  • PWA, Electron, and Capacitor iOS/Android hosts.

Run it

npm install
npm run dev

The default app is the installable messaging PWA. Other useful commands:

npm run build          # packages, declarations, demo, and service worker
npm test               # core transaction tests
npm run test:e2e       # desktop and mobile Playwright projects
npm run electron       # build and launch the Electron host
npm run cap:sync       # build and synchronize iOS and Android projects

Native projects live under apps/capacitor/ios and apps/capacitor/android. Open or run them from apps/capacitor with npx cap open ios, npx cap open android, or npx cap run <platform>.

Test the installed iOS PWA

Run the normal npm run dev command, expose its printed network address through an HTTPS URL, and open that URL on the iPhone. The development server includes the PWA service worker and already listens on the local network. Safari still requires a secure context for the service worker; a plain LAN http:// address is not sufficient. Choose Share → Add to Home Screen, then launch NVR Messenger from its Home Screen icon.

The Navigation Lab reports Standalone, ready, and App reserved when the correct environment is active, and shows the exact build ID plus update-check count. Production builds check for updates whenever the app starts, returns to the foreground, regains connectivity, or has been open for a minute; activation reload waits for any live gesture to finish. Open a conversation and drag from the extreme left edge. The “Leading-edge touches claimed” counter should increment while the router renders its live predictive-back view.

For more aggressive lifecycle testing, open You → Runtime stress lab. It is a deeper route that keeps the primary tab bar, opts into push-style sibling history, exposes its mount lifetime, and renders a one-second async child through <Suspense>. Backing out evicts this pushed screen after its exit; browser Forward reconstructs it and shows the fallback again. To exercise a guard against an already-mounted destination, visit Stories, switch to You, enable Block cached Stories re-entry, and try returning to Stories. The guard rejects and evicts the cached view.

An installed web app cannot access WKWebView.allowsBackForwardNavigationGestures. The demo therefore reserves leading-edge touch sequences at the web-content boundary as an iOS standalone-only safeguard. Capacitor remains the deterministic option when native-level gesture suppression is required.

Minimal integration

import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { createNativeRouter } from "@native-vue-router/core";

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: "/", component: Home },
    {
      path: "/chat/:id",
      component: Chat,
      meta: {
        native: { presentation: "push", parent: "/", gesture: "edge" },
      },
    },
  ],
});

const nativeRouter = createNativeRouter({ router });
createApp(App).use(router).use(nativeRouter).mount("#app");
<script setup lang="ts">
import {
  NativeGestureLink,
  NativeNavigator,
  NativeRouterView,
} from "@native-vue-router/core";
</script>

<template>
  <NativeNavigator :siblings="['/', '/stories', '/profile']">
    <NativeRouterView />
  </NativeNavigator>
  <NativeGestureLink to="/chat/maya" presentation="reveal">
    Drag this row into the chat route
  </NativeGestureLink>
</template>

Import @native-vue-router/core/style.css for the built-in presentation layers. The demo imports @native-vue-router/preset-native/style.css as well.

Use nativeRouter.sibling(to) for tab or peer-route navigation. Direction is derived from siblingOrder, repeated navigation to the active route is a no-op, and siblingHistory: 'replace' keeps cached tab views out of the back stack.

Sibling views are lazy rather than pre-mounted: only the initial route exists on startup, and a sibling joins the bounded cache on its first visit or interactive preview. Route metadata accepts cache: false to opt out or cache: 'pin' for views that must survive ordinary trimming. useNativeViewLifecycle(), the onNativeView* hooks, and useNativeViewActiveEffect() let cached screens pause polling, media, or subscriptions while retaining their local UI state.

Call nativeRouter.unload('/some-route') to manually unmount inactive instances of one location while retaining their lightweight history descriptors. The active route and views participating in a transition are protected.

Capture frame pacing on a real device

The Navigation Lab contains an opt-in profiler. Tap Start profiling, leave the lab, reproduce the choppy navigation once or twice, return to the lab, tap Stop, then Share JSON. Installed iOS PWAs use the system share sheet; other browsers download the file. Attach that JSON to a bug report.

The core API is also available directly:

import { createNativeNavigationProfiler } from "@native-vue-router/core";

const profiler = createNativeNavigationProfiler(nativeRouter, {
  metadata: { build: import.meta.env.VITE_BUILD_ID },
});

profiler.start();
// Reproduce the navigation issue.
const report = profiler.stop();
const json = profiler.toJSON(report);

No rAF loop or browser performance observer runs before start(), and stop() removes them. Reports contain frame intervals, refresh-rate estimates, per-navigation timing, cold-mount preparation, route loading, cache eviction, visibility changes, and browser-supported Long Task/layout-shift/resource timing. Route params, query values, and application state are omitted.

Packages

  • @native-vue-router/core — transactions, route ledger, concurrent views, gestures, caching, and public components/composables.
  • @native-vue-router/preset-native — adaptive tab/back controls, safe-area CSS, and platform motion defaults.
  • @native-vue-router/capacitor — hardware back, deep links, pause cancellation, root exit, and haptics.
  • @native-vue-router/electron — Chromium history-gesture suppression and renderer back/forward bridging.

Design and engineering documentation:

Support contract

The target is Vue 3.5+ and Vue Router 5. Installed PWAs, current Electron, and Capacitor 8 are first-class. Normal browser tabs remain functional but browsers can reserve edge gestures that page content cannot consistently override.

MIT

S
Description
No description provided
Readme
1,001 KiB
Languages
TypeScript 56%
Vue 26.8%
CSS 15%
Swift 0.8%
JavaScript 0.8%
Other 0.6%