- Modified docker-compose.dev.yml to conditionally run npm install based on the presence of package-lock.json. - Upgraded Nuxt version in package-lock.json from 3.21.1 to 4.3.1 for enhanced features. - Enhanced ConfirmModal component with aria-modal attribute for better accessibility. - Updated MapErrorBoundary component's error message for clarity. - Added role and aria-label attributes to MapView and MapSearch components for improved screen reader support. - Refactored various components to manage focus behavior on modal close, enhancing user experience. - Improved ToastContainer styling for better responsiveness and visibility. - Updated layout components to include skip navigation links for improved accessibility.
278 lines
9.0 KiB
Vue
278 lines
9.0 KiB
Vue
<template>
|
|
<!-- Desktop: left panel (md and up) -->
|
|
<div
|
|
class="absolute left-3 top-[10%] z-[502] hidden md:flex min-w-[3rem] min-h-[3rem] transition-all duration-300 ease-out"
|
|
:class="panelCollapsed ? 'w-12' : 'w-64'"
|
|
>
|
|
<div
|
|
class="rounded-xl bg-base-100/80 backdrop-blur-xl border border-base-300/50 shadow-xl overflow-hidden flex flex-col transition-all duration-300 ease-out"
|
|
:class="panelCollapsed ? 'w-12 items-center py-2 gap-1' : 'w-56'"
|
|
>
|
|
<template v-if="panelCollapsed">
|
|
<div class="flex flex-col gap-1">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-sm btn-square transition-all duration-200 hover:scale-105"
|
|
title="Zoom in"
|
|
aria-label="Zoom in"
|
|
@click="$emit('zoomIn')"
|
|
>
|
|
<icons-icon-zoom-in class="size-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-sm btn-square transition-all duration-200 hover:scale-105"
|
|
title="Zoom out"
|
|
aria-label="Zoom out"
|
|
@click="$emit('zoomOut')"
|
|
>
|
|
<icons-icon-zoom-out class="size-4" />
|
|
</button>
|
|
</div>
|
|
</template>
|
|
<Transition name="panel-slide" mode="out-in">
|
|
<div
|
|
v-if="!panelCollapsed"
|
|
key="expanded"
|
|
class="flex flex-col p-4 gap-4 flex-1 min-w-0 overflow-hidden"
|
|
>
|
|
<MapControlsContent
|
|
v-model:hide-markers="hideMarkers"
|
|
:selected-map-id-select="selectedMapIdSelect"
|
|
@update:selected-map-id-select="(v) => (selectedMapIdSelect = v)"
|
|
:overlay-map-id="overlayMapId"
|
|
@update:overlay-map-id="(v) => (overlayMapId = v)"
|
|
:selected-marker-id-select="selectedMarkerIdSelect"
|
|
@update:selected-marker-id-select="(v) => (selectedMarkerIdSelect = v)"
|
|
:selected-player-id-select="selectedPlayerIdSelect"
|
|
@update:selected-player-id-select="(v) => (selectedPlayerIdSelect = v)"
|
|
:maps="maps"
|
|
:quest-givers="questGivers"
|
|
:players="players"
|
|
:current-map-id="currentMapId ?? undefined"
|
|
:current-coords="currentCoords"
|
|
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
|
@zoom-in="$emit('zoomIn')"
|
|
@zoom-out="$emit('zoomOut')"
|
|
@reset-view="$emit('resetView')"
|
|
@jump-to-marker="$emit('jumpToMarker', $event)"
|
|
/>
|
|
</div>
|
|
</Transition>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-sm btn-square shrink-0 m-1 transition-all duration-200 hover:scale-105 mt-auto"
|
|
:title="panelCollapsed ? 'Expand panel' : 'Collapse panel'"
|
|
:aria-label="panelCollapsed ? 'Expand panel' : 'Collapse panel'"
|
|
@click.stop="panelCollapsed = !panelCollapsed"
|
|
>
|
|
<icons-icon-chevron-right v-if="panelCollapsed" class="rotate-0" />
|
|
<icons-icon-panel-left v-else />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile: FAB + bottom sheet -->
|
|
<div class="fixed left-3 bottom-6 z-[502] flex flex-col gap-2 md:hidden">
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary btn-circle shadow-lg min-h-12 min-w-12 touch-manipulation"
|
|
aria-label="Open map controls"
|
|
@click="sheetOpen = true"
|
|
>
|
|
<icons-icon-panel-left class="size-6" />
|
|
</button>
|
|
<div class="flex flex-col gap-1">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-sm btn-circle min-h-10 min-w-10 touch-manipulation"
|
|
title="Zoom in"
|
|
aria-label="Zoom in"
|
|
@click="$emit('zoomIn')"
|
|
>
|
|
<icons-icon-zoom-in class="size-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-sm btn-circle min-h-10 min-w-10 touch-manipulation"
|
|
title="Zoom out"
|
|
aria-label="Zoom out"
|
|
@click="$emit('zoomOut')"
|
|
>
|
|
<icons-icon-zoom-out class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile bottom sheet overlay + panel -->
|
|
<Teleport to="body">
|
|
<Transition name="sheet">
|
|
<div
|
|
v-if="sheetOpen"
|
|
class="fixed inset-0 z-[1100] md:hidden"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Map controls"
|
|
>
|
|
<div
|
|
class="absolute inset-0 bg-black/50"
|
|
aria-hidden="true"
|
|
@click="sheetOpen = false"
|
|
/>
|
|
<div
|
|
class="sheet-panel absolute left-0 right-0 bottom-0 max-h-[85vh] rounded-t-2xl bg-base-100 shadow-2xl flex flex-col overflow-hidden"
|
|
@click.stop
|
|
>
|
|
<div class="flex justify-center py-2 shrink-0">
|
|
<span class="w-12 h-1 rounded-full bg-base-300" aria-hidden="true" />
|
|
</div>
|
|
<div class="overflow-y-auto overscroll-contain flex-1 p-4 pb-8">
|
|
<MapControlsContent
|
|
v-model:hide-markers="hideMarkers"
|
|
:selected-map-id-select="selectedMapIdSelect"
|
|
@update:selected-map-id-select="(v) => (selectedMapIdSelect = v)"
|
|
:overlay-map-id="overlayMapId"
|
|
@update:overlay-map-id="(v) => (overlayMapId = v)"
|
|
:selected-marker-id-select="selectedMarkerIdSelect"
|
|
@update:selected-marker-id-select="(v) => (selectedMarkerIdSelect = v)"
|
|
:selected-player-id-select="selectedPlayerIdSelect"
|
|
@update:selected-player-id-select="(v) => (selectedPlayerIdSelect = v)"
|
|
:maps="maps"
|
|
:quest-givers="questGivers"
|
|
:players="players"
|
|
:current-map-id="currentMapId ?? undefined"
|
|
:current-coords="currentCoords"
|
|
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
|
:touch-friendly="true"
|
|
@zoom-in="$emit('zoomIn')"
|
|
@zoom-out="$emit('zoomOut')"
|
|
@reset-view="$emit('resetView')"
|
|
@jump-to-marker="$emit('jumpToMarker', $event)"
|
|
/>
|
|
</div>
|
|
<div class="p-3 border-t border-base-300 shrink-0 safe-area-pb flex flex-col gap-2">
|
|
<p class="text-xs text-base-content/50 text-center">
|
|
<kbd class="kbd kbd-xs">?</kbd> for shortcuts
|
|
</p>
|
|
<button
|
|
type="button"
|
|
class="btn btn-ghost btn-block touch-manipulation min-h-12"
|
|
@click="sheetOpen = false"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { MapInfo } from '~/types/api'
|
|
import type { SelectedMarkerForBookmark } from '~/components/map/MapBookmarks.vue'
|
|
import MapControlsContent from '~/components/map/MapControlsContent.vue'
|
|
|
|
interface QuestGiver {
|
|
id: number
|
|
name: string
|
|
}
|
|
|
|
interface Player {
|
|
id: number
|
|
name: string
|
|
}
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
maps: MapInfo[]
|
|
questGivers: QuestGiver[]
|
|
players: Player[]
|
|
currentMapId?: number | null
|
|
currentCoords?: { x: number; y: number; z: number } | null
|
|
selectedMarkerForBookmark?: SelectedMarkerForBookmark
|
|
}>(),
|
|
{
|
|
maps: () => [],
|
|
questGivers: () => [],
|
|
players: () => [],
|
|
currentMapId: null,
|
|
currentCoords: null,
|
|
selectedMarkerForBookmark: null,
|
|
}
|
|
)
|
|
|
|
defineEmits<{
|
|
zoomIn: []
|
|
zoomOut: []
|
|
resetView: []
|
|
jumpToMarker: [id: number]
|
|
}>()
|
|
|
|
const hideMarkers = defineModel<boolean>('hideMarkers', { default: false })
|
|
const panelCollapsed = ref(false)
|
|
const sheetOpen = ref(false)
|
|
const selectedMapId = defineModel<number | null>('selectedMapId', { default: null })
|
|
const overlayMapId = defineModel<number>('overlayMapId', { default: -1 })
|
|
const selectedMarkerId = defineModel<number | null>('selectedMarkerId', { default: null })
|
|
const selectedPlayerId = defineModel<number | null>('selectedPlayerId', { default: null })
|
|
|
|
const selectedMapIdSelect = computed({
|
|
get() {
|
|
const v = selectedMapId.value
|
|
return v == null ? '' : String(v)
|
|
},
|
|
set(v: string) {
|
|
selectedMapId.value = v === '' ? null : Number(v)
|
|
},
|
|
})
|
|
const selectedMarkerIdSelect = computed({
|
|
get() {
|
|
const v = selectedMarkerId.value
|
|
return v == null ? '' : String(v)
|
|
},
|
|
set(v: string) {
|
|
selectedMarkerId.value = v === '' ? null : Number(v)
|
|
},
|
|
})
|
|
const selectedPlayerIdSelect = computed({
|
|
get() {
|
|
const v = selectedPlayerId.value
|
|
return v == null ? '' : String(v)
|
|
},
|
|
set(v: string) {
|
|
selectedPlayerId.value = v === '' ? null : Number(v)
|
|
},
|
|
})
|
|
|
|
const selectedMarkerForBookmark = toRef(props, 'selectedMarkerForBookmark')
|
|
</script>
|
|
|
|
<style scoped>
|
|
.panel-slide-enter-active,
|
|
.panel-slide-leave-active {
|
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
}
|
|
.panel-slide-enter-from,
|
|
.panel-slide-leave-to {
|
|
opacity: 0;
|
|
transform: translateX(-0.5rem);
|
|
}
|
|
.sheet-enter-active,
|
|
.sheet-leave-active {
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
.sheet-enter-from,
|
|
.sheet-leave-to {
|
|
opacity: 0;
|
|
}
|
|
.sheet-enter-active .sheet-panel,
|
|
.sheet-leave-active .sheet-panel {
|
|
transition: transform 0.25s ease-out;
|
|
}
|
|
.sheet-enter-from .sheet-panel,
|
|
.sheet-leave-to .sheet-panel {
|
|
transform: translateY(100%);
|
|
}
|
|
</style>
|