Add dark mode with system default and toggle
This commit is contained in:
+111
-9
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
<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">
|
||||
|
||||
@@ -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 initMap() {
|
||||
const map = L.map("map", { preferCanvas: true }).setView([34.3, -111.7], 6);
|
||||
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/";
|
||||
L.tileLayer(esri + "World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}", {
|
||||
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 © Esri, HERE, Garmin, © OpenStreetMap contributors"
|
||||
}).addTo(map);
|
||||
}),
|
||||
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);
|
||||
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 => {
|
||||
|
||||
Reference in New Issue
Block a user