Add dark mode with system default and toggle

This commit is contained in:
Justin Oros
2026-10-07 14:34:05 -07:00
parent cb3cad3e1f
commit 50a060bacf
3 changed files with 210 additions and 24 deletions
+111 -9
View File
@@ -9,10 +9,68 @@
--jacket: #F0B400;
--contract: #A23B72;
--error: #9B2C2C;
--field: #FFFFFF;
--map-bg: #E4E8EA;
--button: #17212B;
--button-text: #FFFFFF;
--badge-bg: #F3E4EC;
--badge-open-bg: #FCEFC4;
--badge-open-text: #6B5000;
--legend-bg: rgba(252, 253, 253, 0.94);
--glint: rgba(255, 255, 255, 0.95);
color-scheme: light;
--font: "Barlow", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--display: "Barlow Condensed", "Arial Narrow", "Roboto Condensed", sans-serif;
}
:root[data-theme="dark"] {
--ink: #E3E9EE;
--muted: #9AA9B6;
--paper: #0E161D;
--panel: #141E27;
--line: #2A3946;
--strand: #2BB5C0;
--strand-deep: #6FD3DA;
--jacket: #F2BE1E;
--contract: #D86FA8;
--error: #F29A9A;
--field: #0F1922;
--map-bg: #1C252C;
--button: #E3E9EE;
--button-text: #141E27;
--badge-bg: #3A2231;
--badge-open-bg: #3A3010;
--badge-open-text: #F2D27A;
--legend-bg: rgba(20, 30, 39, 0.94);
--glint: rgba(255, 255, 255, 0.6);
color-scheme: dark;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--ink: #E3E9EE;
--muted: #9AA9B6;
--paper: #0E161D;
--panel: #141E27;
--line: #2A3946;
--strand: #2BB5C0;
--strand-deep: #6FD3DA;
--jacket: #F2BE1E;
--contract: #D86FA8;
--error: #F29A9A;
--field: #0F1922;
--map-bg: #1C252C;
--button: #E3E9EE;
--button-text: #141E27;
--badge-bg: #3A2231;
--badge-open-bg: #3A3010;
--badge-open-text: #F2D27A;
--legend-bg: rgba(20, 30, 39, 0.94);
--glint: rgba(255, 255, 255, 0.6);
color-scheme: dark;
}
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
@@ -41,7 +99,7 @@ body {
border-right: 1px solid var(--line);
}
.map { height: 100%; min-height: 320px; background: #E4E8EA; }
.map { height: 100%; min-height: 320px; background: var(--map-bg); }
h1 {
margin: 0;
@@ -60,7 +118,7 @@ h1 {
font: inherit;
padding: 12px 14px;
color: var(--ink);
background: #fff;
background: var(--field);
border: 1.5px solid var(--line);
border-radius: 6px;
}
@@ -74,17 +132,61 @@ h1 {
.search button {
font: 600 1rem var(--font);
padding: 12px 16px;
color: #fff;
background: var(--ink);
color: var(--button-text);
background: var(--button);
border: 0;
border-radius: 6px;
cursor: pointer;
}
.search button:hover { background: var(--strand-deep); }
.search button:hover { color: var(--button-text); background: var(--strand); }
.search button:focus-visible { outline: 3px solid var(--jacket); outline-offset: 2px; }
.search button[disabled] { opacity: 0.6; cursor: progress; }
.top { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.theme {
flex-shrink: 0;
margin-top: 4px;
padding: 5px 10px;
font: 500 0.85rem var(--font);
color: var(--ink);
background: transparent;
border: 1px solid var(--line);
border-radius: 14px;
cursor: pointer;
}
.theme:hover { border-color: var(--strand); }
.theme:focus-visible { outline: 3px solid var(--jacket); outline-offset: 2px; }
.leaflet-container { background: var(--map-bg); font-family: var(--font); }
.leaflet-tooltip {
color: var(--ink);
background: var(--panel);
border-color: var(--line);
}
.leaflet-tooltip-top::before { border-top-color: var(--panel); }
.leaflet-tooltip-bottom::before { border-bottom-color: var(--panel); }
.leaflet-tooltip-left::before { border-left-color: var(--panel); }
.leaflet-tooltip-right::before { border-right-color: var(--panel); }
.leaflet-bar a,
.leaflet-bar a:hover {
color: var(--ink);
background: var(--panel);
border-bottom-color: var(--line);
}
.leaflet-control-attribution.leaflet-control {
color: var(--muted);
background: var(--legend-bg);
}
.leaflet-control-attribution a { color: var(--strand-deep); }
.result { display: flex; flex-direction: column; gap: 26px; }
.result:empty { display: none; }
@@ -119,7 +221,7 @@ h1 {
position: absolute;
inset: 0;
width: 30%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.95), transparent);
background: linear-gradient(90deg, transparent, var(--glint), transparent);
transform: translateX(-120%);
animation: pulse 1.4s ease-out 0.15s 1 forwards;
}
@@ -180,11 +282,11 @@ h1 {
font-size: 0.8rem;
font-weight: 600;
color: var(--contract);
background: #F3E4EC;
background: var(--badge-bg);
border-radius: 10px;
}
.badge.open { color: #6B5000; background: #FCEFC4; }
.badge.open { color: var(--badge-open-text); background: var(--badge-open-bg); }
.result a, .meta a { color: var(--strand-deep); }
@@ -214,7 +316,7 @@ h1 {
padding: 8px 10px;
font: 0.82rem var(--font);
color: var(--ink);
background: rgba(252, 253, 253, 0.94);
background: var(--legend-bg);
border: 1px solid var(--line);
border-radius: 6px;
}
+6 -1
View File
@@ -3,6 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<script>try{var t=localStorage.getItem("theme");if(t==="dark"||t==="light")document.documentElement.dataset.theme=t}catch(e){}</script>
<title>Got Fiber</title>
<meta name="description" content="Check whether fiber internet is reported at a US address and see broadband contracts and bids within 50 miles.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='9' fill='%23F0B400' stroke='%2317212B' stroke-width='4'/%3E%3C/svg%3E">
@@ -16,7 +18,10 @@
<main class="app">
<aside class="panel">
<header>
<h1>Got Fiber?</h1>
<div class="top">
<h1>Got Fiber?</h1>
<button id="theme" class="theme" type="button" aria-pressed="false">Dark mode</button>
</div>
<p class="lede">Enter a home address to see if fiber internet is reported there, who offers it, and which broadband contracts and bids are within 50 miles.</p>
</header>
<form id="search" class="search">
+93 -14
View File
@@ -1,6 +1,7 @@
const RADIUS_KM = 80.4672;
const KM_PER_MI = 1.609344;
const COLORS = { strand: "#0A8F99", jacket: "#F0B400", ink: "#17212B", contract: "#A23B72" };
let COLORS = { strand: "#0A8F99", jacket: "#F0B400", ink: "#17212B", contract: "#A23B72" };
const DARK_QUERY = window.matchMedia("(prefers-color-scheme: dark)");
const STATUS = {
open: "Open for bids",
proposed: "Proposed",
@@ -89,32 +90,106 @@ function geomDistanceKm(geom, latlng) {
return best;
}
function cssVar(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
function savedTheme() {
try {
const t = localStorage.getItem("theme");
return t === "dark" || t === "light" ? t : null;
} catch {
return null;
}
}
function currentTheme() {
return savedTheme() || S.themeOverride || (DARK_QUERY.matches ? "dark" : "light");
}
function refreshColors() {
COLORS = {
strand: cssVar("--strand") || COLORS.strand,
jacket: cssVar("--jacket") || COLORS.jacket,
ink: cssVar("--ink") || COLORS.ink,
contract: cssVar("--contract") || COLORS.contract
};
}
function setTiles() {
const dark = currentTheme() === "dark";
const shade = dark ? "Dark" : "Light";
const esri = "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/";
if (S.tiles) S.tiles.forEach(t => S.map.removeLayer(t));
S.tiles = [
L.tileLayer(esri + `World_${shade}_Gray_Base/MapServer/tile/{z}/{y}/{x}`, {
maxZoom: 16,
attribution: "Tiles &copy; Esri, HERE, Garmin, &copy; OpenStreetMap contributors"
}),
L.tileLayer(esri + `World_${shade}_Gray_Reference/MapServer/tile/{z}/{y}/{x}`, { maxZoom: 16, pane: "labels" })
];
S.tiles.forEach(t => t.addTo(S.map));
}
function renderLegend() {
if (!S.legendEl) return;
S.legendEl.innerHTML =
`<div><i style="background:${COLORS.strand};opacity:.55"></i>Fiber reported</div>` +
`<div><i style="background:${COLORS.contract};opacity:.55"></i>Contract or bid area</div>` +
`<div><i style="border:2px dashed ${COLORS.ink}"></i>50 miles</div>`;
}
function updateToggle() {
const btn = $("theme");
if (!btn) return;
const dark = currentTheme() === "dark";
btn.textContent = dark ? "Light mode" : "Dark mode";
btn.setAttribute("aria-pressed", String(dark));
}
function applyTheme() {
const saved = savedTheme() || S.themeOverride;
if (saved) document.documentElement.dataset.theme = saved;
else delete document.documentElement.dataset.theme;
refreshColors();
setTiles();
renderLegend();
updateToggle();
if (S.last) {
const { place, at, region, bead, deals } = S.last;
drawRegion(region, bead);
drawContracts(deals.items);
drawFocus(place, at);
}
}
function toggleTheme() {
const next = currentTheme() === "dark" ? "light" : "dark";
S.themeOverride = next;
try {
localStorage.setItem("theme", next);
} catch {}
applyTheme();
}
function initMap() {
const map = L.map("map", { preferCanvas: true }).setView([34.3, -111.7], 6);
const esri = "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/";
L.tileLayer(esri + "World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}", {
maxZoom: 16,
attribution: "Tiles &copy; Esri, HERE, Garmin, &copy; OpenStreetMap contributors"
}).addTo(map);
S.map = map;
const labels = map.createPane("labels");
labels.style.zIndex = 450;
labels.style.pointerEvents = "none";
L.tileLayer(esri + "World_Light_Gray_Reference/MapServer/tile/{z}/{y}/{x}", { maxZoom: 16, pane: "labels" }).addTo(map);
S.layers.bead = L.layerGroup().addTo(map);
S.layers.fiber = L.layerGroup().addTo(map);
S.layers.contracts = L.layerGroup().addTo(map);
S.layers.focus = L.layerGroup().addTo(map);
const legend = L.control({ position: "bottomright" });
legend.onAdd = () => {
const d = L.DomUtil.create("div", "legend");
d.innerHTML =
`<div><i style="background:${COLORS.strand};opacity:.55"></i>Fiber reported</div>` +
`<div><i style="background:${COLORS.contract};opacity:.55"></i>Contract or bid area</div>` +
`<div><i style="border:2px dashed ${COLORS.ink}"></i>50 miles</div>`;
return d;
S.legendEl = L.DomUtil.create("div", "legend");
renderLegend();
return S.legendEl;
};
legend.addTo(map);
S.map = map;
applyTheme();
}
async function geocodeEsri(q) {
@@ -667,6 +742,7 @@ async function check(q) {
if (bead && (S.manifest.beadStates || []).includes(place.state)) {
deals.items = deals.items.filter(it => it.program !== "BEAD");
}
S.last = { place, at, region, bead, deals };
drawRegion(region, bead);
drawContracts(deals.items);
drawFocus(place, at);
@@ -684,7 +760,10 @@ async function check(q) {
}
async function main() {
refreshColors();
initMap();
$("theme").addEventListener("click", toggleTheme);
DARK_QUERY.addEventListener("change", () => { if (!savedTheme() && !S.themeOverride) applyTheme(); });
S.manifest = (await getJSON("data/manifest.json")) || { states: [], contractStates: [], fineRes: 8, fineShardRes: 5, coarseRes: 6, coarseShardRes: 3 };
renderMeta();
$("search").addEventListener("submit", e => {