Lint/format cleanup: void-expression and empty-function fixes, optional-chain narrowing, ignore share/ in eslint+prettier
This commit is contained in:
parent
3cd9a2377a
commit
99419880a7
11 changed files with 208 additions and 75 deletions
|
|
@ -2,4 +2,5 @@ node_modules
|
|||
dist
|
||||
coverage
|
||||
experiments
|
||||
share
|
||||
*.md
|
||||
|
|
|
|||
|
|
@ -6,7 +6,14 @@ import globals from "globals";
|
|||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ["dist/**", "node_modules/**", "coverage/**", "experiments/**", "public/sw.js"],
|
||||
ignores: [
|
||||
"dist/**",
|
||||
"node_modules/**",
|
||||
"coverage/**",
|
||||
"experiments/**",
|
||||
"public/sw.js",
|
||||
"share/**",
|
||||
],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
<script type="module" src="/src/app/main.ts"></script>
|
||||
<noscript>
|
||||
<p role="alert">
|
||||
Solarpunk requires JavaScript to run the app shell. Emergency information is available on the
|
||||
printed festival materials.
|
||||
Solarpunk requires JavaScript to run the app shell. Emergency information is available on
|
||||
the printed festival materials.
|
||||
</p>
|
||||
</noscript>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -40,12 +40,28 @@ export function createLayout(): {
|
|||
themeBtn.setAttribute("aria-label", "Toggle light/dark theme");
|
||||
themeBtn.onclick = () => {
|
||||
const html = document.documentElement;
|
||||
if (html.classList.contains("dark")) { html.classList.remove("dark"); html.classList.add("light"); localStorage.setItem("theme","light"); themeBtn.textContent="☀"; }
|
||||
else { document.documentElement.classList.remove("light"); document.documentElement.classList.add("dark"); localStorage.setItem("theme","dark"); themeBtn.textContent="◐"; }
|
||||
if (html.classList.contains("dark")) {
|
||||
html.classList.remove("dark");
|
||||
html.classList.add("light");
|
||||
localStorage.setItem("theme", "light");
|
||||
themeBtn.textContent = "☀";
|
||||
} else {
|
||||
document.documentElement.classList.remove("light");
|
||||
document.documentElement.classList.add("dark");
|
||||
localStorage.setItem("theme", "dark");
|
||||
themeBtn.textContent = "◐";
|
||||
}
|
||||
};
|
||||
// Apply saved theme on load
|
||||
if (localStorage.getItem("theme") === "dark") { document.documentElement.classList.add("dark"); document.documentElement.classList.remove("light"); themeBtn.textContent="◐"; }
|
||||
else { document.documentElement.classList.add("light"); document.documentElement.classList.remove("dark"); themeBtn.textContent="☀"; }
|
||||
if (localStorage.getItem("theme") === "dark") {
|
||||
document.documentElement.classList.add("dark");
|
||||
document.documentElement.classList.remove("light");
|
||||
themeBtn.textContent = "◐";
|
||||
} else {
|
||||
document.documentElement.classList.add("light");
|
||||
document.documentElement.classList.remove("dark");
|
||||
themeBtn.textContent = "☀";
|
||||
}
|
||||
header.append(themeBtn);
|
||||
|
||||
const statusChip = document.createElement("div");
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ export interface MapUrlFactory {
|
|||
|
||||
const defaultUrls: MapUrlFactory = {
|
||||
createUrl: (blob) => URL.createObjectURL(blob),
|
||||
revokeUrl: (url) => URL.revokeObjectURL(url),
|
||||
revokeUrl: (url) => {
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
};
|
||||
|
||||
export interface MapHandle {
|
||||
|
|
@ -43,7 +45,12 @@ export async function loadMapData(urls: MapUrlFactory = defaultUrls): Promise<Ma
|
|||
return { data, revoke };
|
||||
} catch {
|
||||
revoke();
|
||||
return { data: null, revoke: () => {} };
|
||||
return {
|
||||
data: null,
|
||||
revoke: () => {
|
||||
/* nothing was created */
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,40 +110,91 @@ html {
|
|||
scroll-behavior: smooth;
|
||||
}
|
||||
html.dark {
|
||||
--bg: #0b1411; --fg: #ede9de; --deck-bg: #0b1411; --deck-fg: #ede9de; --deck-surface: rgba(255,255,255,0.035); --deck-line: rgba(255,255,255,0.1); --accent: #75c7a9; --surface: rgba(255,255,255,0.035); --border: rgba(255,255,255,0.1);
|
||||
--bg: #0b1411;
|
||||
--fg: #ede9de;
|
||||
--deck-bg: #0b1411;
|
||||
--deck-fg: #ede9de;
|
||||
--deck-surface: rgba(255, 255, 255, 0.035);
|
||||
--deck-line: rgba(255, 255, 255, 0.1);
|
||||
--accent: #75c7a9;
|
||||
--surface: rgba(255, 255, 255, 0.035);
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
html.light :root,
|
||||
html.light body {
|
||||
--deck-bg: #f6f9f7; --deck-fg: #172621; --deck-fg-muted: #576b63;
|
||||
--deck-surface: rgba(20,30,25,0.04); --deck-surface-2: rgba(20,30,25,0.06); --deck-line: rgba(20,30,25,0.12); --deck-line-2: rgba(20,30,25,0.2);
|
||||
--deck-pine: #1f7a5f; --deck-indigo: #683ecc; --deck-rose: #d3224b; --deck-clay: #bf4622; --deck-amber: #b8610a;
|
||||
--bg: #f6f9f7; --fg: #172621; --muted: #576b63; --accent: #1f7a5f; --surface: rgba(20,30,25,0.04); --border: rgba(20,30,25,0.12);
|
||||
--highlight-bg: rgba(184,97,10,0.07); --highlight-border: rgba(184,97,10,0.3);
|
||||
--chip-pink-bg: rgba(211,34,75,0.08); --chip-pink-fg: #b5244a;
|
||||
--chip-green-bg: rgba(31,122,95,0.09); --chip-green-fg: #1f7a5f;
|
||||
--chip-indigo-bg: rgba(104,62,204,0.08); --chip-indigo-fg: #683ecc;
|
||||
--chip-amber-bg: rgba(184,97,10,0.09); --chip-amber-fg: #96500a;
|
||||
--focus: #1f7a5f; --emergency: #d3224b; --emergency-contrast: #fff;
|
||||
--deck-bg: #f6f9f7;
|
||||
--deck-fg: #172621;
|
||||
--deck-fg-muted: #576b63;
|
||||
--deck-surface: rgba(20, 30, 25, 0.04);
|
||||
--deck-surface-2: rgba(20, 30, 25, 0.06);
|
||||
--deck-line: rgba(20, 30, 25, 0.12);
|
||||
--deck-line-2: rgba(20, 30, 25, 0.2);
|
||||
--deck-pine: #1f7a5f;
|
||||
--deck-indigo: #683ecc;
|
||||
--deck-rose: #d3224b;
|
||||
--deck-clay: #bf4622;
|
||||
--deck-amber: #b8610a;
|
||||
--bg: #f6f9f7;
|
||||
--fg: #172621;
|
||||
--muted: #576b63;
|
||||
--accent: #1f7a5f;
|
||||
--surface: rgba(20, 30, 25, 0.04);
|
||||
--border: rgba(20, 30, 25, 0.12);
|
||||
--highlight-bg: rgba(184, 97, 10, 0.07);
|
||||
--highlight-border: rgba(184, 97, 10, 0.3);
|
||||
--chip-pink-bg: rgba(211, 34, 75, 0.08);
|
||||
--chip-pink-fg: #b5244a;
|
||||
--chip-green-bg: rgba(31, 122, 95, 0.09);
|
||||
--chip-green-fg: #1f7a5f;
|
||||
--chip-indigo-bg: rgba(104, 62, 204, 0.08);
|
||||
--chip-indigo-fg: #683ecc;
|
||||
--chip-amber-bg: rgba(184, 97, 10, 0.09);
|
||||
--chip-amber-fg: #96500a;
|
||||
--focus: #1f7a5f;
|
||||
--emergency: #d3224b;
|
||||
--emergency-contrast: #fff;
|
||||
}
|
||||
html.dark :root,
|
||||
html.dark body {
|
||||
--deck-bg: #0b1411; --deck-fg: #ede9de; --deck-fg-muted: #96a69e;
|
||||
--deck-surface: rgba(255,255,255,0.035); --deck-surface-2: rgba(255,255,255,0.055);
|
||||
--deck-line: rgba(255,255,255,0.1); --deck-line-2: rgba(255,255,255,0.16);
|
||||
--deck-pine: #75c7a9; --deck-indigo: #af97f7; --deck-rose: #fc88ab; --deck-clay: #e47958; --deck-amber: #fcc669;
|
||||
--bg: var(--deck-bg); --fg: var(--deck-fg); --muted: var(--deck-fg-muted);
|
||||
--accent: var(--deck-pine); --accent-contrast: var(--deck-on-accent);
|
||||
--accent-soft: rgba(117,199,169,0.12);
|
||||
--surface: var(--deck-surface); --border: var(--deck-line); --border-accent: rgba(117,199,169,0.32);
|
||||
--highlight-bg: rgba(252,198,105,0.07); --highlight-border: rgba(252,198,105,0.3);
|
||||
--chip-pink-bg: rgba(252,136,171,0.12); --chip-pink-fg: #fc88ab;
|
||||
--chip-green-bg: rgba(117,199,169,0.12); --chip-green-fg: #75c7a9;
|
||||
--chip-indigo-bg: rgba(175,151,247,0.12); --chip-indigo-fg: #af97f7;
|
||||
--chip-amber-bg: rgba(252,198,105,0.12); --chip-amber-fg: #fcc669;
|
||||
--focus: var(--deck-pine); --emergency: #fc88ab; --emergency-contrast: var(--deck-on-accent);
|
||||
--deck-bg: #0b1411;
|
||||
--deck-fg: #ede9de;
|
||||
--deck-fg-muted: #96a69e;
|
||||
--deck-surface: rgba(255, 255, 255, 0.035);
|
||||
--deck-surface-2: rgba(255, 255, 255, 0.055);
|
||||
--deck-line: rgba(255, 255, 255, 0.1);
|
||||
--deck-line-2: rgba(255, 255, 255, 0.16);
|
||||
--deck-pine: #75c7a9;
|
||||
--deck-indigo: #af97f7;
|
||||
--deck-rose: #fc88ab;
|
||||
--deck-clay: #e47958;
|
||||
--deck-amber: #fcc669;
|
||||
--bg: var(--deck-bg);
|
||||
--fg: var(--deck-fg);
|
||||
--muted: var(--deck-fg-muted);
|
||||
--accent: var(--deck-pine);
|
||||
--accent-contrast: var(--deck-on-accent);
|
||||
--accent-soft: rgba(117, 199, 169, 0.12);
|
||||
--surface: var(--deck-surface);
|
||||
--border: var(--deck-line);
|
||||
--border-accent: rgba(117, 199, 169, 0.32);
|
||||
--highlight-bg: rgba(252, 198, 105, 0.07);
|
||||
--highlight-border: rgba(252, 198, 105, 0.3);
|
||||
--chip-pink-bg: rgba(252, 136, 171, 0.12);
|
||||
--chip-pink-fg: #fc88ab;
|
||||
--chip-green-bg: rgba(117, 199, 169, 0.12);
|
||||
--chip-green-fg: #75c7a9;
|
||||
--chip-indigo-bg: rgba(175, 151, 247, 0.12);
|
||||
--chip-indigo-fg: #af97f7;
|
||||
--chip-amber-bg: rgba(252, 198, 105, 0.12);
|
||||
--chip-amber-fg: #fcc669;
|
||||
--focus: var(--deck-pine);
|
||||
--emergency: #fc88ab;
|
||||
--emergency-contrast: var(--deck-on-accent);
|
||||
}
|
||||
html.dark body {
|
||||
background: radial-gradient(120% 80% at 80% -10%, rgba(14,57,44,0.55), transparent 60%), radial-gradient(90% 70% at -10% 110%, rgba(83,48,9,0.35), transparent 55%), var(--bg);
|
||||
background:
|
||||
radial-gradient(120% 80% at 80% -10%, rgba(14, 57, 44, 0.55), transparent 60%),
|
||||
radial-gradient(90% 70% at -10% 110%, rgba(83, 48, 9, 0.35), transparent 55%), var(--bg);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
|
|
@ -359,7 +410,14 @@ h3 {
|
|||
line-height: 1;
|
||||
}
|
||||
.theme-btn {
|
||||
min-height: 36px; padding: 0.25rem 0.5rem; font: inherit; background: var(--surface); border: 1px solid var(--border); border-radius: 999px; cursor: pointer; color: var(--fg);
|
||||
min-height: 36px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font: inherit;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
color: var(--fg);
|
||||
}
|
||||
.app-header__status {
|
||||
font-size: 0.8125rem;
|
||||
|
|
|
|||
|
|
@ -12,15 +12,28 @@ const MS_PER_DAY = 24 * 3600 * 1000;
|
|||
/* ClockService — pure, no network, no persistence. Pure time-model logic per SPIKE-08. */
|
||||
export interface ClockService {
|
||||
correctedNow(deviceNow: number, skew: ClockSkew | null): number;
|
||||
hasDrift(baseDevice: number, monoElapsed: number, observedDevice: number, thresholdMs?: number): boolean;
|
||||
shouldWarnOutsideWindow(nowCorrected: number, win: FestivalWindow, skew: ClockSkew | null, opts?: ClockOptions): boolean;
|
||||
hasDrift(
|
||||
baseDevice: number,
|
||||
monoElapsed: number,
|
||||
observedDevice: number,
|
||||
thresholdMs?: number,
|
||||
): boolean;
|
||||
shouldWarnOutsideWindow(
|
||||
nowCorrected: number,
|
||||
win: FestivalWindow,
|
||||
skew: ClockSkew | null,
|
||||
opts?: ClockOptions,
|
||||
): boolean;
|
||||
renderInZone(epochMs: number, timeZone: string): string;
|
||||
formatEventTime(epochMs: number, timeZone: string): string;
|
||||
formatDayLabel(epochMs: number, timeZone: string): string;
|
||||
formatClockTime(epochMs: number, timeZone: string): string;
|
||||
deviceTimeZone(): string;
|
||||
dayKeyFor(epochMs: number, timeZone: string): string;
|
||||
classifyNowNext<T extends SchedEventRef>(events: readonly T[], now: number): { now: readonly T[]; next: readonly T[] };
|
||||
classifyNowNext<T extends SchedEventRef>(
|
||||
events: readonly T[],
|
||||
now: number,
|
||||
): { now: readonly T[]; next: readonly T[] };
|
||||
}
|
||||
|
||||
/** Render epoch ms in festival IANA zone via Intl (offline, ECMA-402). */
|
||||
|
|
@ -72,7 +85,7 @@ export function formatClockTime(epochMs: number, timeZone: string): string {
|
|||
/** Device IANA zone for the display toggle — UTC fallback when unresolvable. */
|
||||
export function deviceTimeZone(): string {
|
||||
try {
|
||||
return new Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC";
|
||||
return new Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
||||
} catch {
|
||||
return "UTC";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,11 @@ export function groupFacilities(
|
|||
category,
|
||||
pois: [...items].sort((a, b) => a.name.localeCompare(b.name)),
|
||||
}))
|
||||
.sort((a, b) => (order.get(a.category) ?? Number.MAX_SAFE_INTEGER) - (order.get(b.category) ?? Number.MAX_SAFE_INTEGER));
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(order.get(a.category) ?? Number.MAX_SAFE_INTEGER) -
|
||||
(order.get(b.category) ?? Number.MAX_SAFE_INTEGER),
|
||||
);
|
||||
}
|
||||
|
||||
/** Distinct categories present, in section order — for filter chips. */
|
||||
|
|
@ -147,7 +151,7 @@ function validPoi(value: unknown): boolean {
|
|||
/** Structural map check — per-POI tolerance lives in the queries above. */
|
||||
export function narrowMapSection(value: unknown): MapSection | null {
|
||||
const section = record(value);
|
||||
if (!section || section.section !== "map") return null;
|
||||
if (section?.section !== "map") return null;
|
||||
const base = record(section.base);
|
||||
if (!base || !Array.isArray(base.levels) || !base.levels.every(validLevel)) return null;
|
||||
if (!Array.isArray(section.pois) || !section.pois.every(validPoi)) return null;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@
|
|||
* optional lat/lng is a hook for future positioning only (no GPS in V1).
|
||||
* Trace: IMPLEMENTATION-CONTRACT.md §22, SPIKE-03.
|
||||
*/
|
||||
import type { MapLevel, MapPoi, MapSection, PoiCategory } from "../../data/festival-package/types.js";
|
||||
import type {
|
||||
MapLevel,
|
||||
MapPoi,
|
||||
MapSection,
|
||||
PoiCategory,
|
||||
} from "../../data/festival-package/types.js";
|
||||
|
||||
export type { MapPoi, PoiCategory };
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,7 @@ import {
|
|||
poiPosition,
|
||||
presentCategories,
|
||||
} from "../../../domain/map/logic.js";
|
||||
import type {
|
||||
MapLevelView,
|
||||
MapPoi,
|
||||
MapViewData,
|
||||
PoiCategory,
|
||||
} from "../../../domain/map/types.js";
|
||||
import type { MapLevelView, MapPoi, MapViewData, PoiCategory } from "../../../domain/map/types.js";
|
||||
|
||||
const MIN_SCALE = 1;
|
||||
const MAX_SCALE = 4;
|
||||
|
|
@ -65,7 +60,10 @@ export function createMapView(input: MapViewData | null): HTMLElement {
|
|||
card.className = "view-placeholder";
|
||||
card.append(text("p", "The map isn't on this device yet."));
|
||||
card.append(
|
||||
text("p", "Open with internet to get festival data — the emergency floor always works meanwhile."),
|
||||
text(
|
||||
"p",
|
||||
"Open with internet to get festival data — the emergency floor always works meanwhile.",
|
||||
),
|
||||
);
|
||||
section.append(card);
|
||||
return section;
|
||||
|
|
@ -127,7 +125,10 @@ export function createMapView(input: MapViewData | null): HTMLElement {
|
|||
const stage = document.createElement("div");
|
||||
stage.className = "map-stage";
|
||||
stage.tabIndex = 0;
|
||||
stage.setAttribute("aria-label", "Festival map. Drag to pan, use the zoom buttons, or arrow keys. Full facility list follows.");
|
||||
stage.setAttribute(
|
||||
"aria-label",
|
||||
"Festival map. Drag to pan, use the zoom buttons, or arrow keys. Full facility list follows.",
|
||||
);
|
||||
stage.style.touchAction = "none";
|
||||
|
||||
const world = document.createElement("div");
|
||||
|
|
@ -216,7 +217,11 @@ export function createMapView(input: MapViewData | null): HTMLElement {
|
|||
(event) => {
|
||||
event.preventDefault();
|
||||
const rect = stage.getBoundingClientRect();
|
||||
zoomAt(event.clientX - rect.left, event.clientY - rect.top, event.deltaY < 0 ? 1.25 : 1 / 1.25);
|
||||
zoomAt(
|
||||
event.clientX - rect.left,
|
||||
event.clientY - rect.top,
|
||||
event.deltaY < 0 ? 1.25 : 1 / 1.25,
|
||||
);
|
||||
},
|
||||
{ passive: false },
|
||||
);
|
||||
|
|
@ -300,7 +305,9 @@ export function createMapView(input: MapViewData | null): HTMLElement {
|
|||
// Filter application shared by chips + search.
|
||||
const applyMarkerVisibility = (): void => {
|
||||
const visible = new Set(
|
||||
filterPois(input.pois, { categories: state.categories, query: state.query }).map((p) => p.id),
|
||||
filterPois(input.pois, { categories: state.categories, query: state.query }).map(
|
||||
(p) => p.id,
|
||||
),
|
||||
);
|
||||
for (const [id, marker] of markers) marker.hidden = !visible.has(id);
|
||||
};
|
||||
|
|
@ -313,7 +320,9 @@ export function createMapView(input: MapViewData | null): HTMLElement {
|
|||
} else {
|
||||
const card = document.createElement("div");
|
||||
card.className = "view-placeholder";
|
||||
card.append(text("p", "The map image isn't downloaded — the facilities list below still works."));
|
||||
card.append(
|
||||
text("p", "The map image isn't downloaded — the facilities list below still works."),
|
||||
);
|
||||
section.append(card);
|
||||
}
|
||||
|
||||
|
|
@ -330,7 +339,14 @@ export function createMapView(input: MapViewData | null): HTMLElement {
|
|||
const paintChips = (): void => {
|
||||
for (const button of chipButtons) {
|
||||
const category = button.getAttribute("data-category") ?? "";
|
||||
button.setAttribute("aria-pressed", String(category === "" ? state.categories.size === 0 : state.categories.has(category as PoiCategory)));
|
||||
button.setAttribute(
|
||||
"aria-pressed",
|
||||
String(
|
||||
category === ""
|
||||
? state.categories.size === 0
|
||||
: state.categories.has(category as PoiCategory),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
const allButton = document.createElement("button");
|
||||
|
|
@ -382,14 +398,20 @@ export function createMapView(input: MapViewData | null): HTMLElement {
|
|||
facilities.className = "map-facilities";
|
||||
section.append(facilities);
|
||||
|
||||
// Narrowed binding: the `!input` guard above returned early, so `input` is
|
||||
// MapViewData from here on (captured for the closure below).
|
||||
const data = input;
|
||||
|
||||
function refreshFacilities(): void {
|
||||
facilities.replaceChildren();
|
||||
const visible = filterPois(input!.pois, { categories: state.categories, query: state.query });
|
||||
const visible = filterPois(data.pois, { categories: state.categories, query: state.query });
|
||||
if (visible.length === 0) {
|
||||
facilities.append(text("p", input!.pois.length === 0 ? "No facilities listed." : "No facilities match."));
|
||||
facilities.append(
|
||||
text("p", data.pois.length === 0 ? "No facilities listed." : "No facilities match."),
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (const group of groupFacilities(visible, input!.categories)) {
|
||||
for (const group of groupFacilities(visible, data.categories)) {
|
||||
const heading = document.createElement("h2");
|
||||
heading.textContent = `${group.category} (${String(group.pois.length)})`;
|
||||
facilities.append(heading);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class MemoryStorage implements Storage {
|
|||
this.map.clear();
|
||||
}
|
||||
getItem(key: string): string | null {
|
||||
return this.map.has(key) ? (this.map.get(key) as string) : null;
|
||||
return this.map.get(key) ?? null;
|
||||
}
|
||||
key(index: number): string | null {
|
||||
return [...this.map.keys()][index] ?? null;
|
||||
|
|
@ -25,7 +25,7 @@ class MemoryStorage implements Storage {
|
|||
this.map.delete(key);
|
||||
}
|
||||
setItem(key: string, value: string): void {
|
||||
this.map.set(key, String(value));
|
||||
this.map.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue