mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
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:
1 parent
1072df325e
commit
484243fe4b
42 files changed
+133
-152
No files matched your search
@@ -42,6 +42,16 @@ one-panel rule too. Share links continue to record which panels are open.
|
||||
this repository and styled by its existing stylesheet and custom properties.
|
||||
They contain no application state.
|
||||
|
||||
Controls share one skin, so every panel reads the same. Every scrolling
|
||||
surface carries the kit's `.ui-scroll` (ui-vue's name), which draws the thin
|
||||
themed scrollbar from `--scroll-thumb` and `--scroll-track`; the Cyber theme
|
||||
sets those two tokens, and Cockpit's HUD windows keep their accent tint. A
|
||||
surface without it would draw the platform's light scrollbar, because the
|
||||
page declares no dark `color-scheme`. Selects and number fields are
|
||||
`.gev-field`, which draws its own chevron in place of the platform's arrow
|
||||
and keeps native spinners dark; ranges are `.gev-quantitative-slider`, with
|
||||
a feature supplying only its accent.
|
||||
|
||||
## Phases
|
||||
|
||||
1. **Foundation.** Vue, Pinia and the Vite Vue plugin join the browser build
|
||||
|
||||
@@ -95,9 +95,9 @@ test('Context async action buttons remain focused while busy', () => {
|
||||
});
|
||||
|
||||
test('Context header holds only its title and collapse control', () => {
|
||||
const panel = html.match(/<div id="global-context-panel"[\s\S]*?<div class="cyber-panel-body"/);
|
||||
const panel = html.match(/<div id="global-context-panel"[\s\S]*?<div class="cyber-panel-body[ "]/);
|
||||
assert.ok(panel, 'Global Context panel is missing');
|
||||
const header = panel[0].match(/<div class="panel-header">([\s\S]*?)<\/div>\s*<div class="cyber-panel-body"/);
|
||||
const header = panel[0].match(/<div class="panel-header">([\s\S]*?)<\/div>\s*<div class="cyber-panel-body[ "]/);
|
||||
assert.ok(header, 'Global Context header is missing');
|
||||
const tags = [...header[1].matchAll(/<(\w+)\s+class="([^"]+)"/g)].map(([, tag, cls]) => `${tag}.${cls}`);
|
||||
assert.deepEqual(tags, ['span.panel-title', 'span.panel-divider', 'button.panel-collapse-btn']);
|
||||
|
||||
@@ -1054,7 +1054,7 @@ export function createBhoteKoshiEmbeddedMedia({
|
||||
leader.innerHTML =
|
||||
'<path pathLength="1" fill="none" stroke="rgba(147,213,228,.72)" stroke-width="1.5"/><circle r="4" fill="#6be8ff" stroke="#ffffff" stroke-width="1.5"/>';
|
||||
const player = documentRef.createElement('div');
|
||||
player.className = 'bhote-embedded-callout-player';
|
||||
player.className = 'bhote-embedded-callout-player ui-scroll';
|
||||
const footer = documentRef.createElement('footer');
|
||||
const title = documentRef.createElement('strong');
|
||||
title.textContent = bhoteEventText(
|
||||
|
||||
@@ -348,7 +348,7 @@ test('camera updates retain a preload until the visible player loads', () => {
|
||||
assert.equal(frame.parent, root, 'keep the preload while the visible player starts');
|
||||
const card = root.children.find(node => node.tag === 'section');
|
||||
const body = card.children.find(node => node.className === 'bhote-embedded-callout-body');
|
||||
const player = body.children.find(node => node.className === 'bhote-embedded-callout-player');
|
||||
const player = body.children.find(node => node.className.split(' ').includes('bhote-embedded-callout-player'));
|
||||
player.children.find(node => node.tag === 'iframe').dispatchEvent(new Event('load'));
|
||||
assert.equal(frame.parent, null, 'visible player readiness releases the preload');
|
||||
media.destroy();
|
||||
|
||||
@@ -32,7 +32,7 @@ test('Street Level is a collapsible right-rail panel that starts collapsed', ()
|
||||
assert.doesNotMatch(html, /sl-mark|mly-mark/);
|
||||
assert.match(
|
||||
html,
|
||||
/<div class="street-level-panel-inner" data-rail-scroller>/,
|
||||
/<div class="street-level-panel-inner ui-scroll" data-rail-scroller>/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
html,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Vendored
+3
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>◀</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">◎</span>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -41,7 +41,7 @@ export function createVoiceControl({ reset = false, panel = false } = {}) {
|
||||
root.innerHTML = `
|
||||
<label id="gev-voice-provider-field" class="gev-voice-provider-field" hidden>
|
||||
<span>${t('Voice provider')}</span>
|
||||
<select id="gev-voice-provider" aria-label="${t('Voice provider')}" title="${t('Changing provider reconnects voice if it is on.')}"></select>
|
||||
<select id="gev-voice-provider" class="gev-field" aria-label="${t('Voice provider')}" title="${t('Changing provider reconnects voice if it is on.')}"></select>
|
||||
</label>
|
||||
${panel ? `<button id="gev-voice-settings-button" class="gev-voice-settings-button" type="button" aria-label="${t('Voice Settings')}" aria-controls="gev-voice-panel" aria-expanded="false"><span class="material-symbols-outlined gev-voice-settings-icon" aria-hidden="true">settings</span></button>` : ''}
|
||||
<div class="gev-voice-heading">
|
||||
@@ -81,12 +81,12 @@ export function createVoiceControl({ reset = false, panel = false } = {}) {
|
||||
</div>
|
||||
<button id="gev-voice-settings-close" type="button" aria-label="${t('Close voice panel')}">×</button>
|
||||
</div>
|
||||
<div class="gev-voice-panel-body">
|
||||
<div class="gev-voice-panel-body ui-scroll">
|
||||
<section id="gev-voice-preview-panel" role="tabpanel" aria-labelledby="gev-voice-preview-tab">
|
||||
<p id="gev-voice-preview-empty">${t('Voice replies appear here.')}</p>`
|
||||
: ''
|
||||
}
|
||||
<section id="gev-voice-card" class="gev-voice-card" hidden aria-label="${t('Voice assistant')}">
|
||||
<section id="gev-voice-card" class="gev-voice-card ui-scroll" hidden aria-label="${t('Voice assistant')}">
|
||||
<div class="gev-voice-card-header">
|
||||
<span class="gev-voice-card-kicker">${t('VOICE')}</span>
|
||||
<span id="gev-voice-card-phase" class="gev-voice-card-phase"></span>
|
||||
@@ -117,7 +117,7 @@ export function createVoiceControl({ reset = false, panel = false } = {}) {
|
||||
<div class="gev-voice-timeout-group">
|
||||
<label class="gev-voice-preference gev-voice-setting-title" for="gev-voice-timeout-select">${t('Voice inactivity timeout')}</label>
|
||||
<div class="gev-voice-timeout-controls">
|
||||
<select id="gev-voice-timeout-select" aria-describedby="gev-voice-timeout-note gev-voice-timeout-status">
|
||||
<select id="gev-voice-timeout-select" class="gev-field" aria-describedby="gev-voice-timeout-note gev-voice-timeout-status">
|
||||
<option value="3">3 ${t('Minutes')}</option><option value="5" selected>5 ${t('Minutes')}</option><option value="10">10 ${t('Minutes')}</option><option value="15">15 ${t('Minutes')}</option>
|
||||
<option value="custom">${t('Custom')}</option><option value="0">${t('None')}</option>
|
||||
</select>
|
||||
|
||||
Reference in new issue
Block a user