refactor(ui): remove the action button and status badge builders

The Vue views apply the gev-action-button and gev-status-badge classes
themselves, so the DOM builders have no users left. The docs describe the
views instead.
This commit is contained in:
Sameh Khamis committed 2026-10-31 18:40:25 -07:00
1 parent 9d9eb267c5
commit 2ffd7a4afd
7 files changed
+19 -199

No files matched your search

+10 -14
View File
@@ -372,12 +372,10 @@ facade's asynchronous teardown can yield.
## Ordinary action buttons ## Ordinary action buttons
`src/ui/actionButton.js` is a native, listener-free view helper used by Scene The scene panels view and the CCTV panel draw their action buttons with the shared
presentation and sharing-toolbar owners. It receives translated text and explicit `gev-action-button` classes in `src/ui/styles/action-button.css`. Feature
presentation/accessibility properties. Feature controllers own action dispatch, controllers own action dispatch, state, persistence and cleanup, and the views
state, persistence and cleanup. Static Scenes/CCTV templates use the same CSS preserve IDs and layout hooks.
surface and preserve IDs and layout hooks. The helper imports no application,
renderer, storage or services; it belongs to the consuming package closures.
## Display controls ## Display controls
@@ -759,11 +757,9 @@ field rules separate, so Radio's palette remains an intentional exception.
### Read-only status badges ### Read-only status badges
`src/ui/statusBadge.js` constructs/updates ordinary badges without listeners, Views draw read-only badges with the `gev-status-badge` classes, without
status inference or live-region policy. It consumes translated text and controlled listeners, status inference or live-region policy. Appearance variants share
visibility; appearance variants share CSS rather than mapping machine states to CSS rather than mapping machine states to universal severity. CCTV owns
universal severity. CCTV owns frame/source and calibration rules and their data frame/source and calibration rules and their data attributes, while Weather
attributes, while Weather owns coverage labels. The keyed rail-card owner keeps owns coverage labels. The rail card keeps node identity, header focus,
node identity, header focus, disclosure and teardown. The shared adapter never disclosure and teardown, and does not announce every card update.
creates another state store or announces all card updates. A Vue view can replace
this adapter and retain these presentation inputs and styles.
+9 -14
View File
@@ -1460,11 +1460,9 @@ boundaries preserve the current Tactical/Cyber appearance and native behavior.
## Ordinary status badges ## Ordinary status badges
CCTV source/frame labels, calibration provenance and Weather coverage labels use CCTV source/frame labels, calibration provenance and Weather coverage labels use
`ui/statusBadge.js` and the `gev-status-badge` CSS surface. The listener-free the `gev-status-badge` CSS surface, drawn by the CCTV view and the rail card from
adapter accepts already-translated `label` and `hidden` values; pill, box and plain already-translated labels; pill, box and plain appearances describe
appearances describe shape/density only. Static CCTV markup uses the same classes shape/density only. Empty coverage remains hidden.
as generated rail-card badges. Repeated identical updates do not rewrite text or
replace nodes. Empty coverage remains hidden.
Feature owners retain the status rules, machine-state attributes, translations Feature owners retain the status rules, machine-state attributes, translations
and announcement policy. CCTV frame acquisition (`idle/loading/error/ready`) 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 remain geographic scope, not health claims. Existing colors and the source
badge's polite live region are preserved; coverage and calibration do not gain badge's polite live region are preserved; coverage and calibration do not gain
live regions. Interactive layer toggles, progress indicators and world overlays live regions. Interactive layer toggles, progress indicators and world overlays
retain their specialized ownership. Vue can reuse the view contract and CSS while retain their specialized ownership. Browser acceptance:
replacing DOM construction. Browser acceptance:
`QA_BASE_URL=http://127.0.0.1:4189 node scripts/qa-status-badges.mjs`. `QA_BASE_URL=http://127.0.0.1:4189 node scripts/qa-status-badges.mjs`.
## Ordinary action buttons ## Ordinary action buttons
Scene panel actions, shot LOAD/DEL actions, the EDIT DETAILS / SHARE SCENE 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 bar and one-shot CCTV actions use the shared `gev-action-button` surface in
`gev-action-button` surface in `ui/styles/action-button.css`. Static templates `ui/styles/action-button.css`, with default/danger variants and
apply the same classes as generated buttons. The native builder accepts translated default/compact sizes, drawn by the scene panels view (`LayerPanelsView.vue`,
text, default/danger variants, default/compact sizes and disabled/accessibility `SceneShotList.vue`) and the CCTV panel. Feature
properties; it installs no listeners and owns no application state. Feature
controllers retain callbacks, enable/disable rules, confirmation prompts, controllers retain callbacks, enable/disable rules, confirmation prompts,
persistence and teardown. The smaller shot actions retain their existing sizing persistence and teardown. The smaller shot actions retain their existing sizing
and hover treatment. Feature CSS still owns panel layout and translated wrapping. 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 CCTV enable, coverage, auto-hop, projection and adjustment toggles, editable
calibration-value chips and the scene authoring dialogs retain their specialized calibration-value chips and the scene authoring dialogs retain their specialized
controls. Existing `scene-btn` hooks remain compatible with Street Level and controls. Existing `scene-btn` hooks remain compatible with Street Level and
legacy toggles. The shared CSS, explicit input/action boundary and behavior tests legacy toggles.
can carry to a Vue view while Vue replaces native DOM construction.
Browser acceptance: `QA_BASE_URL=http://127.0.0.1:4179 node scripts/qa-action-buttons.mjs`; 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. `qa-scene-controls.mjs` also covers real Scene authoring/playback with native Space capture.
-1
View File
@@ -1093,7 +1093,6 @@
"server/providers/overpass/requestAdmission.test.mjs", "server/providers/overpass/requestAdmission.test.mjs",
"src/tooling/trackArrivalObservation.test.mjs", "src/tooling/trackArrivalObservation.test.mjs",
"src/ui/styles/status-badge.css", "src/ui/styles/status-badge.css",
"src/ui/statusBadge.test.mjs",
"src/ui/cctvBadges.test.mjs", "src/ui/cctvBadges.test.mjs",
"scripts/qa-status-badges.mjs", "scripts/qa-status-badges.mjs",
"src/dockerTrustedPeers.test.mjs", "src/dockerTrustedPeers.test.mjs",
-8
View File
@@ -609,7 +609,6 @@
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
"src/ui/keyGuidance.js", "src/ui/keyGuidance.js",
"src/ui/statusText.js", "src/ui/statusText.js",
"src/ui/statusBadge.js",
"src/ui/rail/RailChips.vue", "src/ui/rail/RailChips.vue",
"src/ui/rail/RailRowList.vue", "src/ui/rail/RailRowList.vue",
"src/ui/rail/RailCardSetting.vue", "src/ui/rail/RailCardSetting.vue",
@@ -695,7 +694,6 @@
"src/i18n/locales/zh-CN.json", "src/i18n/locales/zh-CN.json",
"src/i18n/locales/zh-Hant.json", "src/i18n/locales/zh-Hant.json",
"src/ui/unitLabels.js", "src/ui/unitLabels.js",
"src/ui/statusBadge.js",
"src/ui/vueRuntime.js", "src/ui/vueRuntime.js",
"src/ui/stores/cctv.js" "src/ui/stores/cctv.js"
], ],
@@ -787,7 +785,6 @@
"src/ui/stores/scenes.js", "src/ui/stores/scenes.js",
"src/ui/views/SceneOptions.vue", "src/ui/views/SceneOptions.vue",
"src/ui/views/SceneShotList.vue", "src/ui/views/SceneShotList.vue",
"src/ui/actionButton.js",
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/de.json", "src/i18n/locales/de.json",
@@ -931,7 +928,6 @@
"src/ui/stores/scenes.js", "src/ui/stores/scenes.js",
"src/ui/views/SceneOptions.vue", "src/ui/views/SceneOptions.vue",
"src/ui/views/SceneShotList.vue", "src/ui/views/SceneShotList.vue",
"src/ui/actionButton.js",
"src/ui/scenes.js", "src/ui/scenes.js",
"src/ui/shareFollow.js", "src/ui/shareFollow.js",
"src/share/follow.js", "src/share/follow.js",
@@ -977,7 +973,6 @@
"src/ui/keyGuidance.js", "src/ui/keyGuidance.js",
"src/ui/layerNames.js", "src/ui/layerNames.js",
"src/ui/statusText.js", "src/ui/statusText.js",
"src/ui/statusBadge.js",
"src/scopeMaskPolicy.js", "src/scopeMaskPolicy.js",
"src/visualStylePolicy.js", "src/visualStylePolicy.js",
"src/ui/stores/panels.js", "src/ui/stores/panels.js",
@@ -1090,7 +1085,6 @@
"src/ui/stores/scenes.js", "src/ui/stores/scenes.js",
"src/ui/views/SceneOptions.vue", "src/ui/views/SceneOptions.vue",
"src/ui/views/SceneShotList.vue", "src/ui/views/SceneShotList.vue",
"src/ui/actionButton.js",
"src/contextModePolicy.js", "src/contextModePolicy.js",
"src/director/clock.js", "src/director/clock.js",
"src/director/timeline.js", "src/director/timeline.js",
@@ -2641,7 +2635,6 @@
"src/ui/stores/scenes.js", "src/ui/stores/scenes.js",
"src/ui/views/SceneOptions.vue", "src/ui/views/SceneOptions.vue",
"src/ui/views/SceneShotList.vue", "src/ui/views/SceneShotList.vue",
"src/ui/actionButton.js",
"src/ui/scenes.js", "src/ui/scenes.js",
"src/ui/shareFollow.js", "src/ui/shareFollow.js",
"src/ui/shareRestoration.js", "src/ui/shareRestoration.js",
@@ -2956,7 +2949,6 @@
"src/voice/geminiSession.js", "src/voice/geminiSession.js",
"src/voice/geminiAudio.js", "src/voice/geminiAudio.js",
"src/voice/geminiCaptureWorklet.js", "src/voice/geminiCaptureWorklet.js",
"src/ui/statusBadge.js",
"src/sceneCommandRelease.js", "src/sceneCommandRelease.js",
"src/scopeMaskPolicy.js", "src/scopeMaskPolicy.js",
"src/visualStylePolicy.js", "src/visualStylePolicy.js",
-41
View File
@@ -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;
}
-39
View File
@@ -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;
}
-82
View File
@@ -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: '<img src=x onerror=alert(1)> · НЕИЗВЕСТНО',
});
assert.equal(badge.tagName, 'SPAN');
assert.equal(
badge.textContent,
'<img src=x onerror=alert(1)> · НЕИЗВЕСТНО',
);
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);
});