feat(aep): carte AEP — push Gitea 2026-04-28
This commit is contained in:
218
components/MobileSheet.vue
Normal file
218
components/MobileSheet.vue
Normal file
@@ -0,0 +1,218 @@
|
||||
<!--
|
||||
MobileSheet — Bottom sheet swipable 3 états (collapsed / half / full)
|
||||
Pattern Google Maps mobile
|
||||
|
||||
États :
|
||||
- collapsed : juste la poignée + compteur (~56px)
|
||||
- half : ~50dvh (état initial)
|
||||
- full : ~92dvh (plein écran)
|
||||
|
||||
Déclenché par touch/drag sur la poignée
|
||||
-->
|
||||
<template>
|
||||
<div
|
||||
class="mobile-sheet"
|
||||
:class="`mobile-sheet--${state}`"
|
||||
:style="{ transform: `translateY(${dragOffset}px)` }"
|
||||
>
|
||||
<!-- Poignée drag -->
|
||||
<div
|
||||
class="sheet-handle-area"
|
||||
@touchstart.passive="onTouchStart"
|
||||
@touchmove.passive="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
>
|
||||
<div class="sheet-handle-bar" />
|
||||
</div>
|
||||
|
||||
<!-- Compteur compact (toujours visible) -->
|
||||
<div class="sheet-header" @click="onHeaderClick">
|
||||
<span class="sheet-counter">
|
||||
<span v-if="pending">Chargement…</span>
|
||||
<span v-else>{{ resultCount }} fiche{{ resultCount > 1 ? 's' : '' }}</span>
|
||||
</span>
|
||||
<svg
|
||||
width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round" aria-hidden="true"
|
||||
class="sheet-chevron"
|
||||
:class="{ 'sheet-chevron--up': state !== 'collapsed' }"
|
||||
style="color: var(--nav-text-muted); flex-shrink: 0;"
|
||||
>
|
||||
<polyline points="18 15 12 9 6 15"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Contenu scrollable (caché si collapsed) -->
|
||||
<div
|
||||
v-show="state !== 'collapsed'"
|
||||
ref="contentEl"
|
||||
class="sheet-content"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type SheetState = 'collapsed' | 'half' | 'full'
|
||||
|
||||
const props = defineProps<{
|
||||
resultCount: number
|
||||
pending?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'state-change': [state: SheetState]
|
||||
}>()
|
||||
|
||||
const state = ref<SheetState>('half')
|
||||
const dragOffset = ref(0)
|
||||
const contentEl = ref<HTMLElement | null>(null)
|
||||
|
||||
let touchStartY = 0
|
||||
let touchCurrentY = 0
|
||||
let isDragging = false
|
||||
|
||||
// Cycle états au clic header
|
||||
function onHeaderClick() {
|
||||
if (state.value === 'collapsed') {
|
||||
setSheetState('half')
|
||||
} else if (state.value === 'half') {
|
||||
setSheetState('full')
|
||||
} else {
|
||||
setSheetState('collapsed')
|
||||
}
|
||||
}
|
||||
|
||||
function setSheetState(next: SheetState) {
|
||||
state.value = next
|
||||
dragOffset.value = 0
|
||||
emit('state-change', next)
|
||||
}
|
||||
|
||||
// Touch handlers
|
||||
function onTouchStart(e: TouchEvent) {
|
||||
touchStartY = e.touches[0].clientY
|
||||
touchCurrentY = touchStartY
|
||||
isDragging = true
|
||||
dragOffset.value = 0
|
||||
}
|
||||
|
||||
function onTouchMove(e: TouchEvent) {
|
||||
if (!isDragging) return
|
||||
touchCurrentY = e.touches[0].clientY
|
||||
const delta = touchCurrentY - touchStartY
|
||||
// Limiter le drag visuellement (résistance)
|
||||
dragOffset.value = delta * 0.6
|
||||
}
|
||||
|
||||
function onTouchEnd() {
|
||||
if (!isDragging) return
|
||||
isDragging = false
|
||||
|
||||
const delta = touchCurrentY - touchStartY
|
||||
const threshold = 60 // px minimum pour changer d'état
|
||||
|
||||
if (delta > threshold) {
|
||||
// Swipe vers le bas → état inférieur
|
||||
if (state.value === 'full') setSheetState('half')
|
||||
else if (state.value === 'half') setSheetState('collapsed')
|
||||
else setSheetState('collapsed')
|
||||
} else if (delta < -threshold) {
|
||||
// Swipe vers le haut → état supérieur
|
||||
if (state.value === 'collapsed') setSheetState('half')
|
||||
else if (state.value === 'half') setSheetState('full')
|
||||
else setSheetState('full')
|
||||
} else {
|
||||
// Snap back
|
||||
dragOffset.value = 0
|
||||
}
|
||||
|
||||
// Transition smooth au relâchement
|
||||
dragOffset.value = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-sheet {
|
||||
position: fixed;
|
||||
inset-x: 0;
|
||||
bottom: 0;
|
||||
z-index: 500;
|
||||
background: var(--nav-surface);
|
||||
border-radius: 16px 16px 0 0;
|
||||
box-shadow: 0 -4px 24px rgba(26, 34, 56, 0.14);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: height 0.3s cubic-bezier(0.32, 0.72, 0, 1), transform 0.05s linear;
|
||||
will-change: height, transform;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── États hauteur ───────────────────────────────────────────────────── */
|
||||
.mobile-sheet--collapsed {
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.mobile-sheet--half {
|
||||
height: 50dvh;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.mobile-sheet--full {
|
||||
height: 92dvh;
|
||||
}
|
||||
|
||||
/* ── Poignée ─────────────────────────────────────────────────────────── */
|
||||
.sheet-handle-area {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px 0 4px;
|
||||
cursor: grab;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sheet-handle-area:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.sheet-handle-bar {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--nav-bg-alt);
|
||||
}
|
||||
|
||||
/* ── Header compteur ─────────────────────────────────────────────────── */
|
||||
.sheet-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px 8px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sheet-counter {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--nav-text-muted);
|
||||
}
|
||||
|
||||
.sheet-chevron {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.sheet-chevron--up {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* ── Contenu scrollable ──────────────────────────────────────────────── */
|
||||
.sheet-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user