mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
feat(kit): render the status badges and empty states with UiBadge and UiEmpty
UiBadge takes a tone and carries Core's gev-status-badge class, with the density variants and colours left to its callers; the CCTV source and calibration badges and the rail card badges render through it. UiEmpty takes a title and hint, with the title as the line itself, since Core's empty states are one line; the Scene shot list, mission roster and imagery strip use it.
This commit is contained in:
1 parent
ecaf723035
commit
b9ecb9b9f5
12 files changed
+126
-19
No files matched your search
+3
-1
@@ -68,7 +68,9 @@
|
||||
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`. The selects are the kit's new `UiSelect`.
|
||||
are the kit's new `UiChip`. The selects are the kit's new `UiSelect`, the
|
||||
status badges its new `UiBadge` and the one-line empty states its new
|
||||
`UiEmpty`.
|
||||
`syncScopeControls`
|
||||
(`./ui/display`) and `shouldHideCollapsedRightPanels` (`./ui/layout`) are
|
||||
deprecated: the Display view and the rail now own what they did. `./ui/input` imports Vue
|
||||
|
||||
@@ -72,6 +72,14 @@ options that carry their own attributes come through the slot, a caller's
|
||||
Without `hold`, a select takes its model back after its own change renders,
|
||||
so a pick its owner refuses goes back; an owner that answers later (the SDR
|
||||
gain) takes `hold`. Every select in the views is a `UiSelect`.
|
||||
`UiBadge` takes ui-vue's `tone` and carries Core's `gev-status-badge`; its
|
||||
density variants and colours stay caller classes, and it renders the CCTV
|
||||
source and calibration badges and the rail card badges. `UiEmpty` takes
|
||||
ui-vue's `title` and `hint`, but Core's empty states are one line, so the title
|
||||
is the text itself rather than a bold heading; the Scene shot list, mission
|
||||
roster and imagery strip use it. The locale picker's and the mission tables'
|
||||
empty rows stay a paragraph and table cells, and no Core block matches
|
||||
`UiSection`'s header and well, so none uses it yet.
|
||||
`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
|
||||
|
||||
@@ -1136,6 +1136,9 @@
|
||||
"src/ui/kit/UiChip.test.mjs",
|
||||
"src/ui/kit/UiSelect.js",
|
||||
"src/ui/kit/UiSelect.test.mjs",
|
||||
"src/ui/kit/UiBadge.vue",
|
||||
"src/ui/kit/UiBadge.test.mjs",
|
||||
"src/ui/kit/UiEmpty.vue",
|
||||
"src/ui/rail/RailChips.vue",
|
||||
"src/ui/displayPanelActions.test.mjs",
|
||||
"src/testSupport/vueDom.mjs",
|
||||
|
||||
@@ -136,7 +136,9 @@
|
||||
"src/sdr/gain.js",
|
||||
"src/ui/kit/UiSlider.js",
|
||||
"src/ui/kit/UiChip.vue",
|
||||
"src/ui/kit/UiSelect.js"
|
||||
"src/ui/kit/UiSelect.js",
|
||||
"src/ui/kit/UiEmpty.vue",
|
||||
"src/ui/kit/UiBadge.vue"
|
||||
],
|
||||
"external": ["vue", "pinia"]
|
||||
},
|
||||
@@ -670,7 +672,8 @@
|
||||
"src/ui/rail/RailTimeline.vue",
|
||||
"src/ui/rail/rangeValue.js",
|
||||
"src/ui/weather/WeatherReadout.vue",
|
||||
"src/ui/kit/UiButton.vue"
|
||||
"src/ui/kit/UiButton.vue",
|
||||
"src/ui/kit/UiBadge.vue"
|
||||
],
|
||||
"external": ["vue"]
|
||||
},
|
||||
@@ -1070,7 +1073,8 @@
|
||||
"src/ui/rail/panelState.js",
|
||||
"src/ui/rail/fieldValue.js",
|
||||
"src/ui/kit/UiSlider.js",
|
||||
"src/ui/kit/UiButton.vue"
|
||||
"src/ui/kit/UiButton.vue",
|
||||
"src/ui/kit/UiBadge.vue"
|
||||
],
|
||||
"external": ["cesium", "vue", "pinia"]
|
||||
},
|
||||
@@ -3081,7 +3085,9 @@
|
||||
"src/ui/rail/panelState.js",
|
||||
"src/ui/rail/fieldValue.js",
|
||||
"src/ui/kit/UiSlider.js",
|
||||
"src/ui/kit/UiButton.vue"
|
||||
"src/ui/kit/UiButton.vue",
|
||||
"src/ui/kit/UiEmpty.vue",
|
||||
"src/ui/kit/UiBadge.vue"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { after, before, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { installVueDom } from '../../testSupport/vueDom.mjs';
|
||||
|
||||
let mount;
|
||||
let UiBadge;
|
||||
let UiEmpty;
|
||||
let uninstall;
|
||||
before(async () => {
|
||||
uninstall = installVueDom();
|
||||
({ mount } = await import('@vue/test-utils'));
|
||||
UiBadge = (await import('./UiBadge.vue')).default;
|
||||
UiEmpty = (await import('./UiEmpty.vue')).default;
|
||||
});
|
||||
after(() => uninstall());
|
||||
|
||||
test('a badge carries the kit classes, Core’s and the caller’s', async () => {
|
||||
const wrapper = mount(UiBadge, {
|
||||
attrs: { id: 'cal', class: 'gev-status-badge--box', role: 'status' },
|
||||
slots: { default: 'CAL · --' },
|
||||
});
|
||||
const badge = wrapper.get('span');
|
||||
assert.deepEqual(badge.classes(), [
|
||||
'ui-badge',
|
||||
'gev-status-badge',
|
||||
'ui-badge--neutral',
|
||||
'gev-status-badge--box',
|
||||
]);
|
||||
assert.equal(badge.attributes('role'), 'status');
|
||||
assert.equal(badge.element.textContent, 'CAL · --');
|
||||
await wrapper.setProps({ tone: 'warn' });
|
||||
assert.ok(badge.classes().includes('ui-badge--warn'));
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('an empty state is its title, then its hint', async () => {
|
||||
const wrapper = mount(UiEmpty, {
|
||||
props: { title: 'No shots yet.' },
|
||||
attrs: { class: 'scene-shot-empty', hidden: true },
|
||||
});
|
||||
const empty = wrapper.get('div');
|
||||
assert.deepEqual(empty.classes(), ['ui-empty', 'scene-shot-empty']);
|
||||
assert.equal(empty.element.textContent, 'No shots yet.');
|
||||
assert.equal(empty.attributes('title'), undefined);
|
||||
assert.equal(empty.element.hidden, true);
|
||||
await wrapper.setProps({ hint: 'Capture one.' });
|
||||
assert.equal(empty.get('.ui-empty__hint').text(), 'Capture one.');
|
||||
wrapper.unmount();
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
// Core's read-only badge, with @halfpixel/ui-vue UiBadge's `tone` and classes
|
||||
// so the two kits can merge. It carries Core's `gev-status-badge` class, and
|
||||
// callers add its density variants (`gev-status-badge--box`, `--plain`) and
|
||||
// the feature classes that colour it; the kit classes carry no Core styles
|
||||
// yet. Not mirrored yet: pulse, busy, the tone glyphs and the clipped title.
|
||||
defineProps({
|
||||
tone: { type: String, default: 'neutral' },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="ui-badge gev-status-badge" :class="`ui-badge--${tone}`"
|
||||
><slot
|
||||
/></span>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
// Core's empty state, with @halfpixel/ui-vue UiEmpty's `title` and `hint` so
|
||||
// the two kits can merge. Core's empty states are one line, so the title is
|
||||
// the text itself rather than ui-vue's bold heading; a hint follows it. Core
|
||||
// styles it through the classes its callers pass. Not mirrored yet: icon.
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
hint: { type: String, default: undefined },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui-empty">
|
||||
{{ title }}<span v-if="hint" class="ui-empty__hint">{{ hint }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount } from 'vue';
|
||||
import RailCardBlocks from './RailCardBlocks.vue';
|
||||
import UiBadge from '../kit/UiBadge.vue';
|
||||
|
||||
// One keyed card: a disclosure header and its blocks. The owner decides
|
||||
// which card is open.
|
||||
@@ -45,10 +46,10 @@ const nextBodyId = () => ++bodies;
|
||||
><span class="rail-card-title rail-card-nowrap" :title="card.title">{{
|
||||
card.title
|
||||
}}</span></span
|
||||
><span
|
||||
:class="`gev-status-badge gev-status-badge--plain rail-card-badge${badgeClassName ? ` ${badgeClassName}` : ''}`"
|
||||
><UiBadge
|
||||
:class="`gev-status-badge--plain rail-card-badge${badgeClassName ? ` ${badgeClassName}` : ''}`"
|
||||
:hidden="!card.badge"
|
||||
>{{ card.badge || '' }}</span
|
||||
>{{ card.badge || '' }}</UiBadge
|
||||
></span
|
||||
><span class="rail-card-disclosure" aria-hidden="true">{{
|
||||
open ? '▾' : '▸'
|
||||
|
||||
@@ -6,6 +6,7 @@ import RailCardBlocks from '../rail/RailCardBlocks.vue';
|
||||
import RailCards from '../rail/RailCards.vue';
|
||||
import { vRangeValue } from '../rail/rangeValue.js';
|
||||
import RecentImageryCard from './RecentImageryCard.vue';
|
||||
import UiEmpty from '../kit/UiEmpty.vue';
|
||||
|
||||
// Every block exists from the start, in its final order. The owner computes
|
||||
// the view and listens on the root; a mode choice calls `onSetMode`.
|
||||
@@ -78,10 +79,10 @@ const modeOptions = computed(() =>
|
||||
><RecentImageryCard
|
||||
v-for="card in view.strip.cards"
|
||||
:key="card.key"
|
||||
:card="card"
|
||||
/><div class="ri-strip-empty" :hidden="view.strip.cards.length > 0">{{
|
||||
view.strip.empty
|
||||
}}</div></div
|
||||
:card="card" /><UiEmpty
|
||||
class="ri-strip-empty"
|
||||
:hidden="view.strip.cards.length > 0"
|
||||
:title="view.strip.empty" /></div
|
||||
><div id="ri-hint" class="ri-block ri-line ri-hint" :title="view.hint">{{
|
||||
view.hint
|
||||
}}</div
|
||||
|
||||
@@ -5,6 +5,7 @@ import { t } from '../../i18n/index.js';
|
||||
import { describeShot } from '../scenePresentation.js';
|
||||
import UiButton from '../kit/UiButton.vue';
|
||||
import { useSceneStore } from '../stores/scenes.js';
|
||||
import UiEmpty from '../kit/UiEmpty.vue';
|
||||
|
||||
// The selected scene's shots. Rows keep their identity across selection so a
|
||||
// click followed by a double-click lands on the same label.
|
||||
@@ -73,9 +74,11 @@ function onRenameKey(shot, event) {
|
||||
<template>
|
||||
<div id="scene-shot-list" class="scene-shot-list ui-scroll">
|
||||
<template v-if="attached">
|
||||
<div v-if="!scene?.shots.length" class="scene-shot-empty">{{
|
||||
t('No shots yet. Use CAPTURE SHOT to save current look.')
|
||||
}}</div>
|
||||
<UiEmpty
|
||||
v-if="!scene?.shots.length"
|
||||
class="scene-shot-empty"
|
||||
:title="t('No shots yet. Use CAPTURE SHOT to save current look.')"
|
||||
/>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="shot in scene.shots"
|
||||
|
||||
@@ -7,6 +7,7 @@ import RailPanel from '../../rail/RailPanel.vue';
|
||||
import CctvCalValue from '../../cctv/CctvCalValue.vue';
|
||||
import CctvPicker from '../../cctv/CctvPicker.vue';
|
||||
import { useCctvStore } from '../../stores/cctv.js';
|
||||
import UiBadge from '../../kit/UiBadge.vue';
|
||||
|
||||
// The CCTV panel, rendered from what CctvControls presents and publishes;
|
||||
// until it renders, the panel shows its initial markup. The controls draw
|
||||
@@ -35,7 +36,7 @@ const frameData = computed(() =>
|
||||
<img id="cctv-frame" alt="CCTV feed frame" :class="{ active: frame?.hasFrame }" :src="frame?.src || undefined" :hidden="live" v-bind="frameData" />
|
||||
<canvas id="cctv-video" class="cctv-frame" :hidden="!live"></canvas>
|
||||
</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>
|
||||
<UiBadge id="cctv-source-badge" role="status" aria-live="polite" :data-frame-state="sourceBadge?.state">{{ sourceBadge?.label ?? t('SOURCE · UNKNOWN') }}</UiBadge>
|
||||
<div id="cctv-meta">{{ view?.meta ?? t('Enable CCTV to load camera intersections') }}</div>
|
||||
<div class="cctv-controls">
|
||||
<UiButton class="scene-btn" :pressed="Boolean(view?.enabled)" pressed-class="active" id="cctv-enable-btn">{{ view?.enabled ? t('CCTV ON') : t('CCTV OFF') }}</UiButton>
|
||||
@@ -53,7 +54,7 @@ const frameData = computed(() =>
|
||||
</div>
|
||||
<div class="cctv-controls">
|
||||
<UiButton class="scene-btn" :pressed="Boolean(view?.projection.active)" pressed-class="active" id="cctv-projection-btn" :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>
|
||||
<UiBadge id="cctv-quality-chip" class="gev-status-badge--box" :data-cal-badge="view?.quality.calBadge">{{ view?.quality.label ?? t('CAL · {badge}', { badge: '--' }) }}</UiBadge>
|
||||
</div>
|
||||
<div class="cctv-calibration-block">
|
||||
<div class="cctv-calibration-header">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { storeToRefs } from 'pinia';
|
||||
import { onBeforeUpdate, onUpdated, ref } from 'vue';
|
||||
import { t } from '../../../i18n/index.js';
|
||||
import { useContextStore } from '../../stores/context.js';
|
||||
import UiEmpty from '../../kit/UiEmpty.vue';
|
||||
|
||||
// The Space Missions roster. The launches layer presents it; until it does,
|
||||
// the list shows its loading line. Hover and keyboard focus drive the
|
||||
@@ -69,8 +70,8 @@ onUpdated(() => {
|
||||
<output data-mission-roster-count>{{ roster ? roster.count : '—' }}</output>
|
||||
</div>
|
||||
<div class="space-mission-roster-list ui-scroll" data-mission-roster-list>
|
||||
<div v-if="!roster" class="space-mission-roster-empty">{{ t('LOADING 30-DAY MISSION INDEX') }}</div>
|
||||
<div v-else-if="!roster.entries.length" class="space-mission-roster-empty">{{ roster.empty }}</div>
|
||||
<UiEmpty v-if="!roster" class="space-mission-roster-empty" :title="t('LOADING 30-DAY MISSION INDEX')" />
|
||||
<UiEmpty v-else-if="!roster.entries.length" class="space-mission-roster-empty" :title="roster.empty" />
|
||||
<button v-for="entry in roster?.entries" v-else :key="entry.id" type="button" class="space-mission-roster-item" :data-mission-roster-index="entry.index" :data-mission-roster-id="entry.id" :aria-label="entry.ariaLabel" @click="layer?.selectMissionAt(entry.index)" @mouseenter="enter(entry.index)" @mousemove="move(entry.index)" @mouseleave="layer?.rosterPointerLeave()" @focus="layer?.rosterFocus(entry.index)" @blur="blur"><span class="space-mission-roster-marker" :style="{ '--mission-roster-color': entry.color }" aria-hidden="true"></span><span class="space-mission-roster-copy"><strong>{{ entry.label }}</strong><small>{{ entry.provider }} · {{ entry.date }}</small></span><span class="space-mission-roster-chevron" aria-hidden="true">›</span></button>
|
||||
</div>
|
||||
<p class="space-mission-roster-focus-continuation" tabindex="-1" data-mission-roster-focus-continuation data-i18n>TAB PREVIEWS · ENTER / SPACE SELECTS</p>
|
||||
|
||||
Reference in new issue
Block a user