diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b42a7bc..22f5257d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ (`./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 + publish the panel's state to a store that the CCTV and Radio views render, + instead of writing the panel's markup themselves (samehkhamis). + ## [0.3.0] — 2026-10-27 — Street Level, On Your Phone, in Your Language - Show a Street Level view facing 359.5° or more as 0°. The viewer caption diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index 8f2f7ac4f..cc7d11622 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -102,7 +102,11 @@ They contain no application state. style parameter rows render through Vue behind `createStyleParameters()`, as a render function so the portable `./ui/input` entry needs no component compiler; the other groups follow one owner at a time), - Context and Radio, the command dock, + Context and Radio (in progress: `RadioPanel` renders the directory row, + station card, transport, volume and playback line from the `gev-radio` + store that RadioControls publishes to; the tuner, the local SDR card and + the Cockpit radio controls keep their owners' element bindings), the + command dock, the phone shell, then Cockpit. Layers that write into the right rail directly gain a slot instead. diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 84cec6de3..a78030afc 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -24,6 +24,7 @@ "src/ui/views/CockpitView.vue", "src/ui/views/CommandDockView.vue", "src/ui/views/ContextView.vue", + "src/ui/views/panels/RadioPanel.vue", "src/ui/views/DisplayControlsView.vue", "src/ui/views/LayerPanelsView.vue", "src/ui/views/ProviderSettingsView.vue", @@ -50,11 +51,13 @@ "src/ui/cctv/CctvCalValue.vue", "src/ui/cctv/CctvPicker.vue", "src/ui/stores/cctv.js", + "src/ui/stores/radio.js", "src/ui/stores/streetLevel.js", "src/ui/rail/RailChips.vue", "src/ui/views/panels/DisplayPanel.vue", "src/ui/stores/display.js", - "src/ui/rail/rangeValue.js" + "src/ui/rail/rangeValue.js", + "src/ui/rail/classToggle.js" ], "external": ["vue", "pinia"] }, @@ -622,6 +625,8 @@ "src/ui/radioControls.js", "src/ui/radioBindings.js", "src/ui/radioPresentation.js", + "src/ui/vueRuntime.js", + "src/ui/stores/radio.js", "src/ui/radioTunerModel.js", "src/i18n/index.js", "src/i18n/locale.js", @@ -636,7 +641,7 @@ "src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-Hant.json" ], - "external": [] + "external": ["vue", "pinia"] }, "cctv-controls": { "exports": ["./ui/cctv"], @@ -959,6 +964,7 @@ "src/ui/weather/WeatherReadout.vue", "src/ui/stores/streetLevel.js", "src/ui/stores/cctv.js", + "src/ui/stores/radio.js", "src/ui/stores/display.js" ], "external": ["cesium", "vue", "pinia"] @@ -2927,6 +2933,7 @@ "src/ui/recentImagery/RecentImageryReadout.vue", "src/ui/stores/streetLevel.js", "src/ui/stores/cctv.js", + "src/ui/stores/radio.js", "src/ui/stores/display.js" ], "external": [ diff --git a/scripts/qa-radio.mjs b/scripts/qa-radio.mjs index a71c69099..366557185 100644 --- a/scripts/qa-radio.mjs +++ b/scripts/qa-radio.mjs @@ -2957,7 +2957,7 @@ async function main() { await page.evaluate(async () => { await window.__godsEyeView.dataManager.layers.get('radio').module.update(); }); - const tunerRefreshRelease = await page.evaluate((target) => { + const tunerRefreshRelease = await page.evaluate(async (target) => { const gev = window.__godsEyeView; const slider = document.getElementById('radio-tuner-slider'); const displayedBeforeRelease = document.getElementById('radio-tuner-station').textContent; @@ -2970,6 +2970,10 @@ async function main() { clientY: target.clientY, })); const state = gev.dataManager.layers.get('radio').module.getUIState(); + // The playback line renders from the Radio store once Vue flushes. + const message = await Promise.resolve().then( + () => document.getElementById('radio-playback-state').textContent, + ); return { displayedBeforeRelease, frozenGeneration, @@ -2978,7 +2982,7 @@ async function main() { plays: window.__qaRadioPlayCalls.length, value: document.getElementById('radio-tuner-value').textContent, stationText: document.getElementById('radio-tuner-station').textContent, - message: document.getElementById('radio-playback-state').textContent, + message, targetId: target.id, }; }, tunerRefreshTarget); diff --git a/src/contextTabKeyboard.test.mjs b/src/contextTabKeyboard.test.mjs index cf221756a..07386647e 100644 --- a/src/contextTabKeyboard.test.mjs +++ b/src/contextTabKeyboard.test.mjs @@ -130,16 +130,20 @@ test('Context tabs draw a visible keyboard-focus outline including active tabs', test('Context async action buttons remain focused while busy', () => { const init = _initGlobalContextPanel.toString(); const radio = radioBindings.slice(radioBindings.indexOf('const toggleRadio = async (trigger) => {'), radioBindings.indexOf('this.listen(this._cockpitRadioToggleBtn,')); - const radioSync = radioPresentation.slice(0, radioPresentation.indexOf('if (this._radioFilter)')); + const radioSync = radioPresentation.slice(0, radioPresentation.indexOf('this._radioView = {')); + const radioPanel = readFileSync(new URL('./ui/views/panels/RadioPanel.vue', import.meta.url), 'utf8'); const clear = clearSelectedLayers.toString(); assert.match(init, /button\.getAttribute\('aria-busy'\) === 'true'/); assert.doesNotMatch(init, /button\.disabled\s*=\s*true/); assert.match(radio, /trigger\.getAttribute\('aria-busy'\) === 'true'/); assert.doesNotMatch(radio, /trigger\.disabled\s*=\s*true/); - for (const name of ['_radioEnableBtn', '_cockpitRadioEnableBtn']) { - assert.match(radioSync, new RegExp(`${name}\\.disabled = false`)); - } + assert.match(radioSync, /_cockpitRadioEnableBtn\.disabled = false/); + // The Radio view never disables its Enable button; it marks it busy. + const enable = radioPanel.match(/