feat(kit): render the chips with UiChip and UiButton

UiChip is a non-interactive label chip and renders the voice
card's chips. The pressable chips and pills are pressed toggles, so the
rail card chips, map source chips and city and place pills render through
UiButton with pressedClass, keeping their classes, attributes and the
classes other code adds. The pills now report aria-pressed.
This commit is contained in:
Sameh Khamis committed 2026-11-01 12:09:38 -08:00
1 parent d98a7c81be
commit 28ca784d49
10 files changed
+81 -16

No files matched your search

+4 -1
View File
@@ -65,7 +65,10 @@
what the answering action returned. The Display, Radio, RTL-SDR, Cockpit,
mission replay and style parameter sliders are the kit's `UiSlider`, a
native range input that emits numbers, and the pressed toggles are the
kit's `UiButton`, which gains `pressedClass`. `syncScopeControls`
kit's `UiButton`, which gains `pressedClass`; so are the rail card
chips, map source chips and location pills (which now report
`aria-pressed`), and the voice card's chips
are the kit's new `UiChip`. `syncScopeControls`
(`./ui/display`) and `shouldHideCollapsedRightPanels` (`./ui/layout`) are
deprecated: the Display view and the rail now own what they did. `./ui/input` imports Vue
(samehkhamis).
+4 -1
View File
@@ -61,7 +61,10 @@ adapter is gone.
`active`); its pressed classes toggle through `classList`, so a class other
code adds (a voice highlight) survives a press. Every pressed toggle in the
views is a `UiButton` except Bhote Koshi's cinematic button and the
tilt-map button, whose owners still write their state into the DOM.
tilt-map button, whose owners still write their state into the DOM. Core's
pressable chips and pills (rail card chips, map sources, cities and places)
are `UiButton`s too; `UiChip` is ui-vue's non-interactive label chip, used by
the voice card.
`UiSlider` keeps the native range input where ui-vue draws a
`role="slider"` track, so Core's keyboard, form behaviour and styling stay;
it takes ui-vue's `v-model`, `min`, `max`, `step` and `disabled`, emits a
+3
View File
@@ -1132,6 +1132,9 @@
"src/ui/kit/UiSlider.test.mjs",
"src/ui/kit/UiButton.vue",
"src/ui/kit/UiButton.test.mjs",
"src/ui/kit/UiChip.vue",
"src/ui/kit/UiChip.test.mjs",
"src/ui/rail/RailChips.vue",
"src/ui/displayPanelActions.test.mjs",
"src/testSupport/vueDom.mjs",
"build/vueLoader.js",
+8 -4
View File
@@ -134,7 +134,8 @@
"src/ui/rail/PanelCollapseButton.vue",
"src/ui/rail/panelState.js",
"src/sdr/gain.js",
"src/ui/kit/UiSlider.js"
"src/ui/kit/UiSlider.js",
"src/ui/kit/UiChip.vue"
],
"external": ["vue", "pinia"]
},
@@ -667,7 +668,8 @@
"src/ui/rail/utcClock.js",
"src/ui/rail/RailTimeline.vue",
"src/ui/rail/rangeValue.js",
"src/ui/weather/WeatherReadout.vue"
"src/ui/weather/WeatherReadout.vue",
"src/ui/kit/UiButton.vue"
],
"external": ["vue"]
},
@@ -1066,7 +1068,8 @@
"src/ui/stores/panelChrome.js",
"src/ui/rail/panelState.js",
"src/ui/rail/fieldValue.js",
"src/ui/kit/UiSlider.js"
"src/ui/kit/UiSlider.js",
"src/ui/kit/UiButton.vue"
],
"external": ["cesium", "vue", "pinia"]
},
@@ -3076,7 +3079,8 @@
"src/ui/stores/panelChrome.js",
"src/ui/rail/panelState.js",
"src/ui/rail/fieldValue.js",
"src/ui/kit/UiSlider.js"
"src/ui/kit/UiSlider.js",
"src/ui/kit/UiButton.vue"
],
"external": [
"hls.js",
+3 -1
View File
@@ -898,7 +898,9 @@ test('row info and legends reconcile in place while chips retain focus and order
controls.chips.push({ id: 'two', label: 'Two' });
manager._refreshTogglePanel();
assert.deepEqual(
[...block.children].map((node) => node.className),
[...block.children].map((node) =>
[...node.classList].filter((name) => !name.startsWith('ui-')).join(' '),
),
[
'data-toggle-chip chip-idle',
'data-toggle-chip chip-idle',
+28
View File
@@ -0,0 +1,28 @@
import { after, before, test } from 'node:test';
import assert from 'node:assert/strict';
import { installVueDom } from '../../testSupport/vueDom.mjs';
let mount;
let UiChip;
let uninstall;
before(async () => {
uninstall = installVueDom();
({ mount } = await import('@vue/test-utils'));
UiChip = (await import('./UiChip.vue')).default;
});
after(() => uninstall());
test('a chip is a span with the kit classes and Core’s', async () => {
const wrapper = mount(UiChip, {
attrs: { class: 'gev-voice-card-chip', 'data-tone': 'ok' },
slots: { default: 'ok' },
});
const chip = wrapper.get('span');
assert.deepEqual(chip.classes(), ['ui-chip', 'gev-voice-card-chip']);
assert.equal(chip.attributes('data-tone'), 'ok');
assert.equal(chip.text(), 'ok');
await wrapper.setProps({ selected: true, mono: true });
assert.ok(chip.classes().includes('ui-chip--selected'));
assert.ok(chip.classes().includes('ui-mono'));
wrapper.unmount();
});
+19
View File
@@ -0,0 +1,19 @@
<script setup>
// Core's chip, with @halfpixel/ui-vue UiChip's props and classes so the two
// kits can merge: a label that says what something is, never a control.
// Core styles it through the classes its callers pass; the kit classes carry
// no Core styles yet. A chip that presses is a UiButton with pressedClass.
defineProps({
selected: { type: Boolean, default: false },
mono: { type: Boolean, default: false },
});
</script>
<template>
<span
class="ui-chip"
:class="{ 'ui-chip--selected': selected, 'ui-mono': mono }"
>
<slot />
</span>
</template>
+7 -5
View File
@@ -1,4 +1,6 @@
<script setup>
import UiButton from '../kit/UiButton.vue';
// Keyed chips shared by rail cards; the owner decides what a click does.
defineProps({
chips: { type: Array, default: () => [] },
@@ -6,23 +8,23 @@ defineProps({
});
const chipClass = (chip) =>
`data-toggle-chip chip-${chip.state || (chip.active ? 'active' : 'idle')}${chip.active ? ' active' : ''}`;
`data-toggle-chip chip-${chip.state || (chip.active ? 'active' : 'idle')}`;
</script>
<template>
<button
<UiButton
v-for="chip in chips"
:key="chip.id"
type="button"
:data-chip-id="chip.id"
:class="chipClass(chip)"
:pressed="Boolean(chip.active)"
pressed-class="active"
:style="chip.color ? `--chip-color: ${chip.color}` : undefined"
:data-chip-color="chip.color || undefined"
:title="chip.title || undefined"
:disabled="Boolean(chip.disabled)"
:aria-pressed="String(Boolean(chip.active))"
:aria-busy="String(Boolean(chip.busy))"
@click="onSelect(chip)"
>{{ chip.label }}</button
>{{ chip.label }}</UiButton
>
</template>
+3 -3
View File
@@ -117,7 +117,7 @@ function selectMapSource(chip) {
<span data-i18n id="map-source-label">MAP SOURCE</span>
<span id="map-stack-status" class="pp-stack-status" v-class-toggle="{ warn: !!mapSource?.status.warn }" aria-live="polite">{{ mapSource ? mapSource.status.text : '3D' }}</span>
</div>
<div id="map-stack-chips" class="map-stack-chip-row" role="group" aria-label="Map source" data-i18n-aria-label><button v-for="chip in mapSource?.chips" :key="chip.id" type="button" class="map-stack-chip" v-class-toggle="{ active: chip.active, unavailable: !chip.available }" :data-stack-id="chip.id" :title="chip.title" :aria-pressed="String(chip.active)" :aria-disabled="String(!chip.available)" :aria-label="chip.ariaLabel" @click="selectMapSource(chip)"><span class="map-stack-chip-label">{{ chip.label }}</span><span v-if="chip.requirement" class="map-stack-chip-req">{{ chip.requirement }}</span></button></div>
<div id="map-stack-chips" class="map-stack-chip-row" role="group" aria-label="Map source" data-i18n-aria-label><UiButton v-for="chip in mapSource?.chips" :key="chip.id" class="map-stack-chip" :pressed="Boolean(chip.active)" pressed-class="active" v-class-toggle="{ unavailable: !chip.available }" :data-stack-id="chip.id" :title="chip.title" :aria-disabled="String(!chip.available)" :aria-label="chip.ariaLabel" @click="selectMapSource(chip)"><span class="map-stack-chip-label">{{ chip.label }}</span><span v-if="chip.requirement" class="map-stack-chip-req">{{ chip.requirement }}</span></UiButton></div>
</section>
</div>
<div id="style-mini-status" class="panel-mini-status" aria-live="polite">
@@ -145,10 +145,10 @@ function selectMapSource(chip) {
</div>
<div id="location-bar-popover" class="dock-popover-content">
<UiButton variant="icon" class="dock-pin-btn" data-pin-target="location-bar" :label="t('Pin location tray')" :title="t('Keep location tray open')" :pressed="!!panels['location-bar']?.pinned" @click="pin($event, 'location-bar')"><img src="/pin.svg" alt="" /></UiButton>
<div id="poi-row" class="poi-row-container" v-class-toggle="{ expanded: !!loc?.poisExpanded }"><button v-for="poi in loc?.pois" :key="`${loc.poiCity}:${poi.index}`" type="button" class="poi-pill" v-class-toggle="{ active: poi.active }" :data-poi-index="poi.index" @click="locationControls?.choosePoi(poi.index)"><span class="poi-pill-key">{{ poi.key }}</span><span class="poi-pill-name">{{ poi.name }}</span></button></div>
<div id="poi-row" class="poi-row-container" v-class-toggle="{ expanded: !!loc?.poisExpanded }"><UiButton v-for="poi in loc?.pois" :key="`${loc.poiCity}:${poi.index}`" class="poi-pill" :pressed="Boolean(poi.active)" pressed-class="active" :data-poi-index="poi.index" @click="locationControls?.choosePoi(poi.index)"><span class="poi-pill-key">{{ poi.key }}</span><span class="poi-pill-name">{{ poi.name }}</span></UiButton></div>
<div class="location-bar-divider" id="location-bar-divider" v-class-toggle="{ visible: !!loc?.poisExpanded }"></div>
<div class="location-city-row">
<div id="location-pills" class="location-pills"><button v-for="city in loc?.cities" :key="city.id" type="button" class="location-pill" v-class-toggle="{ active: city.active }" :data-location-id="city.id" @click="locationControls?.chooseCity(city.id)">{{ city.label }}</button></div>
<div id="location-pills" class="location-pills"><UiButton v-for="city in loc?.cities" :key="city.id" class="location-pill" :pressed="Boolean(city.active)" pressed-class="active" :data-location-id="city.id" @click="locationControls?.chooseCity(city.id)">{{ city.label }}</UiButton></div>
<div class="location-search-wrap">
<button id="search-toggle" class="search-toggle-btn" title="Search any location" data-i18n-title>
<span>&#x1F50E;</span>
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup>
import { t } from '../../../i18n/index.js';
import { ASSISTANT_NAME } from '../../../voice/control.js';
import UiChip from '../../kit/UiChip.vue';
// Captions, plan and result, as VoiceCardControls (src/voice/voiceCard.js)
// presents them. Captions update silently; the polite live region carries
@@ -22,7 +23,7 @@ const emit = defineEmits(['dismiss']);
<ol id="gev-voice-card-plan" class="gev-voice-card-plan" :aria-label="t('Plan')" :hidden="card.steps.length === 0"><li v-for="(step, index) in card.steps" :key="step.key ?? index" class="gev-voice-card-step" :data-status="step.status"><span class="gev-voice-card-step-label">{{ step.label }}</span><span class="gev-voice-card-sr">{{ step.statusText ? `, ${step.statusText}` : '' }}</span><span v-if="step.detail" class="gev-voice-card-step-detail">{{ step.detail }}</span></li></ol>
<div id="gev-voice-card-result" class="gev-voice-card-result" :hidden="!card.result.visible">
<div id="gev-voice-card-result-title" class="gev-voice-card-result-title">{{ card.result.title }}</div>
<div id="gev-voice-card-chips" class="gev-voice-card-chips" :hidden="card.result.chips.length === 0"><span v-for="(chip, index) in card.result.chips" :key="index" class="gev-voice-card-chip" :data-tone="chip">{{ chip }}</span></div>
<div id="gev-voice-card-chips" class="gev-voice-card-chips" :hidden="card.result.chips.length === 0"><UiChip v-for="(chip, index) in card.result.chips" :key="index" class="gev-voice-card-chip" :data-tone="chip">{{ chip }}</UiChip></div>
<ul id="gev-voice-card-lines" class="gev-voice-card-lines"><li v-for="(line, index) in card.result.lines" :key="index">{{ line }}</li></ul>
<ol id="gev-voice-card-referents" class="gev-voice-card-referents" :aria-label="t('Referenced items')" :hidden="card.result.referents.length === 0"><li v-for="(ref, index) in card.result.referents" :key="index" :value="ref.n">{{ ref.label }}</li></ol>
<details id="gev-voice-card-notes" class="gev-voice-card-notes" :hidden="card.result.notes.length === 0">