feat(unified-mobile-material-design): Complete US-112 - Creare BottomSheet.vue component pentru filtre
Implemented by Ralph autonomous loop. Iteration: 9 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -153,8 +153,8 @@
|
||||
"Slot default pentru conținut",
|
||||
"npm run build passes"
|
||||
],
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
"passes": true,
|
||||
"notes": "Completed in iteration 9"
|
||||
},
|
||||
{
|
||||
"id": "US-106",
|
||||
|
||||
@@ -54,3 +54,9 @@ Mon Jan 12 09:44:54 AM UTC 2026
|
||||
[2026-01-12 10:00:20] Working on story: US-105
|
||||
[2026-01-12 10:00:20] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_8_US-105.log)
|
||||
[2026-01-12 10:03:01] SUCCESS: Story US-105 passed!
|
||||
[2026-01-12 10:03:01] Changes committed
|
||||
[2026-01-12 10:03:01] Progress: 7/20 stories completed
|
||||
[2026-01-12 10:03:03] === Iteration 9/100 ===
|
||||
[2026-01-12 10:03:03] Working on story: US-112
|
||||
[2026-01-12 10:03:03] Running Claude... (log: /workspace/roa2web/scripts/ralph/logs/iteration_9_US-112.log)
|
||||
[2026-01-12 10:04:31] SUCCESS: Story US-112 passed!
|
||||
|
||||
281
src/shared/components/mobile/BottomSheet.vue
Normal file
281
src/shared/components/mobile/BottomSheet.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="bottom-sheet">
|
||||
<div v-if="modelValue" class="bottom-sheet-overlay" @click.self="handleOverlayClick">
|
||||
<div
|
||||
class="bottom-sheet"
|
||||
ref="sheetRef"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
>
|
||||
<!-- Drag Handle -->
|
||||
<div class="bottom-sheet-handle" @click="close">
|
||||
<div class="handle-bar"></div>
|
||||
</div>
|
||||
|
||||
<!-- Content Slot -->
|
||||
<div class="bottom-sheet-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
/**
|
||||
* BottomSheet - Material Design 3 inspired bottom sheet for mobile filters and actions
|
||||
*
|
||||
* Props:
|
||||
* - modelValue (v-model:visible): Controls visibility of the bottom sheet
|
||||
* - closeOnOverlay: Whether to close when clicking the overlay (default: true)
|
||||
* - closeOnSwipeDown: Whether to close when swiping down on handle (default: true)
|
||||
*
|
||||
* Events:
|
||||
* - update:modelValue: Emitted when visibility changes (for v-model support)
|
||||
*
|
||||
* Slots:
|
||||
* - default: Content to display inside the bottom sheet
|
||||
*
|
||||
* Features:
|
||||
* - Smooth slide-up animation
|
||||
* - Drag handle at top for visual affordance
|
||||
* - Close on tap outside (overlay)
|
||||
* - Swipe down to close (touch gesture)
|
||||
* - Teleported to body to avoid z-index issues
|
||||
*
|
||||
* Usage:
|
||||
* <BottomSheet v-model="isFilterOpen">
|
||||
* <FilterContent />
|
||||
* </BottomSheet>
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* Controls visibility of the bottom sheet (v-model support)
|
||||
*/
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/**
|
||||
* Whether clicking the overlay closes the sheet
|
||||
*/
|
||||
closeOnOverlay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
/**
|
||||
* Whether swiping down on the handle closes the sheet
|
||||
*/
|
||||
closeOnSwipeDown: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// Reference to the sheet element for touch handling
|
||||
const sheetRef = ref(null)
|
||||
|
||||
// Touch tracking state
|
||||
let touchStartY = 0
|
||||
let touchCurrentY = 0
|
||||
const SWIPE_THRESHOLD = 100 // Minimum swipe distance to close
|
||||
|
||||
/**
|
||||
* Close the bottom sheet
|
||||
*/
|
||||
const close = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click on overlay background
|
||||
*/
|
||||
const handleOverlayClick = () => {
|
||||
if (props.closeOnOverlay) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch start for swipe gesture
|
||||
*/
|
||||
const handleTouchStart = (event) => {
|
||||
if (!props.closeOnSwipeDown) return
|
||||
touchStartY = event.touches[0].clientY
|
||||
touchCurrentY = touchStartY
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch move for swipe gesture
|
||||
*/
|
||||
const handleTouchMove = (event) => {
|
||||
if (!props.closeOnSwipeDown) return
|
||||
touchCurrentY = event.touches[0].clientY
|
||||
|
||||
// Calculate drag distance (only allow downward movement)
|
||||
const dragDistance = Math.max(0, touchCurrentY - touchStartY)
|
||||
|
||||
// Apply transform for visual feedback
|
||||
if (sheetRef.value && dragDistance > 0) {
|
||||
sheetRef.value.style.transform = `translateY(${dragDistance}px)`
|
||||
sheetRef.value.style.transition = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle touch end - close if swiped enough
|
||||
*/
|
||||
const handleTouchEnd = () => {
|
||||
if (!props.closeOnSwipeDown || !sheetRef.value) return
|
||||
|
||||
const dragDistance = touchCurrentY - touchStartY
|
||||
|
||||
// Reset transform and enable transition
|
||||
sheetRef.value.style.transition = ''
|
||||
sheetRef.value.style.transform = ''
|
||||
|
||||
// Close if dragged past threshold
|
||||
if (dragDistance > SWIPE_THRESHOLD) {
|
||||
close()
|
||||
}
|
||||
|
||||
// Reset touch tracking
|
||||
touchStartY = 0
|
||||
touchCurrentY = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ================================================
|
||||
BottomSheet Component Styles
|
||||
Material Design 3 inspired bottom sheet
|
||||
================================================ */
|
||||
|
||||
/* Overlay background */
|
||||
.bottom-sheet-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: var(--z-modal-backdrop);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Main bottom sheet container */
|
||||
.bottom-sheet {
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
background: var(--surface-card);
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
box-shadow: var(--shadow-xl);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: var(--z-modal);
|
||||
}
|
||||
|
||||
/* Drag handle container */
|
||||
.bottom-sheet-handle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
cursor: pointer;
|
||||
/* Touch target minimum size */
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
/* Drag handle bar (visual indicator) */
|
||||
.handle-bar {
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
background: var(--surface-border);
|
||||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
/* Content area with scroll support */
|
||||
.bottom-sheet-content {
|
||||
flex: 1;
|
||||
padding: 0 var(--space-md) var(--space-lg);
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
Slide-up Animation (Vue Transition)
|
||||
================================================ */
|
||||
|
||||
.bottom-sheet-enter-active,
|
||||
.bottom-sheet-leave-active {
|
||||
transition: opacity var(--transition-normal);
|
||||
}
|
||||
|
||||
.bottom-sheet-enter-active .bottom-sheet,
|
||||
.bottom-sheet-leave-active .bottom-sheet {
|
||||
transition: transform var(--transition-normal);
|
||||
}
|
||||
|
||||
/* Initial state: invisible + sheet below viewport */
|
||||
.bottom-sheet-enter-from,
|
||||
.bottom-sheet-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bottom-sheet-enter-from .bottom-sheet,
|
||||
.bottom-sheet-leave-to .bottom-sheet {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
/* Final state: visible + sheet in place */
|
||||
.bottom-sheet-enter-to,
|
||||
.bottom-sheet-leave-from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.bottom-sheet-enter-to .bottom-sheet,
|
||||
.bottom-sheet-leave-from .bottom-sheet {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
Dark Mode Support
|
||||
================================================ */
|
||||
|
||||
/* Manual dark mode via data-theme attribute */
|
||||
[data-theme="dark"] .bottom-sheet-overlay {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .bottom-sheet {
|
||||
background: var(--surface-card);
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .handle-bar {
|
||||
background: var(--surface-border);
|
||||
}
|
||||
|
||||
/* Auto dark mode (when no manual theme is set) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .bottom-sheet-overlay {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
:root:not([data-theme]) .bottom-sheet {
|
||||
background: var(--surface-card);
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
:root:not([data-theme]) .handle-bar {
|
||||
background: var(--surface-border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user