diff --git a/frontend/src/components/BaseBottomSheet.vue b/frontend/src/components/BaseBottomSheet.vue index 0075bba..f1ce6b1 100644 --- a/frontend/src/components/BaseBottomSheet.vue +++ b/frontend/src/components/BaseBottomSheet.vue @@ -13,9 +13,21 @@
- @@ -36,12 +48,67 @@ const emit = defineEmits<{ }>() const sheetRef = ref(null) +const dragY = ref(0) +const isDragging = ref(false) +let dragStartY = 0 +let pointerId: number | null = null let triggerEl: HTMLElement | null = null +const DISMISS_THRESHOLD = 80 + function close() { emit('update:modelValue', false) } +function onDragStart(e: TouchEvent) { + dragStartY = e.touches[0].clientY + isDragging.value = true + dragY.value = 0 +} + +function onDragMove(e: TouchEvent) { + if (!isDragging.value) return + const delta = e.touches[0].clientY - dragStartY + dragY.value = delta > 0 ? delta : 0 +} + +function onDragEnd() { + if (!isDragging.value) return + isDragging.value = false + if (dragY.value > DISMISS_THRESHOLD) { + close() + } + dragY.value = 0 +} + +// Pointer events let desktop / non-touch testing exercise the drag too. +function onPointerStart(e: PointerEvent) { + if (e.pointerType === 'touch') return // touch events handle this + dragStartY = e.clientY + isDragging.value = true + dragY.value = 0 + pointerId = e.pointerId + ;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId) + window.addEventListener('pointermove', onPointerMove) + window.addEventListener('pointerup', onPointerEnd) + window.addEventListener('pointercancel', onPointerEnd) +} + +function onPointerMove(e: PointerEvent) { + if (!isDragging.value || e.pointerId !== pointerId) return + const delta = e.clientY - dragStartY + dragY.value = delta > 0 ? delta : 0 +} + +function onPointerEnd(e: PointerEvent) { + if (e.pointerId !== pointerId) return + pointerId = null + window.removeEventListener('pointermove', onPointerMove) + window.removeEventListener('pointerup', onPointerEnd) + window.removeEventListener('pointercancel', onPointerEnd) + onDragEnd() +} + watch(() => props.modelValue, async (open) => { if (open) { triggerEl = document.activeElement as HTMLElement @@ -50,6 +117,8 @@ watch(() => props.modelValue, async (open) => { } else { triggerEl?.focus() triggerEl = null + dragY.value = 0 + isDragging.value = false } }) @@ -68,17 +137,32 @@ watch(() => props.modelValue, async (open) => { width: 100%; background: var(--color-surface); border-radius: var(--radius-lg) var(--radius-lg) 0 0; - padding: var(--space-3) var(--space-4) calc(var(--space-6) + env(safe-area-inset-bottom)); + padding: 0 var(--space-4) calc(var(--space-6) + env(safe-area-inset-bottom)); max-height: 90dvh; overflow-y: auto; outline: none; + transition: transform 200ms var(--ease-out); + + &--dragging { + transition: none; + } + + &__handle-target { + padding: var(--space-3) 0 var(--space-4); + margin: 0 calc(-1 * var(--space-4)); + display: flex; + justify-content: center; + cursor: grab; + touch-action: none; + + &:active { cursor: grabbing; } + } &__handle { width: 36px; height: 4px; border-radius: var(--radius-full); background: var(--color-border); - margin: 0 auto var(--space-4); } } diff --git a/frontend/src/components/BaseInput.vue b/frontend/src/components/BaseInput.vue index a01125b..05718d1 100644 --- a/frontend/src/components/BaseInput.vue +++ b/frontend/src/components/BaseInput.vue @@ -46,8 +46,8 @@ const inputId = computed(() => props.id ?? `input-${Math.random().toString(36).s &__field { width: 100%; - min-height: var(--touch-min); - padding: var(--space-4) var(--space-4) var(--space-2); + min-height: 56px; + padding: 22px var(--space-4) 8px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-surface); @@ -67,8 +67,10 @@ const inputId = computed(() => props.id ?? `input-${Math.random().toString(36).s &:not(:placeholder-shown) ~ .input-wrap__label, &:focus ~ .input-wrap__label { - transform: translateY(-10px) scale(0.78); + top: 8px; + font-size: var(--text-xs); color: var(--color-primary); + transform: none; } } @@ -80,8 +82,7 @@ const inputId = computed(() => props.id ?? `input-${Math.random().toString(36).s color: var(--color-text-muted); font-size: var(--text-base); pointer-events: none; - transform-origin: left center; - transition: transform var(--duration-fast), color var(--duration-fast); + transition: top var(--duration-fast), font-size var(--duration-fast), color var(--duration-fast), transform var(--duration-fast); } &--error &__field { diff --git a/frontend/src/components/FrameCard.vue b/frontend/src/components/FrameCard.vue index a620f7a..38ea644 100644 --- a/frontend/src/components/FrameCard.vue +++ b/frontend/src/components/FrameCard.vue @@ -3,15 +3,9 @@ :class="[ 'frame-card', `frame-card--${size}`, - status !== 'ok' && `frame-card--${status}`, + `frame-card--${status}`, ]" > - -
-
-