Files

10 KiB
Raw Permalink Blame History

Core-v2 integration patterns

Contents

  1. Install and bootstrap
  2. Mental model
  3. Actions and retained history
  4. Gesture builder
  5. Custom choreography
  6. Connected partial presentations
  7. Nested scenes
  8. Constraints and verification

Install and bootstrap

Install the Git package. Pin a commit or tag for reproducible applications:

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

The unpinned default-branch form is useful during active development:

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

The installed package name is @native-vue-router/core-v2. It has one peer dependency: Vue ^3.5.0. It does not depend on Vue Router.

Create the scene:

// src/scene.ts
import { createOriginScene, originView } from "@native-vue-router/core-v2";
import HomeView from "./views/HomeView.vue";

export const scene = createOriginScene({
  initial: originView(
    HomeView,
    { accountId: "42" },
    { key: "home", name: "Home" },
  ),
});

Render it:

<script setup lang="ts">
import { OriginScene } from "@native-vue-router/core-v2";
import "@native-vue-router/core-v2/style.css";
import { scene } from "./scene";
</script>

<template>
  <main class="app-shell">
    <OriginScene :scene="scene" />
  </main>
</template>

<style>
html,
body,
#app,
.app-shell {
  width: 100%;
  height: 100%;
  margin: 0;
}
</style>

The scene needs a definite width and height. Its view hosts are stable, absolutely positioned siblings.

Mental model

  • OriginView: immutable recipe containing a Vue component, props, and optional diagnostic key/name.
  • Scene node: one mounted instance created from a recipe.
  • Operation edge: temporary source→target relationship with progress, velocity, choreography, and commit/cancel outcome.
  • Origin: the mounted component that handled the event. There is no global current view.
  • Retained history: linked mounted instances, independent of URLs.

A newly created target can originate another operation while its incoming operation settles. Visual transforms compose by origin frame:

visual(Y) = X→Y.target × Y→Z.source
visual(Z) = X→Y.target × Y→Z.target

Actions and retained history

Helpers are overloaded:

  • Without choreography they return OriginNavigationIntent for a gesture builder.
  • With choreography they return a complete OriginAction for begin() or perform().

Forward

const profileView = () =>
  originView(
    ProfileView,
    { userId: "7" },
    { key: "profile-7", name: "Profile" },
  );

await origin.perform(forward(profileView(), slideLeft));

Commit parks but retains the origin. Back later restores that exact instance, including local state and DOM scroll position.

Back

if (origin.context.value.canGoBack) {
  await origin.perform(back(slideRight));
}

Back has no target recipe. It resolves previousNodeKey, animates the retained instance, and unmounts the current entry only on commit.

Useful context:

const origin = useOrigin();

origin.nodeKey;
origin.context.value.view;
origin.context.value.previous;
origin.context.value.history;
origin.context.value.canGoBack;

Replace

await origin.perform(
  replace(
    originView(
      OrderConfirmationView,
      { orderId: "NVO-2048" },
      { key: "confirmation", name: "Confirmation" },
    ),
    slideLeft,
  ),
);

Replace creates a target linked to the current origin's predecessor. Commit unmounts the current entry; cancellation removes the proposed replacement and restores the current entry. Back from the replacement skips the removed view.

Placement

above() and under() select stacking, not direction or history:

above(originView(DialogView), fade);
under(originView(BackdropView), reveal);

Prefer forward, back, and replace when expressing history semantics.

Gesture builder

Declare an anywhere forward gesture and responsive edge Back:

<script setup lang="ts">
import {
  OriginGestureSurface,
  back,
  forward,
  gesture,
  originView,
  slideLeft,
  slideRight,
  useOrigin,
} from "@native-vue-router/core-v2";
import DetailsView from "./DetailsView.vue";

const origin = useOrigin();
const detailsView = () =>
  originView(DetailsView, undefined, {
    key: "details",
    name: "Details",
  });

const openDetails = gesture.to
  .left()
  .navigate(() => forward(detailsView()))
  .animate(slideLeft);

const goBack = gesture.from
  .left("clamp(24px, 8%, 64px)")
  .to.right()
  .navigate((context) => (context.canGoBack ? back() : null))
  .animate(slideRight);

const gestures = [openDetails, goBack] as const;
</script>

<template>
  <OriginGestureSurface as="main" :gestures="gestures">
    <button
      type="button"
      data-origin-gesture="ignore"
      @click="origin.perform(forward(detailsView(), slideLeft))"
    >
      Open details
    </button>
  </OriginGestureSurface>
</template>

Builder stages:

  1. Optional .from: pointer-down eligibility.
  2. .to: direction recognition and axis lock.
  3. Optional .complete: release policy.
  4. .navigate: destination/history intent.
  5. .animate: visual choreography.

