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:
Claude Agent
2026-01-12 10:04:31 +00:00
parent 8448f4c37e
commit 99ceeeff0a
3 changed files with 289 additions and 2 deletions

View File

@@ -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",

View File

@@ -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!

View 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>