Map: readable incident rail — dark basemap, opaque cards, AA colours (#136) #281

Merged
logan merged 1 commits from fix/136-map-contrast into main 2026-10-25 21:03:23 -04:00
2 changed files with 24 additions and 14 deletions
+10 -2
View File
@@ -43,11 +43,11 @@
--line-strong: rgba(255,255,255,.17);
--ink: #E8EBF0;
--ink-2: #A5B0C2;
--ink-muted: #77839A;
--ink-muted: #8C98AE; /* was #77839A: 4.2:1 on raised, failed AA at the 10-12px it's used at (#136) */
--accent: #3987E5;
--sev-moderate: #C98500;
--sev-major: #D03B3B;
--sev-major: #F05252; /* was #D03B3B: 3.7:1 as text on surface (#136) */
--map-bg: #0E131B;
--map-block: #161C26;
@@ -191,6 +191,14 @@ html:not(.dark) .border-indigo-800 { border-color: #a5b4fc !important; }
z-index: 0;
}
/* "Dark" base layer (#136): the prod tile URL is bright OSM, so dark-theme
* overlay cards sat on a bright, busy map. Inverting the raster keeps the
* keyless OSM source (CARTO's dark_all CDN proved flaky, #117) while giving
* the overlays the dark ground their palette was chosen for. */
.leaflet-tile-pane .drb-tiles-dark {
filter: invert(1) hue-rotate(180deg) brightness(0.9) contrast(0.9) saturate(0.6);
}
/* Pinned 511 camera tiles (MapView DotCameraLayer): strip Leaflet's tooltip
* padding and let the tile's own theme colours show, so the tile is no bigger
* than the video it holds. */
+14 -12
View File
@@ -1802,6 +1802,7 @@ function MapView({
<TileLayer
url={MAP_TILE_URL}
attribution={MAP_TILE_ATTRIBUTION}
className={MAP_TILE_URL.includes("dark") ? undefined : "drb-tiles-dark"}
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer checked={startBase === "light"} name={BASE_NAMES.light}>
@@ -2083,7 +2084,7 @@ function MapView({
{/* Gate A / A2 (server-26#46) — the rail's titles, locations and
unit counts are pipeline output. Pinned above the scroll area
so it cannot be scrolled off the screen it qualifies. */}
<div className="bg-surface/90 backdrop-blur-sm border border-line rounded-lg px-2 py-1.5 shrink-0 pointer-events-auto">
<div className="bg-surface border border-line-strong rounded-lg px-2 py-1.5 shrink-0 pointer-events-auto shadow">
{filteredFrom != null && (
<p className="text-[11px] text-ink-2 mb-1"><b className="text-ink font-semibold tabular-nums">{incidents.length} of {filteredFrom}</b> · filtered</p>
)}
@@ -2104,12 +2105,13 @@ function MapView({
const latest = (callsByIncident.get(inc.incident_id) ?? [])
.filter(hasAudio)
.reduce<CallRecord | null>((best, c) => (!best || c.started_at > best.started_at ? c : best), null);
const baseClass = `w-full text-left bg-surface/90 backdrop-blur-sm border rounded-lg px-3 py-2 text-xs hover:brightness-110 transition-all ${selected ? "ring-2 ring-accent" : ""}`;
// Opaque, not glass (#136): any see-through over a busy basemap put text on map colour.
const baseClass = `w-full text-left bg-surface border shadow rounded-lg px-3 py-2 text-xs hover:brightness-110 transition-all ${selected ? "ring-2 ring-accent" : ""}`;
const cardBody = (
<>
<div className="flex items-center gap-1.5 mb-0.5">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" dangerouslySetInnerHTML={{ __html: TYPE_GLYPH_PATHS[inc.type === "accident" ? "collision" : (inc.type && TYPE_GLYPH_PATHS[inc.type] ? inc.type : "other")] }} />
<span className="uppercase tracking-wide font-semibold text-[10px]" style={{ color }}>
<span className="uppercase tracking-wide font-semibold text-[11px]" style={{ color }}>
{isKnownSeverity(inc.severity) ? SEVERITY_LABEL[inc.severity] : "Unknown"}
</span>
{live && <span className="ml-auto bg-accent text-white rounded px-1 text-[9px] font-semibold tracking-wide">ON AIR</span>}
@@ -2123,9 +2125,9 @@ function MapView({
{onSelectIncident && units.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{units.slice(0, 4).map((u) => (
<span key={u} className="font-mono text-[10px] px-1 rounded bg-raised text-ink-2">{u}</span>
<span key={u} className="font-mono text-[11px] px-1 rounded bg-raised text-ink-2">{u}</span>
))}
{units.length > 4 && <span className="font-mono text-[10px] px-1 rounded bg-raised text-ink-muted">+{units.length - 4}</span>}
{units.length > 4 && <span className="font-mono text-[11px] px-1 rounded bg-raised text-ink-2">+{units.length - 4}</span>}
</div>
)}
<div className="flex items-center justify-between mt-0.5">
@@ -2137,7 +2139,7 @@ function MapView({
)}
</div>
{!inc.location_coords && !onSelectIncident && (
<p className="text-[10px] text-accent mt-1">View details →</p>
<p className="text-[11px] text-accent mt-1">View details →</p>
)}
</>
);
@@ -2153,7 +2155,7 @@ function MapView({
onClick={() => handleIncidentSelect(inc)}
aria-pressed={selected}
className={`${baseClass} ${latest ? "pr-10" : ""}`}
style={{ borderColor: color + "55" }}
style={{ borderColor: `color-mix(in srgb, ${color} 60%, transparent)` }}
>
{cardBody}
</button>
@@ -2166,7 +2168,7 @@ function MapView({
<a
href={`/incidents/${inc.incident_id}`}
className={`block ${baseClass} ${latest ? "pr-10" : ""}`}
style={{ borderColor: color + "55" }}
style={{ borderColor: `color-mix(in srgb, ${color} 60%, transparent)` }}
>
{cardBody}
</a>
@@ -2181,13 +2183,13 @@ function MapView({
<div className="absolute bottom-0 left-0 right-0 z-[1001] md:hidden">
<button
onClick={() => setDrawerOpen((v: boolean) => !v)}
className="w-full bg-surface/95 border-t border-line px-4 py-2 text-xs text-ink-2 flex items-center justify-between"
className="w-full bg-surface border-t border-line-strong px-4 py-2 text-xs text-ink-2 flex items-center justify-between"
>
<span>Incidents ({filteredFrom != null ? `${incidents.length} of ${filteredFrom} · filtered` : incidents.length})</span>
<span>{drawerOpen ? "▼" : "▲"}</span>
</button>
{drawerOpen && (
<div className="bg-surface/95 border-t border-line max-h-52 overflow-y-auto px-3 py-2 space-y-1.5">
<div className="bg-surface border-t border-line max-h-52 overflow-y-auto px-3 py-2 space-y-1.5">
{/* Gate A / A2 (server-26#46) */}
<MachineOutputNotice variant="inline" className="text-[10px] items-start" />
{railIncidents.map((inc) => {
@@ -2210,7 +2212,7 @@ function MapView({
handleIncidentSelect(inc);
}}
className="w-full text-left border rounded px-2 py-1.5 text-xs"
style={{ borderColor: color + "55" }}
style={{ borderColor: `color-mix(in srgb, ${color} 60%, transparent)` }}
>
{label}
</button>
@@ -2221,7 +2223,7 @@ function MapView({
key={inc.incident_id}
href={`/incidents/${inc.incident_id}`}
className="block w-full text-left border rounded px-2 py-1.5 text-xs"
style={{ borderColor: color + "55" }}
style={{ borderColor: `color-mix(in srgb, ${color} 60%, transparent)` }}
>
{label}
</a>