diff --git a/docs/CODE-BOUNDARIES.md b/docs/CODE-BOUNDARIES.md index 1e039fa1b..ed77a4ecf 100644 --- a/docs/CODE-BOUNDARIES.md +++ b/docs/CODE-BOUNDARIES.md @@ -372,12 +372,10 @@ facade's asynchronous teardown can yield. ## Ordinary action buttons -`src/ui/actionButton.js` is a native, listener-free view helper used by Scene -presentation and sharing-toolbar owners. It receives translated text and explicit -presentation/accessibility properties. Feature controllers own action dispatch, -state, persistence and cleanup. Static Scenes/CCTV templates use the same CSS -surface and preserve IDs and layout hooks. The helper imports no application, -renderer, storage or services; it belongs to the consuming package closures. +The scene panels view and the CCTV panel draw their action buttons with the shared +`gev-action-button` classes in `src/ui/styles/action-button.css`. Feature +controllers own action dispatch, state, persistence and cleanup, and the views +preserve IDs and layout hooks. ## Display controls @@ -759,11 +757,9 @@ field rules separate, so Radio's palette remains an intentional exception. ### Read-only status badges -`src/ui/statusBadge.js` constructs/updates ordinary badges without listeners, -status inference or live-region policy. It consumes translated text and controlled -visibility; appearance variants share CSS rather than mapping machine states to -universal severity. CCTV owns frame/source and calibration rules and their data -attributes, while Weather owns coverage labels. The keyed rail-card owner keeps -node identity, header focus, disclosure and teardown. The shared adapter never -creates another state store or announces all card updates. A Vue view can replace -this adapter and retain these presentation inputs and styles. +Views draw read-only badges with the `gev-status-badge` classes, without +listeners, status inference or live-region policy. Appearance variants share +CSS rather than mapping machine states to universal severity. CCTV owns +frame/source and calibration rules and their data attributes, while Weather +owns coverage labels. The rail card keeps node identity, header focus, +disclosure and teardown, and does not announce every card update. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 1c7d3a347..dda5a86e3 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1460,11 +1460,9 @@ boundaries preserve the current Tactical/Cyber appearance and native behavior. ## Ordinary status badges CCTV source/frame labels, calibration provenance and Weather coverage labels use -`ui/statusBadge.js` and the `gev-status-badge` CSS surface. The listener-free -adapter accepts already-translated `label` and `hidden` values; pill, box and plain -appearances describe shape/density only. Static CCTV markup uses the same classes -as generated rail-card badges. Repeated identical updates do not rewrite text or -replace nodes. Empty coverage remains hidden. +the `gev-status-badge` CSS surface, drawn by the CCTV view and the rail card from +already-translated labels; pill, box and plain appearances describe +shape/density only. Empty coverage remains hidden. Feature owners retain the status rules, machine-state attributes, translations and announcement policy. CCTV frame acquisition (`idle/loading/error/ready`) @@ -1474,18 +1472,16 @@ prior poses; unsaved edits override the label until save/reset. Coverage labels remain geographic scope, not health claims. Existing colors and the source badge's polite live region are preserved; coverage and calibration do not gain live regions. Interactive layer toggles, progress indicators and world overlays -retain their specialized ownership. Vue can reuse the view contract and CSS while -replacing DOM construction. Browser acceptance: +retain their specialized ownership. Browser acceptance: `QA_BASE_URL=http://127.0.0.1:4189 node scripts/qa-status-badges.mjs`. ## Ordinary action buttons Scene panel actions, shot LOAD/DEL actions, the EDIT DETAILS / SHARE SCENE -bar and one-shot CCTV actions use `ui/actionButton.js` and the shared -`gev-action-button` surface in `ui/styles/action-button.css`. Static templates -apply the same classes as generated buttons. The native builder accepts translated -text, default/danger variants, default/compact sizes and disabled/accessibility -properties; it installs no listeners and owns no application state. Feature +bar and one-shot CCTV actions use the shared `gev-action-button` surface in +`ui/styles/action-button.css`, with default/danger variants and +default/compact sizes, drawn by the scene panels view (`LayerPanelsView.vue`, +`SceneShotList.vue`) and the CCTV panel. Feature controllers retain callbacks, enable/disable rules, confirmation prompts, persistence and teardown. The smaller shot actions retain their existing sizing and hover treatment. Feature CSS still owns panel layout and translated wrapping. @@ -1493,8 +1489,7 @@ and hover treatment. Feature CSS still owns panel layout and translated wrapping CCTV enable, coverage, auto-hop, projection and adjustment toggles, editable calibration-value chips and the scene authoring dialogs retain their specialized controls. Existing `scene-btn` hooks remain compatible with Street Level and -legacy toggles. The shared CSS, explicit input/action boundary and behavior tests -can carry to a Vue view while Vue replaces native DOM construction. +legacy toggles. Browser acceptance: `QA_BASE_URL=http://127.0.0.1:4179 node scripts/qa-action-buttons.mjs`; `qa-scene-controls.mjs` also covers real Scene authoring/playback with native Space capture. diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 4273bc39a..7d6e12c2e 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1093,7 +1093,6 @@ "server/providers/overpass/requestAdmission.test.mjs", "src/tooling/trackArrivalObservation.test.mjs", "src/ui/styles/status-badge.css", - "src/ui/statusBadge.test.mjs", "src/ui/cctvBadges.test.mjs", "scripts/qa-status-badges.mjs", "src/dockerTrustedPeers.test.mjs", diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 25e564755..5afe95f18 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -609,7 +609,6 @@ "src/ui/unitLabels.js", "src/ui/keyGuidance.js", "src/ui/statusText.js", - "src/ui/statusBadge.js", "src/ui/rail/RailChips.vue", "src/ui/rail/RailRowList.vue", "src/ui/rail/RailCardSetting.vue", @@ -695,7 +694,6 @@ "src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-Hant.json", "src/ui/unitLabels.js", - "src/ui/statusBadge.js", "src/ui/vueRuntime.js", "src/ui/stores/cctv.js" ], @@ -787,7 +785,6 @@ "src/ui/stores/scenes.js", "src/ui/views/SceneOptions.vue", "src/ui/views/SceneShotList.vue", - "src/ui/actionButton.js", "src/i18n/index.js", "src/i18n/locale.js", "src/i18n/locales/de.json", @@ -931,7 +928,6 @@ "src/ui/stores/scenes.js", "src/ui/views/SceneOptions.vue", "src/ui/views/SceneShotList.vue", - "src/ui/actionButton.js", "src/ui/scenes.js", "src/ui/shareFollow.js", "src/share/follow.js", @@ -977,7 +973,6 @@ "src/ui/keyGuidance.js", "src/ui/layerNames.js", "src/ui/statusText.js", - "src/ui/statusBadge.js", "src/scopeMaskPolicy.js", "src/visualStylePolicy.js", "src/ui/stores/panels.js", @@ -1090,7 +1085,6 @@ "src/ui/stores/scenes.js", "src/ui/views/SceneOptions.vue", "src/ui/views/SceneShotList.vue", - "src/ui/actionButton.js", "src/contextModePolicy.js", "src/director/clock.js", "src/director/timeline.js", @@ -2641,7 +2635,6 @@ "src/ui/stores/scenes.js", "src/ui/views/SceneOptions.vue", "src/ui/views/SceneShotList.vue", - "src/ui/actionButton.js", "src/ui/scenes.js", "src/ui/shareFollow.js", "src/ui/shareRestoration.js", @@ -2956,7 +2949,6 @@ "src/voice/geminiSession.js", "src/voice/geminiAudio.js", "src/voice/geminiCaptureWorklet.js", - "src/ui/statusBadge.js", "src/sceneCommandRelease.js", "src/scopeMaskPolicy.js", "src/visualStylePolicy.js", diff --git a/src/ui/actionButton.js b/src/ui/actionButton.js deleted file mode 100644 index f1b4b85f9..000000000 --- a/src/ui/actionButton.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Create an ordinary native action button. Static templates use the same CSS - * classes. Labels are already translated; feature owners retain callbacks, - * disabled-state updates and listener cleanup. Construction binds no listeners. - * @param {object} options - * @param {Document} [options.documentRef] Document that owns the button. - * @param {string} options.label Visible text, never interpreted as markup. - * @param {string} [options.className] Additional feature/layout hooks. - * @param {'default'|'danger'} [options.variant] - * @param {'default'|'compact'} [options.size] - * @param {boolean} [options.disabled] - * @param {string} [options.ariaLabel] Accessible name when visible text needs context. - * @param {string} [options.title] - * @returns {HTMLButtonElement} - */ -export function createActionButton({ - documentRef = globalThis.document, - label, - className = '', - variant = 'default', - size = 'default', - disabled = false, - ariaLabel, - title, -}) { - const button = documentRef.createElement('button'); - button.type = 'button'; - button.className = [ - 'gev-action-button', - variant === 'danger' && 'gev-action-button--danger', - size === 'compact' && 'gev-action-button--compact', - className, - ] - .filter(Boolean) - .join(' '); - button.textContent = label; - button.disabled = disabled; - if (ariaLabel !== undefined) button.setAttribute('aria-label', ariaLabel); - if (title !== undefined) button.title = title; - return button; -} diff --git a/src/ui/statusBadge.js b/src/ui/statusBadge.js deleted file mode 100644 index ec7f3019a..000000000 --- a/src/ui/statusBadge.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Ordinary read-only badge view. Labels are already translated. Feature owners - * retain status derivation, machine-state attributes and announcement policy. - * No listeners, timers, state inference or automatic live regions are installed. - * Vue can replace this DOM adapter while retaining the label/hidden contract - * and the gev-status-badge CSS surface. - * @param {object} options - * @param {Document} [options.documentRef] - * @param {string} [options.label] - * @param {'pill'|'box'|'plain'} [options.appearance] - * @param {string} [options.className] Feature/layout hooks. - * @param {boolean} [options.hidden] - * @returns {HTMLSpanElement} - */ -export function createStatusBadge({ - documentRef = globalThis.document, - label = '', - appearance = 'pill', - className = '', - hidden = false, -} = {}) { - const badge = documentRef.createElement('span'); - badge.className = [ - 'gev-status-badge', - appearance === 'box' && 'gev-status-badge--box', - appearance === 'plain' && 'gev-status-badge--plain', - className, - ] - .filter(Boolean) - .join(' '); - updateStatusBadge(badge, { label, hidden }); - return badge; -} - -/** Paint controlled text/visibility without replacing nodes or redundant writes. */ -export function updateStatusBadge(badge, { label, hidden = false }) { - if (badge.textContent !== label) badge.textContent = label; - if (badge.hidden !== hidden) badge.hidden = hidden; -} diff --git a/src/ui/statusBadge.test.mjs b/src/ui/statusBadge.test.mjs deleted file mode 100644 index c1c1b782b..000000000 --- a/src/ui/statusBadge.test.mjs +++ /dev/null @@ -1,82 +0,0 @@ -import '../testSupport/registerDom.mjs'; -import test from 'node:test'; -import assert from 'node:assert/strict'; -import { assertSameNode } from '../testSupport/domAssert.mjs'; -import { createStatusBadge, updateStatusBadge } from './statusBadge.js'; -import { createRailCards } from './railCards.js'; -import { railDocumentFixture as railFixture } from './railTestFixture.mjs'; - -test('badges paint literal translated text without installing announcement or interaction semantics', () => { - const { document } = railFixture(); - for (const appearance of ['pill', 'box', 'plain']) { - const badge = createStatusBadge({ - documentRef: document, - appearance, - className: 'feature-hook', - label: ' · НЕИЗВЕСТНО', - }); - assert.equal(badge.tagName, 'SPAN'); - assert.equal( - badge.textContent, - ' · НЕИЗВЕСТНО', - ); - assert.equal(badge.children.length, 0); - assert.equal(badge.classList.contains('gev-status-badge'), true); - assert.equal(badge.classList.contains('feature-hook'), true); - assert.equal(badge.getAttribute('role'), null); - assert.equal(badge.getAttribute('aria-live'), null); - assert.equal(badge.getAttribute('tabindex'), null); - assert.equal( - badge.classList.contains(`gev-status-badge--${appearance}`), - appearance !== 'pill', - ); - } -}); - -test('painting preserves feature state, live regions and focus with zero identical writes', () => { - const f = railFixture(); - const { document } = f; - const badge = createStatusBadge({ documentRef: document, hidden: true }); - f.container.appendChild(badge); - badge.setAttribute('role', 'status'); - badge.setAttribute('aria-live', 'polite'); - badge.dataset.frameState = 'ready'; - const focusOwner = document.createElement('button'); - document.body.appendChild(focusOwner); - focusOwner.focus(); - updateStatusBadge(badge, { label: 'SNAPSHOT · DEGRADED' }); - assert.equal(badge.hidden, false); - f.writes(); - updateStatusBadge(badge, { label: 'SNAPSHOT · DEGRADED' }); - assert.equal(f.writes(), 0); - assert.equal(badge.getAttribute('role'), 'status'); - assert.equal(badge.getAttribute('aria-live'), 'polite'); - assert.equal(badge.dataset.frameState, 'ready'); - assertSameNode(document.activeElement, focusOwner); -}); - -test('coverage badges keep keyed identity, hide empty coverage and never claim feed health', () => { - const f = railFixture(); - const view = createRailCards({ ...f, badgeClassName: 'weather-coverage' }); - const card = { id: 'radar', title: 'Rain radar', badge: 'CONUS', blocks: [] }; - view.update([card]); - const badge = f.find((node) => node.classList.contains('gev-status-badge')); - const header = f.container.children[0].children[0]; - header.focus(); - assert.equal(badge.classList.contains('gev-status-badge--plain'), true); - assert.equal(badge.classList.contains('weather-coverage'), true); - view.update([{ ...card, badge: '' }]); - assert.equal(badge.hidden, true); - view.update([{ ...card, badge: 'Global · 1° grid' }]); - assert.equal(badge.hidden, false); - assert.equal(badge.textContent, 'Global · 1° grid'); - assert.equal( - f.find((node) => node.classList.contains('gev-status-badge')), - badge, - ); - assert.deepEqual({ ...badge.dataset }, {}); - assertSameNode(f.document.activeElement, header); - view.destroy(); - view.update([card]); - assert.equal(f.container.children.length, 0); -});