Convert the frontend to ES modules; split nav.js by concern (#48)
The frontend was classic scripts sharing one global scope, with a
load-order contract enforced only by comments (leaflet -> nav ->
shared -> mappicker -> page) and hand-rolled window.Thermograph /
window.LocationPicker namespaces. Page scripts now import what they use;
the dependency graph replaces the ordering contract, and no app globals
remain (Leaflet stays a classic script / global L, loaded first).
nav.js had grown four concerns; it's now three single-purpose modules:
- nav.js: last-location memory + header view-links + locHash.
- units.js: the °F/°C toggle and unit-aware formatting. The compare
special case is gone — pages that want the toggle import units.js;
compare simply doesn't.
- cache.js: the IndexedDB response cache, SWR getJSON, bundle-seeded
view prefetch and neighbor warming. prefetchViews(lat, lon, ownViews)
now takes the calling page's own view names instead of a page-identity
map (VIEW_OWN) — adding a page no longer means editing this module.
The slice->URL map stays here as bundle-contract knowledge.
frontend/package.json ({"type": "module"}) makes CI's node --check
parse the files as modules.
Verified: node --check on all files as modules; 108 backend tests;
headless-Chromium smoke across all five pages against live data — zero
console/page errors, all render assertions pass (cards, chart, calendar
grid + metric switch, ladders, compare ranking, legend scales).
2026-07-11 20:28:33 +00:00
|
|
|
// Temperature units — the shared °C/°F toggle and unit-aware formatting.
|
|
|
|
|
// The API always speaks Fahrenheit; the unit is a pure display concern. Pages
|
|
|
|
|
// format temps through `fmtTemp`/`toUnit` and re-render on `onUnitChange`, so the
|
|
|
|
|
// stored/plotted values stay in °F and only the numbers shown flip.
|
|
|
|
|
|
|
|
|
|
const UNIT_KEY = "thermograph:unit";
|
|
|
|
|
let _unit = (localStorage.getItem(UNIT_KEY) === "C") ? "C" : "F";
|
|
|
|
|
const _unitCbs = [];
|
|
|
|
|
|
|
|
|
|
export function getUnit() { return _unit; }
|
|
|
|
|
|
|
|
|
|
// A Fahrenheit value as a number in the active unit.
|
|
|
|
|
export function toUnit(vF) { return _unit === "C" ? (vF - 32) * 5 / 9 : vF; }
|
|
|
|
|
|
|
|
|
|
// A Fahrenheit value formatted as a rounded temperature in the active unit.
|
|
|
|
|
// `withLetter` appends the C/F letter (off by default — the nav toggle shows it).
|
|
|
|
|
export function fmtTemp(vF, withLetter) {
|
|
|
|
|
if (vF == null || isNaN(vF)) return "—";
|
|
|
|
|
return `${Math.round(toUnit(vF))}°${withLetter ? _unit : ""}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 21:48:21 +00:00
|
|
|
// A Fahrenheit-degree *difference* (a span or tolerance, not an absolute
|
|
|
|
|
// reading) formatted in the active unit. Differences scale by 5/9 with no 32°
|
|
|
|
|
// offset, so they can't go through toUnit/fmtTemp.
|
|
|
|
|
export function fmtDelta(dF, withLetter) {
|
|
|
|
|
if (dF == null || isNaN(dF)) return "—";
|
|
|
|
|
const d = _unit === "C" ? dF * 5 / 9 : dF;
|
|
|
|
|
return `${Math.round(d)}°${withLetter ? _unit : ""}`;
|
|
|
|
|
}
|
|
|
|
|
|
Convert the frontend to ES modules; split nav.js by concern (#48)
The frontend was classic scripts sharing one global scope, with a
load-order contract enforced only by comments (leaflet -> nav ->
shared -> mappicker -> page) and hand-rolled window.Thermograph /
window.LocationPicker namespaces. Page scripts now import what they use;
the dependency graph replaces the ordering contract, and no app globals
remain (Leaflet stays a classic script / global L, loaded first).
nav.js had grown four concerns; it's now three single-purpose modules:
- nav.js: last-location memory + header view-links + locHash.
- units.js: the °F/°C toggle and unit-aware formatting. The compare
special case is gone — pages that want the toggle import units.js;
compare simply doesn't.
- cache.js: the IndexedDB response cache, SWR getJSON, bundle-seeded
view prefetch and neighbor warming. prefetchViews(lat, lon, ownViews)
now takes the calling page's own view names instead of a page-identity
map (VIEW_OWN) — adding a page no longer means editing this module.
The slice->URL map stays here as bundle-contract knowledge.
frontend/package.json ({"type": "module"}) makes CI's node --check
parse the files as modules.
Verified: node --check on all files as modules; 108 backend tests;
headless-Chromium smoke across all five pages against live data — zero
console/page errors, all render assertions pass (cards, chart, calendar
grid + metric switch, ladders, compare ranking, legend scales).
2026-07-11 20:28:33 +00:00
|
|
|
export function onUnitChange(cb) { _unitCbs.push(cb); }
|
|
|
|
|
|
|
|
|
|
export function setUnit(u) {
|
|
|
|
|
const nu = (u === "C") ? "C" : "F";
|
|
|
|
|
if (nu === _unit) return;
|
|
|
|
|
_unit = nu;
|
|
|
|
|
try { localStorage.setItem(UNIT_KEY, _unit); } catch (e) {}
|
|
|
|
|
document.querySelectorAll(".unit-toggle").forEach(syncUnitToggle);
|
|
|
|
|
_unitCbs.forEach((cb) => { try { cb(_unit); } catch (e) {} });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function syncUnitToggle(wrap) {
|
|
|
|
|
wrap.querySelectorAll("button[data-unit]").forEach((b) => {
|
|
|
|
|
const on = b.dataset.unit === _unit;
|
|
|
|
|
b.classList.toggle("active", on);
|
|
|
|
|
b.setAttribute("aria-pressed", on ? "true" : "false");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drop a segmented °F/°C control into the header's top-right, ahead of the view
|
2026-07-11 21:48:21 +00:00
|
|
|
// switcher. A page opts out with data-no-unit-toggle on <body>.
|
Convert the frontend to ES modules; split nav.js by concern (#48)
The frontend was classic scripts sharing one global scope, with a
load-order contract enforced only by comments (leaflet -> nav ->
shared -> mappicker -> page) and hand-rolled window.Thermograph /
window.LocationPicker namespaces. Page scripts now import what they use;
the dependency graph replaces the ordering contract, and no app globals
remain (Leaflet stays a classic script / global L, loaded first).
nav.js had grown four concerns; it's now three single-purpose modules:
- nav.js: last-location memory + header view-links + locHash.
- units.js: the °F/°C toggle and unit-aware formatting. The compare
special case is gone — pages that want the toggle import units.js;
compare simply doesn't.
- cache.js: the IndexedDB response cache, SWR getJSON, bundle-seeded
view prefetch and neighbor warming. prefetchViews(lat, lon, ownViews)
now takes the calling page's own view names instead of a page-identity
map (VIEW_OWN) — adding a page no longer means editing this module.
The slice->URL map stays here as bundle-contract knowledge.
frontend/package.json ({"type": "module"}) makes CI's node --check
parse the files as modules.
Verified: node --check on all files as modules; 108 backend tests;
headless-Chromium smoke across all five pages against live data — zero
console/page errors, all render assertions pass (cards, chart, calendar
grid + metric switch, ladders, compare ranking, legend scales).
2026-07-11 20:28:33 +00:00
|
|
|
(function buildUnitToggle() {
|
|
|
|
|
const brand = document.querySelector(".brand");
|
|
|
|
|
if (!brand || brand.querySelector(".unit-toggle")) return;
|
|
|
|
|
if (document.body.dataset.noUnitToggle != null) return;
|
|
|
|
|
const wrap = document.createElement("div");
|
|
|
|
|
wrap.className = "unit-toggle";
|
|
|
|
|
wrap.setAttribute("role", "group");
|
|
|
|
|
wrap.setAttribute("aria-label", "Temperature units");
|
|
|
|
|
wrap.innerHTML = '<button type="button" data-unit="F">°F</button>'
|
|
|
|
|
+ '<button type="button" data-unit="C">°C</button>';
|
|
|
|
|
wrap.addEventListener("click", (e) => {
|
|
|
|
|
const b = e.target.closest("button[data-unit]");
|
|
|
|
|
if (b) setUnit(b.dataset.unit);
|
|
|
|
|
});
|
2026-07-16 05:42:59 +00:00
|
|
|
const anchor = brand.querySelector(".nav-menu") || brand.querySelector(".view-nav");
|
|
|
|
|
brand.insertBefore(wrap, anchor); // sits just left of the nav (top-right)
|
Convert the frontend to ES modules; split nav.js by concern (#48)
The frontend was classic scripts sharing one global scope, with a
load-order contract enforced only by comments (leaflet -> nav ->
shared -> mappicker -> page) and hand-rolled window.Thermograph /
window.LocationPicker namespaces. Page scripts now import what they use;
the dependency graph replaces the ordering contract, and no app globals
remain (Leaflet stays a classic script / global L, loaded first).
nav.js had grown four concerns; it's now three single-purpose modules:
- nav.js: last-location memory + header view-links + locHash.
- units.js: the °F/°C toggle and unit-aware formatting. The compare
special case is gone — pages that want the toggle import units.js;
compare simply doesn't.
- cache.js: the IndexedDB response cache, SWR getJSON, bundle-seeded
view prefetch and neighbor warming. prefetchViews(lat, lon, ownViews)
now takes the calling page's own view names instead of a page-identity
map (VIEW_OWN) — adding a page no longer means editing this module.
The slice->URL map stays here as bundle-contract knowledge.
frontend/package.json ({"type": "module"}) makes CI's node --check
parse the files as modules.
Verified: node --check on all files as modules; 108 backend tests;
headless-Chromium smoke across all five pages against live data — zero
console/page errors, all render assertions pass (cards, chart, calendar
grid + metric switch, ladders, compare ranking, legend scales).
2026-07-11 20:28:33 +00:00
|
|
|
syncUnitToggle(wrap);
|
|
|
|
|
})();
|