feat(ui): give every panel one scrollbar, select and slider skin

Every scrolling surface carries the kit's .ui-scroll, which
draws the thin themed scrollbar from --scroll-thumb and --scroll-track; the
Context panel and others drew the platform's light scrollbar because the
page declares no dark color-scheme. The per-panel scrollbar rules go; Cyber
sets the two tokens and Cockpit's HUD windows keep their accent tint.

Selects draw .gev-field's own chevron instead of the platform arrow, the
voice selects join .gev-field, number fields keep dark spinners, and the
Street Level range uses the shared slider in its green.
This commit is contained in:
Sameh Khamis committed 2026-10-31 17:28:46 -07:00
1 parent 1072df325e
commit 484243fe4b
42 files changed
+133 -152

No files matched your search

+6 -2
View File
@@ -22,10 +22,14 @@ test('CCTV and Context keep their headers outside Cyber-only scroll bodies', ()
new URL(`./views/${file}`, import.meta.url),
'utf8',
);
assert.match(markup, /class="cyber-panel-body" data-rail-scroller/);
// The body wears the shared scrollbar skin where Cyber makes it scroll.
assert.match(
markup,
/class="cyber-panel-body ui-scroll" data-rail-scroller/,
);
assert.ok(
markup.indexOf('class="panel-header"') <
markup.indexOf('class="cyber-panel-body"'),
markup.indexOf('class="cyber-panel-body ui-scroll"'),
);
}
const shared = readFileSync(
+1 -1
View File
@@ -431,7 +431,7 @@ export function mountLocalePicker({
heading.textContent = t('Interface language');
popup.append(heading);
const listbox = element(documentRef, 'ul', 'locale-picker-list');
const listbox = element(documentRef, 'ul', 'locale-picker-list ui-scroll');
listbox.id = ids.listbox;
listbox.setAttribute('role', 'listbox');
listbox.setAttribute('aria-labelledby', ids.heading);
+1 -1
View File
@@ -35,7 +35,7 @@ onUpdated(followCurrent);
<template>
<ol
ref="root"
class="data-row-list"
class="data-row-list ui-scroll"
:data-block-id="blockId"
:hidden="!list?.items?.length"
:aria-label="list?.ariaLabel || ''"
@@ -54,7 +54,10 @@ defineProps({
></div
><div
id="ri-strip"
:class="['ri-block ri-strip', { searching: view.strip.searching }]"
:class="[
'ri-block ri-strip ui-scroll',
{ searching: view.strip.searching },
]"
tabindex="0"
role="listbox"
aria-orientation="horizontal"
-2
View File
@@ -106,8 +106,6 @@
min-height: 0;
overflow: hidden;
background: #03080d;
scrollbar-width: thin;
scrollbar-color: rgba(107, 232, 255, 0.36) transparent;
}
.bhote-embedded-callout.is-youtube {
-21
View File
@@ -35,8 +35,6 @@
gap: 8px;
max-height: calc(100vh - 24px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.46) rgba(255, 255, 255, 0.06);
backdrop-filter: blur(24px) saturate(1.4);
-webkit-backdrop-filter: blur(24px) saturate(1.4);
box-shadow:
@@ -45,25 +43,6 @@
0 1px 0 rgba(255, 255, 255, 0.06) inset;
}
.cctv-panel-inner::-webkit-scrollbar {
width: 8px;
}
.cctv-panel-inner::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.04);
border-radius: 999px;
}
.cctv-panel-inner::-webkit-scrollbar-thumb {
background: linear-gradient(
180deg,
rgba(0, 212, 255, 0.6),
rgba(0, 212, 255, 0.28)
);
border: 1px solid rgba(0, 212, 255, 0.4);
border-radius: 999px;
}
.cctv-controls {
display: flex;
gap: 8px;
+8 -4
View File
@@ -41,8 +41,6 @@
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(165, 188, 199, 0.35) transparent;
}
:root[data-ui-theme='cyber'] .cctv-panel-inner > .cyber-panel-body {
@@ -68,8 +66,6 @@
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(165, 188, 199, 0.35) transparent;
}
:root[data-ui-theme='cyber'] #pp-toggles > .pp-panel-body > * {
@@ -82,6 +78,8 @@
:root[data-ui-theme='cyber'] {
color-scheme: dark;
--scroll-thumb: rgba(165, 188, 199, 0.35);
--scroll-track: transparent;
--cyber-red: #c45454;
--cyber-red-bright: #e16666;
--cyber-slate: #26343a;
@@ -796,6 +794,12 @@
accent-color: var(--cyber-red);
}
/* Street Level's range wears Cyber's slider accent like every other range. */
:root[data-ui-theme='cyber'] .sl-range {
--accent: inherit;
--accent-glow: inherit;
}
:root[data-ui-theme='cyber']
:where(button, a[href], input, select, textarea):focus-visible {
outline-color: var(--cyber-red-bright) !important;
+2 -11
View File
@@ -110,20 +110,11 @@
overflow-y: auto;
overscroll-behavior: contain;
scroll-behavior: smooth;
/* A visible, non-overlaying affordance that the list continues. */
scrollbar-width: thin;
scrollbar-color: rgba(54, 220, 255, 0.45) transparent;
/* A visible, non-overlaying affordance that the list continues; the
scrollbar itself is the kit's .ui-scroll. */
scrollbar-gutter: stable;
}
.first-run-choices::-webkit-scrollbar {
width: 6px;
}
.first-run-choices::-webkit-scrollbar-thumb {
background: rgba(54, 220, 255, 0.45);
border-radius: 3px;
}
.first-run-choices button {
display: grid;
grid-template-columns: 1.7rem 1fr 1.3rem;
+19
View File
@@ -10,6 +10,20 @@
font-family: var(--font-sans);
font-size: 12px;
outline: none;
/* Native number spinners and the open select list follow the dark skin. */
color-scheme: dark;
}
/* A select draws the field's own chevron, not the platform's arrow, so every
dropdown in the app reads the same. The open list stays native. */
select.gev-field {
-webkit-appearance: none;
appearance: none;
padding-right: 22px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='rgba(255,255,255,0.62)' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 10px 6px;
}
.gev-field:focus-visible {
@@ -30,6 +44,11 @@
padding: 2px 6px;
}
select.gev-field--compact {
padding-right: 18px;
background-position: right 6px center;
}
/* Compact inline editors share density and hide native number spinners.
Their containing feature sets width to fit the surrounding readout. */
.gev-field--inline {
+3
View File
@@ -9,6 +9,9 @@
--glass-border: rgba(255, 255, 255, 0.08);
--glass-border-hover: rgba(255, 255, 255, 0.15);
--accent: #00d4ff;
/* Every scrolling surface's scrollbar (the kit's .ui-scroll). */
--scroll-thumb: rgba(0, 212, 255, 0.46);
--scroll-track: rgba(255, 255, 255, 0.06);
--accent-dim: rgba(0, 212, 255, 0.15);
--accent-glow: rgba(0, 212, 255, 0.4);
--text-primary: #e8eaed;
+29
View File
@@ -1,6 +1,35 @@
/* Generic components in src/ui/kit. Names follow Halfpixel's ui-vue kit; the
surfaces use this application's glass, accent and type tokens. */
/* The thin themed scrollbar every scrolling surface wears, as ui-vue's
.ui-scroll. Without it a surface draws the platform's light scrollbar,
because the page declares no dark color-scheme. */
/* Cesium builds its credit list and the rail card builder its lines, so
those two surfaces are named here instead of carrying the class. */
.ui-scroll,
.cesium-credit-lightbox > ul,
.weather-card [data-line-id='status'] {
scrollbar-width: thin;
scrollbar-color: var(--scroll-thumb) var(--scroll-track);
}
.ui-scroll::-webkit-scrollbar,
.cesium-credit-lightbox > ul::-webkit-scrollbar,
.weather-card [data-line-id='status']::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.ui-scroll::-webkit-scrollbar-thumb,
.cesium-credit-lightbox > ul::-webkit-scrollbar-thumb,
.weather-card [data-line-id='status']::-webkit-scrollbar-thumb {
border-radius: 999px;
background: var(--scroll-thumb);
}
.ui-scroll::-webkit-scrollbar-track,
.cesium-credit-lightbox > ul::-webkit-scrollbar-track,
.weather-card [data-line-id='status']::-webkit-scrollbar-track {
background: var(--scroll-track);
}
.ui-icon {
display: inline-block;
line-height: 1;
-24
View File
@@ -84,29 +84,9 @@
overflow-y: auto;
overscroll-behavior: contain;
padding-right: 4px;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.5) rgba(255, 255, 255, 0.04);
scrollbar-gutter: stable;
}
.data-toggle-list::-webkit-scrollbar {
width: 6px;
}
.data-toggle-list::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.04);
border-radius: 999px;
}
.data-toggle-list::-webkit-scrollbar-thumb {
background: linear-gradient(
180deg,
rgba(0, 212, 255, 0.65),
rgba(0, 212, 255, 0.2)
);
border-radius: 999px;
}
.data-layer-group-heading {
margin: 8px 10px 2px;
padding-top: 12px;
@@ -311,8 +291,6 @@ body.recording-mode .gev-panel-rail {
#right-context-rail.layout-focus > #pp-toggles:not(.collapsed) {
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.4) transparent;
}
#right-context-rail.layout-focus
> #weather-panel:not(.collapsed)
@@ -445,8 +423,6 @@ body.recording-mode .gev-panel-rail {
overflow-x: hidden;
overflow-y: auto;
padding-right: 3px;
scrollbar-color: rgba(34, 230, 230, 0.38) transparent;
scrollbar-width: thin;
}
#space-mission-panel-host .space-mission-roster-item {
position: relative;
+1 -1
View File
@@ -152,7 +152,7 @@
overflow: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: var(--glass-border-hover) transparent;
scrollbar-color: var(--scroll-thumb) var(--scroll-track);
/* Keep an inset focus ring inside the intentional scrolling boundary. */
padding: 3px;
}
-10
View File
@@ -184,19 +184,9 @@
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(54, 220, 255, 0.45) transparent;
scrollbar-gutter: stable;
}
.key-setup-rows::-webkit-scrollbar {
width: 6px;
}
.key-setup-rows::-webkit-scrollbar-thumb {
background: rgba(54, 220, 255, 0.45);
border-radius: 3px;
}
.key-setup-row {
display: grid;
gap: 0.35rem;
+5 -6
View File
@@ -18,8 +18,6 @@
border-radius: 8px;
background: rgba(2, 15, 20, 0.42);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035);
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.46) rgba(255, 255, 255, 0.06);
}
#radio-panel .panel-header {
margin-bottom: 0;
@@ -1092,11 +1090,12 @@ html:not([lang='en']) .data-row-list-text {
.sdr-radio-card select,
.sdr-radio-card input[type='number'] {
min-height: 28px;
border: 1px solid rgba(255, 183, 82, 0.28);
border-radius: 5px;
color: #ffd79a;
background: rgba(38, 25, 8, 0.52);
font: 8px/1 var(--font-mono);
/* The shared field paints the surface (and the select's chevron). */
--gev-field-border-color: rgba(255, 183, 82, 0.28);
--gev-field-color: #ffd79a;
--gev-field-background: rgba(38, 25, 8, 0.52);
}
.sdr-radio-card button {
padding: 6px 8px;
@@ -1129,7 +1128,7 @@ html:not([lang='en']) .data-row-list-text {
.sdr-gain-row select {
flex: 1;
min-width: 0;
padding: 5px 7px;
padding: 5px 22px 5px 7px;
}
.sdr-adsb-stats {
display: grid;
-3
View File
@@ -46,8 +46,6 @@
max-height: calc(var(--right-stack-max-height, 50svh) - 64px);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.46) transparent;
}
#recent-imagery-panel.collapsed #recent-imagery-panel-body,
#recent-imagery-panel.collapsed #recent-imagery-panel-count,
@@ -168,7 +166,6 @@
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x proximity;
scrollbar-width: thin;
outline: none;
border-radius: 6px;
}
-9
View File
@@ -265,15 +265,6 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
counter-reset: scene-shot;
}
.scene-shot-list::-webkit-scrollbar {
width: 6px;
}
.scene-shot-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.16);
border-radius: 999px;
}
.scene-shot-empty {
border: 1px dashed rgba(255, 255, 255, 0.14);
border-radius: 8px;
+3 -12
View File
@@ -39,8 +39,6 @@
0 8px 32px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.03) inset,
0 1px 0 rgba(255, 255, 255, 0.06) inset;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.46) rgba(255, 255, 255, 0.06);
}
/* Class-qualified, not id-qualified, so a theme's header rule still wins. */
@@ -236,17 +234,10 @@ html:not([lang='en']) .sl-meta-row .sl-nearest-btn {
gap: 4px 10px;
}
/* The shared slider surface, in Street Level's green. */
.sl-range {
width: 100%;
min-width: 0;
margin: 0;
accent-color: var(--sl-green);
cursor: pointer;
}
.sl-range:focus-visible {
outline: 1px solid var(--accent);
outline-offset: 3px;
--accent: var(--sl-green);
--accent-glow: color-mix(in srgb, var(--sl-green) 55%, transparent);
}
/* The value and its note share a line: ANY DATE (by upload date). */
+3 -3
View File
@@ -467,7 +467,8 @@ body.recording-mode .gev-pointer-reticle {
min-height: 2rem;
box-sizing: border-box;
color: var(--text-primary);
background: rgba(255, 255, 255, 0.04);
/* background-color, so a .gev-field select keeps its chevron. */
background-color: rgba(255, 255, 255, 0.04);
border: 1px solid var(--glass-border);
border-radius: 0.3rem;
font: inherit;
@@ -504,7 +505,6 @@ body.recording-mode .gev-pointer-reticle {
min-height: 0;
overflow: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
padding: 0.55rem 0.75rem 3px 3px;
line-height: 1.45;
overflow-wrap: anywhere;
@@ -553,7 +553,7 @@ body.recording-mode .gev-pointer-reticle {
#gev-voice-timeout-select {
flex: 1 1 auto;
min-width: 0;
padding: 0 0.45rem;
padding: 0 22px 0 0.45rem;
}
#gev-voice-timeout-input {
flex: 0 0 4.5rem;
+2 -3
View File
@@ -180,12 +180,11 @@ html:not([lang='en'])
min-width: 0;
width: 100%;
min-height: 30px;
color: var(--text-primary);
background: #09121b;
border: 1px solid var(--glass-border);
border-radius: 4px;
font: inherit;
font-size: 0.65rem;
--gev-field-background: #09121b;
--gev-field-border-color: var(--glass-border);
}
#gev-voice-control:hover .gev-voice-provider-field,
#gev-voice-control:focus-within .gev-voice-provider-field {
-2
View File
@@ -42,8 +42,6 @@
max-height: calc(var(--right-stack-max-height, 50svh) - 64px);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(0, 212, 255, 0.46) transparent;
}
#weather-panel.collapsed #weather-panel-body,
#weather-panel[hidden],
+1 -1
View File
@@ -22,7 +22,7 @@ function selectMapSource(chip) {
<template>
<!-- Unified bottom command dock: locations, voice (injected at runtime), visual presets -->
<div id="command-dock" aria-label="Navigation, voice, and visual preset controls" data-i18n-aria-label>
<div id="command-dock" class="ui-scroll" aria-label="Navigation, voice, and visual preset controls" data-i18n-aria-label>
<!-- Control panel -->
<div id="control-panel" class="panel-collapsible collapsed">
<div class="panel-glow"></div>
+5 -5
View File
@@ -14,7 +14,7 @@ const { view: ctx } = storeToRefs(useContextStore());
<template>
<!-- Global Context is deliberately independent from the left layer accordion. -->
<aside id="right-context-rail">
<aside id="right-context-rail" class="ui-scroll">
<div id="weather-panel" class="panel-collapsible" data-panel-id="weather-panel" hidden>
<div class="panel-glow"></div>
<div class="weather-panel-inner" data-rail-scroller>
@@ -24,7 +24,7 @@ const { view: ctx } = storeToRefs(useContextStore());
<span class="panel-divider"></span>
<button class="panel-collapse-btn" data-collapse-target="weather-panel" type="button" title="Collapse panel" data-i18n-title>▼</button>
</div>
<div id="weather-panel-body" data-rail-scroller></div>
<div id="weather-panel-body" class="ui-scroll" data-rail-scroller></div>
</div>
</div>
<div id="recent-imagery-panel" class="panel-collapsible" data-panel-id="recent-imagery-panel" hidden>
@@ -36,18 +36,18 @@ const { view: ctx } = storeToRefs(useContextStore());
<span class="panel-divider"></span>
<button class="panel-collapse-btn" data-collapse-target="recent-imagery-panel" type="button" title="Collapse panel" data-i18n-title>▼</button>
</div>
<div id="recent-imagery-panel-body" data-rail-scroller></div>
<div id="recent-imagery-panel-body" class="ui-scroll" data-rail-scroller></div>
</div>
</div>
<div id="global-context-panel" class="panel-collapsible collapsed" v-class-toggle="{ ...(ctx && { 'context-enabled': ctx.enabled }) }" :data-context-mode="ctx?.mode" data-panel-id="global-context-panel">
<div class="panel-glow"></div>
<div class="global-context-panel-inner" data-rail-scroller>
<div class="global-context-panel-inner ui-scroll" data-rail-scroller>
<div class="panel-header">
<span class="panel-title" data-i18n>CONTEXT</span>
<span class="panel-divider"></span>
<button class="panel-collapse-btn" data-collapse-target="global-context-panel" title="Expand panel" data-i18n-title>▶</button>
</div>
<div class="cyber-panel-body" data-rail-scroller>
<div class="cyber-panel-body ui-scroll" data-rail-scroller>
<div class="global-context-modes" role="tablist" aria-label="Context mode" data-i18n-aria-label>
<button id="global-context-flights-btn" class="context-mode-button" v-class-toggle="{ active: ctx?.flightsActive }" type="button" role="tab" tabindex="0" :aria-selected="String(ctx?.flightsActive ?? false)" :aria-disabled="ctx && String(ctx.busy)" :aria-busy="ctx && String(ctx.busy)" aria-controls="context-flights-view" aria-label="CONTACTS" title="Cycles the nearest contacts of whatever type you select — planes, vessels, installations. Satellites track independently." data-i18n-aria-label data-i18n-title>
<span class="material-symbols-outlined" aria-hidden="true">radar</span>
+3 -3
View File
@@ -6,7 +6,7 @@ import StreetLevelPanel from './panels/StreetLevelPanel.vue';
</script>
<template>
<div id="left-panel-stack">
<div id="left-panel-stack" class="ui-scroll">
<!-- Data Layer Toggle Panel -->
<div id="data-panel" class="panel-collapsible collapsed active" data-panel-id="data-panel">
<div class="panel-glow"></div>
@@ -16,7 +16,7 @@ import StreetLevelPanel from './panels/StreetLevelPanel.vue';
<span class="panel-divider"></span>
<button class="panel-collapse-btn" data-collapse-target="data-panel" title="Collapse panel" data-i18n-title>+</button>
</div>
<div id="data-toggles" class="data-toggle-list"></div>
<div id="data-toggles" class="data-toggle-list ui-scroll"></div>
</div>
</div>
@@ -29,7 +29,7 @@ import StreetLevelPanel from './panels/StreetLevelPanel.vue';
<!-- Scene Director Panel -->
<div id="scene-panel" class="panel-collapsible collapsed" data-panel-id="scene-panel">
<div class="panel-glow"></div>
<div class="scene-panel-inner">
<div class="scene-panel-inner ui-scroll">
<div class="panel-header">
<span class="panel-title" title="Create and edit scenes" data-i18n data-i18n-title>AUTHORING</span>
<span class="panel-divider"></span>
+1 -1
View File
@@ -22,7 +22,7 @@
</header>
<h2 id="key-setup-title" data-i18n>Power up the globe</h2>
<p id="key-setup-description" data-i18n>The globe already flies keyless. Every key below switches on another real feed — paste one and it's saved into this app's local configuration, then the server restarts itself. Server-side keys stay on this machine; Google Maps and Cesium ion run in the browser and must be provider-restricted. Keys you configured elsewhere are shown but never touched.</p>
<div class="key-setup-rows" data-key-setup-rows></div>
<div class="key-setup-rows ui-scroll" data-key-setup-rows></div>
<div class="key-setup-footer">
<button type="button" class="key-setup-apply" data-key-setup-apply data-i18n>SAVE KEYS</button>
<span class="key-setup-hint" data-i18n>ESC to close</span>
+1 -1
View File
@@ -70,7 +70,7 @@ function onRenameKey(shot, event) {
</script>
<template>
<div id="scene-shot-list" class="scene-shot-list">
<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.')
+1 -1
View File
@@ -14,7 +14,7 @@
like something you should not have access to → the sources are all
public → the data is real. Tile subcopy stays purely functional. -->
<p id="first-run-description" data-i18n-runtime>It feels like a forbidden cockpit—then you realize the sources are public and the data is real.</p>
<div class="first-run-choices">
<div class="first-run-choices ui-scroll">
<button type="button" data-first-run-choice="contacts">
<span class="material-symbols-outlined" aria-hidden="true">radar</span>
<span><strong data-i18n>LIVE CONTACTS</strong><small data-i18n>Aircraft, vessels and nearby intelligence</small></span>
+1 -1
View File
@@ -4,5 +4,5 @@ defineProps({ names: { type: Object, required: true } });
</script>
<template>
<details class="military-awareness-names"><summary>{{ names.summary }}</summary><div><template v-for="(name, index) in names.names" :key="index"><br v-if="index">{{ name }}</template></div></details>
<details class="military-awareness-names"><summary>{{ names.summary }}</summary><div class="ui-scroll"><template v-for="(name, index) in names.names" :key="index"><br v-if="index">{{ name }}</template></div></details>
</template>
+1 -1
View File
@@ -41,7 +41,7 @@ onUpdated(() => {
</script>
<template>
<aside id="military-awareness-panel" ref="root">
<aside id="military-awareness-panel" class="ui-scroll" ref="root">
<div v-if="!view" class="military-awareness-standby">
<strong>{{ t('CONTACTS CONTEXT OFF') }}</strong>
<span>{{ t('SELECT CONTACTS TO LOAD OBSERVED / MAPPED PROXIMITY') }}</span>
+2 -2
View File
@@ -28,13 +28,13 @@ const frameData = computed(() =>
<template>
<div id="cctv-panel" class="panel-collapsible collapsed" data-panel-id="cctv-panel">
<div class="panel-glow"></div>
<div class="cctv-panel-inner" data-rail-scroller>
<div class="cctv-panel-inner ui-scroll" data-rail-scroller>
<div class="panel-header">
<span class="panel-title" data-i18n>CCTV</span>
<span class="panel-divider"></span>
<button class="panel-collapse-btn" data-collapse-target="cctv-panel" title="Collapse panel" data-i18n-title>+</button>
</div>
<div class="cyber-panel-body" data-rail-scroller>
<div class="cyber-panel-body ui-scroll" data-rail-scroller>
<div id="cctv-frame-wrap" :class="['cctv-frame-wrap', { loading: frame?.wrapLoading, 'has-frame': frame?.hasFrame }]">
<span class="cctv-frame-caption" aria-hidden="true" data-i18n>ACQUIRING FRAME</span>
<img id="cctv-frame" alt="CCTV feed frame" :class="{ active: frame?.hasFrame }" :src="frame?.src || undefined" :hidden="live" v-bind="frameData" />
@@ -28,20 +28,20 @@ const radioLabel = computed(() =>
<template>
<aside id="cockpit-utility-controls" class="cockpit-utility-controls" aria-label="Cockpit display and Radio controls" data-i18n-aria-label>
<div class="cockpit-utility-control" v-class-toggle="{ 'is-expanded': open.display }">
<div class="cockpit-utility-control ui-scroll" v-class-toggle="{ 'is-expanded': open.display }">
<div class="cockpit-utility-launcher" data-cockpit-launcher="display">
<span data-i18n>DISPLAY</span>
<span class="cockpit-utility-divider" aria-hidden="true"></span>
<button id="cockpit-display-toggle-btn" class="cockpit-utility-glyph" type="button" :aria-expanded="String(open.display)" aria-controls="cockpit-display-panel" :aria-label="displayLabel" :title="displayLabel">{{ open.display ? '▶' : '◀' }}</button>
</div>
<div id="cockpit-display-panel" class="cockpit-utility-popover" role="group" aria-label="Cockpit display options" :hidden="!open.display" data-i18n-aria-label>
<div id="cockpit-display-panel" class="cockpit-utility-popover ui-scroll" role="group" aria-label="Cockpit display options" :hidden="!open.display" data-i18n-aria-label>
<div class="cockpit-display-slot" data-cockpit-display-slot="hud"></div>
<div class="cockpit-display-slot" data-cockpit-display-slot="detection"></div>
<div class="cockpit-display-slot" data-cockpit-display-slot="parameters"></div>
<div class="cockpit-display-slot" data-cockpit-display-slot="models3d"></div>
</div>
</div>
<div class="cockpit-utility-control" v-class-toggle="{ 'is-expanded': open.radio }">
<div class="cockpit-utility-control ui-scroll" v-class-toggle="{ 'is-expanded': open.radio }">
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
<span data-i18n>RADIO</span>
<span class="cockpit-utility-divider" aria-hidden="true"></span>
+2 -2
View File
@@ -42,13 +42,13 @@ const geoListed = computed(() => geoRows.value.length > 0);
<template>
<!-- Post-processing toggles (top-right) -->
<div id="pp-toggles" class="panel-collapsible collapsed" data-panel-id="pp-toggles">
<div id="pp-toggles" class="panel-collapsible collapsed ui-scroll" data-panel-id="pp-toggles">
<div class="compact pp-header-row">
<span data-i18n class="pp-header-label">DISPLAY</span>
<span class="panel-divider" aria-hidden="true"></span>
<button class="panel-collapse-btn pp-collapse-btn" data-collapse-target="pp-toggles" title="Collapse panel" data-i18n-title>&#x25C0;</button>
</div>
<div class="pp-panel-body" data-rail-scroller>
<div class="pp-panel-body ui-scroll" data-rail-scroller>
<div class="pp-toggle-group">
<button class="pp-toggle-btn" :class="{ active: hud?.visible }" id="hud-toggle" title="Intelligence HUD (H)" data-i18n-title>
<span class="pp-icon">&#x25CE;</span>
+2 -2
View File
@@ -40,7 +40,7 @@ watch(
</div>
<section class="mission-data-section">
<h4>{{ t('PAYLOAD') }}</h4>
<div class="mission-table-scroll">
<div class="mission-table-scroll ui-scroll">
<table class="mission-data-table">
<thead><tr><th>{{ t('NAME') }}</th><th>{{ t('TYPE') }}</th><th>{{ t('DESTINATION') }}</th></tr></thead>
<tbody data-mission-payloads>
@@ -51,7 +51,7 @@ watch(
</section>
<section class="mission-data-section" data-mission-stages-section :hidden="m && !m.stages.length">
<h4>{{ t('STAGE / RE-ENTRY / RECOVERY') }}</h4>
<div class="mission-table-scroll">
<div class="mission-table-scroll ui-scroll">
<table class="mission-data-table">
<thead><tr><th>{{ t('STAGE') }}</th><th>{{ t('STATUS') }}</th><th>{{ t('FINAL POSITION') }}</th></tr></thead>
<tbody data-mission-stages>
+1 -1
View File
@@ -68,7 +68,7 @@ onUpdated(() => {
</div>
<output data-mission-roster-count>{{ roster ? roster.count : '—' }}</output>
</div>
<div class="space-mission-roster-list" 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>
<div v-else-if="!roster.entries.length" class="space-mission-roster-empty">{{ roster.empty }}</div>
<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>
+1 -1
View File
@@ -21,7 +21,7 @@ const sdrConnected = computed(() => sdr.value?.connectionActive ?? null);
<template>
<section id="radio-panel" class="panel-collapsible collapsed" v-class-toggle="{ ...(radio && { 'radio-enabled': radio.enabled, 'lifecycle-uncertain': radio.uncertain }), ...(sdrConnected !== null && { 'sdr-connected': sdrConnected }) }" data-panel-id="radio-panel" aria-label="Internet radio and local SDR" data-i18n-aria-label>
<div class="radio-panel-inner">
<div class="radio-panel-inner ui-scroll">
<div class="panel-header">
<span class="panel-title" data-i18n>RADIO</span>
<span id="radio-layer-state" class="radio-layer-state" :class="{ active: radio?.layerState.active }">{{ radio ? radio.layerState.text : t('OFF') }}</span>
+2 -2
View File
@@ -102,7 +102,7 @@ const vThumb = { mounted: placeThumb, updated: placeThumb };
<template>
<div id="street-level-panel" class="panel-collapsible collapsed" data-panel-id="street-level-panel" :data-sl-enabled="view ? String(view.enabled) : undefined">
<div class="panel-glow"></div>
<div class="street-level-panel-inner" data-rail-scroller>
<div class="street-level-panel-inner ui-scroll" data-rail-scroller>
<div class="panel-header">
<span class="panel-title" data-i18n>STREET LEVEL</span>
<span class="panel-divider"></span>
@@ -153,7 +153,7 @@ const vThumb = { mounted: placeThumb, updated: placeThumb };
</div>
<div id="sl-since-row" class="sl-since" :hidden="!shows.since">
<label class="cctv-summary-label" for="sl-since" data-i18n>SINCE</label>
<input id="sl-since" v-thumb="view?.since" class="sl-range" type="range" min="0" max="8" step="1" value="0" :aria-valuetext="sinceLabel" aria-describedby="sl-since-label sl-since-note" @input="onSinceInput" @change="onSinceChange" />
<input id="sl-since" v-thumb="view?.since" class="sl-range gev-quantitative-slider" type="range" min="0" max="8" step="1" value="0" :aria-valuetext="sinceLabel" aria-describedby="sl-since-label sl-since-note" @input="onSinceInput" @change="onSinceChange" />
<span class="sl-since-value">
<output id="sl-since-label" class="sl-since-label" for="sl-since">{{ sinceLabel }}</output>
<small id="sl-since-note" class="sl-since-note" :hidden="!view?.sinceNote">{{ view?.sinceNote || '' }}</small>