✨ feat(mobile): add BottomBar navigation and CheminLibre drawer (Story 3.7)
- Add ZoneCard component for zone display with status indicators - Add CheminLibre drawer with vertical zone cards and path decoration - Add BottomBar with Map, Progress, and Settings buttons - Add ProgressDetail modal showing visited sections - Add SettingsDrawer with language, consent, and reset options - Add i18n translations for zone, cheminLibre, bottomBar, settings - Add --bottom-bar-height CSS variable for spacing - Modify layouts to include BottomBar on mobile (< 768px) - Support safe-area-inset for iOS devices - Touch targets minimum 48x48px for WCAG compliance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
103
frontend/app/components/feature/ZoneCard.vue
Normal file
103
frontend/app/components/feature/ZoneCard.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import type { MapZone } from '~/data/mapZones'
|
||||
|
||||
const props = defineProps<{
|
||||
zone: MapZone
|
||||
isVisited: boolean
|
||||
isLocked: boolean
|
||||
isCurrent: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: []
|
||||
}>()
|
||||
|
||||
const { locale, t } = useI18n()
|
||||
|
||||
const zoneName = computed(() => {
|
||||
return locale.value === 'fr' ? props.zone.label.fr : props.zone.label.en
|
||||
})
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (props.isLocked) return t('zone.locked')
|
||||
if (props.isVisited) return t('zone.visited')
|
||||
return t('zone.new')
|
||||
})
|
||||
|
||||
const statusClass = computed(() => {
|
||||
if (props.isLocked) return 'text-gray-500'
|
||||
if (props.isVisited) return 'text-green-400'
|
||||
return 'text-sky-accent'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="zone-card relative w-full flex items-center gap-4 p-4 bg-sky-dark-50 rounded-xl border border-sky-dark-100 transition-all active:scale-98"
|
||||
:class="[
|
||||
isCurrent && 'ring-2 ring-sky-accent',
|
||||
isLocked && 'opacity-60',
|
||||
]"
|
||||
:disabled="isLocked"
|
||||
@click="emit('select')"
|
||||
>
|
||||
<!-- Illustration / Emoji -->
|
||||
<div
|
||||
class="shrink-0 w-16 h-16 rounded-lg flex items-center justify-center text-3xl"
|
||||
:style="{ backgroundColor: `${zone.color}20` }"
|
||||
>
|
||||
<template v-if="isLocked">
|
||||
🔒
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ zone.emoji }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Contenu -->
|
||||
<div class="flex-1 text-left">
|
||||
<h3 class="font-ui font-semibold text-sky-text text-lg">
|
||||
{{ zoneName }}
|
||||
</h3>
|
||||
<p class="text-sm" :class="statusClass">
|
||||
{{ statusText }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Indicateurs -->
|
||||
<div class="shrink-0">
|
||||
<!-- Checkmark si visité -->
|
||||
<span
|
||||
v-if="isVisited && !isLocked"
|
||||
class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-green-500/20 text-green-400"
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
<!-- Badge "Nouveau" si non visité et non verrouillé -->
|
||||
<span
|
||||
v-else-if="!isVisited && !isLocked"
|
||||
class="inline-flex items-center justify-center px-2 py-1 rounded-full bg-sky-accent/20 text-sky-accent text-xs font-ui font-medium"
|
||||
>
|
||||
{{ t('zone.newBadge') }}
|
||||
</span>
|
||||
<!-- Cadenas si verrouillé -->
|
||||
<span
|
||||
v-else-if="isLocked"
|
||||
class="inline-flex items-center justify-center w-8 h-8 text-gray-500"
|
||||
>
|
||||
🔒
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.zone-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.zone-card:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user