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",
|
"Slot default pentru conținut",
|
||||||
"npm run build passes"
|
"npm run build passes"
|
||||||
],
|
],
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed in iteration 9"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-106",
|
"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] 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: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] 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