Add support for dark/light mode.

This commit is contained in:
knadh committed 2026-08-03 12:20:12 +05:30
1 parent 631541a23e
commit dd4d89ef02
3 files changed
+90 -30

No files matched your search

+42 -14
View File
@@ -1,5 +1,22 @@
document.addEventListener('DOMContentLoaded', () => {
const $ = s => document.querySelector(s), $$ = s => document.querySelectorAll(s);
// Dark/light theme.
const darkQuery = matchMedia('(prefers-color-scheme: dark)');
const themeListeners = [];
const isDark = () =>
(document.body.dataset.theme || (darkQuery.matches ? 'dark' : 'light')) === 'dark';
const notifyTheme = () => themeListeners.forEach(fn => fn(isDark()));
$('#theme-toggle')?.addEventListener('click', () => {
document.body.dataset.theme = isDark() ? 'light' : 'dark';
try { localStorage.setItem('ooru-theme', document.body.dataset.theme); } catch {}
notifyTheme();
});
// Fall back to system if there's no saved pref.
darkQuery.addEventListener('change', () => { if (!document.body.dataset.theme) notifyTheme(); });
const items = $$('[data-space]');
if (!items.length) {
return;
@@ -60,25 +77,36 @@ document.addEventListener('DOMContentLoaded', () => {
map = L.map('map', { maxBounds: [[5, 67], [38, 98]], maxBoundsViscosity: 1, minZoom: 4 }).setView([22, 79], 5);
map.on('moveend', onMapMove);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd'
}).addTo(map);
const addLabels = () => L.tileLayer(
'https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png',
{ subdomains: 'abcd', pane: 'shadowPane' }
).addTo(map);
// Make the country border brighter in the dark mode.
const dimStyle = () => isDark()
? { fillColor: '#000', fillOpacity: 0.6, stroke: true, color: '#7a8390', weight: 1, opacity: 0.8 }
: { fillColor: '#ddd', fillOpacity: 0.6, stroke: false };
// Fetch India's boundaries and apply to the map.
// Swap CARTO basemap + labels + dim styling to match the active theme.
let baseLayer, labelLayer, dimLayer;
function applyMapTheme() {
const v = isDark() ? 'dark' : 'light';
baseLayer?.remove();
labelLayer?.remove();
baseLayer = L.tileLayer(`https://{s}.basemaps.cartocdn.com/${v}_nolabels/{z}/{x}/{y}{r}.png`, {
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a>, &copy; <a href="https://carto.com/">CARTO</a>',
subdomains: 'abcd'
}).addTo(map);
labelLayer = L.tileLayer(`https://{s}.basemaps.cartocdn.com/${v}_only_labels/{z}/{x}/{y}{r}.png`,
{ subdomains: 'abcd', pane: 'shadowPane' }).addTo(map);
dimLayer?.setStyle(dimStyle());
}
applyMapTheme();
themeListeners.push(applyMapTheme);
// Fetch India's boundaries and dim everything else outside it.
fetch('/india.geojson').then(r => r.json()).then(india => {
const world = [[-90, -180], [-90, 180], [90, 180], [90, -180]];
const polys = india.geometry.type === 'Polygon' ? [india.geometry.coordinates] : india.geometry.coordinates;
L.polygon([world, ...polys.map(p => p[0].map(c => [c[1], c[0]]))], {
color: 'none', fillColor: '#ddd', fillOpacity: 0.6, interactive: false
}).addTo(map);
addLabels();
}).catch(addLabels);
dimLayer = L.polygon([world, ...polys.map(p => p[0].map(c => [c[1], c[0]]))],
{ interactive: false, ...dimStyle() }).addTo(map);
}).catch(() => {});
items.forEach(item => {
const lat = parseFloat(item.dataset.lat), lng = parseFloat(item.dataset.lng);
+40 -15
View File
@@ -1,14 +1,23 @@
:root {
color-scheme: light dark;
--text-heading: 1.1rem;
--text-regular: 0.9rem;
--text-small: 0.8rem;
--text-xsmall: 0.7rem;
--color-primary: blue;
--color-muted: #888;
--color-border: #ddd;
--color-hover: #f5f5f5;
--color-bg: light-dark(#ffffff, #16181c);
--color-text: light-dark(#1c1e21, #e3e5e8);
--color-primary: light-dark(#1a53d8, #7aa2ff);
--color-visited: light-dark(#7b3ff2, #c0a6ff);
--color-muted: light-dark(#6b7075, #979ca3);
--color-border: light-dark(#dddddd, #2c3037);
--color-hover: light-dark(#f5f5f5, #22262d);
}
:root:has(body[data-theme="dark"]) { color-scheme: dark; }
:root:has(body[data-theme="light"]) { color-scheme: light; }
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
@@ -24,12 +33,14 @@ body {
line-height: 1.5;
display: flex;
flex-direction: column;
background: var(--color-bg);
color: var(--color-text);
}
a {
text-decoration: none;
color: var(--color-primary);
&:visited { color: purple; }
&:visited { color: var(--color-visited); }
&:hover { color: inherit; }
&.light {
@@ -57,7 +68,9 @@ input[type="checkbox"] {
content: '';
position: absolute;
inset: 0;
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2 6L5 9.5L10 2.5' stroke='blue' stroke-width='2.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/80% no-repeat;
background: var(--color-primary);
-webkit-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2 6L5 9.5L10 2.5' stroke='%23000' stroke-width='2.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/80% no-repeat;
mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 12 12' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2 6L5 9.5L10 2.5' stroke='%23000' stroke-width='2.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/80% no-repeat;
}
&:focus-visible {
@@ -70,7 +83,7 @@ input[type="checkbox"] {
header {
display: flex;
flex-wrap: wrap;
align-items: baseline;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--color-border);
@@ -84,9 +97,28 @@ header {
font-size: var(--text-xsmall);
color: var(--color-muted);
display: inline-flex;
align-items: center;
gap: 1rem;
}
.theme-toggle {
display: inline-flex;
align-items: center;
padding: 0;
background: none;
border: none;
line-height: 0;
cursor: pointer;
color: var(--color-muted);
&:hover { color: var(--color-primary); }
&:focus-visible {
outline: 1.5px solid var(--color-primary);
outline-offset: 2px;
border-radius: 2px;
}
}
/* Three-column layout */
.layout {
display: flex;
@@ -288,14 +320,7 @@ main.detail {
body { display: block; }
header { flex-direction: column; align-items: flex-start; }
.header-right {
margin-left: 0;
display: block;
> * {
display: block;
}
}
.header-right { margin-left: 0; }
.layout { flex-direction: column; flex: none; }
+8 -1
View File
@@ -10,12 +10,19 @@
{% block meta %}{% endblock %}
</head>
<body>
<script>try{var t=localStorage.getItem('ooru-theme');if(t)document.body.dataset.theme=t}catch(e){}</script>
<header>
<h1><a href="{{ get_url(path='/') }}">ooru.space</a></h1>
{% block subtitle %}{% endblock %}
<nav class="header-right">
<a href="https://gitea.samagata.org/ooru.space/ooru.space">Source</a>
<span>Add or correct something? <a href="https://docs.google.com/forms/d/1I6JjCiQ4_wVXgY1Y9EbpmJht4T6D714QIEDnAAR-nOk">Send suggestions</a></span>
<a href="https://docs.google.com/forms/d/1I6JjCiQ4_wVXgY1Y9EbpmJht4T6D714QIEDnAAR-nOk">Send suggestions</a>
<button type="button" id="theme-toggle" class="theme-toggle" title="Toggle dark mode" aria-label="Toggle dark mode">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
<circle cx="12" cy="12" r="4.5"/>
<path d="M12 1.5v3M12 19.5v3M4.22 4.22l2.12 2.12M17.66 17.66l2.12 2.12M1.5 12h3M19.5 12h3M4.22 19.78l2.12-2.12M17.66 6.34l2.12-2.12"/>
</svg>
</button>
</nav>
</header>
{% block content %}{% endblock %}