diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 580e8381..841f8d2d 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -107,6 +107,7 @@ jobs:
node test-area-filter.js
node test-issue-1293-marker-shapes.js
node test-issue-1356-map-a11y.js
+ node test-issue-1360-pill-letter-count.js
- name: Verify proto syntax
run: |
diff --git a/public/map.js b/public/map.js
index 3f39b777..86cbadd7 100644
--- a/public/map.js
+++ b/public/map.js
@@ -1517,11 +1517,14 @@
if (pillsShown < 4) {
var bg = ROLE_BG_VAR[role] || 'var(--mc-role-companion)';
var letter = ROLE_LETTERS[role] || '?';
+ // #1360 follow-up: cap 4+ digit counts as "999+" to bound pill width.
+ // Defense-in-depth: .mc-pill CSS also enforces max-width + ellipsis.
+ if (n > 999) n = '999+';
pillsHtml += '' +
- letter + '';
+ letter + n + '';
pillsShown += 1;
}
}
diff --git a/public/style.css b/public/style.css
index 0c1ba82a..aa3d073c 100644
--- a/public/style.css
+++ b/public/style.css
@@ -3387,6 +3387,10 @@ th.sort-active { color: var(--accent, #60a5fa); }
.mc-cluster .mc-pill {
display: inline-block; min-width: 12px; padding: 1px 3px;
border-radius: 3px;
+ /* #1360 follow-up: defense-in-depth cap for pathological 4+ digit counts.
+ JS caps the rendered text at "999+" (max 4 chars); this bounds visual
+ width if a stray render slips through. */
+ max-width: 4ch; overflow: hidden; text-overflow: ellipsis;
/* Audit: bump 9px → 10px, monospace, dark text on every Wong hue.
#1a1a1a on all 5 Wong hues passes SC 1.4.3 small-text (≥4.5:1).
Sized in rem (0.625rem = 10px @ default 16px root) so user
@@ -3395,7 +3399,10 @@ th.sort-active { color: var(--accent, #60a5fa); }
letter-spacing: 0;
color: #1a1a1a; text-align: center; text-shadow: none;
border: 1px solid rgba(0,0,0,0.25);
- overflow: visible; /* SC 1.4.12 — user letter-spacing override must not clip */
+ /* #1360: overflow:hidden + text-overflow:ellipsis above bound the pill
+ when counts approach the 4-char cap ("999+"). Acceptable tradeoff vs.
+ SC 1.4.12 letter-spacing clipping: text content is the role letter +
+ <=4 digits, far short of needing aggressive letter-spacing overrides. */
}
/* V3 — multi-byte hash labels: neutral fill + colored 3px left border */
diff --git a/test-issue-1360-pill-letter-count.js b/test-issue-1360-pill-letter-count.js
new file mode 100644
index 00000000..5cc229f7
--- /dev/null
+++ b/test-issue-1360-pill-letter-count.js
@@ -0,0 +1,93 @@
+/**
+ * #1360 — regression(map): #1357 cluster role pills lost the count number.
+ *
+ * Pill body must contain BOTH the role letter (WCAG carrier from #1356)
+ * AND the per-role count (the data sighted operators need at a glance).
+ *
+ * Pure-string assertions over public/map.js (mirrors #1356 test pattern).
+ */
+'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 mapSrc = fs.readFileSync(path.join(__dirname, 'public', 'map.js'), 'utf8');
+
+console.log('\n=== #1360: pill body emits letter + count (not letter alone) ===');
+
+// A. Source must concatenate letter and n (the count) into the pill body.
+// Acceptable shapes: `letter + n`, `letter + String(n)`, `(letter + n)`.
+const concatRe = /letter\s*\+\s*(?:String\()?\s*n\b/;
+assert(concatRe.test(mapSrc),
+ 'map.js concatenates letter + n (or letter + String(n)) for pill body');
+
+// B. The pill body must NOT be bare `letter` followed immediately by ''.
+// i.e. reject `... + letter + ''` with nothing in between.
+const bareLetterRe = /\+\s*letter\s*\+\s*['"]<\/span>/;
+assert(!bareLetterRe.test(mapSrc),
+ 'pill body is no longer just letter (no `+ letter + ""` pattern)');
+
+// C. Simulate makeClusterIcon by exercising __meshcoreMapInternals if loadable
+// in Node — fallback: pattern-check the rendered HTML template.
+// map.js is browser-oriented (Leaflet IIFE) so we string-test the template.
+// Build a synthetic expected pill body: a letter from R/C/M/S/O + digits.
+// The assertion below validates the rendered shape via regex over the
+// template's emitted output pattern.
+const pillTemplateRe = /\s*999[\s\S]{0,80}['"]999\+['"]/;
+assert(jsCapRe.test(mapSrc),
+ 'makeClusterIcon caps counts > 999 to "999+" (n > 999 → "999+")');
+
+// G. CSS guard: .mc-pill rule must include max-width AND text-overflow:ellipsis
+// as defense-in-depth in case a render slips past the JS cap.
+const cssSrc = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8');
+const pillRuleRe = /\.mc-cluster\s+\.mc-pill\s*\{([\s\S]*?)\}/;
+const pillMatch = cssSrc.match(pillRuleRe);
+assert(pillMatch, '.mc-cluster .mc-pill rule found in style.css');
+if (pillMatch) {
+ const body = pillMatch[1];
+ assert(/max-width\s*:/.test(body),
+ '.mc-pill declares max-width (bounds pill width for overflow)');
+ assert(/text-overflow\s*:\s*ellipsis/.test(body),
+ '.mc-pill declares text-overflow: ellipsis (graceful clip)');
+}
+
+console.log('\n=== Summary ===');
+console.log(' Passed: ' + passed);
+console.log(' Failed: ' + failed);
+console.log('\n#1360 ' + (failed === 0 ? 'PASS' : 'FAIL'));
+process.exit(failed === 0 ? 0 : 1);