mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor(voice): isolate gemini live provider core
This commit is contained in:
commit
10916227cb
655 files changed
+45946
-26483
No files matched your search
+126
-17
@@ -12,7 +12,9 @@
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 18px 52px rgba(0, 0, 0, 0.48), inset 0 0 28px rgba(14, 78, 92, 0.12);
|
||||
box-shadow:
|
||||
0 18px 52px rgba(0, 0, 0, 0.48),
|
||||
inset 0 0 28px rgba(14, 78, 92, 0.12);
|
||||
backdrop-filter: blur(24px) saturate(1.4);
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: 0;
|
||||
@@ -153,33 +155,69 @@
|
||||
}
|
||||
|
||||
@keyframes bhote-embedded-anchor-enter {
|
||||
from { opacity: 0; transform: scale(0.2); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.2);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bhote-embedded-leader-enter {
|
||||
from { opacity: 0; stroke-dashoffset: 1; }
|
||||
to { opacity: 1; stroke-dashoffset: 0; }
|
||||
from {
|
||||
opacity: 0;
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bhote-embedded-body-enter {
|
||||
from { opacity: 0; transform: translateY(8px) scale(0.98); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bhote-embedded-body-exit {
|
||||
from { opacity: 1; transform: translateY(0) scale(1); }
|
||||
to { opacity: 0; transform: translateY(6px) scale(0.985); }
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(6px) scale(0.985);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bhote-embedded-leader-exit {
|
||||
from { opacity: 1; stroke-dashoffset: 0; }
|
||||
to { opacity: 0; stroke-dashoffset: 1; }
|
||||
from {
|
||||
opacity: 1;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
stroke-dashoffset: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bhote-embedded-anchor-exit {
|
||||
from { opacity: 1; transform: scale(1); }
|
||||
to { opacity: 0; transform: scale(0.2); }
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@@ -267,6 +305,27 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* The title takes the full card width and up to two lines, so a translated
|
||||
title keeps its place name; the link sits beside the status below it. */
|
||||
html:not([lang='en']) .bhote-embedded-callout footer strong {
|
||||
grid-column: 1 / -1;
|
||||
display: -webkit-box;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .bhote-embedded-callout footer small {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .bhote-embedded-callout footer a {
|
||||
grid-row: 2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bhote-embedded-callout footer a:hover,
|
||||
.bhote-embedded-callout footer a:focus-visible {
|
||||
text-decoration: underline;
|
||||
@@ -354,6 +413,12 @@
|
||||
color: rgba(222, 240, 245, 0.88);
|
||||
}
|
||||
|
||||
/* A longer translated heading wraps; the readout beside it stays one piece. */
|
||||
html:not([lang='en']) .bhote-event-section-head span:last-child {
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bhote-event-range {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
@@ -372,6 +437,12 @@
|
||||
color: rgba(174, 208, 216, 0.62);
|
||||
}
|
||||
|
||||
/* Keep a translated date ("16 OCT 2021") together so the year never wraps
|
||||
alone. */
|
||||
html:not([lang='en']) .bhote-event-date {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bhote-event-story-nav {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr) 34px;
|
||||
@@ -388,7 +459,11 @@
|
||||
border-radius: 4px;
|
||||
color: var(--accent);
|
||||
background: rgba(7, 25, 35, 0.9);
|
||||
font: 22px/1 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace;
|
||||
font:
|
||||
22px/1 'JetBrains Mono',
|
||||
'SFMono-Regular',
|
||||
Consolas,
|
||||
monospace;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -428,6 +503,31 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .bhote-event-story-current {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
/* Up to two lines: a translated beat title keeps its subject. Two title lines
|
||||
and one meta line still fit the 54px arrow buttons, so the controls stay put. */
|
||||
html:not([lang='en']) .bhote-event-story-current strong {
|
||||
display: -webkit-box;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
/* Phase / time / sources run long in translation: allow a second line. */
|
||||
html:not([lang='en']) .bhote-event-story-current small {
|
||||
display: -webkit-box;
|
||||
margin-top: 3px;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.bhote-event-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -458,6 +558,15 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* A longer translation wraps to a second line instead of being cut. */
|
||||
html:not([lang='en']) .bhote-event-actions button {
|
||||
padding: 3px 5px;
|
||||
line-height: 1.2;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.bhote-event-actions button:disabled,
|
||||
.bhote-event-story-nav > button:disabled {
|
||||
opacity: 0.3;
|
||||
@@ -469,7 +578,7 @@
|
||||
border-color: rgba(88, 196, 218, 0.36);
|
||||
}
|
||||
|
||||
.bhote-event-actions button[data-action="cinematic"][aria-pressed="true"] {
|
||||
.bhote-event-actions button[data-action='cinematic'][aria-pressed='true'] {
|
||||
color: #071017;
|
||||
border-color: var(--accent);
|
||||
background: var(--accent);
|
||||
@@ -593,7 +702,6 @@
|
||||
max-height: 48vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -602,7 +710,8 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
+52
-4
@@ -70,6 +70,18 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Spanish labels overflow the nowrap .scene-btn row of the fixed-width
|
||||
panel; wrap the row under es only, English stays as is. */
|
||||
html[lang='es'] .cctv-controls {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* A label longer than its button wraps inside it, not past the panel edge. */
|
||||
html:not([lang='en']) .cctv-controls .scene-btn {
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.cctv-calibration-block {
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 9px;
|
||||
@@ -157,6 +169,9 @@
|
||||
padding: 6px 8px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -260,12 +275,12 @@
|
||||
animation: cctv-frame-shimmer 1.4s linear infinite;
|
||||
}
|
||||
|
||||
/* "Acquiring" caption only when no previous frame is showing underneath */
|
||||
.cctv-frame-wrap.loading:not(.has-frame)::before {
|
||||
content: 'ACQUIRING FRAME';
|
||||
/* "Acquiring" caption only when no previous frame is showing underneath.
|
||||
The text lives in the markup (data-i18n) so it is translated. */
|
||||
.cctv-frame-caption {
|
||||
display: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-mono);
|
||||
@@ -275,6 +290,10 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cctv-frame-wrap.loading:not(.has-frame) .cctv-frame-caption {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@keyframes cctv-frame-shimmer {
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
@@ -365,3 +384,32 @@
|
||||
color: #a8f7ff;
|
||||
text-shadow: 0 0 10px rgba(0, 212, 255, 0.18);
|
||||
}
|
||||
|
||||
/* Tighter calibration chips keep the seven values to two rows, so
|
||||
SAVE / RESET stay in view. */
|
||||
html:not([lang='en']) .cctv-cal-readout {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .cctv-cal-value {
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.3px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
html:not([lang='en']) #cctv-camera-select {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* The camera name gets its own row; PREV / NEXT share the row below it. */
|
||||
html:not([lang='en']) .cctv-controls:has(#cctv-camera-select) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
html:not([lang='en']) #cctv-camera-select {
|
||||
flex: 1 1 100%;
|
||||
order: -1;
|
||||
}
|
||||
@@ -575,6 +575,11 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed))
|
||||
text-align: center;
|
||||
vertical-align: -0.08em;
|
||||
}
|
||||
/* Grouping and decimal separators (a space or comma in Spanish and Russian)
|
||||
get a narrow cell; English keeps the full 0.56em cell. */
|
||||
html:not([lang='en']) .cockpit-roll-token.is-separator {
|
||||
min-width: 0.3em;
|
||||
}
|
||||
.cockpit-roll-token.is-rolling {
|
||||
color: color-mix(in srgb, currentColor 92%, var(--cockpit-accent));
|
||||
text-shadow: 0 0 10px
|
||||
@@ -1506,6 +1511,14 @@ body.cockpit-mode #view-switcher button {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
html:not([lang='en']) .cockpit-signal-header > div {
|
||||
min-width: 0;
|
||||
}
|
||||
html:not([lang='en']) .cockpit-signal-kicker {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cockpit-signal-header strong {
|
||||
color: #fff;
|
||||
font-size: 8px;
|
||||
@@ -1532,6 +1545,11 @@ body.cockpit-mode #view-switcher button {
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
/* A longer translated state label widens the cycle button; the other
|
||||
controls keep their size instead of being squeezed. */
|
||||
html:not([lang='en']) .cockpit-brief-actions > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cockpit-brief-actions button {
|
||||
display: grid;
|
||||
width: 23px;
|
||||
@@ -1559,6 +1577,12 @@ body.cockpit-mode #view-switcher button {
|
||||
font: 700 6px/1 var(--font-mono);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
html:not([lang='en']) #cockpit-brief-auto {
|
||||
width: auto;
|
||||
min-width: 50px;
|
||||
padding: 0 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#cockpit-brief-auto[aria-pressed='true'] {
|
||||
border-color: var(--cockpit-accent);
|
||||
background: color-mix(
|
||||
@@ -1871,6 +1895,18 @@ body.cockpit-mode #view-switcher button {
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
}
|
||||
/* Translated tab names are longer: the tabs keep their words whole and the
|
||||
footer text beside them gives way. */
|
||||
html:not([lang='en']) .cockpit-signal-footer > span {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
html:not([lang='en']) .cockpit-brief-tabs {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
html:not([lang='en']) .cockpit-brief-tabs button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cockpit-brief-tabs button {
|
||||
padding: 2px 4px;
|
||||
border: 0;
|
||||
|
||||
@@ -647,6 +647,11 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* The ru/es push-to-talk hint is longer than the tray: wrap it instead of clipping. */
|
||||
html:not([lang='en']) #gev-voice-control .gev-voice-help-detail {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
#gev-voice-control:not([data-status='error']):hover .gev-voice-help-tray,
|
||||
#gev-voice-control:not([data-status='error'])
|
||||
#gev-voice-button:focus-visible
|
||||
|
||||
@@ -392,6 +392,12 @@
|
||||
transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
/* The side wings sit at z-index 100 (controls.css, location.css), so their
|
||||
open trays would cover the alert; lift the voice column while it shows. */
|
||||
#command-dock > #gev-voice-control[data-status='error']:not(.error-dismissed) {
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
#command-dock
|
||||
#gev-voice-control[data-status='error']:not(.error-dismissed)
|
||||
.gev-voice-error-tray {
|
||||
|
||||
@@ -221,6 +221,18 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* A longer translated title in a collapsed rail launcher tightens its
|
||||
tracking rather than pushing the disclosure button out of the card. */
|
||||
html:not([lang='en'])
|
||||
#right-context-rail
|
||||
.panel-collapsible.collapsed
|
||||
.panel-title {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
letter-spacing: 1.5px;
|
||||
}
|
||||
|
||||
.panel-divider {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
@@ -244,6 +256,11 @@
|
||||
color var(--transition-fast);
|
||||
}
|
||||
|
||||
/* A long translated title must not squeeze the toggle into a sliver. */
|
||||
html:not([lang='en']) .panel-collapse-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-collapse-btn:hover {
|
||||
border-color: var(--glass-border-hover);
|
||||
color: var(--text-primary);
|
||||
@@ -770,6 +787,14 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* One label column across the Display rows: --pp-label-chars is the longest
|
||||
label of the active language (set in applicationShell.js); the labels are
|
||||
monospace. 0.125em is the 1px letter-spacing at 8px. */
|
||||
html:not([lang='en']) .pp-slider-row > .pp-slider-mini-label {
|
||||
flex: 0 0 auto;
|
||||
min-width: calc(var(--pp-label-chars, 0) * (1ch + 0.125em));
|
||||
}
|
||||
|
||||
.detection-slider-row {
|
||||
min-width: 170px;
|
||||
padding: 5px 8px 6px;
|
||||
@@ -966,6 +991,11 @@
|
||||
#draw-mode-row {
|
||||
min-width: 184px;
|
||||
}
|
||||
/* Equal thirds with room around the label, so a long translated shape name
|
||||
never runs edge to edge in its segment. */
|
||||
html:not([lang='en']) #draw-mode-row .pp-mode-btn {
|
||||
padding: 0 4px;
|
||||
}
|
||||
#draw-label-row {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
@@ -990,6 +1020,11 @@
|
||||
.draw-color-select {
|
||||
flex: 0 0 74px;
|
||||
}
|
||||
/* Sized to its longest colour name so a translated one is never clipped. */
|
||||
html:not([lang='en']) .draw-color-select {
|
||||
flex: 0 0 auto;
|
||||
min-width: 74px;
|
||||
}
|
||||
.draw-clear-btn {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 8px;
|
||||
@@ -1319,6 +1354,22 @@ body.ui-clean-view #scene-runtime.active {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* One label column for every parameter, as wide as the longest translated
|
||||
label, so all slider tracks start and end at the same x. */
|
||||
html:not([lang='en']) #param-sliders {
|
||||
display: grid;
|
||||
grid-template-columns: max-content minmax(0, 1fr) max-content;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .param-slider-row {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
|
||||
@@ -174,6 +174,8 @@
|
||||
.context-mode-button,
|
||||
.map-stack-chip,
|
||||
.key-setup-chip,
|
||||
.locale-picker-button,
|
||||
.locale-picker-popup,
|
||||
.first-run-card
|
||||
) {
|
||||
border-color: var(--glass-border);
|
||||
@@ -1834,9 +1836,11 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Sits above the language picker row in the bottom-right corner
|
||||
(locale-picker.css: 0.9rem inset + one ~2rem button + a gap). */
|
||||
#intel-hud[data-variant='cyber'] .hud-bottom-right {
|
||||
right: 28px;
|
||||
bottom: 28px;
|
||||
bottom: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2733,6 +2737,9 @@
|
||||
|
||||
:root[data-ui-theme='cyber'] body.cockpit-mode .cockpit-position-readout {
|
||||
right: 30px;
|
||||
/* Clears the language picker row it shares the corner with, as in the
|
||||
other themes (locale-picker.css). */
|
||||
bottom: 3.5rem;
|
||||
}
|
||||
|
||||
:root[data-ui-theme='cyber'] body.cockpit-mode .cockpit-help {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
body.ui-embed #clean-view-exit,
|
||||
body.ui-embed #intel-hud,
|
||||
body.ui-embed #key-setup-chip,
|
||||
body.ui-embed #corner-actions,
|
||||
body.ui-embed #key-setup,
|
||||
body.ui-embed #first-run-launcher,
|
||||
body.ui-embed #command-dock,
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
// Layout rules written for long translations must not change the English
|
||||
// rendering: every rule checked here has no `html:not([lang='en'])` guard,
|
||||
// so it applies to English too.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const css = (name) => readFileSync(join(here, name), 'utf8');
|
||||
|
||||
/** Declarations of every rule whose (unguarded) selector list is `selector`. */
|
||||
function unguardedRules(source, selector) {
|
||||
const norm = (s) => s.replace(/\s+/g, ' ').trim();
|
||||
const want = norm(selector);
|
||||
const out = [];
|
||||
const re = /([^{}]+)\{([^{}]*)\}/g;
|
||||
let m;
|
||||
while ((m = re.exec(source))) {
|
||||
// The captured "selector" may carry the tail of a preceding block
|
||||
// (comments, an @media prelude); keep what follows the last `}` or `*/`.
|
||||
const raw = m[1].split(/\*\/|\}/).pop();
|
||||
const sel = norm(raw);
|
||||
if (sel === want && !sel.includes("html:not([lang='en'])")) out.push(m[2]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function declaration(block, prop) {
|
||||
const m = block.match(new RegExp(`(?:^|;)\\s*${prop}\\s*:\\s*([^;]+)`));
|
||||
return m ? m[1].replace(/\s+/g, ' ').trim() : undefined;
|
||||
}
|
||||
|
||||
test('layer status chip keeps its English width (82px)', () => {
|
||||
const rules = unguardedRules(
|
||||
css('radio.css'),
|
||||
`.data-toggle-btn.feed-loading, .data-toggle-btn.feed-degraded,
|
||||
.data-toggle-btn.feed-stale, .data-toggle-btn.feed-partial,
|
||||
.data-toggle-btn.feed-fallback, .data-toggle-btn.feed-unavailable`,
|
||||
);
|
||||
assert.equal(rules.length, 1);
|
||||
assert.equal(declaration(rules[0], 'min-width'), '82px');
|
||||
});
|
||||
|
||||
test('Directions steps keep one line with an ellipsis in English', () => {
|
||||
const [rule] = unguardedRules(css('radio.css'), '.data-row-list-text');
|
||||
assert.ok(rule);
|
||||
assert.equal(declaration(rule, 'white-space'), 'nowrap');
|
||||
assert.equal(declaration(rule, 'text-overflow'), 'ellipsis');
|
||||
});
|
||||
|
||||
test('count-line layout is not reachable from an unguarded selector', () => {
|
||||
const source = css('radio.css');
|
||||
assert.equal(
|
||||
unguardedRules(source, '.data-toggle-row--count-line .data-count').length,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('weather status line keeps its fixed English height', () => {
|
||||
const [rule] = unguardedRules(
|
||||
css('weather.css'),
|
||||
".weather-card [data-line-id='status']",
|
||||
);
|
||||
assert.ok(rule);
|
||||
assert.equal(declaration(rule, 'height'), '1.45em');
|
||||
});
|
||||
|
||||
test('cockpit number separators keep the English cell width', () => {
|
||||
assert.equal(
|
||||
unguardedRules(css('cockpit.css'), '.cockpit-roll-token.is-separator')
|
||||
.length,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('Bhote Koshi callout keeps its English footer layout', () => {
|
||||
const source = css('bhote-koshi.css');
|
||||
const [strong] = unguardedRules(
|
||||
source,
|
||||
'.bhote-embedded-callout footer strong',
|
||||
);
|
||||
assert.equal(declaration(strong, 'white-space'), 'nowrap');
|
||||
const [link] = unguardedRules(source, '.bhote-embedded-callout footer a');
|
||||
assert.equal(declaration(link, 'grid-row'), '1 / span 2');
|
||||
});
|
||||
|
||||
test('SCENES actions stay on one line in English', () => {
|
||||
assert.equal(
|
||||
unguardedRules(css('scenes.css'), '#scene-panel .scene-controls .scene-btn')
|
||||
.length,
|
||||
0,
|
||||
);
|
||||
const [select] = unguardedRules(css('scenes.css'), '#scene-select');
|
||||
assert.equal(declaration(select, 'white-space'), undefined);
|
||||
});
|
||||
|
||||
test('Display and parameter rows keep their English layout', () => {
|
||||
const source = css('controls.css');
|
||||
assert.equal(
|
||||
unguardedRules(source, '.pp-slider-row > .pp-slider-mini-label').length,
|
||||
0,
|
||||
);
|
||||
assert.equal(unguardedRules(source, '#param-sliders').length, 0);
|
||||
const [row] = unguardedRules(source, '.param-slider-row');
|
||||
assert.equal(declaration(row, 'display'), 'flex');
|
||||
const [color] = unguardedRules(source, '.draw-color-select');
|
||||
assert.equal(declaration(color, 'flex'), '0 0 74px');
|
||||
});
|
||||
|
||||
test('weather setting rows keep the 56px English label column', () => {
|
||||
const source = css('weather.css');
|
||||
assert.equal(unguardedRules(source, '.rail-card-settings').length, 0);
|
||||
const [row] = unguardedRules(source, '.rail-card-setting');
|
||||
assert.equal(
|
||||
declaration(row, 'grid-template-columns'),
|
||||
'56px minmax(0, 1fr)',
|
||||
);
|
||||
});
|
||||
|
||||
test('cockpit brief controls keep their English sizes', () => {
|
||||
const source = css('cockpit.css');
|
||||
const [auto] = unguardedRules(source, '#cockpit-brief-auto');
|
||||
assert.equal(declaration(auto, 'width'), '50px');
|
||||
assert.equal(unguardedRules(source, '.cockpit-brief-actions > *').length, 0);
|
||||
});
|
||||
|
||||
test('the count-line class is set only outside English', async () => {
|
||||
const { setLocale } = await import('../../i18n/index.js');
|
||||
const { LayerPanel } = await import('../layerPanel.js');
|
||||
const classes = () => {
|
||||
const toggled = new Map();
|
||||
const node = () => ({
|
||||
textContent: '',
|
||||
classList: { toggle: (name, on) => toggled.set(name, on) },
|
||||
appendChild() {},
|
||||
});
|
||||
LayerPanel.prototype._syncCount.call(LayerPanel.prototype, node(), node(), {
|
||||
countLabel: '693 nearby · 93 on screen',
|
||||
});
|
||||
return toggled.get('data-toggle-row--count-line');
|
||||
};
|
||||
globalThis.document ??= { createElement: () => ({}) };
|
||||
try {
|
||||
setLocale('en');
|
||||
assert.equal(classes(), false);
|
||||
setLocale('es');
|
||||
assert.equal(classes(), true);
|
||||
} finally {
|
||||
setLocale('en');
|
||||
}
|
||||
});
|
||||
|
||||
test('Recent Imagery readout keeps its English layout', () => {
|
||||
const source = css('recent-imagery.css');
|
||||
const one = (selector) => {
|
||||
const rules = unguardedRules(source, selector);
|
||||
assert.equal(rules.length, 1, selector);
|
||||
return rules[0];
|
||||
};
|
||||
// The SHOW chip shares its row with the reserved B half.
|
||||
assert.equal(
|
||||
unguardedRules(
|
||||
source,
|
||||
".recent-imagery-readout:not([data-mode='ab']) .ri-card-chips .ri-chip[data-slot='a']",
|
||||
).length,
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
unguardedRules(
|
||||
source,
|
||||
".recent-imagery-readout:not([data-mode='ab']) .ri-card-chips .ri-chip[data-slot='b']",
|
||||
).length,
|
||||
0,
|
||||
);
|
||||
// Slot rows: their own fixed 44px tag column, no subgrid.
|
||||
const selection = one('.recent-imagery-readout .ri-selection');
|
||||
assert.equal(declaration(selection, 'grid-template-columns'), undefined);
|
||||
assert.equal(declaration(selection, 'column-gap'), undefined);
|
||||
assert.equal(
|
||||
declaration(one('.recent-imagery-readout .ri-mode'), 'grid-column'),
|
||||
undefined,
|
||||
);
|
||||
const slot = one('.recent-imagery-readout .ri-slot');
|
||||
assert.equal(
|
||||
declaration(slot, 'grid-template-columns'),
|
||||
'44px minmax(0, 1fr) 22px',
|
||||
);
|
||||
assert.equal(declaration(slot, 'grid-column'), undefined);
|
||||
assert.equal(declaration(slot, 'height'), '20px');
|
||||
assert.equal(
|
||||
declaration(one('.recent-imagery-readout .ri-slot-tag'), 'padding'),
|
||||
undefined,
|
||||
);
|
||||
// Opacity / SWAP / export row: fixed English columns.
|
||||
assert.equal(
|
||||
declaration(
|
||||
one('.recent-imagery-readout .ri-controls'),
|
||||
'grid-template-columns',
|
||||
),
|
||||
'76px minmax(0, 1fr) 40px 128px',
|
||||
);
|
||||
const label = one('.recent-imagery-readout .ri-opacity-label');
|
||||
assert.equal(declaration(label, 'flex-wrap'), undefined);
|
||||
assert.equal(declaration(label, 'gap'), '4px');
|
||||
assert.equal(declaration(label, 'line-height'), undefined);
|
||||
const swap = one('.recent-imagery-readout .ri-swap');
|
||||
assert.equal(declaration(swap, 'width'), '40px');
|
||||
assert.equal(declaration(swap, 'min-width'), undefined);
|
||||
});
|
||||
|
||||
test('CCTV control buttons stay on one line in English', () => {
|
||||
assert.equal(
|
||||
unguardedRules(css('cctv.css'), '.cctv-controls .scene-btn').length,
|
||||
0,
|
||||
);
|
||||
});
|
||||
@@ -1371,10 +1371,12 @@
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
/* ALPR keeps its nearby count in the legend while a wider status chip is shown. */
|
||||
/* ALPR keeps its nearby count in the legend while a wider status chip
|
||||
(including ENABLING / DISABLING) is shown. */
|
||||
.data-toggle-row[data-layer-id='alpr-cameras']
|
||||
.data-toggle-right:has(
|
||||
.data-toggle-btn:is(
|
||||
.transitioning,
|
||||
.feed-loading,
|
||||
.feed-degraded,
|
||||
.feed-stale,
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
/* ============================================================
|
||||
Bottom-right corner row: the POWER UP chip (provider-settings.css)
|
||||
and, in the very corner, the interface language picker
|
||||
(src/ui/localePicker.js). Logical properties throughout, so a
|
||||
right-to-left page mirrors the row and the list.
|
||||
============================================================ */
|
||||
|
||||
#corner-actions {
|
||||
position: fixed;
|
||||
inset-inline-end: 0.9rem;
|
||||
inset-block-end: 0.9rem;
|
||||
/* Above the dock (145), the cockpit HUD and panel stacks (146–147);
|
||||
below the first-run launcher (175) and Provider Settings (190). */
|
||||
z-index: 150;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0.4rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#corner-actions > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.locale-picker {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Same HUD frame as #key-setup-chip, so the two read as one group. */
|
||||
.locale-picker-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
margin: 0;
|
||||
padding: 0.45rem 0.6rem;
|
||||
color: rgba(225, 244, 248, 0.92);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.6rem;
|
||||
letter-spacing: 0.09rem;
|
||||
line-height: 1;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
rgba(10, 27, 37, 0.92),
|
||||
rgba(7, 13, 22, 0.9)
|
||||
);
|
||||
border: 1px solid rgba(54, 220, 255, 0.38);
|
||||
border-radius: 0.42rem;
|
||||
box-shadow:
|
||||
0 0.4rem 1.6rem rgba(0, 0, 0, 0.5),
|
||||
0 0 1.2rem rgba(0, 212, 255, 0.07);
|
||||
backdrop-filter: blur(18px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(1.2);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
background var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.locale-picker-button:hover,
|
||||
.locale-picker-button:focus-visible,
|
||||
.locale-picker.is-open .locale-picker-button {
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
rgba(14, 38, 51, 0.95),
|
||||
rgba(9, 18, 28, 0.93)
|
||||
);
|
||||
border-color: rgba(54, 220, 255, 0.7);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.locale-picker-button:hover,
|
||||
.locale-picker-button:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.locale-picker-globe {
|
||||
flex: none;
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
color: #55dff5;
|
||||
}
|
||||
|
||||
.locale-picker-caret {
|
||||
flex: none;
|
||||
width: 0.62rem;
|
||||
height: 0.62rem;
|
||||
margin-inline-start: -0.1rem;
|
||||
opacity: 0.75;
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.locale-picker.is-open .locale-picker-caret {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Opens upward, aligned to the corner edge. */
|
||||
.locale-picker-popup {
|
||||
position: absolute;
|
||||
inset-block-end: calc(100% + 0.4rem);
|
||||
inset-inline-end: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
width: max-content;
|
||||
min-width: 14rem;
|
||||
max-width: calc(100vw - 1.8rem);
|
||||
max-height: min(26rem, calc(100vh - 4.5rem));
|
||||
max-height: min(26rem, calc(100dvh - 4.5rem));
|
||||
padding: 0.45rem;
|
||||
color: var(--text-primary);
|
||||
background:
|
||||
linear-gradient(145deg, rgba(10, 27, 37, 0.97), rgba(7, 13, 22, 0.95)),
|
||||
rgba(7, 17, 25, 0.96);
|
||||
border: 1px solid rgba(54, 220, 255, 0.42);
|
||||
border-radius: 0.55rem;
|
||||
box-shadow:
|
||||
0 1rem 3rem rgba(0, 0, 0, 0.65),
|
||||
0 0 2rem rgba(0, 212, 255, 0.08),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.025);
|
||||
backdrop-filter: blur(24px) saturate(1.25);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.25);
|
||||
}
|
||||
|
||||
.locale-picker-popup[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.locale-picker-heading {
|
||||
padding: 0.2rem 0.4rem 0.45rem;
|
||||
color: rgba(85, 223, 245, 0.85);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.55rem;
|
||||
letter-spacing: 0.14rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.locale-picker-search {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin: 0 0 0.4rem;
|
||||
padding: 0.45rem 0.55rem;
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.7rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(54, 220, 255, 0.28);
|
||||
border-radius: 0.35rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.locale-picker-search:focus {
|
||||
border-color: rgba(54, 220, 255, 0.7);
|
||||
}
|
||||
|
||||
.locale-picker-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
outline: none;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.locale-picker-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.45rem 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.35rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.locale-picker-option.is-active {
|
||||
background: rgba(54, 220, 255, 0.09);
|
||||
border-color: rgba(54, 220, 255, 0.28);
|
||||
}
|
||||
|
||||
/* Keyboard focus lives on the list (aria-activedescendant): show it. */
|
||||
.locale-picker-list:focus-visible .locale-picker-option.is-active,
|
||||
.locale-picker-search:focus
|
||||
~ .locale-picker-list
|
||||
.locale-picker-option.is-active {
|
||||
border-color: rgba(54, 220, 255, 0.75);
|
||||
}
|
||||
|
||||
.locale-picker-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.locale-picker-native {
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.locale-picker-option[aria-selected='true'] .locale-picker-native {
|
||||
color: #55dff5;
|
||||
}
|
||||
|
||||
.locale-picker-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
color: rgba(199, 225, 231, 0.6);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.55rem;
|
||||
letter-spacing: 0.05rem;
|
||||
}
|
||||
|
||||
.locale-picker-check {
|
||||
flex: none;
|
||||
width: 0.95rem;
|
||||
height: 0.95rem;
|
||||
margin-inline-start: auto;
|
||||
color: #55dff5;
|
||||
}
|
||||
|
||||
.locale-picker-empty {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
color: rgba(199, 225, 231, 0.62);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.locale-picker-empty[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The surfaces that hide the rest of the chrome hide the corner too. The
|
||||
cockpit keeps the language picker (provider-settings.css hides the chip). */
|
||||
body.ui-clean-view #corner-actions,
|
||||
body.recording-mode #corner-actions,
|
||||
body.scene-playback-mode #corner-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* The cockpit's position readout shares this corner: it steps up to clear
|
||||
the picker row (0.9rem inset + one ~2rem button + a gap). */
|
||||
body.cockpit-mode .cockpit-position-readout {
|
||||
bottom: max(clamp(22px, 4vh, 42px), 3.5rem);
|
||||
}
|
||||
|
||||
/* Below this width the full chip label runs into the centred dock: the chip
|
||||
keeps its bolt, and its count stays in its title and aria-label. */
|
||||
@media (max-width: 1180px) {
|
||||
#corner-actions #key-setup-chip [data-key-setup-chip-label] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
/* Phone: the corner beside the dock, clear of the attribution above it.
|
||||
There is room for one button across, so the chip stacks on the globe. */
|
||||
#corner-actions {
|
||||
inset-inline-end: 10px;
|
||||
inset-block-end: max(16px, env(safe-area-inset-bottom));
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#corner-actions #key-setup-chip {
|
||||
justify-content: center;
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
/* Phone: the globe alone; the label stays in aria-label. */
|
||||
.locale-picker-code,
|
||||
.locale-picker-caret {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.locale-picker-button {
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
/* The list becomes a bottom sheet across the screen. */
|
||||
.locale-picker-popup {
|
||||
position: fixed;
|
||||
inset-inline: 16px;
|
||||
inset-block-end: 16px;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
max-height: calc(100vh - 32px);
|
||||
max-height: calc(100dvh - 32px);
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
.locale-picker-option {
|
||||
padding-block: 0.7rem;
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,9 @@
|
||||
mono kickers. The chip retires once every key is configured.
|
||||
============================================================ */
|
||||
|
||||
/* The chip sits in the bottom-right corner row (#corner-actions,
|
||||
locale-picker.css), which owns the position. */
|
||||
#key-setup-chip {
|
||||
position: fixed;
|
||||
right: 0.9rem;
|
||||
bottom: 0.9rem;
|
||||
z-index: 60;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
@@ -389,13 +387,6 @@ body.scene-playback-mode #key-setup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
/* Clear the dock. Desktop-first app; this is a courtesy, not a layout. */
|
||||
#key-setup-chip {
|
||||
bottom: 5.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Provider Settings additions: externally-managed badge + per-row remove. */
|
||||
.key-setup-external {
|
||||
padding: 0.05rem 0.3rem;
|
||||
|
||||
@@ -260,6 +260,13 @@
|
||||
letter-spacing: 0.06em;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* A longer translated action stays on one line, cut with an ellipsis. */
|
||||
html:not([lang='en']) .radio-transport button {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.radio-transport button:hover:not(:disabled),
|
||||
.radio-transport button:focus-visible:not(:disabled) {
|
||||
border-color: #65f7ff;
|
||||
@@ -644,6 +651,89 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Longer translations below; English keeps the layout above. */
|
||||
|
||||
/* The name takes the room the status column leaves; a long word wraps inside
|
||||
it instead of pushing the toggle out of line. */
|
||||
html:not([lang='en']) .data-toggle-left {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* A fixed icon box, so every name (and the meta line under it, 26px in)
|
||||
starts at the same x whatever the glyph's width. */
|
||||
html:not([lang='en']) .data-toggle-left .data-icon {
|
||||
flex: none;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Translated layer names are longer: break between words, and hyphenate a
|
||||
word that still does not fit instead of splitting it anywhere. */
|
||||
html:not([lang='en']) .data-name {
|
||||
min-width: 0;
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
/* No two-letter pieces such as "es-paciales": a word hyphenates only when
|
||||
at least four letters stay on each side. */
|
||||
hyphenate-limit-chars: 8 4 4;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .data-count {
|
||||
min-width: auto;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/* Row text pieces that must not break inside: a hyphenated code (ADS-B) and
|
||||
the space before a " · " separator (see setRowText in layerPanel.js). */
|
||||
html:not([lang='en']) .data-keep {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* A count phrase ("24 nearby · 2 on screen") takes its own line under the
|
||||
name instead of squeezing it beside the toggle. layerPanel.js sets the
|
||||
class only outside English. */
|
||||
html:not([lang='en']) .data-toggle-row--count-line .data-toggle-top {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 2px;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .data-toggle-row--count-line .data-toggle-right {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* A zero basis, so a long name wraps beside the toggle instead of pushing the
|
||||
toggle onto the count's line. */
|
||||
html:not([lang='en']) .data-toggle-row--count-line .data-toggle-left {
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .data-toggle-row--count-line .data-count {
|
||||
order: 1;
|
||||
flex: 1 0 100%;
|
||||
padding-left: 26px;
|
||||
font-size: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* The "—" placeholder gives way to a wide status chip, so the name keeps the
|
||||
room it needs. */
|
||||
html:not([lang='en'])
|
||||
.data-toggle-right:has(
|
||||
.data-toggle-btn:is(
|
||||
.transitioning,
|
||||
.feed-loading,
|
||||
.feed-degraded,
|
||||
.feed-stale,
|
||||
.feed-partial,
|
||||
.feed-fallback,
|
||||
.feed-unavailable
|
||||
)
|
||||
)
|
||||
.data-count-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.data-toggle-btn {
|
||||
padding: 3px 10px;
|
||||
font-family: var(--font-mono);
|
||||
@@ -666,6 +756,11 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .data-toggle-btn {
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* An inset ring stays visible inside the scrolling layer list. */
|
||||
.data-toggle-btn:focus-visible {
|
||||
outline: 2px solid var(--text-primary);
|
||||
@@ -682,6 +777,20 @@
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
|
||||
/* Status chips up to eight characters share one width; a longer word grows
|
||||
its own chip rather than widening every chip and squeezing every name. */
|
||||
html:not([lang='en'])
|
||||
.data-toggle-btn:is(
|
||||
.feed-loading,
|
||||
.feed-degraded,
|
||||
.feed-stale,
|
||||
.feed-partial,
|
||||
.feed-fallback,
|
||||
.feed-unavailable
|
||||
) {
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.data-toggle-btn:hover {
|
||||
border-color: var(--glass-border-hover);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
@@ -893,6 +1002,20 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* A translated instruction wraps under itself rather than losing its tail:
|
||||
"Gira a la derecha hacia …" says nothing. */
|
||||
html:not([lang='en']) .data-row-list-lead {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .data-row-list-text {
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* A list line that states a limit rather than offering an action. */
|
||||
.data-row-list-item.note {
|
||||
cursor: default;
|
||||
|
||||
@@ -293,6 +293,21 @@
|
||||
height: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
/* Outside A / B there is one chip (SHOW): in a translation it takes the
|
||||
whole row, so a long label is not cut to a stub. English keeps the
|
||||
reserved B half. */
|
||||
html:not([lang='en'])
|
||||
.recent-imagery-readout:not([data-mode='ab'])
|
||||
.ri-card-chips
|
||||
.ri-chip[data-slot='a'] {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
html:not([lang='en'])
|
||||
.recent-imagery-readout:not([data-mode='ab'])
|
||||
.ri-card-chips
|
||||
.ri-chip[data-slot='b'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 4. The selection bar: mode, then two rows that are always there. */
|
||||
.recent-imagery-readout .ri-selection {
|
||||
@@ -347,6 +362,28 @@
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
/* The slot rows share the bar's columns (subgrid), so the tag column fits
|
||||
the longest tag and both rows align; a long slot line (date · source ·
|
||||
resolution · state) takes a second line instead of losing the state. */
|
||||
html:not([lang='en']) .recent-imagery-readout .ri-selection {
|
||||
grid-template-columns: minmax(44px, max-content) minmax(0, 1fr) 22px;
|
||||
column-gap: 6px;
|
||||
}
|
||||
html:not([lang='en']) .recent-imagery-readout .ri-mode {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
html:not([lang='en']) .recent-imagery-readout .ri-slot {
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
height: auto;
|
||||
min-height: 20px;
|
||||
}
|
||||
html:not([lang='en']) .recent-imagery-readout .ri-slot-tag {
|
||||
padding: 0 3px;
|
||||
}
|
||||
html:not([lang='en']) .recent-imagery-readout .ri-slot-value {
|
||||
white-space: normal;
|
||||
}
|
||||
.recent-imagery-readout .ri-unpin {
|
||||
width: 22px;
|
||||
padding: 0;
|
||||
@@ -371,6 +408,19 @@
|
||||
letter-spacing: 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* In a translation a label longer than its 76px column widens it to the
|
||||
label's longest word, and the value wraps under the label. */
|
||||
html:not([lang='en']) .recent-imagery-readout .ri-controls {
|
||||
grid-template-columns:
|
||||
minmax(76px, min-content) minmax(0, 1fr) minmax(40px, max-content)
|
||||
128px;
|
||||
}
|
||||
html:not([lang='en']) .recent-imagery-readout .ri-opacity-label {
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
row-gap: 0;
|
||||
line-height: 10px;
|
||||
}
|
||||
.recent-imagery-readout .ri-opacity-value {
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text-secondary);
|
||||
@@ -388,6 +438,10 @@
|
||||
width: 40px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
html:not([lang='en']) .recent-imagery-readout .ri-swap {
|
||||
width: auto;
|
||||
min-width: 40px;
|
||||
}
|
||||
.recent-imagery-readout .ri-exports .rail-card-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -163,6 +163,26 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* A translated scene name needs the width: the select takes its own row,
|
||||
NEW / DEL share the next one. */
|
||||
html:not([lang='en']) #scene-select {
|
||||
flex: 1 1 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* A translated action longer than its button wraps inside the button instead
|
||||
of running past it. English keeps its one-line buttons ("EXPORT PRESETS"
|
||||
included). */
|
||||
html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.scene-btn {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
|
||||
@@ -123,6 +123,11 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Translated retry details ("cámaras ALPR · reintento en 53 s") run longer
|
||||
than the English one; English keeps its 220px. */
|
||||
html:not([lang='en']) #global-loading-detail {
|
||||
max-width: min(300px, 46vw);
|
||||
}
|
||||
#global-loading-status:is(
|
||||
[data-state='complete'],
|
||||
[data-state='cancelled'],
|
||||
@@ -384,17 +389,10 @@
|
||||
|
||||
/* ── Toast Notification ──────────────────────── */
|
||||
#toast {
|
||||
--toast-safe-top: 74px;
|
||||
position: fixed;
|
||||
bottom: 100px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-inline: auto;
|
||||
width: max-content;
|
||||
max-width: calc(100vw - 32px);
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: anywhere;
|
||||
transform: translateY(20px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
z-index: 200;
|
||||
padding: 10px 24px;
|
||||
background: var(--glass-bg);
|
||||
@@ -420,41 +418,5 @@
|
||||
#toast.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Keep transient feedback clear of the raised voice chooser and bottom dock. */
|
||||
body:has(#gev-voice-control:hover) #toast,
|
||||
body:has(#gev-voice-control:focus-within) #toast,
|
||||
body:has(#command-dock.dock-has-pinned-tray) #toast {
|
||||
top: calc(env(safe-area-inset-top, 0px) + var(--toast-safe-top));
|
||||
bottom: auto;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
body:has(#gev-voice-control:hover) #toast.visible,
|
||||
body:has(#gev-voice-control:focus-within) #toast.visible,
|
||||
body:has(#command-dock.dock-has-pinned-tray) #toast.visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (max-width: 900px), (max-height: 640px) {
|
||||
#toast {
|
||||
top: calc(env(safe-area-inset-top, 0px) + var(--toast-safe-top));
|
||||
bottom: auto;
|
||||
letter-spacing: 1px;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
#toast.visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* On a narrow portrait viewport the collapsed layer and scene headers occupy
|
||||
the first two rows. Keep feedback below them, clear of the voice controls. */
|
||||
@media (max-width: 600px) and (orientation: portrait) {
|
||||
#toast {
|
||||
--toast-safe-top: 214px;
|
||||
}
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
@@ -216,6 +216,19 @@ button.sl-status:focus-visible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* When the hint and the button do not both fit, the button takes its own line. */
|
||||
html:not([lang='en']) .sl-meta-row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .sl-meta-row .sl-meta {
|
||||
flex: 1 1 14em;
|
||||
}
|
||||
|
||||
html:not([lang='en']) .sl-meta-row .sl-nearest-btn {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
/* ── Errors ──────────────────────────────────────────────────────────────── */
|
||||
.sl-error {
|
||||
font-family: var(--font-sans);
|
||||
|
||||
@@ -231,6 +231,7 @@
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.gev-voice-card-chips {
|
||||
@@ -271,6 +272,8 @@
|
||||
font-size: 0.66rem;
|
||||
line-height: 1.4;
|
||||
color: rgba(223, 245, 250, 0.82);
|
||||
/* A whole error line or a long headline wraps inside the card. */
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.gev-voice-card-referents {
|
||||
|
||||
@@ -83,7 +83,65 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#command-dock #gev-voice-control[data-provider] {
|
||||
/* The heading row is only as wide as the voice column: the cost cluster
|
||||
keeps its size and a long state (ПОДКЛЮЧЕНИЕ, CONECTANDO) ends in an
|
||||
ellipsis. Dropping the kicker returns its gap to the state label; the
|
||||
negative margin reaches over the gutter beside the mic ring, which has
|
||||
curved away at heading height. */
|
||||
html:not([lang='en']) #command-dock .gev-voice-heading {
|
||||
gap: 0.2rem;
|
||||
margin-left: -0.6rem;
|
||||
}
|
||||
|
||||
html:not([lang='en']) #command-dock .gev-voice-heading > .gev-voice-kicker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html:not([lang='en']) #command-dock .gev-voice-heading > #gev-voice-status {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
letter-spacing: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
html:not([lang='en']) #command-dock .gev-voice-cost {
|
||||
gap: 0.16rem;
|
||||
padding-left: 0.1rem;
|
||||
}
|
||||
|
||||
html:not([lang='en']) #command-dock .gev-voice-tier-btn {
|
||||
min-width: 0;
|
||||
padding: 0 0.2rem;
|
||||
}
|
||||
|
||||
html:not([lang='en']) #command-dock .gev-voice-cost-value {
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* Live hints and the tier switch ("СТАНДАРТ — со следующей сессии") run
|
||||
longer: a second line from the spare space under the readout, with no
|
||||
tracking so the tier line does not need a third. */
|
||||
html:not([lang='en'])
|
||||
#command-dock
|
||||
#gev-voice-control:is(
|
||||
[data-status='connecting'],
|
||||
[data-status='listening'],
|
||||
[data-status='executing']
|
||||
)
|
||||
#gev-voice-detail {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
line-height: 1.15;
|
||||
letter-spacing: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#gev-voice-control[data-provider-choice='true'] {
|
||||
--gev-provider-lift: 0px;
|
||||
}
|
||||
|
||||
@@ -125,12 +183,17 @@
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
#command-dock #gev-voice-control[data-provider] .gev-voice-help-tray,
|
||||
#command-dock #gev-voice-control[data-provider] .gev-voice-card,
|
||||
#command-dock #gev-voice-control[data-provider] .gev-voice-error-tray {
|
||||
#command-dock
|
||||
#gev-voice-control[data-provider-choice='true']
|
||||
.gev-voice-help-tray,
|
||||
#command-dock #gev-voice-control[data-provider-choice='true'] .gev-voice-card,
|
||||
#command-dock
|
||||
#gev-voice-control[data-provider-choice='true']
|
||||
.gev-voice-error-tray {
|
||||
bottom: calc(100% + 4.8rem + var(--gev-provider-lift));
|
||||
}
|
||||
#command-dock.dock-has-pinned-tray #gev-voice-control[data-provider] {
|
||||
#command-dock.dock-has-pinned-tray
|
||||
#gev-voice-control[data-provider-choice='true'] {
|
||||
--gev-provider-lift: calc(1.7rem + var(--dock-pinned-stack-height));
|
||||
}
|
||||
/* Pinned trays separate the chooser from its hover owner. Keep it reachable
|
||||
@@ -146,18 +209,20 @@
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
#command-dock #gev-voice-control[data-provider] {
|
||||
#command-dock #gev-voice-control[data-provider-choice='true'] {
|
||||
--gev-provider-lift: 3rem;
|
||||
}
|
||||
#command-dock.dock-has-pinned-tray #gev-voice-control[data-provider] {
|
||||
#command-dock.dock-has-pinned-tray
|
||||
#gev-voice-control[data-provider-choice='true'] {
|
||||
--gev-provider-lift: calc(3.5rem + var(--dock-pinned-stack-height));
|
||||
}
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
#command-dock #gev-voice-control[data-provider] {
|
||||
#command-dock #gev-voice-control[data-provider-choice='true'] {
|
||||
--gev-provider-lift: calc(2vh + 2.5rem);
|
||||
}
|
||||
#command-dock.dock-has-pinned-tray #gev-voice-control[data-provider] {
|
||||
#command-dock.dock-has-pinned-tray
|
||||
#gev-voice-control[data-provider-choice='true'] {
|
||||
--gev-provider-lift: calc(2vh + 3rem + var(--dock-pinned-stack-height));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,13 @@
|
||||
overflow-y: auto;
|
||||
color: var(--accent);
|
||||
}
|
||||
/* One line stays reserved so the card does not jump when a status appears;
|
||||
a translated status runs longer and wraps instead of being cut. */
|
||||
html:not([lang='en']) .weather-card [data-line-id='status'] {
|
||||
height: auto;
|
||||
min-height: 1.45em;
|
||||
overflow-y: visible;
|
||||
}
|
||||
.rail-card-ramp {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
@@ -261,6 +268,9 @@ body.recording-mode .gev-wind-inspection-marker {
|
||||
font: 9px/1.45 var(--font-mono);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
html:not([lang='en']) .rail-card-zero::after {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.weather-observed-group {
|
||||
border: 1px solid var(--weather-card-border);
|
||||
@@ -318,6 +328,18 @@ body.recording-mode .gev-wind-inspection-marker {
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
/* One label column per card, as wide as its longest label: chips line up
|
||||
across rows, and a long translated label never runs under its chips. */
|
||||
html:not([lang='en']) .rail-card-settings {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(56px, max-content) minmax(0, 1fr);
|
||||
column-gap: 6px;
|
||||
}
|
||||
html:not([lang='en']) .rail-card-setting {
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: minmax(56px, max-content) minmax(0, 1fr);
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
.rail-card-setting-label {
|
||||
color: var(--text-dim);
|
||||
font: 9px var(--font-mono);
|
||||
|
||||
Reference in new issue
Block a user