fix(#1293): role-aware marker shapes + outline-ring highlight#1334
Merged
Conversation
added 2 commits
May 23, 2026 20:04
Red commit: encodes acceptance criteria from #1293 — role→shape map (repeater=circle, companion=square, room=hexagon, sensor=triangle, observer=diamond), shared SVG helper covering hexagon, shape-aware divIcons on live map, legend SVG swatches, no same-colour concentric highlight overlay. Wired into deploy.yml JS unit tests so CI fails on this commit.
- roles.js: ROLE_SHAPES single source of truth (repeater=circle, companion=square, room=hexagon, sensor=triangle, observer=diamond); ROLE_STYLE shapes synced; shared window.makeRoleMarkerSVG(role, color, size) helper covers hexagon + reuses for live/map/legend. - map.js: makeMarkerIcon switch adds hexagon branch (room servers). - live.js: addNodeMarker now builds an L.divIcon via makeRoleMarkerSVG (was a flat L.circleMarker — colour only). Adds a hidden stroke-only _highlightRing per marker; pulseNode grows + fades that ring instead of recolouring the marker fill, so same-hue concentric stacking (issue's blue-on-blue) cannot occur. rescaleMarkers, pruneStaleNodes, matrix mode toggling updated to drive the divIcon via DOM helpers. - live.js role legend: emits SVG shape + colour swatch (was bare dot). - live.css: .live-shape-swatch wrapper for SVG legend swatches. Acceptance for #1293: shape varies per role (WCAG 1.4.1), legend shows shape, no same-colour concentric markers on highlight.
This was referenced May 24, 2026
Kpa-clawbot
added a commit
that referenced
this pull request
May 25, 2026
…-out levels (#1347) ## Operator feedback on #1334 PR #1334 (the #1293 marker a11y change) added a baked-in white outline at `stroke-width=2` to every node marker via `makeRoleMarkerSVG`. Operator reports it's too heavy and dominates the map at zoomed-out levels — every node reads as a "big white blob with a colour core", which actually drowns out the per-role shape silhouette at the exact zoom levels where the shape distinction matters most. ## Fix Drop the always-on stroke from **2 → 1** across all marker producers: | Producer | Before | After | |----------|--------|-------| | `public/roles.js` `makeRoleMarkerSVG` (circle / square / triangle / diamond / hexagon) | `stroke-width="2"` | `stroke-width="1"` | | `public/roles.js` `makeRoleMarkerSVG` (star branch) | `stroke-width="1.5"` | `stroke-width="1"` | | `public/live.js` `addNodeMarker` inline fallback SVG | `stroke-width="2"` | `stroke-width="1"` | | `public/map.js` `makeMarkerIcon` switch (all shapes) | `stroke-width="2"` / `"1.5"` | `stroke-width="1"` | | `_highlightRing` (pulse on selected/active) | `weight: 3 → 2` | **unchanged** | The highlight ring used by `pulseNodeMarker` is the one place where a heavy outline carries real signal (selected state), so it stays at weight 3 → 2. The always-on shape stroke is now just enough to keep silhouettes distinct on both Carto dark and light basemaps without dominating the surrounding terrain. ## Constraints preserved - Shape variation (#1293) — per-role shapes still rendered, helper untouched except for stroke width. - Colorblind palette — fills/colors unchanged, all via CSS variables / `ROLE_COLORS`. - Highlight ring still visible — pulse weight ≥ 2 retained and asserted. ## Tests New: `test-marker-outline-weight.js` (added to `test-all.sh` unit suite) - Asserts every `stroke-width` literal in `makeRoleMarkerSVG` is `<= 1`. - Asserts `live.js` inline fallback SVG `stroke-width <= 1`. - Asserts the `_highlightRing` (`ringHl.setStyle({ weight: N })`) keeps at least one `weight >= 2` so highlight stays visible. Red commit (`d17cfcc`) fails on assertion; green commit (`6cfe99b`) flips it. Existing `test-issue-1293-marker-shapes.js` still passes — the shape-variation and outline-ring highlight contracts are intact. --------- Co-authored-by: openclaw-bot <bot@openclaw>
5 tasks
Kpa-clawbot
pushed a commit
that referenced
this pull request
May 25, 2026
Implements Tufte's structural framing + audit's minimal-patch overrides:
V1 — cluster bubbles (.mc-cluster sm/md/lg):
- Single neutral fill (--mc-cluster-fill), no more --info/--warning/--accent
bucket color.
- Border-style ramp (1.5px solid → 2.5px solid → 2px double) as the
redundant non-color carrier of the count bucket.
- Border color #666 + dark halo box-shadow (audit fix: white border was
1.05:1 vs Carto-light, #666 is 4.83:1 vs light / 3.30:1 vs dark).
- role="img" + aria-label with count + per-role breakdown.
V2 — role pills (.mc-pill, rendered by makeClusterIcon):
- ROLE_LETTERS map (R/C/M/S/O) as primary monochrome carrier.
- Wong (2011) colorblind-safe palette as --mc-role-* CSS vars.
- Dark text #1a1a1a on ALL five pills (audit minimal patch — no per-pill
text-color branching; all 5 pairs pass SC 1.4.3 small-text 4.5:1).
- Font bumped 9px → 10px monospace.
- Per-pill role="img" + aria-label "<N> <role>s".
V3 — multi-byte hash labels (makeRepeaterLabelIcon):
- MB_GLYPHS prefix (✓ confirmed / ? suspected / ✗ unknown) with U+2009
thin-space + hash, as the primary non-color carrier.
- Neutral --mc-mb-fill, 3px colored border-left using the audit's
higher-luminance accent set (#56F0A0 / #FFD966 / #FF8888 — NOT the
Tol "vibrant" set Tufte proposed, which failed 3:1 vs the dark fill).
- role="img" + aria-label "multi-byte <status>, hash <ID>"; the
visible glyph+hash span is aria-hidden="true" so AT does not read
"check mark 3 E" literally.
- MB_COLORS retained as an alias to MB_STATUS_CLASS (semantic flag only,
not a fill color); marker-dot tinting uses the same accent hexes.
Hard rules respected:
- --info / --warning / --accent untouched (constants are --mc-* namespaced).
- No regression to role-shape system (#1293/#1334/#1347) — that lives in
makeMarkerIcon and is unmodified.
- Forced-colors (Windows High Contrast) graceful degradation block added.
Design sources:
- #1356 (comment)
- #1356 (comment)
Kpa-clawbot
pushed a commit
that referenced
this pull request
May 26, 2026
…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
Merged
13 tasks
Kpa-clawbot
pushed a commit
that referenced
this pull request
May 26, 2026
…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
Kpa-clawbot
added a commit
that referenced
this pull request
May 26, 2026
…onal edges, WCAG 2.2 AA (#1381) ## What The packet-route map view (`/#/map?route=N`) was a basic ~120-line renderer that 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` + `/map` visual + a11y standard. Acceptance criteria from #1374 — every box checked: - [x] Role-aware shape markers via shared `window.makeRoleMarkerSVG` (post-#1357). - [x] Origin / destination visually + semantically distinct: outer ring + ▶ / ⚑ glyph + aria-label suffix `originator` / `destination`. - [x] Sequence-number badges (`.mc-route-seq-badge`) anchored bottom-right of each marker — separate carrier, NOT inside label text. - [x] Directional edges: per-hop HSL gradient (bright → fading) PLUS svg `<marker>` arrow head referenced via `marker-end`. Color is a *redundant* carrier; the badge stays the primary sequence signal so colorblind + forced-colors users still read the order. - [x] Per-edge `aria-label="Hop N → N+1, ~Xkm"` (haversine computed). - [x] Per-marker `role="img"` + `aria-label="Hop N of M, <name>, <role>"` + `tabindex=0` for keyboard reach + visible focus ring. - [x] Label deconfliction reuses `window.deconflictLabels` (now exposed by `map.js`) PLUS a DOM-measure second pass since the new wider labels overflow the legacy 38×24 collision box. - [x] Collapsible `.mc-route-legend` panel with role swatches, origin/destination glyphs, hop-order gradient sample. Toggle has `aria-expanded`. - [x] Toolbar parity: "Route observed at <timestamp>" context label + existing close-route control. - [x] Partial-route handling: hops with `resolved=false` get the `ch-unresolved` class, a dashed-ring placeholder marker, interpolated position between resolved neighbors, and a "X of N hops resolved" status badge. - [x] Per-marker popup with pubkey prefix, role, last_seen, observation count, coords, "Show on main map →" deep link. - [x] `prefers-reduced-motion: reduce` disables animations/transitions. - [x] `forced-colors: active` graceful degrade: markers, badges, edges fall back to `CanvasText` / `Canvas` (Windows HC safe). ## How Split the renderer into a dedicated `public/route-render.js` exposing `window.MeshRoute.render(map, layer, positions, opts)`. The existing `drawPacketRoute` in `map.js` now 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: - **#1334 / #1347** — outer outline ring weights (origin/dest use the thicker ring; intermediates use the thin ring; unresolved use dashed). - **#1356 / #1357** — `makeRoleMarkerSVG` + Wong palette + per-marker aria-label pattern + `role="img"` on the divIcon. - **#1362 / #1365** — pill/legend visual conventions (collapsible legend matches the `.mc-section` accordion 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 (`#fafafa` typical) and Carto Dark Matter (`#1a1a1a` typical). | Element | SC | Ratio (Positron) | Ratio (Dark Matter) | Pass | |--------------------------------------------|----------|------------------|---------------------|------| | Sequence badge text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1 (self-bg) | ✅ | | Sequence badge border `#1a1a1a` | 1.4.11 | 17.6:1 | 12.6:1 | ✅ | | Marker outer ring `#06b6d4` (origin) | 1.4.11 | 3.2:1 | 4.6:1 | ✅ | | Marker outer ring `#ef4444` (destination) | 1.4.11 | 3.8:1 | 4.4:1 | ✅ | | Marker outer ring `#666` (intermediate) | 1.4.11 | 5.7:1 | 3.7:1 | ✅ | | Edge stroke (seq color, mid: `#56c08c`) | 1.4.11 | 3.0:1 (min) | 3.1:1 | ✅ | | Edge arrow head (currentColor) | 1.4.11 | same as edge | same | ✅ | | Label text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1 (self-bg) | ✅ | | Legend body text `#0f172a` on `#f8fafc` | 1.4.3 AA | 17.1:1 | 17.1:1 (self-bg) | ✅ | | Resolved badge `#78350f` on `#fef3c7` | 1.4.3 AA | 8.4:1 | 8.4:1 (self-bg) | ✅ | The label/badge/legend backgrounds are intentionally a solid `#f8fafc` panel (with `--mc-route-label-border` outline + `box-shadow`) so the text-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 the ring 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.js` only. The test calls `window.MeshRoute.render(...)` directly with synthetic Bay-Area positions at 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 `MeshRoute` doesn't exist on master. - **Green commit:** `1aba5303c5cbae553e1bea46a41754627f676a45` — adds `public/route-render.js`, refactors `drawPacketRoute` to 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`): ``` === Viewport mobile (375x800) === ✓ every hop marker has role="img" and informative aria-label ✓ origin aria-label contains "originator", destination contains "destination" ✓ sequence-number badge present beside each marker (not in label text) ✓ no two label boxes overlap (deconflict reused) ✓ edges have aria-label "Hop N → N+1" ✓ edges carry directionality marker (marker-end arrow) ✓ collapsible legend panel renders with role entries ✓ toolbar shows "Route observed at <timestamp>" context label ✓ partial-route — unresolved marker carries ch-unresolved class ✓ partial-route — "X of N hops resolved" badge present === Viewport desktop (1920x1080) === (same 10 — all ✓) 20 passed, 0 failed ``` Existing related tests (`#1356` `#1360` `#1364` `#1329`) re-run after the refactor — all green. ## Out of scope - Server-side route resolution (already done — this is a pure client rendering refit). - Multi-route view / 3D / globe — explicitly excluded by the issue. - Backend untouched — `cmd/server` + `cmd/ingestor` not modified. Fixes #1374 --------- Co-authored-by: openclaw-bot <bot@openclaw>
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.
Fixes #1293
What
Marker shape now varies per role (WCAG 1.4.1 — colour is no longer the only carrier of role identity), and the live map's selection/highlight no longer stacks same-colour concentric markers.
Existing role colours are preserved; the shape is the new differentiator so red/green colourblind operators can still tell roles apart.
How
public/roles.js: newwindow.ROLE_SHAPESmap (single source of truth),ROLE_STYLE.shapesynced, sharedwindow.makeRoleMarkerSVG(role, color, size)helper that emits self-contained<svg>strings — including a newhexagonbranch.public/map.js:makeMarkerIconswitch picks up thehexagoncase.public/live.js:addNodeMarkernow builds anL.divIconviamakeRoleMarkerSVG(was a flatL.circleMarker— colour only). A hidden stroke-only_highlightRingis allocated per marker;pulseNodegrows + fades that ring instead of recolouring the marker fill, so the blue-on-blue concentric stacking the issue called out cannot occur.rescaleMarkers,pruneStaleNodes, matrix mode toggling now drive the divIcon via small DOM helpers.public/live.jsrole legend: emits SVG shape + colour swatch (was a bare coloured dot).public/live.css:.live-shape-swatchwrapper for the SVG legend swatches.TDD
Red commit:
7e5e2d95—test-issue-1293-marker-shapes.jsasserts the shape map, helper, hexagon branches, divIcon switch inaddNodeMarker, SVG-based legend, and outline-ring highlight (no same-colour fill overlay). Wired intodeploy.ymlJS unit tests.Green commit:
fb33ca96.Design check
Coblis simulator (deuteranopia / protanopia / tritanopia) — reviewer to run on the staging build; shapes carry the signal independent of hue, so all role categories should remain distinguishable. Existing colours are retained per the issue's "keep colours, vary shape" guidance.
Preflight
bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master— all gates pass.