mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-03 23:21:19 +00:00
feat(1488): customize marker stroke (color, width, opacity) (#1494)
## Summary Reporter (@EldoonNemar in #1488) found the new white marker stroke overwhelming with hundreds of nodes on screen. This PR exposes the stroke through CSS vars + a customizer panel so operators can dial color/width/opacity (or remove it) without code edits. **Scope:** ship stroke customization only. The reporter also asked for the old glow-style highlight ring as an alternative — that's a separate visual feature that needs design discussion, so it's deferred to a follow-up issue. ## Changes - **`public/style.css`** `:root` declares `--mc-marker-stroke-color` / `--mc-marker-stroke-width` / `--mc-marker-stroke-opacity` with sensible defaults (white, 1, 1) that match current behavior. - **`public/roles.js`** `makeRoleMarkerSVG` — replaced the 6 baked `stroke="#fff" stroke-width="1"` literals with a single shared `strokeAttr` referencing the CSS vars. One source of truth for all role shapes. - **`public/map.js`** `makeMarkerIcon` — same migration. The observer star overlay keeps its narrow 0.8 width but routes color + opacity through the same vars. - **`public/live.js`** `addNodeMarker` fallback SVG — same migration. - **`public/customize-v2.js`** — new `markerStroke` object section (color/width/opacity) with validation, `applyCSS` writes, three controls on the Colors tab → "Marker Stroke" panel (color picker + width slider 0–4 + opacity slider 0–100%). Optimistic CSS-var writes on the `input` event so markers repaint live as the operator drags. - **`cmd/server/{config,types,routes}.go`** — `ThemeFile` / `Config` / `ThemeResponse` pick up `MarkerStroke` so `theme.json` and `config.json` can ship server-side defaults. Defaults mirror the `:root` CSS values so no breaking change for current operators. - **`config.example.json`** — documented `markerStroke` section with usage hint. ## TDD - **Red commit** `92183f95` — `test-issue-1488-marker-stroke-vars.js` (5 sections, 18 assertions); failed 14/18 before implementation. - **Green commit** `ce39637e` — implementation; same test now passes 18/18. - Existing `#1438` (marker CSS-var migration) and `#1293` (marker shapes) regression tests still pass. - Go tests (`cmd/server/...`) all green. ## CDP validation Synthetic page with 600 markers, three blocks proving CSS-var control works end-to-end: | Block | Stroke setting | Computed `getComputedStyle().stroke` / width / opacity | | --- | --- | --- | | Default | `var(--mc-marker-stroke-color)` (no override) | `rgba(255,255,255,0.85)` / `1px` / `1` | | Tuned | inline `--mc-marker-stroke-*` (operator override) | `rgb(255,255,255)` / `0.5px` / `0.3` | | Cyan | inline `--mc-marker-stroke-*` (branding/CB) | `rgb(0,229,255)` / `2px` / `1` | Same SVG source, three different rendered strokes — that's the whole point. Runtime `documentElement.style.setProperty(...)` (which is exactly what the customizer slider's `input` handler does) repaints mounted markers without reload. CDP screenshot attached to the implementation note. ## Hot-deploy Frontend + Go binary changes. Safe to hot-deploy frontend files (`public/*.js`, `public/style.css`) via the standard staging path; Go binary update needs a container restart. ## Defer Glow highlight ring (the second half of #1488) — separate follow-up issue. This PR delivers the immediately-useful, smaller deliverable. Partial fix for #1488 (stroke customization shipped; glow ring deferred to a follow-up issue). --------- Co-authored-by: meshcore-bot <bot@meshcore.local>
This commit is contained in:
@@ -48,6 +48,12 @@ type Config struct {
|
||||
TypeColors map[string]interface{} `json:"typeColors"`
|
||||
Home map[string]interface{} `json:"home"`
|
||||
|
||||
// #1488 — marker stroke (outline) settings. Operators dial color, width
|
||||
// and opacity to soften the default white outline when hundreds of
|
||||
// nodes feel overwhelming. Frontend reads these as CSS vars; see
|
||||
// public/customize-v2.js applyCSS markerStroke block.
|
||||
MarkerStroke map[string]interface{} `json:"markerStroke,omitempty"`
|
||||
|
||||
MapDefaults struct {
|
||||
Center []float64 `json:"center"`
|
||||
Zoom int `json:"zoom"`
|
||||
@@ -329,6 +335,8 @@ type ThemeFile struct {
|
||||
NodeColors map[string]interface{} `json:"nodeColors"`
|
||||
TypeColors map[string]interface{} `json:"typeColors"`
|
||||
Home map[string]interface{} `json:"home"`
|
||||
// #1488 — marker stroke overlay from theme.json.
|
||||
MarkerStroke map[string]interface{} `json:"markerStroke,omitempty"`
|
||||
}
|
||||
|
||||
func LoadConfig(baseDirs ...string) (*Config, error) {
|
||||
|
||||
+16
-6
@@ -523,13 +523,23 @@ func (s *Server) handleConfigTheme(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
home := mergeMap(defaultHome, s.cfg.Home, theme.Home)
|
||||
|
||||
// #1488 — marker stroke overlay. Defaults mirror the :root values in
|
||||
// public/style.css so a fresh visitor with no config + no override
|
||||
// still gets the same painted outline as the static CSS fallback.
|
||||
markerStroke := mergeMap(map[string]interface{}{
|
||||
"color": "rgba(255,255,255,0.85)",
|
||||
"width": 1,
|
||||
"opacity": 1,
|
||||
}, s.cfg.MarkerStroke, theme.MarkerStroke)
|
||||
|
||||
writeJSON(w, ThemeResponse{
|
||||
Branding: branding,
|
||||
Theme: themeColors,
|
||||
ThemeDark: themeDark,
|
||||
NodeColors: nodeColors,
|
||||
TypeColors: typeColors,
|
||||
Home: home,
|
||||
Branding: branding,
|
||||
Theme: themeColors,
|
||||
ThemeDark: themeDark,
|
||||
NodeColors: nodeColors,
|
||||
TypeColors: typeColors,
|
||||
Home: home,
|
||||
MarkerStroke: markerStroke,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -979,6 +979,9 @@ type ThemeResponse struct {
|
||||
NodeColors map[string]interface{} `json:"nodeColors"`
|
||||
TypeColors map[string]interface{} `json:"typeColors"`
|
||||
Home interface{} `json:"home"`
|
||||
// #1488 — marker stroke overlay so the frontend can apply server-side
|
||||
// defaults before the operator's localStorage override loads.
|
||||
MarkerStroke map[string]interface{} `json:"markerStroke,omitempty"`
|
||||
}
|
||||
|
||||
type MapConfigResponse struct {
|
||||
|
||||
@@ -47,6 +47,12 @@
|
||||
"observer": "#8b5cf6",
|
||||
"_comment": "Marker/badge colors per node role. Used on map, nodes list, and live feed."
|
||||
},
|
||||
"markerStroke": {
|
||||
"color": "rgba(255,255,255,0.85)",
|
||||
"width": 1,
|
||||
"opacity": 1,
|
||||
"_comment": "#1488 — outline around each map marker (live + map pages). 'color' accepts any CSS color (hex, rgb, rgba); use rgba() or drop 'opacity' below ~0.5 to soften the outline when hundreds of nodes feel overwhelming. 'width' is the SVG stroke width (0 hides the outline entirely). Operators can override per-browser via the in-app Theme Customizer (Colors tab → Marker Stroke)."
|
||||
},
|
||||
"mapDarkTileProvider": "carto-dark",
|
||||
"_comment_mapDarkTileProvider": "Default dark-mode basemap provider. Allowed: 'carto-dark' (Carto dark_all — default), 'esri-darkgray-labels' (Esri Dark Gray Canvas + reference labels), 'voyager-inverted' (Carto Voyager with CSS invert filter), 'positron-inverted' (Carto Positron with CSS invert filter). Light mode is unaffected. Users can override per-browser via the in-app customizer (persisted to localStorage). #1420.",
|
||||
"home": {
|
||||
|
||||
+108
-2
@@ -33,8 +33,8 @@
|
||||
'meshcore-live-heatmap-opacity'
|
||||
];
|
||||
|
||||
var VALID_SECTIONS = ['branding', 'theme', 'themeDark', 'nodeColors', 'typeColors', 'home', 'timestamps', 'heatmapOpacity', 'liveHeatmapOpacity', 'distanceUnit', 'favorites', 'myNodes'];
|
||||
var OBJECT_SECTIONS = ['branding', 'theme', 'themeDark', 'nodeColors', 'typeColors', 'home', 'timestamps'];
|
||||
var VALID_SECTIONS = ['branding', 'theme', 'themeDark', 'nodeColors', 'typeColors', 'home', 'timestamps', 'heatmapOpacity', 'liveHeatmapOpacity', 'distanceUnit', 'favorites', 'myNodes', 'markerStroke'];
|
||||
var OBJECT_SECTIONS = ['branding', 'theme', 'themeDark', 'nodeColors', 'typeColors', 'home', 'timestamps', 'markerStroke'];
|
||||
var SCALAR_SECTIONS = ['heatmapOpacity', 'liveHeatmapOpacity'];
|
||||
var DISTANCE_UNIT_VALUES = ['km', 'mi', 'auto'];
|
||||
|
||||
@@ -409,6 +409,32 @@
|
||||
} else {
|
||||
console.warn('[customizer-v2] Invalid opacity value rejected:', key, delta[key]);
|
||||
}
|
||||
} else if (key === 'markerStroke' && typeof delta[key] === 'object' && delta[key] !== null) {
|
||||
// #1488 — markerStroke { color, width, opacity }
|
||||
var msIn = delta[key];
|
||||
var msOut = {};
|
||||
if (typeof msIn.color === 'string' && isValidColor(msIn.color)) {
|
||||
msOut.color = msIn.color;
|
||||
} else if (msIn.color != null) {
|
||||
console.warn('[customizer-v2] Invalid markerStroke.color rejected:', msIn.color);
|
||||
}
|
||||
var msW = typeof msIn.width === 'string' ? parseFloat(msIn.width) : msIn.width;
|
||||
if (msIn.width != null) {
|
||||
if (isFinite(msW) && msW >= 0 && msW <= 10) {
|
||||
msOut.width = msW;
|
||||
} else {
|
||||
console.warn('[customizer-v2] Invalid markerStroke.width rejected:', msIn.width);
|
||||
}
|
||||
}
|
||||
var msO = typeof msIn.opacity === 'string' ? parseFloat(msIn.opacity) : msIn.opacity;
|
||||
if (msIn.opacity != null) {
|
||||
if (isValidOpacity(msO)) {
|
||||
msOut.opacity = msO;
|
||||
} else {
|
||||
console.warn('[customizer-v2] Invalid markerStroke.opacity rejected:', msIn.opacity);
|
||||
}
|
||||
}
|
||||
if (Object.keys(msOut).length) clean[key] = msOut;
|
||||
} else if (key === 'timestamps' && typeof delta[key] === 'object' && delta[key] !== null) {
|
||||
var ts = {};
|
||||
var tsrc = delta[key];
|
||||
@@ -637,6 +663,22 @@
|
||||
localStorage.setItem('meshcore-live-heatmap-opacity', effectiveConfig.liveHeatmapOpacity);
|
||||
}
|
||||
|
||||
// #1488 — marker stroke: drive CSS vars from effective config. SVG
|
||||
// markers across map.js / live.js / roles.js all read these vars, so
|
||||
// a single write here repaints every mounted marker without a reload.
|
||||
var ms = effectiveConfig.markerStroke;
|
||||
if (ms && typeof ms === 'object') {
|
||||
if (typeof ms.color === 'string' && ms.color) {
|
||||
root.setProperty('--mc-marker-stroke-color', ms.color);
|
||||
}
|
||||
if (ms.width != null && isFinite(ms.width)) {
|
||||
root.setProperty('--mc-marker-stroke-width', String(ms.width));
|
||||
}
|
||||
if (ms.opacity != null && isFinite(ms.opacity)) {
|
||||
root.setProperty('--mc-marker-stroke-opacity', String(ms.opacity));
|
||||
}
|
||||
}
|
||||
|
||||
// Distance unit → sync to localStorage for all pages
|
||||
if (typeof effectiveConfig.distanceUnit === 'string' && DISTANCE_UNIT_VALUES.indexOf(effectiveConfig.distanceUnit) >= 0) {
|
||||
localStorage.setItem('meshcore-distance-unit', effectiveConfig.distanceUnit);
|
||||
@@ -1262,6 +1304,15 @@
|
||||
var liveHeatOpacity = typeof eff.liveHeatmapOpacity === 'number' ? eff.liveHeatmapOpacity : 0.3;
|
||||
var liveHeatPct = Math.round(liveHeatOpacity * 100);
|
||||
|
||||
// #1488 — marker stroke controls. Defaults match the :root values in
|
||||
// style.css; the UI shows the effective merged value (server config →
|
||||
// local override) so the operator sees what's actually painted.
|
||||
var ms = eff.markerStroke || {};
|
||||
var msColor = typeof ms.color === 'string' && ms.color ? ms.color : '#ffffff';
|
||||
var msWidth = ms.width != null && isFinite(ms.width) ? Number(ms.width) : 1;
|
||||
var msOpacity = ms.opacity != null && isFinite(ms.opacity) ? Number(ms.opacity) : 1;
|
||||
var msOpacityPct = Math.round(msOpacity * 100);
|
||||
|
||||
return '<div class="cust-panel' + (_activeTab === 'nodes' ? ' active' : '') + '" data-panel="nodes">' +
|
||||
'<p class="cust-section-title">Node Role Colors</p>' +
|
||||
'<p class="cust-hint" style="margin-bottom:8px">These are the canonical role colors used across the app. They inherit from your server config (or built-in defaults), and can be optionally remapped by a colorblind-safe preset below.</p>' +
|
||||
@@ -1279,6 +1330,22 @@
|
||||
'<div class="cust-hint">Heatmap overlay on the Live page (0–100%)</div></div>' +
|
||||
'<input type="range" data-cv2-slider="liveHeatmapOpacity" min="0" max="100" value="' + liveHeatPct + '" style="width:120px;cursor:pointer">' +
|
||||
'<span class="cust-hex" id="cv2LiveHeatPct">' + liveHeatPct + '%</span></div>' +
|
||||
'<hr style="border:none;border-top:1px solid var(--border);margin:16px 0">' +
|
||||
'<p class="cust-section-title">Marker Stroke <span style="font-weight:normal;color:var(--text-muted);font-size:11px">(#1488)</span></p>' +
|
||||
'<p class="cust-hint" style="margin-bottom:8px">Outline around each map marker. Dial these down when hundreds of nodes make the default white border overwhelming. Changes are live on every mounted marker.</p>' +
|
||||
'<div class="cust-color-row"><div><label>Color' + _overrideDot('markerStroke', 'color') + '</label>' +
|
||||
'<div class="cust-hint">Marker outline color. Defaults to white for contrast on dark + light tiles.</div></div>' +
|
||||
'<input type="color" data-cv2-field="markerStroke.color" value="' + esc(msColor) + '">' +
|
||||
'<span class="cust-node-dot" style="background:' + esc(msColor) + '"></span>' +
|
||||
'<span class="cust-hex">' + esc(msColor) + '</span></div>' +
|
||||
'<div class="cust-color-row"><div><label>Width' + _overrideDot('markerStroke', 'width') + '</label>' +
|
||||
'<div class="cust-hint">Stroke thickness in SVG units (0–4). Set to 0 to remove the outline entirely.</div></div>' +
|
||||
'<input type="range" data-cv2-marker-stroke="width" min="0" max="4" step="0.1" value="' + msWidth + '" style="width:120px;cursor:pointer">' +
|
||||
'<span class="cust-hex" id="cv2MarkerStrokeW">' + msWidth + '</span></div>' +
|
||||
'<div class="cust-color-row"><div><label>Opacity' + _overrideDot('markerStroke', 'opacity') + '</label>' +
|
||||
'<div class="cust-hint">Stroke alpha (0–100%). Drop this to ~30% for a softer outline.</div></div>' +
|
||||
'<input type="range" data-cv2-marker-stroke="opacity" min="0" max="100" value="' + msOpacityPct + '" style="width:120px;cursor:pointer">' +
|
||||
'<span class="cust-hex" id="cv2MarkerStrokeO">' + msOpacityPct + '%</span></div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -1995,6 +2062,11 @@
|
||||
// Mirror to logo brand vars so the wordmark recolors live too.
|
||||
if (key === 'accent') document.documentElement.style.setProperty('--logo-accent', inp.value);
|
||||
if (key === 'accentHover') document.documentElement.style.setProperty('--logo-accent-hi', inp.value);
|
||||
// #1488 — marker stroke color also gets a live CSS-var write
|
||||
// so the operator sees outlines repaint on drag.
|
||||
if (section === 'markerStroke' && key === 'color') {
|
||||
document.documentElement.style.setProperty('--mc-marker-stroke-color', inp.value);
|
||||
}
|
||||
// Update hex display
|
||||
var hex = inp.parentElement.querySelector('.cust-hex');
|
||||
if (hex) hex.textContent = inp.value;
|
||||
@@ -2074,6 +2146,40 @@
|
||||
});
|
||||
});
|
||||
|
||||
// #1488 — Marker stroke width/opacity sliders. Color picker uses the
|
||||
// generic data-cv2-field handler (markerStroke.color) above.
|
||||
container.querySelectorAll('[data-cv2-marker-stroke]').forEach(function (inp) {
|
||||
var which = inp.dataset.cv2MarkerStroke; // 'width' | 'opacity'
|
||||
inp.addEventListener('input', function () {
|
||||
var raw = parseFloat(inp.value);
|
||||
if (!isFinite(raw)) return;
|
||||
if (which === 'opacity') {
|
||||
var lbl = document.getElementById('cv2MarkerStrokeO');
|
||||
if (lbl) lbl.textContent = Math.round(raw) + '%';
|
||||
// Optimistic CSS write so the markers repaint on drag.
|
||||
document.documentElement.style.setProperty('--mc-marker-stroke-opacity', String(raw / 100));
|
||||
} else {
|
||||
var lblW = document.getElementById('cv2MarkerStrokeW');
|
||||
if (lblW) lblW.textContent = String(raw);
|
||||
document.documentElement.style.setProperty('--mc-marker-stroke-width', String(raw));
|
||||
}
|
||||
});
|
||||
inp.addEventListener('change', function () {
|
||||
var raw = parseFloat(inp.value);
|
||||
if (!isFinite(raw)) return;
|
||||
var eff = _getEffective();
|
||||
var current = JSON.parse(JSON.stringify(eff.markerStroke || {}));
|
||||
if (which === 'opacity') current.opacity = raw / 100;
|
||||
else current.width = raw;
|
||||
// Persist the whole object so partial picks survive (color +
|
||||
// width + opacity coexist in a single section).
|
||||
var delta = JSON.parse(JSON.stringify(readOverrides()));
|
||||
delta.markerStroke = Object.assign({}, delta.markerStroke || {}, current);
|
||||
writeOverrides(delta);
|
||||
_runPipeline();
|
||||
});
|
||||
});
|
||||
|
||||
// Home page list editing
|
||||
container.querySelectorAll('[data-cv2-home]').forEach(function (inp) {
|
||||
inp.addEventListener('input', function () {
|
||||
|
||||
+1
-1
@@ -2422,7 +2422,7 @@
|
||||
? window.makeRoleMarkerSVG(n.role, null, sizePx)
|
||||
: '<svg width="' + sizePx + '" height="' + sizePx + '" viewBox="0 0 ' + sizePx + ' ' + sizePx +
|
||||
'"><circle cx="' + (sizePx/2) + '" cy="' + (sizePx/2) + '" r="' + (sizePx/2 - 2) +
|
||||
'" fill="' + fillExpr + '" stroke="#fff" stroke-width="1"/></svg>');
|
||||
'" fill="' + fillExpr + '" stroke="var(--mc-marker-stroke-color)" stroke-width="var(--mc-marker-stroke-width)" stroke-opacity="var(--mc-marker-stroke-opacity)"/></svg>');
|
||||
|
||||
const icon = L.divIcon({
|
||||
html: svgHtml,
|
||||
|
||||
+10
-7
@@ -62,18 +62,21 @@
|
||||
// operator picks a per-role override. colorOverride (MultiByte
|
||||
// status tint) wins when provided.
|
||||
const fillColor = colorOverride || ('var(--mc-role-' + (role || 'companion') + ')');
|
||||
// #1488 — marker stroke through CSS vars so operators can dial
|
||||
// colour / width / opacity via the customizer (Colors → Marker Stroke).
|
||||
const strokeAttr = ' stroke="var(--mc-marker-stroke-color)" stroke-width="var(--mc-marker-stroke-width)" stroke-opacity="var(--mc-marker-stroke-opacity)"';
|
||||
const size = s.radius * 2 + 4;
|
||||
const c = size / 2;
|
||||
let path;
|
||||
switch (s.shape) {
|
||||
case 'diamond':
|
||||
path = `<polygon points="${c},2 ${size-2},${c} ${c},${size-2} 2,${c}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
path = `<polygon points="${c},2 ${size-2},${c} ${c},${size-2} 2,${c}" fill="${fillColor}"${strokeAttr}/>`;
|
||||
break;
|
||||
case 'square':
|
||||
path = `<rect x="3" y="3" width="${size-6}" height="${size-6}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
path = `<rect x="3" y="3" width="${size-6}" height="${size-6}" fill="${fillColor}"${strokeAttr}/>`;
|
||||
break;
|
||||
case 'triangle':
|
||||
path = `<polygon points="${c},2 ${size-2},${size-2} 2,${size-2}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
path = `<polygon points="${c},2 ${size-2},${size-2} 2,${size-2}" fill="${fillColor}"${strokeAttr}/>`;
|
||||
break;
|
||||
case 'hexagon': {
|
||||
// #1293 — pointy-top hexagon for room servers
|
||||
@@ -84,7 +87,7 @@
|
||||
hpts += (c + hr * Math.cos(ha)).toFixed(2) + ',' +
|
||||
(c + hr * Math.sin(ha)).toFixed(2) + ' ';
|
||||
}
|
||||
path = `<polygon points="${hpts.trim()}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
path = `<polygon points="${hpts.trim()}" fill="${fillColor}"${strokeAttr}/>`;
|
||||
break;
|
||||
}
|
||||
case 'star': {
|
||||
@@ -97,11 +100,11 @@
|
||||
pts += `${cx + outer * Math.cos(aOuter)},${cy + outer * Math.sin(aOuter)} `;
|
||||
pts += `${cx + inner * Math.cos(aInner)},${cy + inner * Math.sin(aInner)} `;
|
||||
}
|
||||
path = `<polygon points="${pts.trim()}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
path = `<polygon points="${pts.trim()}" fill="${fillColor}"${strokeAttr}/>`;
|
||||
break;
|
||||
}
|
||||
default: // circle
|
||||
path = `<circle cx="${c}" cy="${c}" r="${c-2}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
path = `<circle cx="${c}" cy="${c}" r="${c-2}" fill="${fillColor}"${strokeAttr}/>`;
|
||||
}
|
||||
// If this node is also an observer, add a small star overlay
|
||||
let obsOverlay = '';
|
||||
@@ -116,7 +119,7 @@
|
||||
starPts += `${scx + so * Math.cos(aO)},${scy + so * Math.sin(aO)} `;
|
||||
starPts += `${scx + si * Math.cos(aI)},${scy + si * Math.sin(aI)} `;
|
||||
}
|
||||
obsOverlay = `<g transform="translate(${sx},${sy})"><polygon points="${starPts.trim()}" fill="var(--mc-role-observer)" stroke="#fff" stroke-width="0.8"/></g>`;
|
||||
obsOverlay = `<g transform="translate(${sx},${sy})"><polygon points="${starPts.trim()}" fill="var(--mc-role-observer)" stroke="var(--mc-marker-stroke-color)" stroke-width="0.8" stroke-opacity="var(--mc-marker-stroke-opacity)"/></g>`;
|
||||
}
|
||||
const svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">${path}${obsOverlay}</svg>`;
|
||||
return L.divIcon({
|
||||
|
||||
+9
-6
@@ -278,20 +278,23 @@
|
||||
// that need a fixed tint (matrix mode, stale dim) keep passing
|
||||
// their explicit colour.
|
||||
var fill = color || ('var(--mc-role-' + (role || 'companion') + ')');
|
||||
// #1488 — stroke routed through CSS vars so operators can dial
|
||||
// colour/width without code edits (customizer Colors → Marker Stroke).
|
||||
var strokeAttr = ' stroke="var(--mc-marker-stroke-color)" stroke-width="var(--mc-marker-stroke-width)" stroke-opacity="var(--mc-marker-stroke-opacity)"';
|
||||
var path;
|
||||
switch (shape) {
|
||||
case 'square':
|
||||
path = '<rect x="3" y="3" width="' + (size - 6) + '" height="' + (size - 6) +
|
||||
'" fill="' + fill + '" stroke="#fff" stroke-width="1"/>';
|
||||
'" fill="' + fill + '"' + strokeAttr + '/>';
|
||||
break;
|
||||
case 'triangle':
|
||||
path = '<polygon points="' + c + ',2 ' + (size - 2) + ',' + (size - 2) +
|
||||
' 2,' + (size - 2) + '" fill="' + fill + '" stroke="#fff" stroke-width="1"/>';
|
||||
' 2,' + (size - 2) + '" fill="' + fill + '"' + strokeAttr + '/>';
|
||||
break;
|
||||
case 'diamond':
|
||||
path = '<polygon points="' + c + ',2 ' + (size - 2) + ',' + c + ' ' +
|
||||
c + ',' + (size - 2) + ' 2,' + c +
|
||||
'" fill="' + fill + '" stroke="#fff" stroke-width="1"/>';
|
||||
'" fill="' + fill + '"' + strokeAttr + '/>';
|
||||
break;
|
||||
case 'hexagon': {
|
||||
// Pointy-top hexagon centred at (c,c), inscribed radius ≈ c-1.5
|
||||
@@ -303,7 +306,7 @@
|
||||
(c + r * Math.sin(a)).toFixed(2) + ' ';
|
||||
}
|
||||
path = '<polygon points="' + pts.trim() + '" fill="' + fill +
|
||||
'" stroke="#fff" stroke-width="1"/>';
|
||||
'"' + strokeAttr + '/>';
|
||||
break;
|
||||
}
|
||||
case 'star': {
|
||||
@@ -316,12 +319,12 @@
|
||||
spts += (cx + inner * Math.cos(aI)) + ',' + (cy + inner * Math.sin(aI)) + ' ';
|
||||
}
|
||||
path = '<polygon points="' + spts.trim() + '" fill="' + fill +
|
||||
'" stroke="#fff" stroke-width="1"/>';
|
||||
'"' + strokeAttr + '/>';
|
||||
break;
|
||||
}
|
||||
default: // circle
|
||||
path = '<circle cx="' + c + '" cy="' + c + '" r="' + (c - 2) +
|
||||
'" fill="' + fill + '" stroke="#fff" stroke-width="1"/>';
|
||||
'" fill="' + fill + '"' + strokeAttr + '/>';
|
||||
}
|
||||
return '<svg width="' + size + '" height="' + size +
|
||||
'" viewBox="0 0 ' + size + ' ' + size +
|
||||
|
||||
@@ -3733,6 +3733,16 @@ th.sort-active { color: var(--accent, #60a5fa); }
|
||||
--mc-role-sensor: #F0E442; /* yellow */
|
||||
--mc-role-observer: #CC79A7; /* reddish-purple */
|
||||
|
||||
/* #1488 — marker stroke (outline around the role shape on map + live).
|
||||
* Reporter (EldoonNemar) found the new white stroke overwhelming with
|
||||
* hundreds of nodes. Defaults preserve visibility on both dark + light
|
||||
* tiles; operators dial these via the customizer (Colors → Marker
|
||||
* Stroke) or via config.json (markerStroke.*). All SVG marker builders
|
||||
* read these vars so changes propagate live without a reload. */
|
||||
--mc-marker-stroke-color: rgba(255,255,255,0.85);
|
||||
--mc-marker-stroke-width: 1;
|
||||
--mc-marker-stroke-opacity: 1;
|
||||
|
||||
/* #1407 — per-role text colors paired with each --mc-role-X bg so
|
||||
* .mc-pill (and any badge using the var) hits WCAG 1.4.3 AA against
|
||||
* the active preset. Wong defaults all clear 4.5:1 with dark text;
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* #1488 — Give operators control over marker stroke (color, width, opacity).
|
||||
*
|
||||
* Background: #1438 migrated marker SVG fills to var(--mc-role-X) but the
|
||||
* stroke="#fff" / stroke-width="1" literals were left baked. With hundreds
|
||||
* of nodes the new white outline is overwhelming. Expose stroke through CSS
|
||||
* vars so customizer (and config.json) can dial it down, recolor it, or
|
||||
* narrow it without code edits.
|
||||
*
|
||||
* RED gate asserts the source pattern:
|
||||
* - public/roles.js makeRoleMarkerSVG strokes use var(--mc-marker-stroke-*)
|
||||
* - public/map.js makeMarkerIcon + observer overlay use the same vars
|
||||
* - public/live.js addNodeMarker fallback uses the same vars
|
||||
* - public/style.css declares defaults under :root
|
||||
* - public/customize-v2.js has marker-stroke controls + routes them to CSS
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function assert(cond, msg) {
|
||||
if (cond) { passed++; console.log(' ✓ ' + msg); }
|
||||
else { failed++; console.error(' ✗ ' + msg); }
|
||||
}
|
||||
|
||||
const rolesSrc = fs.readFileSync(path.join(__dirname, 'public', 'roles.js'), 'utf8');
|
||||
const mapSrc = fs.readFileSync(path.join(__dirname, 'public', 'map.js'), 'utf8');
|
||||
const liveSrc = fs.readFileSync(path.join(__dirname, 'public', 'live.js'), 'utf8');
|
||||
const styleSrc = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8');
|
||||
const customizeSrc = fs.readFileSync(path.join(__dirname, 'public', 'customize-v2.js'), 'utf8');
|
||||
|
||||
console.log('\n=== #1488 A: style.css declares marker-stroke CSS vars ===');
|
||||
{
|
||||
assert(/--mc-marker-stroke-color\s*:/.test(styleSrc),
|
||||
'style.css declares --mc-marker-stroke-color');
|
||||
assert(/--mc-marker-stroke-width\s*:/.test(styleSrc),
|
||||
'style.css declares --mc-marker-stroke-width');
|
||||
assert(/--mc-marker-stroke-opacity\s*:/.test(styleSrc),
|
||||
'style.css declares --mc-marker-stroke-opacity');
|
||||
}
|
||||
|
||||
console.log('\n=== #1488 B: roles.js makeRoleMarkerSVG uses CSS-var stroke ===');
|
||||
{
|
||||
const helperMatch = rolesSrc.match(/window\.makeRoleMarkerSVG[\s\S]*?\n\s*\};/);
|
||||
const block = helperMatch ? helperMatch[0] : '';
|
||||
assert(block.length > 0, 'makeRoleMarkerSVG block located');
|
||||
const bakedFff = (block.match(/stroke="#fff"/g) || []).length;
|
||||
const bakedWhite = (block.match(/stroke="white"/g) || []).length;
|
||||
assert(bakedFff === 0,
|
||||
'makeRoleMarkerSVG has no baked stroke="#fff" (got: ' + bakedFff + ')');
|
||||
assert(bakedWhite === 0,
|
||||
'makeRoleMarkerSVG has no baked stroke="white" (got: ' + bakedWhite + ')');
|
||||
assert(/var\(--mc-marker-stroke-color\)/.test(block),
|
||||
'makeRoleMarkerSVG emits stroke="var(--mc-marker-stroke-color)"');
|
||||
assert(/var\(--mc-marker-stroke-width\)/.test(block),
|
||||
'makeRoleMarkerSVG emits stroke-width="var(--mc-marker-stroke-width)"');
|
||||
}
|
||||
|
||||
console.log('\n=== #1488 C: map.js makeMarkerIcon uses CSS-var stroke ===');
|
||||
{
|
||||
const fnIdx = mapSrc.indexOf('function makeMarkerIcon');
|
||||
assert(fnIdx >= 0, 'makeMarkerIcon function located');
|
||||
const block = mapSrc.slice(fnIdx, fnIdx + 3500);
|
||||
const bakedFff = (block.match(/stroke="#fff"/g) || []).length;
|
||||
assert(bakedFff === 0,
|
||||
'makeMarkerIcon has no baked stroke="#fff" in default path (got: ' + bakedFff + ')');
|
||||
assert(/var\(--mc-marker-stroke-color\)/.test(block),
|
||||
'makeMarkerIcon references var(--mc-marker-stroke-color)');
|
||||
}
|
||||
|
||||
console.log('\n=== #1488 D: live.js addNodeMarker fallback uses CSS-var stroke ===');
|
||||
{
|
||||
const addIdx = liveSrc.indexOf('function addNodeMarker');
|
||||
assert(addIdx >= 0, 'addNodeMarker function located');
|
||||
const block = liveSrc.slice(addIdx, addIdx + 3500);
|
||||
const bakedFff = (block.match(/stroke="#fff"/g) || []).length;
|
||||
assert(bakedFff === 0,
|
||||
'addNodeMarker fallback has no baked stroke="#fff" (got: ' + bakedFff + ')');
|
||||
assert(/var\(--mc-marker-stroke-color\)/.test(block),
|
||||
'addNodeMarker fallback references var(--mc-marker-stroke-color)');
|
||||
}
|
||||
|
||||
console.log('\n=== #1488 E: customize-v2.js exposes marker stroke controls ===');
|
||||
{
|
||||
assert(/markerStroke|marker-stroke/i.test(customizeSrc),
|
||||
'customize-v2.js mentions markerStroke section');
|
||||
assert(/--mc-marker-stroke-color/.test(customizeSrc),
|
||||
'customize-v2.js writes --mc-marker-stroke-color');
|
||||
assert(/--mc-marker-stroke-width/.test(customizeSrc),
|
||||
'customize-v2.js writes --mc-marker-stroke-width');
|
||||
assert(/--mc-marker-stroke-opacity/.test(customizeSrc),
|
||||
'customize-v2.js writes --mc-marker-stroke-opacity');
|
||||
}
|
||||
|
||||
console.log('\n--- Summary ---');
|
||||
console.log(passed + ' passed, ' + failed + ' failed');
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user