diff --git a/CHANGELOG.md b/CHANGELOG.md index f414857b3..b9a35f0fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/VUE-MIGRATION.md b/docs/VUE-MIGRATION.md index 9e43e4030..fddfdaa0d 100644 --- a/docs/VUE-MIGRATION.md +++ b/docs/VUE-MIGRATION.md @@ -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 diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 9ae37e6b8..4fe6adf6f 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -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", diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 8c56e857b..00e7cbf03 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -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", diff --git a/src/ui/kit/UiBadge.test.mjs b/src/ui/kit/UiBadge.test.mjs new file mode 100644 index 000000000..ac1e4372d --- /dev/null +++ b/src/ui/kit/UiBadge.test.mjs @@ -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(); +}); diff --git a/src/ui/kit/UiBadge.vue b/src/ui/kit/UiBadge.vue new file mode 100644 index 000000000..086de3761 --- /dev/null +++ b/src/ui/kit/UiBadge.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/ui/kit/UiEmpty.vue b/src/ui/kit/UiEmpty.vue new file mode 100644 index 000000000..0bd88dbb5 --- /dev/null +++ b/src/ui/kit/UiEmpty.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/ui/rail/RailCard.vue b/src/ui/rail/RailCard.vue index 0881e4079..9cd8cc0b8 100644 --- a/src/ui/rail/RailCard.vue +++ b/src/ui/rail/RailCard.vue @@ -1,6 +1,7 @@