feat(controls): render the action and toggle buttons with UiButton

The Authoring panel's and shot list's buttons, the CCTV panel's buttons and
Street Level's nearest button are the kit's UiButton, keeping their classes:
danger buttons take the alert tone and primary ones the primary variant. The
CCTV on/off, coverage, auto-hop, projection and adjust toggles, which showed
their state only by colour, now carry aria-pressed too.
This commit is contained in:
Sameh Khamis committed 2026-11-01 08:39:49 -08:00
1 parent cd231faae9
commit fc53682129
6 files changed
+46 -30

No files matched your search

+6
View File
@@ -8,6 +8,12 @@
the bottom tab bar. The rail is the first view built with Vue and Pinia,
which now ship with the app; see docs/VUE-MIGRATION.md (samehkhamis).
- The Authoring, shot list and CCTV buttons and Street Level's nearest button
are the kit's `UiButton` (danger buttons in the `alert` tone, primary ones the `primary`
variant), keeping their classes; the CCTV on/off, coverage, auto-hop,
projection and adjust toggles now say their state with `aria-pressed`
(samehkhamis).
- The Data, Authoring, Weather, Recent Imagery, Context, CCTV, Street Level
and Radio panels are one component, `UiPanel` (`src/ui/kit/`), with the
same markup plus the kit's `ui-panel` classes, their titles translated by
+7
View File
@@ -126,6 +126,13 @@ test('an active camera renders the switch, toggles, CAL chip and meta line', asy
assert.equal(byId('cctv-auto-hop-btn').textContent, 'AUTO HOP ON');
assert.equal(byId('cctv-projection-btn').textContent, 'PROJECTION OFF');
assert.equal(byId('cctv-projection-btn').classList.contains('active'), false);
// The toggles say their state to assistive technology, not only by colour.
const pressed = (id) => byId(id).getAttribute('aria-pressed');
assert.equal(pressed('cctv-enable-btn'), 'true');
assert.equal(pressed('cctv-coverage-btn'), 'true');
assert.equal(pressed('cctv-auto-hop-btn'), 'true');
assert.equal(pressed('cctv-projection-btn'), 'false');
assert.equal(pressed('cctv-adjust-btn'), 'false');
assert.equal(byId('cctv-quality-chip').textContent, 'CAL · CURATED');
assert.equal(byId('cctv-quality-chip').dataset.calBadge, 'curated');
assert.equal(
+13 -12
View File
@@ -1,6 +1,7 @@
<script setup>
import { storeToRefs } from 'pinia';
import { t } from '../../i18n/index.js';
import UiButton from '../kit/UiButton.vue';
import RailPanel from '../rail/RailPanel.vue';
import { useSceneSharingStore } from '../stores/sceneSharing.js';
import { useSceneStore } from '../stores/scenes.js';
@@ -52,32 +53,32 @@ async function importChosen(event) {
<p class="scene-step-heading" data-i18n>SCENE</p>
<div class="scene-controls">
<select class="gev-field" id="scene-select" aria-label="Scene" data-i18n-aria-label :disabled="off('select')" @change="scenes.act('selectScene', $event.currentTarget.value)"><SceneOptions /></select>
<button id="scene-new-btn" type="button" :disabled="off('new')" @click="scenes.act('create')" class="gev-action-button scene-btn" title="Start a new, empty scene" data-i18n data-i18n-title>NEW SCENE</button>
<button id="scene-delete-btn" type="button" :disabled="off('delete')" @click="scenes.act('deleteScene')" class="gev-action-button gev-action-button--danger scene-btn scene-btn-danger" title="Delete this scene and all of its shots" data-i18n data-i18n-title>DELETE</button>
<UiButton id="scene-new-btn" type="button" :disabled="off('new')" @click="scenes.act('create')" class="gev-action-button scene-btn" title="Start a new, empty scene" data-i18n data-i18n-title>NEW SCENE</UiButton>
<UiButton tone="alert" id="scene-delete-btn" type="button" :disabled="off('delete')" @click="scenes.act('deleteScene')" class="gev-action-button gev-action-button--danger scene-btn scene-btn-danger" title="Delete this scene and all of its shots" data-i18n data-i18n-title>DELETE</UiButton>
</div>
<p class="scene-step-heading" data-i18n>SHOTS</p>
<div class="scene-controls">
<button id="scene-capture-btn" type="button" :disabled="off('capture')" @click="scenes.act('capture')" class="gev-action-button scene-btn scene-btn-primary" title="Add the current view, style and layers as the next shot" data-i18n data-i18n-title>+ CAPTURE SHOT</button>
<button id="scene-update-shot-btn" type="button" :disabled="off('update')" @click="scenes.act('update')" class="gev-action-button scene-btn" title="Replace the selected shot with the current view" data-i18n data-i18n-title>UPDATE SHOT</button>
<UiButton variant="primary" id="scene-capture-btn" type="button" :disabled="off('capture')" @click="scenes.act('capture')" class="gev-action-button scene-btn scene-btn-primary" title="Add the current view, style and layers as the next shot" data-i18n data-i18n-title>+ CAPTURE SHOT</UiButton>
<UiButton id="scene-update-shot-btn" type="button" :disabled="off('update')" @click="scenes.act('update')" class="gev-action-button scene-btn" title="Replace the selected shot with the current view" data-i18n data-i18n-title>UPDATE SHOT</UiButton>
</div>
<SceneShotList />
<p class="scene-shot-hint" data-i18n>Click a shot to select it, double-click to rename.</p>
<p class="scene-step-heading" data-i18n>PREVIEW</p>
<div class="scene-controls">
<button id="scene-start-btn" type="button" class="gev-action-button scene-btn scene-btn-primary" title="Play from this scene through the rest of the project" data-i18n-title :disabled="off('start')" @click="scenes.act('start')">{{ scene?.playLabel ?? t('PLAY', { context: 'scene' }) }}</button>
<button id="scene-stop-btn" type="button" :disabled="off('stop')" @click="scenes.act('stop')" class="gev-action-button gev-action-button--danger scene-btn scene-btn-danger" title="Stop playback (Esc)" data-i18n data-i18n-title>STOP</button>
<button id="scene-next-btn" type="button" :disabled="off('next')" @click="scenes.act('next')" class="gev-action-button scene-btn" title="Play the shot after the selected one, moving on to the next scene when needed" data-i18n data-i18n-title>NEXT</button>
<UiButton variant="primary" id="scene-start-btn" type="button" class="gev-action-button scene-btn scene-btn-primary" title="Play from this scene through the rest of the project" data-i18n-title :disabled="off('start')" @click="scenes.act('start')">{{ scene?.playLabel ?? t('PLAY', { context: 'scene' }) }}</UiButton>
<UiButton tone="alert" id="scene-stop-btn" type="button" :disabled="off('stop')" @click="scenes.act('stop')" class="gev-action-button gev-action-button--danger scene-btn scene-btn-danger" title="Stop playback (Esc)" data-i18n data-i18n-title>STOP</UiButton>
<UiButton id="scene-next-btn" type="button" :disabled="off('next')" @click="scenes.act('next')" class="gev-action-button scene-btn" title="Play the shot after the selected one, moving on to the next scene when needed" data-i18n data-i18n-title>NEXT</UiButton>
</div>
<p class="scene-step-heading" data-i18n>SAVE &amp; SHARE</p>
<div class="scene-controls scene-file-controls">
<button id="scene-export-btn" type="button" :disabled="off('export')" @click="scenes.act('export')" class="gev-action-button scene-btn" title="Download every scene as a JSON file you can import later" data-i18n data-i18n-title>EXPORT</button>
<button id="scene-import-btn" type="button" :disabled="off('import')" @click="chooseImport($refs.sceneFile)" class="gev-action-button scene-btn" title="Load scenes from an exported JSON file" data-i18n data-i18n-title>IMPORT</button>
<button id="scene-download-btn" type="button" :disabled="off('download')" @click="scenes.act('download')" class="gev-action-button scene-btn" title="Download the timing log of the last play" data-i18n data-i18n-title>RUN LOG</button>
<UiButton id="scene-export-btn" type="button" :disabled="off('export')" @click="scenes.act('export')" class="gev-action-button scene-btn" title="Download every scene as a JSON file you can import later" data-i18n data-i18n-title>EXPORT</UiButton>
<UiButton id="scene-import-btn" type="button" :disabled="off('import')" @click="chooseImport($refs.sceneFile)" class="gev-action-button scene-btn" title="Load scenes from an exported JSON file" data-i18n data-i18n-title>IMPORT</UiButton>
<UiButton id="scene-download-btn" type="button" :disabled="off('download')" @click="scenes.act('download')" class="gev-action-button scene-btn" title="Download the timing log of the last play" data-i18n data-i18n-title>RUN LOG</UiButton>
<input ref="sceneFile" type="file" id="scene-import-file" accept="application/json,.json" hidden @change="importChosen" />
</div>
<div v-if="toolbar" data-director-authoring class="scene-controls">
<button type="button" class="gev-action-button scene-btn" :title="t('Edit anchors, data packs and shot details')" @click="toolbar.edit()">{{ t('EDIT DETAILS') }}</button>
<button type="button" class="gev-action-button scene-btn" :title="t('Download this scene as JSON or an asset bundle')" @click="toolbar.share()">{{ t('SHARE SCENE') }}</button>
<UiButton type="button" class="gev-action-button scene-btn" :title="t('Edit anchors, data packs and shot details')" @click="toolbar.edit()">{{ t('EDIT DETAILS') }}</UiButton>
<UiButton type="button" class="gev-action-button scene-btn" :title="t('Download this scene as JSON or an asset bundle')" @click="toolbar.share()">{{ t('SHARE SCENE') }}</UiButton>
</div>
<div class="scene-progress">
<div id="scene-progress-fill" :style="scene ? { width: `${scene.progress}%` } : undefined">{{ scene?.progress ?? 0 }}%</div>
+6 -6
View File
@@ -3,6 +3,7 @@ import { computed, nextTick, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { t } from '../../i18n/index.js';
import { describeShot } from '../scenePresentation.js';
import UiButton from '../kit/UiButton.vue';
import { useSceneStore } from '../stores/scenes.js';
// The selected scene's shots. Rows keep their identity across selection so a
@@ -100,17 +101,16 @@ function onRenameKey(shot, event) {
/><template v-else>{{ shownTitle(shot) }}</template></div
>
<div class="scene-shot-actions">
<button
type="button"
<UiButton
class="gev-action-button gev-action-button--compact scene-shot-btn"
@click="act('load', shot)"
>{{ t('LOAD') }}</button
>{{ t('LOAD') }}</UiButton
>
<button
type="button"
<UiButton
tone="alert"
class="gev-action-button gev-action-button--danger gev-action-button--compact scene-shot-btn scene-shot-danger"
@click="act('remove', shot)"
>{{ t('DEL') }}</button
>{{ t('DEL') }}</UiButton
>
</div>
</div>
+12 -11
View File
@@ -2,6 +2,7 @@
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import { t } from '../../../i18n/index.js';
import UiButton from '../../kit/UiButton.vue';
import RailPanel from '../../rail/RailPanel.vue';
import CctvCalValue from '../../cctv/CctvCalValue.vue';
import CctvPicker from '../../cctv/CctvPicker.vue';
@@ -37,27 +38,27 @@ const frameData = computed(() =>
<div id="cctv-source-badge" class="gev-status-badge" role="status" aria-live="polite" :data-frame-state="sourceBadge?.state">{{ sourceBadge?.label ?? t('SOURCE · UNKNOWN') }}</div>
<div id="cctv-meta">{{ view?.meta ?? t('Enable CCTV to load camera intersections') }}</div>
<div class="cctv-controls">
<button id="cctv-enable-btn" :class="['scene-btn', { active: view?.enabled }]">{{ view?.enabled ? t('CCTV ON') : t('CCTV OFF') }}</button>
<button id="cctv-nearest-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled" data-i18n>NEAREST</button>
<UiButton :pressed="Boolean(view?.enabled)" id="cctv-enable-btn" :class="['scene-btn', { active: view?.enabled }]">{{ view?.enabled ? t('CCTV ON') : t('CCTV OFF') }}</UiButton>
<UiButton id="cctv-nearest-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled" data-i18n>NEAREST</UiButton>
</div>
<div class="cctv-controls">
<button id="cctv-prev-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled" data-i18n>PREV</button>
<UiButton id="cctv-prev-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled" data-i18n>PREV</UiButton>
<CctvPicker class="gev-field" id="cctv-camera-select" aria-label="CCTV camera" data-i18n-aria-label />
<button id="cctv-next-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled" data-i18n>NEXT</button>
<UiButton id="cctv-next-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled" data-i18n>NEXT</UiButton>
</div>
<div class="cctv-controls">
<button id="cctv-focus-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.focusDisabled" data-i18n>FOCUS</button>
<button id="cctv-coverage-btn" :class="['scene-btn', { active: view?.coverage.active }]" :disabled="view?.toggleDisabled">{{ view?.coverage.text ?? t('COVERAGE OFF') }}</button>
<button id="cctv-auto-hop-btn" :class="['scene-btn', { active: view?.autoHop.active }]" :disabled="view?.toggleDisabled">{{ view?.autoHop.text ?? t('AUTO HOP OFF') }}</button>
<UiButton id="cctv-focus-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.focusDisabled" data-i18n>FOCUS</UiButton>
<UiButton :pressed="Boolean(view?.coverage.active)" id="cctv-coverage-btn" :class="['scene-btn', { active: view?.coverage.active }]" :disabled="view?.toggleDisabled">{{ view?.coverage.text ?? t('COVERAGE OFF') }}</UiButton>
<UiButton :pressed="Boolean(view?.autoHop.active)" id="cctv-auto-hop-btn" :class="['scene-btn', { active: view?.autoHop.active }]" :disabled="view?.toggleDisabled">{{ view?.autoHop.text ?? t('AUTO HOP OFF') }}</UiButton>
</div>
<div class="cctv-controls">
<button id="cctv-projection-btn" :class="['scene-btn', { active: view?.projection.active }]" :disabled="view?.toggleDisabled">{{ view?.projection.text ?? t('PROJECTION ON') }}</button>
<UiButton :pressed="Boolean(view?.projection.active)" id="cctv-projection-btn" :class="['scene-btn', { active: view?.projection.active }]" :disabled="view?.toggleDisabled">{{ view?.projection.text ?? t('PROJECTION ON') }}</UiButton>
<span id="cctv-quality-chip" class="gev-status-badge gev-status-badge--box" :data-cal-badge="view?.quality.calBadge">{{ view?.quality.label ?? t('CAL · {badge}', { badge: '--' }) }}</span>
</div>
<div class="cctv-calibration-block">
<div class="cctv-calibration-header">
<div class="cctv-summary-label" data-i18n>CALIBRATION</div>
<button id="cctv-adjust-btn" :class="['scene-btn', { active: view?.calibration.adjust.active }]" title="Drag the camera in the world: rings rotate, arrows move, handles set range/FOV" data-i18n-title :disabled="view?.calibration.disabled">{{ view?.calibration.adjust.text ?? t('ADJUST') }}</button>
<UiButton :pressed="Boolean(view?.calibration.adjust.active)" id="cctv-adjust-btn" :class="['scene-btn', { active: view?.calibration.adjust.active }]" title="Drag the camera in the world: rings rotate, arrows move, handles set range/FOV" data-i18n-title :disabled="view?.calibration.disabled">{{ view?.calibration.adjust.text ?? t('ADJUST') }}</UiButton>
</div>
<div id="cctv-cal-readout" class="cctv-cal-readout" aria-label="Camera pose — click a value to type" data-i18n-aria-label>
<CctvCalValue field="heading" label="HDG" title="Heading (compass °) — click to type" data-i18n-title />
@@ -69,8 +70,8 @@ const frameData = computed(() =>
<CctvCalValue field="east" label="ΔE" title="East offset from catalog position (m) — click to type" data-i18n-title />
</div>
<div class="cctv-controls cctv-calibration-actions">
<button id="cctv-calib-save-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.calibration.disabled" data-i18n>SAVE CAL</button>
<button id="cctv-calib-reset-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.calibration.disabled" data-i18n>RESET CAL</button>
<UiButton id="cctv-calib-save-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.calibration.disabled" data-i18n>SAVE CAL</UiButton>
<UiButton id="cctv-calib-reset-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.calibration.disabled" data-i18n>RESET CAL</UiButton>
</div>
</div>
<div class="cctv-summary-label" data-i18n>SCENE SUMMARY</div>
+2 -1
View File
@@ -2,6 +2,7 @@
import { storeToRefs } from 'pinia';
import { computed, shallowRef, watch } from 'vue';
import { t } from '../../../i18n/index.js';
import UiButton from '../../kit/UiButton.vue';
import RailPanel from '../../rail/RailPanel.vue';
import RailChips from '../../rail/RailChips.vue';
import { useStreetLevelStore } from '../../stores/streetLevel.js';
@@ -157,7 +158,7 @@ const vThumb = { mounted: placeThumb, updated: placeThumb };
<!-- The hint says how to open imagery; the button does it without a pointer. -->
<div class="sl-meta-row">
<div id="sl-coverage-meta" class="sl-meta">{{ view?.meta ?? '' }}</div>
<button id="sl-nearest-btn" class="scene-btn sl-nearest-btn" type="button" :hidden="!view?.nearest.visible" :title="nearest.title || undefined" :disabled="nearest.disabled" data-i18n-runtime>{{ nearest.label }}</button>
<UiButton id="sl-nearest-btn" class="scene-btn sl-nearest-btn" type="button" :hidden="!view?.nearest.visible" :title="nearest.title || undefined" :disabled="nearest.disabled" data-i18n-runtime>{{ nearest.label }}</UiButton>
</div>
</div>
</RailPanel>