Switch rolodex to vertical swipe (avoids Pixel back gesture conflict)
Swipe up/down instead of left/right. rotateX drum effect instead of rotateY. ChevronUp/Down buttons. No horizontal touch tracking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
73a416d423
commit
623c3783b5
1 changed files with 20 additions and 36 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useRef } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
import { Pencil, Trash2, ChevronLeft, ChevronRight } from 'lucide-react'
|
import { Pencil, Trash2, ChevronUp, ChevronDown } from 'lucide-react'
|
||||||
import type { Store } from '../types'
|
import type { Store } from '../types'
|
||||||
import { RouteBadge } from './RouteBadge'
|
import { RouteBadge } from './RouteBadge'
|
||||||
import { TagChip } from './TagChip'
|
import { TagChip } from './TagChip'
|
||||||
|
|
@ -17,7 +17,7 @@ export function MobileRolodex({ stores, onEdit, onDelete }: Props) {
|
||||||
const [dragOffset, setDragOffset] = useState(0)
|
const [dragOffset, setDragOffset] = useState(0)
|
||||||
const touchStartX = useRef(0)
|
const touchStartX = useRef(0)
|
||||||
const touchStartY = useRef(0)
|
const touchStartY = useRef(0)
|
||||||
const isHorizontalDrag = useRef<boolean | null>(null)
|
const isVerticalDrag = useRef<boolean | null>(null)
|
||||||
|
|
||||||
const total = stores.length
|
const total = stores.length
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ export function MobileRolodex({ stores, onEdit, onDelete }: Props) {
|
||||||
function onTouchStart(e: React.TouchEvent) {
|
function onTouchStart(e: React.TouchEvent) {
|
||||||
touchStartX.current = e.touches[0].clientX
|
touchStartX.current = e.touches[0].clientX
|
||||||
touchStartY.current = e.touches[0].clientY
|
touchStartY.current = e.touches[0].clientY
|
||||||
isHorizontalDrag.current = null
|
isVerticalDrag.current = null
|
||||||
setDragging(true)
|
setDragging(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,41 +38,41 @@ export function MobileRolodex({ stores, onEdit, onDelete }: Props) {
|
||||||
const dy = e.touches[0].clientY - touchStartY.current
|
const dy = e.touches[0].clientY - touchStartY.current
|
||||||
|
|
||||||
// Determine drag axis on first significant move
|
// Determine drag axis on first significant move
|
||||||
if (isHorizontalDrag.current === null && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) {
|
if (isVerticalDrag.current === null && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) {
|
||||||
isHorizontalDrag.current = Math.abs(dx) > Math.abs(dy)
|
isVerticalDrag.current = Math.abs(dy) > Math.abs(dx)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHorizontalDrag.current) {
|
if (isVerticalDrag.current) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
// Dampen drag at edges
|
// Dampen drag at edges
|
||||||
const atStart = index === 0 && dx > 0
|
const atStart = index === 0 && dy > 0
|
||||||
const atEnd = index === total - 1 && dx < 0
|
const atEnd = index === total - 1 && dy < 0
|
||||||
setDragOffset(atStart || atEnd ? dx * 0.2 : dx)
|
setDragOffset(atStart || atEnd ? dy * 0.2 : dy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTouchEnd() {
|
function onTouchEnd() {
|
||||||
setDragging(false)
|
setDragging(false)
|
||||||
if (isHorizontalDrag.current && Math.abs(dragOffset) > 60) {
|
if (isVerticalDrag.current && Math.abs(dragOffset) > 60) {
|
||||||
dragOffset < 0 ? goTo(index + 1) : goTo(index - 1)
|
dragOffset < 0 ? goTo(index + 1) : goTo(index - 1)
|
||||||
} else {
|
} else {
|
||||||
setDragOffset(0)
|
setDragOffset(0)
|
||||||
}
|
}
|
||||||
isHorizontalDrag.current = null
|
isVerticalDrag.current = null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (total === 0) return null
|
if (total === 0) return null
|
||||||
|
|
||||||
// Positions relative to current index
|
// Positions relative to current index
|
||||||
function getCardStyle(offset: number): React.CSSProperties {
|
function getCardStyle(offset: number): React.CSSProperties {
|
||||||
const x = offset * 88 + dragOffset * (offset === 0 ? 1 : 0.15)
|
const y = offset * 88 + dragOffset * (offset === 0 ? 1 : 0.15)
|
||||||
const scale = offset === 0 ? 1 : 0.88
|
const scale = offset === 0 ? 1 : 0.88
|
||||||
const opacity = Math.abs(offset) > 1 ? 0 : offset === 0 ? 1 : 0.5
|
const opacity = Math.abs(offset) > 1 ? 0 : offset === 0 ? 1 : 0.5
|
||||||
const zIndex = offset === 0 ? 10 : Math.abs(offset) === 1 ? 5 : 0
|
const zIndex = offset === 0 ? 10 : Math.abs(offset) === 1 ? 5 : 0
|
||||||
const rotateY = offset === 0 ? (dragOffset * -0.04) : (offset > 0 ? -6 : 6)
|
const rotateX = offset === 0 ? (dragOffset * 0.04) : (offset > 0 ? 6 : -6)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transform: `translateX(${x}%) scale(${scale}) rotateY(${rotateY}deg)`,
|
transform: `translateY(${y}%) scale(${scale}) rotateX(${rotateX}deg)`,
|
||||||
opacity,
|
opacity,
|
||||||
zIndex,
|
zIndex,
|
||||||
transition: dragging ? 'none' : 'all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
transition: dragging ? 'none' : 'all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
|
|
@ -149,36 +149,20 @@ export function MobileRolodex({ stores, onEdit, onDelete }: Props) {
|
||||||
disabled={index === 0}
|
disabled={index === 0}
|
||||||
className="w-9 h-9 flex items-center justify-center rounded-xl glass text-slate-400 hover:text-slate-200 disabled:opacity-25 disabled:cursor-not-allowed transition-colors"
|
className="w-9 h-9 flex items-center justify-center rounded-xl glass text-slate-400 hover:text-slate-200 disabled:opacity-25 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
<ChevronLeft size={18} />
|
<ChevronUp size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Dot indicators — show up to 9, scroll window for larger sets */}
|
{/* Counter */}
|
||||||
<div className="flex items-center gap-1.5 overflow-hidden max-w-[160px]">
|
|
||||||
{total <= 9 ? (
|
|
||||||
Array.from({ length: total }, (_, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
onClick={() => goTo(i)}
|
|
||||||
className={`rounded-full transition-all ${
|
|
||||||
i === index
|
|
||||||
? 'w-5 h-2 bg-blue-400'
|
|
||||||
: 'w-2 h-2 bg-slate-700 hover:bg-slate-500'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span className="text-sm font-medium text-slate-400 tabular-nums">
|
<span className="text-sm font-medium text-slate-400 tabular-nums">
|
||||||
{index + 1} <span className="text-slate-600">/ {total}</span>
|
{index + 1} <span className="text-slate-600">/ {total}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => goTo(index + 1)}
|
onClick={() => goTo(index + 1)}
|
||||||
disabled={index === total - 1}
|
disabled={index === total - 1}
|
||||||
className="w-9 h-9 flex items-center justify-center rounded-xl glass text-slate-400 hover:text-slate-200 disabled:opacity-25 disabled:cursor-not-allowed transition-colors"
|
className="w-9 h-9 flex items-center justify-center rounded-xl glass text-slate-400 hover:text-slate-200 disabled:opacity-25 disabled:cursor-not-allowed transition-colors"
|
||||||
>
|
>
|
||||||
<ChevronRight size={18} />
|
<ChevronDown size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue