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);
-});