jedmund-svelte/src/lib/components/edra/extensions/geolocation/geolocation-extended.svelte
Justin Edmund a31291d69f refactor: replace deprecated $grey- variables with $gray-
- Replace 802 instances of $grey- variables with $gray- across 106 files
- Remove legacy color aliases from variables.scss
- Maintain consistent naming convention throughout codebase

This completes the migration to the new color scale system.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-25 21:41:50 -04:00

352 lines
7.4 KiB
Svelte

<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import type { NodeViewProps } from '@tiptap/core'
interface Props extends NodeViewProps {}
let { node, updateAttributes }: Props = $props()
let mapContainer: HTMLDivElement
let map: any
let marker: any
let leaflet: any
let isEditing = $state(false)
// Extract attributes
const latitude = $derived(node.attrs.latitude)
const longitude = $derived(node.attrs.longitude)
const title = $derived(node.attrs.title)
const description = $derived(node.attrs.description)
const markerColor = $derived(node.attrs.markerColor || '#ef4444')
const zoom = $derived(node.attrs.zoom || 15)
// Load Leaflet dynamically
async function loadLeaflet() {
if (typeof window === 'undefined') return
// Check if already loaded
if (window.L) {
leaflet = window.L
return
}
// Load Leaflet CSS
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'
link.integrity = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY='
link.crossOrigin = ''
document.head.appendChild(link)
// Load Leaflet JS
const script = document.createElement('script')
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'
script.integrity = 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo='
script.crossOrigin = ''
await new Promise((resolve, reject) => {
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
leaflet = window.L
}
// Initialize map
async function initMap() {
if (!mapContainer || !leaflet || !latitude || !longitude) return
// Create map
map = leaflet.map(mapContainer, {
center: [latitude, longitude],
zoom: zoom,
scrollWheelZoom: false, // Disabled by default in editor
dragging: !isEditing,
touchZoom: !isEditing,
doubleClickZoom: !isEditing,
boxZoom: false,
keyboard: false,
zoomControl: true
})
// Add tile layer (using OpenStreetMap)
leaflet
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution:
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19
})
.addTo(map)
// Create custom marker icon if color is specified
let markerOptions = {}
if (markerColor) {
const markerIcon = leaflet.divIcon({
className: 'custom-marker',
html: `<div style="background-color: ${markerColor}; width: 24px; height: 24px; border-radius: 50%; border: 3px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
iconSize: [30, 30],
iconAnchor: [15, 15],
popupAnchor: [0, -15]
})
markerOptions = { icon: markerIcon }
}
// Add marker
marker = leaflet.marker([latitude, longitude], markerOptions).addTo(map)
// Add popup if title or description exists
if (title || description) {
const popupContent = `
<div class="location-popup">
${title ? `<h3>${title}</h3>` : ''}
${description ? `<p>${description}</p>` : ''}
</div>
`
marker.bindPopup(popupContent, {
autoPan: true,
keepInView: true
})
}
}
// Cleanup
function cleanup() {
if (map) {
map.remove()
map = null
}
marker = null
}
// Handle edit mode
function toggleEdit() {
isEditing = !isEditing
if (map) {
map.dragging[isEditing ? 'disable' : 'enable']()
map.touchZoom[isEditing ? 'disable' : 'enable']()
map.doubleClickZoom[isEditing ? 'disable' : 'enable']()
map.scrollWheelZoom[isEditing ? 'disable' : 'enable']()
}
}
onMount(async () => {
try {
await loadLeaflet()
await initMap()
} catch (error) {
console.error('Failed to load map:', error)
}
})
onDestroy(() => {
cleanup()
})
// Reinitialize if attributes change
$effect(() => {
if (map && (latitude || longitude)) {
cleanup()
initMap()
}
})
</script>
<div class="geolocation-node" data-drag-handle>
<div class="geolocation-header">
<div class="header-info">
<span class="icon">📍</span>
{#if title}
<span class="title">{title}</span>
{:else}
<span class="coordinates">{latitude?.toFixed(4)}, {longitude?.toFixed(4)}</span>
{/if}
</div>
<div class="header-actions">
<button
class="action-button"
onclick={toggleEdit}
title={isEditing ? 'Enable map interaction' : 'Disable map interaction'}
>
{isEditing ? '🔒' : '🔓'}
</button>
</div>
</div>
<div
bind:this={mapContainer}
class="map-container"
class:editing={isEditing}
role="img"
aria-label="Map showing {title || 'location'} at coordinates {latitude}, {longitude}"
>
<noscript>
<div class="map-fallback">
<div class="fallback-content">
{#if title}
<h3>{title}</h3>
{/if}
{#if description}
<p>{description}</p>
{/if}
<p class="coordinates">
{latitude?.toFixed(6)}, {longitude?.toFixed(6)}
</p>
<a
href="https://www.openstreetmap.org/?mlat={latitude}&mlon={longitude}#map={zoom}/{latitude}/{longitude}"
target="_blank"
rel="noopener noreferrer"
>
View on OpenStreetMap
</a>
</div>
</div>
</noscript>
</div>
</div>
<style lang="scss">
@import '$styles/variables.scss';
.geolocation-node {
margin: $unit-2x 0;
border: 1px solid $gray-90;
border-radius: $corner-radius-md;
overflow: hidden;
background: white;
}
.geolocation-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: $unit $unit-2x;
background: $gray-95;
border-bottom: 1px solid $gray-90;
font-size: 0.875rem;
}
.header-info {
display: flex;
align-items: center;
gap: $unit;
.icon {
font-size: 1rem;
}
.title {
font-weight: 500;
color: $gray-10;
}
.coordinates {
font-family: 'SF Mono', Monaco, monospace;
color: $gray-40;
font-size: 0.75rem;
}
}
.action-button {
padding: 4px 8px;
background: transparent;
border: 1px solid $gray-80;
border-radius: $corner-radius-sm;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s ease;
&:hover {
background: white;
border-color: $gray-60;
}
}
.map-container {
height: 400px;
width: 100%;
position: relative;
background: $gray-95;
&.editing {
opacity: 0.8;
pointer-events: none;
}
:global(.leaflet-container) {
font-family: inherit;
}
:global(.location-popup) {
h3 {
margin: 0 0 $unit-half;
font-size: 1rem;
font-weight: 600;
color: $gray-10;
}
p {
margin: 0;
font-size: 0.875rem;
color: $gray-30;
line-height: 1.4;
}
}
:global(.leaflet-popup-content-wrapper) {
border-radius: $corner-radius-md;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
:global(.leaflet-popup-content) {
margin: $unit-2x;
}
}
.map-fallback {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: $gray-95;
padding: $unit-3x;
text-align: center;
}
.fallback-content {
h3 {
margin: 0 0 $unit;
font-size: 1.25rem;
color: $gray-10;
}
p {
margin: 0 0 $unit;
color: $gray-40;
line-height: 1.5;
}
.coordinates {
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.875rem;
color: $gray-60;
margin-bottom: $unit-2x;
}
a {
color: $red-60;
text-decoration: none;
font-weight: 500;
&:hover {
text-decoration: underline;
}
}
}
/* Ensure Leaflet attribution is styled properly */
:global(.leaflet-control-attribution) {
font-size: 0.75rem;
background: rgba(255, 255, 255, 0.9) !important;
}
</style>