diff --git a/src/server/request-handler.ts b/src/server/request-handler.ts index 3b79635..3fb7af1 100644 --- a/src/server/request-handler.ts +++ b/src/server/request-handler.ts @@ -98,55 +98,62 @@ function htmlPage(): string { body[data-theme="dark"] { color-scheme: dark; - --bg: #0f172a; - --bg-accent-a: rgba(71, 85, 105, 0.1); - --bg-accent-b: rgba(30, 41, 59, 0.08); - --panel: rgba(15, 23, 42, 0.84); - --panel-float: rgba(15, 23, 42, 0.52); - --panel-strong: rgba(15, 23, 42, 0.96); - --surface: rgba(15, 23, 42, 0.6); - --surface-float: rgba(15, 23, 42, 0.42); - --surface-strong: rgba(30, 41, 59, 0.88); - --border: rgba(148, 163, 184, 0.14); - --border-strong: rgba(148, 163, 184, 0.2); + --bg: #0b1120; + --bg-accent-a: rgba(59, 130, 246, 0.12); + --bg-accent-b: rgba(30, 41, 59, 0.14); + --panel: rgba(15, 23, 42, 0.9); + --panel-float: rgba(15, 23, 42, 0.72); + --panel-strong: rgba(30, 41, 59, 0.96); + --surface: rgba(15, 23, 42, 0.72); + --surface-float: rgba(15, 23, 42, 0.54); + --surface-strong: rgba(30, 41, 59, 0.95); + --border: rgba(148, 163, 184, 0.16); + --border-strong: rgba(148, 163, 184, 0.26); --text: #e2e8f0; --muted: #94a3b8; --accent: #94a3b8; --accent-strong: #cbd5e1; - --input-bg: rgba(15, 23, 42, 0.9); - --button-secondary-bg: rgba(148, 163, 184, 0.1); - --button-secondary-hover: rgba(148, 163, 184, 0.16); - --danger-bg: rgba(239, 68, 68, 0.1); - --danger-hover: rgba(239, 68, 68, 0.14); + --input-bg: rgba(15, 23, 42, 0.92); + --button-secondary-bg: rgba(148, 163, 184, 0.12); + --button-secondary-hover: rgba(148, 163, 184, 0.18); + --danger-bg: rgba(239, 68, 68, 0.12); + --danger-hover: rgba(239, 68, 68, 0.18); --danger-text: #fca5a5; - --status-tint: rgba(148, 163, 184, 0.14); - --progress-track: rgba(148, 163, 184, 0.14); - --photo-bg: rgba(15, 23, 42, 0.56); - --photo-active-bg: rgba(71, 85, 105, 0.22); - --photo-active-border: rgba(148, 163, 184, 0.28); - --empty-border: rgba(148, 163, 184, 0.18); - --overlay-bg: rgba(2, 6, 23, 0.84); - --card-border: rgba(148, 163, 184, 0.12); - --timeline-surface: #f0f0f0c0; - --timeline-surface-soft: rgba(255, 255, 255, 0.76); - --timeline-surface-strong: rgba(255, 255, 255, 0.94); - --timeline-border: rgba(75, 85, 99, 0.24); - --timeline-border-strong: rgba(75, 85, 99, 0.36); - --timeline-text: #303030; - --timeline-text-muted: rgba(48, 48, 48, 0.7); - --timeline-bar-border: rgba(64, 64, 64, 0.96); - --timeline-bar-fill: rgba(128, 128, 128, 0.92); - --timeline-bar-fill-hover: rgba(144, 144, 144, 0.96); - --timeline-bar-fill-selected: rgba(160, 160, 160, 0.96); - --timeline-bar-fill-selected-active: rgba(176, 176, 176, 0.98); - --timeline-axis-color: rgba(156, 163, 175, 0.55); - --timeline-axis-label: #303030; - --timeline-meta: var(--timeline-axis-label); - --timeline-selection-border: rgba(107, 114, 128, 0.42); - --timeline-selection-bg: rgba(107, 114, 128, 0.08); - --timeline-brush-border: rgba(107, 114, 128, 0.62); - --timeline-brush-bg: rgba(107, 114, 128, 0.12); - --chart-grid: rgba(255, 255, 255, 0.04); + --status-tint: rgba(148, 163, 184, 0.16); + --progress-track: rgba(148, 163, 184, 0.16); + --photo-bg: rgba(15, 23, 42, 0.7); + --photo-active-bg: rgba(51, 65, 85, 0.76); + --photo-active-border: rgba(148, 163, 184, 0.34); + --empty-border: rgba(148, 163, 184, 0.22); + --overlay-bg: rgba(2, 6, 23, 0.9); + --card-border: rgba(148, 163, 184, 0.16); + --timeline-surface: rgba(15, 23, 42, 0.88); + --timeline-surface-soft: rgba(15, 23, 42, 0.76); + --timeline-surface-strong: rgba(30, 41, 59, 0.96); + --timeline-border: rgba(148, 163, 184, 0.18); + --timeline-border-strong: rgba(191, 219, 254, 0.28); + --timeline-text: #e2e8f0; + --timeline-text-muted: #94a3b8; + --timeline-bar-border: rgba(148, 163, 184, 0.48); + --timeline-bar-fill: rgba(148, 163, 184, 0.84); + --timeline-bar-fill-hover: rgba(125, 211, 252, 0.88); + --timeline-bar-fill-selected: rgba(186, 230, 253, 0.92); + --timeline-bar-fill-selected-active: rgba(226, 232, 240, 0.98); + --timeline-axis-color: rgba(148, 163, 184, 0.5); + --timeline-axis-label: #d1d5db; + --timeline-meta: rgba(209, 213, 219, 0.72); + --timeline-selection-border: rgba(148, 163, 184, 0.34); + --timeline-selection-bg: rgba(148, 163, 184, 0.08); + --timeline-brush-border: rgba(148, 163, 184, 0.56); + --timeline-brush-bg: rgba(148, 163, 184, 0.14); + --chart-grid: rgba(255, 255, 255, 0.05); + --accent-shadow: rgba(148, 163, 184, 0.18); + --spinner-track: rgba(125, 211, 252, 0.26); + --route-color: #7dd3fc; + --marker-border: rgba(15, 23, 42, 0.94); + --marker-border-active: rgba(191, 219, 254, 0.96); + --marker-shadow: 0 8px 18px rgba(2, 6, 23, 0.34); + --marker-active-shadow: 0 0 0 3px rgba(125, 211, 252, 0.18), 0 10px 22px rgba(2, 6, 23, 0.38); } body[data-theme="light"] { @@ -154,16 +161,16 @@ function htmlPage(): string { --bg: #eef2f6; --bg-accent-a: rgba(100, 116, 139, 0.08); --bg-accent-b: rgba(148, 163, 184, 0.06); - --panel: rgba(255, 255, 255, 0.86); - --panel-float: rgba(255, 255, 255, 0.52); - --panel-strong: rgba(255, 255, 255, 0.96); - --surface: rgba(255, 255, 255, 0.8); - --surface-float: rgba(255, 255, 255, 0.46); - --surface-strong: rgba(241, 245, 249, 0.96); + --panel: rgba(255, 255, 255, 0.9); + --panel-float: rgba(255, 255, 255, 0.68); + --panel-strong: rgba(255, 255, 255, 0.98); + --surface: rgba(255, 255, 255, 0.84); + --surface-float: rgba(255, 255, 255, 0.56); + --surface-strong: rgba(241, 245, 249, 0.98); --border: rgba(15, 23, 42, 0.1); --border-strong: rgba(15, 23, 42, 0.16); --text: #111827; - --muted: #667085; + --muted: #5b6472; --accent: #64748b; --accent-strong: #475569; --input-bg: white; @@ -174,32 +181,39 @@ function htmlPage(): string { --danger-text: #b91c1c; --status-tint: rgba(100, 116, 139, 0.12); --progress-track: rgba(15, 23, 42, 0.08); - --photo-bg: rgba(255, 255, 255, 0.76); + --photo-bg: rgba(255, 255, 255, 0.84); --photo-active-bg: rgba(100, 116, 139, 0.12); - --photo-active-border: rgba(100, 116, 139, 0.3); + --photo-active-border: rgba(100, 116, 139, 0.32); --empty-border: rgba(15, 23, 42, 0.16); --overlay-bg: rgba(8, 12, 18, 0.78); --card-border: rgba(15, 23, 42, 0.11); - --timeline-surface: #f0f0f0c0; - --timeline-surface-soft: rgba(255, 255, 255, 0.76); - --timeline-surface-strong: rgba(255, 255, 255, 0.94); - --timeline-border: rgba(75, 85, 99, 0.24); - --timeline-border-strong: rgba(75, 85, 99, 0.36); + --timeline-surface: rgba(255, 255, 255, 0.84); + --timeline-surface-soft: rgba(255, 255, 255, 0.94); + --timeline-surface-strong: rgba(241, 245, 249, 0.98); + --timeline-border: rgba(148, 163, 184, 0.22); + --timeline-border-strong: rgba(100, 116, 139, 0.32); --timeline-text: #303030; - --timeline-text-muted: rgba(48, 48, 48, 0.7); - --timeline-bar-border: rgba(64, 64, 64, 0.96); - --timeline-bar-fill: rgba(128, 128, 128, 0.92); - --timeline-bar-fill-hover: rgba(144, 144, 144, 0.96); - --timeline-bar-fill-selected: rgba(160, 160, 160, 0.96); - --timeline-bar-fill-selected-active: rgba(176, 176, 176, 0.98); + --timeline-text-muted: rgba(48, 48, 48, 0.72); + --timeline-bar-border: rgba(75, 85, 99, 0.82); + --timeline-bar-fill: rgba(100, 116, 139, 0.82); + --timeline-bar-fill-hover: rgba(75, 85, 99, 0.92); + --timeline-bar-fill-selected: rgba(55, 65, 81, 0.92); + --timeline-bar-fill-selected-active: rgba(15, 23, 42, 0.96); --timeline-axis-color: rgba(107, 114, 128, 0.52); --timeline-axis-label: #303030; - --timeline-meta: var(--timeline-axis-label); + --timeline-meta: rgba(48, 48, 48, 0.72); --timeline-selection-border: rgba(107, 114, 128, 0.3); --timeline-selection-bg: rgba(107, 114, 128, 0.08); --timeline-brush-border: rgba(107, 114, 128, 0.52); --timeline-brush-bg: rgba(107, 114, 128, 0.12); --chart-grid: rgba(15, 23, 42, 0.06); + --accent-shadow: rgba(100, 116, 139, 0.16); + --spinner-track: rgba(96, 165, 250, 0.22); + --route-color: #2d6cdf; + --marker-border: rgba(255, 255, 255, 0.92); + --marker-border-active: rgba(100, 116, 139, 0.95); + --marker-shadow: 0 8px 18px rgba(15, 23, 42, 0.22); + --marker-active-shadow: 0 0 0 3px rgba(100, 116, 139, 0.18), 0 10px 22px rgba(15, 23, 42, 0.28); } * { @@ -394,7 +408,7 @@ function htmlPage(): string { .primary { background: var(--accent); color: white; - box-shadow: 0 6px 14px rgba(100, 116, 139, 0.16); + box-shadow: 0 6px 14px var(--accent-shadow); } .primary:hover { @@ -442,7 +456,7 @@ function htmlPage(): string { width: 16px; height: 16px; border-radius: 50%; - border: 2px solid rgba(96, 165, 250, 0.22); + border: 2px solid var(--spinner-track); border-top-color: var(--accent); opacity: 0; transform: scale(0.8); @@ -674,13 +688,9 @@ function htmlPage(): string { height: 44px; border-radius: 50%; overflow: hidden; - border: 2px solid rgba(255, 255, 255, 0.92); - box-shadow: 0 8px 18px rgba(15, 23, 42, 0.22); - background: rgba(15, 23, 42, 0.2); - } - - body[data-theme="dark"] .photo-map-marker { - border-color: rgba(15, 23, 42, 0.96); + border: 2px solid var(--marker-border); + box-shadow: var(--marker-shadow); + background: var(--timeline-surface-strong); } .photo-map-marker img { @@ -691,8 +701,8 @@ function htmlPage(): string { } .photo-map-marker.active { - border-color: rgba(100, 116, 139, 0.95); - box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.18), 0 10px 22px rgba(15, 23, 42, 0.28); + border-color: var(--marker-border-active); + box-shadow: var(--marker-active-shadow); } .timeline { @@ -1037,10 +1047,22 @@ function htmlPage(): string { const SHARE_PARAM_NAME = "share"; const map = L.map("map", { zoomControl: true }).setView([52.5208, 13.4095], 13); - L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { - maxZoom: 19, - attribution: "© OpenStreetMap contributors" - }).addTo(map); + const lightMapLayer = L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", { + maxZoom: 20, + subdomains: "abcd", + attribution: "© OpenStreetMap contributors © CARTO" + }); + const darkMapLayer = L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", { + maxZoom: 20, + subdomains: "abcd", + attribution: "© OpenStreetMap contributors © CARTO" + }); + const mapLayers = { + light: lightMapLayer, + dark: darkMapLayer + }; + let activeMapLayer = lightMapLayer; + activeMapLayer.addTo(map); const markers = L.layerGroup().addTo(map); const route = L.polyline([], { @@ -1048,6 +1070,10 @@ function htmlPage(): string { weight: 4, opacity: 0.85 }).addTo(map); + const routeColors = { + light: "#2d6cdf", + dark: "#7dd3fc" + }; const overlay = mustGet("overlay"); const overlayTitle = mustGet("overlay-title"); @@ -1100,6 +1126,7 @@ function htmlPage(): string { const resolvedTheme = theme === "light" ? "light" : "dark"; document.body.dataset.theme = resolvedTheme; localStorage.setItem("theme", resolvedTheme); + setMapTheme(resolvedTheme); if (resolvedTheme === "dark") { themeToggleLabel.textContent = "Light mode"; @@ -1119,6 +1146,21 @@ function htmlPage(): string { } } + function setMapTheme(theme) { + const resolvedTheme = theme === "dark" ? "dark" : "light"; + const nextMapLayer = mapLayers[resolvedTheme]; + + if (activeMapLayer !== nextMapLayer) { + map.removeLayer(activeMapLayer); + activeMapLayer = nextMapLayer; + activeMapLayer.addTo(map); + } + + route.setStyle({ + color: routeColors[resolvedTheme] + }); + } + function scheduleMapResize() { window.clearTimeout(scheduleMapResize.timeoutId); scheduleMapResize.timeoutId = window.setTimeout(() => {