167 lines
4.1 KiB
Vue
167 lines
4.1 KiB
Vue
<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>
|