mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
fix(i18n): second screenshot review: remaining English, numbers and wraps
From the re-shot Russian screenshots:
- Translated: weather frame/imagery status errors, HUD summary provenance
("НЕДОСТУПНО: СУДА AIS ОНЛАЙН"), Bhote Koshi map labels, cockpit signal
distances (КМ). The HUD summary fields are separated with " · " in every
language, so the style name and view band no longer read as one phrase.
- Numbers follow the locale: cockpit altitude ("26 175"), precipitation.
- No breaks inside codes and dates: ADS-B and similar in layer rows,
Sentinel-2, cyclone issue dates (word joiner; the UI font has no U+2011),
no-break space after label colons in cyclone details.
- Layout: count lines under ALPR, short unavailable chip (context
"status chip"), roomier draw segments, tighter CCTV calibration chips,
two-line beat titles and media card titles, wrapping weather status.
- Wording: ru advisory is «сводка» (was «бюллетень»; "Advisory center" is
«Центр циклона по сводке»), Street View is «Просмотр улиц», OPACITY
«НЕПРОЗР.», synced «синхр.», Area «Зона»; the import dialog no longer says
"this scene" for several scenes. Glossary updated.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2b6ccad732
commit
54dce5b3bd
32 files changed
+1204
-346
No files matched your search
@@ -1,5 +1,5 @@
|
||||
/** Regional briefing requests, pages and rotation for the Cockpit controller. */
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
import { formatNumber, t, msg } from '../i18n/index.js';
|
||||
import {
|
||||
COCKPIT_BRIEF_ROTATE_MS,
|
||||
COCKPIT_BRIEF_CYCLE_OFF_HELP,
|
||||
@@ -296,7 +296,11 @@ export function renderRegionalBrief(payload, info) {
|
||||
this.localPrecipitation.textContent = Number.isFinite(
|
||||
weather?.precipitationMm,
|
||||
)
|
||||
? weather.precipitationMm.toFixed(1)
|
||||
? formatNumber(weather.precipitationMm, {
|
||||
useGrouping: false,
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
: '—';
|
||||
}
|
||||
if (this.signalStream)
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
relativeBearing,
|
||||
resolveCockpitContextReadout,
|
||||
} from '../cockpitMath.js';
|
||||
import { formatCockpitDistance } from './cockpitPresentation.js';
|
||||
|
||||
/** The Context scope line in the active language (see cockpitMath.js). */
|
||||
const LOCALIZED_CONTEXT_SCOPE = Object.freeze({
|
||||
@@ -117,12 +118,7 @@ export function updateContext(info, heading) {
|
||||
if (this.contextDistance) {
|
||||
const distanceM = closest?.distanceM;
|
||||
this.contextDistance.textContent = Number.isFinite(distanceM)
|
||||
? t('{value} KM', {
|
||||
value:
|
||||
distanceM < 10000
|
||||
? (distanceM / 1000).toFixed(1)
|
||||
: Math.round(distanceM / 1000),
|
||||
})
|
||||
? formatCockpitDistance(distanceM)
|
||||
: '—';
|
||||
this.contextDistance.setAttribute(
|
||||
'aria-label',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Instrument values, routes and vision controls for the Cockpit controller. */
|
||||
import { t } from '../i18n/index.js';
|
||||
import { formatNumber, t } from '../i18n/index.js';
|
||||
import {
|
||||
COCKPIT_VISION_MODES,
|
||||
normalizeCockpitVisionMode,
|
||||
@@ -69,7 +69,7 @@ export function updateHud(
|
||||
setCockpitRollingValue(
|
||||
this.altitude,
|
||||
displayedAltitudeFt !== null
|
||||
? displayedAltitudeFt.toLocaleString('en-US')
|
||||
? formatNumber(displayedAltitudeFt)
|
||||
: '-----',
|
||||
displayedAltitudeFt,
|
||||
{ immediate: forceContext },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Pure formatting and fixed presentation constants used by Cockpit components. */
|
||||
import { formatNumber, t, msg } from '../i18n/index.js';
|
||||
import { formatQuantity } from './unitLabels.js';
|
||||
|
||||
export const COCKPIT_HEADING_SLEW_DPS = 28;
|
||||
|
||||
@@ -126,6 +127,18 @@ export function formatCockpitCoordinate(value, positive, negative) {
|
||||
return `${amount}°${compassPointLabel(value >= 0 ? positive : negative)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* A contact distance such as `4.2 KM` / `44 KM` in the active language
|
||||
* (`4,2 КМ`): one decimal under 10 km, whole kilometres beyond.
|
||||
*/
|
||||
export function formatCockpitDistance(distanceM) {
|
||||
const digits = distanceM < 10000 ? 1 : 0;
|
||||
return formatQuantity(distanceM / 1000, 'km', {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
}).toUpperCase();
|
||||
}
|
||||
|
||||
export function formatCockpitWindDirection(value) {
|
||||
if (!Number.isFinite(value)) return t('DIR UNKNOWN');
|
||||
const labels = Object.keys(COMPASS_POINT_LABELS);
|
||||
@@ -133,6 +146,16 @@ export function formatCockpitWindDirection(value) {
|
||||
return `${compassPointLabel(labels[Math.round(normalized / 45) % labels.length])} · ${Math.round(normalized)}°`;
|
||||
}
|
||||
|
||||
// Digit grouping and decimal marks (`26,175`, `26 175`, `26.175`) take a
|
||||
// narrow cell; a full digit cell would open a gap in the middle of a number.
|
||||
const ROLL_SEPARATOR = /^[,.\u00a0\u202f]$/;
|
||||
|
||||
function rollTokenClass(character) {
|
||||
return ROLL_SEPARATOR.test(character)
|
||||
? 'cockpit-roll-token is-separator'
|
||||
: 'cockpit-roll-token';
|
||||
}
|
||||
|
||||
export function setCockpitRollingValue(
|
||||
element,
|
||||
text,
|
||||
@@ -169,7 +192,7 @@ export function setCockpitRollingValue(
|
||||
element.replaceChildren(
|
||||
...Array.from(nextText, (character) => {
|
||||
const token = document.createElement('span');
|
||||
token.className = 'cockpit-roll-token';
|
||||
token.className = rollTokenClass(character);
|
||||
token.setAttribute('aria-hidden', 'true');
|
||||
token.textContent = character;
|
||||
return token;
|
||||
@@ -198,7 +221,7 @@ export function setCockpitRollingValue(
|
||||
const previousCharacter = from[index];
|
||||
const nextCharacter = to[index];
|
||||
const token = document.createElement('span');
|
||||
token.className = 'cockpit-roll-token';
|
||||
token.className = rollTokenClass(nextCharacter);
|
||||
token.setAttribute('aria-hidden', 'true');
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Reconcile actionable signal rows without disturbing native keyboard focus. */
|
||||
import { t, tn } from '../i18n/index.js';
|
||||
import { formatCockpitDistance } from './cockpitPresentation.js';
|
||||
|
||||
export function renderCockpitSignals() {
|
||||
if (!this.signalList) return;
|
||||
@@ -157,7 +158,7 @@ export function updateCockpitSignals(snapshot, unknownCount) {
|
||||
? t('MILITARY FLIGHT')
|
||||
: t('COMMERCIAL FLIGHT'),
|
||||
distance: Number.isFinite(item.distanceM)
|
||||
? `${item.distanceM < 10000 ? (item.distanceM / 1000).toFixed(1) : Math.round(item.distanceM / 1000)} KM`
|
||||
? formatCockpitDistance(item.distanceM)
|
||||
: t('DISTANCE UNKNOWN'),
|
||||
}),
|
||||
target: { layerId: cohort.id, id: String(id) },
|
||||
|
||||
+89
-8
@@ -206,6 +206,64 @@ export function layerKeyRequirementTooltip(layer = {}) {
|
||||
: '';
|
||||
}
|
||||
|
||||
const ROW_TEXT_SEPARATOR = ' · ';
|
||||
// A short Latin code joined by hyphens. Longer runs (a URL, a token in an
|
||||
// error) keep their break opportunities so they cannot overflow the rail.
|
||||
const HYPHENATED_CODE = /[A-Za-z0-9]+(?:-[A-Za-z0-9]+)+/g;
|
||||
const HYPHENATED_CODE_MAX = 16;
|
||||
|
||||
/** A span for one run of row text; `keep` stops the browser breaking inside it. */
|
||||
function rowTextSpan(text, keep) {
|
||||
const span = document.createElement('span');
|
||||
if (keep) span.className = 'data-keep';
|
||||
span.textContent = text;
|
||||
return span;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a row's name, count or meta line so that a narrow column wraps it well
|
||||
* in every language: a Latin code with hyphens (ADS-B, RTL-SDR, GTFS-RT) never
|
||||
* splits at its hyphen, and a " · " separator stays at the end of the line it
|
||||
* closes, so no line starts with "·". The element's textContent is still the
|
||||
* plain string; text with neither feature is set directly.
|
||||
*
|
||||
* @param {HTMLElement} element
|
||||
* @param {string} text
|
||||
*/
|
||||
export function setRowText(element, text) {
|
||||
const value = String(text ?? '');
|
||||
// Refreshes run every second; leave unchanged text (and its spans) alone.
|
||||
if (element.textContent === value) return;
|
||||
const codes = [...value.matchAll(HYPHENATED_CODE)].filter(
|
||||
(match) => match[0].length <= HYPHENATED_CODE_MAX,
|
||||
);
|
||||
if (!codes.length && !value.includes(ROW_TEXT_SEPARATOR)) {
|
||||
element.textContent = value;
|
||||
return;
|
||||
}
|
||||
const spans = [];
|
||||
const pushPlain = (from, to) => {
|
||||
if (to <= from) return;
|
||||
const pieces = value.slice(from, to).split(ROW_TEXT_SEPARATOR);
|
||||
pieces.forEach((piece, index) => {
|
||||
// The space before the dot belongs to a no-wrap span, so the only break
|
||||
// opportunity around a separator is the space after it.
|
||||
if (index > 0) spans.push(rowTextSpan(' ·', true));
|
||||
const textPiece = index > 0 ? ` ${piece}` : piece;
|
||||
if (textPiece) spans.push(rowTextSpan(textPiece, false));
|
||||
});
|
||||
};
|
||||
let cursor = 0;
|
||||
for (const match of codes) {
|
||||
pushPlain(cursor, match.index);
|
||||
spans.push(rowTextSpan(match[0], true));
|
||||
cursor = match.index + match[0].length;
|
||||
}
|
||||
pushPlain(cursor, value.length);
|
||||
element.textContent = '';
|
||||
for (const span of spans) element.appendChild(span);
|
||||
}
|
||||
|
||||
/** Layer row presentation over supplied state and actions; no layer imports. */
|
||||
export class LayerPanel {
|
||||
constructor({
|
||||
@@ -331,7 +389,7 @@ export class LayerPanel {
|
||||
icon.textContent = layer.icon;
|
||||
const name = document.createElement('span');
|
||||
name.className = 'data-name';
|
||||
name.textContent = panelLabel(layer);
|
||||
setRowText(name, panelLabel(layer));
|
||||
left.appendChild(icon);
|
||||
left.appendChild(name);
|
||||
|
||||
@@ -340,7 +398,7 @@ export class LayerPanel {
|
||||
|
||||
const count = document.createElement('span');
|
||||
count.className = 'data-count';
|
||||
count.textContent = this._layerCountText(layer.stats);
|
||||
this._syncCount(row, count, layer.stats);
|
||||
|
||||
const toggle = document.createElement('button');
|
||||
toggle.type = 'button';
|
||||
@@ -379,7 +437,7 @@ export class LayerPanel {
|
||||
|
||||
const bottomRow = document.createElement('div');
|
||||
bottomRow.className = 'data-toggle-meta';
|
||||
bottomRow.textContent = this._buildMetaText(layer);
|
||||
setRowText(bottomRow, this._buildMetaText(layer));
|
||||
|
||||
row.appendChild(topRow);
|
||||
row.appendChild(bottomRow);
|
||||
@@ -482,6 +540,21 @@ export class LayerPanel {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a row's count. A count that is a phrase carrying its own number
|
||||
* ("24 nearby · 2 on screen") moves to its own line under the name: beside
|
||||
* the toggle it squeezes the name to a word per line in any language. The
|
||||
* "—" placeholder is marked so it can give way to a wide status chip.
|
||||
*/
|
||||
_syncCount(row, count, stats) {
|
||||
const text = this._layerCountText(stats);
|
||||
setRowText(count, text);
|
||||
const phrase =
|
||||
typeof stats.countLabel === 'string' && /\d/.test(stats.countLabel);
|
||||
row.classList.toggle('data-toggle-row--count-line', phrase);
|
||||
count.classList.toggle('data-count-empty', text === '—');
|
||||
}
|
||||
|
||||
/** Qualify a loaded count when it does not mean items currently on screen. */
|
||||
_layerCountText(stats) {
|
||||
if (typeof stats.countLabel === 'string' && stats.countLabel.trim())
|
||||
@@ -605,12 +678,12 @@ export class LayerPanel {
|
||||
|
||||
const count = row.querySelector('.data-count');
|
||||
if (count) {
|
||||
count.textContent = this._layerCountText(layer.stats);
|
||||
this._syncCount(row, count, layer.stats);
|
||||
}
|
||||
|
||||
const meta = row.querySelector('.data-toggle-meta');
|
||||
if (meta) {
|
||||
meta.textContent = this._buildMetaText(layer);
|
||||
setRowText(meta, this._buildMetaText(layer));
|
||||
}
|
||||
|
||||
this._syncRowControls(
|
||||
@@ -765,13 +838,21 @@ export class LayerPanel {
|
||||
String(transitioning || (unavailable && !layer.enabled)),
|
||||
);
|
||||
button.setAttribute('aria-busy', String(transitioning));
|
||||
button.textContent = transitioning
|
||||
const showsFeedState =
|
||||
!transitioning && !uncertain && (layer.enabled || unavailable);
|
||||
const stateText = transitioning
|
||||
? lifecycleStateLabel(layer.lifecycleState)
|
||||
: uncertain
|
||||
? t('UNCERTAIN')
|
||||
: layer.enabled || unavailable
|
||||
: showsFeedState
|
||||
? t(FEED_STATE_LABEL_KEYS[feedState])
|
||||
: t('OFF');
|
||||
// The chip may abbreviate where a language's word is wider than the
|
||||
// status column; the meta line and the accessible name keep the full word.
|
||||
button.textContent =
|
||||
showsFeedState && feedState === 'unavailable'
|
||||
? t('UNAVAILABLE', { context: 'status chip' })
|
||||
: stateText;
|
||||
const keyGuidance = unavailable
|
||||
? String(layer.stats.error || t('Data source unavailable'))
|
||||
: layerKeyRequirementTooltip(layer);
|
||||
@@ -781,7 +862,7 @@ export class LayerPanel {
|
||||
button.title = keyGuidance;
|
||||
const ariaLabel = t('{name}: {state}', {
|
||||
name: panelLabel(layer),
|
||||
state: button.textContent,
|
||||
state: stateText,
|
||||
});
|
||||
button.setAttribute(
|
||||
'aria-label',
|
||||
|
||||
@@ -260,3 +260,72 @@ test('an omitted source is visibly unavailable and cannot be toggled on', async
|
||||
'an active layer can always be switched off',
|
||||
);
|
||||
});
|
||||
|
||||
test('row text keeps a hyphenated code whole and never starts a line with the separator', async () => {
|
||||
const { setRowText } = await import('./layerPanel.js');
|
||||
const previousDocument = globalThis.document;
|
||||
const element = () => ({
|
||||
className: '',
|
||||
textContent: '',
|
||||
children: [],
|
||||
appendChild(child) {
|
||||
this.children.push(child);
|
||||
return child;
|
||||
},
|
||||
});
|
||||
globalThis.document = { createElement: element };
|
||||
try {
|
||||
const text = (node) =>
|
||||
node.children.length
|
||||
? node.children.map((child) => child.textContent).join('')
|
||||
: node.textContent;
|
||||
const kept = (node) =>
|
||||
node.children
|
||||
.filter((child) => child.className === 'data-keep')
|
||||
.map((child) => child.textContent);
|
||||
|
||||
const name = element();
|
||||
setRowText(name, 'Локальный ADS-B');
|
||||
assert.equal(text(name), 'Локальный ADS-B');
|
||||
assert.deepEqual(kept(name), ['ADS-B']);
|
||||
|
||||
const meta = element();
|
||||
setRowText(meta, 'UNAVAILABLE · NASA FIRMS · LIVE · KEY REQUIRED');
|
||||
assert.equal(text(meta), 'UNAVAILABLE · NASA FIRMS · LIVE · KEY REQUIRED');
|
||||
assert.deepEqual(kept(meta), [' ·', ' ·', ' ·']);
|
||||
assert.ok(
|
||||
meta.children.every((child) => !child.textContent.startsWith('·')),
|
||||
'the space before each dot is held to the piece it closes',
|
||||
);
|
||||
|
||||
const plain = element();
|
||||
setRowText(plain, 'Satellites');
|
||||
assert.equal(plain.textContent, 'Satellites');
|
||||
assert.equal(plain.children.length, 0);
|
||||
|
||||
const long = element();
|
||||
setRowText(long, 'see https-proxy-endpoint-config-x');
|
||||
assert.deepEqual(kept(long), [], 'a long hyphenated run may still wrap');
|
||||
} finally {
|
||||
globalThis.document = previousDocument;
|
||||
}
|
||||
});
|
||||
|
||||
test('the unavailable chip may abbreviate, but its accessible name keeps the full state', async () => {
|
||||
const { LayerPanel } = await import('./layerPanel.js');
|
||||
const attrs = new Map();
|
||||
const button = {
|
||||
classList: { toggle() {} },
|
||||
dataset: {},
|
||||
setAttribute: (key, value) => attrs.set(key, value),
|
||||
};
|
||||
LayerPanel.prototype._syncToggleButton(button, {
|
||||
id: 'local-firms',
|
||||
name: 'FIRMS Active Fires',
|
||||
enabled: false,
|
||||
stats: { sourceUnavailable: true, error: 'KEY REQUIRED' },
|
||||
});
|
||||
assert.equal(button.textContent, 'UNAVAILABLE');
|
||||
assert.equal(button.dataset.feedState, 'unavailable');
|
||||
assert.match(attrs.get('aria-label'), /^FIRMS Active Fires: UNAVAILABLE\./);
|
||||
});
|
||||
@@ -281,8 +281,10 @@
|
||||
border-top: 3px solid #6be8ff;
|
||||
}
|
||||
|
||||
/* Up to two lines, so a translated title is not cut to a stub. */
|
||||
/* 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. */
|
||||
.bhote-embedded-callout footer strong {
|
||||
grid-column: 1 / -1;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
@@ -292,9 +294,9 @@
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
/* The status takes the full width under the title and link. */
|
||||
.bhote-embedded-callout footer small {
|
||||
grid-column: 1 / -1;
|
||||
grid-column: 1;
|
||||
align-self: center;
|
||||
color: rgba(151, 211, 224, 0.7);
|
||||
font-size: 8px;
|
||||
line-height: 1.35;
|
||||
@@ -302,7 +304,7 @@
|
||||
|
||||
.bhote-embedded-callout footer a {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
grid-row: 2;
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
color: #6be8ff;
|
||||
@@ -450,7 +452,7 @@
|
||||
align-content: center;
|
||||
column-gap: 9px;
|
||||
min-width: 0;
|
||||
padding: 7px 10px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid rgba(88, 196, 218, 0.18);
|
||||
background: rgba(6, 19, 27, 0.72);
|
||||
}
|
||||
@@ -462,20 +464,24 @@
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
.bhote-event-story-current strong {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: 11px;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow-wrap: break-word;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
/* Phase / time / sources run long in translation: allow a second line. */
|
||||
.bhote-event-story-current small {
|
||||
display: -webkit-box;
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 4px;
|
||||
margin-top: 3px;
|
||||
overflow: hidden;
|
||||
color: rgba(116, 216, 235, 0.7);
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
@@ -106,22 +106,26 @@ html[lang='es'] .cctv-controls {
|
||||
}
|
||||
|
||||
/* Click-to-edit effective-pose readout (viewshed/gizmo design §3d): compact
|
||||
value chips replace the seven offset sliders. */
|
||||
value chips replace the seven offset sliders. Tight enough that the seven
|
||||
chips take two rows in a long-label language too, which keeps SAVE / RESET
|
||||
in view; chips grow to fill each row, and a value never wraps inside one. */
|
||||
.cctv-cal-readout {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cctv-cal-value {
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.7px;
|
||||
letter-spacing: 0.3px;
|
||||
color: #91edff;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 7px;
|
||||
padding: 4px 7px;
|
||||
padding: 4px;
|
||||
cursor: text;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
|
||||
@@ -569,6 +569,9 @@ body.cockpit-mode:has(#left-panel-stack > .panel-collapsible:not(.collapsed))
|
||||
text-align: center;
|
||||
vertical-align: -0.08em;
|
||||
}
|
||||
.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
|
||||
|
||||
@@ -977,6 +977,11 @@
|
||||
#draw-mode-row {
|
||||
min-width: 184px;
|
||||
}
|
||||
/* Equal thirds with room around the label, so a long shape name never runs
|
||||
edge to edge in its segment. */
|
||||
#draw-mode-row .pp-mode-btn {
|
||||
padding: 0 4px;
|
||||
}
|
||||
#draw-label-row {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 6px;
|
||||
|
||||
+51
-1
@@ -663,6 +663,54 @@
|
||||
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). */
|
||||
.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. */
|
||||
.data-toggle-row--count-line .data-toggle-top {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 2px;
|
||||
}
|
||||
|
||||
.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. */
|
||||
.data-toggle-row--count-line .data-toggle-left {
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.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. */
|
||||
.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);
|
||||
@@ -693,13 +741,15 @@
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
.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 {
|
||||
min-width: 82px;
|
||||
min-width: 72px;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -147,9 +147,10 @@
|
||||
.rail-card-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
/* One line stays reserved so the card does not jump when a status appears;
|
||||
a long (or translated) status wraps instead of being cut. */
|
||||
.weather-card [data-line-id='status'] {
|
||||
height: 1.45em;
|
||||
overflow-y: auto;
|
||||
min-height: 1.45em;
|
||||
color: var(--accent);
|
||||
}
|
||||
.rail-card-ramp {
|
||||
|
||||
Reference in new issue
Block a user