mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-28 18:22:09 +00:00
Red commit: 12e921d9ba (CI run: pending —
see Actions tab on this branch)
Partial fix for #1648 (M1 of 6). **Do NOT close the tracking issue** —
only top-nav, mobile nav, drawer, mobile-page-actions, and the Compare
entry points are migrated here. M2-M6 (page headers, table chrome,
detail panes, map overlays, settings, lint gate) follow in subsequent
PRs.
## What changed
Replaces emoji glyphs used as UI iconography with vendored Phosphor SVG
sprite refs. Pattern: `<svg class="ph-icon"><use
href="/icons/phosphor-sprite.svg#ph-NAME"/></svg>`. Inherits color via
`currentColor`, sizes to `1em`, no FOUT, no CDN, no webfont.
Sprite: `public/icons/phosphor-sprite.svg` — 34 symbols, 13 KB, regular
weight (plus `circle-fill`/`star-fill`/`square-fill` for status dots).
## Surfaces swapped (M1)
| File | Before (emoji) | After (ph-NAME) |
|---|---|---|
| `public/index.html` L123 | 🔴 | `ph-broadcast` |
| `public/index.html` L129 | ⚡ | `ph-lightning` |
| `public/index.html` L130 | 🎵 | `ph-music-note` |
| `public/index.html` L143 | 🔍 | `ph-magnifying-glass` |
| `public/index.html` L144 | 🎨 | `ph-palette` |
| `public/index.html` L149/L150 | ☀️/🌙 | `ph-sun` / `ph-moon` |
| `public/index.html` L153 | ☰ | `ph-list` |
| `public/bottom-nav.js` TABS | 🏠📦🔴🗺️💬☰ | `house package broadcast
map-trifold chat-circle list` |
| `public/bottom-nav.js` MORE_ROUTES | 🖥️🛠️👁️📊⚡🎵 | `monitor wrench eye
chart-bar lightning music-note` |
| `public/bottom-nav.js` L265 | 🌙/☀️ | `ph-moon` / `ph-sun` |
| `public/nav-drawer.js` ROUTES | (mirror of MORE_ROUTES) | same
Phosphor mapping |
| `public/mobile-page-actions.js` | 🔍🎨 | `ph-magnifying-glass`
`ph-palette` |
| `public/observers.js` Compare obs | 🔍 | `ph-magnifying-glass` |
| `public/observers.js` Compare-selected | ⚖️ | `ph-scales` |
| `public/observers.js` refresh | 🔄 | `ph-arrow-clockwise` |
| `public/observers.js` packetBadge | 📡⚠ | `ph-broadcast` + `ph-warning`
|
| `public/observers.js` row health-dot | ●/▲/✕ | `ph-circle-fill` /
`ph-triangle` / `ph-x` |
| `public/observer-detail.js` Compare | 🔍 | `ph-magnifying-glass` |
| `public/observer-detail.js` health-dot | ● | `ph-circle-fill` |
Also: misc-symbols (`●▲✕`) on observer health dots and the box-drawing
role shapes (per #1648 surprise #3) are migrated here because they live
on the same lines as M1 emoji.
## Tests
E2E assertion added: `test-issue-1648-m1-icons-e2e.js:104` (asserts each
bottom-nav tab renders a `.ph-icon` with non-zero
`getBoundingClientRect`)
Two new tests, both committed RED first then GREEN:
- `test-issue-1648-m1-emoji-scan.js` — static file scan; fails if any M1
file contains emoji or misc-icon codepoints.
- `test-issue-1648-m1-icons-e2e.js` — Playwright; loads top-nav +
bottom-nav + `/observers`, asserts `.ph-icon` children render with
non-zero size and the rendered nav DOM has zero emoji codepoints.
Existing tests untouched and still green (e.g.
`test-observers-headings.js`, frontend-helpers).
## Browser verified
Local Chromium against `python3 -m http.server` from `public/`.
Screenshots taken at 375 / 768 / 1200 × dark/light — all icons render
via `currentColor`, theme toggle recolors, no `.notdef` glyphs, no
layout shift vs pre-fix master.
## Out of scope (deferred to M2-M6)
- Home-page chooser cards (📱⚡), per-page header `<h2>` glyphs,
packet-table chrome — M2.
- Status pills, role/packet-type badges, payload-type icon maps — M3.
- Map popups + route overlays — M4.
- Customize panel emoji configs, channel modals, settings — M5.
- Server-rendered onboarding strings in `cmd/server/routes.go`, `make
lint-no-emoji` gate — M6.
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
This commit is contained in:
+29
-20
@@ -41,35 +41,44 @@
|
||||
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
// 5 primary tabs + the More toggle. Each entry: { route, hash, label, icon }.
|
||||
// For More, hash is null (not a route).
|
||||
// Phosphor sprite ref helper (#1648 M1).
|
||||
// Returns an inline SVG that uses the bundled sprite at /icons/phosphor-sprite.svg.
|
||||
// Inherits color via currentColor and size via the surrounding font-size.
|
||||
function phIconHTML(name) {
|
||||
return '<svg class="ph-icon" aria-hidden="true" focusable="false">' +
|
||||
'<use href="/icons/phosphor-sprite.svg#ph-' + name + '"></use></svg>';
|
||||
}
|
||||
|
||||
// 5 primary tabs + the More toggle. Each entry: { route, hash, label, ph }.
|
||||
// For More, hash is null (not a route). `ph` is the Phosphor icon id
|
||||
// (no "ph-" prefix) — see public/icons/phosphor-sprite.svg.
|
||||
var TABS = [
|
||||
{ route: 'home', hash: '#/home', label: 'Home', icon: '🏠' },
|
||||
{ route: 'packets', hash: '#/packets', label: 'Packets', icon: '📦' },
|
||||
{ route: 'live', hash: '#/live', label: 'Live', icon: '🔴' },
|
||||
{ route: 'map', hash: '#/map', label: 'Map', icon: '🗺️' },
|
||||
{ route: 'channels', hash: '#/channels', label: 'Channels', icon: '💬' },
|
||||
{ route: 'more', hash: null, label: 'More', icon: '☰' },
|
||||
{ route: 'home', hash: '#/home', label: 'Home', ph: 'house' },
|
||||
{ route: 'packets', hash: '#/packets', label: 'Packets', ph: 'package' },
|
||||
{ route: 'live', hash: '#/live', label: 'Live', ph: 'broadcast' },
|
||||
{ route: 'map', hash: '#/map', label: 'Map', ph: 'map-trifold' },
|
||||
{ route: 'channels', hash: '#/channels', label: 'Channels', ph: 'chat-circle' },
|
||||
{ route: 'more', hash: null, label: 'More', ph: 'list' },
|
||||
];
|
||||
|
||||
// Long-tail routes surfaced in the More sheet. Mirrors data-route values
|
||||
// from the existing top-nav (public/index.html). Order matches what
|
||||
// operators expect from the desktop top-nav.
|
||||
//
|
||||
// ⚠️ MANUAL SYNC REQUIRED ⚠️
|
||||
// !! MANUAL SYNC REQUIRED !!
|
||||
// This list is intentionally hardcoded (not generated from
|
||||
// `.top-nav .nav-link[data-route]`) because the top-nav HTML is in
|
||||
// mid-rewrite and not a reliable single-source-of-truth. If you add a
|
||||
// new top-nav route (e.g. a future "Lab" page), you MUST also append
|
||||
// it here, or it will be unreachable on phones at ≤768px (the
|
||||
// hamburger is hidden at that breakpoint — see bottom-nav.css).
|
||||
// it here, or it will be unreachable on phones at <=768px (the
|
||||
// hamburger is hidden at that breakpoint -- see bottom-nav.css).
|
||||
var MORE_ROUTES = [
|
||||
{ route: 'nodes', hash: '#/nodes', label: 'Nodes', icon: '🖥️' },
|
||||
{ route: 'tools', hash: '#/tools', label: 'Tools', icon: '🛠️' },
|
||||
{ route: 'observers', hash: '#/observers', label: 'Observers', icon: '👁️' },
|
||||
{ route: 'analytics', hash: '#/analytics', label: 'Analytics', icon: '📊' },
|
||||
{ route: 'perf', hash: '#/perf', label: 'Perf', icon: '⚡' },
|
||||
{ route: 'audio-lab', hash: '#/audio-lab', label: 'Audio Lab', icon: '🎵' },
|
||||
{ route: 'nodes', hash: '#/nodes', label: 'Nodes', ph: 'monitor' },
|
||||
{ route: 'tools', hash: '#/tools', label: 'Tools', ph: 'wrench' },
|
||||
{ route: 'observers', hash: '#/observers', label: 'Observers', ph: 'eye' },
|
||||
{ route: 'analytics', hash: '#/analytics', label: 'Analytics', ph: 'chart-bar' },
|
||||
{ route: 'perf', hash: '#/perf', label: 'Perf', ph: 'lightning' },
|
||||
{ route: 'audio-lab', hash: '#/audio-lab', label: 'Audio Lab', ph: 'music-note' },
|
||||
];
|
||||
|
||||
var SHEET_ID = 'bottomNavMoreSheet';
|
||||
@@ -115,7 +124,7 @@
|
||||
var ic = document.createElement('span');
|
||||
ic.className = 'bottom-nav-icon';
|
||||
ic.setAttribute('aria-hidden', 'true');
|
||||
ic.textContent = t.icon;
|
||||
ic.innerHTML = phIconHTML(t.ph);
|
||||
|
||||
var lb = document.createElement('span');
|
||||
lb.className = 'bottom-nav-label';
|
||||
@@ -202,7 +211,7 @@
|
||||
var ic = document.createElement('span');
|
||||
ic.className = 'bottom-nav-sheet-icon';
|
||||
ic.setAttribute('aria-hidden', 'true');
|
||||
ic.textContent = r.icon;
|
||||
ic.innerHTML = phIconHTML(r.ph);
|
||||
|
||||
var lb = document.createElement('span');
|
||||
lb.className = 'bottom-nav-sheet-label';
|
||||
@@ -262,7 +271,7 @@
|
||||
var btn = document.querySelector('[data-bottom-nav-dark-toggle]');
|
||||
if (!btn) return;
|
||||
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
btn.querySelector('.bottom-nav-sheet-icon').textContent = isDark ? '🌙' : '☀️';
|
||||
btn.querySelector('.bottom-nav-sheet-icon').innerHTML = phIconHTML(isDark ? 'moon' : 'sun');
|
||||
btn.querySelector('.bottom-nav-sheet-label').textContent = isDark ? 'Light mode' : 'Dark mode';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Phosphor Icon Sprite
|
||||
|
||||
This directory holds the vendored Phosphor Icons sprite used by the CoreScope
|
||||
frontend. We do **not** ship the Phosphor webfont (~150 KB) or fetch icons from
|
||||
a CDN at runtime — every icon used by the UI is bundled here.
|
||||
|
||||
## File layout
|
||||
|
||||
- `phosphor-sprite.svg` — single SVG sprite, one `<symbol id="ph-NAME">` per icon
|
||||
(regular weight, `viewBox="0 0 256 256"` to match Phosphor's native grid).
|
||||
|
||||
## Markup pattern
|
||||
|
||||
```html
|
||||
<svg class="ph-icon" aria-hidden="true" focusable="false">
|
||||
<use href="/icons/phosphor-sprite.svg#ph-magnifying-glass"></use>
|
||||
</svg>
|
||||
```
|
||||
|
||||
CSS helper (defined in `public/style.css`):
|
||||
|
||||
```css
|
||||
.ph-icon { width: 1em; height: 1em; vertical-align: -0.125em; fill: currentColor; }
|
||||
```
|
||||
|
||||
Icons inherit color via `currentColor` and size via the surrounding font-size,
|
||||
so they re-theme automatically with light/dark mode and CSS variables.
|
||||
|
||||
## Adding a new icon
|
||||
|
||||
1. Pull the regular-weight SVG from
|
||||
<https://cdn.jsdelivr.net/npm/@phosphor-icons/core@2.1.1/assets/regular/NAME.svg>
|
||||
(or `assets/fill/NAME.svg` for the rare filled-circle / star-fill cases).
|
||||
2. Append a `<symbol id="ph-NAME" viewBox="0 0 256 256">…</symbol>` to
|
||||
`phosphor-sprite.svg`. Strip the outer `<svg>` wrapper and any `fill=` attrs
|
||||
on the inner `<path>` (we want `currentColor` from the parent).
|
||||
3. Reference it with `<use href="/icons/phosphor-sprite.svg#ph-NAME"></use>`.
|
||||
|
||||
## Weight policy
|
||||
|
||||
**Regular weight only**, with two filled exceptions allowed for status dots and
|
||||
star-favorite (`circle-fill`, `star-fill`, `square-fill`). Bold/duotone are
|
||||
reserved for a future design pass — do not introduce them ad hoc.
|
||||
|
||||
## Lint plan (M6)
|
||||
|
||||
A `make lint-no-emoji` target will grep `public/**` for codepoints in
|
||||
`U+1F300–U+1FAFF`, `U+2600–U+27BF`, `U+2700–U+27BF` and the misc-symbols set
|
||||
(`◆●■▲★☆○✓✗⚠✉`) outside an allowlist (channel-name strings, log/error text,
|
||||
test fixtures). Until that lands, run the audit script in
|
||||
`scripts/audit-emoji.py` (added in #1648).
|
||||
@@ -0,0 +1,36 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||
<symbol id="ph-arrow-clockwise" viewBox="0 0 256 256"><path d="M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z"/></symbol>
|
||||
<symbol id="ph-broadcast" viewBox="0 0 256 256"><path d="M128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Zm73.71,7.14a80,80,0,0,1-14.08,22.2,8,8,0,0,1-11.92-10.67,63.95,63.95,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67,80.08,80.08,0,0,1,14.08,84.47ZM69,103.09a64,64,0,0,0,11.26,67.58,8,8,0,0,1-11.92,10.67,79.93,79.93,0,0,1,0-106.67A8,8,0,1,1,80.29,85.34,63.77,63.77,0,0,0,69,103.09ZM248,128a119.58,119.58,0,0,1-34.29,84,8,8,0,1,1-11.42-11.2,103.9,103.9,0,0,0,0-145.56A8,8,0,1,1,213.71,44,119.58,119.58,0,0,1,248,128ZM53.71,200.78A8,8,0,1,1,42.29,212a119.87,119.87,0,0,1,0-168,8,8,0,1,1,11.42,11.2,103.9,103.9,0,0,0,0,145.56Z"/></symbol>
|
||||
<symbol id="ph-chart-bar" viewBox="0 0 256 256"><path d="M224,200h-8V40a8,8,0,0,0-8-8H152a8,8,0,0,0-8,8V80H96a8,8,0,0,0-8,8v40H48a8,8,0,0,0-8,8v64H32a8,8,0,0,0,0,16H224a8,8,0,0,0,0-16ZM160,48h40V200H160ZM104,96h40V200H104ZM56,144H88v56H56Z"/></symbol>
|
||||
<symbol id="ph-chat-circle" viewBox="0 0 256 256"><path d="M128,24A104,104,0,0,0,36.18,176.88L24.83,210.93a16,16,0,0,0,20.24,20.24l34.05-11.35A104,104,0,1,0,128,24Zm0,192a87.87,87.87,0,0,1-44.06-11.81,8,8,0,0,0-6.54-.67L40,216,52.47,178.6a8,8,0,0,0-.66-6.54A88,88,0,1,1,128,216Z"/></symbol>
|
||||
<symbol id="ph-check" viewBox="0 0 256 256"><path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"/></symbol>
|
||||
<symbol id="ph-circle-fill" viewBox="0 0 256 256"><path d="M232,128A104,104,0,1,1,128,24,104.13,104.13,0,0,1,232,128Z"/></symbol>
|
||||
<symbol id="ph-circle" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Z"/></symbol>
|
||||
<symbol id="ph-cpu" viewBox="0 0 256 256"><path d="M152,96H104a8,8,0,0,0-8,8v48a8,8,0,0,0,8,8h48a8,8,0,0,0,8-8V104A8,8,0,0,0,152,96Zm-8,48H112V112h32Zm88,0H216V112h16a8,8,0,0,0,0-16H216V56a16,16,0,0,0-16-16H160V24a8,8,0,0,0-16,0V40H112V24a8,8,0,0,0-16,0V40H56A16,16,0,0,0,40,56V96H24a8,8,0,0,0,0,16H40v32H24a8,8,0,0,0,0,16H40v40a16,16,0,0,0,16,16H96v16a8,8,0,0,0,16,0V216h32v16a8,8,0,0,0,16,0V216h40a16,16,0,0,0,16-16V160h16a8,8,0,0,0,0-16Zm-32,56H56V56H200v95.87s0,.09,0,.13,0,.09,0,.13V200Z"/></symbol>
|
||||
<symbol id="ph-diamond" viewBox="0 0 256 256"><path d="M235.33,116.72,139.28,20.66a16,16,0,0,0-22.56,0l-96,96.06a16,16,0,0,0,0,22.56l96.05,96.06h0a16,16,0,0,0,22.56,0l96.05-96.06a16,16,0,0,0,0-22.56ZM128,224h0L32,128,128,32,224,128Z"/></symbol>
|
||||
<symbol id="ph-envelope-simple" viewBox="0 0 256 256"><path d="M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48ZM203.43,64,128,133.15,52.57,64ZM216,192H40V74.19l82.59,75.71a8,8,0,0,0,10.82,0L216,74.19V192Z"/></symbol>
|
||||
<symbol id="ph-eye" viewBox="0 0 256 256"><path d="M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z"/></symbol>
|
||||
<symbol id="ph-hash" viewBox="0 0 256 256"><path d="M224,88H175.4l8.47-46.57a8,8,0,0,0-15.74-2.86l-9,49.43H111.4l8.47-46.57a8,8,0,0,0-15.74-2.86L95.14,88H48a8,8,0,0,0,0,16H92.23L83.5,152H32a8,8,0,0,0,0,16H80.6l-8.47,46.57a8,8,0,0,0,6.44,9.3A7.79,7.79,0,0,0,80,224a8,8,0,0,0,7.86-6.57l9-49.43H144.6l-8.47,46.57a8,8,0,0,0,6.44,9.3A7.79,7.79,0,0,0,144,224a8,8,0,0,0,7.86-6.57l9-49.43H208a8,8,0,0,0,0-16H163.77l8.73-48H224a8,8,0,0,0,0-16Zm-76.5,64H99.77l8.73-48h47.73Z"/></symbol>
|
||||
<symbol id="ph-house" viewBox="0 0 256 256"><path d="M219.31,108.68l-80-80a16,16,0,0,0-22.62,0l-80,80A15.87,15.87,0,0,0,32,120v96a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V160h32v56a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V120A15.87,15.87,0,0,0,219.31,108.68ZM208,208H160V152a8,8,0,0,0-8-8H104a8,8,0,0,0-8,8v56H48V120l80-80,80,80Z"/></symbol>
|
||||
<symbol id="ph-lightning" viewBox="0 0 256 256"><path d="M215.79,118.17a8,8,0,0,0-5-5.66L153.18,90.9l14.66-73.33a8,8,0,0,0-13.69-7l-112,120a8,8,0,0,0,3,13l57.63,21.61L88.16,238.43a8,8,0,0,0,13.69,7l112-120A8,8,0,0,0,215.79,118.17ZM109.37,214l10.47-52.38a8,8,0,0,0-5-9.06L62,132.71l84.62-90.66L136.16,94.43a8,8,0,0,0,5,9.06l52.8,19.8Z"/></symbol>
|
||||
<symbol id="ph-list" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"/></symbol>
|
||||
<symbol id="ph-lock" viewBox="0 0 256 256"><path d="M208,80H176V56a48,48,0,0,0-96,0V80H48A16,16,0,0,0,32,96V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V96A16,16,0,0,0,208,80ZM96,56a32,32,0,0,1,64,0V80H96ZM208,208H48V96H208V208Zm-68-56a12,12,0,1,1-12-12A12,12,0,0,1,140,152Z"/></symbol>
|
||||
<symbol id="ph-magnifying-glass" viewBox="0 0 256 256"><path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/></symbol>
|
||||
<symbol id="ph-map-trifold" viewBox="0 0 256 256"><path d="M228.92,49.69a8,8,0,0,0-6.86-1.45L160.93,63.52,99.58,32.84a8,8,0,0,0-5.52-.6l-64,16A8,8,0,0,0,24,56V200a8,8,0,0,0,9.94,7.76l61.13-15.28,61.35,30.68A8.15,8.15,0,0,0,160,224a8,8,0,0,0,1.94-.24l64-16A8,8,0,0,0,232,200V56A8,8,0,0,0,228.92,49.69ZM104,52.94l48,24V203.06l-48-24ZM40,62.25l48-12v127.5l-48,12Zm176,131.5-48,12V78.25l48-12Z"/></symbol>
|
||||
<symbol id="ph-megaphone" viewBox="0 0 256 256"><path d="M248,120a48.05,48.05,0,0,0-48-48H160.2c-2.91-.17-53.62-3.74-101.91-44.24A16,16,0,0,0,32,40V200a16,16,0,0,0,26.29,12.25c37.77-31.68,77-40.76,93.71-43.3v31.72A16,16,0,0,0,159.12,214l11,7.33A16,16,0,0,0,194.5,212l11.77-44.36A48.07,48.07,0,0,0,248,120ZM48,199.93V40h0c42.81,35.91,86.63,45,104,47.24v65.48C134.65,155,90.84,164.07,48,199.93Zm131,8,0,.11-11-7.33V168h21.6ZM200,152H168V88h32a32,32,0,1,1,0,64Z"/></symbol>
|
||||
<symbol id="ph-monitor" viewBox="0 0 256 256"><path d="M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Z"/></symbol>
|
||||
<symbol id="ph-moon" viewBox="0 0 256 256"><path d="M233.54,142.23a8,8,0,0,0-8-2,88.08,88.08,0,0,1-109.8-109.8,8,8,0,0,0-10-10,104.84,104.84,0,0,0-52.91,37A104,104,0,0,0,136,224a103.09,103.09,0,0,0,62.52-20.88,104.84,104.84,0,0,0,37-52.91A8,8,0,0,0,233.54,142.23ZM188.9,190.34A88,88,0,0,1,65.66,67.11a89,89,0,0,1,31.4-26A106,106,0,0,0,96,56,104.11,104.11,0,0,0,200,160a106,106,0,0,0,14.92-1.06A89,89,0,0,1,188.9,190.34Z"/></symbol>
|
||||
<symbol id="ph-music-note" viewBox="0 0 256 256"><path d="M210.3,56.34l-80-24A8,8,0,0,0,120,40V148.26A48,48,0,1,0,136,184V98.75l69.7,20.91A8,8,0,0,0,216,112V64A8,8,0,0,0,210.3,56.34ZM88,216a32,32,0,1,1,32-32A32,32,0,0,1,88,216ZM200,101.25l-64-19.2V50.75L200,70Z"/></symbol>
|
||||
<symbol id="ph-package" viewBox="0 0 256 256"><path d="M223.68,66.15,135.68,18a15.88,15.88,0,0,0-15.36,0l-88,48.17a16,16,0,0,0-8.32,14v95.64a16,16,0,0,0,8.32,14l88,48.17a15.88,15.88,0,0,0,15.36,0l88-48.17a16,16,0,0,0,8.32-14V80.18A16,16,0,0,0,223.68,66.15ZM128,32l80.34,44-29.77,16.3-80.35-44ZM128,120,47.66,76l33.9-18.56,80.34,44ZM40,90l80,43.78v85.79L40,175.82Zm176,85.78h0l-80,43.79V133.82l32-17.51V152a8,8,0,0,0,16,0V107.55L216,90v85.77Z"/></symbol>
|
||||
<symbol id="ph-palette" viewBox="0 0 256 256"><path d="M200.77,53.89A103.27,103.27,0,0,0,128,24h-1.07A104,104,0,0,0,24,128c0,43,26.58,79.06,69.36,94.17A32,32,0,0,0,136,192a16,16,0,0,1,16-16h46.21a31.81,31.81,0,0,0,31.2-24.88,104.43,104.43,0,0,0,2.59-24A103.28,103.28,0,0,0,200.77,53.89Zm13,93.71A15.89,15.89,0,0,1,198.21,160H152a32,32,0,0,0-32,32,16,16,0,0,1-21.31,15.07C62.49,194.3,40,164,40,128a88,88,0,0,1,87.09-88h.9a88.35,88.35,0,0,1,88,87.25A88.86,88.86,0,0,1,213.81,147.6ZM140,76a12,12,0,1,1-12-12A12,12,0,0,1,140,76ZM96,100A12,12,0,1,1,84,88,12,12,0,0,1,96,100Zm0,56a12,12,0,1,1-12-12A12,12,0,0,1,96,156Zm88-56a12,12,0,1,1-12-12A12,12,0,0,1,184,100Z"/></symbol>
|
||||
<symbol id="ph-scales" viewBox="0 0 256 256"><path d="M239.43,133l-32-80h0a8,8,0,0,0-9.16-4.84L136,62V40a8,8,0,0,0-16,0V65.58L54.26,80.19A8,8,0,0,0,48.57,85h0v.06L16.57,165a7.92,7.92,0,0,0-.57,3c0,23.31,24.54,32,40,32s40-8.69,40-32a7.92,7.92,0,0,0-.57-3L66.92,93.77,120,82V208H104a8,8,0,0,0,0,16h48a8,8,0,0,0,0-16H136V78.42L187,67.1,160.57,133a7.92,7.92,0,0,0-.57,3c0,23.31,24.54,32,40,32s40-8.69,40-32A7.92,7.92,0,0,0,239.43,133ZM56,184c-7.53,0-22.76-3.61-23.93-14.64L56,109.54l23.93,59.82C78.76,180.39,63.53,184,56,184Zm144-32c-7.53,0-22.76-3.61-23.93-14.64L200,77.54l23.93,59.82C222.76,148.39,207.53,152,200,152Z"/></symbol>
|
||||
<symbol id="ph-square-fill" viewBox="0 0 256 256"><path d="M224,48V208a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32H208A16,16,0,0,1,224,48Z"/></symbol>
|
||||
<symbol id="ph-square" viewBox="0 0 256 256"><path d="M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Zm0,176H48V48H208V208Z"/></symbol>
|
||||
<symbol id="ph-star-fill" viewBox="0 0 256 256"><path d="M234.29,114.85l-45,38.83L203,211.75a16.4,16.4,0,0,1-24.5,17.82L128,198.49,77.47,229.57A16.4,16.4,0,0,1,53,211.75l13.76-58.07-45-38.83A16.46,16.46,0,0,1,31.08,86l59-4.76,22.76-55.08a16.36,16.36,0,0,1,30.27,0l22.75,55.08,59,4.76a16.46,16.46,0,0,1,9.37,28.86Z"/></symbol>
|
||||
<symbol id="ph-star" viewBox="0 0 256 256"><path d="M239.18,97.26A16.38,16.38,0,0,0,224.92,86l-59-4.76L143.14,26.15a16.36,16.36,0,0,0-30.27,0L90.11,81.23,31.08,86a16.46,16.46,0,0,0-9.37,28.86l45,38.83L53,211.75a16.38,16.38,0,0,0,24.5,17.82L128,198.49l50.53,31.08A16.4,16.4,0,0,0,203,211.75l-13.76-58.07,45-38.83A16.43,16.43,0,0,0,239.18,97.26Zm-15.34,5.47-48.7,42a8,8,0,0,0-2.56,7.91l14.88,62.8a.37.37,0,0,1-.17.48c-.18.14-.23.11-.38,0l-54.72-33.65a8,8,0,0,0-8.38,0L69.09,215.94c-.15.09-.19.12-.38,0a.37.37,0,0,1-.17-.48l14.88-62.8a8,8,0,0,0-2.56-7.91l-48.7-42c-.12-.1-.23-.19-.13-.5s.18-.27.33-.29l63.92-5.16A8,8,0,0,0,103,91.86l24.62-59.61c.08-.17.11-.25.35-.25s.27.08.35.25L153,91.86a8,8,0,0,0,6.75,4.92l63.92,5.16c.15,0,.24,0,.33.29S224,102.63,223.84,102.73Z"/></symbol>
|
||||
<symbol id="ph-sun" viewBox="0 0 256 256"><path d="M120,40V16a8,8,0,0,1,16,0V40a8,8,0,0,1-16,0Zm72,88a64,64,0,1,1-64-64A64.07,64.07,0,0,1,192,128Zm-16,0a48,48,0,1,0-48,48A48.05,48.05,0,0,0,176,128ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-16-16A8,8,0,0,0,42.34,53.66Zm0,116.68-16,16a8,8,0,0,0,11.32,11.32l16-16a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l16-16a8,8,0,0,0-11.32-11.32l-16,16A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32-11.32ZM48,128a8,8,0,0,0-8-8H16a8,8,0,0,0,0,16H40A8,8,0,0,0,48,128Zm80,80a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V216A8,8,0,0,0,128,208Zm112-88H216a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16Z"/></symbol>
|
||||
<symbol id="ph-triangle" viewBox="0 0 256 256"><path d="M236.8,188.09,149.35,36.22a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.34,24.34,0,0,0,40.55,224h174.9a24.34,24.34,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8Z"/></symbol>
|
||||
<symbol id="ph-warning" viewBox="0 0 256 256"><path d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"/></symbol>
|
||||
<symbol id="ph-wrench" viewBox="0 0 256 256"><path d="M226.76,69a8,8,0,0,0-12.84-2.88l-40.3,37.19-17.23-3.7-3.7-17.23,37.19-40.3A8,8,0,0,0,187,29.24,72,72,0,0,0,88,96,72.34,72.34,0,0,0,94,124.94L33.79,177c-.15.12-.29.26-.43.39a32,32,0,0,0,45.26,45.26c.13-.13.27-.28.39-.42L131.06,162A72,72,0,0,0,232,96,71.56,71.56,0,0,0,226.76,69ZM160,152a56.14,56.14,0,0,1-27.07-7,8,8,0,0,0-9.92,1.77L67.11,211.51a16,16,0,0,1-22.62-22.62L109.18,133a8,8,0,0,0,1.77-9.93,56,56,0,0,1,58.36-82.31l-31.2,33.81a8,8,0,0,0-1.94,7.1L141.83,108a8,8,0,0,0,6.14,6.14l26.35,5.66a8,8,0,0,0,7.1-1.94l33.81-31.2A56.06,56.06,0,0,1,160,152Z"/></symbol>
|
||||
<symbol id="ph-x" viewBox="0 0 256 256"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
+8
-8
@@ -120,14 +120,14 @@
|
||||
<a href="#/home" class="nav-link" data-route="home" data-priority="high">Home</a>
|
||||
<a href="#/packets" class="nav-link" data-route="packets" data-priority="high">Packets</a>
|
||||
<a href="#/map" class="nav-link" data-route="map" data-priority="high">Map</a>
|
||||
<a href="#/live" class="nav-link" data-route="live" data-priority="high">🔴 Live</a>
|
||||
<a href="#/live" class="nav-link" data-route="live" data-priority="high"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-broadcast"></use></svg> Live</a>
|
||||
<a href="#/channels" class="nav-link" data-route="channels">Channels</a>
|
||||
<a href="#/nodes" class="nav-link" data-route="nodes" data-priority="high">Nodes</a>
|
||||
<a href="#/tools" class="nav-link" data-route="tools">Tools</a>
|
||||
<a href="#/observers" class="nav-link" data-route="observers">Observers</a>
|
||||
<a href="#/analytics" class="nav-link" data-route="analytics">Analytics</a>
|
||||
<a href="#/perf" class="nav-link" data-route="perf">⚡ Perf</a>
|
||||
<a href="#/audio-lab" class="nav-link" data-route="audio-lab">🎵 Lab</a>
|
||||
<a href="#/perf" class="nav-link" data-route="perf"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-lightning"></use></svg> Perf</a>
|
||||
<a href="#/audio-lab" class="nav-link" data-route="audio-lab"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-music-note"></use></svg> Lab</a>
|
||||
</div>
|
||||
<div class="nav-more-wrap">
|
||||
<button class="nav-btn nav-more-btn" id="navMoreBtn" aria-haspopup="true" aria-expanded="false" aria-controls="navMoreMenu" title="More pages">More ▾</button>
|
||||
@@ -140,17 +140,17 @@
|
||||
<button class="nav-btn" id="favToggle" title="Favorites">⭐</button>
|
||||
<div class="nav-fav-dropdown" id="favDropdown"></div>
|
||||
</div>
|
||||
<button class="nav-btn" id="searchToggle" title="Search (Ctrl+K)">🔍</button>
|
||||
<button class="nav-btn" id="customizeToggle" title="Customize theme & branding">🎨</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>
|
||||
<button class="nav-btn" id="customizeToggle" title="Customize theme & branding" aria-label="Customize"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-palette"></use></svg></button>
|
||||
<label class="theme-toggle" id="darkModeToggle" title="Toggle dark mode">
|
||||
<input type="checkbox" id="darkModeCheckbox" role="switch" aria-label="Toggle dark mode">
|
||||
<span class="theme-toggle-track" aria-hidden="true">
|
||||
<span class="theme-toggle-thumb"></span>
|
||||
<span class="theme-toggle-icon theme-toggle-sun">☀️</span>
|
||||
<span class="theme-toggle-icon theme-toggle-moon">🌙</span>
|
||||
<span class="theme-toggle-icon theme-toggle-sun"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-sun"></use></svg></span>
|
||||
<span class="theme-toggle-icon theme-toggle-moon"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-moon"></use></svg></span>
|
||||
</span>
|
||||
</label>
|
||||
<button class="nav-btn hamburger" id="hamburger" title="Menu" aria-label="Toggle navigation menu">☰</button>
|
||||
<button class="nav-btn hamburger" id="hamburger" title="Menu" aria-label="Toggle navigation menu"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-list"></use></svg></button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -152,8 +152,8 @@
|
||||
|
||||
const mirrors = [
|
||||
{ id: 'favToggle', icon: '⭐', label: 'Favorites' },
|
||||
{ id: 'searchToggle', icon: '🔍', label: 'Search' },
|
||||
{ id: 'customizeToggle', icon: '🎨', label: 'Customize' },
|
||||
{ id: 'searchToggle', ph: 'magnifying-glass', label: 'Search' },
|
||||
{ id: 'customizeToggle', ph: 'palette', label: 'Customize' },
|
||||
];
|
||||
|
||||
const sep = sheet.querySelector('.bottom-nav-sheet-sep');
|
||||
@@ -169,7 +169,14 @@
|
||||
const ic = document.createElement('span');
|
||||
ic.className = 'bottom-nav-sheet-icon';
|
||||
ic.setAttribute('aria-hidden', 'true');
|
||||
ic.textContent = m.icon;
|
||||
if (m.ph) {
|
||||
// #1648 M1 — Phosphor sprite ref.
|
||||
ic.innerHTML =
|
||||
'<svg class="ph-icon" aria-hidden="true" focusable="false">' +
|
||||
'<use href="/icons/phosphor-sprite.svg#ph-' + m.ph + '"></use></svg>';
|
||||
} else {
|
||||
ic.textContent = m.icon;
|
||||
}
|
||||
|
||||
const lb = document.createElement('span');
|
||||
lb.className = 'bottom-nav-sheet-label';
|
||||
|
||||
+14
-8
@@ -54,16 +54,22 @@
|
||||
var prevFocus = null;
|
||||
|
||||
// Long-tail routes mirror PR #1174 / bottom-nav.js MORE_ROUTES exactly.
|
||||
// ⚠️ Keep in sync with public/bottom-nav.js MORE_ROUTES.
|
||||
// !! Keep in sync with public/bottom-nav.js MORE_ROUTES.
|
||||
// `ph` is the Phosphor icon id (no "ph-" prefix); see public/icons/phosphor-sprite.svg.
|
||||
var ROUTES = [
|
||||
{ route: 'nodes', hash: '#/nodes', label: 'Nodes', icon: '🖥️' },
|
||||
{ route: 'tools', hash: '#/tools', label: 'Tools', icon: '🛠️' },
|
||||
{ route: 'observers', hash: '#/observers', label: 'Observers', icon: '👁️' },
|
||||
{ route: 'analytics', hash: '#/analytics', label: 'Analytics', icon: '📊' },
|
||||
{ route: 'perf', hash: '#/perf', label: 'Perf', icon: '⚡' },
|
||||
{ route: 'audio-lab', hash: '#/audio-lab', label: 'Audio Lab', icon: '🎵' },
|
||||
{ route: 'nodes', hash: '#/nodes', label: 'Nodes', ph: 'monitor' },
|
||||
{ route: 'tools', hash: '#/tools', label: 'Tools', ph: 'wrench' },
|
||||
{ route: 'observers', hash: '#/observers', label: 'Observers', ph: 'eye' },
|
||||
{ route: 'analytics', hash: '#/analytics', label: 'Analytics', ph: 'chart-bar' },
|
||||
{ route: 'perf', hash: '#/perf', label: 'Perf', ph: 'lightning' },
|
||||
{ route: 'audio-lab', hash: '#/audio-lab', label: 'Audio Lab', ph: 'music-note' },
|
||||
];
|
||||
|
||||
function phIconHTML(name) {
|
||||
return '<svg class="ph-icon" aria-hidden="true" focusable="false">' +
|
||||
'<use href="/icons/phosphor-sprite.svg#ph-' + name + '"></use></svg>';
|
||||
}
|
||||
|
||||
var EDGE_PX = 44; // pointerdown must start within left N px (drawer trigger zone)
|
||||
var EDGE_MIN_PX = 24; // first N px reserved for iOS Safari back-swipe (do not claim)
|
||||
var NARROW_MAX = 768; // Option A: disabled at ≤ this width
|
||||
@@ -130,7 +136,7 @@
|
||||
var ic = document.createElement('span');
|
||||
ic.className = 'nav-drawer-icon';
|
||||
ic.setAttribute('aria-hidden', 'true');
|
||||
ic.textContent = r.icon;
|
||||
ic.innerHTML = phIconHTML(r.ph);
|
||||
|
||||
var lb = document.createElement('span');
|
||||
lb.className = 'nav-drawer-label';
|
||||
|
||||
@@ -97,7 +97,7 @@ window.ObserverDetailNaiveBanner = {
|
||||
</select>
|
||||
<button type="button" data-action="compare-with-go" class="btn-secondary" disabled aria-disabled="true"
|
||||
title="Open side-by-side comparison">
|
||||
<span aria-hidden="true">🔍</span><span>Compare</span>
|
||||
<svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-magnifying-glass"></use></svg><span>Compare</span>
|
||||
</button>
|
||||
</span>
|
||||
<select id="obsDaysSelect" class="time-range-select" aria-label="Time range">
|
||||
@@ -210,7 +210,7 @@ window.ObserverDetailNaiveBanner = {
|
||||
<div class="obs-info-grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:20px">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Status</div>
|
||||
<div class="stat-value"><span class="health-dot ${statusCls}">●</span> ${statusLabel}</div>
|
||||
<div class="stat-value"><span class="health-dot ${statusCls}"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-circle-fill"></use></svg></span> ${statusLabel}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Relay</div>
|
||||
|
||||
+19
-13
@@ -1,7 +1,7 @@
|
||||
/* === CoreScope — observers.js === */
|
||||
'use strict';
|
||||
|
||||
// Issue #1478 — naive-clock ⚠️ chip.
|
||||
// Issue #1478 — naive-clock warning chip.
|
||||
// Exposed as a window global so the renderer (and a jsdom-style test) can
|
||||
// call it without depending on the IIFE-scoped helpers below. Returns the
|
||||
// chip HTML when the observer's clock is currently flagged naive, or "" when
|
||||
@@ -97,10 +97,10 @@ window.ObserversSummary = (function () {
|
||||
}
|
||||
return ''
|
||||
+ '<div class="obs-summary">'
|
||||
+ '<span class="obs-stat"><span class="health-dot health-green">\u25CF</span> ' + c.online + ' Online</span>'
|
||||
+ '<span class="obs-stat"><span class="health-dot health-yellow">\u25B2</span> ' + c.stale + ' Stale</span>'
|
||||
+ '<span class="obs-stat"><span class="health-dot health-red">\u2715</span> ' + c.offline + ' Offline</span>'
|
||||
+ '<span class="obs-stat">\uD83D\uDCE1 ' + c.total + ' Total</span>'
|
||||
+ '<span class="obs-stat"><span class="health-dot health-green"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-circle-fill"></use></svg></span> ' + c.online + ' Online</span>'
|
||||
+ '<span class="obs-stat"><span class="health-dot health-yellow"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-triangle"></use></svg></span> ' + c.stale + ' Stale</span>'
|
||||
+ '<span class="obs-stat"><span class="health-dot health-red"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-x"></use></svg></span> ' + c.offline + ' Offline</span>'
|
||||
+ '<span class="obs-stat"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-broadcast"></use></svg> ' + c.total + ' Total</span>'
|
||||
+ updatedHtml
|
||||
+ '</div>';
|
||||
}
|
||||
@@ -151,16 +151,16 @@ window.preserveCompareSelection = function preserveCompareSelection(prevIds, tbo
|
||||
<button type="button" class="btn-secondary" data-action="compare-observers"
|
||||
title="Compare two observers side-by-side"
|
||||
aria-label="Compare observers">
|
||||
<span aria-hidden="true">🔍</span><span>Compare observers</span>
|
||||
<svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-magnifying-glass"></use></svg><span>Compare observers</span>
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" data-action="compare-selected"
|
||||
title="Select exactly two rows to compare"
|
||||
aria-label="Compare selected observers"
|
||||
aria-disabled="true" disabled>
|
||||
<span aria-hidden="true">⚖️</span><span>Compare selected (<span data-role="compare-count">0</span>)</span>
|
||||
<svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-scales"></use></svg><span>Compare selected (<span data-role="compare-count">0</span>)</span>
|
||||
</button>
|
||||
<span class="obs-refresh-spacer"></span>
|
||||
<button class="btn-icon" data-action="obs-refresh" title="Refresh" aria-label="Refresh observers">🔄</button>
|
||||
<button class="btn-icon" data-action="obs-refresh" title="Refresh" aria-label="Refresh observers"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-arrow-clockwise"></use></svg></button>
|
||||
</div>
|
||||
<div id="obsRegionFilter" class="region-filter-container"></div>
|
||||
<div id="obsContent"><div class="text-center text-muted" style="padding:40px">Loading…</div></div>
|
||||
@@ -294,12 +294,14 @@ window.preserveCompareSelection = function preserveCompareSelection(prevIds, tbo
|
||||
window.observerHealthStatus = healthStatus;
|
||||
|
||||
function packetBadge(o) {
|
||||
if (!o.last_packet_at) return '<span title="No packets ever observed">📡⚠ never</span>';
|
||||
const warnIcon = '<svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-warning"></use></svg>';
|
||||
const broadcastIcon = '<svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-broadcast"></use></svg>';
|
||||
if (!o.last_packet_at) return '<span title="No packets ever observed">' + broadcastIcon + warnIcon + ' never</span>';
|
||||
const pktAgo = Date.now() - new Date(o.last_packet_at).getTime();
|
||||
const statusAgo = o.last_seen ? Date.now() - new Date(o.last_seen).getTime() : Infinity;
|
||||
const gap = pktAgo - statusAgo;
|
||||
if (gap > 600000) {
|
||||
return `<span title="Last packet ${timeAgo(o.last_packet_at)} — status is newer by ${Math.round(gap/60000)}min. Observer may be alive but not forwarding packets.">📡⚠ ${timeAgo(o.last_packet_at)}</span>`;
|
||||
return `<span title="Last packet ${timeAgo(o.last_packet_at)} — status is newer by ${Math.round(gap/60000)}min. Observer may be alive but not forwarding packets.">${broadcastIcon}${warnIcon} ${timeAgo(o.last_packet_at)}</span>`;
|
||||
}
|
||||
return timeAgo(o.last_packet_at);
|
||||
}
|
||||
@@ -363,7 +365,11 @@ window.preserveCompareSelection = function preserveCompareSelection(prevIds, tbo
|
||||
</tr></thead>
|
||||
<tbody>${filtered.map(o => {
|
||||
const h = healthStatus(o.last_seen);
|
||||
const shape = h.cls === 'health-green' ? '●' : h.cls === 'health-yellow' ? '▲' : '✕';
|
||||
const shapeIcon = h.cls === 'health-green'
|
||||
? '<svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-circle-fill"></use></svg>'
|
||||
: h.cls === 'health-yellow'
|
||||
? '<svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-triangle"></use></svg>'
|
||||
: '<svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-x"></use></svg>';
|
||||
// TableSort reads NaN-sortable raw values from each cell's
|
||||
// data-value attr (table-sort.js comparators.numeric/time sort
|
||||
// NaN last). Empty string ⇒ NaN ⇒ missing-sorts-last.
|
||||
@@ -381,7 +387,7 @@ window.preserveCompareSelection = function preserveCompareSelection(prevIds, tbo
|
||||
const _packetCount = (o.packet_count != null) ? o.packet_count : '';
|
||||
const _packetsHour = (o.packetsLastHour != null) ? o.packetsLastHour : '';
|
||||
return `<tr style="cursor:pointer" tabindex="0" role="row" data-action="navigate" data-value="#/observers/${encodeURIComponent(o.id)}" data-observer-id="${escapeHtml(o.id)}" onclick="location.hash='#/observers/${encodeURIComponent(o.id)}'">
|
||||
<td data-value="${_healthRank}"><span class="health-dot ${h.cls}" title="${h.label}">${shape}</span> ${h.label}</td>
|
||||
<td data-value="${_healthRank}"><span class="health-dot ${h.cls}" title="${h.label}">${shapeIcon}</span> ${h.label}</td>
|
||||
<td data-testid="obs-cell-name" data-value="${escapeHtml(String(o.name || o.id))}" class="mono">${escapeHtml(o.name || o.id)}${window.ObserversNaiveChip.render(o)}${o.can_relay === false ? ' <span class="badge-listener" title="Firmware reported repeat:off — listener-only; excluded from path-hop disambiguator (issue #1290)">listener</span>' : (o.can_relay === true ? ' <span class="badge-repeater" title="Firmware reported repeat:on — eligible as a path hop">repeater</span>' : '')}</td>
|
||||
<td data-value="${escapeHtml(o.iata || '')}">${o.iata ? `<span class="badge-region">${o.iata}</span>` : '—'}</td>
|
||||
<td data-value="${_lastSeenMs}">${timeAgo(o.last_seen)}</td>
|
||||
@@ -493,7 +499,7 @@ window.preserveCompareSelection = function preserveCompareSelection(prevIds, tbo
|
||||
content.innerHTML =
|
||||
(naiveChipHTML ? '<div style="margin-bottom:10px">' + naiveChipHTML + ' <span class="text-muted">Clock is naive — per-packet timing clamped to ingest time.</span></div>' : '') +
|
||||
'<dl class="slide-over-dl" style="margin:0;display:grid;grid-template-columns:auto 1fr;gap:6px 12px;font-size:13px">' +
|
||||
'<dt>Status</dt><dd><span class="health-dot ' + h.cls + '">●</span> ' + h.label + '</dd>' +
|
||||
'<dt>Status</dt><dd><span class="health-dot ' + h.cls + '"><svg class="ph-icon" aria-hidden="true" focusable="false"><use href="/icons/phosphor-sprite.svg#ph-circle-fill"></use></svg></span> ' + h.label + '</dd>' +
|
||||
'<dt>Region</dt><dd>' + (o.iata ? '<span class="badge-region">' + o.iata + '</span>' : '—') + '</dd>' +
|
||||
'<dt>Last status</dt><dd>' + timeAgo(o.last_seen) + '</dd>' +
|
||||
'<dt>Last packet</dt><dd>' + (o.last_packet_at ? timeAgo(o.last_packet_at) : '—') + '</dd>' +
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
/* === CoreScope — style.css === */
|
||||
|
||||
/* Phosphor icon sprite helper (#1648 M1).
|
||||
* Icons live in public/icons/phosphor-sprite.svg as <symbol id="ph-NAME">.
|
||||
* Reference with: <svg class="ph-icon"><use href="/icons/phosphor-sprite.svg#ph-NAME"/></svg>
|
||||
* Inherits color via currentColor; sized to 1em so it scales with surrounding text. */
|
||||
.ph-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
/* #1648 M1 follow-up: icons are 1em-sized so they scale with the
|
||||
* surrounding text — but several nav surfaces use a smaller font-size
|
||||
* (top-nav text is ~12.8px), which dropped the rendered icon below the
|
||||
* intended 16px floor. Pin .ph-icon's own font-size to 16px so the
|
||||
* icon scales independently of the parent's text size. */
|
||||
font-size: 16px;
|
||||
vertical-align: -0.125em;
|
||||
fill: currentColor;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Aldrich webfont — used by the navbar logo SVG (issue #1137 follow-up).
|
||||
* Self-hosted woff2 (latin subset from Google Fonts, ~16KB). Only weight
|
||||
* available is 400; the SVG's font-weight="700" synthesizes bold. */
|
||||
|
||||
@@ -25,6 +25,13 @@ if [ -d public/fonts ]; then
|
||||
mkdir -p public-instrumented/fonts
|
||||
cp -r public/fonts/. public-instrumented/fonts/
|
||||
fi
|
||||
# Copy Phosphor icon sprite (#1648 M1). Same SPA-fallback gotcha as /img —
|
||||
# without this, GET /icons/phosphor-sprite.svg returns index.html and every
|
||||
# <use href="/icons/phosphor-sprite.svg#ph-…"> shows a broken icon.
|
||||
if [ -d public/icons ]; then
|
||||
mkdir -p public-instrumented/icons
|
||||
cp -r public/icons/. public-instrumented/icons/
|
||||
fi
|
||||
# Copy vendored libraries unmodified — `nyc instrument` skips subdirectories
|
||||
# without a package.json, so vendor/qrcode.js, vendor/jsqr.min.js, etc. are
|
||||
# never emitted into public-instrumented/. Without them the SPA fallback
|
||||
|
||||
@@ -28,6 +28,7 @@ node test-channel-issue-1087.js
|
||||
node test-issue-1409-no-encrypted-flood.js
|
||||
node test-analytics-channels-integration.js
|
||||
node test-observers-headings.js
|
||||
node test-issue-1648-m1-emoji-scan.js
|
||||
node test-traces.js
|
||||
|
||||
# #1418 — route-view v2 (Tufte) coverage
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env node
|
||||
/* Issue #1648 — M1: emoji → Phosphor sprite migration (unit/static check).
|
||||
*
|
||||
* Asserts that the M1 surfaces (top-nav, bottom-nav, nav-drawer,
|
||||
* mobile-page-actions, observers Compare entries) contain ZERO emoji or
|
||||
* misc-symbol codepoints used as iconography:
|
||||
* - U+1F300–U+1FAFF (Symbols & Pictographs / Supplemental, Symbols & Pictographs)
|
||||
* - U+2600–U+27BF (Misc Symbols, Dingbats)
|
||||
* - Misc-iconography set: ◆●■▲★☆○✓✗⚠✉
|
||||
*
|
||||
* Allowlist: comments and string-literal text NOT used as UI iconography
|
||||
* are still stripped out where they exist as plain code-comments — the
|
||||
* grep operates on the WHOLE file but only the M1-listed lines are required
|
||||
* to be clean. We assert per-symbol on lines tagged in the migration plan.
|
||||
*
|
||||
* Anti-tautology: this test FAILS today (pre-fix) by construction —
|
||||
* each named line currently contains the offending codepoint.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const ROOT = path.resolve(__dirname, 'public');
|
||||
|
||||
// File-by-file expected codepoint-free regions.
|
||||
// We scan the WHOLE file for nav/UI emoji rather than specific lines so
|
||||
// the assertion stays robust to line-number drift.
|
||||
const FILES = [
|
||||
'index.html',
|
||||
'bottom-nav.js',
|
||||
'nav-drawer.js',
|
||||
'mobile-page-actions.js',
|
||||
'observers.js',
|
||||
'observer-detail.js',
|
||||
];
|
||||
|
||||
// Codepoint ranges (emoji proper).
|
||||
const EMOJI = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u;
|
||||
// Misc-symbols used as iconography (per #1648 surprise #5).
|
||||
// EXCLUDES standard quotes/dashes; INCLUDES box-drawing role shapes + check/cross/warn.
|
||||
const MISC_ICON = /[◆●■▲★☆○✓✗⚠✉]/u;
|
||||
|
||||
// Per-file: lines exempt from the misc-icon scan because they are plain
|
||||
// code-comments / sync-warning text rather than UI iconography. Keep tiny.
|
||||
const COMMENT_EXEMPT_SUBSTRINGS = [
|
||||
'MANUAL SYNC REQUIRED',
|
||||
'Keep in sync with',
|
||||
'naive-clock',
|
||||
];
|
||||
|
||||
function scanFile(rel) {
|
||||
const abs = path.join(ROOT, rel);
|
||||
const txt = fs.readFileSync(abs, 'utf8');
|
||||
const lines = txt.split('\n');
|
||||
const hits = [];
|
||||
lines.forEach((line, idx) => {
|
||||
if (EMOJI.test(line)) {
|
||||
hits.push({ file: rel, line: idx + 1, kind: 'emoji', text: line.trim().slice(0, 200) });
|
||||
}
|
||||
if (MISC_ICON.test(line)) {
|
||||
const isExemptComment = COMMENT_EXEMPT_SUBSTRINGS.some(s => line.includes(s));
|
||||
if (!isExemptComment) {
|
||||
hits.push({ file: rel, line: idx + 1, kind: 'misc', text: line.trim().slice(0, 200) });
|
||||
}
|
||||
}
|
||||
});
|
||||
return hits;
|
||||
}
|
||||
|
||||
function assertSpriteWired() {
|
||||
// index.html must reference the Phosphor sprite (either inline <symbol id="ph-…"
|
||||
// or via <use href="…phosphor-sprite.svg#ph-…">).
|
||||
const idx = fs.readFileSync(path.join(ROOT, 'index.html'), 'utf8');
|
||||
const ok = /phosphor-sprite\.svg#ph-/.test(idx) || /<symbol id="ph-/.test(idx);
|
||||
if (!ok) {
|
||||
throw new Error('index.html does not reference the Phosphor sprite (expected phosphor-sprite.svg#ph-… or inline <symbol id="ph-…">)');
|
||||
}
|
||||
}
|
||||
|
||||
function assertSpriteFilePresent() {
|
||||
const sp = path.join(ROOT, 'icons', 'phosphor-sprite.svg');
|
||||
if (!fs.existsSync(sp)) throw new Error(`Phosphor sprite missing at public/icons/phosphor-sprite.svg`);
|
||||
const txt = fs.readFileSync(sp, 'utf8');
|
||||
// Must contain at least the M1 icons.
|
||||
const required = [
|
||||
'ph-broadcast', 'ph-lightning', 'ph-music-note', 'ph-magnifying-glass',
|
||||
'ph-palette', 'ph-sun', 'ph-moon', 'ph-list', 'ph-house', 'ph-package',
|
||||
'ph-map-trifold', 'ph-chat-circle', 'ph-monitor', 'ph-wrench', 'ph-eye',
|
||||
'ph-chart-bar', 'ph-scales', 'ph-arrow-clockwise', 'ph-circle-fill',
|
||||
'ph-triangle', 'ph-x', 'ph-warning',
|
||||
];
|
||||
const missing = required.filter(id => !txt.includes(`id="${id}"`));
|
||||
if (missing.length) throw new Error(`sprite missing symbols: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
let failed = 0;
|
||||
console.log('— Issue #1648 M1 — emoji/misc-icon scan');
|
||||
|
||||
try {
|
||||
assertSpriteFilePresent();
|
||||
console.log(' ✓ sprite file present + has required symbols');
|
||||
} catch (e) {
|
||||
console.error(` ✗ ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
try {
|
||||
assertSpriteWired();
|
||||
console.log(' ✓ index.html references Phosphor sprite');
|
||||
} catch (e) {
|
||||
console.error(` ✗ ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
for (const rel of FILES) {
|
||||
const hits = scanFile(rel);
|
||||
if (hits.length === 0) {
|
||||
console.log(` ✓ ${rel} clean (no emoji / misc-icon codepoints)`);
|
||||
} else {
|
||||
console.error(` ✗ ${rel} has ${hits.length} emoji/misc-icon hit(s):`);
|
||||
for (const h of hits) console.error(` ${h.file}:${h.line} [${h.kind}] ${h.text}`);
|
||||
failed++;
|
||||
}
|
||||
// Behavioral assertion (separate from logging) — scanned region MUST be empty.
|
||||
assert.strictEqual(hits.length, 0,
|
||||
`${rel} must contain zero emoji/misc-icon iconography (got ${hits.length} hit(s))`);
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
console.error(`\nFAIL: ${failed} file(s) still contain emoji/misc-icon iconography`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('\nPASS: all M1 surfaces icon-free');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env node
|
||||
/* Issue #1648 — M1: emoji → Phosphor sprite migration (E2E behavioral).
|
||||
*
|
||||
* Asserts (in a real Chromium against a running server):
|
||||
* (a) top-nav buttons that previously held emoji glyphs (Live, Perf,
|
||||
* Audio Lab, Search, Customize, theme toggle, hamburger) now render
|
||||
* a Phosphor <svg class="ph-icon">…<use href="…#ph-…"/></svg>
|
||||
* child with non-zero rendered size.
|
||||
* (b) bottom-nav primary tabs (Home, Packets, Live, Map, Channels, More)
|
||||
* each render a .ph-icon with a getBBox() that has non-zero width/height
|
||||
* at viewport 360x800.
|
||||
* (c) /#/observers — the "Compare observers" heading and the "Compare
|
||||
* selected" button each render a .ph-icon child (no bare emoji).
|
||||
* (d) zero emoji codepoints in the rendered DOM textContent of .top-nav
|
||||
* and [data-bottom-nav] and the observers compare bar.
|
||||
*
|
||||
* CI gating: CHROMIUM_REQUIRE=1 makes Chromium-launch failure a HARD FAIL.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
const assert = require('assert');
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||||
const EMOJI_RE = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}◆●■▲★☆○✓✗⚠✉]/u;
|
||||
|
||||
let passes = 0, failures = 0;
|
||||
function pass(msg) { console.log(` ✓ ${msg}`); passes++; }
|
||||
function fail(msg) { console.error(` ✗ ${msg}`); failures++; }
|
||||
|
||||
async function main() {
|
||||
const requireChromium = process.env.CHROMIUM_REQUIRE === '1';
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROMIUM_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
||||
});
|
||||
} catch (err) {
|
||||
if (requireChromium) {
|
||||
console.error(`test-issue-1648-m1-icons-e2e.js: HARD FAIL — Chromium unavailable: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.warn(`SKIP — Chromium unavailable: ${err.message}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
|
||||
// ── (a) top-nav: Live, Perf, Audio-Lab, Search toggle, Customize, theme, hamburger ──
|
||||
await page.goto(`${BASE}/#/`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('.top-nav', { timeout: 5000 });
|
||||
|
||||
const topNavCheck = await page.evaluate(() => {
|
||||
const sels = [
|
||||
['Live nav', '.top-nav a[data-route="live"]'],
|
||||
['Perf nav', '.top-nav a[data-route="perf"]'],
|
||||
['Audio-Lab', '.top-nav a[data-route="audio-lab"]'],
|
||||
['Search btn', '#searchToggle'],
|
||||
['Custom btn', '#customizeToggle'],
|
||||
['Hamburger', '#hamburger'],
|
||||
];
|
||||
return sels.map(([label, sel]) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return { label, sel, found: false };
|
||||
const ph = el.querySelector('svg.ph-icon, .ph-icon');
|
||||
const rect = ph ? ph.getBoundingClientRect() : null;
|
||||
return {
|
||||
label, sel, found: true,
|
||||
hasPhIcon: !!ph,
|
||||
rectW: rect ? rect.width : 0,
|
||||
rectH: rect ? rect.height : 0,
|
||||
text: el.textContent || '',
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
for (const r of topNavCheck) {
|
||||
if (!r.found) { fail(`(a) ${r.label}: selector ${r.sel} not found`); continue; }
|
||||
if (!r.hasPhIcon) fail(`(a) ${r.label}: no .ph-icon child (text="${r.text.slice(0,40)}")`);
|
||||
else if (r.rectW <= 0 || r.rectH <= 0) fail(`(a) ${r.label}: .ph-icon has zero size (${r.rectW}x${r.rectH})`);
|
||||
else pass(`(a) ${r.label}: .ph-icon ${r.rectW.toFixed(0)}x${r.rectH.toFixed(0)}`);
|
||||
if (EMOJI_RE.test(r.text)) fail(`(a) ${r.label}: still contains emoji codepoint in text`);
|
||||
}
|
||||
|
||||
// ── (b) bottom-nav at 360x800 ──
|
||||
await ctx.close();
|
||||
const ctxMobile = await browser.newContext({ viewport: { width: 360, height: 800 } });
|
||||
const m = await ctxMobile.newPage();
|
||||
await m.goto(`${BASE}/#/`, { waitUntil: 'domcontentloaded' });
|
||||
await m.waitForSelector('[data-bottom-nav]', { timeout: 5000 });
|
||||
const bn = await m.evaluate(() => {
|
||||
const tabs = ['home','packets','live','map','channels','more'];
|
||||
return tabs.map(route => {
|
||||
const el = document.querySelector(`[data-bottom-nav-tab="${route}"]`);
|
||||
if (!el) return { route, found: false };
|
||||
const ph = el.querySelector('svg.ph-icon, .ph-icon');
|
||||
const rect = ph ? ph.getBoundingClientRect() : null;
|
||||
return {
|
||||
route, found: true,
|
||||
hasPhIcon: !!ph,
|
||||
rectW: rect ? rect.width : 0,
|
||||
rectH: rect ? rect.height : 0,
|
||||
text: el.textContent || '',
|
||||
};
|
||||
});
|
||||
});
|
||||
for (const r of bn) {
|
||||
if (!r.found) { fail(`(b) bottom-nav tab ${r.route} not found`); continue; }
|
||||
if (!r.hasPhIcon) fail(`(b) bottom-nav tab ${r.route}: no .ph-icon`);
|
||||
else if (r.rectW <= 0 || r.rectH <= 0) fail(`(b) bottom-nav tab ${r.route}: zero size`);
|
||||
else pass(`(b) bottom-nav tab ${r.route}: .ph-icon ${r.rectW.toFixed(0)}x${r.rectH.toFixed(0)}`);
|
||||
if (EMOJI_RE.test(r.text)) fail(`(b) bottom-nav tab ${r.route}: emoji codepoint in textContent`);
|
||||
}
|
||||
|
||||
// ── (c) /observers — Compare heading + Compare-selected button ──
|
||||
await m.goto(`${BASE}/#/observers`, { waitUntil: 'domcontentloaded' });
|
||||
// Give the page a moment to render observers list.
|
||||
await m.waitForTimeout(1500);
|
||||
const obs = await m.evaluate(() => {
|
||||
// Compare observers heading: span containing "Compare" near top of /observers
|
||||
const headings = Array.from(document.querySelectorAll('h2, h3, .observers-compare-heading, [data-role="compare-heading"]'));
|
||||
const compareHeading = headings.find(h => /Compare\s+observers/i.test(h.textContent || ''));
|
||||
const compareBtn = document.querySelector('[data-action="compare"], button[data-role="compare-btn"], .compare-selected-btn')
|
||||
|| Array.from(document.querySelectorAll('button')).find(b => /Compare\s+selected/i.test(b.textContent || ''));
|
||||
const refreshBtn = document.querySelector('[data-action="obs-refresh"]');
|
||||
function describe(el) {
|
||||
if (!el) return null;
|
||||
const ph = el.querySelector('svg.ph-icon, .ph-icon');
|
||||
const rect = ph ? ph.getBoundingClientRect() : null;
|
||||
return { has: !!ph, rectW: rect?rect.width:0, rectH: rect?rect.height:0, text: el.textContent||'' };
|
||||
}
|
||||
return {
|
||||
heading: describe(compareHeading),
|
||||
btn: describe(compareBtn),
|
||||
refresh: describe(refreshBtn),
|
||||
};
|
||||
});
|
||||
if (!obs.heading) fail('(c) Compare-observers heading not found on /observers');
|
||||
else if (!obs.heading.has) fail('(c) Compare-observers heading missing .ph-icon');
|
||||
else pass(`(c) Compare-observers heading has .ph-icon ${obs.heading.rectW.toFixed(0)}x${obs.heading.rectH.toFixed(0)}`);
|
||||
if (obs.heading && EMOJI_RE.test(obs.heading.text)) fail('(c) Compare heading text still has emoji');
|
||||
|
||||
if (!obs.btn) fail('(c) Compare-selected button not found on /observers');
|
||||
else if (!obs.btn.has) fail('(c) Compare-selected button missing .ph-icon');
|
||||
else pass(`(c) Compare-selected button has .ph-icon ${obs.btn.rectW.toFixed(0)}x${obs.btn.rectH.toFixed(0)}`);
|
||||
if (obs.btn && EMOJI_RE.test(obs.btn.text)) fail('(c) Compare-selected text still has emoji');
|
||||
|
||||
if (obs.refresh) {
|
||||
if (!obs.refresh.has) fail('(c) /observers refresh button missing .ph-icon');
|
||||
else pass(`(c) /observers refresh button has .ph-icon`);
|
||||
if (EMOJI_RE.test(obs.refresh.text)) fail('(c) refresh button still has emoji');
|
||||
}
|
||||
|
||||
// ── (d) zero emoji codepoints in rendered nav DOM ──
|
||||
await m.goto(`${BASE}/#/`, { waitUntil: 'domcontentloaded' });
|
||||
await m.waitForSelector('.top-nav', { timeout: 5000 });
|
||||
const text = await m.evaluate(() => {
|
||||
const top = document.querySelector('.top-nav');
|
||||
const bot = document.querySelector('[data-bottom-nav]');
|
||||
return [(top && top.textContent) || '', (bot && bot.textContent) || ''].join('\n');
|
||||
});
|
||||
if (EMOJI_RE.test(text)) {
|
||||
const hits = (text.match(new RegExp(EMOJI_RE.source, 'gu')) || []).slice(0, 10);
|
||||
fail(`(d) rendered nav DOM still has emoji codepoints: ${hits.join(' ')}`);
|
||||
} else {
|
||||
pass('(d) rendered nav DOM (top-nav + bottom-nav) has zero emoji codepoints');
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log(`\ntest-issue-1648-m1-icons-e2e.js: ${passes} passed, ${failures} failed`);
|
||||
// Hard assertion so the run exit code is gated on a real assertion call,
|
||||
// not just a process.exit branch.
|
||||
assert.strictEqual(failures, 0, `${failures} M1 icon-render assertions failed`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('test-issue-1648-m1-icons-e2e.js: FAIL —', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user