From 034cc8f71a0d5fb30fff41f7ef187c2840cdccd4 Mon Sep 17 00:00:00 2001 From: n30nex Date: Sun, 11 Oct 2026 22:37:33 -0400 Subject: [PATCH] Use native control templates and share duplicated view styling --- web/e2e/netgraph-regions.spec.ts | 6 +- web/netgraph/index.html | 3 + web/src/main.ts | 1 + web/src/netgraph/cameraControls.ts | 8 +- web/src/netgraph/main.ts | 1 + web/src/netgraph/regionControls.ts | 7 +- web/src/netgraph/styles.css | 164 ++++--------------- web/src/networkControls.css | 63 +++++++ web/src/styles.css | 255 ++++------------------------- 9 files changed, 152 insertions(+), 356 deletions(-) create mode 100644 web/src/networkControls.css diff --git a/web/e2e/netgraph-regions.spec.ts b/web/e2e/netgraph-regions.spec.ts index 4600a1a..8f73a74 100644 --- a/web/e2e/netgraph-regions.spec.ts +++ b/web/e2e/netgraph-regions.spec.ts @@ -40,7 +40,8 @@ test('spacing and dragging survive refresh while live packets keep their routes const stage = page.locator('#netgraph-stage'); await expect(stage).toHaveAttribute('data-render-state', 'idle'); await layoutPanel(page); - await page.getByLabel('Region spacing', { exact: true }).fill('2'); + await page.getByLabel('Region spacing', { exact: true }).focus(); await page.keyboard.press('Home'); + for (let i = 0; i < 10; i++) await page.keyboard.press('ArrowRight'); await expect(stage).toHaveAttribute('data-region-spacing', '2'); await expect(stage).toHaveAttribute('data-visible-routes', '2'); await page.getByRole('button', { name: 'Move region right', exact: true }).click(); @@ -76,6 +77,8 @@ test('spacing and dragging survive refresh while live packets keep their routes await expect(stage).toHaveAttribute('data-last-packet-hops', '1'); await expect.poll(() => stage.getAttribute('data-active-packets').then(Number)).toBeGreaterThan(0); await page.screenshot({ path: info.outputPath('netgraph-arranged-live-only.png') }); + await expect(stage).toHaveAttribute('data-active-packets', '0', { timeout: 12_000 }); + await expect(stage).toHaveAttribute('data-residue-count', '0', { timeout: 5_000 }); await page.reload(); await expect(stage).toHaveAttribute('data-render-state', 'idle'); expect(await saved(page)).toEqual({ ...moved, routes: false }); await expect(stage).toHaveAttribute('data-region-spacing', '2'); @@ -88,6 +91,7 @@ test('spacing and dragging survive refresh while live packets keep their routes await page.locator('#region-filter > summary').click(); const box = (await page.locator('.region-options').boundingBox())!; expect(box.x).toBeGreaterThanOrEqual(0); expect(box.x + box.width).toBeLessThanOrEqual(page.viewportSize()!.width); + expect(box.y + box.height).toBeLessThan((await page.locator('.controls').boundingBox())!.y); await page.keyboard.press('Escape'); await expect(page.locator('#region-filter')).not.toHaveAttribute('open', ''); await expect(page.locator('#region-filter > summary')).toBeFocused(); }); diff --git a/web/netgraph/index.html b/web/netgraph/index.html index 07b8e27..0ed3a83 100644 --- a/web/netgraph/index.html +++ b/web/netgraph/index.html @@ -93,6 +93,9 @@ Try again

+ + + diff --git a/web/src/main.ts b/web/src/main.ts index 48bc069..9a0d997 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -1,3 +1,4 @@ +import './networkControls.css'; import { mountSoundPreview } from './soundPreview'; import { requestedNode } from './selection'; import { mountCinematicDock, mountLayerCombinations } from './cinematicChrome'; diff --git a/web/src/netgraph/cameraControls.ts b/web/src/netgraph/cameraControls.ts index b13d2cb..e84319b 100644 --- a/web/src/netgraph/cameraControls.ts +++ b/web/src/netgraph/cameraControls.ts @@ -7,9 +7,7 @@ export function mountGraphCamera(graph: NetgraphRenderer): () => void { button.textContent = '3D'; button.setAttribute('aria-label', '3D Netgraph'); button.setAttribute('aria-pressed', 'false'); button.title = 'Tilt and rotate the topology'; controls.insertBefore(button, controls.querySelector('.display-disclosure')); - const panel = document.createElement('div'); panel.className = 'graph-camera'; panel.hidden = true; - panel.setAttribute('role', 'group'); panel.setAttribute('aria-label', '3D camera controls'); - panel.innerHTML = '3D camera
Drag to pan. Shift-drag or right-drag to rotate. Pinch to zoom and twist to rotate. Height is schematic.'; + const panel = graphTemplate('graph-camera-ui'); document.getElementById('display-panel')!.append(panel); const tilt = panel.querySelector('input')!, output = panel.querySelector('output')!; const navigate = () => document.getElementById('netgraph-stage')!.dispatchEvent(new Event('graphnavigate')); @@ -28,3 +26,7 @@ export function mountGraphCamera(graph: NetgraphRenderer): () => void { }; sync(); return sync; } + +export function graphTemplate(id: string): T { + return (document.getElementById(id) as HTMLTemplateElement).content.firstElementChild!.cloneNode(true) as T; +} diff --git a/web/src/netgraph/main.ts b/web/src/netgraph/main.ts index ef5d2d5..b292796 100644 --- a/web/src/netgraph/main.ts +++ b/web/src/netgraph/main.ts @@ -1,3 +1,4 @@ +import '../networkControls.css'; import { mountRegionControls } from './regionControls'; import { mountGraphFollow } from './follow'; import { mountGraphCamera } from './cameraControls'; diff --git a/web/src/netgraph/regionControls.ts b/web/src/netgraph/regionControls.ts index eecc326..4d6d3ac 100644 --- a/web/src/netgraph/regionControls.ts +++ b/web/src/netgraph/regionControls.ts @@ -1,16 +1,15 @@ +import { graphTemplate } from './cameraControls'; import type { NetgraphRenderer } from './renderer'; export function mountRegionControls(graph: NetgraphRenderer, host: HTMLElement, changed: () => void, closePanels: () => void) { const stage = document.getElementById('netgraph-stage')!; const navigate = () => stage.dispatchEvent(new Event('graphnavigate')); - const menu = document.createElement('details'); menu.id = 'region-filter'; menu.className = 'region-filter'; - menu.innerHTML = 'Regions · All'; + const menu = graphTemplate('graph-regions-ui'); const summary = menu.querySelector('summary')!, search = menu.querySelector('input')!, list = menu.querySelector('.region-checks')!; const routes = document.createElement('button'); routes.type = 'button'; routes.id = 'graph-routes-button'; routes.textContent = 'Routes'; routes.setAttribute('aria-label', 'Show historical routes'); routes.title = 'Hide standing lines; live packets still light their routes briefly'; host.append(menu, routes); - const layout = document.createElement('details'); layout.className = 'graph-layout'; - layout.innerHTML = 'Region layout
Drag a region label to move its whole bubble. Saved on this device.'; + const layout = graphTemplate('graph-layout-ui'); document.getElementById('display-panel')!.append(layout); const spacing = layout.querySelector('input')!, output = layout.querySelector('output')!, move = layout.querySelector('select')!; let lastAreas: ReturnType | undefined, lastRegions: string[] | null | undefined, signature = ''; diff --git a/web/src/netgraph/styles.css b/web/src/netgraph/styles.css index d4ffd0f..0192bd9 100644 --- a/web/src/netgraph/styles.css +++ b/web/src/netgraph/styles.css @@ -71,14 +71,7 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible box-shadow: inset 0 0 110px rgba(0, 4, 8, 0.28); } -.glass { - z-index: 5; - border: 1px solid rgba(151, 220, 224, 0.16); - background: linear-gradient(145deg, rgba(8, 20, 26, 0.9), rgba(2, 9, 14, 0.82)); - box-shadow: 0 10px 34px rgba(0, 0, 0, 0.34), inset 0 1px rgba(219, 255, 255, 0.04); - -webkit-backdrop-filter: blur(14px) saturate(1.18); - backdrop-filter: blur(14px) saturate(1.18); -} +.glass { border: 1px solid rgba(151, 220, 224, 0.16); background: linear-gradient(145deg, rgba(8, 20, 26, 0.9), rgba(2, 9, 14, 0.82)); box-shadow: 0 10px 34px rgba(0, 0, 0, 0.34), inset 0 1px rgba(219, 255, 255, 0.04); } /* Live canvases need not be blurred again underneath every Android control. */ @media (pointer: coarse), (max-width: 700px) { @@ -89,78 +82,33 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible } } -.topbar { - position: absolute; - top: max(12px, env(safe-area-inset-top)); - left: max(12px, env(safe-area-inset-left)); - display: flex; - min-height: 46px; - align-items: center; - gap: 15px; - padding: 0 12px; - border-radius: 13px; -} +.topbar { position: absolute; top: max(12px, env(safe-area-inset-top)); left: max(12px, env(safe-area-inset-left)); min-height: 46px; gap: 15px; border-radius: 13px; } .brand, .workspace-link, .view-title, .status { display: inline-flex; min-height: 44px; align-items: center; gap: 7px; } .brand, .workspace-link { color: #effdff; text-decoration: none; } -.brand { font-size: 13px; font-weight: 760; letter-spacing: 0.02em; } -.brand-mark { width: 9px; height: 9px; border: 2px solid #35cdbc; border-radius: 50%; box-shadow: 0 0 11px #36d7c2; } +.brand { font-weight: 760; } .view-title { color: #bfe6e3; font-size: 11px; font-weight: 760; letter-spacing: 0.055em; text-transform: uppercase; } .view-title > span:first-child, .workspace-link > span:first-child { color: #63d9cc; text-shadow: 0 0 10px rgba(99, 217, 204, 0.68); } .workspace-link { color: #9ccbc8; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; } .brand:hover, .workspace-link:hover { color: #fff; } -.status { color: #aebdcb; font-size: 11px; font-weight: 650; } -.status-dot { width: 6px; height: 6px; border-radius: 50%; background: #86919b; } -.status[data-state="active"] .status-dot { background: #45df9a; box-shadow: 0 0 8px #45df9a; } -.status[data-state="quiet"] .status-dot { background: #70a99f; } -.status[data-state="reconnecting"] .status-dot { background: #e9c35b; animation: status-pulse 1.1s ease-in-out infinite; } -.status[data-state="offline"] .status-dot { background: #ed7283; } -.topbar.traffic-pulse .brand-mark { animation: traffic-brand 680ms ease-out; } - -.traffic-meter { display: inline-flex; height: 12px; align-items: flex-end; gap: 2px; } -.traffic-meter i { width: 2px; height: 2px; border-radius: 2px; background: rgba(var(--traffic-rgb), 0.3); transition: height 160ms ease, background 160ms ease, box-shadow 160ms ease; } +.status { font-weight: 650; } +.traffic-meter i { background: rgba(var(--traffic-rgb), 0.3); transition: height 160ms ease, background 160ms ease, box-shadow 160ms ease; } .traffic-meter[data-level="1"] i:nth-child(-n+1), .traffic-meter[data-level="2"] i:nth-child(-n+2), .traffic-meter[data-level="3"] i:nth-child(-n+3), .traffic-meter[data-level="4"] i:nth-child(-n+4), -.traffic-meter[data-level="5"] i:nth-child(-n+5) { height: 10px; background: rgba(var(--traffic-rgb), 0.92); box-shadow: 0 0 7px rgba(var(--traffic-rgb), 0.62); } +.traffic-meter[data-level="5"] i:nth-child(-n+5) { background: rgba(var(--traffic-rgb), 0.92); } .traffic-meter i:nth-child(2), .traffic-meter i:nth-child(4) { max-height: 7px; } -.traffic-meter i:nth-child(3) { max-height: 12px; } - -.controls { - z-index: 8; - position: absolute; - top: max(12px, env(safe-area-inset-top)); - right: max(12px, env(safe-area-inset-right)); - display: flex; - min-height: 46px; - overflow: visible; - border-radius: 13px; -} +.controls { z-index: 8; position: absolute; top: max(12px, env(safe-area-inset-top)); right: max(12px, env(safe-area-inset-right)); min-height: 46px; border-radius: 13px; } .popover-control { position: relative; display: flex; } -.control-button { - display: inline-flex; - min-width: 44px; - min-height: 44px; - align-items: center; - justify-content: center; - gap: 7px; - border: 0; - border-right: 1px solid rgba(139, 205, 214, 0.12); - padding: 0 11px; - color: #bacbd5; - background: transparent; - cursor: pointer; - font-size: 12px; - font-weight: 680; -} -.control-button:hover, .control-button:focus-visible, .control-button.selected { color: #efffff; background: rgba(41, 184, 169, 0.14); outline-offset: -3px; } +.control-button { min-width: 44px; justify-content: center; gap: 7px; color: #bacbd5; font-weight: 680; } +.control-button:hover, .control-button:focus-visible, .control-button.selected { color: #efffff; outline-offset: -3px; } .control-button svg { width: 16px; height: 16px; flex: none; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; } .control-button small { color: #8195a0; font-size: 9px; font-weight: 760; } .control-button[data-sound-state="on"] small { color: #6ce2c5; } .control-button[data-sound-state="resume"] small { color: #f0c36a; } .control-button.sounding svg, .sound-activity.active { animation: sound-hop 680ms ease-out; } .reset-button { border-right: 0; } -.route-window { display: inline-flex; min-height: 44px; align-items: center; gap: 7px; border-right: 1px solid rgba(139, 205, 214, 0.12); padding: 0 8px 0 11px; color: #869aa6; font-size: 10px; font-weight: 700; } +.route-window { gap: 7px; border-right: 1px solid rgba(139, 205, 214, 0.12); padding: 0 8px 0 11px; color: #869aa6; font-size: 10px; font-weight: 700; } .route-window select, .scene-control select { min-height: 32px; border: 1px solid rgba(127, 200, 204, 0.2); @@ -173,38 +121,23 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible .find-panel, .sound-panel { position: absolute; top: calc(100% + 8px); z-index: 9; display: grid; border-radius: 13px; } .find-panel[hidden], .sound-panel[hidden] { display: none; } -.find-panel { right: auto; left: 0; width: min(292px, calc(100vw - 24px)); gap: 8px; padding: 12px; } -.find-panel > label { color: #dff8f5; font-size: 11px; font-weight: 760; } -.find-panel input { - width: 100%; - min-height: 44px; - border: 1px solid rgba(99, 217, 204, 0.26); - border-radius: 8px; - padding: 0 11px; - color: #efffff; - background: rgba(4, 15, 20, 0.96); -} -.node-search-results { display: grid; max-height: min(330px, 52vh); overflow-y: auto; overscroll-behavior: contain; } -.node-search-results > p { margin: 7px 2px; color: #8297a4; font-size: 11px; } -.node-search-result { display: grid; min-height: 50px; gap: 2px; border: 0; border-top: 1px solid rgba(139, 205, 214, 0.1); padding: 7px 8px; color: #eaffff; background: transparent; text-align: left; cursor: pointer; } -.node-search-result strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } -.node-search-result span { color: #8298a5; font-size: 9px; text-transform: capitalize; } +.find-panel { right: auto; } +.find-panel > label { color: #dff8f5; font-weight: 760; } +.find-panel input { color: #efffff; } +.node-search-results > p { margin: 7px 2px; } +.node-search-result { min-height: 50px; color: #eaffff; } +.node-search-result strong { font-size: 11px; } +.node-search-result span { color: #8298a5; font-size: 9px; } .node-search-result:hover, .node-search-result:focus-visible, .node-search-result[aria-selected="true"] { background: rgba(41, 184, 169, 0.13); outline-offset: -2px; } -.sound-panel { right: 0; width: 250px; gap: 11px; padding: 13px; } -.sound-panel header { display: flex; align-items: center; justify-content: space-between; color: #efffff; font-size: 12px; } -.sound-panel header span { color: #899da8; font-size: 11px; font-weight: 750; } +.sound-panel { width: 250px; } +.sound-panel header { color: #efffff; } .sound-panel[data-sound-state="on"] header span { color: #6ce2c5; } .sound-panel[data-sound-state="resume"] header span { color: #f0c36a; } -.sound-toggle { min-height: 44px; border: 1px solid rgba(99, 217, 204, 0.25); border-radius: 9px; color: #eaffff; background: rgba(38, 166, 155, 0.14); cursor: pointer; font-size: 12px; font-weight: 720; } -.sound-toggle:hover, .sound-toggle:focus-visible { background: rgba(38, 166, 155, 0.24); outline-offset: 1px; } +.sound-toggle { color: #eaffff; font-weight: 720; } .scene-control, .volume-control { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 6px 10px; color: #afc0c9; font-size: 11px; font-weight: 650; } -.volume-control output { color: #e9ffff; font-variant-numeric: tabular-nums; } -.volume-control input { grid-column: 1 / -1; width: 100%; min-height: 40px; accent-color: #4dcfbe; cursor: pointer; } -.sound-panel p { display: flex; align-items: center; gap: 7px; margin: 0; color: #859aa5; font-size: 10px; } -.sound-activity { width: 7px; height: 7px; flex: none; border-radius: 50%; background: #41545c; } -.sound-activity.active { background: #6ce2c5; box-shadow: 0 0 9px rgba(108, 226, 197, 0.9); } - +.volume-control output { color: #e9ffff; } +.volume-control input { min-height: 40px; } .graph-summary { position: absolute; top: max(72px, calc(env(safe-area-inset-top) + 72px)); @@ -231,18 +164,7 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible .zoom-controls .control-button { width: 44px; height: 44px; padding: 0; font-size: 23px; touch-action: manipulation; } .zoom-controls .control-button + .control-button { border-top: 1px solid rgba(151, 220, 224, 0.16); } -.legend { - position: absolute; - right: max(12px, env(safe-area-inset-right)); - bottom: max(12px, env(safe-area-inset-bottom)); - display: grid; - gap: 7px; - padding: 9px 11px; - border-radius: 11px; - color: #93aab4; - font-size: 9px; - pointer-events: none; -} +.legend { right: max(12px, env(safe-area-inset-right)); bottom: max(12px, env(safe-area-inset-bottom)); display: grid; gap: 7px; padding: 9px 11px; border-radius: 11px; color: #93aab4; font-size: 9px; pointer-events: none; } .role-legend, .packet-legend { display: flex; flex-wrap: wrap; gap: 6px 11px; } .role-legend span, .packet-legend span { display: inline-flex; align-items: center; gap: 5px; } .role-legend i { width: 7px; height: 7px; border: 1px solid rgba(255, 255, 255, 0.35); border-radius: 50%; background: #67ead2; } @@ -256,34 +178,20 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible .packet-legend i[data-kind="Control"] { background: #a78bfa; } .packet-legend i[data-kind="inter-area"] { background: transparent; border-top: 1px dashed #76a8af; box-shadow: none; } -.tooltip { position: absolute; z-index: 7; max-width: min(260px, calc(100vw - 24px)); padding: 7px 9px; border-radius: 8px; color: #efffff; font-size: 10px; pointer-events: none; transform: translate(10px, calc(-100% - 10px)); } -.tooltip[hidden] { display: none; } -.tooltip strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.tooltip span { color: #8da3af; font-size: 9px; text-transform: capitalize; } +.tooltip { z-index: 7; max-width: min(260px, calc(100vw - 24px)); padding: 7px 9px; color: #efffff; font-size: 10px; transform: translate(10px, calc(-100% - 10px)); } +.tooltip strong { display: block; } +.tooltip span { color: #8da3af; } -.node-inspector-sheet { position: absolute; z-index: 7; top: max(76px, calc(env(safe-area-inset-top) + 76px)); right: max(12px, env(safe-area-inset-right)); width: min(350px, calc(100vw - 24px)); max-height: min(620px, calc(100vh - 164px)); overflow: hidden; border-radius: 14px; } -.node-inspector-sheet[hidden] { display: none; } -.node-inspector { display: grid; grid-template-rows: auto auto auto minmax(0, 1fr); max-height: min(620px, calc(100vh - 164px)); } -.node-inspector > header { display: flex; min-height: 54px; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 8px 9px 13px; border-bottom: 1px solid rgba(139, 205, 214, 0.11); } -.node-inspector > header > div { display: grid; min-width: 0; gap: 2px; } -.node-inspector > header strong { overflow: hidden; color: #f0ffff; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; } -.node-inspector > header span { color: #8ca2af; font-size: 10px; text-transform: capitalize; } -.node-inspector-close { width: 44px; height: 44px; flex: none; border: 0; border-radius: 9px; color: #a9bdc8; background: transparent; cursor: pointer; font-size: 24px; } -.node-inspector-close:hover, .node-inspector-close:focus-visible { color: #efffff; background: rgba(41, 184, 169, 0.16); outline-offset: -3px; } -.node-inspector dl { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 0; padding: 10px 13px; } -.node-inspector dl div { display: grid; min-width: 0; gap: 2px; } -.node-inspector dt { color: #708895; font-size: 9px; font-weight: 650; text-transform: uppercase; } -.node-inspector dd { overflow: hidden; margin: 0; color: #d9e9ef; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } -.node-inspector h3 { margin: 0; padding: 6px 13px 7px; color: #8da3ae; font-size: 10px; font-weight: 700; } -.neighbor-list { overflow-y: auto; overscroll-behavior: contain; border-top: 1px solid rgba(139, 205, 214, 0.09); } -.neighbor-row { display: grid; width: 100%; min-height: 54px; gap: 1px; border: 0; border-bottom: 1px solid rgba(139, 205, 214, 0.08); padding: 7px 13px; color: #eaffff; background: transparent; text-align: left; cursor: pointer; } -.neighbor-row strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } -.neighbor-row span { color: #8298a5; font-size: 9px; text-transform: capitalize; } -.neighbor-row:hover, .neighbor-row:focus-visible { background: rgba(41, 184, 169, 0.13); outline-offset: -2px; } +.node-inspector-sheet { top: max(76px, calc(env(safe-area-inset-top) + 76px)); right: max(12px, env(safe-area-inset-right)); width: min(350px, calc(100vw - 24px)); max-height: min(620px, calc(100vh - 164px)); border-radius: 14px; } +.node-inspector { max-height: min(620px, calc(100vh - 164px)); } +.node-inspector > header { min-height: 54px; padding: 9px 8px 9px 13px; } +.node-inspector-close:hover, .node-inspector-close:focus-visible { color: #efffff; } +.neighbor-row { color: #eaffff; } +.neighbor-row:hover, .neighbor-row:focus-visible { outline-offset: -2px; } .empty-state, .fatal { position: absolute; z-index: 8; top: 50%; left: 50%; margin: 0; border-radius: 12px; padding: 12px 15px; color: #b8cbd3; font-size: 12px; transform: translate(-50%, -50%); } .empty-state[hidden], .fatal[hidden] { display: none; } -.fatal { color: #ffd2d8; border-color: rgba(237, 114, 131, 0.35); } +.fatal { color: #ffd2d8; } @keyframes status-pulse { 50% { opacity: 0.42; transform: scale(0.78); } } @keyframes traffic-brand { 45% { border-color: rgb(var(--traffic-rgb)); box-shadow: 0 0 15px rgba(var(--traffic-rgb), 0.9); transform: scale(1.25); } } @@ -377,9 +285,9 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible .region-filter summary, #graph-routes-button { cursor:pointer; white-space:nowrap; padding:9px 6px; font:inherit; color:var(--ink-strong); } #graph-routes-button { border:1px solid var(--panel-border); border-radius:7px; background:var(--field-bg); } #graph-routes-button.selected { background:var(--selected); } -.region-options { position:absolute; top:calc(100% + 6px); left:0; width:min(300px,calc(100vw - 16px)); padding:10px; border:1px solid var(--panel-border); border-radius:10px; background:var(--panel-solid); box-shadow:0 8px 25px #0003; } +.region-options { max-height:min(420px,calc(100dvh - var(--dock-height,54px) - var(--dock-bottom,12px) - 176px)); overflow:auto; overscroll-behavior:contain; position:absolute; top:calc(100% + 6px); left:0; width:min(300px,calc(100vw - 16px)); padding:10px; border:1px solid var(--panel-border); border-radius:10px; background:var(--panel-solid); box-shadow:0 8px 25px #0003; } .region-options > input { width:100%; min-height:40px; } -.region-checks { max-height:min(280px,45dvh); overflow:auto; overscroll-behavior:contain; } + .region-checks label { display:flex; align-items:center; gap:8px; min-height:44px; cursor:pointer; } .region-checks label[hidden] { display:none; } .region-checks input { accent-color:var(--focus-ring); } @@ -388,7 +296,7 @@ button:focus-visible, select:focus-visible, input:focus-visible, a:focus-visible .graph-layout { padding-block:10px; border-top:1px solid var(--panel-border); } .graph-layout summary { cursor:pointer; padding-block:8px; font-weight:700; } .graph-layout label { display:flex; align-items:center; gap:8px; margin-block:8px; } -.graph-layout input { min-width:0; flex:1; accent-color:var(--focus-ring); } +.graph-layout input { min-height:44px; min-width:0; flex:1; accent-color:var(--focus-ring); } .graph-layout select { min-width:0; max-width:66%; min-height:44px; color:var(--ink-strong); background:var(--field-bg); } .graph-layout small { display:block; margin-top:8px; line-height:1.4; color:var(--muted); } #netgraph-stage.region-hover { cursor:grab; } diff --git a/web/src/networkControls.css b/web/src/networkControls.css new file mode 100644 index 0000000..d33bd39 --- /dev/null +++ b/web/src/networkControls.css @@ -0,0 +1,63 @@ +/* Shared declarations only. View-specific layout and responsive overrides stay in each view. */ +:where(#app, #netgraph-app) .glass { z-index: 5; -webkit-backdrop-filter: blur(14px) saturate(1.18); backdrop-filter: blur(14px) saturate(1.18); } +:where(#app, #netgraph-app) .topbar { display: flex; align-items: center; padding: 0 12px; } +:where(#app, #netgraph-app) .brand { font-size: 13px; letter-spacing: 0.02em; } +:where(#app, #netgraph-app) .brand-mark { width: 9px; height: 9px; border: 2px solid #35cdbc; border-radius: 50%; box-shadow: 0 0 11px #36d7c2; } +:where(#app, #netgraph-app) .status { color: #aebdcb; font-size: 11px; } +:where(#app, #netgraph-app) .status-dot { width: 6px; height: 6px; border-radius: 50%; background: #86919b; } +:where(#app, #netgraph-app) .status[data-state="active"] .status-dot { background: #45df9a; box-shadow: 0 0 8px #45df9a; } +:where(#app, #netgraph-app) .status[data-state="quiet"] .status-dot { background: #70a99f; } +:where(#app, #netgraph-app) .status[data-state="reconnecting"] .status-dot { background: #e9c35b; animation: status-pulse 1.1s ease-in-out infinite; } +:where(#app, #netgraph-app) .status[data-state="offline"] .status-dot { background: #ed7283; } +:where(#app, #netgraph-app) .topbar.traffic-pulse .brand-mark { animation: traffic-brand 680ms ease-out; } +:where(#app, #netgraph-app) .traffic-meter { display: inline-flex; height: 12px; align-items: flex-end; gap: 2px; } +:where(#app, #netgraph-app) .traffic-meter i { width: 2px; height: 2px; border-radius: 2px; } +:where(#app, #netgraph-app) .traffic-meter[data-level="1"] i:nth-child(-n+1), :where(#app, #netgraph-app) .traffic-meter[data-level="2"] i:nth-child(-n+2), :where(#app, #netgraph-app) .traffic-meter[data-level="3"] i:nth-child(-n+3), :where(#app, #netgraph-app) .traffic-meter[data-level="4"] i:nth-child(-n+4), :where(#app, #netgraph-app) .traffic-meter[data-level="5"] i:nth-child(-n+5) { height: 10px; box-shadow: 0 0 7px rgba(var(--traffic-rgb), 0.62); } +:where(#app, #netgraph-app) .traffic-meter i:nth-child(3) { max-height: 12px; } +:where(#app, #netgraph-app) .controls { display: flex; overflow: visible; } +:where(#app, #netgraph-app) .control-button { display: inline-flex; align-items: center; min-height: 44px; border: 0; border-right: 1px solid rgba(139, 205, 214, 0.12); padding: 0 11px; background: transparent; cursor: pointer; font-size: 12px; } +:where(#app, #netgraph-app) .control-button:hover, :where(#app, #netgraph-app) .control-button:focus-visible, :where(#app, #netgraph-app) .control-button.selected { background: rgba(41, 184, 169, 0.14); } +:where(#app, #netgraph-app) .sound-panel { right: 0; gap: 11px; padding: 13px; } +:where(#app, #netgraph-app) .sound-panel header { display: flex; align-items: center; justify-content: space-between; font-size: 12px; } +:where(#app, #netgraph-app) .sound-panel header span { color: #899da8; font-size: 11px; font-weight: 750; } +:where(#app, #netgraph-app) .sound-toggle { min-height: 44px; border: 1px solid rgba(99, 217, 204, 0.25); border-radius: 9px; background: rgba(38, 166, 155, 0.14); cursor: pointer; font-size: 12px; } +:where(#app, #netgraph-app) .sound-toggle:hover, :where(#app, #netgraph-app) .sound-toggle:focus-visible { background: rgba(38, 166, 155, 0.24); outline-offset: 1px; } +:where(#app, #netgraph-app) .volume-control output { font-variant-numeric: tabular-nums; } +:where(#app, #netgraph-app) .volume-control input { grid-column: 1 / -1; width: 100%; accent-color: #4dcfbe; cursor: pointer; } +:where(#app, #netgraph-app) .sound-panel p { display: flex; align-items: center; gap: 7px; margin: 0; color: #859aa5; font-size: 10px; } +:where(#app, #netgraph-app) .sound-activity { width: 7px; height: 7px; flex: none; border-radius: 50%; background: #41545c; } +:where(#app, #netgraph-app) .sound-activity.active { background: #6ce2c5; box-shadow: 0 0 9px rgba(108, 226, 197, 0.9); } +:where(#app, #netgraph-app) .find-panel { left: 0; width: min(292px, calc(100vw - 24px)); gap: 8px; padding: 12px; } +:where(#app, #netgraph-app) .find-panel > label { font-size: 11px; } +:where(#app, #netgraph-app) .find-panel input { min-height: 44px; width: 100%; border: 1px solid rgba(99, 217, 204, 0.26); border-radius: 8px; padding: 0 11px; background: rgba(4, 15, 20, 0.96); } +:where(#app, #netgraph-app) .node-search-results { display: grid; max-height: min(330px, 52vh); overflow-y: auto; overscroll-behavior: contain; } +:where(#app, #netgraph-app) .node-search-results > p { color: #8297a4; font-size: 11px; } +:where(#app, #netgraph-app) .node-search-result { display: grid; gap: 2px; border: 0; border-top: 1px solid rgba(139, 205, 214, 0.1); padding: 7px 8px; background: transparent; text-align: left; cursor: pointer; } +:where(#app, #netgraph-app) .node-search-result strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +:where(#app, #netgraph-app) .node-search-result span { text-transform: capitalize; } +:where(#app, #netgraph-app) .route-window { display: inline-flex; align-items: center; min-height: 44px; } +:where(#app, #netgraph-app) .legend { position: absolute; } +:where(#app, #netgraph-app) .tooltip { position: absolute; border-radius: 8px; pointer-events: none; } +:where(#app, #netgraph-app) .tooltip strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +:where(#app, #netgraph-app) .tooltip span { font-size: 9px; text-transform: capitalize; } +:where(#app, #netgraph-app) .tooltip[hidden] { display: none; } +:where(#app, #netgraph-app) .node-inspector-sheet { position: absolute; z-index: 7; overflow: hidden; } +:where(#app, #netgraph-app) .node-inspector-sheet[hidden] { display: none; } +:where(#app, #netgraph-app) .node-inspector { display: grid; grid-template-rows: auto auto auto minmax(0, 1fr); } +:where(#app, #netgraph-app) .node-inspector > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(139, 205, 214, 0.11); } +:where(#app, #netgraph-app) .node-inspector > header > div { display: grid; min-width: 0; gap: 2px; } +:where(#app, #netgraph-app) .node-inspector > header strong { overflow: hidden; color: #f0ffff; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; } +:where(#app, #netgraph-app) .node-inspector > header span { color: #8ca2af; font-size: 10px; text-transform: capitalize; } +:where(#app, #netgraph-app) .node-inspector-close { width: 44px; height: 44px; flex: none; border: 0; border-radius: 9px; color: #a9bdc8; background: transparent; cursor: pointer; font-size: 24px; } +:where(#app, #netgraph-app) .node-inspector-close:hover, :where(#app, #netgraph-app) .node-inspector-close:focus-visible { background: rgba(41, 184, 169, 0.16); outline-offset: -3px; } +:where(#app, #netgraph-app) .node-inspector dl { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 0; padding: 10px 13px; } +:where(#app, #netgraph-app) .node-inspector dl div { display: grid; min-width: 0; gap: 2px; } +:where(#app, #netgraph-app) .node-inspector dt { color: #708895; font-size: 9px; font-weight: 650; text-transform: uppercase; } +:where(#app, #netgraph-app) .node-inspector dd { overflow: hidden; margin: 0; color: #d9e9ef; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } +:where(#app, #netgraph-app) .node-inspector h3 { margin: 0; padding: 6px 13px 7px; color: #8da3ae; font-size: 10px; font-weight: 700; } +:where(#app, #netgraph-app) .neighbor-list { overflow-y: auto; overscroll-behavior: contain; border-top: 1px solid rgba(139, 205, 214, 0.09); } +:where(#app, #netgraph-app) .neighbor-row { display: grid; width: 100%; min-height: 54px; gap: 1px; border: 0; border-bottom: 1px solid rgba(139, 205, 214, 0.08); padding: 7px 13px; background: transparent; text-align: left; cursor: pointer; } +:where(#app, #netgraph-app) .neighbor-row strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } +:where(#app, #netgraph-app) .neighbor-row span { color: #8298a5; font-size: 9px; text-transform: capitalize; } +:where(#app, #netgraph-app) .neighbor-row:hover, :where(#app, #netgraph-app) .neighbor-row:focus-visible { background: rgba(41, 184, 169, 0.13); } +:where(#app, #netgraph-app) .fatal { border-color: rgba(237, 114, 131, 0.35); } diff --git a/web/src/styles.css b/web/src/styles.css index d384de5..78910e9 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -75,15 +75,7 @@ button { font: inherit; } #packet-canvas { z-index: 4; } -.glass { - z-index: 5; - border: 1px solid var(--panel-border); - background: var(--panel-background); - box-shadow: var(--panel-shadow); - -webkit-backdrop-filter: blur(14px) saturate(1.18); - backdrop-filter: blur(14px) saturate(1.18); - transition: border-color 180ms ease, background-color 180ms ease, box-shadow 180ms ease; -} +.glass { border: 1px solid var(--panel-border); background: var(--panel-background); box-shadow: var(--panel-shadow); transition: border-color 180ms ease, background-color 180ms ease, box-shadow 180ms ease; } @media (pointer: coarse), (max-width: 700px) { .glass { @@ -108,68 +100,31 @@ button { font: inherit; } } .toolbar > section { flex-shrink: 0; pointer-events: auto; } -.topbar { - display: flex; - align-items: center; - gap: 18px; - min-height: 44px; - padding: 0 12px; - border-radius: 12px; -} +.topbar { gap: 18px; min-height: 44px; border-radius: 12px; } .brand, .netgraph-link, .status { display: flex; align-items: center; gap: 7px; } -.brand { min-height: 44px; border: 0; padding: 0 2px; color: #effdff; background: transparent; cursor: pointer; font-size: 13px; font-weight: 720; letter-spacing: 0.02em; } +.brand { min-height: 44px; border: 0; padding: 0 2px; color: #effdff; background: transparent; cursor: pointer; font-weight: 720; } .brand:hover, .brand:focus-visible { color: #ffffff; outline: none; } .brand:focus-visible { border-radius: 6px; box-shadow: 0 0 0 2px #63d9cc; } -.brand-mark { width: 9px; height: 9px; border: 2px solid #35cdbc; border-radius: 50%; box-shadow: 0 0 11px #36d7c2; } .netgraph-link { min-height: 44px; padding: 0 3px; color: #9ccbc8; text-decoration: none; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; } .netgraph-link:hover, .netgraph-link:focus-visible { color: var(--ink-strong); outline: none; } .netgraph-link:focus-visible { border-radius: 6px; box-shadow: 0 0 0 2px #63d9cc; } .netgraph-link > span:first-child { color: #63d9cc; text-shadow: 0 0 10px rgba(99, 217, 204, 0.68); } -.status { color: #aebdcb; font-size: 11px; font-weight: 600; } -.status-dot { width: 6px; height: 6px; border-radius: 50%; background: #86919b; } -.status[data-state="active"] .status-dot { background: #45df9a; box-shadow: 0 0 8px #45df9a; } -.status[data-state="quiet"] .status-dot { background: #70a99f; } -.status[data-state="reconnecting"] .status-dot { background: #e9c35b; animation: status-pulse 1.1s ease-in-out infinite; } -.status[data-state="offline"] .status-dot { background: #ed7283; } -.topbar.traffic-pulse .brand-mark { animation: traffic-brand 680ms ease-out; } +.status { font-weight: 600; } .topbar.traffic-pulse .status-dot { animation: traffic-status 680ms ease-out; } -.traffic-meter { - display: inline-flex; - height: 12px; - align-items: flex-end; - gap: 2px; - margin-left: 2px; -} -.traffic-meter i { - width: 2px; - height: 2px; - border-radius: 2px; - background: rgba(var(--traffic-rgb), 0.32); - box-shadow: 0 0 0 rgba(var(--traffic-rgb), 0); - transition: height 180ms ease, background-color 180ms ease, box-shadow 180ms ease; -} +.traffic-meter { margin-left: 2px; } +.traffic-meter i { background: rgba(var(--traffic-rgb), 0.32); box-shadow: 0 0 0 rgba(var(--traffic-rgb), 0); transition: height 180ms ease, background-color 180ms ease, box-shadow 180ms ease; } .traffic-meter[data-level="1"] i:nth-child(-n+1), .traffic-meter[data-level="2"] i:nth-child(-n+2), .traffic-meter[data-level="3"] i:nth-child(-n+3), .traffic-meter[data-level="4"] i:nth-child(-n+4), -.traffic-meter[data-level="5"] i:nth-child(-n+5) { - height: 10px; - background: rgba(var(--traffic-rgb), 0.9); - box-shadow: 0 0 7px rgba(var(--traffic-rgb), 0.62); -} +.traffic-meter[data-level="5"] i:nth-child(-n+5) { background: rgba(var(--traffic-rgb), 0.9); } .traffic-meter i:nth-child(2) { max-height: 8px; } -.traffic-meter i:nth-child(3) { max-height: 12px; } .traffic-meter i:nth-child(4) { max-height: 7px; } .traffic-meter i:nth-child(5) { max-height: 10px; } -.controls { - margin-left: auto; - display: flex; - overflow: visible; - border-radius: 12px; -} +.controls { margin-left: auto; border-radius: 12px; } .layers-disclosure { position: relative; } .layers-panel[hidden], @@ -177,23 +132,9 @@ button { font: inherit; } .sound-control { position: relative; display: flex; } .find-control { position: relative; display: flex; } -.control-button { - display: inline-flex; - align-items: center; - gap: 6px; - min-height: 44px; - border: 0; - border-right: 1px solid rgba(139, 205, 214, 0.12); - padding: 0 11px; - color: #b9cad7; - background: transparent; - cursor: pointer; - font-size: 12px; - font-weight: 650; - transition: color 150ms ease, background-color 150ms ease, box-shadow 180ms ease, transform 150ms ease; -} +.control-button { gap: 6px; color: #b9cad7; font-weight: 650; transition: color 150ms ease, background-color 150ms ease, box-shadow 180ms ease, transform 150ms ease; } .icon-button { border-right: 0; } -.control-button:hover, .control-button:focus-visible, .control-button.selected { color: var(--ink-strong); background: rgba(41, 184, 169, 0.14); outline: none; } +.control-button:hover, .control-button:focus-visible, .control-button.selected { color: var(--ink-strong); outline: none; } #follow-button[data-mode="director"] { color: var(--ink-strong); box-shadow: inset 0 -1px rgba(99, 217, 204, 0.45), 0 0 18px rgba(50, 200, 187, 0.08); } #app.traffic-awake #follow-button[data-mode="director"] > span:first-child { animation: director-lock 880ms ease-out; } .control-button:focus-visible { outline: 2px solid #63d9cc; outline-offset: -3px; } @@ -208,31 +149,11 @@ button { font: inherit; } .control-button[data-sound-state="resume"] .sound-state, .sound-panel[data-sound-state="resume"] header span { color: #f0c36a; } -.sound-panel { - position: absolute; - top: calc(100% + 8px); - right: 0; - z-index: 9; - display: grid; - width: 244px; - gap: 11px; - padding: 13px; - border-radius: 12px; -} +.sound-panel { position: absolute; top: calc(100% + 8px); z-index: 9; display: grid; width: 244px; border-radius: 12px; } .sound-panel[hidden] { display: none; } -.sound-panel header { display: flex; align-items: center; justify-content: space-between; color: var(--ink-strong); font-size: 12px; } -.sound-panel header span { color: #899da8; font-size: 11px; font-weight: 750; } -.sound-toggle { - min-height: 44px; - border: 1px solid rgba(99, 217, 204, 0.25); - border-radius: 9px; - color: var(--ink-strong); - background: rgba(38, 166, 155, 0.14); - cursor: pointer; - font-size: 12px; - font-weight: 700; -} -.sound-toggle:hover, .sound-toggle:focus-visible { background: rgba(38, 166, 155, 0.24); outline: 2px solid #63d9cc; outline-offset: 1px; } +.sound-panel header { color: var(--ink-strong); } +.sound-toggle { color: var(--ink-strong); font-weight: 700; } +.sound-toggle:hover, .sound-toggle:focus-visible { outline: 2px solid #63d9cc; } .scene-control { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 10px; color: var(--muted); font-size: 11px; font-weight: 650; } .scene-control select { min-height: 36px; @@ -245,52 +166,17 @@ button { font: inherit; } } .scene-control select:focus-visible { outline: 2px solid #63d9cc; outline-offset: 1px; } .volume-control { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 5px 10px; color: var(--muted); font-size: 11px; font-weight: 650; } -.volume-control output { color: var(--ink-strong); font-variant-numeric: tabular-nums; } -.volume-control input { grid-column: 1 / -1; width: 100%; min-height: 44px; accent-color: #4dcfbe; cursor: pointer; } -.sound-panel p { display: flex; align-items: center; gap: 7px; margin: 0; color: #859aa5; font-size: 10px; } -.sound-activity { width: 7px; height: 7px; flex: none; border-radius: 50%; background: #41545c; } -.sound-activity.active { background: #6ce2c5; box-shadow: 0 0 9px rgba(108, 226, 197, 0.9); } - -.find-panel { - position: absolute; - top: calc(100% + 8px); - left: 0; - z-index: 9; - display: grid; - width: min(292px, calc(100vw - 24px)); - gap: 8px; - padding: 12px; - border-radius: 12px; -} +.volume-control output { color: var(--ink-strong); } +.volume-control input { min-height: 44px; } +.find-panel { position: absolute; top: calc(100% + 8px); z-index: 9; display: grid; border-radius: 12px; } .find-panel[hidden] { display: none; } -.find-panel > label { color: var(--ink-strong); font-size: 11px; font-weight: 750; } -.find-panel input { - min-height: 44px; - width: 100%; - border: 1px solid rgba(99, 217, 204, 0.26); - border-radius: 8px; - padding: 0 11px; - color: var(--ink-strong); - background: rgba(4, 15, 20, 0.96); - font: inherit; -} +.find-panel > label { color: var(--ink-strong); font-weight: 750; } +.find-panel input { color: var(--ink-strong); font: inherit; } .find-panel input:focus-visible { outline: 2px solid #63d9cc; outline-offset: 1px; } -.node-search-results { display: grid; max-height: min(330px, 52vh); overflow-y: auto; overscroll-behavior: contain; } -.node-search-results > p { margin: 6px 2px; color: #8297a4; font-size: 11px; } -.node-search-result { - display: grid; - min-height: 48px; - gap: 2px; - border: 0; - border-top: 1px solid rgba(139, 205, 214, 0.1); - padding: 7px 8px; - color: var(--ink-strong); - background: transparent; - text-align: left; - cursor: pointer; -} -.node-search-result strong { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } -.node-search-result span { color: #8197a5; font-size: 10px; text-transform: capitalize; } +.node-search-results > p { margin: 6px 2px; } +.node-search-result { min-height: 48px; color: var(--ink-strong); } +.node-search-result strong { font-size: 12px; } +.node-search-result span { color: #8197a5; font-size: 10px; } .node-search-result:hover, .node-search-result:focus-visible, .node-search-result[aria-selected="true"] { background: rgba(41, 184, 169, 0.14); outline: none; } .node-search-result:focus-visible { box-shadow: inset 0 0 0 2px #63d9cc; } @@ -300,19 +186,7 @@ button { font: inherit; } 100% { filter: drop-shadow(0 0 0 rgba(99, 217, 204, 0)); transform: scale(1); } } -.route-window { - position: static; - display: inline-flex; - align-items: center; - gap: 6px; - min-height: 44px; - padding: 5px 8px 5px 10px; - border: 0; - border-radius: 0; - color: var(--muted); - font-size: 11px; - font-weight: 650; -} +.route-window { position: static; gap: 6px; padding: 5px 8px 5px 10px; border: 0; border-radius: 0; color: var(--muted); font-size: 11px; font-weight: 650; } .route-window select { min-height: 30px; border: 1px solid rgba(139, 205, 214, 0.18); @@ -325,18 +199,7 @@ button { font: inherit; } } .route-window select:focus-visible { outline: 2px solid #63d9cc; outline-offset: 1px; } -.legend { - position: absolute; - bottom: max(13px, env(safe-area-inset-bottom)); - left: max(12px, env(safe-area-inset-left)); - display: flex; - align-items: center; - padding: 8px 10px; - border-radius: 10px; - color: #93a5b6; - font-size: 11px; - font-weight: 600; -} +.legend { bottom: max(13px, env(safe-area-inset-bottom)); left: max(12px, env(safe-area-inset-left)); display: flex; align-items: center; padding: 8px 10px; border-radius: 10px; color: #93a5b6; font-size: 11px; font-weight: 600; } .legend-items { display: flex; align-items: center; gap: 13px; } .legend-items span, .focus-chip { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; } .legend i.node { width: 6px; height: 6px; border-radius: 50%; box-shadow: 0 0 5px currentColor; } @@ -389,23 +252,9 @@ button { font: inherit; } box-shadow: 0 0 6px var(--route-color); } -.tooltip { - position: absolute; - z-index: 6; - display: grid; - gap: 3px; - min-width: 110px; - max-width: 220px; - padding: 8px 10px; - border-radius: 8px; - transform: translateX(-50%); - pointer-events: none; -} -.tooltip strong { overflow: hidden; color: #f3ffff; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } -.tooltip span { color: #92a6b7; font-size: 9px; text-transform: capitalize; } -.tooltip[hidden] { display: none; } - - +.tooltip { z-index: 6; display: grid; gap: 3px; min-width: 110px; max-width: 220px; padding: 8px 10px; transform: translateX(-50%); } +.tooltip strong { color: #f3ffff; font-size: 11px; } +.tooltip span { color: #92a6b7; } /* Raise the popup wrapper's stacking context above traffic (4), below controls (8). */ .node-inspector-popup { z-index: 7; } .node-inspector-popup .maplibregl-popup-content { @@ -429,49 +278,15 @@ button { font: inherit; } } .node-inspector-popup .maplibregl-popup-close-button:hover, .node-inspector-popup .maplibregl-popup-close-button:focus-visible { color: var(--ink-strong); background: rgba(41, 184, 169, 0.16); outline: 2px solid #63d9cc; outline-offset: -3px; } -.node-inspector-sheet { - position: absolute; - z-index: 7; - right: 8px; - bottom: max(8px, env(safe-area-inset-bottom)); - left: 8px; - max-height: min(48vh, 420px); - overflow: hidden; - border-radius: 15px; -} -.node-inspector-sheet[hidden] { display: none; } -.node-inspector { display: grid; grid-template-rows: auto auto auto minmax(0, 1fr); min-width: 300px; max-height: min(430px, 65vh); } -.node-inspector > header { display: flex; min-height: 52px; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 46px 9px 13px; border-bottom: 1px solid rgba(139, 205, 214, 0.11); } -.node-inspector > header > div { display: grid; min-width: 0; gap: 2px; } -.node-inspector > header strong { overflow: hidden; color: #f0ffff; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; } -.node-inspector > header span { color: #8ca2af; font-size: 10px; text-transform: capitalize; } -.node-inspector-close { width: 44px; height: 44px; flex: none; border: 0; border-radius: 9px; color: #a9bdc8; background: transparent; cursor: pointer; font-size: 24px; } -.node-inspector-close:hover, .node-inspector-close:focus-visible { color: var(--ink-strong); background: rgba(41, 184, 169, 0.16); outline: 2px solid #63d9cc; outline-offset: -3px; } -.node-inspector dl { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 0; padding: 10px 13px; } -.node-inspector dl div { display: grid; min-width: 0; gap: 2px; } -.node-inspector dt { color: #708895; font-size: 9px; font-weight: 650; text-transform: uppercase; } -.node-inspector dd { overflow: hidden; margin: 0; color: #d9e9ef; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } -.node-inspector h3 { margin: 0; padding: 6px 13px 7px; color: #8da3ae; font-size: 10px; font-weight: 700; } -.neighbor-list { overflow-y: auto; overscroll-behavior: contain; border-top: 1px solid rgba(139, 205, 214, 0.09); } -.neighbor-row { display: grid; width: 100%; min-height: 54px; gap: 1px; border: 0; border-bottom: 1px solid rgba(139, 205, 214, 0.08); padding: 7px 13px; color: var(--ink-strong); background: transparent; text-align: left; cursor: pointer; } -.neighbor-row strong { overflow: hidden; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } -.neighbor-row span { color: #8298a5; font-size: 9px; text-transform: capitalize; } -.neighbor-row:hover, .neighbor-row:focus-visible { background: rgba(41, 184, 169, 0.13); outline: none; } +.node-inspector-sheet { right: 8px; bottom: max(8px, env(safe-area-inset-bottom)); left: 8px; max-height: min(48vh, 420px); border-radius: 15px; } +.node-inspector { min-width: 300px; max-height: min(430px, 65vh); } +.node-inspector > header { min-height: 52px; padding: 10px 46px 9px 13px; } +.node-inspector-close:hover, .node-inspector-close:focus-visible { color: var(--ink-strong); outline: 2px solid #63d9cc; } +.neighbor-row { color: var(--ink-strong); } +.neighbor-row:hover, .neighbor-row:focus-visible { outline: none; } .neighbor-row:focus-visible { box-shadow: inset 0 0 0 2px #63d9cc; } -.fatal { - position: absolute; - z-index: 8; - top: 50%; - left: 50%; - max-width: min(420px, calc(100% - 32px)); - padding: 14px 18px; - border-color: rgba(237, 114, 131, 0.35); - border-radius: 12px; - color: #ffd7dc; - font-size: 12px; - transform: translate(-50%, -50%); -} +.fatal { position: absolute; z-index: 8; top: 50%; left: 50%; max-width: min(420px, calc(100% - 32px)); padding: 14px 18px; border-radius: 12px; color: #ffd7dc; font-size: 12px; transform: translate(-50%, -50%); } .fatal[hidden] { display: none; } .about-dialog {