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:
Sameh Khamis committed 2026-11-01 12:31:09 -08:00
1 parent ecaf723035
commit b9ecb9b9f5
12 files changed
+126 -19

No files matched your search

+49
View File
@@ -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();
});
+16
View File
@@ -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>
+16
View File
@@ -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>