feat: add dark mode toggle
This commit is contained in:
@@ -62,18 +62,91 @@ function htmlPage(): string {
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--radius-xl: 16px;
|
||||
--radius-lg: 12px;
|
||||
--radius-md: 10px;
|
||||
--shadow: 0 16px 42px rgba(2, 6, 23, 0.24);
|
||||
--shadow-soft: 0 10px 24px rgba(2, 6, 23, 0.14);
|
||||
--chart-grid: rgba(255, 255, 255, 0.04);
|
||||
--chart-band: rgba(59, 130, 246, 0.18);
|
||||
}
|
||||
|
||||
body[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bg: #0b1220;
|
||||
--bg-accent-a: rgba(37, 99, 235, 0.16);
|
||||
--bg-accent-b: rgba(14, 116, 144, 0.12);
|
||||
--panel: rgba(15, 23, 42, 0.82);
|
||||
--panel-strong: rgba(15, 23, 42, 0.92);
|
||||
--surface: rgba(15, 23, 42, 0.62);
|
||||
--surface-strong: rgba(30, 41, 59, 0.84);
|
||||
--border: rgba(148, 163, 184, 0.16);
|
||||
--border-strong: rgba(148, 163, 184, 0.24);
|
||||
--text: #e5eefb;
|
||||
--muted: #93a3b8;
|
||||
--accent: #60a5fa;
|
||||
--accent-strong: #3b82f6;
|
||||
--input-bg: rgba(15, 23, 42, 0.88);
|
||||
--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(96, 165, 250, 0.16);
|
||||
--progress-track: rgba(148, 163, 184, 0.16);
|
||||
--photo-bg: rgba(15, 23, 42, 0.55);
|
||||
--photo-active-bg: rgba(37, 99, 235, 0.22);
|
||||
--photo-active-border: rgba(96, 165, 250, 0.44);
|
||||
--empty-border: rgba(148, 163, 184, 0.22);
|
||||
--map-label-bg: rgba(15, 23, 42, 0.92);
|
||||
--map-label-border: rgba(148, 163, 184, 0.16);
|
||||
--overlay-bg: rgba(3, 7, 18, 0.84);
|
||||
--card-border: rgba(148, 163, 184, 0.14);
|
||||
--leaflet-popup-bg: rgba(15, 23, 42, 0.96);
|
||||
--leaflet-popup-text: #e5eefb;
|
||||
--timeline-bg:
|
||||
linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(15, 23, 42, 0.9)),
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.18), transparent 42%);
|
||||
--chart-grid: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
body[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bg: #eef2f7;
|
||||
--bg-accent-a: rgba(37, 99, 235, 0.12);
|
||||
--bg-accent-b: rgba(15, 118, 110, 0.08);
|
||||
--panel: rgba(255, 255, 255, 0.84);
|
||||
--panel-strong: rgba(255, 255, 255, 0.94);
|
||||
--surface: rgba(255, 255, 255, 0.72);
|
||||
--surface-strong: rgba(255, 255, 255, 0.88);
|
||||
--border: rgba(15, 23, 42, 0.11);
|
||||
--border-strong: rgba(15, 23, 42, 0.18);
|
||||
--text: #0f172a;
|
||||
--muted: #5f6b7a;
|
||||
--accent: #2563eb;
|
||||
--accent-strong: #1d4ed8;
|
||||
--shadow: 0 16px 42px rgba(15, 23, 42, 0.12);
|
||||
--radius-xl: 16px;
|
||||
--radius-lg: 12px;
|
||||
--radius-md: 10px;
|
||||
--input-bg: white;
|
||||
--button-secondary-bg: rgba(15, 23, 42, 0.06);
|
||||
--button-secondary-hover: rgba(15, 23, 42, 0.1);
|
||||
--danger-bg: rgba(157, 23, 77, 0.1);
|
||||
--danger-hover: rgba(157, 23, 77, 0.16);
|
||||
--danger-text: #b91c1c;
|
||||
--status-tint: rgba(37, 99, 235, 0.12);
|
||||
--progress-track: rgba(15, 23, 42, 0.08);
|
||||
--photo-bg: rgba(255, 255, 255, 0.72);
|
||||
--photo-active-bg: rgba(45, 108, 223, 0.12);
|
||||
--photo-active-border: rgba(45, 108, 223, 0.42);
|
||||
--empty-border: rgba(15, 23, 42, 0.16);
|
||||
--map-label-bg: rgba(255, 255, 255, 0.92);
|
||||
--map-label-border: rgba(15, 23, 42, 0.08);
|
||||
--overlay-bg: rgba(8, 12, 18, 0.82);
|
||||
--card-border: rgba(15, 23, 42, 0.11);
|
||||
--leaflet-popup-bg: rgba(255, 255, 255, 0.97);
|
||||
--leaflet-popup-text: #0f172a;
|
||||
--timeline-bg:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 250, 252, 0.92)),
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.1), transparent 42%);
|
||||
--chart-grid: rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -92,8 +165,8 @@ function htmlPage(): string {
|
||||
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(37, 99, 235, 0.12), transparent 28%),
|
||||
radial-gradient(circle at bottom right, rgba(15, 118, 110, 0.08), transparent 26%),
|
||||
radial-gradient(circle at top left, var(--bg-accent-a), transparent 28%),
|
||||
radial-gradient(circle at bottom right, var(--bg-accent-b), transparent 26%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
@@ -105,6 +178,17 @@ function htmlPage(): string {
|
||||
padding: 20px 24px 12px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
min-width: 128px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
@@ -149,8 +233,8 @@ function htmlPage(): string {
|
||||
.card {
|
||||
padding: 15px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid var(--card-border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
@@ -172,11 +256,12 @@ function htmlPage(): string {
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(15, 23, 42, 0.18);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 11px 13px;
|
||||
font: inherit;
|
||||
background: white;
|
||||
color: var(--text);
|
||||
background: var(--input-bg);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
@@ -223,17 +308,21 @@ function htmlPage(): string {
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
background: var(--button-secondary-bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.secondary:hover {
|
||||
background: var(--button-secondary-hover);
|
||||
}
|
||||
|
||||
.danger {
|
||||
background: rgba(157, 23, 77, 0.1);
|
||||
color: #b91c1c;
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.danger:hover {
|
||||
background: rgba(157, 23, 77, 0.16);
|
||||
background: var(--danger-hover);
|
||||
}
|
||||
|
||||
.status {
|
||||
@@ -253,7 +342,7 @@ function htmlPage(): string {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(45, 108, 223, 0.22);
|
||||
border: 2px solid rgba(96, 165, 250, 0.22);
|
||||
border-top-color: var(--accent);
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
@@ -282,7 +371,7 @@ function htmlPage(): string {
|
||||
height: 10px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: rgba(15, 23, 42, 0.08);
|
||||
background: var(--progress-track);
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
@@ -316,8 +405,8 @@ function htmlPage(): string {
|
||||
align-items: center;
|
||||
padding: 11px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||
background: var(--photo-bg);
|
||||
border: 1px solid var(--card-border);
|
||||
}
|
||||
|
||||
.photo img {
|
||||
@@ -338,16 +427,16 @@ function htmlPage(): string {
|
||||
}
|
||||
|
||||
.photo.active {
|
||||
border-color: rgba(45, 108, 223, 0.42);
|
||||
background: rgba(45, 108, 223, 0.12);
|
||||
border-color: var(--photo-active-border);
|
||||
background: var(--photo-active-bg);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 14px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px dashed rgba(15, 23, 42, 0.16);
|
||||
border: 1px dashed var(--empty-border);
|
||||
color: var(--muted);
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
background: var(--surface);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -384,9 +473,9 @@ function htmlPage(): string {
|
||||
left: 16px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
||||
background: var(--map-label-bg);
|
||||
border: 1px solid var(--map-label-border);
|
||||
box-shadow: var(--shadow-soft);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
@@ -395,7 +484,7 @@ function htmlPage(): string {
|
||||
inset: 0;
|
||||
display: none;
|
||||
place-items: center;
|
||||
background: rgba(8, 12, 18, 0.82);
|
||||
background: var(--overlay-bg);
|
||||
z-index: 1000;
|
||||
padding: 24px;
|
||||
}
|
||||
@@ -416,6 +505,7 @@ function htmlPage(): string {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--panel-strong);
|
||||
}
|
||||
|
||||
.overlay-card img {
|
||||
@@ -427,8 +517,8 @@ function htmlPage(): string {
|
||||
}
|
||||
|
||||
.close {
|
||||
background: #111827;
|
||||
color: white;
|
||||
background: var(--surface-strong);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.thumb {
|
||||
@@ -454,11 +544,13 @@ function htmlPage(): string {
|
||||
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
background: var(--leaflet-popup-bg);
|
||||
color: var(--leaflet-popup-text);
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 10px 12px;
|
||||
color: var(--leaflet-popup-text);
|
||||
}
|
||||
|
||||
.timeline {
|
||||
@@ -491,9 +583,7 @@ function htmlPage(): string {
|
||||
padding: 14px 12px 10px;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(15, 23, 42, 0.88)),
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.18), transparent 42%);
|
||||
background: var(--timeline-bg);
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
@@ -507,8 +597,8 @@ function htmlPage(): string {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(to top, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
||||
linear-gradient(to top, var(--chart-grid) 1px, transparent 1px),
|
||||
linear-gradient(to right, var(--chart-grid) 1px, transparent 1px);
|
||||
background-size: 100% 33.333%, 10% 100%;
|
||||
opacity: 0.35;
|
||||
pointer-events: none;
|
||||
@@ -637,7 +727,7 @@ function htmlPage(): string {
|
||||
}
|
||||
|
||||
.timeline-footer span:last-child {
|
||||
color: rgba(30, 41, 59, 0.92);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media (max-width: 920px) {
|
||||
@@ -661,13 +751,23 @@ function htmlPage(): string {
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<body data-theme="dark">
|
||||
<header>
|
||||
<div class="brand">
|
||||
<h1>mapy-mg</h1>
|
||||
<p>Load photos, read EXIF locally in the browser, and show them on OpenStreetMap.</p>
|
||||
</div>
|
||||
<div class="meta">Client-side import · no image storage on the server</div>
|
||||
<div class="header-actions">
|
||||
<div class="meta">Client-side import · no image storage on the server</div>
|
||||
<button class="secondary theme-toggle" id="theme-toggle" type="button" aria-label="Switch color theme">
|
||||
<span class="button-icon" id="theme-toggle-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.7 12.2a5.7 5.7 0 1 1 0-8.4 4.9 4.9 0 0 0 0 8.4Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span id="theme-toggle-label">Light mode</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
@@ -828,6 +928,9 @@ function htmlPage(): string {
|
||||
const timelineAxis = mustGet("timeline-axis");
|
||||
const timelineRange = mustGet("timeline-range");
|
||||
const timelineUnit = mustGet("timeline-unit");
|
||||
const themeToggle = mustGet("theme-toggle");
|
||||
const themeToggleIcon = mustGet("theme-toggle-icon");
|
||||
const themeToggleLabel = mustGet("theme-toggle-label");
|
||||
const shareUrl = mustGet("share-url");
|
||||
const shareStatus = mustGet("share-status");
|
||||
const loadShare = mustGet("load-share");
|
||||
@@ -841,6 +944,35 @@ function htmlPage(): string {
|
||||
return element;
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
const resolvedTheme = theme === "light" ? "light" : "dark";
|
||||
document.body.dataset.theme = resolvedTheme;
|
||||
localStorage.setItem("theme", resolvedTheme);
|
||||
|
||||
if (resolvedTheme === "dark") {
|
||||
themeToggleLabel.textContent = "Light mode";
|
||||
themeToggle.setAttribute("aria-label", "Switch to light mode");
|
||||
themeToggleIcon.innerHTML =
|
||||
'<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<path d="M10.7 12.2a5.7 5.7 0 1 1 0-8.4 4.9 4.9 0 0 0 0 8.4Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/>' +
|
||||
"</svg>";
|
||||
} else {
|
||||
themeToggleLabel.textContent = "Dark mode";
|
||||
themeToggle.setAttribute("aria-label", "Switch to dark mode");
|
||||
themeToggleIcon.innerHTML =
|
||||
'<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<path d="M8 1.8V3.1M8 12.9v1.3M3.1 3.1l.9.9M12 12l.9.9M1.8 8H3.1M12.9 8h1.3M3.1 12.9l.9-.9M12 4l.9-.9" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>' +
|
||||
'<circle cx="8" cy="8" r="3.2" stroke="currentColor" stroke-width="1.4"/>' +
|
||||
"</svg>";
|
||||
}
|
||||
}
|
||||
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
setTheme(storedTheme === "light" ? "light" : "dark");
|
||||
themeToggle.addEventListener("click", () => {
|
||||
setTheme(document.body.dataset.theme === "dark" ? "light" : "dark");
|
||||
});
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) {
|
||||
return "no timestamp";
|
||||
|
||||
Reference in New Issue
Block a user