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, 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). 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 - The Data, Authoring, Weather, Recent Imagery, Context, CCTV, Street Level
and Radio panels are one component, `UiPanel` (`src/ui/kit/`), with the 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 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-auto-hop-btn').textContent, 'AUTO HOP ON');
assert.equal(byId('cctv-projection-btn').textContent, 'PROJECTION OFF'); assert.equal(byId('cctv-projection-btn').textContent, 'PROJECTION OFF');
assert.equal(byId('cctv-projection-btn').classList.contains('active'), false); 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').textContent, 'CAL · CURATED');
assert.equal(byId('cctv-quality-chip').dataset.calBadge, 'curated'); assert.equal(byId('cctv-quality-chip').dataset.calBadge, 'curated');
assert.equal( assert.equal(
+13 -12
View File
@@ -1,6 +1,7 @@
<script setup> <script setup>
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { t } from '../../i18n/index.js'; import { t } from '../../i18n/index.js';
import UiButton from '../kit/UiButton.vue';
import RailPanel from '../rail/RailPanel.vue'; import RailPanel from '../rail/RailPanel.vue';
import { useSceneSharingStore } from '../stores/sceneSharing.js'; import { useSceneSharingStore } from '../stores/sceneSharing.js';
import { useSceneStore } from '../stores/scenes.js'; import { useSceneStore } from '../stores/scenes.js';
@@ -52,32 +53,32 @@ async function importChosen(event) {
<p class="scene-step-heading" data-i18n>SCENE</p> <p class="scene-step-heading" data-i18n>SCENE</p>
<div class="scene-controls"> <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> <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> <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>
<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 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> </div>
<p class="scene-step-heading" data-i18n>SHOTS</p> <p class="scene-step-heading" data-i18n>SHOTS</p>
<div class="scene-controls"> <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> <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>
<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 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> </div>
<SceneShotList /> <SceneShotList />
<p class="scene-shot-hint" data-i18n>Click a shot to select it, double-click to rename.</p> <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> <p class="scene-step-heading" data-i18n>PREVIEW</p>
<div class="scene-controls"> <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> <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>
<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> <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>
<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 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> </div>
<p class="scene-step-heading" data-i18n>SAVE &amp; SHARE</p> <p class="scene-step-heading" data-i18n>SAVE &amp; SHARE</p>
<div class="scene-controls scene-file-controls"> <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> <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>
<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> <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>
<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-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" /> <input ref="sceneFile" type="file" id="scene-import-file" accept="application/json,.json" hidden @change="importChosen" />
</div> </div>
<div v-if="toolbar" data-director-authoring class="scene-controls"> <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> <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>
<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('Download this scene as JSON or an asset bundle')" @click="toolbar.share()">{{ t('SHARE SCENE') }}</UiButton>
</div> </div>
<div class="scene-progress"> <div class="scene-progress">
<div id="scene-progress-fill" :style="scene ? { width: `${scene.progress}%` } : undefined">{{ scene?.progress ?? 0 }}%</div> <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 { storeToRefs } from 'pinia';
import { t } from '../../i18n/index.js'; import { t } from '../../i18n/index.js';
import { describeShot } from '../scenePresentation.js'; import { describeShot } from '../scenePresentation.js';
import UiButton from '../kit/UiButton.vue';
import { useSceneStore } from '../stores/scenes.js'; import { useSceneStore } from '../stores/scenes.js';
// The selected scene's shots. Rows keep their identity across selection so a // 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 /><template v-else>{{ shownTitle(shot) }}</template></div
> >
<div class="scene-shot-actions"> <div class="scene-shot-actions">
<button <UiButton
type="button"
class="gev-action-button gev-action-button--compact scene-shot-btn" class="gev-action-button gev-action-button--compact scene-shot-btn"
@click="act('load', shot)" @click="act('load', shot)"
>{{ t('LOAD') }}</button >{{ t('LOAD') }}</UiButton
> >
<button <UiButton
type="button" tone="alert"
class="gev-action-button gev-action-button--danger gev-action-button--compact scene-shot-btn scene-shot-danger" class="gev-action-button gev-action-button--danger gev-action-button--compact scene-shot-btn scene-shot-danger"
@click="act('remove', shot)" @click="act('remove', shot)"
>{{ t('DEL') }}</button >{{ t('DEL') }}</UiButton
> >
</div> </div>
</div> </div>
+12 -11
View File
@@ -2,6 +2,7 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { computed } from 'vue'; import { computed } from 'vue';
import { t } from '../../../i18n/index.js'; import { t } from '../../../i18n/index.js';
import UiButton from '../../kit/UiButton.vue';
import RailPanel from '../../rail/RailPanel.vue'; import RailPanel from '../../rail/RailPanel.vue';
import CctvCalValue from '../../cctv/CctvCalValue.vue'; import CctvCalValue from '../../cctv/CctvCalValue.vue';
import CctvPicker from '../../cctv/CctvPicker.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-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 id="cctv-meta">{{ view?.meta ?? t('Enable CCTV to load camera intersections') }}</div>
<div class="cctv-controls"> <div class="cctv-controls">
<button id="cctv-enable-btn" :class="['scene-btn', { active: view?.enabled }]">{{ view?.enabled ? t('CCTV ON') : t('CCTV OFF') }}</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>
<button id="cctv-nearest-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled" data-i18n>NEAREST</button> <UiButton id="cctv-nearest-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.navDisabled" data-i18n>NEAREST</UiButton>
</div> </div>
<div class="cctv-controls"> <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 /> <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>
<div class="cctv-controls"> <div class="cctv-controls">
<button id="cctv-focus-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.focusDisabled" data-i18n>FOCUS</button> <UiButton id="cctv-focus-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.focusDisabled" data-i18n>FOCUS</UiButton>
<button id="cctv-coverage-btn" :class="['scene-btn', { active: view?.coverage.active }]" :disabled="view?.toggleDisabled">{{ view?.coverage.text ?? t('COVERAGE OFF') }}</button> <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>
<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 :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>
<div class="cctv-controls"> <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> <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>
<div class="cctv-calibration-block"> <div class="cctv-calibration-block">
<div class="cctv-calibration-header"> <div class="cctv-calibration-header">
<div class="cctv-summary-label" data-i18n>CALIBRATION</div> <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>
<div id="cctv-cal-readout" class="cctv-cal-readout" aria-label="Camera pose — click a value to type" data-i18n-aria-label> <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 /> <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 /> <CctvCalValue field="east" label="ΔE" title="East offset from catalog position (m) — click to type" data-i18n-title />
</div> </div>
<div class="cctv-controls cctv-calibration-actions"> <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> <UiButton id="cctv-calib-save-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.calibration.disabled" data-i18n>SAVE CAL</UiButton>
<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-reset-btn" type="button" class="gev-action-button scene-btn" :disabled="view?.calibration.disabled" data-i18n>RESET CAL</UiButton>
</div> </div>
</div> </div>
<div class="cctv-summary-label" data-i18n>SCENE SUMMARY</div> <div class="cctv-summary-label" data-i18n>SCENE SUMMARY</div>
+2 -1
View File
@@ -2,6 +2,7 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { computed, shallowRef, watch } from 'vue'; import { computed, shallowRef, watch } from 'vue';
import { t } from '../../../i18n/index.js'; import { t } from '../../../i18n/index.js';
import UiButton from '../../kit/UiButton.vue';
import RailPanel from '../../rail/RailPanel.vue'; import RailPanel from '../../rail/RailPanel.vue';
import RailChips from '../../rail/RailChips.vue'; import RailChips from '../../rail/RailChips.vue';
import { useStreetLevelStore } from '../../stores/streetLevel.js'; 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. --> <!-- The hint says how to open imagery; the button does it without a pointer. -->
<div class="sl-meta-row"> <div class="sl-meta-row">
<div id="sl-coverage-meta" class="sl-meta">{{ view?.meta ?? '' }}</div> <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>
</div> </div>
</RailPanel> </RailPanel>