- 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>
352 lines
7.4 KiB
Svelte
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>
|