navigator/src/lib/components/LocationDialog.svelte
2026-08-10 21:46:07 +00:00

164 lines
6.2 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import type { SavedPoint } from '$lib/savedPoints';
interface Props {
/** Click location in map-container pixel coordinates (anchoring the dialog). */
x: number;
y: number;
/** The clicked coordinates. */
lat: number;
lon: number;
/** If the click landed on an existing feature (POI/zone/path). */
feature?: { name: string; rows: [string, string][] } | null;
/** If the click landed on a saved point. */
savedPoint?: SavedPoint | null;
/** Whether a route origin has been set (controls whether "to here" is shown). */
hasOrigin: boolean;
/** Whether this point is currently the route origin. */
isOrigin: boolean;
onDirectionsFrom: (lat: number, lon: number, label?: string) => void;
onDirectionsTo: (lat: number, lon: number, label?: string) => void;
onSavePoint: (label?: string) => void;
onDeleteSavedPoint: (id: string) => void;
onClose: () => void;
}
let {
x,
y,
lat,
lon,
feature,
savedPoint,
hasOrigin,
isOrigin,
onDirectionsFrom,
onDirectionsTo,
onSavePoint,
onDeleteSavedPoint,
onClose
}: Props = $props();
let saving = $state(false);
let label = $state('');
const isPoint = $derived(!!feature || !!savedPoint);
const title = $derived(feature?.name ?? savedPoint?.label ?? 'New location');
const rows = $derived(feature?.rows ?? []);
// Position the dialog so its 16x16 arrow points at the click.
// Shift 8px left (arrow center lands on the click X via the 50% translate)
// and 16px down (bubble body sits under the arrow tip at the click Y).
const ARROW_X = 8;
const ARROW_Y = 16;
const LEFT = $derived(Math.min(Math.max(x - ARROW_X, 8), window.innerWidth - 320));
const TOP = $derived(Math.min(Math.max(y + ARROW_Y, 8), window.innerHeight - 260));
function save() {
onSavePoint(label.trim() ? label.trim() : undefined);
saving = false;
label = '';
onClose();
}
</script>
<button class="ctx-backdrop" aria-label="Close" onclick={() => onClose()}></button>
<div class="ctx" role="dialog" aria-label="Location" tabindex="-1" style="left:{LEFT}px; top:{TOP}px" onclick={(e) => e.stopPropagation()} onkeydown={(e) => { if (e.key === 'Escape') onClose(); }}>
<div class="ctx-arrow" style="left:{Math.min(Math.max(x - LEFT, 10), 268)}px"></div>
<button class="ctx-x" aria-label="Close" onclick={() => onClose()}>×</button>
{#if isPoint}
<div class="ctx-title">{title}</div>
<div class="ctx-coords">{lat.toFixed(5)}, {lon.toFixed(5)}</div>
{#if rows.length}
<table class="ctx-rows">
<tbody>
{#each rows as [k, v] (k)}
<tr><th>{k}</th><td>{v}</td></tr>
{/each}
</tbody>
</table>
{/if}
{#if savedPoint}
<div class="ctx-id">Saved point</div>
{/if}
<div class="ctx-actions">
<button type="button" class="ctx-btn primary" onclick={() => onDirectionsFrom(lat, lon, title)}>
Directions from here
</button>
{#if hasOrigin && !isOrigin}
<button type="button" class="ctx-btn" onclick={() => onDirectionsTo(lat, lon, title)}>
Directions to here
</button>
{/if}
{#if savedPoint}
<button type="button" class="ctx-btn danger" onclick={() => onDeleteSavedPoint(savedPoint.id)}>
Delete
</button>
{/if}
</div>
{:else}
<div class="ctx-title">New location</div>
<div class="ctx-coords">{lat.toFixed(5)}, {lon.toFixed(5)}</div>
{#if saving}
<input
type="text"
class="ctx-input"
placeholder="Label (optional)"
bind:value={label}
onkeydown={(e) => { if (e.key === 'Enter') save(); if (e.key === 'Escape') { saving = false; onClose(); } }}
/>
<div class="ctx-actions">
<button type="button" class="ctx-btn primary" onclick={() => save()}>Save point</button>
<button type="button" class="ctx-btn" onclick={() => { saving = false; onClose(); }}>Cancel</button>
</div>
{:else}
<div class="ctx-note">This isn't a known point. Save it to use later.</div>
<div class="ctx-actions">
<button type="button" class="ctx-btn primary" onclick={() => (saving = true)}>Save point…</button>
<button type="button" class="ctx-btn" onclick={() => onDirectionsFrom(lat, lon)}>Directions from here</button>
</div>
{/if}
{/if}
</div>
<style>
.ctx-backdrop {
position: fixed; inset: 0; z-index: 2500; background: transparent; border: none; padding: 0; margin: 0; width: 100vw; height: 100vh; cursor: default;
}
.ctx {
position: fixed; z-index: 2600; width: 300px; background: #fff; color: #0f172a;
border-radius: 10px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); padding: 12px 14px;
font: 0.8rem system-ui, sans-serif;
}
.ctx-arrow {
position: absolute; top: -8px; width: 16px; height: 16px; background: #fff;
transform: translateX(-50%) rotate(45deg); border-radius: 3px; box-shadow: -2px -2px 6px rgba(0,0,0,.08);
}
.ctx-x {
position: absolute; top: 6px; right: 8px; width: 24px; height: 24px;
border: none; background: none; cursor: pointer; font-size: 18px; line-height: 1;
color: #64748b; border-radius: 6px; text-align: center; padding: 0;
}
.ctx-x:hover { background: #f1f5f9; color: #0f172a; }
.ctx-title { font-weight: 700; font-size: 0.9rem; margin-bottom: 2px; padding-right: 20px; }
.ctx-coords { color: #94a3b8; font-size: 0.72rem; margin-bottom: 8px; }
.ctx-id { color: #8b5cf6; font-size: 0.7rem; font-weight: 600; margin-bottom: 6px; }
.ctx-note { color: #64748b; font-size: 0.75rem; margin-bottom: 8px; }
.ctx-input { width: 100%; box-sizing: border-box; padding: 7px 9px; border-radius: 6px; border: 1px solid #cbd5e1; font-size: 0.82rem; margin-bottom: 8px; }
.ctx-rows { border-collapse: collapse; margin: 6px 0; width: 100%; font-size: 0.76rem; }
.ctx-rows th { text-align: left; padding: 2px 8px 2px 0; color: #64748b; font-weight: 600; vertical-align: top; }
.ctx-rows td { padding: 2px 0; color: #0f172a; }
.ctx-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.ctx-btn {
padding: 6px 10px; border-radius: 6px; border: 1px solid #cbd5e1; background: #fff;
cursor: pointer; font-size: 0.76rem; color: #0f172a;
}
.ctx-btn:hover { background: #f1f5f9; }
.ctx-btn.primary { background: #0f172a; color: #fff; border-color: #0f172a; font-weight: 600; }
.ctx-btn.primary:hover { background: #1e293b; }
.ctx-btn.danger { color: #b91c1c; border-color: #fecaca; }
.ctx-btn.danger:hover { background: #fef2f2; }
</style>