Starting at .to permits pointer-down anywhere. Edge distances accept CSS pixels, percentages, calc(), and clamp(). Use .from.when(context => ...) for arbitrary shapes, safe areas, or exclusion regions.

Set explicit completion policy for product-specific velocity behavior:

const open = gesture.to
  .left()
  .complete(
    ({ progress, velocity }) =>
      progress >= 0.38 || (progress >= 0.06 && velocity >= 0.85),
  )
  .navigate(() => forward(detailsView()))
  .animate(slideLeft);

Use OriginGesture when only one definition owns the host:

<OriginGesture :gesture="goBack">
  <article>...</article>
</OriginGesture>

Custom choreography

Effects are recomputed often. Keep them pure:

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

const percent = (value: number) => `${value * 100}%`;

export const focusCard = defineOriginChoreography({
  name: "focus-card",
  commitThreshold: 0.36,
  effects: ({ progress }) => ({
    frame: {
      transform: `translateY(${-Math.sin(progress * Math.PI) * 8}px)`,
    },
    source: {
      transform: `scale(${1 - progress * 0.08})`,
      opacity: 1 - progress * 0.45,
    },
    target: {
      transform: `translateY(${percent((1 - progress) * 0.1)}) scale(${
        0.82 + progress * 0.18
      })`,
      opacity: progress,
      style: {
        borderRadius: `${(1 - progress) * 28}px`,
      },
    },
  }),
});

frame affects both sides and descendants. Transforms concatenate, opacities multiply, and other style properties use local-last precedence.

Choreography context includes progress, velocity, phase, intent, measured origin/target rectangles, and scene viewport.

Connected partial presentations

Normal forward commit removes its operation edge and parks the retained source. Set persistAtRest: true when progress-1 effects must remain connected:

export const openDrawer = defineOriginChoreography({
  name: "open-two-thirds-drawer",
  persistAtRest: true,
  effects: ({ progress }) => ({
    source: {
      transform: `translateX(${progress * 66.6667}%)`,
    },
    target: {
      transform: `translateX(${(progress - 1) * 66.6667}%)`,
    },
  }),
});

export const closeDrawer = defineOriginChoreography({
  name: "close-two-thirds-drawer",
  effects: ({ progress }) => ({
    source: {
      transform: `translateX(${-progress * 66.6667}%)`,
    },
    target: {
      transform: `translateX(${(1 - progress) * 66.6667}%)`,
    },
  }),
});

Open with a retained push:

forward(originView(DrawerView), openDrawer);

The source becomes exposed: mounted and visible, but inert and aria-hidden. The drawer is active. Back temporarily suspends the settled opening edge and uses the close choreography. A cancelled Back restores the opening relationship; committed Back removes the drawer.

Requirements:

  • Use persistAtRest only with push history.
  • Make close progress 0 visually identical to open progress 1.
  • Put a transparent interactive region in the target if tapping the exposed source area should dismiss; the source itself is intentionally inert.
  • Do not treat settled connected edges as live animations in diagnostics.

Nested scenes

Create another scene when an embedded component needs independent history:

const carouselScene = createOriginScene({
  initial: originView(CarouselSlide, { index: 0 }),
});

Render <OriginScene :scene="carouselScene" /> inside the parent view. The nearest injected node scope owns useOrigin() and gestures.

Current parent/child ownership is selected at pointer-down. Give the child a non-overlapping .from region when the parent must retain fallback behavior. Do not assume a child can decline after capture and automatically hand the same pointer sequence to its parent.

Constraints and verification

  • No URL, deep-link, route-param, or browser-history model is included. Model data as props/state and bridge host Back separately.
  • No global active view exists. The component receiving the event is the origin.
  • Pushed entries remain mounted until Back pops them. There is no eviction policy yet.
  • Parked/exposed entries are inert, but their Vue watchers, timers, and effects continue running.
  • One origin owns at most one outgoing operation; a target may originate the next operation once its incoming intent commits.
  • Gesture recognition handles one primary pointer and one axis.
  • Nested scenes have separate history and measurements.
  • KeepAlive is not the scene cache and should not wrap scene navigation.
  • Reduced-motion preference settles operations immediately.

Verify scene diagnostics and DOM behavior:

scene.nodes.value;
scene.operations.value;
scene.contextFor(nodeKey);
scene.styleForNode(nodeKey);

Test committed and cancelled operations, component mount/unmount counts, retained DOM identity, scroll position, rapid chained operations, replace history, connected presentation cancellation, responsive edge regions, focus, inert/ARIA behavior, and production bundling.