V2: Origin based animations, Gesture Builder, New Demo, non-url-based-routing. Massive improvements.
This commit is contained in:
1
apps/origins-demo/.gitignore
vendored
Normal file
1
apps/origins-demo/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dev-dist
|
||||
25
apps/origins-demo/index.html
Normal file
25
apps/origins-demo/index.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="theme-color" content="#080b12" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
name="apple-mobile-web-app-status-bar-style"
|
||||
content="black-translucent"
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="Origins" />
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" href="/favicon.svg" />
|
||||
<title>Routeless Origins Lab</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
22
apps/origins-demo/package.json
Normal file
22
apps/origins-demo/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@native-vue-router/origins-demo",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --config vite.config.ts --host",
|
||||
"build": "vue-tsc -p tsconfig.json --noEmit && vite build --config vite.config.ts",
|
||||
"preview": "vite preview --config vite.config.ts --host 0.0.0.0 --port 5173"
|
||||
},
|
||||
"dependencies": {
|
||||
"@native-vue-router/core-v2": "0.1.0-experimental.0",
|
||||
"vue": "^3.5.39"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.1.1",
|
||||
"vite-plugin-pwa": "^1.1.0",
|
||||
"vue-tsc": "^3.3.5"
|
||||
}
|
||||
}
|
||||
78
apps/origins-demo/src/App.vue
Normal file
78
apps/origins-demo/src/App.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
OriginScene,
|
||||
createOriginScene,
|
||||
originView,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import { labEvents } from "./lab-state";
|
||||
import HubView from "./views/HubView.vue";
|
||||
|
||||
/*
|
||||
* This scene is the entire application state for the experiment. There is no
|
||||
* Vue Router installation, route table, URL, or hidden RouterView.
|
||||
*/
|
||||
const scene = createOriginScene({
|
||||
initial: originView(HubView, undefined, {
|
||||
key: "origins-lab",
|
||||
name: "Origins Lab",
|
||||
}),
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="demo-shell">
|
||||
<OriginScene :scene="scene" />
|
||||
|
||||
<details class="scene-debug">
|
||||
<summary>
|
||||
<span
|
||||
class="debug-pulse"
|
||||
:class="{ live: scene.operations.value.length }"
|
||||
/>
|
||||
{{ scene.nodes.value.length }} node{{
|
||||
scene.nodes.value.length === 1 ? "" : "s"
|
||||
}}
|
||||
</summary>
|
||||
<div class="debug-content" aria-live="polite">
|
||||
<section>
|
||||
<strong>Scene graph</strong>
|
||||
<ol>
|
||||
<li v-for="node in scene.nodes.value" :key="node.key">
|
||||
<span>
|
||||
{{ node.view.name }}
|
||||
<em :class="`node-state node-state--${node.state}`">
|
||||
{{ node.state }}
|
||||
</em>
|
||||
</span>
|
||||
<code>{{ node.key.split("::").at(-1) }}</code>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<section v-if="scene.operations.value.length">
|
||||
<strong>Animation edges</strong>
|
||||
<div
|
||||
v-for="operation in scene.operations.value"
|
||||
:key="operation.id"
|
||||
class="debug-operation"
|
||||
>
|
||||
<span>{{ operation.choreography.name }}</span>
|
||||
<progress :value="operation.progress" max="1" />
|
||||
<small>
|
||||
{{ operation.phase }} ·
|
||||
{{ Math.round(operation.progress * 100) }}%
|
||||
</small>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<strong>Recent Vue lifecycle</strong>
|
||||
<ul class="event-log">
|
||||
<li v-for="event in labEvents.slice(0, 6)" :key="event.id">
|
||||
<time>{{ event.time }}</time>
|
||||
{{ event.message }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
15
apps/origins-demo/src/components/InstanceCard.vue
Normal file
15
apps/origins-demo/src/components/InstanceCard.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
label: string;
|
||||
instance: string;
|
||||
status: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="instance-card">
|
||||
<span>{{ label }}</span>
|
||||
<strong>{{ instance }}</strong>
|
||||
<small>{{ status }}</small>
|
||||
</div>
|
||||
</template>
|
||||
37
apps/origins-demo/src/gallery-data.ts
Normal file
37
apps/origins-demo/src/gallery-data.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface DemoPhoto {
|
||||
id: string;
|
||||
title: string;
|
||||
location: string;
|
||||
gradient: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const demoPhotos: readonly DemoPhoto[] = [
|
||||
{
|
||||
id: "ember",
|
||||
title: "Afterglow",
|
||||
location: "Reykjanes · 23:14",
|
||||
gradient:
|
||||
"radial-gradient(circle at 72% 25%, #ffcc8a 0, #ff774d 18%, transparent 43%), linear-gradient(145deg, #622c54, #171526 72%)",
|
||||
description:
|
||||
"A warm focus portal demonstrates scale, blur, opacity, and rounded-corner effects.",
|
||||
},
|
||||
{
|
||||
id: "tide",
|
||||
title: "Low tide",
|
||||
location: "Lofoten · 05:42",
|
||||
gradient:
|
||||
"radial-gradient(circle at 25% 25%, #b8f2ff 0, #3f9eb8 24%, transparent 48%), linear-gradient(160deg, #123b50, #07131f 72%)",
|
||||
description:
|
||||
"Swipe left while this view is present to create the next photo as another origin.",
|
||||
},
|
||||
{
|
||||
id: "moss",
|
||||
title: "Quiet giant",
|
||||
location: "Yakushima · 16:08",
|
||||
gradient:
|
||||
"radial-gradient(circle at 64% 30%, #d9ff9f 0, #608b52 22%, transparent 45%), linear-gradient(145deg, #294235, #091812 74%)",
|
||||
description:
|
||||
"The like button is component-local state. Move forward, then return to see the same retained instance and state.",
|
||||
},
|
||||
];
|
||||
49
apps/origins-demo/src/lab-state.ts
Normal file
49
apps/origins-demo/src/lab-state.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||
|
||||
export interface LabEvent {
|
||||
id: number;
|
||||
message: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
let instanceSequence = 0;
|
||||
let eventSequence = 0;
|
||||
|
||||
export const labEvents = reactive<LabEvent[]>([]);
|
||||
|
||||
export function recordLabEvent(message: string) {
|
||||
labEvents.unshift({
|
||||
id: ++eventSequence,
|
||||
message,
|
||||
time: new Date().toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}),
|
||||
});
|
||||
if (labEvents.length > 18) labEvents.length = 18;
|
||||
}
|
||||
|
||||
/**
|
||||
* Give every Vue setup execution a visible identity. A scene-edge collapse
|
||||
* and a forward push both retain the ID. A committed back only unmounts the
|
||||
* entry being popped, then reveals the exact previous instance.
|
||||
*/
|
||||
export function useDemoInstance(label: string) {
|
||||
const instance = `${label.slice(0, 2).toUpperCase()}-${String(
|
||||
++instanceSequence,
|
||||
).padStart(2, "0")}`;
|
||||
const status = ref("created");
|
||||
recordLabEvent(`${instance} created`);
|
||||
|
||||
onMounted(() => {
|
||||
status.value = "mounted";
|
||||
recordLabEvent(`${instance} mounted`);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
status.value = "unmounting";
|
||||
recordLabEvent(`${instance} unmounted`);
|
||||
});
|
||||
|
||||
return { instance, status };
|
||||
}
|
||||
13
apps/origins-demo/src/main.ts
Normal file
13
apps/origins-demo/src/main.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createApp } from "vue";
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
import App from "./App.vue";
|
||||
import "./style.css";
|
||||
|
||||
/*
|
||||
* Registration is deliberately immediate so an installed app becomes
|
||||
* offline-capable during its first session. `autoUpdate` in the Vite plugin
|
||||
* activates a fresh build without requiring a custom update prompt yet.
|
||||
*/
|
||||
registerSW({ immediate: true });
|
||||
|
||||
createApp(App).mount("#app");
|
||||
325
apps/origins-demo/src/motions.ts
Normal file
325
apps/origins-demo/src/motions.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import { defineOriginChoreography } from "@native-vue-router/core-v2";
|
||||
|
||||
const percent = (value: number) => `${(value * 100).toFixed(3)}%`;
|
||||
|
||||
/** A bottom-up cover similar to presenting a native media player. */
|
||||
export const coverUp = defineOriginChoreography({
|
||||
name: "cover-up",
|
||||
commitThreshold: 0.3,
|
||||
commitVelocity: 0.75,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `translate3d(0, ${percent(progress * -0.035)}, 0) scale(${1 - progress * 0.055})`,
|
||||
opacity: 1 - progress * 0.22,
|
||||
style: { filter: `saturate(${1 - progress * 0.3})` },
|
||||
},
|
||||
target: {
|
||||
transform: `translate3d(0, ${percent(1 - progress)}, 0)`,
|
||||
style: {
|
||||
borderRadius: `${(1 - progress) * 30}px`,
|
||||
boxShadow: `0 -24px 80px rgb(0 0 0 / ${progress * 0.38})`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/** Reverse of coverUp. The retained origin instance is revealed underneath. */
|
||||
export const coverDown = defineOriginChoreography({
|
||||
name: "cover-down",
|
||||
commitThreshold: 0.28,
|
||||
commitVelocity: 0.72,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `translate3d(0, ${percent(progress)}, 0)`,
|
||||
style: { borderRadius: `${progress * 30}px` },
|
||||
},
|
||||
target: {
|
||||
transform: `translate3d(0, ${percent(-0.035 + progress * 0.035)}, 0) scale(${0.945 + progress * 0.055})`,
|
||||
opacity: 0.78 + progress * 0.22,
|
||||
style: { filter: `saturate(${0.7 + progress * 0.3})` },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* A scale-and-focus transition. Filters use local-last CSS composition while
|
||||
* geometry remains independently composable with any ancestor operation.
|
||||
*/
|
||||
export const focusPortal = defineOriginChoreography({
|
||||
name: "focus-portal",
|
||||
commitThreshold: 0.32,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `scale(${1 - progress * 0.12})`,
|
||||
opacity: 1 - progress * 0.55,
|
||||
style: { filter: `blur(${progress * 8}px)` },
|
||||
},
|
||||
target: {
|
||||
transform: `translate3d(0, ${percent((1 - progress) * 0.08)}, 0) scale(${0.72 + progress * 0.28})`,
|
||||
opacity: progress,
|
||||
style: {
|
||||
borderRadius: `${(1 - progress) * 42}px`,
|
||||
filter: `blur(${(1 - progress) * 3}px)`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const unfocusPortal = defineOriginChoreography({
|
||||
name: "unfocus-portal",
|
||||
commitThreshold: 0.3,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `translate3d(0, ${percent(progress * 0.08)}, 0) scale(${1 - progress * 0.28})`,
|
||||
opacity: 1 - progress,
|
||||
style: {
|
||||
borderRadius: `${progress * 42}px`,
|
||||
filter: `blur(${progress * 3}px)`,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
transform: `scale(${0.88 + progress * 0.12})`,
|
||||
opacity: 0.45 + progress * 0.55,
|
||||
style: { filter: `blur(${(1 - progress) * 8}px)` },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/** A deliberately expressive transition for the four-direction gesture lab. */
|
||||
export const cardFlip = defineOriginChoreography({
|
||||
name: "card-flip",
|
||||
commitThreshold: 0.42,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `perspective(900px) rotateY(${-progress * 86}deg) scale(${1 - progress * 0.08})`,
|
||||
opacity: 1 - progress * 0.68,
|
||||
style: { transformOrigin: "left center" },
|
||||
},
|
||||
target: {
|
||||
transform: `perspective(900px) rotateY(${(1 - progress) * 86}deg) scale(${0.92 + progress * 0.08})`,
|
||||
opacity: 0.3 + progress * 0.7,
|
||||
style: { transformOrigin: "right center" },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* The frame contribution affects both sides of the edge. If this operation is
|
||||
* chained, descendants inherit the same small vertical arc.
|
||||
*/
|
||||
export const arcSlide = defineOriginChoreography({
|
||||
name: "arc-slide",
|
||||
commitThreshold: 0.34,
|
||||
commitVelocity: 0.8,
|
||||
effects: ({ progress }) => ({
|
||||
frame: {
|
||||
transform: `translate3d(0, ${-Math.sin(progress * Math.PI) * 9}px, 0)`,
|
||||
},
|
||||
source: {
|
||||
transform: `translate3d(${percent(progress * -0.28)}, 0, 0) rotate(${-progress * 1.5}deg) scale(${1 - progress * 0.035})`,
|
||||
opacity: 1 - progress * 0.22,
|
||||
},
|
||||
target: {
|
||||
transform: `translate3d(${percent(1 - progress)}, 0, 0) rotate(${(1 - progress) * 2.5}deg)`,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const rise = defineOriginChoreography({
|
||||
name: "rise",
|
||||
commitThreshold: 0.33,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `translate3d(0, ${percent(progress * -0.18)}, 0) scale(${1 - progress * 0.04})`,
|
||||
opacity: 1 - progress * 0.3,
|
||||
},
|
||||
target: {
|
||||
transform: `translate3d(0, ${percent(1 - progress)}, 0)`,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export const fall = defineOriginChoreography({
|
||||
name: "fall",
|
||||
commitThreshold: 0.33,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `translate3d(0, ${percent(progress)}, 0)`,
|
||||
},
|
||||
target: {
|
||||
transform: `translate3d(0, ${percent(-0.18 + progress * 0.18)}, 0) scale(${0.96 + progress * 0.04})`,
|
||||
opacity: 0.7 + progress * 0.3,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Reveal a full-screen overlay from above while its actual dialog remains
|
||||
* anchored to the left. The gesture's custom start predicate lives in the
|
||||
* originating view; choreography remains concerned only with movement.
|
||||
*/
|
||||
export const dropEdgeDialog = defineOriginChoreography({
|
||||
name: "drop-edge-dialog",
|
||||
commitThreshold: 0.24,
|
||||
commitVelocity: 0.62,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `scale(${1 - progress * 0.025})`,
|
||||
opacity: 1 - progress * 0.38,
|
||||
style: {
|
||||
filter: `saturate(${1 - progress * 0.35})`,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
transform: `translate3d(0, ${percent((1 - progress) * -0.018)}, 0)`,
|
||||
opacity: 1,
|
||||
style: {
|
||||
clipPath: `inset(0 0 ${percent(1 - progress)} 0)`,
|
||||
filter: `drop-shadow(0 28px 70px rgb(0 0 0 / ${progress * 0.55}))`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/** Reverse the edge dialog upward and reveal its retained origin instance. */
|
||||
export const liftEdgeDialog = defineOriginChoreography({
|
||||
name: "lift-edge-dialog",
|
||||
commitThreshold: 0.24,
|
||||
commitVelocity: 0.62,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `translate3d(0, ${percent(progress * -0.018)}, 0)`,
|
||||
opacity: 1,
|
||||
style: {
|
||||
clipPath: `inset(0 0 ${percent(progress)} 0)`,
|
||||
},
|
||||
},
|
||||
target: {
|
||||
transform: `scale(${0.975 + progress * 0.025})`,
|
||||
opacity: 0.62 + progress * 0.38,
|
||||
style: {
|
||||
filter: `saturate(${0.65 + progress * 0.35})`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Compact horizontal motion for a nested carousel. Its smaller source travel
|
||||
* keeps the retained slide visible behind the incoming card.
|
||||
*/
|
||||
export const nestedCarouselForward = defineOriginChoreography({
|
||||
name: "nested-carousel-forward",
|
||||
commitThreshold: 0.3,
|
||||
commitVelocity: 0.7,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `translate3d(${percent(progress * -0.18)}, 0, 0) scale(${1 - progress * 0.045})`,
|
||||
opacity: 1 - progress * 0.35,
|
||||
style: { filter: `saturate(${1 - progress * 0.25})` },
|
||||
},
|
||||
target: {
|
||||
transform: `translate3d(${percent(1 - progress)}, 0, 0)`,
|
||||
style: {
|
||||
borderRadius: `${(1 - progress) * 28}px`,
|
||||
boxShadow: `-24px 0 60px rgb(0 0 0 / ${progress * 0.3})`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/** Reverse reveal for the cooperative nested carousel. */
|
||||
export const nestedCarouselBack = defineOriginChoreography({
|
||||
name: "nested-carousel-back",
|
||||
commitThreshold: 0.3,
|
||||
commitVelocity: 0.7,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `translate3d(${percent(progress)}, 0, 0)`,
|
||||
style: { borderRadius: `${progress * 28}px` },
|
||||
},
|
||||
target: {
|
||||
transform: `translate3d(${percent(-0.18 + progress * 0.18)}, 0, 0) scale(${0.955 + progress * 0.045})`,
|
||||
opacity: 0.65 + progress * 0.35,
|
||||
style: { filter: `saturate(${0.75 + progress * 0.25})` },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Vertical nested-deck motion with a shared sideways arc. The frame effect
|
||||
* demonstrates that nested scenes retain the same composition semantics.
|
||||
*/
|
||||
export const nestedDeckForward = defineOriginChoreography({
|
||||
name: "nested-deck-forward",
|
||||
commitThreshold: 0.32,
|
||||
effects: ({ progress }) => ({
|
||||
frame: {
|
||||
transform: `translate3d(${Math.sin(progress * Math.PI) * 10}px, 0, 0)`,
|
||||
},
|
||||
source: {
|
||||
transform: `translate3d(0, ${percent(progress * -0.12)}, 0) rotate(${-progress * 2}deg) scale(${1 - progress * 0.06})`,
|
||||
opacity: 1 - progress * 0.4,
|
||||
},
|
||||
target: {
|
||||
transform: `translate3d(0, ${percent(1 - progress)}, 0) rotate(${(1 - progress) * 3}deg)`,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/** Downward reverse of the nested vertical deck. */
|
||||
export const nestedDeckBack = defineOriginChoreography({
|
||||
name: "nested-deck-back",
|
||||
commitThreshold: 0.32,
|
||||
effects: ({ progress }) => ({
|
||||
frame: {
|
||||
transform: `translate3d(${-Math.sin(progress * Math.PI) * 10}px, 0, 0)`,
|
||||
},
|
||||
source: {
|
||||
transform: `translate3d(0, ${percent(progress)}, 0) rotate(${progress * 3}deg)`,
|
||||
},
|
||||
target: {
|
||||
transform: `translate3d(0, ${percent(-0.12 + progress * 0.12)}, 0) rotate(${(1 - progress) * -2}deg) scale(${0.94 + progress * 0.06})`,
|
||||
opacity: 0.6 + progress * 0.4,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Intentionally dramatic cube turn used by the conflict case so it is obvious
|
||||
* when the child scene, rather than the page scene, owns the gesture.
|
||||
*/
|
||||
export const nestedCubeForward = defineOriginChoreography({
|
||||
name: "nested-cube-forward",
|
||||
commitThreshold: 0.38,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `perspective(720px) translate3d(${percent(progress * -0.5)}, 0, 0) rotateY(${progress * 72}deg)`,
|
||||
opacity: 1 - progress * 0.55,
|
||||
style: { transformOrigin: "right center" },
|
||||
},
|
||||
target: {
|
||||
transform: `perspective(720px) translate3d(${percent((1 - progress) * 0.5)}, 0, 0) rotateY(${(progress - 1) * 72}deg)`,
|
||||
opacity: 0.45 + progress * 0.55,
|
||||
style: { transformOrigin: "left center" },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
/** Reverse cube turn for retained slides in the conflict case. */
|
||||
export const nestedCubeBack = defineOriginChoreography({
|
||||
name: "nested-cube-back",
|
||||
commitThreshold: 0.38,
|
||||
effects: ({ progress }) => ({
|
||||
source: {
|
||||
transform: `perspective(720px) translate3d(${percent(progress * 0.5)}, 0, 0) rotateY(${-progress * 72}deg)`,
|
||||
opacity: 1 - progress * 0.55,
|
||||
style: { transformOrigin: "left center" },
|
||||
},
|
||||
target: {
|
||||
transform: `perspective(720px) translate3d(${percent((progress - 1) * 0.5)}, 0, 0) rotateY(${(1 - progress) * 72}deg)`,
|
||||
opacity: 0.45 + progress * 0.55,
|
||||
style: { transformOrigin: "right center" },
|
||||
},
|
||||
}),
|
||||
});
|
||||
1963
apps/origins-demo/src/style.css
Normal file
1963
apps/origins-demo/src/style.css
Normal file
File diff suppressed because it is too large
Load Diff
65
apps/origins-demo/src/views/ChatView.vue
Normal file
65
apps/origins-demo/src/views/ChatView.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from "vue";
|
||||
import { back, slideRight, useOrigin } from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance("Chat");
|
||||
const draft = ref("");
|
||||
const messages = ref([
|
||||
"This page deliberately owns no edge recognizer.",
|
||||
"Try swiping right. Nothing should happen.",
|
||||
]);
|
||||
|
||||
function goBack() {
|
||||
if (origin.context.value.canGoBack) void origin.perform(back(slideRight));
|
||||
}
|
||||
|
||||
async function send() {
|
||||
const message = draft.value.trim();
|
||||
if (!message) return;
|
||||
messages.value.push(message);
|
||||
draft.value = "";
|
||||
await nextTick();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!--
|
||||
No gesture binding is used here. Gesture absence is the policy, rather
|
||||
than a global back gesture followed by an exception.
|
||||
-->
|
||||
<div class="chat-view">
|
||||
<header class="chat-header">
|
||||
<button type="button" @click="goBack">←</button>
|
||||
<div class="chat-avatar">M</div>
|
||||
<div>
|
||||
<strong>Maya</strong>
|
||||
<span>online</span>
|
||||
</div>
|
||||
<span class="chat-lock">edge locked</span>
|
||||
</header>
|
||||
|
||||
<main class="messages">
|
||||
<div
|
||||
v-for="(message, index) in messages"
|
||||
:key="`${index}-${message}`"
|
||||
class="message"
|
||||
:class="{ 'message--mine': index > 1 }"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
<InstanceCard
|
||||
label="Chat instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
</main>
|
||||
|
||||
<form class="composer" @submit.prevent="send">
|
||||
<input v-model="draft" placeholder="Write something…" />
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
116
apps/origins-demo/src/views/DirectionLabView.vue
Normal file
116
apps/origins-demo/src/views/DirectionLabView.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
above,
|
||||
back,
|
||||
gesture,
|
||||
OriginGestureSurface,
|
||||
originView,
|
||||
useOrigin,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import { cardFlip, rise, unfocusPortal } from "../motions";
|
||||
import DirectionResultView from "./DirectionResultView.vue";
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance("Direction");
|
||||
|
||||
const eastView = () =>
|
||||
originView(
|
||||
DirectionResultView,
|
||||
{
|
||||
direction: "east",
|
||||
heading: "Horizontal declarations can own a full-page flip.",
|
||||
accent: "#38d8ff",
|
||||
createdBy: identity.instance,
|
||||
},
|
||||
{ key: "direction-east", name: "East Result" },
|
||||
);
|
||||
const openEast = () => above(eastView(), cardFlip);
|
||||
|
||||
const northView = () =>
|
||||
originView(
|
||||
DirectionResultView,
|
||||
{
|
||||
direction: "north",
|
||||
heading: "The same origin can create a completely different view upward.",
|
||||
accent: "#b9ff66",
|
||||
createdBy: identity.instance,
|
||||
},
|
||||
{ key: "direction-north", name: "North Result" },
|
||||
);
|
||||
const openNorth = () => above(northView(), rise);
|
||||
|
||||
const flipGesture = gesture.to
|
||||
.left()
|
||||
.navigate(() => above(eastView()))
|
||||
.animate(cardFlip);
|
||||
const riseGesture = gesture.to
|
||||
.up()
|
||||
.navigate(() => above(northView()))
|
||||
.animate(rise);
|
||||
const backGesture = gesture.from
|
||||
.left("clamp(24px, 8%, 64px)")
|
||||
.to.right()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(unfocusPortal);
|
||||
const gestures = [flipGesture, riseGesture, backGesture] as const;
|
||||
|
||||
function perform(action: ReturnType<typeof openEast>) {
|
||||
void origin.perform(action);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGestureSurface
|
||||
as="main"
|
||||
class="lab-page direction-view"
|
||||
:gestures="gestures"
|
||||
>
|
||||
<div class="eyebrow">Lab 02 · Direction matrix</div>
|
||||
<h1>One component. Three explicit choices.</h1>
|
||||
<p>
|
||||
Swipe left for a perspective flip, swipe up for a rising cover, or use the
|
||||
left edge to go back. None of these gestures exist outside this view.
|
||||
</p>
|
||||
|
||||
<div class="direction-compass">
|
||||
<button
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
class="compass-action compass-action--up"
|
||||
@click="perform(openNorth())"
|
||||
>
|
||||
<span>↑</span>
|
||||
<strong>Rise</strong>
|
||||
<small>target above</small>
|
||||
</button>
|
||||
<div class="compass-core">
|
||||
<span>origin</span>
|
||||
<strong>{{ identity.instance }}</strong>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
class="compass-action compass-action--left"
|
||||
@click="perform(openEast())"
|
||||
>
|
||||
<span>←</span>
|
||||
<strong>Flip left</strong>
|
||||
<small>custom perspective</small>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<InstanceCard
|
||||
label="Direction view"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
<div class="gesture-map">
|
||||
<span>↑ full-surface target</span>
|
||||
<span>← full-surface target</span>
|
||||
<span>left edge → history back</span>
|
||||
</div>
|
||||
<div class="edge-marker edge-marker--left">back edge</div>
|
||||
</OriginGestureSurface>
|
||||
</template>
|
||||
87
apps/origins-demo/src/views/DirectionResultView.vue
Normal file
87
apps/origins-demo/src/views/DirectionResultView.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
OriginGesture,
|
||||
back,
|
||||
gesture,
|
||||
slideRight,
|
||||
useOrigin,
|
||||
type OriginContext,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import { fall } from "../motions";
|
||||
|
||||
const props = defineProps<{
|
||||
direction: "east" | "north";
|
||||
heading: string;
|
||||
accent: string;
|
||||
createdBy?: string;
|
||||
}>();
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance(props.direction === "east" ? "East" : "North");
|
||||
const goBack = (context: OriginContext) => {
|
||||
if (!context.canGoBack) return null;
|
||||
return back(props.direction === "north" ? fall : slideRight);
|
||||
};
|
||||
|
||||
const returnGesture =
|
||||
props.direction === "east"
|
||||
? gesture.from
|
||||
.left("clamp(24px, 8%, 64px)")
|
||||
.to.right()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(slideRight)
|
||||
: gesture.to
|
||||
.down()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(fall);
|
||||
|
||||
function backWithButton() {
|
||||
const action = goBack(origin.context.value);
|
||||
if (action) void origin.perform(action);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGesture
|
||||
class="lab-page direction-result"
|
||||
:class="`direction-result--${direction}`"
|
||||
:style="{ '--result-accent': accent }"
|
||||
:gesture="returnGesture"
|
||||
>
|
||||
<div class="result-orbit" />
|
||||
<div class="eyebrow">Resolved from {{ direction }}</div>
|
||||
<h1>{{ heading }}</h1>
|
||||
<p>
|
||||
This component was created by <strong>{{ createdBy }}</strong
|
||||
>. Its own setup has now produced <strong>{{ identity.instance }}</strong
|
||||
>.
|
||||
</p>
|
||||
<InstanceCard
|
||||
label="Result instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
<button
|
||||
class="primary-action"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
@click="backWithButton"
|
||||
>
|
||||
Return to the retained direction instance
|
||||
</button>
|
||||
<div
|
||||
class="gesture-hint"
|
||||
:class="{ 'gesture-hint--back': direction === 'east' }"
|
||||
>
|
||||
<template v-if="direction === 'north'"
|
||||
>Swipe down to return <span>↓</span></template
|
||||
>
|
||||
<template v-else><span>←</span> Swipe from the left edge</template>
|
||||
</div>
|
||||
<div v-if="direction === 'east'" class="edge-marker edge-marker--left">
|
||||
back edge
|
||||
</div>
|
||||
</OriginGesture>
|
||||
</template>
|
||||
116
apps/origins-demo/src/views/EdgeDialogView.vue
Normal file
116
apps/origins-demo/src/views/EdgeDialogView.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import {
|
||||
OriginGesture,
|
||||
back,
|
||||
gesture,
|
||||
useOrigin,
|
||||
type OriginContext,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import { liftEdgeDialog } from "../motions";
|
||||
|
||||
defineProps<{
|
||||
openedBy?: string;
|
||||
predicate?: string;
|
||||
}>();
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance("Dialog");
|
||||
const selected = ref("Motion");
|
||||
|
||||
const dismiss = (context: OriginContext) =>
|
||||
context.canGoBack ? back(liftEdgeDialog) : null;
|
||||
|
||||
const dismissGesture = gesture.to
|
||||
.up()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(liftEdgeDialog);
|
||||
|
||||
function close() {
|
||||
const action = dismiss(origin.context.value);
|
||||
if (action) void origin.perform(action);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGesture class="edge-dialog-overlay" :gesture="dismissGesture">
|
||||
<button
|
||||
class="edge-dialog-backdrop"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
aria-label="Close edge dialog"
|
||||
@click="close"
|
||||
/>
|
||||
|
||||
<section
|
||||
class="edge-dialog"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="edge-dialog-title"
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<div class="eyebrow">Custom-origin dialog</div>
|
||||
<h1 id="edge-dialog-title">Dropped from the top.</h1>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
aria-label="Close dialog"
|
||||
@click="close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<p>
|
||||
The target is a normal scene component. Its content is anchored left,
|
||||
while the operation moves the target frame from top to bottom.
|
||||
</p>
|
||||
|
||||
<dl class="dialog-facts">
|
||||
<div>
|
||||
<dt>Admitted by</dt>
|
||||
<dd>{{ predicate }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Origin instance</dt>
|
||||
<dd>{{ openedBy }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Placement</dt>
|
||||
<dd>above</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<fieldset>
|
||||
<legend>Dialog-local state</legend>
|
||||
<button
|
||||
v-for="option in ['Motion', 'Gesture', 'Scene']"
|
||||
:key="option"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
:class="{ selected: selected === option }"
|
||||
@click="selected = option"
|
||||
>
|
||||
{{ option }}
|
||||
</button>
|
||||
</fieldset>
|
||||
|
||||
<InstanceCard
|
||||
label="Dialog instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
|
||||
<footer>
|
||||
<strong>Swipe up anywhere on the panel to dismiss.</strong>
|
||||
<small
|
||||
>The reverse operation reveals the retained origin instance.</small
|
||||
>
|
||||
</footer>
|
||||
</section>
|
||||
</OriginGesture>
|
||||
</template>
|
||||
125
apps/origins-demo/src/views/EdgePredicateView.vue
Normal file
125
apps/origins-demo/src/views/EdgePredicateView.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
above,
|
||||
back,
|
||||
gesture,
|
||||
OriginGestureSurface,
|
||||
originView,
|
||||
useOrigin,
|
||||
type OriginContext,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import { dropEdgeDialog, unfocusPortal } from "../motions";
|
||||
import EdgeDialogView from "./EdgeDialogView.vue";
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance("Predicate");
|
||||
|
||||
/*
|
||||
* This is deliberately not the built-in `edge` option. A down gesture's
|
||||
* built-in edge would be the top edge, while this lab wants down movement to
|
||||
* begin at the left edge. The predicate can describe that cross-axis rule—or
|
||||
* any safe area, shape, percentage, or exclusion zone the application needs.
|
||||
*/
|
||||
function dialogEdgeWidth(host: HTMLElement) {
|
||||
return Math.min(72, Math.max(36, host.clientWidth * 0.08));
|
||||
}
|
||||
|
||||
const dialogView = () =>
|
||||
originView(
|
||||
EdgeDialogView,
|
||||
{
|
||||
openedBy: identity.instance,
|
||||
predicate: "x ≤ clamp(36px, 8%, 72px)",
|
||||
},
|
||||
{ key: "edge-dialog", name: "Edge Dialog" },
|
||||
);
|
||||
|
||||
const openDialog = () => above(dialogView(), dropEdgeDialog);
|
||||
|
||||
const goBack = (context: OriginContext) =>
|
||||
context.canGoBack ? back(unfocusPortal) : null;
|
||||
|
||||
const dialogGesture = gesture.from
|
||||
.when(({ point, host }) => point.localX <= dialogEdgeWidth(host))
|
||||
.to.down()
|
||||
.navigate(() => above(dialogView()))
|
||||
.animate(dropEdgeDialog);
|
||||
|
||||
/*
|
||||
* The same physical left edge also supports a conventional rightward back
|
||||
* gesture. Both recognizers see pointer-down; their axis checks decide which
|
||||
* one captures once movement becomes unambiguous.
|
||||
*/
|
||||
const backGesture = gesture.from
|
||||
.left("clamp(36px, 8%, 72px)")
|
||||
.to.right()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(unfocusPortal);
|
||||
|
||||
const gestures = [dialogGesture, backGesture] as const;
|
||||
|
||||
function backWithButton() {
|
||||
const action = goBack(origin.context.value);
|
||||
if (action) void origin.perform(action);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGestureSurface
|
||||
as="main"
|
||||
class="lab-page predicate-view"
|
||||
:gestures="gestures"
|
||||
>
|
||||
<button
|
||||
class="view-back"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
@click="backWithButton"
|
||||
>
|
||||
← Labs
|
||||
</button>
|
||||
|
||||
<div class="predicate-copy">
|
||||
<div class="eyebrow">Lab 06 · Predicate edge</div>
|
||||
<h1>Direction and starting edge are independent.</h1>
|
||||
<p>
|
||||
Begin inside the illuminated left rail and drag <strong>down</strong>. A
|
||||
custom predicate admits the pointer; the normal direction recognizer
|
||||
still handles axis capture, progress, velocity, and release.
|
||||
</p>
|
||||
|
||||
<div class="predicate-code">
|
||||
<span>start predicate</span>
|
||||
<code>x ≤ clamp(36px, 8%, 72px)</code>
|
||||
<small>movement: down · built-in edge: intentionally omitted</small>
|
||||
</div>
|
||||
|
||||
<InstanceCard
|
||||
label="Predicate origin"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
|
||||
<button
|
||||
class="primary-action"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
@click="origin.perform(openDialog())"
|
||||
>
|
||||
Open accessibly without dragging
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="predicate-edge" aria-hidden="true">
|
||||
<span>custom left-edge predicate</span>
|
||||
<div class="predicate-arrow">↓</div>
|
||||
<small>start here · pull down</small>
|
||||
</div>
|
||||
|
||||
<div class="predicate-axis-note">
|
||||
A rightward drag on this same rail still performs edge-back.
|
||||
</div>
|
||||
</OriginGestureSurface>
|
||||
</template>
|
||||
89
apps/origins-demo/src/views/FirstView.vue
Normal file
89
apps/origins-demo/src/views/FirstView.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
above,
|
||||
back,
|
||||
gesture,
|
||||
OriginGestureSurface,
|
||||
originView,
|
||||
slideLeft,
|
||||
slideRight,
|
||||
useOrigin,
|
||||
type OriginContext,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import SecondView from "./SecondView.vue";
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance("X");
|
||||
|
||||
const secondView = () =>
|
||||
originView(
|
||||
SecondView,
|
||||
{
|
||||
openedAt: new Date().toLocaleTimeString(),
|
||||
createdBy: identity.instance,
|
||||
},
|
||||
{ key: "second", name: "Second" },
|
||||
);
|
||||
const createSecond = () => above(secondView(), slideLeft);
|
||||
|
||||
const goBack = (context: OriginContext) =>
|
||||
context.canGoBack ? back(slideRight) : null;
|
||||
|
||||
const forwardGesture = gesture.to
|
||||
.left()
|
||||
.navigate(() => above(secondView()))
|
||||
.animate(slideLeft);
|
||||
const backGesture = gesture
|
||||
// .from.left("clamp(24px, 8%, 64px)")
|
||||
// .from.left("20%")
|
||||
.to.right()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(slideRight);
|
||||
const gestures = [forwardGesture, backGesture] as const;
|
||||
|
||||
function backWithButton() {
|
||||
const action = goBack(origin.context.value);
|
||||
if (action) void origin.perform(action);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGestureSurface
|
||||
as="main"
|
||||
class="lab-page chain-view view-one"
|
||||
:gestures="gestures"
|
||||
>
|
||||
<button
|
||||
class="view-back"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
@click="backWithButton"
|
||||
>
|
||||
← Labs
|
||||
</button>
|
||||
<div class="eyebrow">Chain stress test · Origin X</div>
|
||||
<h1>Every component starts its own movement.</h1>
|
||||
<p>
|
||||
Drag this screen left. Release it, then immediately drag the blue screen
|
||||
while this spring is still settling. Keep going to build a four-node
|
||||
scene.
|
||||
</p>
|
||||
<InstanceCard
|
||||
label="X Vue instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
<button
|
||||
class="primary-action"
|
||||
data-origin-gesture="ignore"
|
||||
type="button"
|
||||
@click="origin.perform(createSecond())"
|
||||
>
|
||||
Open Y without a gesture
|
||||
</button>
|
||||
<div class="gesture-hint">Swipe left <span>→</span></div>
|
||||
<div class="edge-marker edge-marker--left">back edge</div>
|
||||
</OriginGestureSurface>
|
||||
</template>
|
||||
63
apps/origins-demo/src/views/FourthView.vue
Normal file
63
apps/origins-demo/src/views/FourthView.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
OriginGesture,
|
||||
back,
|
||||
gesture,
|
||||
slideRight,
|
||||
useOrigin,
|
||||
type OriginContext,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
|
||||
defineProps<{ chain?: string }>();
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance("Omega");
|
||||
const goBack = (context: OriginContext) =>
|
||||
context.canGoBack ? back(slideRight) : null;
|
||||
|
||||
const backGesture = gesture
|
||||
// .from.left("clamp(24px, 8%, 64px)")
|
||||
// .from.left("20%")
|
||||
.to.right()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(slideRight);
|
||||
|
||||
function backWithButton() {
|
||||
const action = goBack(origin.context.value);
|
||||
if (action) void origin.perform(action);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGesture class="lab-page chain-view view-four" :gesture="backGesture">
|
||||
<div class="eyebrow">Chain stress test · Origin Ω</div>
|
||||
<h1>Four independently mounted components. Three live edges.</h1>
|
||||
<p>
|
||||
If you moved quickly enough, the inspector showed X, Y, Z, and Ω at once.
|
||||
Each component stayed a flat sibling while inherited transforms composed
|
||||
mathematically.
|
||||
</p>
|
||||
<div class="proof">
|
||||
Reported ancestry <strong>{{ chain }}</strong>
|
||||
</div>
|
||||
<InstanceCard
|
||||
label="Ω Vue instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
<button
|
||||
class="primary-action"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
@click="backWithButton"
|
||||
>
|
||||
Return to the retained Z instance
|
||||
</button>
|
||||
<div class="gesture-hint gesture-hint--back">
|
||||
<span>←</span> Or swipe from the left edge
|
||||
</div>
|
||||
<div class="edge-marker edge-marker--left">back edge</div>
|
||||
</OriginGesture>
|
||||
</template>
|
||||
88
apps/origins-demo/src/views/GalleryView.vue
Normal file
88
apps/origins-demo/src/views/GalleryView.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
OriginGesture,
|
||||
above,
|
||||
back,
|
||||
gesture,
|
||||
originView,
|
||||
useOrigin,
|
||||
type OriginContext,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { demoPhotos, type DemoPhoto } from "../gallery-data";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import { focusPortal, unfocusPortal } from "../motions";
|
||||
import PhotoDetailView from "./PhotoDetailView.vue";
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance("Gallery");
|
||||
|
||||
const photoAction = (photo: DemoPhoto, index: number) =>
|
||||
above(
|
||||
originView(
|
||||
PhotoDetailView,
|
||||
{ photo, index },
|
||||
{ key: `photo-${photo.id}`, name: photo.title },
|
||||
),
|
||||
focusPortal,
|
||||
);
|
||||
|
||||
const goBack = (context: OriginContext) =>
|
||||
context.canGoBack ? back(unfocusPortal) : null;
|
||||
|
||||
const backGesture = gesture.from
|
||||
.left("clamp(24px, 8%, 64px)")
|
||||
.to.right()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(unfocusPortal);
|
||||
|
||||
function backWithButton() {
|
||||
const action = goBack(origin.context.value);
|
||||
if (action) void origin.perform(action);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGesture class="lab-page gallery-view" :gesture="backGesture">
|
||||
<header class="section-header">
|
||||
<div>
|
||||
<button
|
||||
class="view-back"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
@click="backWithButton"
|
||||
>
|
||||
← Labs
|
||||
</button>
|
||||
<div class="eyebrow">Lab 03 · Focus gallery</div>
|
||||
<h1>Events can originate motion without being gestures.</h1>
|
||||
<p>
|
||||
Select a card to run the same scene primitive programmatically. Once
|
||||
inside, the destination declares its own traversal gestures.
|
||||
</p>
|
||||
</div>
|
||||
<InstanceCard
|
||||
label="Gallery instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div class="photo-grid">
|
||||
<button
|
||||
v-for="(photo, index) in demoPhotos"
|
||||
:key="photo.id"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
class="photo-card"
|
||||
:style="{ background: photo.gradient }"
|
||||
@click="origin.perform(photoAction(photo, index))"
|
||||
>
|
||||
<span>{{ String(index + 1).padStart(2, "0") }}</span>
|
||||
<strong>{{ photo.title }}</strong>
|
||||
<small>{{ photo.location }}</small>
|
||||
</button>
|
||||
</div>
|
||||
<div class="edge-marker edge-marker--left">back edge</div>
|
||||
</OriginGesture>
|
||||
</template>
|
||||
177
apps/origins-demo/src/views/HubView.vue
Normal file
177
apps/origins-demo/src/views/HubView.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
OriginGesture,
|
||||
above,
|
||||
gesture,
|
||||
originView,
|
||||
slideLeft,
|
||||
useOrigin,
|
||||
type OriginChoreography,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import { coverUp, focusPortal } from "../motions";
|
||||
import ChatView from "./ChatView.vue";
|
||||
import DirectionLabView from "./DirectionLabView.vue";
|
||||
import EdgePredicateView from "./EdgePredicateView.vue";
|
||||
import FirstView from "./FirstView.vue";
|
||||
import GalleryView from "./GalleryView.vue";
|
||||
import NestedScenesView from "./NestedScenesView.vue";
|
||||
import PlayerView from "./PlayerView.vue";
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance("Hub");
|
||||
|
||||
function viewFor(component: Parameters<typeof originView>[0], name: string) {
|
||||
return originView(component, undefined, {
|
||||
key: name.toLowerCase().replaceAll(" ", "-"),
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
function actionFor(
|
||||
component: Parameters<typeof originView>[0],
|
||||
name: string,
|
||||
choreography: OriginChoreography,
|
||||
) {
|
||||
return above(viewFor(component, name), choreography);
|
||||
}
|
||||
|
||||
/*
|
||||
* No `.from` is declared: every eligible pointer-down on the Hub can become
|
||||
* this leftward gesture once movement passes the direction lock.
|
||||
*/
|
||||
const enterChainGesture = gesture.to
|
||||
.left()
|
||||
// .complete(
|
||||
// ({ progress, velocity }) =>
|
||||
// progress >= 0.3 || (progress >= 0.05 && velocity >= 0.8),
|
||||
// )
|
||||
.navigate(() => above(viewFor(FirstView, "Chain X")))
|
||||
.animate(slideLeft);
|
||||
|
||||
const openChain = () => actionFor(FirstView, "Chain X", slideLeft);
|
||||
const openDirections = () =>
|
||||
actionFor(DirectionLabView, "Direction Lab", focusPortal);
|
||||
const openGallery = () => actionFor(GalleryView, "Gallery", focusPortal);
|
||||
const openPlayer = () => actionFor(PlayerView, "Player", coverUp);
|
||||
const openChat = () => actionFor(ChatView, "Locked Chat", slideLeft);
|
||||
const openPredicate = () =>
|
||||
actionFor(EdgePredicateView, "Predicate Edge", focusPortal);
|
||||
const openNestedScenes = () =>
|
||||
actionFor(NestedScenesView, "Nested Scenes", focusPortal);
|
||||
|
||||
const labs = [
|
||||
{
|
||||
number: "01",
|
||||
title: "Concurrent chain",
|
||||
description: "Swipe through X→Y→Z→Ω before the preceding springs settle.",
|
||||
tags: ["4 live nodes", "frame composition"],
|
||||
action: openChain,
|
||||
accent: "violet",
|
||||
},
|
||||
{
|
||||
number: "02",
|
||||
title: "Direction matrix",
|
||||
description:
|
||||
"One component declares horizontal, vertical, and edge-only behavior.",
|
||||
tags: ["multi-axis", "custom flip"],
|
||||
action: openDirections,
|
||||
accent: "cyan",
|
||||
},
|
||||
{
|
||||
number: "03",
|
||||
title: "Focus gallery",
|
||||
description:
|
||||
"Programmatic card selection and gesture-driven photo traversal.",
|
||||
tags: ["portal", "local state"],
|
||||
action: openGallery,
|
||||
accent: "amber",
|
||||
},
|
||||
{
|
||||
number: "04",
|
||||
title: "Native player",
|
||||
description:
|
||||
"A vertical cover with media state and intentionally no back edge.",
|
||||
tags: ["vertical dismiss", "gesture policy"],
|
||||
action: openPlayer,
|
||||
accent: "green",
|
||||
},
|
||||
{
|
||||
number: "05",
|
||||
title: "Locked chat",
|
||||
description:
|
||||
"A Snapchat-style page that exposes only an explicit back button.",
|
||||
tags: ["blocked edge", "form state"],
|
||||
action: openChat,
|
||||
accent: "rose",
|
||||
},
|
||||
{
|
||||
number: "06",
|
||||
title: "Predicate edge",
|
||||
description: "Swipe down from a dynamic left-edge region to drop a dialog.",
|
||||
tags: ["custom hit test", "axis arbitration"],
|
||||
action: openPredicate,
|
||||
accent: "lime",
|
||||
},
|
||||
{
|
||||
number: "07",
|
||||
title: "Nested scenes",
|
||||
description:
|
||||
"Carousels and decks with local history—including deliberate gesture conflicts.",
|
||||
tags: ["carousel", "nested ownership"],
|
||||
action: openNestedScenes,
|
||||
accent: "blue",
|
||||
},
|
||||
] as const;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGesture class="lab-page hub-view" :gesture="enterChainGesture">
|
||||
<keep-alive>
|
||||
<header class="hub-header">
|
||||
<div>
|
||||
<div class="eyebrow">Routeless origins · interactive field guide</div>
|
||||
<h1>A navigation engine made of component-owned movement.</h1>
|
||||
<p>
|
||||
No route table decides what this screen can do. Every lab below
|
||||
creates its target, chooses its choreography, and declares its own
|
||||
gesture policy.
|
||||
</p>
|
||||
</div>
|
||||
<InstanceCard
|
||||
label="Hub instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
</header>
|
||||
|
||||
<main class="lab-grid">
|
||||
<button
|
||||
v-for="lab in labs"
|
||||
:key="lab.number"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
class="lab-card"
|
||||
:class="`lab-card--${lab.accent}`"
|
||||
@click="origin.perform(lab.action())"
|
||||
>
|
||||
<span class="lab-number">{{ lab.number }}</span>
|
||||
<span class="lab-copy">
|
||||
<strong>{{ lab.title }}</strong>
|
||||
<small>{{ lab.description }}</small>
|
||||
</span>
|
||||
<span class="tag-row">
|
||||
<em v-for="tag in lab.tags" :key="tag">{{ tag }}</em>
|
||||
</span>
|
||||
<span class="lab-arrow">↗</span>
|
||||
</button>
|
||||
</main>
|
||||
|
||||
<footer class="hub-footer">
|
||||
<span>Tip: swipe anywhere left to enter the chain stress test.</span>
|
||||
<span>The inspector in the corner exposes live nodes and edges.</span>
|
||||
</footer>
|
||||
</keep-alive>
|
||||
</OriginGesture>
|
||||
</template>
|
||||
166
apps/origins-demo/src/views/NestedSceneSlide.vue
Normal file
166
apps/origins-demo/src/views/NestedSceneSlide.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import {
|
||||
back,
|
||||
forward,
|
||||
gesture,
|
||||
OriginGestureSurface,
|
||||
useOrigin,
|
||||
type OriginChoreography,
|
||||
type OriginGestureDirectedBuilder,
|
||||
type OriginView,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import {
|
||||
nestedCarouselBack,
|
||||
nestedCarouselForward,
|
||||
nestedCubeBack,
|
||||
nestedCubeForward,
|
||||
nestedDeckBack,
|
||||
nestedDeckForward,
|
||||
} from "../motions";
|
||||
|
||||
type NestedSceneKind = "cooperative" | "vertical" | "conflict";
|
||||
|
||||
const props = defineProps<{
|
||||
kind: NestedSceneKind;
|
||||
index: number;
|
||||
total: number;
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
accent: string;
|
||||
next?: () => OriginView;
|
||||
}>();
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance(`Nested ${props.kind} ${props.index + 1}`);
|
||||
const localLikes = ref(0);
|
||||
|
||||
const vertical = props.kind === "vertical";
|
||||
const forwardMotion: OriginChoreography = vertical
|
||||
? nestedDeckForward
|
||||
: props.kind === "conflict"
|
||||
? nestedCubeForward
|
||||
: nestedCarouselForward;
|
||||
const backMotion: OriginChoreography = vertical
|
||||
? nestedDeckBack
|
||||
: props.kind === "conflict"
|
||||
? nestedCubeBack
|
||||
: nestedCarouselBack;
|
||||
|
||||
/*
|
||||
* Cooperative cases deliberately leave the outer page's first 44 pixels
|
||||
* untouched. The conflict case starts anywhere, so its pointer-down handler
|
||||
* stops propagation before it knows whether `back()` is available.
|
||||
*/
|
||||
function movementBuilder(
|
||||
direction: "forward" | "back",
|
||||
): OriginGestureDirectedBuilder {
|
||||
const to = vertical
|
||||
? direction === "forward"
|
||||
? "up"
|
||||
: "down"
|
||||
: direction === "forward"
|
||||
? "left"
|
||||
: "right";
|
||||
const start =
|
||||
props.kind === "conflict"
|
||||
? gesture.to
|
||||
: gesture.from.when(({ point, bounds }) => {
|
||||
const parentRail = Math.min(52, Math.max(22, bounds.width * 0.12));
|
||||
return point.localX > parentRail;
|
||||
}).to;
|
||||
return start[to]();
|
||||
}
|
||||
|
||||
const forwardGesture = movementBuilder("forward")
|
||||
.navigate(() => {
|
||||
const target = props.next?.();
|
||||
return target ? forward(target) : null;
|
||||
})
|
||||
.animate(forwardMotion);
|
||||
|
||||
const backGesture = movementBuilder("back")
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(backMotion);
|
||||
|
||||
const gestures = [forwardGesture, backGesture] as const;
|
||||
|
||||
function nextWithButton() {
|
||||
const target = props.next?.();
|
||||
if (target) void origin.perform(forward(target, forwardMotion));
|
||||
}
|
||||
|
||||
function backWithButton() {
|
||||
if (origin.context.value.canGoBack) void origin.perform(back(backMotion));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGestureSurface
|
||||
as="article"
|
||||
class="nested-slide"
|
||||
:class="`nested-slide--${kind}`"
|
||||
:data-nested-kind="kind"
|
||||
:data-nested-index="index"
|
||||
:style="{ '--nested-accent': accent }"
|
||||
:gestures="gestures"
|
||||
>
|
||||
<div class="nested-slide__orb" aria-hidden="true" />
|
||||
|
||||
<header>
|
||||
<span>{{ eyebrow }}</span>
|
||||
<strong>{{ index + 1 }} / {{ total }}</strong>
|
||||
</header>
|
||||
|
||||
<div class="nested-slide__copy">
|
||||
<h3>{{ title }}</h3>
|
||||
<p>{{ description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="nested-slide__state">
|
||||
<InstanceCard
|
||||
label="Nested instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
@click="localLikes += 1"
|
||||
>
|
||||
Local state · {{ localLikes }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<button
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
:disabled="!origin.canGoBack.value"
|
||||
@click="backWithButton"
|
||||
>
|
||||
{{ vertical ? "↓ Previous" : "← Previous" }}
|
||||
</button>
|
||||
|
||||
<div class="nested-slide__dots" aria-label="Slide position">
|
||||
<span
|
||||
v-for="dot in total"
|
||||
:key="dot"
|
||||
:class="{ active: dot === index + 1 }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
:disabled="!next"
|
||||
@click="nextWithButton"
|
||||
>
|
||||
{{ vertical ? "Next ↑" : "Next →" }}
|
||||
</button>
|
||||
</footer>
|
||||
</OriginGestureSurface>
|
||||
</template>
|
||||
275
apps/origins-demo/src/views/NestedScenesView.vue
Normal file
275
apps/origins-demo/src/views/NestedScenesView.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
OriginGesture,
|
||||
OriginScene,
|
||||
back,
|
||||
createOriginScene,
|
||||
gesture,
|
||||
originView,
|
||||
useOrigin,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import { unfocusPortal } from "../motions";
|
||||
import NestedSceneSlide from "./NestedSceneSlide.vue";
|
||||
|
||||
type NestedSceneKind = "cooperative" | "vertical" | "conflict";
|
||||
|
||||
interface SlideCopy {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
description: string;
|
||||
accent: string;
|
||||
}
|
||||
|
||||
const slideSets: Record<NestedSceneKind, readonly SlideCopy[]> = {
|
||||
cooperative: [
|
||||
{
|
||||
eyebrow: "Cooperative carousel",
|
||||
title: "A scene inside a page",
|
||||
description:
|
||||
"Swipe left away from the glowing pass-through rail. The target mounts only inside this clipped carousel.",
|
||||
accent: "#76dcff",
|
||||
},
|
||||
{
|
||||
eyebrow: "Retained nested history",
|
||||
title: "Slide one is still mounted",
|
||||
description:
|
||||
"Increment local state, move again, then return. The same nested Vue instances are revealed.",
|
||||
accent: "#a78bfa",
|
||||
},
|
||||
{
|
||||
eyebrow: "Independent operation graph",
|
||||
title: "Three carousel instances",
|
||||
description:
|
||||
"This history belongs to the carousel scene. The outer page remains one mounted application entry.",
|
||||
accent: "#65f2b1",
|
||||
},
|
||||
],
|
||||
vertical: [
|
||||
{
|
||||
eyebrow: "Vertical nested deck",
|
||||
title: "Pull upward inside this card",
|
||||
description:
|
||||
"The deck uses vertical progress, rotation, and a shared sideways frame arc.",
|
||||
accent: "#ffbd70",
|
||||
},
|
||||
{
|
||||
eyebrow: "Orthogonal choreography",
|
||||
title: "The page still owns its left rail",
|
||||
description:
|
||||
"Pull down to reveal the retained card, or begin on its pass-through rail and swipe right to leave the lab.",
|
||||
accent: "#ff7f9f",
|
||||
},
|
||||
{
|
||||
eyebrow: "Nested composition",
|
||||
title: "Frames remain local",
|
||||
description:
|
||||
"Transforms compose exactly like the full-screen scene, but measurements come from this smaller viewport.",
|
||||
accent: "#ffe079",
|
||||
},
|
||||
],
|
||||
conflict: [
|
||||
{
|
||||
eyebrow: "Intentional conflict",
|
||||
title: "This child claims every pointer-down",
|
||||
description:
|
||||
"Try the page's rightward back gesture over this card. The child sees it first, even though nested back is unavailable.",
|
||||
accent: "#ff5f78",
|
||||
},
|
||||
{
|
||||
eyebrow: "Child-owned back",
|
||||
title: "Now rightward swipe works here",
|
||||
description:
|
||||
"After moving forward, the exact same gesture can resolve nested back, producing the cube reversal.",
|
||||
accent: "#ff8d4d",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function nestedView(kind: NestedSceneKind, index: number) {
|
||||
const slides = slideSets[kind];
|
||||
const copy = slides[index]!;
|
||||
return originView(
|
||||
NestedSceneSlide,
|
||||
{
|
||||
kind,
|
||||
index,
|
||||
total: slides.length,
|
||||
...copy,
|
||||
next:
|
||||
index + 1 < slides.length
|
||||
? () => nestedView(kind, index + 1)
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
key: `${kind}-slide-${index + 1}`,
|
||||
name: `${kind} slide ${index + 1}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function nestedScene(kind: NestedSceneKind) {
|
||||
return createOriginScene({ initial: nestedView(kind, 0) });
|
||||
}
|
||||
|
||||
interface NestedCase {
|
||||
kind: NestedSceneKind;
|
||||
status: "supported" | "supported-with-policy" | "known-conflict";
|
||||
title: string;
|
||||
summary: string;
|
||||
expected: string;
|
||||
issue: string;
|
||||
scene: ReturnType<typeof createOriginScene>;
|
||||
}
|
||||
|
||||
const cases: readonly NestedCase[] = [
|
||||
{
|
||||
kind: "cooperative",
|
||||
status: "supported-with-policy",
|
||||
title: "Reserved-edge carousel",
|
||||
summary:
|
||||
"Horizontal nested history with a start predicate that leaves a clamp(22px, 12%, 52px) rail for its parent.",
|
||||
expected:
|
||||
"Swipe left or right beyond the rail; swipe right from the card's rail to trigger parent back.",
|
||||
issue:
|
||||
"The parent/child agreement is spatial policy written by the developer, not automatic arbitration.",
|
||||
scene: nestedScene("cooperative"),
|
||||
},
|
||||
{
|
||||
kind: "vertical",
|
||||
status: "supported",
|
||||
title: "Vertical component deck",
|
||||
summary:
|
||||
"A second nested scene uses upward/downward gestures and an entirely different frame animation.",
|
||||
expected:
|
||||
"Swipe up for the next card, down for retained back, and swipe right from its rail for parent back.",
|
||||
issue:
|
||||
"Its touch-action prevents page scrolling while a touch begins over the deck, which is appropriate here but must be intentional.",
|
||||
scene: nestedScene("vertical"),
|
||||
},
|
||||
{
|
||||
kind: "conflict",
|
||||
status: "known-conflict",
|
||||
title: "Greedy child recognizer",
|
||||
summary:
|
||||
"The cube carousel deliberately uses `.to.left()` and `.to.right()` without a `.from` constraint.",
|
||||
expected: "Nested forward/back works and remains clipped to the card.",
|
||||
issue:
|
||||
"On slide one, parent back fails everywhere over this card: the child stops pointer-down propagation before `.navigate()` returns null.",
|
||||
scene: nestedScene("conflict"),
|
||||
},
|
||||
];
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance("Nested scenes");
|
||||
|
||||
const pageBackGesture = gesture.to
|
||||
.right()
|
||||
.complete(
|
||||
({ distance, bounds, progress, velocity }) =>
|
||||
distance >= bounds.width * 0.08 || progress >= 0.22 || velocity >= 0.65,
|
||||
)
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(unfocusPortal);
|
||||
|
||||
function backWithButton() {
|
||||
if (origin.context.value.canGoBack) void origin.perform(back(unfocusPortal));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGesture class="lab-page nested-scenes-view" :gesture="pageBackGesture">
|
||||
<button
|
||||
class="view-back"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
@click="backWithButton"
|
||||
>
|
||||
← Labs
|
||||
</button>
|
||||
|
||||
<header class="nested-scenes-header">
|
||||
<div>
|
||||
<div class="eyebrow">Lab 07 · Nested origin scenes</div>
|
||||
<h1>Components can own smaller worlds.</h1>
|
||||
<p>
|
||||
Each card below contains a completely independent
|
||||
<code>OriginScene</code>. Its views, history, gestures, measurements,
|
||||
and animations stay inside that card while this page remains the outer
|
||||
origin.
|
||||
</p>
|
||||
</div>
|
||||
<div class="nested-page-instance">
|
||||
<span>Outer page instance</span>
|
||||
<strong>{{ identity.instance }}</strong>
|
||||
<small>rightward 8% → page back</small>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="nested-case-grid">
|
||||
<section
|
||||
v-for="item in cases"
|
||||
:key="item.kind"
|
||||
class="nested-case"
|
||||
:class="`nested-case--${item.kind}`"
|
||||
>
|
||||
<header>
|
||||
<span class="nested-case__status" :class="item.status">
|
||||
{{ item.status.replaceAll("-", " ") }}
|
||||
</span>
|
||||
<h2>{{ item.title }}</h2>
|
||||
<p>{{ item.summary }}</p>
|
||||
</header>
|
||||
|
||||
<div class="nested-stage-shell">
|
||||
<OriginScene class="nested-stage" :scene="item.scene" />
|
||||
<div class="nested-stage-edge" aria-hidden="true">
|
||||
{{
|
||||
item.kind === "conflict"
|
||||
? "parent blocked"
|
||||
: "parent pass-through"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nested-case__diagnostics">
|
||||
<span>
|
||||
{{ item.scene.nodes.value.length }} nested node{{
|
||||
item.scene.nodes.value.length === 1 ? "" : "s"
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{ item.scene.operations.value.length }} live edge{{
|
||||
item.scene.operations.value.length === 1 ? "" : "s"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Expected</dt>
|
||||
<dd>{{ item.expected }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>
|
||||
{{ item.kind === "conflict" ? "Predicted failure" : "Caveat" }}
|
||||
</dt>
|
||||
<dd>{{ item.issue }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="nested-scenes-footer">
|
||||
<strong>Ownership today is decided at pointer-down.</strong>
|
||||
<span>
|
||||
A future gesture arena could delay that decision and let a parent claim
|
||||
the sequence when a child declines navigation.
|
||||
</span>
|
||||
</footer>
|
||||
|
||||
<div class="nested-page-gesture">
|
||||
Parent gesture: swipe right outside a child, or from a cooperative rail
|
||||
</div>
|
||||
</OriginGesture>
|
||||
</template>
|
||||
114
apps/origins-demo/src/views/PhotoDetailView.vue
Normal file
114
apps/origins-demo/src/views/PhotoDetailView.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, getCurrentInstance, ref, type Component } from "vue";
|
||||
import {
|
||||
above,
|
||||
back,
|
||||
gesture,
|
||||
OriginGestureSurface,
|
||||
originView,
|
||||
slideRight,
|
||||
useOrigin,
|
||||
type OriginContext,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { demoPhotos, type DemoPhoto } from "../gallery-data";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import { arcSlide, coverDown } from "../motions";
|
||||
|
||||
const props = defineProps<{ photo: DemoPhoto; index: number }>();
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance(`Photo ${props.index + 1}`);
|
||||
const liked = ref(false);
|
||||
const ownComponent = getCurrentInstance()?.type as Component;
|
||||
const nextPhoto = computed(
|
||||
() => demoPhotos[(props.index + 1) % demoPhotos.length]!,
|
||||
);
|
||||
|
||||
/*
|
||||
* The target uses the same SFC definition with different props. It is still a
|
||||
* fresh scene node and Vue instance, which makes recipe identity visible.
|
||||
*/
|
||||
const nextPhotoView = () =>
|
||||
originView(
|
||||
ownComponent,
|
||||
{
|
||||
photo: nextPhoto.value,
|
||||
index: (props.index + 1) % demoPhotos.length,
|
||||
},
|
||||
{ key: `photo-${nextPhoto.value.id}`, name: nextPhoto.value.title },
|
||||
);
|
||||
const goNext = () => above(nextPhotoView(), arcSlide);
|
||||
|
||||
const goBack = (context: OriginContext, vertical = false) =>
|
||||
context.canGoBack ? back(vertical ? coverDown : slideRight) : null;
|
||||
|
||||
const nextGesture = gesture.to
|
||||
.left()
|
||||
.navigate(() => above(nextPhotoView()))
|
||||
.animate(arcSlide);
|
||||
const dismissGesture = gesture.to
|
||||
.down()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(coverDown);
|
||||
const edgeBackGesture = gesture.from
|
||||
.left("clamp(24px, 8%, 64px)")
|
||||
.to.right()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(slideRight);
|
||||
const gestures = [nextGesture, dismissGesture, edgeBackGesture] as const;
|
||||
|
||||
function close() {
|
||||
const action = goBack(origin.context.value, true);
|
||||
if (action) void origin.perform(action);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGestureSurface
|
||||
as="article"
|
||||
class="photo-detail"
|
||||
:style="{ background: photo.gradient }"
|
||||
:gestures="gestures"
|
||||
>
|
||||
<div class="photo-vignette" />
|
||||
<header class="photo-toolbar">
|
||||
<button type="button" data-origin-gesture="ignore" @click="close">
|
||||
↓ Close
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
:aria-pressed="liked"
|
||||
@click="liked = !liked"
|
||||
>
|
||||
{{ liked ? "♥ Liked" : "♡ Like" }}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<main class="photo-copy">
|
||||
<div class="eyebrow">{{ photo.location }}</div>
|
||||
<h1>{{ photo.title }}</h1>
|
||||
<p>{{ photo.description }}</p>
|
||||
<InstanceCard
|
||||
label="Photo instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
<button
|
||||
class="primary-action"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
@click="origin.perform(goNext())"
|
||||
>
|
||||
Next: {{ nextPhoto.title }}
|
||||
</button>
|
||||
<div class="gesture-map">
|
||||
<span>← next photo</span>
|
||||
<span>↓ retained previous instance</span>
|
||||
<span>left edge → retained previous instance</span>
|
||||
</div>
|
||||
</main>
|
||||
<div class="edge-marker edge-marker--left">back edge</div>
|
||||
</OriginGestureSurface>
|
||||
</template>
|
||||
105
apps/origins-demo/src/views/PlayerView.vue
Normal file
105
apps/origins-demo/src/views/PlayerView.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref } from "vue";
|
||||
import {
|
||||
OriginGesture,
|
||||
back,
|
||||
gesture,
|
||||
useOrigin,
|
||||
type OriginContext,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import { coverDown } from "../motions";
|
||||
|
||||
const origin = useOrigin();
|
||||
const identity = useDemoInstance("Player");
|
||||
const playing = ref(false);
|
||||
const position = ref(38);
|
||||
const elapsed = computed(() => `1:${String(position.value).padStart(2, "0")}`);
|
||||
const timer = window.setInterval(() => {
|
||||
if (playing.value) position.value = (position.value + 1) % 60;
|
||||
}, 1000);
|
||||
onBeforeUnmount(() => window.clearInterval(timer));
|
||||
|
||||
const dismiss = (context: OriginContext) =>
|
||||
context.canGoBack ? back(coverDown) : null;
|
||||
|
||||
const dismissGesture = gesture.to
|
||||
.down()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(coverDown);
|
||||
|
||||
function close() {
|
||||
const action = dismiss(origin.context.value);
|
||||
if (action) void origin.perform(action);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGesture class="player-view" :gesture="dismissGesture">
|
||||
<header class="player-header">
|
||||
<button type="button" data-origin-gesture="ignore" @click="close">
|
||||
⌄
|
||||
</button>
|
||||
<span>Now playing</span>
|
||||
<button type="button" data-origin-gesture="ignore">•••</button>
|
||||
</header>
|
||||
|
||||
<div class="album-art">
|
||||
<span class="album-ring album-ring--one" />
|
||||
<span class="album-ring album-ring--two" />
|
||||
<span class="album-center">O</span>
|
||||
</div>
|
||||
|
||||
<section class="track-copy">
|
||||
<div>
|
||||
<h1>Independent Frames</h1>
|
||||
<p>The Scene Graph</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
:aria-pressed="playing"
|
||||
@click="playing = !playing"
|
||||
>
|
||||
{{ playing ? "♥" : "♡" }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<label class="timeline">
|
||||
<input
|
||||
v-model="position"
|
||||
data-origin-gesture="ignore"
|
||||
type="range"
|
||||
min="0"
|
||||
max="59"
|
||||
/>
|
||||
<span>{{ elapsed }}</span>
|
||||
<span>3:42</span>
|
||||
</label>
|
||||
|
||||
<div class="player-controls">
|
||||
<button type="button" data-origin-gesture="ignore">↶</button>
|
||||
<button
|
||||
class="play-button"
|
||||
type="button"
|
||||
data-origin-gesture="ignore"
|
||||
@click="playing = !playing"
|
||||
>
|
||||
{{ playing ? "Ⅱ" : "▶" }}
|
||||
</button>
|
||||
<button type="button" data-origin-gesture="ignore">↷</button>
|
||||
</div>
|
||||
|
||||
<InstanceCard
|
||||
label="Player instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
<p class="policy-note">
|
||||
Swipe down anywhere to dismiss. Horizontal back is intentionally absent,
|
||||
like a native full-screen player.
|
||||
</p>
|
||||
<div class="drag-handle" />
|
||||
</OriginGesture>
|
||||
</template>
|
||||
67
apps/origins-demo/src/views/SecondView.vue
Normal file
67
apps/origins-demo/src/views/SecondView.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
above,
|
||||
back,
|
||||
gesture,
|
||||
OriginGestureSurface,
|
||||
originView,
|
||||
slideLeft,
|
||||
slideRight,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import ThirdView from "./ThirdView.vue";
|
||||
|
||||
defineProps<{ openedAt?: string; createdBy?: string }>();
|
||||
|
||||
const identity = useDemoInstance("Y");
|
||||
const thirdView = () =>
|
||||
originView(
|
||||
ThirdView,
|
||||
{ createdBy: identity.instance },
|
||||
{ key: "third", name: "Third" },
|
||||
);
|
||||
const forwardGesture = gesture.to
|
||||
.left()
|
||||
.navigate(() => above(thirdView()))
|
||||
.animate(slideLeft);
|
||||
const backGesture = gesture
|
||||
// .from.left("clamp(24px, 8%, 64px)")
|
||||
// .from.left("20%")
|
||||
.to.right()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(slideRight);
|
||||
const gestures = [forwardGesture, backGesture] as const;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGestureSurface
|
||||
as="main"
|
||||
class="lab-page chain-view view-two"
|
||||
:gestures="gestures"
|
||||
>
|
||||
<div class="eyebrow">Chain stress test · Origin Y</div>
|
||||
<h1>Y can move while X is still moving it.</h1>
|
||||
<p>
|
||||
X→Y supplies Y's inherited coordinate frame. This Y→Z gesture adds a
|
||||
second transform layer inside that frame.
|
||||
</p>
|
||||
<div class="metric-grid">
|
||||
<div>
|
||||
<span>Created by X</span>
|
||||
<strong>{{ createdBy }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Created at</span>
|
||||
<strong>{{ openedAt ?? "unknown" }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<InstanceCard
|
||||
label="Y Vue instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
<div class="gesture-hint">Swipe left again <span>→</span></div>
|
||||
<div class="edge-marker edge-marker--left">back edge</div>
|
||||
</OriginGestureSurface>
|
||||
</template>
|
||||
60
apps/origins-demo/src/views/ThirdView.vue
Normal file
60
apps/origins-demo/src/views/ThirdView.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
above,
|
||||
back,
|
||||
gesture,
|
||||
OriginGestureSurface,
|
||||
originView,
|
||||
slideRight,
|
||||
} from "@native-vue-router/core-v2";
|
||||
import InstanceCard from "../components/InstanceCard.vue";
|
||||
import { useDemoInstance } from "../lab-state";
|
||||
import { arcSlide } from "../motions";
|
||||
import FourthView from "./FourthView.vue";
|
||||
|
||||
defineProps<{ createdBy?: string }>();
|
||||
|
||||
const identity = useDemoInstance("Z");
|
||||
const fourthView = () =>
|
||||
originView(
|
||||
FourthView,
|
||||
{ chain: `X → Y → ${identity.instance}` },
|
||||
{ key: "fourth", name: "Omega" },
|
||||
);
|
||||
const forwardGesture = gesture.to
|
||||
.left()
|
||||
.navigate(() => above(fourthView()))
|
||||
.animate(arcSlide);
|
||||
const backGesture = gesture
|
||||
// .from.left("clamp(24px, 8%, 64px)")
|
||||
// .from.left("20%")
|
||||
.to.right()
|
||||
.navigate((context) => (context.canGoBack ? back() : null))
|
||||
.animate(slideRight);
|
||||
const gestures = [forwardGesture, backGesture] as const;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OriginGestureSurface
|
||||
as="main"
|
||||
class="lab-page chain-view view-three"
|
||||
:gestures="gestures"
|
||||
>
|
||||
<div class="eyebrow">Chain stress test · Origin Z</div>
|
||||
<h1>Now add an arc while two frames may still be moving.</h1>
|
||||
<p>
|
||||
Z inherits X→Y and Y→Z motion, then originates its own frame contribution.
|
||||
The shared frame lifts both Z and Ω through a small arc.
|
||||
</p>
|
||||
<div class="proof">
|
||||
Created by Y instance <strong>{{ createdBy }}</strong>
|
||||
</div>
|
||||
<InstanceCard
|
||||
label="Z Vue instance"
|
||||
:instance="identity.instance"
|
||||
:status="identity.status.value"
|
||||
/>
|
||||
<div class="gesture-hint">Swipe left for Ω <span>→</span></div>
|
||||
<div class="edge-marker edge-marker--left">back edge</div>
|
||||
</OriginGestureSurface>
|
||||
</template>
|
||||
11
apps/origins-demo/tsconfig.json
Normal file
11
apps/origins-demo/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "../../node_modules/.tmp/origins-demo.tsbuildinfo",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@native-vue-router/core-v2": ["../../packages/core-v2/src/index.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||
}
|
||||
174
apps/origins-demo/vite.config.ts
Normal file
174
apps/origins-demo/vite.config.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import path from "node:path";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig, type Plugin } from "vite";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
|
||||
/**
|
||||
* Recover browsers that visited this hostname while Docker still served Vite's
|
||||
* development mode. Its development worker can retain HTML that imports
|
||||
* `/@vite/client` and `/src/main.ts`; a normal static fallback would return
|
||||
* HTML for those module URLs and leave the visitor on a blank screen.
|
||||
*
|
||||
* This preview-only compatibility endpoint is requested only by that obsolete
|
||||
* shell. It clears the old origin-local worker/cache state and writes the
|
||||
* current production HTML into the document. The production application then
|
||||
* registers the real `/sw.js` worker normally.
|
||||
*/
|
||||
function legacyDevelopmentWorkerMigration(): Plugin {
|
||||
const migrationSource = `
|
||||
void (async () => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
await Promise.all(registrations.map((registration) => registration.unregister()));
|
||||
}
|
||||
if ("caches" in window) {
|
||||
const names = await caches.keys();
|
||||
await Promise.all(names.map((name) => caches.delete(name)));
|
||||
}
|
||||
|
||||
const productionDocument = await fetch(
|
||||
"/index.html?origin-pwa-migration=" + Date.now(),
|
||||
{ cache: "no-store" },
|
||||
);
|
||||
const html = await productionDocument.text();
|
||||
document.open();
|
||||
document.write(html);
|
||||
document.close();
|
||||
})();
|
||||
`;
|
||||
|
||||
return {
|
||||
name: "origins-legacy-development-worker-migration",
|
||||
configurePreviewServer(server) {
|
||||
server.middlewares.use((request, response, next) => {
|
||||
const pathname = new URL(request.url ?? "/", "http://origins.local")
|
||||
.pathname;
|
||||
|
||||
if (pathname === "/@vite/client") {
|
||||
response.statusCode = 200;
|
||||
response.setHeader("Content-Type", "text/javascript; charset=utf-8");
|
||||
response.setHeader("Cache-Control", "no-store");
|
||||
response.end(
|
||||
"// Compatibility stub for the retired Vite dev client.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === "/src/main.ts") {
|
||||
response.statusCode = 200;
|
||||
response.setHeader("Content-Type", "text/javascript; charset=utf-8");
|
||||
response.setHeader("Cache-Control", "no-store");
|
||||
response.end(migrationSource);
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
base: "./",
|
||||
// Reuse the workspace's existing install icons rather than maintaining a
|
||||
// second set that can silently drift from the main demo.
|
||||
publicDir: path.resolve(__dirname, "../../public"),
|
||||
plugins: [
|
||||
vue(),
|
||||
legacyDevelopmentWorkerMigration(),
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
devOptions: {
|
||||
// The hosted Docker service runs Vite's development server, so its
|
||||
// public HTTPS URL also needs a real manifest and service worker.
|
||||
enabled: true,
|
||||
navigateFallback: "index.html",
|
||||
suppressWarnings: true,
|
||||
},
|
||||
includeAssets: [
|
||||
"favicon.svg",
|
||||
"app-icon.svg",
|
||||
"apple-touch-icon.png",
|
||||
"pwa-192.png",
|
||||
"pwa-512.png",
|
||||
],
|
||||
manifest: {
|
||||
id: "/",
|
||||
name: "Routeless Origins Lab",
|
||||
short_name: "Origins Lab",
|
||||
description:
|
||||
"Seven interactive labs for a routeless Vue animation and gesture engine.",
|
||||
theme_color: "#080b12",
|
||||
background_color: "#080b12",
|
||||
display: "standalone",
|
||||
scope: "/",
|
||||
start_url: "/",
|
||||
orientation: "any",
|
||||
icons: [
|
||||
{
|
||||
src: "/pwa-192.png",
|
||||
sizes: "192x192",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/pwa-512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "any",
|
||||
},
|
||||
{
|
||||
src: "/pwa-512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
{
|
||||
src: "/app-icon.svg",
|
||||
sizes: "any",
|
||||
type: "image/svg+xml",
|
||||
purpose: "any",
|
||||
},
|
||||
],
|
||||
},
|
||||
workbox: {
|
||||
cleanupOutdatedCaches: true,
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
navigateFallback: "/index.html",
|
||||
globPatterns: ["**/*.{js,css,html,svg,png,woff2}"],
|
||||
},
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
// The development container is reached through this Traefik hostname.
|
||||
// Keeping the allow-list explicit preserves Vite's DNS-rebinding defense.
|
||||
allowedHosts: ["v2.demo.native-router.harvmaster.com"],
|
||||
},
|
||||
preview: {
|
||||
allowedHosts: ["v2.demo.native-router.harvmaster.com"],
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^@native-vue-router\/core-v2\/style\.css$/,
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
"../../packages/core-v2/src/style.css",
|
||||
),
|
||||
},
|
||||
{
|
||||
find: /^@native-vue-router\/core-v2$/,
|
||||
replacement: path.resolve(
|
||||
__dirname,
|
||||
"../../packages/core-v2/src/index.ts",
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, "dist"),
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user