fix(#1374): packet-route map modernized — role-aware markers, directional edges, WCAG 2.2 AA#1381
Merged
Conversation
added 3 commits
May 26, 2026 05:42
…ges, partial-route Asserts the upcoming MeshRoute renderer: - per-marker role="img" + aria-label "Hop N of M, <name>, <role>" - origin (originator) + destination (destination) semantically distinct - sequence-number badges separate from label text - directional <marker-end> arrows + per-edge aria-label "Hop N \u2192 N+1" - deconflictLabels reused — label boxes never overlap - collapsible legend panel with role + origin/destination entries - toolbar parity: "Route observed at <ts>" context - partial-route: ch-unresolved class + "X of N hops resolved" badge Drives the renderer via window.MeshRoute.render(map, layer, positions, opts) with synthetic Bay-Area positions so no DB is required. Will fail until the GREEN commit lands MeshRoute.
…irectional edges, WCAG 2.2 AA) Splits the legacy ~120-line drawPacketRoute renderer into a dedicated public/route-render.js module exposing window.MeshRoute.render(map, layer, positions, opts). drawPacketRoute keeps responsibility for resolving short hashes against the loaded node list and then delegates the visual layer. Acceptance criteria (issue #1374): - Role-aware shape markers via shared window.makeRoleMarkerSVG (post-#1357). - Origin / destination distinct: larger size + outer ring + ▶ / ⚑ glyph + dedicated aria-label suffix ("originator" / "destination"). - Sequence-number badges (.mc-route-seq-badge) anchored bottom-right of each marker, NOT crammed into label text. Origin = ▶, destination = ⚑. - Directional edges: per-hop HSL sequence-color gradient (bright → fading) PLUS svg <marker> arrow head referenced via marker-end. Color is a redundant carrier (badge stays the primary order signal — colorblind + forced-colors safe). - Per-edge aria-label "Hop N → N+1, ~Xkm" computed via haversine. - Per-marker role="img" + aria-label "Hop N of M, <name>, <role>" + tabindex for keyboard reach + visible focus ring. - Label deconfliction reuses window.deconflictLabels (now exposed by map.js) PLUS a second DOM-measure pass since labels are wider than the legacy 38×24 collision box. - Collapsible .mc-route-legend with role swatches, origin/destination glyphs, and the per-hop gradient sample. Toggle button has aria-expanded. - "Route observed at <timestamp>" toolbar context label. - Partial-route handling: hops with resolved=false get the ch-unresolved class, dashed-ring placeholder marker, interpolated position between resolved neighbors, and a "X of N hops resolved" status badge. - prefers-reduced-motion: animation/transition disabled. - forced-colors: active: marker strokes, badges, edges fall back to CanvasText / Canvas (graceful Windows HC degrade). Files: - public/route-render.js (new) — MeshRoute renderer - public/map.js — drawPacketRoute delegates to MeshRoute; exposes map + routeLayer + deconflictLabels on window for the renderer + tests; close button now also strips legend/context overlays. - public/style.css — .mc-route-* classes + reduced-motion + forced-colors. - public/index.html — load route-render.js after map.js. - .github/workflows/deploy.yml — wire test-issue-1374-route-map-a11y-e2e.js into the E2E step. Verification: - New E2E test (mobile 375x800 + desktop 1920x1080): 20/20 passing. - Existing #1356 / #1360 / #1364 / #1329 tests: unchanged + green. - Backend untouched (route resolution is server-side). Refs: visual heritage from #1334 / #1347 (outline rings) and #1356 / #1357 (aria-label + Wong palette + shape markers). Fixes #1374
The label-overlap second pass ran once via setTimeout(30) right after markers were added, but Leaflet's fitBounds() pan completes asynchronously and re-projects the divIcons — which on wide viewports (desktop 1920x1080) could reintroduce overlap that the first nudge already resolved. Fix: also re-run the nudge on map 'moveend'. Reset any prior margin-top nudges before each pass so iterations don't stack and drift labels off-screen. Bumps test-side wait to 1.8s to cover the chained fitBounds → moveend → nudge sequence. Verified locally: 20/20 (mobile + desktop).
7c0093b to
25939db
Compare
This was referenced May 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The packet-route map view (
/#/map?route=N) was a basic ~120-line rendererthat pre-dated every recent a11y / UX investment (yellow circle markers,
overlapping numeric labels, no directional edges, no aria, no legend). This
PR rebuilds it on top of the modern shared helpers so it matches the
/live+/mapvisual + a11y standard.Acceptance criteria from #1374 — every box checked:
window.makeRoleMarkerSVG(post-fix(#1356): WCAG 2.2 AA map a11y — cluster bubbles, role pills, multi-byte labels #1357).glyph + aria-label suffix
originator/destination..mc-route-seq-badge) anchored bottom-right ofeach marker — separate carrier, NOT inside label text.
<marker>arrow head referenced viamarker-end. Color is aredundant carrier; the badge stays the primary sequence signal so
colorblind + forced-colors users still read the order.
aria-label="Hop N → N+1, ~Xkm"(haversine computed).role="img"+aria-label="Hop N of M, <name>, <role>"+
tabindex=0for keyboard reach + visible focus ring.window.deconflictLabels(now exposed bymap.js) PLUS a DOM-measure second pass since the new wider labelsoverflow the legacy 38×24 collision box.
.mc-route-legendpanel with role swatches,origin/destination glyphs, hop-order gradient sample. Toggle has
aria-expanded.existing close-route control.
resolved=falseget thech-unresolvedclass, a dashed-ring placeholder marker, interpolatedposition between resolved neighbors, and a "X of N hops resolved"
status badge.
coords, "Show on main map →" deep link.
prefers-reduced-motion: reducedisables animations/transitions.forced-colors: activegraceful degrade: markers, badges, edges fallback to
CanvasText/Canvas(Windows HC safe).How
Split the renderer into a dedicated
public/route-render.jsexposingwindow.MeshRoute.render(map, layer, positions, opts). The existingdrawPacketRouteinmap.jsnow owns only short-hash → node resolution(and origin enrichment) and then delegates the entire visual layer. This
makes the renderer testable in isolation with synthetic positions — no DB
required — and avoids dragging the legacy ~100 LOC of marker / circleMarker
/ polyline scaffolding into the new design.
Visual heritage:
thicker ring; intermediates use the thin ring; unresolved use dashed).
makeRoleMarkerSVG+ Wong palette + per-markeraria-label pattern +
role="img"on the divIcon.matches the
.mc-sectionaccordion language users already know from/map).WCAG 2.2 AA — measured contrast (graphics SC 1.4.11, text SC 1.4.3)
All ratios sampled with WebAIM contrast formula on the rendered elements
against both Carto Positron (
#fafafatypical) and Carto Dark Matter(
#1a1a1atypical).#0f172aon#f8fafc#1a1a1a#06b6d4(origin)#ef4444(destination)#666(intermediate)#56c08c)#0f172aon#f8fafc#0f172aon#f8fafc#78350fon#fef3c7The label/badge/legend backgrounds are intentionally a solid
#f8fafcpanel (with
--mc-route-label-borderoutline +box-shadow) so thetext-color → tile-color path never applies — the readable text always sits
on its own opaque panel.
For SC 1.3.1 (info-and-relationships): every visual carrier has a redundant
text or ARIA carrier — sequence position appears in the badge text AND in
each marker's
aria-label; origin/destination appear in the glyph AND thering color AND the aria-label suffix; edge direction appears in the arrow
head AND the per-edge aria-label.
TDD
Red commit:
9e4f58e5547720ff3fcf8695a6c325958904683a(CI:https://github.com/Kpa-clawbot/CoreScope/commits/9e4f58e5547720ff3fcf8695a6c325958904683a/checks)
— adds
test-issue-1374-route-map-a11y-e2e.jsonly. The test callswindow.MeshRoute.render(...)directly with synthetic Bay-Area positionsat mobile (375×800) AND desktop (1920×1080), asserts every acceptance
criterion as a DOM grep on the rendered SVG / divIcon HTML, and includes
the partial-route fixture. Fails on the assertions because
MeshRoutedoesn't exist on master.
Green commit:
1aba5303c5cbae553e1bea46a41754627f676a45— addspublic/route-render.js, refactorsdrawPacketRouteto delegate, adds.mc-route-*CSS (including reduced-motion + forced-colors media queries),wires the script tag in
index.html, and wires the test into.github/workflows/deploy.yml.Visual verification
20/20 assertions pass locally (
CHROMIUM_PATH=/usr/bin/chromium BASE_URL=http://localhost:13581 node test-issue-1374-route-map-a11y-e2e.js):Existing related tests (
#1356#1360#1364#1329) re-run after therefactor — all green.
Out of scope
rendering refit).
cmd/server+cmd/ingestornot modified.Fixes #1374