From d98a7c81be0012480a5713fe2385a8aa13e1c7b0 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 1 Nov 2026 11:59:30 -0800 Subject: [PATCH] feat(kit): render the pressed toggles with UiButton UiButton gains pressedClass, Core's class on a pressed button. The pressed classes toggle through classList, so classes other code adds survive a press, a caller's class change keeps them, a renamed pressedClass leaves the button, and server-rendered markup carries them. The Display, SDR, Radio and Cockpit enable, Cockpit weather and briefing, mission replay, Street Level, TR-3B, overlay, voice and CCTV toggles now render through it with the same ids, classes and aria-pressed values. --- CHANGELOG.md | 7 +- docs/VUE-MIGRATION.md | 10 +- scripts/format-scope.json | 2 + src/cockpitMarkup.test.mjs | 4 +- src/contextTabKeyboard.test.mjs | 4 +- src/data/layerState.test.mjs | 4 +- src/radioMarkup.test.mjs | 4 +- src/ui/displayPanel.test.mjs | 15 +-- src/ui/kit/UiButton.test.mjs | 102 ++++++++++++++++++ src/ui/kit/UiButton.vue | 37 +++++-- src/ui/radioPanel.test.mjs | 13 ++- src/ui/views/ContextView.vue | 3 +- src/ui/views/SceneChromeView.vue | 3 +- src/ui/views/panels/CctvPanel.vue | 10 +- src/ui/views/panels/CockpitBriefing.vue | 3 +- src/ui/views/panels/CockpitContext.vue | 5 +- .../views/panels/CockpitUtilityControls.vue | 3 +- src/ui/views/panels/DisplayPanel.vue | 19 ++-- src/ui/views/panels/MissionDetail.vue | 3 +- src/ui/views/panels/RadioPanel.vue | 3 +- src/ui/views/panels/SdrCard.vue | 7 +- src/ui/views/panels/StreetLevelPanel.vue | 4 +- src/ui/views/panels/VoiceControl.vue | 9 +- 23 files changed, 212 insertions(+), 62 deletions(-) create mode 100644 src/ui/kit/UiButton.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f2fa3051..6f276b151 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,9 +64,10 @@ `connect`/`act`; the card labels its gain options itself. `act` returns what the answering action returned. The Display, Radio, RTL-SDR, Cockpit, mission replay and style parameter sliders are the kit's `UiSlider`, a - native range input that emits numbers. `syncScopeControls` (`./ui/display`) - and `shouldHideCollapsedRightPanels` (`./ui/layout`) are deprecated: the - Display view and the rail now own what they did. `./ui/input` imports Vue + native range input that emits numbers, and the pressed toggles are the + kit's `UiButton`, which gains `pressedClass`. `syncScopeControls` + (`./ui/display`) and `shouldHideCollapsedRightPanels` (`./ui/layout`) are + deprecated: the Display view and the rail now own what they did. `./ui/input` imports Vue (samehkhamis). - `./ui/cctv` and `./ui/radio` import Vue and Pinia: their controllers diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index bb4d4739a..0aeec0ac2 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -37,8 +37,9 @@ one-panel rule too. Share links continue to record which panels are open. ## Component kit `src/ui/kit/` holds the generic components: `UiRail`, `UiTooltip`, `UiIcon`, -`UiButton`, `UiPanel` and `UiSegmented` so far, with more (`UiSlider`, -`UiSelect` and so on) joining as the controls they serve are converted. One component +`UiButton`, `UiPanel`, `UiSegmented` and `UiSlider` so far, with more +(`UiSelect`, `UiChip` and so on) joining as the controls they serve are +converted. One component serves every use of a control, with props for the differences (`UiPanel`'s `sideways`, `glow`, `scroll`), rather than a copy per panel. Names, props, events and class names follow Halfpixel's `ui-vue` kit so the two can merge @@ -56,6 +57,11 @@ Home and End), since each of Core's groups keeps its own keyboard policy, and once and asks for a change only when it lands on another option. Every segmented group in Core is a `UiSegmented`; the old `segmentedControl.js` adapter is gone. +`UiButton` adds `pressedClass`, Core's class on a pressed toggle (most use +`active`); its pressed classes toggle through `classList`, so a class other +code adds (a voice highlight) survives a press. Every pressed toggle in the +views is a `UiButton` except Bhote Koshi's cinematic button and the +tilt-map button, whose owners still write their state into the DOM. `UiSlider` keeps the native range input where ui-vue draws a `role="slider"` track, so Core's keyboard, form behaviour and styling stay; it takes ui-vue's `v-model`, `min`, `max`, `step` and `disabled`, emits a diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 654513258..d8814419c 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1130,6 +1130,8 @@ "src/ui/kit/UiSegmented.test.mjs", "src/ui/kit/UiSlider.js", "src/ui/kit/UiSlider.test.mjs", + "src/ui/kit/UiButton.vue", + "src/ui/kit/UiButton.test.mjs", "src/ui/displayPanelActions.test.mjs", "src/testSupport/vueDom.mjs", "build/vueLoader.js", diff --git a/src/cockpitMarkup.test.mjs b/src/cockpitMarkup.test.mjs index 7cc4a73ac..c6623549b 100644 --- a/src/cockpitMarkup.test.mjs +++ b/src/cockpitMarkup.test.mjs @@ -1176,7 +1176,7 @@ test('cockpit aircraft handoff invalidates the prior world-position anchor', () test('cockpit weather control is off before JavaScript restores an explicit opt-in', () => { const match = html.match( - //, + /]*\sid="cockpit-weather-toggle"[\s\S]*?<\/button>/, ); assert.ok(match, 'cockpit weather toggle markup is missing'); assert.match(match[0], /aria-label="Enable cockpit weather effects"/); @@ -1262,7 +1262,7 @@ test('Global Context standby describes both chooser modes', () => { test('cockpit briefing cycle control keeps its state as the accessible name', () => { const match = html.match( - //, + /]*\sid="cockpit-brief-auto"[\s\S]*?<\/button>/, ); assert.ok(match, 'cockpit briefing cycle toggle is missing'); assert.match(match[0], /aria-label="CYCLE OFF"/); diff --git a/src/contextTabKeyboard.test.mjs b/src/contextTabKeyboard.test.mjs index 59ecf569d..868122ec9 100644 --- a/src/contextTabKeyboard.test.mjs +++ b/src/contextTabKeyboard.test.mjs @@ -78,12 +78,12 @@ test('Context async action buttons remain focused while busy', () => { assert.match(radio, /trigger\.getAttribute\('aria-busy'\) === 'true'/); assert.doesNotMatch(radio, /trigger\.disabled\s*=\s*true/); const cockpitRadio = readFileSync(new URL('./ui/views/panels/CockpitUtilityControls.vue', import.meta.url), 'utf8'); - const cockpitEnable = cockpitRadio.match(/