All checks were successful
ci/woodpecker/push/push Pipeline was successful
Co-authored-by: Madison Grubb <madison@elastiflow.com> Reviewed-on: #4
191 lines
6.9 KiB
Vue
191 lines
6.9 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<Transition name="modal">
|
|
<div
|
|
v-if="show"
|
|
class="fixed inset-0 z-[2000] flex items-center justify-center p-4"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
:aria-labelledby="mode === 'delete' ? 'delete-poi-title' : 'poi-modal-title'"
|
|
@keydown.escape="$emit('close')"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="absolute inset-0 bg-black/60 transition-opacity"
|
|
aria-label="Close"
|
|
@click="$emit('close')"
|
|
/>
|
|
<div
|
|
v-if="mode === 'add' || mode === 'edit'"
|
|
ref="modalRef"
|
|
class="kestrel-card-modal relative w-full max-w-md p-6"
|
|
@click.stop
|
|
>
|
|
<h2
|
|
id="poi-modal-title"
|
|
class="kestrel-section-heading mb-4"
|
|
>
|
|
{{ mode === 'edit' ? 'Edit POI' : 'Add POI' }}
|
|
</h2>
|
|
<form
|
|
class="space-y-4"
|
|
@submit.prevent="$emit('submit', { label: localForm.label, iconType: localForm.iconType })"
|
|
>
|
|
<div>
|
|
<label
|
|
for="add-poi-label"
|
|
class="kestrel-label"
|
|
>Label (optional)</label>
|
|
<input
|
|
id="add-poi-label"
|
|
v-model="localForm.label"
|
|
type="text"
|
|
placeholder="e.g. Rally point"
|
|
class="kestrel-input"
|
|
autocomplete="off"
|
|
>
|
|
</div>
|
|
<div
|
|
ref="iconRef"
|
|
class="relative inline-block w-full"
|
|
>
|
|
<label class="kestrel-label">Icon type</label>
|
|
<button
|
|
type="button"
|
|
class="flex w-full min-w-0 items-center justify-between gap-2 rounded border border-kestrel-border bg-kestrel-bg px-3 py-2 text-left text-sm text-kestrel-text transition-colors hover:border-kestrel-accent/50"
|
|
:aria-expanded="iconOpen"
|
|
aria-haspopup="listbox"
|
|
:aria-label="`Icon type: ${localForm.iconType}`"
|
|
@click="iconOpen = !iconOpen"
|
|
>
|
|
<span class="flex items-center gap-2 capitalize">
|
|
<Icon
|
|
:name="POI_ICONIFY_IDS[localForm.iconType]"
|
|
class="size-4 shrink-0"
|
|
/>
|
|
{{ localForm.iconType }}
|
|
</span>
|
|
<span
|
|
class="text-kestrel-muted transition-transform"
|
|
:class="iconOpen && 'rotate-180'"
|
|
>▾</span>
|
|
</button>
|
|
<Transition
|
|
enter-active-class="transition duration-100 ease-out"
|
|
enter-from-class="opacity-0 scale-95"
|
|
enter-to-class="opacity-100 scale-100"
|
|
leave-active-class="transition duration-75 ease-in"
|
|
leave-from-class="opacity-100 scale-100"
|
|
leave-to-class="opacity-0 scale-95"
|
|
>
|
|
<div
|
|
v-show="iconOpen"
|
|
class="absolute left-0 right-0 top-full z-10 mt-1 rounded border border-kestrel-border bg-kestrel-surface py-1 shadow-glow shadow-glow-dropdown"
|
|
role="listbox"
|
|
>
|
|
<button
|
|
v-for="opt in POI_ICON_TYPES"
|
|
:key="opt"
|
|
type="button"
|
|
role="option"
|
|
:aria-selected="localForm.iconType === opt"
|
|
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm capitalize transition-colors"
|
|
:class="localForm.iconType === opt ? 'bg-kestrel-accent-dim text-kestrel-accent' : 'text-kestrel-text hover:bg-kestrel-border'"
|
|
@click="localForm.iconType = opt; iconOpen = false"
|
|
>
|
|
<Icon
|
|
:name="POI_ICONIFY_IDS[opt]"
|
|
class="size-4 shrink-0"
|
|
/>
|
|
{{ opt }}
|
|
</button>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
<div class="flex justify-end gap-2 pt-2">
|
|
<button
|
|
type="button"
|
|
class="kestrel-btn-secondary"
|
|
@click="$emit('close')"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="rounded bg-kestrel-accent px-4 py-2 text-sm font-medium text-kestrel-bg transition-opacity hover:opacity-90"
|
|
>
|
|
{{ mode === 'edit' ? 'Save changes' : 'Add POI' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div
|
|
v-if="mode === 'delete'"
|
|
ref="modalRef"
|
|
class="kestrel-card-modal relative w-full max-w-sm p-6"
|
|
@click.stop
|
|
>
|
|
<h2
|
|
id="delete-poi-title"
|
|
class="kestrel-section-heading mb-2"
|
|
>
|
|
Delete POI?
|
|
</h2>
|
|
<p class="mb-4 text-sm text-kestrel-muted">
|
|
{{ deletePoi?.label ? `"${deletePoi.label}" will be removed.` : 'This POI will be removed.' }}
|
|
</p>
|
|
<div class="flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
class="kestrel-btn-secondary"
|
|
@click="$emit('close')"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="rounded bg-red-600 px-4 py-2 text-sm font-medium text-white transition-opacity hover:opacity-90"
|
|
@click="$emit('confirmDelete')"
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup>
|
|
const POI_ICONIFY_IDS = { pin: 'tabler:map-pin', flag: 'tabler:flag', waypoint: 'tabler:current-location' }
|
|
const POI_ICON_TYPES = Object.keys(POI_ICONIFY_IDS)
|
|
|
|
const props = defineProps({
|
|
show: Boolean,
|
|
mode: { type: String, default: 'add' },
|
|
form: { type: Object, default: () => ({ label: '', iconType: 'pin' }) },
|
|
editPoi: { type: Object, default: null },
|
|
deletePoi: { type: Object, default: null },
|
|
})
|
|
defineEmits(['close', 'submit', 'confirmDelete'])
|
|
|
|
const modalRef = ref(null)
|
|
const iconRef = ref(null)
|
|
const iconOpen = ref(false)
|
|
const localForm = ref({ label: '', iconType: 'pin' })
|
|
|
|
watch(() => props.show, (show) => {
|
|
if (!show) return
|
|
iconOpen.value = false
|
|
localForm.value = props.mode === 'edit' && props.editPoi
|
|
? { label: (props.editPoi.label ?? '').trim(), iconType: props.editPoi.icon_type || 'pin' }
|
|
: { ...props.form }
|
|
})
|
|
|
|
function onDocClick(e) {
|
|
if (iconOpen.value && iconRef.value && !iconRef.value.contains(e.target)) iconOpen.value = false
|
|
}
|
|
onMounted(() => document.addEventListener('click', onDocClick))
|
|
onBeforeUnmount(() => document.removeEventListener('click', onDocClick))
|
|
</script>
|