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
|
kit's `UiButton`, which gains `pressedClass`; so are the rail card
|
||||||
chips, map source chips and location pills (which now report
|
chips, map source chips and location pills (which now report
|
||||||
`aria-pressed`), and the voice card's chips
|
`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`
|
`syncScopeControls`
|
||||||
(`./ui/display`) and `shouldHideCollapsedRightPanels` (`./ui/layout`) are
|
(`./ui/display`) and `shouldHideCollapsedRightPanels` (`./ui/layout`) are
|
||||||
deprecated: the Display view and the rail now own what they did. `./ui/input` imports Vue
|
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,
|
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
|
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`.
|
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
|
`UiSlider` keeps the native range input where ui-vue draws a
|
||||||
`role="slider"` track, so Core's keyboard, form behaviour and styling stay;
|
`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
|
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/UiChip.test.mjs",
|
||||||
"src/ui/kit/UiSelect.js",
|
"src/ui/kit/UiSelect.js",
|
||||||
"src/ui/kit/UiSelect.test.mjs",
|
"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/rail/RailChips.vue",
|
||||||
"src/ui/displayPanelActions.test.mjs",
|
"src/ui/displayPanelActions.test.mjs",
|
||||||
"src/testSupport/vueDom.mjs",
|
"src/testSupport/vueDom.mjs",
|
||||||
|
|||||||
@@ -136,7 +136,9 @@
|
|||||||
"src/sdr/gain.js",
|
"src/sdr/gain.js",
|
||||||
"src/ui/kit/UiSlider.js",
|
"src/ui/kit/UiSlider.js",
|
||||||
"src/ui/kit/UiChip.vue",
|
"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"]
|
"external": ["vue", "pinia"]
|
||||||
},
|
},
|
||||||
@@ -670,7 +672,8 @@
|
|||||||
"src/ui/rail/RailTimeline.vue",
|
"src/ui/rail/RailTimeline.vue",
|
||||||
"src/ui/rail/rangeValue.js",
|
"src/ui/rail/rangeValue.js",
|
||||||
"src/ui/weather/WeatherReadout.vue",
|
"src/ui/weather/WeatherReadout.vue",
|
||||||
"src/ui/kit/UiButton.vue"
|
"src/ui/kit/UiButton.vue",
|
||||||
|
"src/ui/kit/UiBadge.vue"
|
||||||
],
|
],
|
||||||
"external": ["vue"]
|
"external": ["vue"]
|
||||||
},
|
},
|
||||||
@@ -1070,7 +1073,8 @@
|
|||||||
"src/ui/rail/panelState.js",
|
"src/ui/rail/panelState.js",
|
||||||
"src/ui/rail/fieldValue.js",
|
"src/ui/rail/fieldValue.js",
|
||||||
"src/ui/kit/UiSlider.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"]
|
"external": ["cesium", "vue", "pinia"]
|
||||||
},
|
},
|
||||||
@@ -3081,7 +3085,9 @@
|
|||||||
"src/ui/rail/panelState.js",
|
"src/ui/rail/panelState.js",
|
||||||
"src/ui/rail/fieldValue.js",
|
"src/ui/rail/fieldValue.js",
|
||||||
"src/ui/kit/UiSlider.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": [
|
"external": [
|
||||||
"hls.js",
|
"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>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount } from 'vue';
|
import { computed, onBeforeUnmount } from 'vue';
|
||||||
import RailCardBlocks from './RailCardBlocks.vue';
|
import RailCardBlocks from './RailCardBlocks.vue';
|
||||||
|
import UiBadge from '../kit/UiBadge.vue';
|
||||||
|
|
||||||
// One keyed card: a disclosure header and its blocks. The owner decides
|
// One keyed card: a disclosure header and its blocks. The owner decides
|
||||||
// which card is open.
|
// which card is open.
|
||||||
@@ -45,10 +46,10 @@ const nextBodyId = () => ++bodies;
|
|||||||
><span class="rail-card-title rail-card-nowrap" :title="card.title">{{
|
><span class="rail-card-title rail-card-nowrap" :title="card.title">{{
|
||||||
card.title
|
card.title
|
||||||
}}</span></span
|
}}</span></span
|
||||||
><span
|
><UiBadge
|
||||||
:class="`gev-status-badge gev-status-badge--plain rail-card-badge${badgeClassName ? ` ${badgeClassName}` : ''}`"
|
:class="`gev-status-badge--plain rail-card-badge${badgeClassName ? ` ${badgeClassName}` : ''}`"
|
||||||
:hidden="!card.badge"
|
:hidden="!card.badge"
|
||||||
>{{ card.badge || '' }}</span
|
>{{ card.badge || '' }}</UiBadge
|
||||||
></span
|
></span
|
||||||
><span class="rail-card-disclosure" aria-hidden="true">{{
|
><span class="rail-card-disclosure" aria-hidden="true">{{
|
||||||
open ? '▾' : '▸'
|
open ? '▾' : '▸'
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import RailCardBlocks from '../rail/RailCardBlocks.vue';
|
|||||||
import RailCards from '../rail/RailCards.vue';
|
import RailCards from '../rail/RailCards.vue';
|
||||||
import { vRangeValue } from '../rail/rangeValue.js';
|
import { vRangeValue } from '../rail/rangeValue.js';
|
||||||
import RecentImageryCard from './RecentImageryCard.vue';
|
import RecentImageryCard from './RecentImageryCard.vue';
|
||||||
|
import UiEmpty from '../kit/UiEmpty.vue';
|
||||||
|
|
||||||
// Every block exists from the start, in its final order. The owner computes
|
// 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`.
|
// the view and listens on the root; a mode choice calls `onSetMode`.
|
||||||
@@ -78,10 +79,10 @@ const modeOptions = computed(() =>
|
|||||||
><RecentImageryCard
|
><RecentImageryCard
|
||||||
v-for="card in view.strip.cards"
|
v-for="card in view.strip.cards"
|
||||||
:key="card.key"
|
:key="card.key"
|
||||||
:card="card"
|
:card="card" /><UiEmpty
|
||||||
/><div class="ri-strip-empty" :hidden="view.strip.cards.length > 0">{{
|
class="ri-strip-empty"
|
||||||
view.strip.empty
|
:hidden="view.strip.cards.length > 0"
|
||||||
}}</div></div
|
:title="view.strip.empty" /></div
|
||||||
><div id="ri-hint" class="ri-block ri-line ri-hint" :title="view.hint">{{
|
><div id="ri-hint" class="ri-block ri-line ri-hint" :title="view.hint">{{
|
||||||
view.hint
|
view.hint
|
||||||
}}</div
|
}}</div
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { t } from '../../i18n/index.js';
|
|||||||
import { describeShot } from '../scenePresentation.js';
|
import { describeShot } from '../scenePresentation.js';
|
||||||
import UiButton from '../kit/UiButton.vue';
|
import UiButton from '../kit/UiButton.vue';
|
||||||
import { useSceneStore } from '../stores/scenes.js';
|
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
|
// The selected scene's shots. Rows keep their identity across selection so a
|
||||||
// click followed by a double-click lands on the same label.
|
// click followed by a double-click lands on the same label.
|
||||||
@@ -73,9 +74,11 @@ function onRenameKey(shot, event) {
|
|||||||
<template>
|
<template>
|
||||||
<div id="scene-shot-list" class="scene-shot-list ui-scroll">
|
<div id="scene-shot-list" class="scene-shot-list ui-scroll">
|
||||||
<template v-if="attached">
|
<template v-if="attached">
|
||||||
<div v-if="!scene?.shots.length" class="scene-shot-empty">{{
|
<UiEmpty
|
||||||
t('No shots yet. Use CAPTURE SHOT to save current look.')
|
v-if="!scene?.shots.length"
|
||||||
}}</div>
|
class="scene-shot-empty"
|
||||||
|
:title="t('No shots yet. Use CAPTURE SHOT to save current look.')"
|
||||||
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
v-for="shot in scene.shots"
|
v-for="shot in scene.shots"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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';
|
||||||
import { useCctvStore } from '../../stores/cctv.js';
|
import { useCctvStore } from '../../stores/cctv.js';
|
||||||
|
import UiBadge from '../../kit/UiBadge.vue';
|
||||||
|
|
||||||
// The CCTV panel, rendered from what CctvControls presents and publishes;
|
// The CCTV panel, rendered from what CctvControls presents and publishes;
|
||||||
// until it renders, the panel shows its initial markup. The controls draw
|
// 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" />
|
<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>
|
<canvas id="cctv-video" class="cctv-frame" :hidden="!live"></canvas>
|
||||||
</div>
|
</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 id="cctv-meta">{{ view?.meta ?? t('Enable CCTV to load camera intersections') }}</div>
|
||||||
<div class="cctv-controls">
|
<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>
|
<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>
|
||||||
<div class="cctv-controls">
|
<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>
|
<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>
|
||||||
<div class="cctv-calibration-block">
|
<div class="cctv-calibration-block">
|
||||||
<div class="cctv-calibration-header">
|
<div class="cctv-calibration-header">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { storeToRefs } from 'pinia';
|
|||||||
import { onBeforeUpdate, onUpdated, ref } from 'vue';
|
import { onBeforeUpdate, onUpdated, ref } from 'vue';
|
||||||
import { t } from '../../../i18n/index.js';
|
import { t } from '../../../i18n/index.js';
|
||||||
import { useContextStore } from '../../stores/context.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 Space Missions roster. The launches layer presents it; until it does,
|
||||||
// the list shows its loading line. Hover and keyboard focus drive the
|
// 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>
|
<output data-mission-roster-count>{{ roster ? roster.count : '—' }}</output>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-mission-roster-list ui-scroll" data-mission-roster-list>
|
<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>
|
<UiEmpty v-if="!roster" class="space-mission-roster-empty" :title="t('LOADING 30-DAY MISSION INDEX')" />
|
||||||
<div v-else-if="!roster.entries.length" class="space-mission-roster-empty">{{ roster.empty }}</div>
|
<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>
|
<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>
|
</div>
|
||||||
<p class="space-mission-roster-focus-continuation" tabindex="-1" data-mission-roster-focus-continuation data-i18n>TAB PREVIEWS · ENTER / SPACE SELECTS</p>
|
<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