diff --git a/apps/web/src/components/viewers/DeckViewer.tsx b/apps/web/src/components/viewers/DeckViewer.tsx index 01ab62e..9a4817d 100644 --- a/apps/web/src/components/viewers/DeckViewer.tsx +++ b/apps/web/src/components/viewers/DeckViewer.tsx @@ -1,5 +1,5 @@ import { useMemo, useRef, useState } from 'react'; -import type { RefObject } from 'react'; +import type { RefObject, WheelEvent } from 'react'; import { useFrame } from '@react-three/fiber'; import * as THREE from 'three'; import type { TTSObject } from '@tts/shared'; @@ -223,7 +223,14 @@ function arcRadius(cardWidth: number | null): number { return Math.max(ARC_RADIUS, (cardWidth / 2 + ARC_PADDING) / Math.sin(ARC_STEP)); } -/** Prev/next controls and a counter, rendered as the scene overlay. */ +/** + * Prev/next controls and a sliding-dot pagination, rendered as the scene + * overlay. All dots live in a fixed-width track that slides so the active dot + * stays centered; dots fade out as they leave the window. A counter above the + * bar shows the current index (`1 / 51`). Round first/last and prev/next + * buttons flank the dots. Scrolling over the pagination steps through the + * deck. + */ function CarouselControls({ active, count, @@ -235,25 +242,95 @@ function CarouselControls({ }) { const prev = () => onStep(-1); const next = () => onStep(1); + const first = () => onStep(-active); + const last = () => onStep(count - 1 - active); + + // Dots visible on each side of the active dot. + const DOT_WINDOW = 4; + const DOT_SIZE = 8; + const ACTIVE_WIDTH = 26; + const GAP = 6; + const SLOTS = DOT_WINDOW * 2 + 1; + const slotW = DOT_SIZE + GAP; + // Extra width the active dot (plus its gap) pushes beyond a normal slot. + const extra = ACTIVE_WIDTH + GAP - slotW; + const trackW = SLOTS * slotW + extra; + // Left offset of a dot at slot `s` (0 = leftmost). Dots after the active + // slot shift right by `extra` to make room for the wider active dot. + const slotLeft = (s: number) => s * slotW + (s > DOT_WINDOW ? extra : 0); + const dots = Array.from({ length: count }, (_, i) => i); + + const onWheel = (e: React.WheelEvent) => { + // Scroll up/left goes back, scroll down/right goes forward. + onStep(e.deltaY < 0 ? -1 : 1); + }; + + const roundBtn = + 'flex h-8 w-8 items-center justify-center rounded-full border border-zinc-700 bg-zinc-900/80 text-zinc-300 backdrop-blur transition hover:bg-zinc-800 hover:text-zinc-100 disabled:opacity-40 disabled:hover:bg-zinc-900/80 disabled:hover:text-zinc-300'; + return ( -