75 lines
4.1 KiB
Vue
75 lines
4.1 KiB
Vue
<!--
|
|
Signature 2 · the rete. 15 scroll steps inside one pinned viewport.
|
|
RTL semantics throughout: the next product waits on the inline-end (left) side, so
|
|
panels enter from the left, the progress rule fills right→left, ArrowLeft = next,
|
|
and a rightward swipe pulls the next star under the rule.
|
|
-->
|
|
<script setup lang="ts">
|
|
import { gsap } from "gsap";
|
|
import { products, families, pad } from "~/data/products";
|
|
const story = useStory();
|
|
const panel = ref<HTMLElement | null>(null);
|
|
const p = computed(() => products[story.value.active]);
|
|
const fam = computed(() => families.find((f) => f.id === p.value.family)!);
|
|
|
|
watch(() => story.value.active, async () => {
|
|
await nextTick();
|
|
if (!panel.value || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
gsap.fromTo(panel.value.children, { x: story.value.dir > 0 ? -36 : 36, opacity: 0 }, { x: 0, opacity: 1, duration: 0.7, ease: "expo.out", stagger: 0.06, overwrite: true });
|
|
});
|
|
|
|
let t0: [number, number] | null = null;
|
|
const onTouchStart = (e: TouchEvent) => { const t = e.touches[0]; t0 = [t.clientX, t.clientY]; };
|
|
const onTouchEnd = (e: TouchEvent) => {
|
|
if (!t0) return; const t = e.changedTouches[0];
|
|
const dx = t.clientX - t0[0], dy = t.clientY - t0[1]; t0 = null;
|
|
if (Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy) * 1.3) story.value.go?.(story.value.active + (dx > 0 ? 1 : -1));
|
|
};
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "ArrowLeft") { e.preventDefault(); story.value.go?.(story.value.active + 1); }
|
|
if (e.key === "ArrowRight") { e.preventDefault(); story.value.go?.(story.value.active - 1); }
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<section id="products" aria-labelledby="eco-h" class="relative" style="height: calc(100svh + 15 * 62svh)">
|
|
<div
|
|
class="sticky top-0 h-[100svh] touch-pan-y overflow-hidden outline-none" tabindex="0" role="group"
|
|
aria-roledescription="چرخ محصولات" aria-label="پانزده محصول رصد؛ با کلیدهای جهت حرکت کنید"
|
|
@touchstart.passive="onTouchStart" @touchend="onTouchEnd" @keydown="onKey"
|
|
>
|
|
<h2 id="eco-h" class="sr-only">پانزده محصول، یک آسمان</h2>
|
|
<div class="absolute inset-x-0 bottom-[84px] px-[6vw] md:inset-y-0 md:bottom-auto md:start-0 md:end-auto md:flex md:w-[40vw] md:flex-col md:justify-center md:pb-[72px] md:pe-0 md:pt-[124px]" aria-live="polite">
|
|
<div ref="panel">
|
|
<div>
|
|
<p class="mb-2 flex items-baseline gap-3 text-[15px] md:mb-4">
|
|
<span class="font-medium tabular-nums">{{ pad(story.active + 1) }}<span class="text-mute"> / ۱۵</span></span>
|
|
<span class="text-verm">{{ fam.fa }}</span>
|
|
<span class="ms-auto text-[14px] text-mute md:hidden">بکشید ↔</span>
|
|
</p>
|
|
<h3 class="flex items-baseline gap-4 leading-none">
|
|
<span class="text-[clamp(3rem,min(7vw,12vh),7.4rem)] font-black">{{ p.fa }}</span>
|
|
<bdi dir="ltr" class="font-mono text-[15px] text-mute">{{ p.en }}</bdi>
|
|
</h3>
|
|
<p class="mt-3 max-w-[32ch] text-[17px] leading-7 md:mt-5 md:text-[20px] md:leading-8">{{ p.line }}</p>
|
|
</div>
|
|
<div class="mt-5 max-w-[470px] md:mt-8">
|
|
<ProductScreen :product="p" :compact="story.mobile" />
|
|
<p class="mt-3 flex items-center gap-2 text-[15px] md:mt-4"><span class="h-[6px] w-[6px] rounded-full bg-verm" />{{ p.value }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<nav aria-label="فهرست محصولات" class="absolute inset-x-[6vw] bottom-7 flex items-end gap-[6px] md:bottom-9">
|
|
<button
|
|
v-for="(pr, i) in products" :key="pr.slug" :aria-label="`${pr.fa} (${pr.en})`" :aria-current="i === story.active"
|
|
class="group flex h-6 flex-1 items-end" :class="i > 0 && pr.family !== products[i - 1].family ? 'ms-3 md:ms-5' : ''"
|
|
@click="story.go?.(i)"
|
|
>
|
|
<span class="block w-full transition-all duration-500"
|
|
:class="i === story.active ? 'h-[18px] bg-verm' : i < story.active ? 'h-[3px] bg-ink' : 'h-[3px] bg-ink/20 group-hover:bg-ink/50'" />
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
</section>
|
|
</template>
|