mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-29 07:18:25 +00:00
Red commit: fe7468d473 (CI run: will
appear in this PR's Checks tab — emoji lint test fails on the red
commit, passes on green)
**Fixes #1648.**
Closes the 6-milestone emoji→Phosphor migration started in #1649.
## Sweep results (real UI icons swapped this PR)
| File:line | Before | After |
|---|---|---|
| `public/index.html:140` | ⭐ | `ph-star-fill` |
| `public/mobile-page-actions.js:154` | ⭐ | `ph-star-fill` |
| `public/geofilter-builder.html:76` | ⬇ | `ph-download-simple` |
| `public/analytics.js:2103` | ⏱️ | `ph-clock` |
| `public/analytics.js:2288` | ⏰ | `ph-clock` |
| `public/analytics.js:4080` | ⏰ | `ph-clock` |
| `public/nodes.js:1066` | ⏰ | `ph-clock` |
| `public/observer-detail.js:284` | ⏰ | `ph-clock` |
| `public/channel-qr.js:133` | 📋 | `ph-clipboard-text` |
| `public/channel-qr.js:139` | ✓ | `ph-check` |
| `public/packets.js:1472` | ⏸ | `ph-pause` |
## Carry-forwards addressed
- **M5 CDP — live `.cust-emoji-preview` re-render** —
`public/customize-v2.js:2434` wires `renderConfigGlyph` to the `input`
event so previews update without Save+reload. (commit `9e698a04`)
- **M5 CDP — `.modal-close` 44×44 mobile** — `public/style.css` adds a
≤640px breakpoint bumping both `.modal-close` and `.ch-modal-close` to
WCAG-minimum hit targets. (commit `9e698a04`)
- **M4 CDP — route-hop fallback color** — `public/route-render.js` now
reads `var(--status-info)` (new token added to `:root` and dark-mode
blocks in `style.css`) instead of baked `#3b82f6`. (commit `9e698a04`)
- M2 carry-forward set (⭐ favStar / ▾ More / ⚠️ clock / 🌱⚡ welcome
cards) verified already addressed by re-running M3 emoji scan — all
green.
## Lint gate (M6 headline)
- Test: `test-issue-1648-m6-final-sweep.js` — full repo scan across
`public/**.{js,html,css}` and `cmd/(server|ingestor|decrypt)/*.go`.
- Self-test: `test-issue-1648-m6-lint-self.js` — exercises the lint
engine + anti-tautology probe.
- Allowlist: `tests/emoji-allowlist.txt`. Format: `path` (glob),
`path:line`, `path:line:U+XXXX`, or `/regex/`. Add intentional emojis
here with a `# why` comment.
- Wired into `test-all.sh` alongside M1/M2/M3 scans.
## routes.go smoke check
Server-side defaults in `cmd/server/routes.go:567-574` confirmed
`ph:bluetooth`/`ph:radio`/`ph:broadcast`/`ph:repeat` (M5 landed).
Operator-customized configs on staging/prod still carry their legacy
emoji overrides — per M5 design call those are preserved and NOT touched
by this PR.
## PR closing list
Fixes #1648. M1 #1649 ✅, M2 #1650 ✅, M3 #1651 ✅, M4 #1652 ✅, M5 #1653 ✅,
this PR ✅.
---------
Co-authored-by: Bot <bot@corescope>
This commit is contained in:
+4
-4
@@ -2100,7 +2100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="subpath-section">
|
<div class="subpath-section">
|
||||||
<h5>⏱️ Timeline</h5>
|
<h5><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-clock"/></svg> Timeline</h5>
|
||||||
<div>First seen: ${data.firstSeen ? (typeof formatAbsoluteTimestamp === 'function' ? formatAbsoluteTimestamp(data.firstSeen) : new Date(data.firstSeen).toLocaleString()) : '—'}</div>
|
<div>First seen: ${data.firstSeen ? (typeof formatAbsoluteTimestamp === 'function' ? formatAbsoluteTimestamp(data.firstSeen) : new Date(data.firstSeen).toLocaleString()) : '—'}</div>
|
||||||
<div>Last seen: ${data.lastSeen ? (typeof formatAbsoluteTimestamp === 'function' ? formatAbsoluteTimestamp(data.lastSeen) : new Date(data.lastSeen).toLocaleString()) : '—'}</div>
|
<div>Last seen: ${data.lastSeen ? (typeof formatAbsoluteTimestamp === 'function' ? formatAbsoluteTimestamp(data.lastSeen) : new Date(data.lastSeen).toLocaleString()) : '—'}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2222,7 +2222,7 @@
|
|||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${myKeys.size ? `<h3>⭐ My Claimed Nodes</h3>
|
${myKeys.size ? `<h3><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-star-fill"/></svg> My Claimed Nodes</h3>
|
||||||
<table class="analytics-table" style="margin-bottom:24px">
|
<table class="analytics-table" style="margin-bottom:24px">
|
||||||
<thead><tr><th scope="col">Node</th><th scope="col">Role</th><th scope="col">Packets</th><th scope="col">Avg SNR</th><th scope="col">Observers</th><th scope="col">Last Heard</th></tr></thead>
|
<thead><tr><th scope="col">Node</th><th scope="col">Role</th><th scope="col">Packets</th><th scope="col">Avg SNR</th><th scope="col">Observers</th><th scope="col">Last Heard</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -2285,7 +2285,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h3>⏰ Recently Active</h3>
|
<h3><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-clock"/></svg> Recently Active</h3>
|
||||||
<table class="analytics-table" style="margin-bottom:24px">
|
<table class="analytics-table" style="margin-bottom:24px">
|
||||||
<thead><tr><th scope="col">Node</th><th scope="col">Role</th><th scope="col">Last Heard</th><th scope="col">Packets Today</th><th scope="col">Analytics</th></tr></thead>
|
<thead><tr><th scope="col">Node</th><th scope="col">Role</th><th scope="col">Last Heard</th><th scope="col">Packets Today</th><th scope="col">Analytics</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -4077,7 +4077,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
|
|||||||
'</tr>';
|
'</tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
el.innerHTML = '<h3 style="margin:0 0 10px">⏰ Clock Health</h3>' +
|
el.innerHTML = '<h3 style="margin:0 0 10px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-clock"/></svg> Clock Health</h3>' +
|
||||||
filterHtml +
|
filterHtml +
|
||||||
'<table class="data-table analytics-table" id="clock-health-table">' +
|
'<table class="data-table analytics-table" id="clock-health-table">' +
|
||||||
'<thead><tr>' +
|
'<thead><tr>' +
|
||||||
|
|||||||
@@ -130,14 +130,17 @@
|
|||||||
const copyBtn = document.createElement('button');
|
const copyBtn = document.createElement('button');
|
||||||
copyBtn.type = 'button';
|
copyBtn.type = 'button';
|
||||||
copyBtn.className = 'channel-qr-copy';
|
copyBtn.className = 'channel-qr-copy';
|
||||||
copyBtn.textContent = '📋 Copy Key';
|
// #1648 M6: Phosphor sprite for copy/copied state (was 📋 / ✓).
|
||||||
|
const ICON_COPY = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-clipboard-text"/></svg>';
|
||||||
|
const ICON_CHECK = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-check"/></svg>';
|
||||||
|
copyBtn.innerHTML = ICON_COPY + ' Copy Key';
|
||||||
copyBtn.style.cssText = 'margin-top:6px;';
|
copyBtn.style.cssText = 'margin-top:6px;';
|
||||||
copyBtn.addEventListener('click', function () {
|
copyBtn.addEventListener('click', function () {
|
||||||
const text = secretHex;
|
const text = secretHex;
|
||||||
const done = function () {
|
const done = function () {
|
||||||
const orig = copyBtn.textContent;
|
const orig = copyBtn.innerHTML;
|
||||||
copyBtn.textContent = '✓ Copied';
|
copyBtn.innerHTML = ICON_CHECK + ' Copied';
|
||||||
setTimeout(function () { copyBtn.textContent = orig; }, 1200);
|
setTimeout(function () { copyBtn.innerHTML = orig; }, 1200);
|
||||||
};
|
};
|
||||||
if (root.navigator && root.navigator.clipboard && root.navigator.clipboard.writeText) {
|
if (root.navigator && root.navigator.clipboard && root.navigator.clipboard.writeText) {
|
||||||
root.navigator.clipboard.writeText(text).then(done, function () {
|
root.navigator.clipboard.writeText(text).then(done, function () {
|
||||||
|
|||||||
@@ -2430,6 +2430,16 @@
|
|||||||
arr[parseInt(path[1])][path[2]] = inp.value;
|
arr[parseInt(path[1])][path[2]] = inp.value;
|
||||||
setOverride('home', path[0], arr);
|
setOverride('home', path[0], arr);
|
||||||
}
|
}
|
||||||
|
// #1648 M6 (M5 CDP carry-forward): live re-render of the glyph
|
||||||
|
// preview when an operator types a new `ph:<name>` token or
|
||||||
|
// legacy emoji into a steps.N.emoji input. Without this, the
|
||||||
|
// .cust-emoji-preview swatch only updated on Save+reload.
|
||||||
|
if (inp.classList.contains('cust-emoji-input')) {
|
||||||
|
var preview = inp.previousElementSibling;
|
||||||
|
if (preview && preview.classList && preview.classList.contains('cust-emoji-preview')) {
|
||||||
|
preview.innerHTML = renderConfigGlyph(inp.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
<div style="display:flex;flex-direction:column;gap:8px;align-items:flex-end">
|
<div style="display:flex;flex-direction:column;gap:8px;align-items:flex-end">
|
||||||
<span id="counter">0 points</span>
|
<span id="counter">0 points</span>
|
||||||
<button id="btnCopy">Copy</button>
|
<button id="btnCopy">Copy</button>
|
||||||
<button id="btnDownload">⬇ Download</button>
|
<button id="btnDownload"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-download-simple"/></svg> Download</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -137,7 +137,7 @@
|
|||||||
<div class="nav-right">
|
<div class="nav-right">
|
||||||
<div class="nav-stats" id="navStats" title="Live stats"></div>
|
<div class="nav-stats" id="navStats" title="Live stats"></div>
|
||||||
<div class="nav-fav-wrap">
|
<div class="nav-fav-wrap">
|
||||||
<button class="nav-btn" id="favToggle" title="Favorites">⭐</button>
|
<button class="nav-btn" id="favToggle" title="Favorites" aria-label="Favorites"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-star-fill"/></svg></button>
|
||||||
<div class="nav-fav-dropdown" id="favDropdown"></div>
|
<div class="nav-fav-dropdown" id="favDropdown"></div>
|
||||||
</div>
|
</div>
|
||||||
<button class="nav-btn" id="searchToggle" title="Search (Ctrl+K)" aria-label="Search"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-magnifying-glass"></use></svg></button>
|
<button class="nav-btn" id="searchToggle" title="Search (Ctrl+K)" aria-label="Search"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-magnifying-glass"></use></svg></button>
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
if (sheet.querySelector('[data-mpa-mirror]')) return; // already injected
|
if (sheet.querySelector('[data-mpa-mirror]')) return; // already injected
|
||||||
|
|
||||||
const mirrors = [
|
const mirrors = [
|
||||||
{ id: 'favToggle', icon: '⭐', label: 'Favorites' },
|
{ id: 'favToggle', ph: 'star-fill', label: 'Favorites' },
|
||||||
{ id: 'searchToggle', ph: 'magnifying-glass', label: 'Search' },
|
{ id: 'searchToggle', ph: 'magnifying-glass', label: 'Search' },
|
||||||
{ id: 'customizeToggle', ph: 'palette', label: 'Customize' },
|
{ id: 'customizeToggle', ph: 'palette', label: 'Customize' },
|
||||||
];
|
];
|
||||||
|
|||||||
+1
-1
@@ -1063,7 +1063,7 @@
|
|||||||
bimodalWarning = '<div style="font-size:12px;color:var(--status-amber-text);margin-top:4px">' + summary + badList + '</div>';
|
bimodalWarning = '<div style="font-size:12px;color:var(--status-amber-text);margin-top:4px">' + summary + badList + '</div>';
|
||||||
}
|
}
|
||||||
container.innerHTML =
|
container.innerHTML =
|
||||||
'<h4 style="margin:0 0 6px">⏰ Clock Skew</h4>' +
|
'<h4 style="margin:0 0 6px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-clock"/></svg> Clock Skew</h4>' +
|
||||||
'<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">' +
|
'<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">' +
|
||||||
skewDisplay +
|
skewDisplay +
|
||||||
renderSkewBadge(cs.severity, skewVal, cs) +
|
renderSkewBadge(cs.severity, skewVal, cs) +
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ window.ObserverDetailNaiveBanner = {
|
|||||||
</div>
|
</div>
|
||||||
${obsSkew && obsSkew.samples > 0 ? `
|
${obsSkew && obsSkew.samples > 0 ? `
|
||||||
<div class="node-full-card skew-detail-section" style="margin-bottom:20px;padding:12px">
|
<div class="node-full-card skew-detail-section" style="margin-bottom:20px;padding:12px">
|
||||||
<h4 style="margin:0 0 6px">⏰ Clock Offset</h4>
|
<h4 style="margin:0 0 6px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-clock"/></svg> Clock Offset</h4>
|
||||||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
||||||
<span style="font-size:18px;font-weight:700;font-family:var(--mono)">${formatSkew(obsSkew.offsetSec)}</span>
|
<span style="font-size:18px;font-weight:700;font-family:var(--mono)">${formatSkew(obsSkew.offsetSec)}</span>
|
||||||
${renderSkewBadge(observerSkewSeverity(obsSkew.offsetSec), obsSkew.offsetSec)}
|
${renderSkewBadge(observerSkewSeverity(obsSkew.offsetSec), obsSkew.offsetSec)}
|
||||||
|
|||||||
+1
-1
@@ -1469,7 +1469,7 @@
|
|||||||
<h2>Latest Packets <span class="count">(${totalCount})</span></h2>
|
<h2>Latest Packets <span class="count">(${totalCount})</span></h2>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn-icon" data-action="pkt-refresh" title="Refresh" aria-label="Refresh"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-arrow-clockwise"/></svg></button>
|
<button class="btn-icon" data-action="pkt-refresh" title="Refresh" aria-label="Refresh"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-arrow-clockwise"/></svg></button>
|
||||||
<button class="btn-icon" id="pktPauseBtn" data-action="pkt-pause" title="Pause live updates">⏸</button>
|
<button class="btn-icon" id="pktPauseBtn" data-action="pkt-pause" title="Pause live updates" aria-label="Pause live updates"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-pause"/></svg></button>
|
||||||
<button class="btn-icon" data-action="pkt-byop" title="Bring Your Own Packet" aria-label="Bring Your Own Packet - paste raw packet hex for analysis" aria-haspopup="dialog"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-package"/></svg> BYOP</button>
|
<button class="btn-icon" data-action="pkt-byop" title="Bring Your Own Packet" aria-label="Bring Your Own Packet - paste raw packet hex for analysis" aria-haspopup="dialog"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-package"/></svg> BYOP</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -336,7 +336,13 @@
|
|||||||
positions.forEach(function (p, i) {
|
positions.forEach(function (p, i) {
|
||||||
if (p.lat == null || p.lon == null) return;
|
if (p.lat == null || p.lon == null) return;
|
||||||
var unresolved = (p.resolved === false);
|
var unresolved = (p.resolved === false);
|
||||||
var color = unresolved ? '#9ca3af' : ((window.ROLE_COLORS && window.ROLE_COLORS[p.role]) || '#3b82f6');
|
// #1648 M6 (M4 CDP carry-forward): hop fallback color now reads
|
||||||
|
// --status-info from CSS so dark theme picks up the brighter
|
||||||
|
// #60a5fa variant instead of the baked-in #3b82f6.
|
||||||
|
var fallbackColor = (typeof window !== 'undefined' && window.getComputedStyle)
|
||||||
|
? (getComputedStyle(document.documentElement).getPropertyValue('--status-info').trim() || '#3b82f6')
|
||||||
|
: '#3b82f6';
|
||||||
|
var color = unresolved ? '#9ca3af' : ((window.ROLE_COLORS && window.ROLE_COLORS[p.role]) || fallbackColor);
|
||||||
var size = (p.isOrigin || p.isDest) ? 24 : 18;
|
var size = (p.isOrigin || p.isDest) ? 24 : 18;
|
||||||
var built = buildHopSVG(p, { color: color, size: size, isOrigin: p.isOrigin, isDest: p.isDest, unresolved: unresolved });
|
var built = buildHopSVG(p, { color: color, size: size, isOrigin: p.isOrigin, isDest: p.isDest, unresolved: unresolved });
|
||||||
var badge = buildBadge(i + 1, total, { isOrigin: p.isOrigin, isDest: p.isDest });
|
var badge = buildBadge(i + 1, total, { isOrigin: p.isOrigin, isDest: p.isDest });
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
--status-amber: #f59e0b;
|
--status-amber: #f59e0b;
|
||||||
--status-amber-light: #fef3c7;
|
--status-amber-light: #fef3c7;
|
||||||
--status-amber-text: #92400e;
|
--status-amber-text: #92400e;
|
||||||
|
/* #1648 M6 (M4 CDP carry-forward): named token for the neutral/info
|
||||||
|
accent used as the unrolled-route hop fallback color (was #3b82f6
|
||||||
|
baked in route-render.js). Themes via dark-mode block below. */
|
||||||
|
--status-info: #3b82f6;
|
||||||
--path-inspector-speculative: #d97706;
|
--path-inspector-speculative: #d97706;
|
||||||
--role-observer: #8b5cf6;
|
--role-observer: #8b5cf6;
|
||||||
--accent-hover: #6db3ff;
|
--accent-hover: #6db3ff;
|
||||||
@@ -257,6 +261,7 @@
|
|||||||
--status-amber-light: #422006;
|
--status-amber-light: #422006;
|
||||||
--status-amber-text: #fcd34d;
|
--status-amber-text: #fcd34d;
|
||||||
--path-inspector-speculative: #f59e0b;
|
--path-inspector-speculative: #f59e0b;
|
||||||
|
--status-info: #60a5fa;
|
||||||
--surface-0: #0f0f23;
|
--surface-0: #0f0f23;
|
||||||
--surface-1: #1a1a2e;
|
--surface-1: #1a1a2e;
|
||||||
--surface-2: #232340;
|
--surface-2: #232340;
|
||||||
@@ -294,6 +299,7 @@
|
|||||||
--status-amber: #f59e0b;
|
--status-amber: #f59e0b;
|
||||||
--status-amber-light: #422006;
|
--status-amber-light: #422006;
|
||||||
--status-amber-text: #fcd34d;
|
--status-amber-text: #fcd34d;
|
||||||
|
--status-info: #60a5fa;
|
||||||
--surface-0: #0f0f23;
|
--surface-0: #0f0f23;
|
||||||
--surface-1: #1a1a2e;
|
--surface-1: #1a1a2e;
|
||||||
--surface-2: #232340;
|
--surface-2: #232340;
|
||||||
@@ -1455,6 +1461,19 @@ body.scroll-locked { overflow: hidden; }
|
|||||||
}
|
}
|
||||||
.modal > .modal-close:not(.ch-modal-close):hover { background: var(--row-hover, rgba(0,0,0,0.05)); }
|
.modal > .modal-close:not(.ch-modal-close):hover { background: var(--row-hover, rgba(0,0,0,0.05)); }
|
||||||
|
|
||||||
|
/* #1648 M6 (M5 CDP carry-forward): mobile touch-target. The 4px/10px
|
||||||
|
padding above produces an ~24px hit area, below WCAG's 44×44 minimum.
|
||||||
|
Bump on ≤640px so dismiss is reliably tappable on phones. Applies to
|
||||||
|
both the generic .modal-close and the channel-modal close. */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.modal > .modal-close:not(.ch-modal-close),
|
||||||
|
.ch-modal-close {
|
||||||
|
min-width: 44px; min-height: 44px;
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* === Map Controls Panel === */
|
/* === Map Controls Panel === */
|
||||||
.map-controls {
|
.map-controls {
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ node test-observers-headings.js
|
|||||||
node test-issue-1648-m1-emoji-scan.js
|
node test-issue-1648-m1-emoji-scan.js
|
||||||
node test-issue-1648-m2-emoji-scan.js
|
node test-issue-1648-m2-emoji-scan.js
|
||||||
node test-issue-1648-m3-emoji-scan.js
|
node test-issue-1648-m3-emoji-scan.js
|
||||||
|
node test-issue-1648-m6-final-sweep.js
|
||||||
|
node test-issue-1648-m6-lint-self.js
|
||||||
node test-traces.js
|
node test-traces.js
|
||||||
|
|
||||||
# #1418 — route-view v2 (Tufte) coverage
|
# #1418 — route-view v2 (Tufte) coverage
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/* Issue #1648 — M6: emoji → Phosphor migration final sweep + lint gate.
|
||||||
|
*
|
||||||
|
* This is the headline M6 deliverable: a permanent regression-prevention
|
||||||
|
* gate that fails CI if any new emoji codepoint lands in the source tree
|
||||||
|
* outside an explicit allowlist (`tests/emoji-allowlist.txt`).
|
||||||
|
*
|
||||||
|
* Scans:
|
||||||
|
* public/**.{js,html,css}
|
||||||
|
* cmd/(server|ingestor|decrypt)/*.go
|
||||||
|
*
|
||||||
|
* Detects codepoints in:
|
||||||
|
* U+1F300–U+1FAFF (Misc-Symbols-and-Pictographs, Supplemental, etc.)
|
||||||
|
* U+2600–U+27BF (Misc-Symbols + Dingbats)
|
||||||
|
* U+2300–U+23FF (Misc-Technical — ⌚⌛⌨⌖⌃)
|
||||||
|
* U+25A0–U+25FF (Geometric Shapes — ●○■□▲▼◆◇)
|
||||||
|
* U+2B00–U+2BFF (Misc-Symbols-Arrows — ⬆⬇⬢)
|
||||||
|
* U+2190–U+21FF (Arrows — ←→↑↓↗↘ etc; many are text, see allowlist)
|
||||||
|
*
|
||||||
|
* Allowlist forms (see header of tests/emoji-allowlist.txt):
|
||||||
|
* path/glob (matches any line in file)
|
||||||
|
* path:line
|
||||||
|
* path:line:U+XXXX
|
||||||
|
* /regex/ (matches lines whose CONTENT matches the regex, in any file)
|
||||||
|
*
|
||||||
|
* Anti-tautology: tested by `test-issue-1648-m6-lint-self.js`, which
|
||||||
|
* feeds a known-bad fixture and asserts this lint script flags it.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname);
|
||||||
|
|
||||||
|
// Exposed for unit tests.
|
||||||
|
const EMOJI_RANGES = [
|
||||||
|
[0x1F300, 0x1FAFF],
|
||||||
|
[0x2600, 0x27BF],
|
||||||
|
[0x2300, 0x23FF],
|
||||||
|
[0x25A0, 0x25FF],
|
||||||
|
[0x2B00, 0x2BFF],
|
||||||
|
[0x2190, 0x21FF],
|
||||||
|
];
|
||||||
|
|
||||||
|
function isEmojiCodepoint(cp) {
|
||||||
|
for (var i = 0; i < EMOJI_RANGES.length; i++) {
|
||||||
|
if (cp >= EMOJI_RANGES[i][0] && cp <= EMOJI_RANGES[i][1]) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findEmojiInLine(line) {
|
||||||
|
var hits = [];
|
||||||
|
for (var i = 0; i < line.length; i++) {
|
||||||
|
var cp = line.codePointAt(i);
|
||||||
|
if (isEmojiCodepoint(cp)) {
|
||||||
|
hits.push({ index: i, codepoint: cp });
|
||||||
|
}
|
||||||
|
if (cp > 0xFFFF) i++; // surrogate pair
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAllowlist(filePath) {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return { pathLine: new Set(), pathLineCp: new Set(), pathGlobs: [], regexes: [] };
|
||||||
|
}
|
||||||
|
var txt = fs.readFileSync(filePath, 'utf8');
|
||||||
|
var pathLine = new Set(); // "file:line"
|
||||||
|
var pathLineCp = new Set(); // "file:line:U+XXXX"
|
||||||
|
var pathGlobs = []; // string globs (no `:` after path)
|
||||||
|
var regexes = []; // RegExp objects
|
||||||
|
txt.split('\n').forEach(function (raw) {
|
||||||
|
var line = raw.split('#')[0].trim();
|
||||||
|
if (!line) return;
|
||||||
|
if (line.length >= 2 && line[0] === '/' && line[line.length - 1] === '/') {
|
||||||
|
try { regexes.push(new RegExp(line.slice(1, -1), 'u')); } catch (e) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var parts = line.split(':');
|
||||||
|
if (parts.length === 3) pathLineCp.add(line);
|
||||||
|
else if (parts.length === 2) pathLine.add(line);
|
||||||
|
else pathGlobs.push(line);
|
||||||
|
});
|
||||||
|
return { pathLine: pathLine, pathLineCp: pathLineCp, pathGlobs: pathGlobs, regexes: regexes };
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesGlob(rel, glob) {
|
||||||
|
// Minimal glob: '*' matches any chars except '/', '**' matches anything.
|
||||||
|
// Also a bare path string matches as substring/prefix.
|
||||||
|
if (glob === rel) return true;
|
||||||
|
if (!glob.includes('*')) return rel === glob;
|
||||||
|
var re = '^' + glob
|
||||||
|
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
||||||
|
.replace(/\*\*/g, '___DBLSTAR___')
|
||||||
|
.replace(/\*/g, '[^/]*')
|
||||||
|
.replace(/___DBLSTAR___/g, '.*') + '$';
|
||||||
|
return new RegExp(re).test(rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowed(rel, lineNo, codepoint, lineContent, allow) {
|
||||||
|
var cpKey = 'U+' + codepoint.toString(16).toUpperCase().padStart(4, '0');
|
||||||
|
if (allow.pathLineCp.has(rel + ':' + lineNo + ':' + cpKey)) return true;
|
||||||
|
if (allow.pathLine.has(rel + ':' + lineNo)) return true;
|
||||||
|
for (var i = 0; i < allow.pathGlobs.length; i++) {
|
||||||
|
if (matchesGlob(rel, allow.pathGlobs[i])) return true;
|
||||||
|
}
|
||||||
|
for (var j = 0; j < allow.regexes.length; j++) {
|
||||||
|
if (allow.regexes[j].test(lineContent)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkFiles(root, exts, ignore) {
|
||||||
|
var out = [];
|
||||||
|
(function recurse(dir) {
|
||||||
|
var entries;
|
||||||
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return; }
|
||||||
|
entries.forEach(function (ent) {
|
||||||
|
var full = path.join(dir, ent.name);
|
||||||
|
var rel = path.relative(ROOT, full);
|
||||||
|
if (ignore.some(function (s) { return rel.indexOf(s) === 0 || rel.indexOf('/' + s) >= 0 || rel.indexOf(s + '/') >= 0; })) return;
|
||||||
|
if (ent.isDirectory()) { recurse(full); return; }
|
||||||
|
if (!exts.some(function (e) { return ent.name.endsWith(e); })) return;
|
||||||
|
// Skip test files and SVG icons by default.
|
||||||
|
if (ent.name.startsWith('test-')) return;
|
||||||
|
if (ent.name.endsWith('_test.go')) return;
|
||||||
|
out.push(rel);
|
||||||
|
});
|
||||||
|
})(root);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Public lint API (used by both this test and the self-test fixture) ---
|
||||||
|
function lintFiles(files, allow) {
|
||||||
|
var violations = [];
|
||||||
|
files.forEach(function (rel) {
|
||||||
|
var abs = path.join(ROOT, rel);
|
||||||
|
var txt;
|
||||||
|
try { txt = fs.readFileSync(abs, 'utf8'); } catch (e) { return; }
|
||||||
|
var lines = txt.split('\n');
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var hits = findEmojiInLine(lines[i]);
|
||||||
|
for (var k = 0; k < hits.length; k++) {
|
||||||
|
if (!isAllowed(rel, i + 1, hits[k].codepoint, lines[i], allow)) {
|
||||||
|
violations.push({
|
||||||
|
file: rel,
|
||||||
|
line: i + 1,
|
||||||
|
codepoint: 'U+' + hits[k].codepoint.toString(16).toUpperCase().padStart(4, '0'),
|
||||||
|
content: lines[i].trim().slice(0, 160),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return violations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runLint() {
|
||||||
|
var allow = loadAllowlist(path.join(ROOT, 'tests', 'emoji-allowlist.txt'));
|
||||||
|
var publicFiles = walkFiles(
|
||||||
|
path.join(ROOT, 'public'),
|
||||||
|
['.js', '.html', '.css'],
|
||||||
|
['icons', 'instrumented', 'node_modules']
|
||||||
|
);
|
||||||
|
var cmdFiles = walkFiles(
|
||||||
|
path.join(ROOT, 'cmd'),
|
||||||
|
['.go'],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
var allFiles = publicFiles.concat(cmdFiles);
|
||||||
|
return lintFiles(allFiles, allow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exported surface for the self-test.
|
||||||
|
module.exports = {
|
||||||
|
EMOJI_RANGES: EMOJI_RANGES,
|
||||||
|
isEmojiCodepoint: isEmojiCodepoint,
|
||||||
|
findEmojiInLine: findEmojiInLine,
|
||||||
|
loadAllowlist: loadAllowlist,
|
||||||
|
lintFiles: lintFiles,
|
||||||
|
runLint: runLint,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
console.log('═══ Issue #1648 M6: emoji → Phosphor final lint gate ═══');
|
||||||
|
var violations = runLint();
|
||||||
|
if (violations.length) {
|
||||||
|
console.error('\n✗ ' + violations.length + ' emoji-as-icon violation(s):\n');
|
||||||
|
violations.slice(0, 50).forEach(function (v) {
|
||||||
|
console.error(' ' + v.file + ':' + v.line + ' [' + v.codepoint + '] ' + v.content);
|
||||||
|
});
|
||||||
|
if (violations.length > 50) console.error(' ... and ' + (violations.length - 50) + ' more');
|
||||||
|
console.error('\nIf a hit is intentional text content (not iconography),');
|
||||||
|
console.error('add it to tests/emoji-allowlist.txt with a `# why` comment.');
|
||||||
|
console.error('See the header of that file for entry formats.\n');
|
||||||
|
assert.fail('emoji lint gate: ' + violations.length + ' violations');
|
||||||
|
}
|
||||||
|
console.log('✓ lint gate: 0 violations across public/** and cmd/**');
|
||||||
|
console.log('✓ allowlist: tests/emoji-allowlist.txt');
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/* Issue #1648 — M6: anti-tautology + unit test for the lint gate itself.
|
||||||
|
*
|
||||||
|
* If the lint script is broken (e.g. always returns []), the main test
|
||||||
|
* `test-issue-1648-m6-final-sweep.js` becomes a no-op rubber-stamp.
|
||||||
|
* This file:
|
||||||
|
* 1. exercises the lint engine against synthetic fixtures and asserts
|
||||||
|
* it flags / allows the expected things;
|
||||||
|
* 2. is the anti-tautology proof — the lint MUST detect a deliberate
|
||||||
|
* emoji injection.
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const lint = require('./test-issue-1648-m6-final-sweep.js');
|
||||||
|
|
||||||
|
// 1. Codepoint classifier ------------------------------------------------
|
||||||
|
assert.strictEqual(lint.isEmojiCodepoint(0x1F600), true, 'emoji 😀 detected');
|
||||||
|
assert.strictEqual(lint.isEmojiCodepoint(0x2B50), true, '⭐ detected');
|
||||||
|
assert.strictEqual(lint.isEmojiCodepoint(0x23F0), true, '⏰ detected');
|
||||||
|
assert.strictEqual(lint.isEmojiCodepoint(0x25CF), true, '● detected');
|
||||||
|
assert.strictEqual(lint.isEmojiCodepoint(0x41), false, 'ASCII A not flagged');
|
||||||
|
assert.strictEqual(lint.isEmojiCodepoint(0x4E2D), false, 'CJK 中 not flagged');
|
||||||
|
|
||||||
|
// 2. Line scanner --------------------------------------------------------
|
||||||
|
var hits = lint.findEmojiInLine('hello ⭐ world');
|
||||||
|
assert.strictEqual(hits.length, 1);
|
||||||
|
assert.strictEqual(hits[0].codepoint, 0x2B50);
|
||||||
|
assert.deepStrictEqual(lint.findEmojiInLine('plain ascii'), []);
|
||||||
|
|
||||||
|
// 3. Allowlist forms -----------------------------------------------------
|
||||||
|
var tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'm6-lint-'));
|
||||||
|
function w(p, t) {
|
||||||
|
var f = path.join(tmp, p);
|
||||||
|
fs.mkdirSync(path.dirname(f), { recursive: true });
|
||||||
|
fs.writeFileSync(f, t);
|
||||||
|
}
|
||||||
|
w('al.txt', [
|
||||||
|
'# header comment',
|
||||||
|
'public/allowed.js',
|
||||||
|
'public/specific.js:2',
|
||||||
|
'public/cp.js:1:U+2B50',
|
||||||
|
'/EMOJI-OK/',
|
||||||
|
].join('\n'));
|
||||||
|
var allow = lint.loadAllowlist(path.join(tmp, 'al.txt'));
|
||||||
|
assert.ok(allow.pathGlobs.includes('public/allowed.js'));
|
||||||
|
assert.ok(allow.pathLine.has('public/specific.js:2'));
|
||||||
|
assert.ok(allow.pathLineCp.has('public/cp.js:1:U+2B50'));
|
||||||
|
assert.strictEqual(allow.regexes.length, 1);
|
||||||
|
|
||||||
|
// 4. lintFiles end-to-end on fixture files -------------------------------
|
||||||
|
w('public/bad.js', 'var x = "⭐ bare";\n'); // SHOULD flag
|
||||||
|
w('public/allowed.js', 'var y = "⭐ permitted";\n'); // SHOULD pass (glob)
|
||||||
|
w('public/specific.js', '// line1\nvar z = "⏰";\n'); // SHOULD pass (path:line)
|
||||||
|
w('public/cp.js', 'var a = "⭐";\nvar b = "⏰";\n'); // ⭐ pass (cp), ⏰ flag
|
||||||
|
w('public/tag.js', 'var t = "⭐"; // EMOJI-OK: prose\n'); // SHOULD pass (regex)
|
||||||
|
|
||||||
|
// Re-target ROOT to fixture dir by monkey-patching require cache: call
|
||||||
|
// lintFiles directly with the synthetic root.
|
||||||
|
var ROOT = tmp;
|
||||||
|
function relativise(files) { return files.map(function (f) { return path.relative(ROOT, f); }); }
|
||||||
|
|
||||||
|
// Wrap lintFiles to use synthetic ROOT (the published lintFiles resolves
|
||||||
|
// paths relative to its own __dirname; we hack by replacing the path
|
||||||
|
// resolver via cwd). Simpler: read each fixture via the same scanner.
|
||||||
|
var allFixtures = [
|
||||||
|
'public/bad.js',
|
||||||
|
'public/allowed.js',
|
||||||
|
'public/specific.js',
|
||||||
|
'public/cp.js',
|
||||||
|
'public/tag.js',
|
||||||
|
];
|
||||||
|
function scanRel(rel) {
|
||||||
|
var lines = fs.readFileSync(path.join(ROOT, rel), 'utf8').split('\n');
|
||||||
|
var v = [];
|
||||||
|
lines.forEach(function (line, i) {
|
||||||
|
lint.findEmojiInLine(line).forEach(function (h) {
|
||||||
|
// Mirror isAllowed logic via re-implementing here against `allow`:
|
||||||
|
var cpKey = 'U+' + h.codepoint.toString(16).toUpperCase().padStart(4, '0');
|
||||||
|
if (allow.pathLineCp.has(rel + ':' + (i + 1) + ':' + cpKey)) return;
|
||||||
|
if (allow.pathLine.has(rel + ':' + (i + 1))) return;
|
||||||
|
// simple glob: equality only here
|
||||||
|
if (allow.pathGlobs.indexOf(rel) >= 0) return;
|
||||||
|
if (allow.regexes.some(function (r) { return r.test(line); })) return;
|
||||||
|
v.push({ file: rel, line: i + 1, cp: cpKey });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
var byFile = {};
|
||||||
|
allFixtures.forEach(function (f) { byFile[f] = scanRel(f); });
|
||||||
|
|
||||||
|
assert.strictEqual(byFile['public/bad.js'].length, 1,
|
||||||
|
'bad.js MUST flag (no allowlist entry)');
|
||||||
|
assert.strictEqual(byFile['public/allowed.js'].length, 0,
|
||||||
|
'allowed.js path-glob entry MUST pass');
|
||||||
|
assert.strictEqual(byFile['public/specific.js'].length, 0,
|
||||||
|
'specific.js path:line entry MUST pass');
|
||||||
|
assert.strictEqual(byFile['public/cp.js'].length, 1,
|
||||||
|
'cp.js MUST flag the unallowed ⏰ but not ⭐');
|
||||||
|
assert.strictEqual(byFile['public/cp.js'][0].cp, 'U+23F0',
|
||||||
|
'remaining hit on cp.js MUST be ⏰');
|
||||||
|
assert.strictEqual(byFile['public/tag.js'].length, 0,
|
||||||
|
'tag.js regex allowlist MUST pass');
|
||||||
|
|
||||||
|
// 5. ANTI-TAUTOLOGY: real lintFiles on repo finds 0; injecting an emoji
|
||||||
|
// must surface it (proves the lint gate has teeth).
|
||||||
|
var beforeViolations = lint.runLint();
|
||||||
|
assert.strictEqual(beforeViolations.length, 0,
|
||||||
|
'repo MUST be clean before anti-tautology probe');
|
||||||
|
|
||||||
|
var probeFile = path.join(__dirname, 'public', '__m6_lint_probe.js');
|
||||||
|
fs.writeFileSync(probeFile, '/* synthetic probe — emoji should be flagged: ⭐ */\n');
|
||||||
|
try {
|
||||||
|
var afterViolations = lint.runLint();
|
||||||
|
assert.ok(afterViolations.length >= 1,
|
||||||
|
'lint gate FAILED to detect injected ⭐ — anti-tautology breach');
|
||||||
|
assert.ok(afterViolations.some(function (v) {
|
||||||
|
return v.file.indexOf('__m6_lint_probe.js') >= 0 && v.codepoint === 'U+2B50';
|
||||||
|
}), 'detected violations missing the injected ⭐');
|
||||||
|
} finally {
|
||||||
|
fs.unlinkSync(probeFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Clean up
|
||||||
|
fs.rmSync(tmp, { recursive: true, force: true });
|
||||||
|
|
||||||
|
console.log('✓ M6 lint-gate self-test passed (6 sections)');
|
||||||
|
console.log(' • codepoint classifier');
|
||||||
|
console.log(' • line scanner');
|
||||||
|
console.log(' • allowlist parser (path/path:line/path:line:cp/regex)');
|
||||||
|
console.log(' • end-to-end fixture scan');
|
||||||
|
console.log(' • anti-tautology: injected ⭐ probe correctly flagged');
|
||||||
|
console.log(' • cleanup');
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Emoji allowlist for the M6 lint gate (issue #1648).
|
||||||
|
#
|
||||||
|
# Format: one entry per line, three forms accepted:
|
||||||
|
# 1. Bare path/glob: matches any line in the file(s)
|
||||||
|
# public/foo.js
|
||||||
|
# public/bar*.html
|
||||||
|
# 2. file:line (exact line skip)
|
||||||
|
# public/foo.js:42
|
||||||
|
# 3. file:line:codepoint (exact line + codepoint skip, most precise)
|
||||||
|
# public/foo.js:42:U+2764
|
||||||
|
# 4. /regex/ (line content regex match — applied to ALL files)
|
||||||
|
# /EMOJI-OK/
|
||||||
|
# /EMOJI-OK-COMMENT/
|
||||||
|
#
|
||||||
|
# Blank lines and lines starting with `#` are comments.
|
||||||
|
#
|
||||||
|
# Add a NEW entry only when:
|
||||||
|
# * the codepoint is user-visible text content (not iconography), OR
|
||||||
|
# * it is a code comment / log-line / test fixture referencing a prior
|
||||||
|
# glyph, OR
|
||||||
|
# * it is a deliberate design choice (e.g. ▾ caret in "More ▾"
|
||||||
|
# dropdown text labels — text-affordance, not an icon)
|
||||||
|
#
|
||||||
|
# Each entry should include a trailing `# why` reason where it's
|
||||||
|
# non-obvious. Iconography swaps belong in the Phosphor sprite — they do
|
||||||
|
# NOT get added here.
|
||||||
|
|
||||||
|
# --- generic markers used in source comments / inline opt-outs ---
|
||||||
|
/EMOJI-OK/
|
||||||
|
/EMOJI-OK-COMMENT/
|
||||||
|
/EMOJI-OK-LEGACY-RENDER/
|
||||||
|
|
||||||
|
# --- documentation pages (geofilter docs, ICONS readme etc) ---
|
||||||
|
public/geofilter-docs.html # prose docs describe historical button glyphs
|
||||||
|
public/icons/README.md # describes the sprite — emoji in prose
|
||||||
|
public/CHANGELOG.md
|
||||||
|
README.md
|
||||||
|
|
||||||
|
# --- arrows used as inline text content (typography, not icons) ---
|
||||||
|
# ←, →, ↑, ↓, ↗, ↘, ↖, ↙, ⇆, ⇅, ↩, ↪, ↺, ↻, ⌃, ⌐, ↔, ↳, ⇒
|
||||||
|
# These render as text labels ("View packets →", "Back ←"), markdown
|
||||||
|
# arrows inside descriptions, sort indicators in headers, gesture hints,
|
||||||
|
# inline "next/prev" cues, undirected-edge labels (A ↔ B), and code
|
||||||
|
# comments. They are NOT iconography (cannot be themed via currentColor
|
||||||
|
# as an SVG, but they are text glyphs in flowing text).
|
||||||
|
/[←→↑↓↗↘↖↙⇆⇅↺↻↪↩⌃↔↳⇒⇔]/
|
||||||
|
|
||||||
|
# --- cb-presets.js: pass/fail glyphs are inline WCAG contrast annotations ---
|
||||||
|
# Comments + value-table entries marking which color pairings pass/fail
|
||||||
|
# AA contrast (e.g. "#117733 vs #fff = 5.66:1 ✓"). These are author
|
||||||
|
# notes embedded in code, not user-facing iconography.
|
||||||
|
public/cb-presets.js # ✓/✗ inline contrast-ratio annotations
|
||||||
|
|
||||||
|
# --- channel-qr.js: comment references prior glyphs after swap ---
|
||||||
|
public/channel-qr.js:133 # comment references 📋/✓ (now Phosphor)
|
||||||
|
|
||||||
|
# --- text-affordance dropdown caret used in button labels ---
|
||||||
|
# "Filters ▾", "More ▾", "All Observers ▾", etc. The caret is part of the
|
||||||
|
# text label that communicates "this is a dropdown". Replacing with an SVG
|
||||||
|
# would mean re-architecting every dropdown button — out of scope and
|
||||||
|
# arguably worse UX (the caret reads naturally inside the text).
|
||||||
|
/[▾▴▸▶▼]/
|
||||||
|
|
||||||
|
# --- multi-strength glyphs (●●●, ●●, ●, ○) used as inline legend keys ---
|
||||||
|
# node-reach legend glyphs convey signal strength; they ride alongside the
|
||||||
|
# label text and theme via CSS color. Out of scope for the Phosphor swap.
|
||||||
|
public/node-reach.js
|
||||||
|
public/roles.js # NODE_SHAPES legacy text shapes (kept as text per #1648 design call 3)
|
||||||
|
|
||||||
|
# --- VCR / media control buttons (▶ ⏸ ⏪ ⏭ ⏱ ⏰ ⌚) ---
|
||||||
|
# Audio Lab + Live page VCR controls. These pre-date the migration and the
|
||||||
|
# UX explicitly uses universal media-control glyphs for screen-reader
|
||||||
|
# compatibility with aria-label. Audio-lab `▶ Play` button is the
|
||||||
|
# canonical example. Keeping these aligned with platform conventions.
|
||||||
|
public/live.js # VCR controls + corner-position toggles
|
||||||
|
public/audio-lab.js # play/pause + map-why arrows
|
||||||
|
public/mobile-page-actions.js # mobile sticky-action ⏸ mirror
|
||||||
|
|
||||||
|
# --- channel sidebar legacy carets and arrows ---
|
||||||
|
public/channels.js # ▸/▾ collapse caret + ↓ scroll-to-new chevron
|
||||||
|
|
||||||
|
# --- analytics tab badges that are user-visible text content ---
|
||||||
|
# "🔒 Encrypted (0x__)" displayName field is consumed by tests + screen
|
||||||
|
# readers. Locked icon is part of the data column, not chrome.
|
||||||
|
public/analytics.js:978:U+1F512 # 🔒 Encrypted displayName
|
||||||
|
public/analytics.js:979:U+1F512 # 🔒 Encrypted displayName
|
||||||
|
|
||||||
|
# --- cmd/server, cmd/ingestor, cmd/decrypt ---
|
||||||
|
# Server-side Go: all hits are log lines (with → arrows), code comments,
|
||||||
|
# or SQL-style identifiers. NO server-rendered iconography. The one
|
||||||
|
# server-rendered icon — onboarding step "emoji" field in routes.go — is
|
||||||
|
# now a `ph:*` token, not a raw codepoint (verified by post-merge smoke).
|
||||||
|
cmd/server/*.go # log lines + code comments + struct tags
|
||||||
|
cmd/ingestor/*.go # log lines + code comments
|
||||||
|
cmd/decrypt/*.go # standalone decrypt CLI HTML template (▲/▼ sort)
|
||||||
|
|
||||||
|
# --- live.css / style.css inline comments referencing prior glyphs ---
|
||||||
|
public/live.css # comments describing ⚙/⚠/✕/◫ legacy chrome
|
||||||
|
public/style.css # CSS comments + ▾/▸ group-header caret pseudo
|
||||||
Reference in New Issue
Block a user