From 55e4d957b111163a7ea8e37cd97af8717517cc62 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Wed, 10 Jun 2026 22:54:44 -0700 Subject: [PATCH] =?UTF-8?q?M1:=20emoji=20=E2=86=92=20Phosphor=20Icons=20?= =?UTF-8?q?=E2=80=94=20top-nav,=20mobile=20nav,=20Compare=20(#1648)=20(#16?= =?UTF-8?q?49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Red commit: 12e921d9ba1d1166ca2ac356f4375e9ce8f82027 (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: ``. 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 `

` 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 --- public/bottom-nav.js | 49 +++++---- public/icons/README.md | 51 +++++++++ public/icons/phosphor-sprite.svg | 36 ++++++ public/index.html | 16 +-- public/mobile-page-actions.js | 13 ++- public/nav-drawer.js | 22 ++-- public/observer-detail.js | 4 +- public/observers.js | 32 +++--- public/style.css | 18 +++ scripts/instrument-frontend.sh | 7 ++ test-all.sh | 1 + test-issue-1648-m1-emoji-scan.js | 139 +++++++++++++++++++++++ test-issue-1648-m1-icons-e2e.js | 183 +++++++++++++++++++++++++++++++ 13 files changed, 517 insertions(+), 54 deletions(-) create mode 100644 public/icons/README.md create mode 100644 public/icons/phosphor-sprite.svg create mode 100644 test-issue-1648-m1-emoji-scan.js create mode 100644 test-issue-1648-m1-icons-e2e.js diff --git a/public/bottom-nav.js b/public/bottom-nav.js index f46b84e6..1aaf7fac 100644 --- a/public/bottom-nav.js +++ b/public/bottom-nav.js @@ -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 ''; + } + + // 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'; } diff --git a/public/icons/README.md b/public/icons/README.md new file mode 100644 index 00000000..015f9c33 --- /dev/null +++ b/public/icons/README.md @@ -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 `` per icon + (regular weight, `viewBox="0 0 256 256"` to match Phosphor's native grid). + +## Markup pattern + +```html + +``` + +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 + + (or `assets/fill/NAME.svg` for the rare filled-circle / star-fill cases). +2. Append a `…` to + `phosphor-sprite.svg`. Strip the outer `` wrapper and any `fill=` attrs + on the inner `` (we want `currentColor` from the parent). +3. Reference it with ``. + +## 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). diff --git a/public/icons/phosphor-sprite.svg b/public/icons/phosphor-sprite.svg new file mode 100644 index 00000000..67c5c3da --- /dev/null +++ b/public/icons/phosphor-sprite.svg @@ -0,0 +1,36 @@ + \ No newline at end of file diff --git a/public/index.html b/public/index.html index dd566f03..b42da335 100644 --- a/public/index.html +++ b/public/index.html @@ -120,14 +120,14 @@ Home Packets Map - πŸ”΄ Live + Live Channels Nodes Tools Observers Analytics - ⚑ Perf - 🎡 Lab + Perf + Lab - - + + - + diff --git a/public/mobile-page-actions.js b/public/mobile-page-actions.js index 30a51836..248c90a6 100644 --- a/public/mobile-page-actions.js +++ b/public/mobile-page-actions.js @@ -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 = + ''; + } else { + ic.textContent = m.icon; + } const lb = document.createElement('span'); lb.className = 'bottom-nav-sheet-label'; diff --git a/public/nav-drawer.js b/public/nav-drawer.js index b26ae09c..37820d21 100644 --- a/public/nav-drawer.js +++ b/public/nav-drawer.js @@ -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 ''; + } + 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'; diff --git a/public/observer-detail.js b/public/observer-detail.js index 5b62b85d..79172fae 100644 --- a/public/observer-detail.js +++ b/public/observer-detail.js @@ -97,7 +97,7 @@ window.ObserverDetailNaiveBanner = {