From 484243fe4b4a99b924b4a76b488df38788b900fd Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sat, 31 Oct 2026 17:28:46 -0700 Subject: [PATCH] feat(ui): give every panel one scrollbar, select and slider skin Every scrolling surface carries the kit's .ui-scroll, which draws the thin themed scrollbar from --scroll-thumb and --scroll-track; the Context panel and others drew the platform's light scrollbar because the page declares no dark color-scheme. The per-panel scrollbar rules go; Cyber sets the two tokens and Cockpit's HUD windows keep their accent tint. Selects draw .gev-field's own chevron instead of the platform arrow, the voice selects join .gev-field, number fields keep dark spinners, and the Street Level range uses the shared slider in its green. --- docs/VUE-MIGRATION.md | 10 +++++++ src/contextTabKeyboard.test.mjs | 4 +-- src/data/bhoteKoshiEmbeddedMedia.js | 2 +- src/data/bhoteKoshiEmbeddedMedia.test.mjs | 2 +- src/streetLevelMarkup.test.mjs | 2 +- src/ui/cyberTheme.test.mjs | 8 +++-- src/ui/localePicker.js | 2 +- src/ui/rail/RailRowList.vue | 2 +- src/ui/recentImagery/RecentImageryReadout.vue | 5 +++- src/ui/styles/bhote-koshi.css | 2 -- src/ui/styles/cctv.css | 21 -------------- src/ui/styles/cyber.css | 12 +++++--- src/ui/styles/first-run.css | 13 ++------- src/ui/styles/form-field.css | 19 ++++++++++++ src/ui/styles/foundation.css | 3 ++ src/ui/styles/kit.css | 29 +++++++++++++++++++ src/ui/styles/layers.css | 24 --------------- src/ui/styles/panel-surfaces.css | 2 +- src/ui/styles/provider-settings.css | 10 ------- src/ui/styles/radio.css | 11 ++++--- src/ui/styles/recent-imagery.css | 3 -- src/ui/styles/scenes.css | 9 ------ src/ui/styles/street-level.css | 15 ++-------- src/ui/styles/voice-card.css | 6 ++-- src/ui/styles/voice-cost.css | 5 ++-- src/ui/styles/weather.css | 2 -- src/ui/views/CommandDockView.vue | 2 +- src/ui/views/ContextView.vue | 10 +++---- src/ui/views/LayerPanelsView.vue | 6 ++-- src/ui/views/ProviderSettingsView.vue | 2 +- src/ui/views/SceneShotList.vue | 2 +- src/ui/views/WelcomeView.vue | 2 +- src/ui/views/panels/AwarenessNames.vue | 2 +- src/ui/views/panels/AwarenessPanel.vue | 2 +- src/ui/views/panels/CctvPanel.vue | 4 +-- .../views/panels/CockpitUtilityControls.vue | 6 ++-- src/ui/views/panels/DisplayPanel.vue | 4 +-- src/ui/views/panels/MissionDetail.vue | 4 +-- src/ui/views/panels/MissionRoster.vue | 2 +- src/ui/views/panels/RadioPanel.vue | 2 +- src/ui/views/panels/StreetLevelPanel.vue | 4 +-- src/voice/control.js | 8 ++--- 42 files changed, 133 insertions(+), 152 deletions(-) diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index 9db7fcbfc..f88ade519 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -42,6 +42,16 @@ one-panel rule too. Share links continue to record which panels are open. this repository and styled by its existing stylesheet and custom properties. They contain no application state. +Controls share one skin, so every panel reads the same. Every scrolling +surface carries the kit's `.ui-scroll` (ui-vue's name), which draws the thin +themed scrollbar from `--scroll-thumb` and `--scroll-track`; the Cyber theme +sets those two tokens, and Cockpit's HUD windows keep their accent tint. A +surface without it would draw the platform's light scrollbar, because the +page declares no dark `color-scheme`. Selects and number fields are +`.gev-field`, which draws its own chevron in place of the platform's arrow +and keeps native spinners dark; ranges are `.gev-quantitative-slider`, with +a feature supplying only its accent. + ## Phases 1. **Foundation.** Vue, Pinia and the Vite Vue plugin join the browser build diff --git a/src/contextTabKeyboard.test.mjs b/src/contextTabKeyboard.test.mjs index 05e013e96..adc176196 100644 --- a/src/contextTabKeyboard.test.mjs +++ b/src/contextTabKeyboard.test.mjs @@ -95,9 +95,9 @@ test('Context async action buttons remain focused while busy', () => { }); test('Context header holds only its title and collapse control', () => { - const panel = html.match(/
([\s\S]*?)<\/div>\s*
([\s\S]*?)<\/div>\s*
`${tag}.${cls}`); assert.deepEqual(tags, ['span.panel-title', 'span.panel-divider', 'button.panel-collapse-btn']); diff --git a/src/data/bhoteKoshiEmbeddedMedia.js b/src/data/bhoteKoshiEmbeddedMedia.js index 1ea0ccec9..53e21f8f4 100644 --- a/src/data/bhoteKoshiEmbeddedMedia.js +++ b/src/data/bhoteKoshiEmbeddedMedia.js @@ -1054,7 +1054,7 @@ export function createBhoteKoshiEmbeddedMedia({ leader.innerHTML = ''; const player = documentRef.createElement('div'); - player.className = 'bhote-embedded-callout-player'; + player.className = 'bhote-embedded-callout-player ui-scroll'; const footer = documentRef.createElement('footer'); const title = documentRef.createElement('strong'); title.textContent = bhoteEventText( diff --git a/src/data/bhoteKoshiEmbeddedMedia.test.mjs b/src/data/bhoteKoshiEmbeddedMedia.test.mjs index 6829adca5..af6afc1e8 100644 --- a/src/data/bhoteKoshiEmbeddedMedia.test.mjs +++ b/src/data/bhoteKoshiEmbeddedMedia.test.mjs @@ -348,7 +348,7 @@ test('camera updates retain a preload until the visible player loads', () => { assert.equal(frame.parent, root, 'keep the preload while the visible player starts'); const card = root.children.find(node => node.tag === 'section'); const body = card.children.find(node => node.className === 'bhote-embedded-callout-body'); - const player = body.children.find(node => node.className === 'bhote-embedded-callout-player'); + const player = body.children.find(node => node.className.split(' ').includes('bhote-embedded-callout-player')); player.children.find(node => node.tag === 'iframe').dispatchEvent(new Event('load')); assert.equal(frame.parent, null, 'visible player readiness releases the preload'); media.destroy(); diff --git a/src/streetLevelMarkup.test.mjs b/src/streetLevelMarkup.test.mjs index bc1716f14..18a61de42 100644 --- a/src/streetLevelMarkup.test.mjs +++ b/src/streetLevelMarkup.test.mjs @@ -32,7 +32,7 @@ test('Street Level is a collapsible right-rail panel that starts collapsed', () assert.doesNotMatch(html, /sl-mark|mly-mark/); assert.match( html, - /
/, + /
/, ); assert.doesNotMatch( html, diff --git a/src/ui/cyberTheme.test.mjs b/src/ui/cyberTheme.test.mjs index c31eeba8c..1433da97a 100644 --- a/src/ui/cyberTheme.test.mjs +++ b/src/ui/cyberTheme.test.mjs @@ -22,10 +22,14 @@ test('CCTV and Context keep their headers outside Cyber-only scroll bodies', () new URL(`./views/${file}`, import.meta.url), 'utf8', ); - assert.match(markup, /class="cyber-panel-body" data-rail-scroller/); + // The body wears the shared scrollbar skin where Cyber makes it scroll. + assert.match( + markup, + /class="cyber-panel-body ui-scroll" data-rail-scroller/, + ); assert.ok( markup.indexOf('class="panel-header"') < - markup.indexOf('class="cyber-panel-body"'), + markup.indexOf('class="cyber-panel-body ui-scroll"'), ); } const shared = readFileSync( diff --git a/src/ui/localePicker.js b/src/ui/localePicker.js index 41875042d..f21559e1d 100644 --- a/src/ui/localePicker.js +++ b/src/ui/localePicker.js @@ -431,7 +431,7 @@ export function mountLocalePicker({ heading.textContent = t('Interface language'); popup.append(heading); - const listbox = element(documentRef, 'ul', 'locale-picker-list'); + const listbox = element(documentRef, 'ul', 'locale-picker-list ui-scroll'); listbox.id = ids.listbox; listbox.setAttribute('role', 'listbox'); listbox.setAttribute('aria-labelledby', ids.heading); diff --git a/src/ui/rail/RailRowList.vue b/src/ui/rail/RailRowList.vue index 059244207..8778b059c 100644 --- a/src/ui/rail/RailRowList.vue +++ b/src/ui/rail/RailRowList.vue @@ -35,7 +35,7 @@ onUpdated(followCurrent);