mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor(i18n): key messages by English text; add Russian
Replace the semantic keys with the English source text, so code reads as the
UI does and English needs no catalog: t('Save keys'), tn(count, one, other)
for plurals, msg() for text stored in data, and bare data-i18n* markup that
translates the element's own English. Other languages are one flat JSON each.
- Simplify src/i18n: drop namespaces, the configurable locale pair
(GEV_DEFAULT_LOCALE / GEV_SECONDARY_LOCALE) and its build defines. Locale
comes from ?lang=, the stored choice, then browser languages.
- Add scripts/i18n.mjs (npm run i18n): extracts every message from t/tn/msg
and markup, and fails on missing, unused, placeholder-mismatched or
wrongly-shaped plural entries. npm test runs it via catalog.test.mjs.
- Add Russian (src/i18n/locales/ru.json, 833 messages), started from the
community catalog in #312 and reviewed against the current English.
- Fix data-i18n placed on parents that would wipe their children (cockpit
LEVEL and local weather cells, first-run EXPLORE MANUALLY tile), and a
doubled degree sign in the TRACK ACQUIRED signal.
- Rewrite docs/TRANSLATORS.md for the new workflow; drop sections the earlier
merge resurrected in docs/CURRENT-STATE.md.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
dfbaeafbd8
commit
13efd411b6
112 files changed
+4419
-4856
No files matched your search
+22
-30
@@ -38,7 +38,7 @@ import { ShellFeedback } from './shellFeedback.js';
|
||||
import { runCctvLayerEnableTransition } from '../cctvFocusPolicy.js';
|
||||
|
||||
import { getLocale, persistLocaleAndReload, t } from '../i18n/index.js';
|
||||
import { availableLocales } from '../i18n/locale.js';
|
||||
import { LOCALES, LOCALE_NAMES } from '../i18n/locale.js';
|
||||
|
||||
/**
|
||||
* Central UI orchestrator for the God's Eye View application.
|
||||
@@ -706,15 +706,9 @@ export class StyleManager extends ShellFacade {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the command-dock locale switch (runtime-built since the
|
||||
* configurable-pair stage): one button per locale availableLocales()
|
||||
* yields — the configured GEV_DEFAULT_LOCALE / GEV_SECONDARY_LOCALE pair,
|
||||
* already deduped against the always-shipped English fallback. Labels are
|
||||
* uppercase locale codes (EN/ES/FR); aria-labels come from
|
||||
* shell.locale.<code>.ariaLabel in the active catalog. Pressed state is
|
||||
* synced from the resolved locale at boot; a click persists the choice and
|
||||
* reloads the page, so no live re-apply of document translations is needed
|
||||
* here (persistLocaleAndReload strips ?lang and keeps the hash).
|
||||
* Renders the command-dock locale switch: one button per shipped locale,
|
||||
* labelled with its code and named in its own language. A click stores the
|
||||
* choice and reloads, so nothing re-renders live.
|
||||
* @returns {void}
|
||||
*/
|
||||
_initLocaleSelector() {
|
||||
@@ -723,13 +717,14 @@ export class StyleManager extends ShellFacade {
|
||||
);
|
||||
if (!container) return;
|
||||
const active = getLocale();
|
||||
for (const locale of availableLocales()) {
|
||||
for (const locale of LOCALES) {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'dock-locale-btn';
|
||||
button.dataset.locale = locale;
|
||||
button.textContent = locale.toUpperCase();
|
||||
button.setAttribute('aria-label', t(`shell.locale.${locale}.ariaLabel`));
|
||||
button.setAttribute('aria-label', LOCALE_NAMES[locale]);
|
||||
button.lang = locale;
|
||||
const isActive = locale === active;
|
||||
button.classList.toggle('active', isActive);
|
||||
button.setAttribute('aria-pressed', String(isActive));
|
||||
@@ -770,15 +765,15 @@ export class StyleManager extends ShellFacade {
|
||||
_applyRuntimeStaticHeaderText() {
|
||||
this._setHeaderText(
|
||||
document.querySelector('#control-panel .panel-title'),
|
||||
t('cockpit.presets.title'),
|
||||
t('VISUAL PRESETS'),
|
||||
);
|
||||
this._setHeaderText(
|
||||
document.querySelector('#location-bar .location-toolbar-label'),
|
||||
t('cockpit.location.toolbarLabel'),
|
||||
t('LOCATION'),
|
||||
);
|
||||
this._setHeaderText(
|
||||
document.querySelector('#pp-toggles .pp-header-label'),
|
||||
t('cockpit.display.title'),
|
||||
t('DISPLAY'),
|
||||
);
|
||||
// The cockpit context kicker ('CONTACT') is visible on the map itself, and
|
||||
// the briefing kicker shares its element with the live-dot <i>; both are
|
||||
@@ -786,7 +781,7 @@ export class StyleManager extends ShellFacade {
|
||||
// text node keeps renderBriefPage's leading-space convention.
|
||||
this._setHeaderText(
|
||||
document.querySelector('.cockpit-context-kicker'),
|
||||
t('cockpit.context.kicker'),
|
||||
t('CONTACT'),
|
||||
);
|
||||
// The phase-2 key-only standby description: one span holds BOTH mode
|
||||
// descriptions split by a literal <br>, so no attribute write was possible
|
||||
@@ -797,14 +792,18 @@ export class StyleManager extends ShellFacade {
|
||||
);
|
||||
if (standbyDescription) {
|
||||
standbyDescription.replaceChildren(
|
||||
document.createTextNode(t('cockpit.context.standbyContactsDesc')),
|
||||
document.createTextNode(
|
||||
t('CONTACTS — nearest planes · vessels · sites'),
|
||||
),
|
||||
document.createElement('br'),
|
||||
document.createTextNode(t('cockpit.context.standbyMissionsDesc')),
|
||||
document.createTextNode(
|
||||
t('SPACE MISSIONS — launches & orbital assets'),
|
||||
),
|
||||
);
|
||||
}
|
||||
const briefKicker = document.querySelector('#cockpit-brief-kicker');
|
||||
if (briefKicker) {
|
||||
this._setHeaderText(briefKicker, ` ${t('cockpit.brief.kicker')}`);
|
||||
this._setHeaderText(briefKicker, ` ${t('LIVE SIGNALS')}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,11 +821,8 @@ export class StyleManager extends ShellFacade {
|
||||
// runtime write instead. The locale resolves before UI init and never
|
||||
// changes without a reload, so an init-time write is sufficient.
|
||||
if (this._mapSourceLabel)
|
||||
this._mapSourceLabel.textContent = t('cockpit.presets.mapSourceLabel');
|
||||
this._mapStackChips.setAttribute(
|
||||
'aria-label',
|
||||
t('cockpit.presets.mapSourceChipsAriaLabel'),
|
||||
);
|
||||
this._mapSourceLabel.textContent = t('MAP SOURCE');
|
||||
this._mapStackChips.setAttribute('aria-label', t('Map source'));
|
||||
|
||||
this._mapSourceControls?.destroy();
|
||||
this._mapSourceControls = createMapSourceControls({
|
||||
@@ -1149,7 +1145,7 @@ export class StyleManager extends ShellFacade {
|
||||
const { cctvLayer } = this.services;
|
||||
if (this._disposed) return false;
|
||||
if (!this._dataManager || !this._dataManager.layers?.has('cctv')) {
|
||||
this._showToast(t('cockpit.cctv.toastLayerUnavailable'));
|
||||
this._showToast(t('CCTV layer unavailable'));
|
||||
return false;
|
||||
}
|
||||
const enabled = this._dataManager.isEnabled('cctv');
|
||||
@@ -1598,11 +1594,7 @@ export class StyleManager extends ShellFacade {
|
||||
this._lifetime.listen(this._shareBtn, 'click', async () => {
|
||||
const success = await this.shareLinkManager.copyLink();
|
||||
if (!this._disposed)
|
||||
this._showToast(
|
||||
success
|
||||
? t('cockpit.share.toastCopied')
|
||||
: t('cockpit.share.toastCopyFailed'),
|
||||
);
|
||||
this._showToast(success ? t('Link copied!') : t('Copy failed'));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ export function _initCctvPanel() {
|
||||
},
|
||||
{ origin: 'user' },
|
||||
);
|
||||
this.actions.showToast(t('cockpit.cctv.toastCalibrationSaved'));
|
||||
this.actions.showToast(t('CCTV calibration saved'));
|
||||
});
|
||||
|
||||
this.listen(this._cctvCalibResetBtn, 'click', () => {
|
||||
|
||||
@@ -83,7 +83,7 @@ export function _resetCctvCalibration() {
|
||||
},
|
||||
{ origin: 'user' },
|
||||
);
|
||||
this.actions.showToast(t('cockpit.cctv.toastCalibrationReset'));
|
||||
this.actions.showToast(t('CCTV calibration reset'));
|
||||
}
|
||||
|
||||
export function _beginCctvCalValueEdit(chip) {
|
||||
@@ -164,9 +164,7 @@ export function _syncCctvCalReadout(enabled, activeCamera) {
|
||||
if (this._cctvAdjustBtn) {
|
||||
const adjustOn = !!this._cctvState?.calibrationMode;
|
||||
this._cctvAdjustBtn.classList.toggle('active', adjustOn && canCalibrate);
|
||||
this._cctvAdjustBtn.textContent = adjustOn
|
||||
? t('layers.cctv.adjustOn')
|
||||
: t('layers.cctv.adjustLabel');
|
||||
this._cctvAdjustBtn.textContent = adjustOn ? t('ADJUST ON') : t('ADJUST');
|
||||
this._cctvAdjustBtn.disabled = !canCalibrate;
|
||||
}
|
||||
if (this._cctvCalReadout) {
|
||||
|
||||
@@ -88,19 +88,19 @@ export function _settleCctvFrame(token, src, ok) {
|
||||
export function _syncCctvSourceBadge(activeCamera, enabled) {
|
||||
if (!this._cctvSourceBadge) return;
|
||||
if (!enabled || !activeCamera) {
|
||||
this._cctvSourceBadge.textContent = t('layers.cctv.sourceUnknown');
|
||||
this._cctvSourceBadge.textContent = t('SOURCE · UNKNOWN');
|
||||
this._cctvSourceBadge.dataset.frameState = 'idle';
|
||||
return;
|
||||
}
|
||||
const hasDisplayedFrame =
|
||||
this._cctvFrameWrap?.classList.contains('has-frame');
|
||||
if (this._cctvFrame?.dataset.loading === 'true' && !hasDisplayedFrame) {
|
||||
this._cctvSourceBadge.textContent = t('cockpit.cctv.frameLoading');
|
||||
this._cctvSourceBadge.textContent = t('FRAME · LOADING');
|
||||
this._cctvSourceBadge.dataset.frameState = 'loading';
|
||||
return;
|
||||
}
|
||||
if (this._cctvFrame?.dataset.error === 'true' && !hasDisplayedFrame) {
|
||||
this._cctvSourceBadge.textContent = t('cockpit.cctv.frameUnavailable');
|
||||
this._cctvSourceBadge.textContent = t('FRAME · UNAVAILABLE');
|
||||
this._cctvSourceBadge.dataset.frameState = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
+48
-43
@@ -1,14 +1,14 @@
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, tn } from '../i18n/index.js';
|
||||
|
||||
import { createCctvVideoSurface } from './cctvVideo.js';
|
||||
export function _calBadgeLabel(badge) {
|
||||
switch (badge) {
|
||||
case 'calibrated':
|
||||
return t('cockpit.cctv.calBadgeCalibrated');
|
||||
return t('CALIBRATED');
|
||||
case 'curated':
|
||||
return t('cockpit.cctv.calBadgeCurated');
|
||||
return t('CURATED');
|
||||
case 'raw-prior':
|
||||
return t('cockpit.cctv.calBadgeRawPrior');
|
||||
return t('RAW PRIOR');
|
||||
default:
|
||||
return '--';
|
||||
}
|
||||
@@ -53,9 +53,7 @@ export function _renderCctvState(state) {
|
||||
|
||||
if (this._cctvEnableBtn) {
|
||||
this._cctvEnableBtn.classList.toggle('active', enabled);
|
||||
this._cctvEnableBtn.textContent = enabled
|
||||
? t('layers.cctv.toggleOn')
|
||||
: t('layers.cctv.toggleOff');
|
||||
this._cctvEnableBtn.textContent = enabled ? t('CCTV ON') : t('CCTV OFF');
|
||||
}
|
||||
|
||||
if (this._cctvSelect) {
|
||||
@@ -103,10 +101,10 @@ export function _renderCctvState(state) {
|
||||
this._cctvCoverageBtn.classList.toggle('active', mode !== 'off');
|
||||
this._cctvCoverageBtn.textContent =
|
||||
mode === 'viewshed'
|
||||
? t('cockpit.cctv.coverageViewshedOn')
|
||||
? t('VIEWSHED ON')
|
||||
: mode === 'on'
|
||||
? t('layers.cctv.coverageOn')
|
||||
: t('layers.cctv.coverageOff');
|
||||
? t('COVERAGE ON')
|
||||
: t('COVERAGE OFF');
|
||||
this._cctvCoverageBtn.disabled = !enabled;
|
||||
}
|
||||
|
||||
@@ -114,8 +112,8 @@ export function _renderCctvState(state) {
|
||||
const autoHop = !!state?.autoHop;
|
||||
this._cctvAutoHopBtn.classList.toggle('active', autoHop);
|
||||
this._cctvAutoHopBtn.textContent = autoHop
|
||||
? t('layers.cctv.autoHopOn')
|
||||
: t('layers.cctv.autoHopOff');
|
||||
? t('AUTO HOP ON')
|
||||
: t('AUTO HOP OFF');
|
||||
this._cctvAutoHopBtn.disabled = !enabled;
|
||||
}
|
||||
|
||||
@@ -123,8 +121,8 @@ export function _renderCctvState(state) {
|
||||
const showProjection = state?.showProjection !== false;
|
||||
this._cctvProjectionBtn.classList.toggle('active', showProjection);
|
||||
this._cctvProjectionBtn.textContent = showProjection
|
||||
? t('layers.cctv.projectionOn')
|
||||
: t('layers.cctv.projectionOff');
|
||||
? t('PROJECTION ON')
|
||||
: t('PROJECTION OFF');
|
||||
this._cctvProjectionBtn.disabled = !enabled;
|
||||
}
|
||||
|
||||
@@ -139,8 +137,8 @@ export function _renderCctvState(state) {
|
||||
const badge = activeCamera?.calBadge || null;
|
||||
const dirty = !!activeCamera?.calDirty;
|
||||
this._cctvQualityChip.textContent = dirty
|
||||
? t('cockpit.cctv.calChipEdited')
|
||||
: t('cockpit.cctv.calChipTemplate', {
|
||||
? t('CAL · EDITED (UNSAVED)')
|
||||
: t('CAL · {badge}', {
|
||||
badge: this._calBadgeLabel(badge),
|
||||
});
|
||||
this._cctvQualityChip.dataset.calBadge = dirty ? 'edited' : badge || '';
|
||||
@@ -163,25 +161,36 @@ export function _renderCctvState(state) {
|
||||
? this._calBadgeLabel(activeCamera.calBadge)
|
||||
: '';
|
||||
const projLabel =
|
||||
state?.showProjection !== false
|
||||
? t('cockpit.cctv.metaProjectionMonitor')
|
||||
: t('cockpit.cctv.metaProjectionOff');
|
||||
this._cctvMeta.textContent = t('cockpit.cctv.metaTemplate', {
|
||||
city: activeCamera.city,
|
||||
heading: `${Math.round(activeCamera.headingDeg)}°`,
|
||||
fov: `${Math.round(activeCamera.fovDeg)}°`,
|
||||
range: Math.round(activeCamera.rangeM),
|
||||
projection: projLabel,
|
||||
calBadge: calBadge ? ` · ${calBadge}` : '',
|
||||
provider,
|
||||
status: `${credit}${statusMsg}`,
|
||||
});
|
||||
state?.showProjection !== false ? t('MONITOR') : t('OFF');
|
||||
this._cctvMeta.textContent = t(
|
||||
'{city} · HDG {heading} · FOV {fov} · RANGE {range}m · {projection}{calBadge} · {provider}{status}',
|
||||
{
|
||||
city: activeCamera.city,
|
||||
heading: `${Math.round(activeCamera.headingDeg)}°`,
|
||||
fov: `${Math.round(activeCamera.fovDeg)}°`,
|
||||
range: Math.round(activeCamera.rangeM),
|
||||
projection: projLabel,
|
||||
calBadge: calBadge ? ` · ${calBadge}` : '',
|
||||
provider,
|
||||
status: `${credit}${statusMsg}`,
|
||||
},
|
||||
);
|
||||
} else if (cameras.length > 0) {
|
||||
this._cctvMeta.textContent = enabled
|
||||
? t('cockpit.cctv.metaCamerasClick', { count: cameras.length })
|
||||
: t('cockpit.cctv.metaCamerasEnable', { count: cameras.length });
|
||||
? tn(
|
||||
cameras.length,
|
||||
'{count} camera loaded · click a camera to activate',
|
||||
'{count} cameras loaded · click a camera to activate',
|
||||
)
|
||||
: tn(
|
||||
cameras.length,
|
||||
'{count} camera loaded · enable CCTV to activate',
|
||||
'{count} cameras loaded · enable CCTV to activate',
|
||||
);
|
||||
} else {
|
||||
this._cctvMeta.textContent = t('layers.cctv.metaIdle');
|
||||
this._cctvMeta.textContent = t(
|
||||
'Enable CCTV to load camera intersections',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,13 +238,15 @@ export function _renderCctvState(state) {
|
||||
}
|
||||
|
||||
this._syncCctvSourceBadge(activeCamera, enabled);
|
||||
this._typeCctvSummary(state?.summary || t('layers.cctv.summaryIdle'));
|
||||
this._typeCctvSummary(
|
||||
state?.summary ||
|
||||
t('Enable CCTV to start camera-linked intelligence summaries.'),
|
||||
);
|
||||
}
|
||||
|
||||
export function _typeCctvSummary(text) {
|
||||
if (this.destroyed || !this._cctvSummary) return;
|
||||
const nextText =
|
||||
String(text || '').trim() || t('cockpit.cctv.summaryNoneAvailable');
|
||||
const nextText = String(text || '').trim() || t('No summary available.');
|
||||
if (nextText === this._lastCctvSummaryText) return;
|
||||
this._lastCctvSummaryText = nextText;
|
||||
|
||||
@@ -267,10 +278,7 @@ export function _updateCctvSyncChip(loading, enabled) {
|
||||
clearTimeout(this._cctvChipHideTimer);
|
||||
this._cctvChipHideTimer = null;
|
||||
this._cctvChipWasBusy = true;
|
||||
this.actions.setSplitFlapText(
|
||||
this._cctvSyncLabel,
|
||||
t('cockpit.cctv.syncLoadingFrames'),
|
||||
);
|
||||
this.actions.setSplitFlapText(this._cctvSyncLabel, t('loading frames'));
|
||||
// The counter is left plain on purpose: it ticks every few frames
|
||||
// during a grid load, and flapping it would read as a slot machine.
|
||||
this._cctvSyncProgress.textContent = `${loaded}/${total}`;
|
||||
@@ -281,10 +289,7 @@ export function _updateCctvSyncChip(loading, enabled) {
|
||||
if (this._cctvChipWasBusy && enabled && total > 0) {
|
||||
// Load just completed — flash the final count, then auto-hide.
|
||||
this._cctvChipWasBusy = false;
|
||||
this.actions.setSplitFlapText(
|
||||
this._cctvSyncLabel,
|
||||
t('cockpit.cctv.syncGridReady'),
|
||||
);
|
||||
this.actions.setSplitFlapText(this._cctvSyncLabel, t('camera grid ready'));
|
||||
this._cctvSyncProgress.textContent = `${total}/${total}`;
|
||||
this._cctvSyncChip.classList.add('visible');
|
||||
clearTimeout(this._cctvChipHideTimer);
|
||||
|
||||
@@ -15,8 +15,8 @@ export function bindClearLayersControl(button, clear) {
|
||||
button.setAttribute(
|
||||
'aria-label',
|
||||
busy
|
||||
? t('cockpit.actions.clearLayersBusyAria')
|
||||
: t('shell.actions.clearLayers.ariaLabel'),
|
||||
? t('Clearing selected data layers')
|
||||
: t('Clear selected data layers'),
|
||||
);
|
||||
},
|
||||
destroy() {
|
||||
|
||||
+35
-35
@@ -1,5 +1,5 @@
|
||||
/** Regional briefing requests, pages and rotation for the Cockpit controller. */
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
import {
|
||||
COCKPIT_BRIEF_ROTATE_MS,
|
||||
COCKPIT_BRIEF_CYCLE_OFF_HELP,
|
||||
@@ -15,18 +15,18 @@ import {
|
||||
// so it returns stable English labels; localize them here, at the cockpit
|
||||
// presentation boundary (docs/TRANSLATORS.md).
|
||||
const WEATHER_CONDITION_KEYS = Object.freeze({
|
||||
'CONDITIONS UNKNOWN': 'layers.weather.conditionsUnknown',
|
||||
CLEAR: 'layers.weather.clear',
|
||||
'PARTLY CLOUDY': 'layers.weather.partlyCloudy',
|
||||
OVERCAST: 'layers.weather.overcast',
|
||||
FOG: 'layers.weather.fog',
|
||||
DRIZZLE: 'layers.weather.drizzle',
|
||||
RAIN: 'layers.weather.rain',
|
||||
SNOW: 'layers.weather.snow',
|
||||
'RAIN SHOWERS': 'layers.weather.rainShowers',
|
||||
'SNOW SHOWERS': 'layers.weather.snowShowers',
|
||||
THUNDERSTORM: 'layers.weather.thunderstorm',
|
||||
'MIXED CONDITIONS': 'layers.weather.mixed',
|
||||
'CONDITIONS UNKNOWN': msg('CONDITIONS UNKNOWN'),
|
||||
CLEAR: msg('CLEAR'),
|
||||
'PARTLY CLOUDY': msg('PARTLY CLOUDY'),
|
||||
OVERCAST: msg('OVERCAST'),
|
||||
FOG: msg('FOG'),
|
||||
DRIZZLE: msg('DRIZZLE'),
|
||||
RAIN: msg('RAIN'),
|
||||
SNOW: msg('SNOW'),
|
||||
'RAIN SHOWERS': msg('RAIN SHOWERS'),
|
||||
'SNOW SHOWERS': msg('SNOW SHOWERS'),
|
||||
THUNDERSTORM: msg('THUNDERSTORM'),
|
||||
'MIXED CONDITIONS': msg('MIXED CONDITIONS'),
|
||||
});
|
||||
|
||||
export function showBriefPage(index, { manual = false } = {}) {
|
||||
@@ -80,14 +80,18 @@ export function setBriefAutoRotate(enabled) {
|
||||
// these t() re-writes are same-value overrides that localize the toggle
|
||||
// under other locales (byte-identical under 'en').
|
||||
const labelKey = this.briefAutoRotateEnabled
|
||||
? 'cockpit.brief.autoOn'
|
||||
: 'cockpit.brief.autoOff';
|
||||
? msg('CYCLE ON')
|
||||
: msg('CYCLE OFF');
|
||||
this.briefAutoToggle.textContent = t(labelKey);
|
||||
this.briefAutoToggle.setAttribute('aria-label', t(labelKey));
|
||||
this.briefAutoToggle.title = t(
|
||||
this.briefAutoRotateEnabled
|
||||
? 'cockpit.brief.autoTitleOn'
|
||||
: 'cockpit.brief.autoTitle',
|
||||
? msg(
|
||||
'Stop automatic page cycling. Previous, Next, and the SIG/NEWS/LOCAL tabs stay available.',
|
||||
)
|
||||
: msg(
|
||||
'Cycle briefing pages automatically every 9 seconds (Signals → News → Local). Pauses while you hover or focus the panel. Live signal data refreshes continuously either way.',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (this.briefAutoRotateEnabled) this.startBriefRotation({ reset: true });
|
||||
@@ -127,7 +131,7 @@ export function stopBriefRotation() {
|
||||
export function updateLocalPosition(info) {
|
||||
if (!this.localCoordinates) return;
|
||||
if (!Number.isFinite(info.latitude) || !Number.isFinite(info.longitude)) {
|
||||
this.localCoordinates.textContent = t('cockpit.brief.positionUnavailable');
|
||||
this.localCoordinates.textContent = t('POSITION UNAVAILABLE');
|
||||
return;
|
||||
}
|
||||
const lat = `${Math.abs(info.latitude).toFixed(3)}°${info.latitude >= 0 ? 'N' : 'S'}`;
|
||||
@@ -208,14 +212,14 @@ export function renderRegionalBriefStatus(status, info) {
|
||||
this.newsStatus.dataset.state = status;
|
||||
this.newsStatus.textContent =
|
||||
status === 'loading'
|
||||
? t('cockpit.brief.newsAcquiring')
|
||||
: t('cockpit.brief.newsUnavailable');
|
||||
? t('ACQUIRING REGIONAL NEWS')
|
||||
: t('REGIONAL NEWS UNAVAILABLE');
|
||||
}
|
||||
if (status === 'unavailable') this.newsList?.replaceChildren();
|
||||
if (this.localPlace && status === 'loading')
|
||||
this.localPlace.textContent = t('cockpit.brief.localResolving');
|
||||
this.localPlace.textContent = t('RESOLVING REGION');
|
||||
if (this.localPlace && status === 'unavailable')
|
||||
this.localPlace.textContent = t('cockpit.brief.regionUnavailable');
|
||||
this.localPlace.textContent = t('REGION UNAVAILABLE');
|
||||
this.updateLocalPosition(info);
|
||||
}
|
||||
|
||||
@@ -226,8 +230,8 @@ export function renderRegionalBrief(payload, info) {
|
||||
this.newsStatus.dataset.state = payload?.newsStatus || 'unavailable';
|
||||
this.newsStatus.textContent =
|
||||
payload?.newsStatus === 'empty'
|
||||
? t('cockpit.brief.newsEmpty')
|
||||
: t('cockpit.brief.newsUnavailable');
|
||||
? t('NO RECENT LOCATION MATCHES')
|
||||
: t('REGIONAL NEWS UNAVAILABLE');
|
||||
}
|
||||
if (this.newsList) {
|
||||
this.newsList.replaceChildren(
|
||||
@@ -240,8 +244,8 @@ export function renderRegionalBrief(payload, info) {
|
||||
const title = document.createElement('strong');
|
||||
title.textContent = article.title;
|
||||
const metadata = document.createElement('span');
|
||||
metadata.textContent = t('cockpit.brief.articleMetaTemplate', {
|
||||
domain: article.domain || t('cockpit.brief.metadataSourceFallback'),
|
||||
metadata.textContent = t('{domain} · {age}', {
|
||||
domain: article.domain || t('SOURCE'),
|
||||
age: formatCockpitBriefAge(article.publishedAt),
|
||||
});
|
||||
link.append(title, metadata);
|
||||
@@ -252,9 +256,7 @@ export function renderRegionalBrief(payload, info) {
|
||||
}
|
||||
|
||||
const placeLabel =
|
||||
payload?.place?.label ||
|
||||
payload?.place?.country ||
|
||||
t('cockpit.brief.regionUnavailable');
|
||||
payload?.place?.label || payload?.place?.country || t('REGION UNAVAILABLE');
|
||||
if (this.localPlace) this.localPlace.textContent = placeLabel.toUpperCase();
|
||||
this.updateLocalPosition(info);
|
||||
const weather = payload?.weather;
|
||||
@@ -282,10 +284,10 @@ export function renderRegionalBrief(payload, info) {
|
||||
}
|
||||
if (this.localCloud) {
|
||||
this.localCloud.textContent = Number.isFinite(weather?.cloudCoverPct)
|
||||
? t('cockpit.brief.cloudTemplate', {
|
||||
? t('CLOUD {pct}%', {
|
||||
pct: Math.round(weather.cloudCoverPct),
|
||||
})
|
||||
: t('cockpit.brief.cloudUnknown');
|
||||
: t('CLOUD UNKNOWN');
|
||||
}
|
||||
if (this.localPrecipitation) {
|
||||
this.localPrecipitation.textContent = Number.isFinite(
|
||||
@@ -297,10 +299,8 @@ export function renderRegionalBrief(payload, info) {
|
||||
if (this.signalStream)
|
||||
this.signalStream.dataset.regionalStatus = payload?.status || 'partial';
|
||||
if (this.briefPageIndex === 1 && this.briefSource) {
|
||||
this.briefSource.textContent = t('cockpit.brief.newsSourceLine', {
|
||||
source: String(
|
||||
payload?.newsSource || t('cockpit.brief.kickerNews'),
|
||||
).toUpperCase(),
|
||||
this.briefSource.textContent = t('{source} · LOCATION QUERY', {
|
||||
source: String(payload?.newsSource || t('REGIONAL NEWS')).toUpperCase(),
|
||||
});
|
||||
}
|
||||
this.scheduleContextLayout();
|
||||
|
||||
+19
-19
@@ -1,6 +1,6 @@
|
||||
/** Contact readouts and nearby-signal presentation for the Cockpit controller. */
|
||||
import * as Cesium from 'cesium';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, tn } from '../i18n/index.js';
|
||||
import {
|
||||
bearingBetweenCoordinates,
|
||||
formatCockpitContextScope,
|
||||
@@ -21,8 +21,8 @@ export function updateContext(info, heading) {
|
||||
this.pushCockpitSignal(
|
||||
'context-status',
|
||||
'info',
|
||||
t('cockpit.signal.contextStandby'),
|
||||
t('cockpit.signal.contextStandbyHint'),
|
||||
t('CONTEXT STANDBY'),
|
||||
t('ENABLE GLOBAL CONTEXT FOR PROXIMITY PINGS'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export function updateContext(info, heading) {
|
||||
this.contextUncertainty.textContent =
|
||||
'CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR';
|
||||
this.contextUncertainty.textContent = t(
|
||||
'cockpit.context.uncertaintyContactLost',
|
||||
'CONTACT LOST · LAST KNOWN READOUT · NOT AN ALL-CLEAR',
|
||||
);
|
||||
}
|
||||
// The cue changes the footer's height; re-run layout once on the way in
|
||||
@@ -68,10 +68,10 @@ export function updateContext(info, heading) {
|
||||
this.pushCockpitSignal(
|
||||
'context-status',
|
||||
'warning',
|
||||
t('cockpit.signal.contactLostTitle', {
|
||||
t('CONTACT LOST · {subject}', {
|
||||
subject: snapshot.subject.label || snapshot.subject.id || 'SUBJECT',
|
||||
}),
|
||||
t('cockpit.signal.contactLostDetail'),
|
||||
t('SUBJECT LEFT ITS FEED · READOUT HOLDING LAST KNOWN'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -92,15 +92,15 @@ export function updateContext(info, heading) {
|
||||
const closestLabel = this.services.formatAwarenessLabel(closest);
|
||||
if (this.contextNearestLabel) {
|
||||
this.contextNearestLabel.textContent = closest
|
||||
? t('cockpit.context.nearestTemplate', {
|
||||
? t('{cohort} · {contact}', {
|
||||
cohort: closest.cohort.label.toUpperCase(),
|
||||
contact: closestLabel,
|
||||
})
|
||||
: t('cockpit.context.nearestEmpty');
|
||||
: t('NO AVAILABLE EXAMPLE');
|
||||
this.contextNearestLabel.setAttribute(
|
||||
'aria-label',
|
||||
closest && closestLabel === '—'
|
||||
? t('cockpit.context.nearestUnavailableAria', {
|
||||
? t('{cohort}, Unavailable', {
|
||||
cohort: closest.cohort.label,
|
||||
})
|
||||
: this.contextNearestLabel.textContent,
|
||||
@@ -153,24 +153,24 @@ export function updateContext(info, heading) {
|
||||
else if (Math.abs(relative) < 8) this.contextBearing.textContent = 'AHEAD';
|
||||
else
|
||||
this.contextBearing.textContent = `${relative < 0 ? 'L' : 'R'} ${String(Math.round(Math.abs(relative))).padStart(3, '0')}°`;
|
||||
if (relative === null)
|
||||
this.contextBearing.textContent = t('cockpit.context.bearingNone');
|
||||
if (relative === null) this.contextBearing.textContent = t('BRG —');
|
||||
else if (Math.abs(relative) < 8)
|
||||
this.contextBearing.textContent = t('cockpit.context.bearingAhead');
|
||||
this.contextBearing.textContent = t('AHEAD');
|
||||
else {
|
||||
this.contextBearing.textContent = t('cockpit.context.bearingSide', {
|
||||
side:
|
||||
relative < 0
|
||||
? t('cockpit.context.sideLeft')
|
||||
: t('cockpit.context.sideRight'),
|
||||
this.contextBearing.textContent = t('{side} {angle}', {
|
||||
side: relative < 0 ? t('L') : t('R'),
|
||||
angle: `${String(Math.round(Math.abs(relative))).padStart(3, '0')}°`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.contextUncertainty) {
|
||||
this.contextUncertainty.textContent = unknownCount
|
||||
? t('cockpit.context.uncertaintyInputsUnknown', { count: unknownCount })
|
||||
: t('cockpit.context.uncertaintyInputsCurrent');
|
||||
? tn(
|
||||
unknownCount,
|
||||
'{count} INPUT UNKNOWN · NOT AN ALL-CLEAR',
|
||||
'{count} INPUTS UNKNOWN · NOT AN ALL-CLEAR',
|
||||
)
|
||||
: t('AVAILABLE INPUTS CURRENT · NOT AN ALL-CLEAR');
|
||||
}
|
||||
if (this.contextUpdated) {
|
||||
this.contextUpdated.textContent = Number.isFinite(snapshot.evaluatedAt)
|
||||
|
||||
@@ -32,10 +32,7 @@ export function updateHud(
|
||||
const heading = normalizeHeading(this.heading ?? info.track ?? 0);
|
||||
if (this.callsign) {
|
||||
this.callsign.textContent =
|
||||
info.callsign ||
|
||||
info.registration ||
|
||||
info.icao24 ||
|
||||
t('cockpit.hud.fallbackCallsign');
|
||||
info.callsign || info.registration || info.icao24 || t('AIRCRAFT');
|
||||
}
|
||||
const speedKt = Number.isFinite(info.velocityMps)
|
||||
? info.velocityMps * 1.94384
|
||||
@@ -133,19 +130,20 @@ export function updateHud(
|
||||
// State-sibling keys at the writer: each feed state translates
|
||||
// independently so no state can leak English under other locales.
|
||||
const feedState = this.surfaceAcquiring
|
||||
? t('cockpit.hud.metaFeedAcquiringSurface')
|
||||
? t('ACQUIRING SURFACE')
|
||||
: this.surfaceFallback
|
||||
? t('cockpit.hud.metaFeedSurfaceFallback')
|
||||
? t('SURFACE FALLBACK')
|
||||
: info.stale
|
||||
? t('cockpit.hud.metaFeedStale')
|
||||
: t('cockpit.hud.metaFeedLive');
|
||||
this.aircraftMeta.textContent = t('cockpit.hud.aircraftMetaTemplate', {
|
||||
aircraftClass:
|
||||
info.layerId === 'military'
|
||||
? t('cockpit.hud.metaClassMilitary')
|
||||
: t('cockpit.hud.metaClassCommercial'),
|
||||
feedState,
|
||||
});
|
||||
? t('STALE FEED')
|
||||
: t('LIVE TRACK');
|
||||
this.aircraftMeta.textContent = t(
|
||||
'{aircraftClass} · {feedState} · COURSE ALIGNED',
|
||||
{
|
||||
aircraftClass:
|
||||
info.layerId === 'military' ? t('MILITARY') : t('COMMERCIAL'),
|
||||
feedState,
|
||||
},
|
||||
);
|
||||
}
|
||||
this.updateRoute(info);
|
||||
if (
|
||||
@@ -170,14 +168,13 @@ export function updateRoute(info) {
|
||||
const validDestination =
|
||||
Number.isFinite(destination?.lat) && Number.isFinite(destination?.lon);
|
||||
const routeLabel = (airport) =>
|
||||
[airport?.code, airport?.name].filter(Boolean).join(' · ') ||
|
||||
t('cockpit.route.unknownEndpoint');
|
||||
[airport?.code, airport?.name].filter(Boolean).join(' · ') || t('UNKNOWN');
|
||||
if (this.routeFrom) this.routeFrom.textContent = routeLabel(origin);
|
||||
if (this.routeTo) this.routeTo.textContent = routeLabel(destination);
|
||||
if (this.routeStatus) {
|
||||
this.routeStatus.textContent = validDestination
|
||||
? t('cockpit.route.statusArrowEstimated')
|
||||
: t('cockpit.route.statusUnavailable');
|
||||
? t('ARROW · ESTIMATED DIRECTION')
|
||||
: t('ROUTE DATA UNAVAILABLE');
|
||||
}
|
||||
if (this.route) this.route.hidden = !origin && !destination;
|
||||
if (
|
||||
@@ -211,7 +208,7 @@ export function updateRoute(info) {
|
||||
);
|
||||
}
|
||||
if (this.routeDirectionLabel) {
|
||||
this.routeDirectionLabel.textContent = t('cockpit.route.directionLabel', {
|
||||
this.routeDirectionLabel.textContent = t('DEST {bearing}', {
|
||||
bearing: `${String(Math.round(destinationBearing)).padStart(3, '0')}°`,
|
||||
});
|
||||
}
|
||||
@@ -222,14 +219,12 @@ export function syncWeatherToggle(enabled) {
|
||||
const active = !!enabled;
|
||||
this.weatherToggle.setAttribute('aria-pressed', String(active));
|
||||
const weatherLabel = active
|
||||
? t('cockpit.context.weatherDisableAriaLabel')
|
||||
: t('cockpit.context.weatherEnableAriaLabel');
|
||||
? t('Disable cockpit weather effects')
|
||||
: t('Enable cockpit weather effects');
|
||||
this.weatherToggle.setAttribute('aria-label', weatherLabel);
|
||||
this.weatherToggle.title = weatherLabel;
|
||||
if (this.weatherState) {
|
||||
this.weatherState.textContent = active
|
||||
? t('cockpit.context.weatherStateOn')
|
||||
: t('cockpit.context.weatherStateOff');
|
||||
this.weatherState.textContent = active ? t('ON') : t('OFF');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,19 +243,21 @@ export function setVisionMode(mode, { revealParameters = false } = {}) {
|
||||
const names = {
|
||||
optical: 'Normal',
|
||||
crt: 'CRT',
|
||||
nvg: t('cockpit.vision.styleNameNightVision'),
|
||||
thermal: t('cockpit.vision.styleNameThermal'),
|
||||
anime: t('cockpit.vision.styleNameAnime'),
|
||||
noir: t('cockpit.vision.styleNameNoir'),
|
||||
snow: t('cockpit.vision.styleNameSnow'),
|
||||
nvg: t('Night vision'),
|
||||
thermal: t('Thermal'),
|
||||
anime: t('Anime'),
|
||||
noir: t('Noir'),
|
||||
snow: t('Snow'),
|
||||
};
|
||||
if (this.visionCurrent) {
|
||||
this.visionCurrent.dataset.cockpitVision = next;
|
||||
this.visionCurrent.setAttribute(
|
||||
'aria-label',
|
||||
t('cockpit.vision.currentAriaTemplate', { style: names[next] }),
|
||||
t('Current cockpit vision style: {style}. Activate for next style.', {
|
||||
style: names[next],
|
||||
}),
|
||||
);
|
||||
this.visionCurrent.title = t('cockpit.vision.currentTitleTemplate', {
|
||||
this.visionCurrent.title = t('Current style: {style} — click for next', {
|
||||
style: names[next],
|
||||
});
|
||||
}
|
||||
|
||||
+6
-12
@@ -1,5 +1,5 @@
|
||||
/** Measure Cockpit panels against visible HUD and viewport obstacles. */
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
import {
|
||||
resolveCockpitUtilityAnchor,
|
||||
resolveCockpitUtilityLayout,
|
||||
@@ -45,16 +45,10 @@ export function setContextCollapsed(collapsed) {
|
||||
);
|
||||
this.contextToggle.setAttribute(
|
||||
'aria-label',
|
||||
t(
|
||||
expanded
|
||||
? 'cockpit.context.toggleExpandAriaLabel'
|
||||
: 'cockpit.context.toggleAriaLabel',
|
||||
),
|
||||
t(expanded ? msg('Expand Contact panel') : msg('Collapse Contact panel')),
|
||||
);
|
||||
this.contextToggle.title = t(
|
||||
expanded
|
||||
? 'cockpit.context.toggleExpandTitle'
|
||||
: 'cockpit.context.toggleTitle',
|
||||
expanded ? msg('Expand contact panel') : msg('Collapse contact panel'),
|
||||
);
|
||||
const icon = this.contextToggle.querySelector('.material-symbols-outlined');
|
||||
if (icon) icon.textContent = expanded ? 'chevron_left' : 'chevron_right';
|
||||
@@ -78,12 +72,12 @@ export function setSignalCollapsed(collapsed, { user = false } = {}) {
|
||||
'aria-label',
|
||||
t(
|
||||
expanded
|
||||
? 'cockpit.brief.collapseAriaLabel'
|
||||
: 'cockpit.brief.expandAriaLabel',
|
||||
? msg('Collapse cockpit briefing panel')
|
||||
: msg('Expand cockpit briefing panel'),
|
||||
),
|
||||
);
|
||||
this.signalToggle.title = t(
|
||||
expanded ? 'cockpit.brief.collapseTitle' : 'cockpit.brief.expandTitle',
|
||||
expanded ? msg('Collapse briefing panel') : msg('Expand briefing panel'),
|
||||
);
|
||||
const icon = this.signalToggle.querySelector('.material-symbols-outlined');
|
||||
if (icon)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Pure formatting and fixed presentation constants used by Cockpit components. */
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
|
||||
export const COCKPIT_HEADING_SLEW_DPS = 28;
|
||||
|
||||
@@ -46,21 +46,21 @@ export const COCKPIT_REGIONAL_REFRESH_DISTANCE_M = 25_000;
|
||||
export const COCKPIT_BRIEF_PAGES = [
|
||||
{
|
||||
id: 'signals',
|
||||
kickerKey: 'cockpit.brief.kicker',
|
||||
subtitleKey: 'cockpit.brief.subtitle',
|
||||
sourceKey: 'cockpit.brief.sourceNote',
|
||||
kickerKey: msg('LIVE SIGNALS'),
|
||||
subtitleKey: msg('OBSERVED / MAPPED PINGS'),
|
||||
sourceKey: msg('SOURCE-BACKED EVENTS · NO SYNTHETIC NEWS'),
|
||||
},
|
||||
{
|
||||
id: 'news',
|
||||
kickerKey: 'cockpit.brief.kickerNews',
|
||||
subtitleKey: 'cockpit.brief.subtitleNews',
|
||||
kickerKey: msg('REGIONAL NEWS'),
|
||||
subtitleKey: msg('LATEST LOCATION-MATCHED REPORTING'),
|
||||
// RSS provider attribution is a keep-English boundary string.
|
||||
source: 'GOOGLE NEWS RSS · LOCATION QUERY · RECENT',
|
||||
},
|
||||
{
|
||||
id: 'local',
|
||||
kickerKey: 'cockpit.brief.kickerLocal',
|
||||
subtitleKey: 'cockpit.brief.subtitleLocal',
|
||||
kickerKey: msg('LOCAL INFO'),
|
||||
subtitleKey: msg('PLACE / CONDITIONS / POSITION'),
|
||||
// Data-source attribution is a keep-English boundary string.
|
||||
source: 'NATURAL EARTH · OPEN-METEO · UTC',
|
||||
},
|
||||
@@ -84,17 +84,17 @@ export function isRenderedOnScreen(element) {
|
||||
|
||||
export function formatCockpitBriefAge(value) {
|
||||
const timestamp = Date.parse(value);
|
||||
if (!Number.isFinite(timestamp)) return t('cockpit.brief.age.timeUnknown');
|
||||
if (!Number.isFinite(timestamp)) return t('TIME UNKNOWN');
|
||||
const minutes = Math.max(0, Math.round((Date.now() - timestamp) / 60_000));
|
||||
if (minutes < 60) return t('cockpit.brief.age.minutes', { count: minutes });
|
||||
if (minutes < 60) return t('{count}M AGO', { count: minutes });
|
||||
const hours = Math.round(minutes / 60);
|
||||
return hours < 48
|
||||
? t('cockpit.brief.age.hours', { count: hours })
|
||||
: t('cockpit.brief.age.days', { count: Math.round(hours / 24) });
|
||||
? t('{count}H AGO', { count: hours })
|
||||
: t('{count}D AGO', { count: Math.round(hours / 24) });
|
||||
}
|
||||
|
||||
export function formatCockpitWindDirection(value) {
|
||||
if (!Number.isFinite(value)) return t('cockpit.brief.wind.dirUnknown');
|
||||
if (!Number.isFinite(value)) return t('DIR UNKNOWN');
|
||||
const labels = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
|
||||
const normalized = ((value % 360) + 360) % 360;
|
||||
return `${labels[Math.round(normalized / 45) % labels.length]} · ${Math.round(normalized)}°`;
|
||||
|
||||
+15
-11
@@ -1,5 +1,5 @@
|
||||
/** Reconcile actionable signal rows without disturbing native keyboard focus. */
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, tn } from '../i18n/index.js';
|
||||
|
||||
export function renderCockpitSignals() {
|
||||
if (!this.signalList) return;
|
||||
@@ -58,7 +58,7 @@ export function renderCockpitSignals() {
|
||||
if (item.target) {
|
||||
heading.dataset.signalLayer = item.target.layerId;
|
||||
heading.dataset.signalId = item.target.id;
|
||||
const label = t('cockpit.signal.selectFlightAria', { title: item.title });
|
||||
const label = t('Select flight {title}', { title: item.title });
|
||||
if (heading.getAttribute('aria-label') !== label)
|
||||
heading.setAttribute('aria-label', label);
|
||||
setText(heading.children[0], item.title);
|
||||
@@ -127,11 +127,11 @@ export function updateCockpitSignals(snapshot, unknownCount) {
|
||||
key: `flight:${subject.layerId}:${subject.id}`,
|
||||
tone: 'track',
|
||||
title: subject.label || subject.id,
|
||||
detail: t('cockpit.signal.contactCurrent', {
|
||||
detail: t('{aircraftClass} · CURRENT', {
|
||||
aircraftClass:
|
||||
subject.layerId === 'military'
|
||||
? t('cockpit.signal.classMilitary')
|
||||
: t('cockpit.signal.classCommercial'),
|
||||
? t('MILITARY FLIGHT')
|
||||
: t('COMMERCIAL FLIGHT'),
|
||||
}),
|
||||
target: { layerId: subject.layerId, id: String(subject.id) },
|
||||
distanceM: -1,
|
||||
@@ -151,14 +151,14 @@ export function updateCockpitSignals(snapshot, unknownCount) {
|
||||
// contact reads as its registration here too. Same helper the
|
||||
// Context panel's nearest list uses.
|
||||
title: this.services.formatAwarenessLabel(item),
|
||||
detail: t('cockpit.signal.contactRange', {
|
||||
detail: t('{aircraftClass} · {distance}', {
|
||||
aircraftClass:
|
||||
cohort.id === 'military'
|
||||
? t('cockpit.signal.classMilitary')
|
||||
: t('cockpit.signal.classCommercial'),
|
||||
? 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`
|
||||
: t('cockpit.signal.distanceUnknown'),
|
||||
: t('DISTANCE UNKNOWN'),
|
||||
}),
|
||||
target: { layerId: cohort.id, id: String(id) },
|
||||
distanceM: item.distanceM ?? Infinity,
|
||||
@@ -179,8 +179,12 @@ export function updateCockpitSignals(snapshot, unknownCount) {
|
||||
nextItems.splice(4, Math.max(0, nextItems.length - 4), {
|
||||
key: 'input-status',
|
||||
tone: 'warning',
|
||||
title: t('cockpit.signal.inputsUnknown', { count: unknownCount }),
|
||||
detail: sources || t('cockpit.signal.sourceStatusUnavailable'),
|
||||
title: tn(
|
||||
unknownCount,
|
||||
'{count} INPUT UNKNOWN',
|
||||
'{count} INPUTS UNKNOWN',
|
||||
),
|
||||
detail: sources || t('SOURCE STATUS UNAVAILABLE'),
|
||||
target: null,
|
||||
timestamp:
|
||||
previous.get('input-status')?.timestamp ||
|
||||
|
||||
@@ -59,8 +59,8 @@ export function syncTr3bToggle(info) {
|
||||
this.tr3bToggle.hidden = !icao24;
|
||||
this.tr3bToggle.setAttribute('aria-pressed', converted ? 'true' : 'false');
|
||||
this.tr3bToggle.title = converted
|
||||
? t('cockpit.context.tr3bRestoreTitle')
|
||||
: t('cockpit.context.tr3bTitle');
|
||||
? t('Restore real aircraft')
|
||||
: t('Reclassify as TR-3B');
|
||||
}
|
||||
|
||||
export function syncEntry() {
|
||||
@@ -184,20 +184,17 @@ export function enter() {
|
||||
this.showBriefPage(0);
|
||||
this.startBriefRotation();
|
||||
const trackLabel =
|
||||
info.callsign ||
|
||||
info.registration ||
|
||||
info.icao24 ||
|
||||
t('cockpit.hud.fallbackCallsign');
|
||||
info.callsign || info.registration || info.icao24 || t('AIRCRAFT');
|
||||
const trackHeading = String(
|
||||
Math.round(normalizeHeading(info.track ?? 0)),
|
||||
).padStart(3, '0');
|
||||
this.pushCockpitSignal(
|
||||
'track',
|
||||
'track',
|
||||
t('cockpit.signal.trackAcquired'),
|
||||
t('cockpit.signal.trackDetail', {
|
||||
t('TRACK ACQUIRED'),
|
||||
t('{label} · COURSE {heading}°', {
|
||||
label: trackLabel,
|
||||
heading: `${trackHeading}°`,
|
||||
heading: trackHeading,
|
||||
}),
|
||||
);
|
||||
this.updateHud(info, performance.now(), true);
|
||||
|
||||
+14
-10
@@ -1,4 +1,4 @@
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, tn } from '../i18n/index.js';
|
||||
import {
|
||||
settleUserFacingContextAction,
|
||||
contextModeWord,
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
|
||||
export async function _runUserFacingContextAction(
|
||||
operation,
|
||||
message = t('cockpit.context.toastRestoreFailed'),
|
||||
message = t('Context could not restore every layer; try again'),
|
||||
{ falseIsFailure = true, localizedMessage = null } = {},
|
||||
) {
|
||||
if (this.destroyed) return false;
|
||||
@@ -223,25 +223,29 @@ export function clearSelectedLayers() {
|
||||
const operation = managerOperation
|
||||
.then((result) => {
|
||||
if (result.targetIds.length === 0) {
|
||||
this.showToast(t('layers.clear.toast.noneSelected'));
|
||||
this.showToast(t('No selected data layers'));
|
||||
} else if (result.notClearedIds.length > 0) {
|
||||
this.showToast(
|
||||
t('layers.clear.toast.notCleared', {
|
||||
count: result.notClearedIds.length,
|
||||
}),
|
||||
tn(
|
||||
result.notClearedIds.length,
|
||||
'{count} data layer could not be cleared',
|
||||
'{count} data layers could not be cleared',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
this.showToast(
|
||||
t('layers.clear.toast.cleared', {
|
||||
count: result.clearedIds.length,
|
||||
}),
|
||||
tn(
|
||||
result.clearedIds.length,
|
||||
'Cleared {count} data layer',
|
||||
'Cleared {count} data layers',
|
||||
),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('[Data] clear selected layers failed', error);
|
||||
this.showToast(t('cockpit.actions.clearLayersFailedToast'));
|
||||
this.showToast(t('Selected data layers could not be cleared'));
|
||||
return {
|
||||
targetIds: [],
|
||||
items: [],
|
||||
|
||||
@@ -33,7 +33,7 @@ export function _initGlobalContextPanel() {
|
||||
void this._runUserFacingContextAction(
|
||||
(notificationToken) =>
|
||||
this._selectContextMode(nextMode, { notificationToken }),
|
||||
t('cockpit.context.toastTransitionFailedContacts'),
|
||||
t('Contacts could not complete the requested transition; try again'),
|
||||
).then((succeeded) => {
|
||||
if (
|
||||
!this.destroyed &&
|
||||
@@ -62,7 +62,9 @@ export function _initGlobalContextPanel() {
|
||||
void this._runUserFacingContextAction(
|
||||
(notificationToken) =>
|
||||
this._selectContextMode(nextMode, { notificationToken }),
|
||||
t('cockpit.context.toastTransitionFailedMissions'),
|
||||
t(
|
||||
'Space Missions could not complete the requested transition; try again',
|
||||
),
|
||||
).then((succeeded) => {
|
||||
if (
|
||||
!this.destroyed &&
|
||||
@@ -109,14 +111,16 @@ export function _initGlobalContextPanel() {
|
||||
this.showToast(
|
||||
stats?.statusMessage ||
|
||||
(stats?.status === 'zoom-in'
|
||||
? t('cockpit.context.toastZoomToSearch')
|
||||
: t('cockpit.context.toastInstallationsRefreshed')),
|
||||
? t('Zoom in to search mapped installations')
|
||||
: t('Nearby installations refreshed')),
|
||||
);
|
||||
return true;
|
||||
}, t('cockpit.context.toastInstallationsRefreshFailed')).finally(() => {
|
||||
if (this.destroyed) return;
|
||||
button.setAttribute('aria-disabled', 'false');
|
||||
button.setAttribute('aria-busy', 'false');
|
||||
});
|
||||
}, t('Nearby installations could not be refreshed; try again')).finally(
|
||||
() => {
|
||||
if (this.destroyed) return;
|
||||
button.setAttribute('aria-disabled', 'false');
|
||||
button.setAttribute('aria-busy', 'false');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export function _handleContextLayerChange(change) {
|
||||
},
|
||||
{
|
||||
localizedMessage: t(
|
||||
'cockpit.context.toastMissionsCancelRestoreFailed',
|
||||
'Space Missions cancellation could not restore the previous layer state',
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -94,18 +94,17 @@ export function _handleContextLayerChange(change) {
|
||||
!this._userFacingContextNotificationTokens.has(change.notificationToken)
|
||||
) {
|
||||
this.showToast(
|
||||
change.reason || t('cockpit.context.toastLayerUnavailable'),
|
||||
change.reason ||
|
||||
t('That layer is unavailable in the current Context mode'),
|
||||
);
|
||||
}
|
||||
this._syncContextModeButtons();
|
||||
return;
|
||||
}
|
||||
if (change?.type === 'visibility-failed') {
|
||||
const failureMessage = t('cockpit.context.toastLayerLifecycleFailed', {
|
||||
const failureMessage = t('{layerId} could not {action} cleanly', {
|
||||
layerId: change.layerId,
|
||||
action: change.enabled
|
||||
? t('cockpit.context.actionStart')
|
||||
: t('cockpit.context.actionStop'),
|
||||
action: change.enabled ? t('start') : t('stop'),
|
||||
});
|
||||
// A failed direct Context-shell START has already had its siblings
|
||||
// cleared by the visibility guard. Wait outside the synchronous manager
|
||||
|
||||
@@ -125,12 +125,15 @@ export function connectContextManager(manager) {
|
||||
restoreError,
|
||||
);
|
||||
}
|
||||
return t('cockpit.context.toastStartBlocked', {
|
||||
mode:
|
||||
entryMode === 'space-missions'
|
||||
? t('cockpit.context.modeSpaceMissions')
|
||||
: t('cockpit.context.modeContext'),
|
||||
});
|
||||
return t(
|
||||
'{mode} could not start because another layer did not stop cleanly',
|
||||
{
|
||||
mode:
|
||||
entryMode === 'space-missions'
|
||||
? t('Space Missions')
|
||||
: t('Context'),
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
if (ownsNotificationToken) {
|
||||
this._userFacingContextNotificationTokens.delete(
|
||||
|
||||
+54
-56
@@ -5,49 +5,45 @@ export { layerFeedState } from '../data/feedState.js';
|
||||
import { GUIDANCE_STATUSES } from '../loadingFeedback.js';
|
||||
import { keySetupRequirementEnvVars } from '../keySetupCore.mjs';
|
||||
import { createWeatherPanel } from './weatherPanel.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
|
||||
// Feed-state enum VALUES are machine identifiers (layerFeedState contract, QA
|
||||
// hooks, CSS feed-* classes); only the final label value translates, at this
|
||||
// presentation boundary. See docs/TRANSLATORS.md keep-English boundary.
|
||||
const FEED_STATE_LABEL_KEYS = Object.freeze({
|
||||
nominal: 'layers.status.on',
|
||||
loading: 'layers.status.loading',
|
||||
degraded: 'layers.status.degraded',
|
||||
stale: 'layers.status.stale',
|
||||
partial: 'layers.status.partial',
|
||||
fallback: 'layers.status.fallback',
|
||||
unavailable: 'layers.status.unavailable',
|
||||
nominal: msg('ON'),
|
||||
loading: msg('LOADING'),
|
||||
degraded: msg('DEGRADED'),
|
||||
stale: msg('STALE'),
|
||||
partial: msg('PARTIAL'),
|
||||
fallback: msg('FALLBACK'),
|
||||
unavailable: msg('UNAVAILABLE'),
|
||||
});
|
||||
|
||||
// Layer display names, localized at the same presentation boundary. Registry
|
||||
// ids stay English; a layer without an entry here renders its module name,
|
||||
// falling back to the English panel override when one exists.
|
||||
const LAYER_NAME_KEYS = Object.freeze({
|
||||
flights: 'layers.name.liveFlights',
|
||||
military: 'layers.name.militaryFlights',
|
||||
earthquakes: 'layers.name.earthquakes',
|
||||
satellites: 'layers.name.satellites',
|
||||
'rocket-launches': 'layers.name.rocketLaunches',
|
||||
traffic: 'layers.name.traffic',
|
||||
cctv: 'layers.name.cctv',
|
||||
radio: 'layers.name.radio',
|
||||
bikeshare: 'layers.name.bikeshare',
|
||||
'ais-live-vessels': 'layers.name.aisVessels',
|
||||
'military-installations': 'layers.name.installations',
|
||||
'military-awareness': 'layers.name.globalContext',
|
||||
'local-datacenters': 'layers.name.datacenters',
|
||||
'local-dams': 'layers.name.dams',
|
||||
'telegeography-submarine-cables': 'layers.name.submarineCables',
|
||||
'local-firms': 'layers.name.fires',
|
||||
flights: msg('Live Flights'),
|
||||
military: msg('Military Flights'),
|
||||
earthquakes: msg('Earthquakes (24h)'),
|
||||
satellites: msg('Satellites'),
|
||||
'rocket-launches': msg('Space Missions (30d)'),
|
||||
traffic: msg('Street Traffic'),
|
||||
cctv: msg('CCTV'),
|
||||
radio: msg('Radio'),
|
||||
bikeshare: msg('Bikeshare'),
|
||||
'ais-live-vessels': msg('Live AIS Vessels'),
|
||||
'military-installations': msg('Mapped Installations'),
|
||||
'military-awareness': msg('Global Context'),
|
||||
'local-datacenters': msg('Datacenters'),
|
||||
'local-dams': msg('Dams'),
|
||||
'telegeography-submarine-cables': msg('Submarine Cables'),
|
||||
'local-firms': msg('FIRMS Active Fires'),
|
||||
});
|
||||
|
||||
function lifecycleStateLabel(lifecycleState) {
|
||||
return t(
|
||||
lifecycleState === 'enabling'
|
||||
? 'layers.status.enabling'
|
||||
: 'layers.status.disabling',
|
||||
);
|
||||
return t(lifecycleState === 'enabling' ? msg('ENABLING') : msg('DISABLING'));
|
||||
}
|
||||
const FEED_STATE_LABELS = Object.freeze({
|
||||
nominal: 'ON',
|
||||
@@ -149,7 +145,9 @@ export function layerKeyRequirementTooltip(layer = {}) {
|
||||
const envVars = requiresKeyId
|
||||
? keySetupRequirementEnvVars(requiresKeyId)
|
||||
: null;
|
||||
return envVars ? t('setup.keySetup.requirement', { envVars }) : '';
|
||||
return envVars
|
||||
? t('Needs {envVars} — add it in Provider Settings', { envVars })
|
||||
: '';
|
||||
}
|
||||
|
||||
/** Layer row presentation over supplied state and actions; no layer imports. */
|
||||
@@ -575,26 +573,29 @@ export class LayerPanel {
|
||||
const lifecycleState =
|
||||
layer.lifecycleState || (layer.enabled ? 'enabled' : 'disabled');
|
||||
if (lifecycleState === 'enabling' || lifecycleState === 'disabling') {
|
||||
return t('layers.meta.transitioning', {
|
||||
return t('{state} · {source}', {
|
||||
state: lifecycleStateLabel(lifecycleState),
|
||||
source,
|
||||
});
|
||||
}
|
||||
if (layer.lifecycleUncertain) {
|
||||
return t('layers.meta.uncertainLifecycle', { source });
|
||||
return t(
|
||||
'UNCERTAIN · {source} · lifecycle state requires reconciliation',
|
||||
{ source },
|
||||
);
|
||||
}
|
||||
const presentedError =
|
||||
stats.error || stats.lastError || stats.managerRefreshError;
|
||||
if (presentedError) {
|
||||
if (typeof stats.retryInSec === 'number' && stats.retryInSec > 0) {
|
||||
return t('layers.meta.stateSourceRetry', {
|
||||
return t('{state} · {source} · {detail} · retry {seconds}s', {
|
||||
state: stateLabel,
|
||||
source,
|
||||
detail: presentedError,
|
||||
seconds: stats.retryInSec,
|
||||
});
|
||||
}
|
||||
return t('layers.meta.stateSourceDetail', {
|
||||
return t('{state} · {source} · {detail}', {
|
||||
state: stateLabel,
|
||||
source,
|
||||
detail: presentedError,
|
||||
@@ -609,22 +610,20 @@ export class LayerPanel {
|
||||
) {
|
||||
return `${source} · ${stats.statusMessage.trim()}`;
|
||||
}
|
||||
const ago = stats.lastUpdate
|
||||
? this._timeAgo(stats.lastUpdate)
|
||||
: t('layers.meta.never');
|
||||
const ago = stats.lastUpdate ? this._timeAgo(stats.lastUpdate) : t('never');
|
||||
if (stats.loading) {
|
||||
const loadingLabel =
|
||||
typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()
|
||||
? stats.loadingLabel.trim()
|
||||
: t('layers.meta.loading');
|
||||
return t('layers.meta.sourceLoading', { source, loadingLabel });
|
||||
: t('loading...');
|
||||
return t('{source} · {loadingLabel}', { source, loadingLabel });
|
||||
}
|
||||
if (feedState === 'fallback') {
|
||||
const detail =
|
||||
typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()
|
||||
? stats.loadingLabel.trim()
|
||||
: stats.coverage || ago;
|
||||
return t('layers.meta.stateSourceDetail', {
|
||||
return t('{state} · {source} · {detail}', {
|
||||
state: stateLabel,
|
||||
source,
|
||||
detail,
|
||||
@@ -637,12 +636,12 @@ export class LayerPanel {
|
||||
Number.isInteger(rawRowCount) &&
|
||||
acceptedRowCount >= 0 &&
|
||||
rawRowCount > acceptedRowCount
|
||||
? t('layers.meta.partialCounts', {
|
||||
? t('{accepted} of {raw} records accepted', {
|
||||
accepted: acceptedRowCount,
|
||||
raw: rawRowCount,
|
||||
})
|
||||
: t('layers.meta.partialIncomplete');
|
||||
return t('layers.meta.partialDetail', {
|
||||
: t('incomplete snapshot');
|
||||
return t('{state} · {source} · {detail} · {ago}', {
|
||||
state: stateLabel,
|
||||
source,
|
||||
detail,
|
||||
@@ -651,26 +650,26 @@ export class LayerPanel {
|
||||
}
|
||||
if (feedState === 'stale') {
|
||||
if (typeof stats.retryInSec === 'number' && stats.retryInSec > 0) {
|
||||
return t('layers.meta.stateSourceAgoRetry', {
|
||||
return t('{state} · {source} · {ago} · retrying in {seconds}s', {
|
||||
state: stateLabel,
|
||||
source,
|
||||
ago,
|
||||
seconds: stats.retryInSec,
|
||||
});
|
||||
}
|
||||
return t('layers.meta.stateSourceAgo', {
|
||||
return t('{state} · {source} · {ago}', {
|
||||
state: stateLabel,
|
||||
source,
|
||||
ago,
|
||||
});
|
||||
}
|
||||
if (typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()) {
|
||||
return t('layers.meta.sourceLoading', {
|
||||
return t('{source} · {loadingLabel}', {
|
||||
source,
|
||||
loadingLabel: stats.loadingLabel.trim(),
|
||||
});
|
||||
}
|
||||
return t('layers.meta.sourceAgo', { source, ago });
|
||||
return t('{source} · {ago}', { source, ago });
|
||||
}
|
||||
|
||||
_syncToggleButton(button, layer) {
|
||||
@@ -712,18 +711,18 @@ export class LayerPanel {
|
||||
button.textContent = transitioning
|
||||
? lifecycleStateLabel(layer.lifecycleState)
|
||||
: uncertain
|
||||
? t('layers.status.uncertain')
|
||||
? t('UNCERTAIN')
|
||||
: layer.enabled || unavailable
|
||||
? t(FEED_STATE_LABEL_KEYS[feedState])
|
||||
: t('layers.status.off');
|
||||
: t('OFF');
|
||||
const keyGuidance = unavailable
|
||||
? String(layer.stats.error || t('layers.meta.dataSourceUnavailable'))
|
||||
? String(layer.stats.error || t('Data source unavailable'))
|
||||
: layerKeyRequirementTooltip(layer);
|
||||
// Name the missing key on the control itself: a row reading KEY REQUIRED
|
||||
// without saying WHICH key leaves a dead control and no next step. Empty
|
||||
// when the layer needs no key, or already has one.
|
||||
button.title = keyGuidance;
|
||||
const ariaLabel = t('layers.meta.toggleAriaLabel', {
|
||||
const ariaLabel = t('{name}: {state}', {
|
||||
name: panelLabel(layer),
|
||||
state: button.textContent,
|
||||
});
|
||||
@@ -740,10 +739,9 @@ export class LayerPanel {
|
||||
|
||||
_timeAgo(timestamp) {
|
||||
const diff = Math.floor((Date.now() - timestamp) / 1000);
|
||||
if (diff < 5) return t('layers.meta.justNow');
|
||||
if (diff < 60) return t('layers.meta.secondsAgo', { count: diff });
|
||||
if (diff < 3600)
|
||||
return t('layers.meta.minutesAgo', { count: Math.floor(diff / 60) });
|
||||
return t('layers.meta.hoursAgo', { count: Math.floor(diff / 3600) });
|
||||
if (diff < 5) return t('just now');
|
||||
if (diff < 60) return t('{count}s ago', { count: diff });
|
||||
if (diff < 3600) return t('{count}m ago', { count: Math.floor(diff / 60) });
|
||||
return t('{count}h ago', { count: Math.floor(diff / 3600) });
|
||||
}
|
||||
}
|
||||
@@ -151,16 +151,12 @@ export class LocationControls {
|
||||
// localized static readout (resolved place names stay verbatim data).
|
||||
this.elements.statusCity.textContent =
|
||||
lines.city === LOCATION_MINI_EMPTY.city
|
||||
? t('cockpit.location.miniCityInitial')
|
||||
? t('📍 Location: --')
|
||||
: lines.city;
|
||||
if (lines.poi === LOCATION_MINI_EMPTY.poi) {
|
||||
this.elements.statusPoi.textContent = t(
|
||||
'cockpit.location.miniPoiInitial',
|
||||
);
|
||||
this.elements.statusPoi.textContent = t('Landmark: --');
|
||||
} else if (lines.poi === LOCATION_MINI_SEARCHED_PLACEHOLDER.poi) {
|
||||
this.elements.statusPoi.textContent = t(
|
||||
'cockpit.location.miniSearchedPlaceholder',
|
||||
);
|
||||
this.elements.statusPoi.textContent = t('Searched location');
|
||||
} else {
|
||||
this.elements.statusPoi.textContent = lines.poi;
|
||||
}
|
||||
@@ -173,7 +169,7 @@ export class LocationControls {
|
||||
const icon = this.doc.createElement('span');
|
||||
icon.className = 'orbit-icon';
|
||||
icon.textContent = '↻';
|
||||
this.orbitIndicator.append(icon, ` ${t('cockpit.location.orbitLabel')}`);
|
||||
this.orbitIndicator.append(icon, ` ${t('ORBIT')}`);
|
||||
this.doc.body.appendChild(this.orbitIndicator);
|
||||
}
|
||||
return this.orbitIndicator;
|
||||
|
||||
@@ -89,9 +89,8 @@ export class LocationNavigation {
|
||||
this._collapsePOIRow();
|
||||
this._updateLocationMiniStatus();
|
||||
} else if (change.type === 'missing')
|
||||
this._showToast(t('cockpit.location.toastNotFound'));
|
||||
else if (change.type === 'failed')
|
||||
this._showToast(t('cockpit.location.toastSearchFailed'));
|
||||
this._showToast(t('Location not found'));
|
||||
else if (change.type === 'failed') this._showToast(t('Search failed'));
|
||||
else if (change.type === 'settled')
|
||||
this._settleLocationSearchUi(change.generation);
|
||||
else if (
|
||||
@@ -280,7 +279,7 @@ export class LocationNavigation {
|
||||
|
||||
_toggleOrbit() {
|
||||
if (!this._currentTarget) {
|
||||
this._showToast(t('cockpit.location.toastFlyToPoiFirst'));
|
||||
this._showToast(t('Fly to a POI first'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -382,11 +381,11 @@ export class LocationNavigation {
|
||||
};
|
||||
this._resetGlobeBtn?.setAttribute(
|
||||
'aria-label',
|
||||
t('cockpit.hud.resetGlobeAria'),
|
||||
t('Reset to full globe view'),
|
||||
);
|
||||
this._cockpitResetGlobeBtn?.setAttribute(
|
||||
'aria-label',
|
||||
t('cockpit.hud.resetGlobeCockpitAria'),
|
||||
t('Reset cockpit to full globe view'),
|
||||
);
|
||||
this._globeResetPromise = null;
|
||||
this._cancelGlobeReset = null;
|
||||
@@ -402,11 +401,11 @@ export class LocationNavigation {
|
||||
}, 4200);
|
||||
this._resetGlobeBtn?.setAttribute(
|
||||
'aria-label',
|
||||
t('cockpit.hud.resettingGlobeAria'),
|
||||
t('Resetting to full globe view'),
|
||||
);
|
||||
this._cockpitResetGlobeBtn?.setAttribute(
|
||||
'aria-label',
|
||||
t('cockpit.hud.resettingGlobeCockpitAria'),
|
||||
t('Resetting cockpit to full globe view'),
|
||||
);
|
||||
const target = flyToGlobeView(this.viewer, {
|
||||
onComplete: () => finish(false),
|
||||
|
||||
@@ -29,9 +29,7 @@ export function createMapSourceControls({
|
||||
statusElement.textContent =
|
||||
state.status === 'switching'
|
||||
? '...'
|
||||
: stack?.shortLabel ||
|
||||
stack?.label ||
|
||||
t('cockpit.presets.mapStackFallback');
|
||||
: stack?.shortLabel || stack?.label || t('MAP');
|
||||
statusElement.classList.toggle('warn', !!state.lastError);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-12
@@ -1,5 +1,5 @@
|
||||
import { UiLifetime } from './uiLifetime.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
import { displayPanelScroller } from './displayPanelScroll.js';
|
||||
import { PanelPositionControls } from './panelPositionControls.js';
|
||||
import { PanelLayoutController } from './panelLayoutController.js';
|
||||
@@ -311,26 +311,24 @@ export class PanelChrome {
|
||||
const panelName =
|
||||
panelEl
|
||||
.querySelector('.panel-title, .pp-header-label')
|
||||
?.textContent?.trim() || t('cockpit.panel.fallbackName');
|
||||
?.textContent?.trim() || t('panel');
|
||||
const titleKey = collapsed
|
||||
? 'cockpit.panel.expandTitle'
|
||||
: 'cockpit.panel.collapseTitle';
|
||||
? msg('Expand {name}')
|
||||
: msg('Collapse {name}');
|
||||
btn.title = t(titleKey, { name: panelName });
|
||||
btn.setAttribute('aria-label', t(titleKey, { name: panelName }));
|
||||
if (panelEl.id === 'radio-panel') {
|
||||
// Expand state reuses the layers.* keys seeded on the static button;
|
||||
// the collapse state is runtime-only, so it owns cockpit.* keys.
|
||||
btn.title = t(
|
||||
collapsed
|
||||
? 'layers.radio.expandTitle'
|
||||
: 'cockpit.panel.radioCollapseTitle',
|
||||
collapsed ? msg('Expand Radio') : msg('Collapse Radio'),
|
||||
);
|
||||
btn.setAttribute(
|
||||
'aria-label',
|
||||
t(
|
||||
collapsed
|
||||
? 'layers.radio.expandAriaLabel'
|
||||
: 'cockpit.panel.radioCollapseAria',
|
||||
? msg('Expand Radio section')
|
||||
: msg('Collapse Radio section'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -342,10 +340,10 @@ export class PanelChrome {
|
||||
const panelName =
|
||||
panelEl
|
||||
.querySelector('.panel-title, .location-toolbar-label')
|
||||
?.textContent?.trim() || t('cockpit.panel.fallbackName');
|
||||
?.textContent?.trim() || t('panel');
|
||||
const titleKey = collapsed
|
||||
? 'cockpit.panel.expandTitle'
|
||||
: 'cockpit.panel.collapseTitle';
|
||||
? msg('Expand {name}')
|
||||
: msg('Collapse {name}');
|
||||
dockToggle.setAttribute('aria-expanded', String(!collapsed));
|
||||
dockToggle.setAttribute('aria-label', t(titleKey, { name: panelName }));
|
||||
dockToggle.title = t(titleKey, { name: panelName });
|
||||
|
||||
@@ -220,7 +220,9 @@ export class PanelPositionControls {
|
||||
key.startsWith('godsEyeView.v6.panelPos.'),
|
||||
);
|
||||
if (hadOldPositions) {
|
||||
this._showToast(t('cockpit.panel.layoutResetToast'));
|
||||
this._showToast(
|
||||
t('Panel layout updated — positions reset to new defaults'),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// storage unavailable
|
||||
|
||||
+17
-21
@@ -1,4 +1,4 @@
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
import {
|
||||
radioTunerSlot,
|
||||
radioTunerCommitSlot,
|
||||
@@ -53,14 +53,14 @@ export function bindRadioControls() {
|
||||
'aria-label',
|
||||
t(
|
||||
displayOpen
|
||||
? 'cockpit.utility.displayToggleCollapseAriaLabel'
|
||||
: 'cockpit.utility.displayToggleAriaLabel',
|
||||
? msg('Collapse Cockpit display options')
|
||||
: msg('Expand Cockpit display options'),
|
||||
),
|
||||
);
|
||||
this._cockpitDisplayToggleBtn.title = t(
|
||||
displayOpen
|
||||
? 'cockpit.utility.displayToggleCollapseAriaLabel'
|
||||
: 'cockpit.utility.displayToggleAriaLabel',
|
||||
? msg('Collapse Cockpit display options')
|
||||
: msg('Expand Cockpit display options'),
|
||||
);
|
||||
}
|
||||
if (this._cockpitRadioToggleBtn) {
|
||||
@@ -69,14 +69,14 @@ export function bindRadioControls() {
|
||||
'aria-label',
|
||||
t(
|
||||
radioOpen
|
||||
? 'cockpit.utility.radioToggleCollapseAriaLabel'
|
||||
: 'cockpit.utility.radioToggleAriaLabel',
|
||||
? msg('Collapse Cockpit Radio controls')
|
||||
: msg('Expand Cockpit Radio controls'),
|
||||
),
|
||||
);
|
||||
this._cockpitRadioToggleBtn.title = t(
|
||||
radioOpen
|
||||
? 'cockpit.utility.radioToggleCollapseAriaLabel'
|
||||
: 'cockpit.utility.radioToggleAriaLabel',
|
||||
? msg('Collapse Cockpit Radio controls')
|
||||
: msg('Expand Cockpit Radio controls'),
|
||||
);
|
||||
}
|
||||
if (!expanded && returnFocus) {
|
||||
@@ -155,21 +155,21 @@ export function bindRadioControls() {
|
||||
if (this._radioTunerValue) {
|
||||
this._radioTunerValue.textContent = station
|
||||
? `CH ${String(slot.stationIndex + 1).padStart(2, '0')} / ${String(this._radioTunerStations.length).padStart(2, '0')}`
|
||||
: t('cockpit.radio.tunerNoStations');
|
||||
: t('NO STATIONS');
|
||||
}
|
||||
if (this._radioTunerStation)
|
||||
this._radioTunerStation.textContent =
|
||||
station?.name || t('layers.radio.noStationAvailable');
|
||||
station?.name || t('NO STATION AVAILABLE');
|
||||
if (this._radioTunerSlider) {
|
||||
this._radioTunerSlider.setAttribute(
|
||||
'aria-valuetext',
|
||||
station
|
||||
? t('cockpit.radio.tunerStationAria', {
|
||||
? t('{name}, station {index} of {total}', {
|
||||
name: station.name,
|
||||
index: slot.stationIndex + 1,
|
||||
total: this._radioTunerStations.length,
|
||||
})
|
||||
: t('cockpit.radio.tunerNoStationAria'),
|
||||
: t('No station available'),
|
||||
);
|
||||
}
|
||||
if (syncStatic)
|
||||
@@ -320,11 +320,9 @@ export function bindRadioControls() {
|
||||
this._radioTunerBandPinnedForNavigation = false;
|
||||
if (result.reason === 'station-unavailable') {
|
||||
if (this._radioTunerValue)
|
||||
this._radioTunerValue.textContent = t('cockpit.radio.tunerOffAir');
|
||||
this._radioTunerValue.textContent = t('OFF AIR');
|
||||
if (this._radioTunerStation)
|
||||
this._radioTunerStation.textContent = t(
|
||||
'cockpit.radio.tunerStationUnavailable',
|
||||
);
|
||||
this._radioTunerStation.textContent = t('STATION UNAVAILABLE');
|
||||
this._radioTunerSlider?.setAttribute(
|
||||
'aria-valuetext',
|
||||
'Station unavailable after directory refresh',
|
||||
@@ -367,10 +365,8 @@ export function bindRadioControls() {
|
||||
// (identical value under 'en').
|
||||
`Radio could not ${enabling ? 'start' : 'stop'} cleanly`,
|
||||
{
|
||||
localizedMessage: t('cockpit.radio.toastLifecycleFailed', {
|
||||
action: enabling
|
||||
? t('cockpit.context.actionStart')
|
||||
: t('cockpit.context.actionStop'),
|
||||
localizedMessage: t('Radio could not {action} cleanly', {
|
||||
action: enabling ? t('start') : t('stop'),
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
import { bindRadioControls } from './radioBindings.js';
|
||||
import { renderRadioState } from './radioPresentation.js';
|
||||
|
||||
@@ -201,8 +201,8 @@ export class RadioControls {
|
||||
);
|
||||
const launcherLabel = t(
|
||||
compactOpen
|
||||
? 'cockpit.context.radioToggleCloseAriaLabel'
|
||||
: 'cockpit.context.radioToggleAriaLabel',
|
||||
? msg('Close compact Radio controls')
|
||||
: msg('Open compact Radio controls'),
|
||||
);
|
||||
this._contextRadioToggleBtn.setAttribute('aria-label', launcherLabel);
|
||||
this._contextRadioToggleBtn.title = launcherLabel;
|
||||
|
||||
+49
-58
@@ -35,29 +35,27 @@ export function renderRadioState(state) {
|
||||
if (this._radioLayerState) {
|
||||
// Lifecycle enum values map to status-chip keys; counts stay machine.
|
||||
const lifecycleLabel =
|
||||
lifecycleState === 'enabling'
|
||||
? t('layers.status.enabling')
|
||||
: t('layers.status.disabling');
|
||||
lifecycleState === 'enabling' ? t('ENABLING') : t('DISABLING');
|
||||
this._radioLayerState.textContent = transitioning
|
||||
? lifecycleLabel
|
||||
: uncertain
|
||||
? t('layers.status.uncertain')
|
||||
? t('UNCERTAIN')
|
||||
: state.loading
|
||||
? t('cockpit.radio.stateSync')
|
||||
? t('SYNC')
|
||||
: enabled
|
||||
? `${state.filteredCount}/${state.stationCount}`
|
||||
: t('layers.status.off');
|
||||
: t('OFF');
|
||||
}
|
||||
const enableLabel = uncertain
|
||||
? t('layers.radio.reconcile')
|
||||
? t('RECONCILE')
|
||||
: enabled
|
||||
? t('layers.radio.disable')
|
||||
: t('layers.radio.enable');
|
||||
? t('DISABLE')
|
||||
: t('ENABLE');
|
||||
const enableAria = uncertain
|
||||
? t('layers.radio.reconcileAria')
|
||||
? t('Reconcile Radio — lifecycle uncertain')
|
||||
: enabled
|
||||
? t('layers.radio.disableAria')
|
||||
: t('layers.radio.enableAria');
|
||||
? t('Disable Radio')
|
||||
: t('Enable Radio');
|
||||
if (this._radioEnableBtn) {
|
||||
this._radioEnableBtn.classList.toggle('active', enabled);
|
||||
this._radioEnableBtn.setAttribute('aria-pressed', String(enabled));
|
||||
@@ -146,8 +144,8 @@ export function renderRadioState(state) {
|
||||
);
|
||||
this._radioTunerBandLabel.textContent =
|
||||
state.filter === 'all'
|
||||
? t('layers.radio.bandLabel')
|
||||
: t('cockpit.radio.tunerCategoryBand', {
|
||||
? t('DIRECTORY BAND')
|
||||
: t('{category} BAND', {
|
||||
category: String(
|
||||
activeCategory?.label || state.filter,
|
||||
).toUpperCase(),
|
||||
@@ -170,7 +168,7 @@ export function renderRadioState(state) {
|
||||
|
||||
if (this._radioStationName)
|
||||
this._radioStationName.textContent =
|
||||
selected?.name || t('layers.radio.noStation');
|
||||
selected?.name || t('NO STATION SELECTED');
|
||||
if (this._radioStationMeta) {
|
||||
const place = selected
|
||||
? [selected.state, selected.countryCode].filter(Boolean).join(' · ')
|
||||
@@ -182,15 +180,15 @@ export function renderRadioState(state) {
|
||||
: '';
|
||||
this._radioStationMeta.textContent = selected
|
||||
? [place, signal].filter(Boolean).join(' / ') ||
|
||||
t('layers.radio.meta.metadataOnly')
|
||||
t('Directory metadata only')
|
||||
: state.loading
|
||||
? t('layers.radio.meta.loading')
|
||||
: t('layers.radio.meta.chooseHint');
|
||||
? t('Loading station directory…')
|
||||
: t('Choose a globe marker or use next.');
|
||||
}
|
||||
if (this._radioStationTags) {
|
||||
const tags = Array.isArray(selected?.tags) ? selected.tags.slice(0, 8) : [];
|
||||
this._radioStationTags.textContent = tags.length
|
||||
? t('layers.radio.tags', { tags: tags.join(' · ') })
|
||||
? t('TAGS · {tags}', { tags: tags.join(' · ') })
|
||||
: '';
|
||||
}
|
||||
if (this._radioStationHomepage) {
|
||||
@@ -213,14 +211,12 @@ export function renderRadioState(state) {
|
||||
if (this._cockpitRadioNextBtn)
|
||||
this._cockpitRadioNextBtn.disabled = !interactive || !hasStations;
|
||||
const playbackAction = activePlayback
|
||||
? t('cockpit.radio.actionPause')
|
||||
? t('Pause')
|
||||
: state.audioState === 'paused'
|
||||
? t('cockpit.radio.actionResume')
|
||||
: t('cockpit.radio.actionPlay');
|
||||
const playTargetWord = selected
|
||||
? t('cockpit.radio.targetSelected')
|
||||
: t('cockpit.radio.targetNearest');
|
||||
const playAria = t('cockpit.radio.playStateAria', {
|
||||
? t('Resume')
|
||||
: t('Play');
|
||||
const playTargetWord = selected ? t('selected') : t('nearest');
|
||||
const playAria = t('{action} {target} radio station', {
|
||||
action: playbackAction,
|
||||
target: playTargetWord,
|
||||
});
|
||||
@@ -277,65 +273,60 @@ export function renderRadioState(state) {
|
||||
}
|
||||
if (this._contextRadioMiniStation) {
|
||||
this._contextRadioMiniStation.textContent = uncertain
|
||||
? t('cockpit.radio.miniStateUncertain')
|
||||
? t('RADIO STATE UNCERTAIN')
|
||||
: selected?.name ||
|
||||
(state.loading
|
||||
? t('cockpit.radio.miniSyncingDirectory')
|
||||
: t('cockpit.context.radioMiniReady'));
|
||||
(state.loading ? t('SYNCING DIRECTORY') : t('RADIO READY'));
|
||||
}
|
||||
if (this._cockpitRadioStation) {
|
||||
this._cockpitRadioStation.textContent = uncertain
|
||||
? t('layers.status.uncertain')
|
||||
: selected?.name ||
|
||||
(state.loading
|
||||
? t('cockpit.radio.stationSyncing')
|
||||
: t('cockpit.radio.station.ready'));
|
||||
? t('UNCERTAIN')
|
||||
: selected?.name || (state.loading ? t('SYNCING') : t('READY'));
|
||||
}
|
||||
if (this._radioPlaybackState) {
|
||||
const stationName = selected?.name || t('cockpit.radio.stationFallback');
|
||||
const stationName = selected?.name || t('station');
|
||||
const catalogSuffix = state.degraded
|
||||
? state.stale
|
||||
? t('layers.radio.state.staleDegradedDirectory')
|
||||
: t('layers.radio.state.degradedDirectory')
|
||||
? t(' · stale/degraded directory')
|
||||
: t(' · degraded directory')
|
||||
: state.stale
|
||||
? t('layers.radio.state.staleDirectory')
|
||||
? t(' · stale directory')
|
||||
: '';
|
||||
const outsideFilter =
|
||||
selected && state.selectedIndex < 0
|
||||
? t('layers.radio.state.outsideFilter')
|
||||
: '';
|
||||
selected && state.selectedIndex < 0 ? t(' · outside current filter') : '';
|
||||
const messages = {
|
||||
stopped: enabled
|
||||
? t('layers.radio.state.ready')
|
||||
: t('layers.radio.playbackOff'),
|
||||
loading: t('layers.radio.state.loading'),
|
||||
buffering: t('layers.radio.state.buffering'),
|
||||
playing: t('layers.radio.state.playing', { station: stationName }),
|
||||
paused: t('layers.radio.state.paused', { station: stationName }),
|
||||
error: state.audioError || t('layers.radio.state.error'),
|
||||
? t('Ready — playback starts only from your action')
|
||||
: t('Radio off'),
|
||||
loading: t('Connecting directly to broadcaster…'),
|
||||
buffering: t('Buffering broadcaster stream…'),
|
||||
playing: t('Playing {station}', { station: stationName }),
|
||||
paused: t('Paused {station}', { station: stationName }),
|
||||
error: state.audioError || t('Broadcaster stream unavailable'),
|
||||
};
|
||||
const voiceSuffix = state.voiceDucked
|
||||
? t('layers.radio.state.voiceMuted')
|
||||
? t(' · muted during voice interaction')
|
||||
: state.voiceRestoring
|
||||
? t('layers.radio.state.voiceRestoring')
|
||||
? t(' · restoring volume after voice')
|
||||
: '';
|
||||
const tuningSuffix = state.tuningAwaitingStationId
|
||||
? state.audioState === 'error'
|
||||
? t('layers.radio.state.staticNoAudio')
|
||||
: t('layers.radio.state.tuningStatic')
|
||||
? t(' · static indicates no broadcaster audio')
|
||||
: t(' · tuning static until broadcaster starts')
|
||||
: '';
|
||||
const unavailable = state.tuningUnavailableStationId
|
||||
? t('layers.radio.state.stationUnavailable')
|
||||
? t(
|
||||
'Station unavailable after directory refresh — choose another channel',
|
||||
)
|
||||
: null;
|
||||
const lifecycleMessage = transitioning
|
||||
? lifecycleState === 'enabling'
|
||||
? t('layers.radio.state.enabling')
|
||||
: t('layers.radio.state.disabling')
|
||||
? t('Radio is enabling…')
|
||||
: t('Radio is disabling…')
|
||||
: null;
|
||||
const uncertainMessage = uncertain
|
||||
? t('layers.radio.state.uncertain')
|
||||
? t('Radio lifecycle is uncertain — use Enable or Disable to reconcile')
|
||||
: null;
|
||||
this._radioPlaybackState.textContent = `${uncertainMessage || unavailable || lifecycleMessage || state.error || messages[state.audioState] || t('cockpit.radio.playbackReadyFallback')}${tuningSuffix}${voiceSuffix}${catalogSuffix}${outsideFilter}`;
|
||||
this._radioPlaybackState.textContent = `${uncertainMessage || unavailable || lifecycleMessage || state.error || messages[state.audioState] || t('Ready')}${tuningSuffix}${voiceSuffix}${catalogSuffix}${outsideFilter}`;
|
||||
this._radioPlaybackState.classList.toggle(
|
||||
'error',
|
||||
Boolean(
|
||||
|
||||
@@ -58,7 +58,7 @@ export class SceneControls {
|
||||
if (!this.destroyed) elements.file.value = '';
|
||||
});
|
||||
if (!subscribe) {
|
||||
this.updateStatus(t('setup.scenes.statusReady'));
|
||||
this.updateStatus(t('Ready'));
|
||||
this.setProgress(0);
|
||||
this.setButtons(false);
|
||||
}
|
||||
@@ -124,7 +124,7 @@ export class SceneControls {
|
||||
const generation = ++this.actionGeneration;
|
||||
const failed = () => {
|
||||
if (!this.destroyed && generation === this.actionGeneration)
|
||||
this.updateStatus(t('setup.scenes.status.actionFailed'));
|
||||
this.updateStatus(t('Scene action failed'));
|
||||
};
|
||||
try {
|
||||
const result = this.actions[action](...args);
|
||||
@@ -163,9 +163,7 @@ export class SceneControls {
|
||||
const shot = scene?.shots.find((item) => item.id === shotId);
|
||||
if (
|
||||
shot &&
|
||||
window.confirm(
|
||||
t('setup.scenes.status.deleteShotConfirm', { shot: shot.title }),
|
||||
)
|
||||
window.confirm(t('Delete shot "{shot}"?', { shot: shot.title }))
|
||||
)
|
||||
this.run('deleteShot', sceneId, shotId);
|
||||
}
|
||||
|
||||
@@ -189,12 +189,10 @@ export class ShareRestoration {
|
||||
if (result.classification === 'pending') {
|
||||
this._shareTrackingNoticeGeneration += 1;
|
||||
this._shareTrackingAcquiringKey = trackingKey;
|
||||
this.showStatus(t('cockpit.status.acquiring'), {
|
||||
this.showStatus(t('ACQUIRING'), {
|
||||
state: 'acquiring',
|
||||
detail: t('cockpit.status.sharedSubjectDetail', {
|
||||
subject: String(
|
||||
result.label || t('cockpit.status.subjectFallback'),
|
||||
).toUpperCase(),
|
||||
detail: t('SHARED {subject}', {
|
||||
subject: String(result.label || t('entity')).toUpperCase(),
|
||||
}),
|
||||
persistent: true,
|
||||
});
|
||||
@@ -219,13 +217,15 @@ export class ShareRestoration {
|
||||
const noticeGeneration = ownsAcquiringNotice
|
||||
? this._shareTrackingNoticeGeneration
|
||||
: ++this._shareTrackingNoticeGeneration;
|
||||
const subject = result.label || t('cockpit.status.subjectFallback');
|
||||
const subject = result.label || t('entity');
|
||||
const message =
|
||||
result.classification === 'expired'
|
||||
? t('cockpit.status.sharedFollowExpired', { subject })
|
||||
? t('Shared {subject} follow expired', { subject })
|
||||
: result.classification === 'source-unavailable'
|
||||
? t('cockpit.status.sharedRestoreFailed', { subject })
|
||||
: t('cockpit.status.sharedUnavailable', { subject });
|
||||
? t('Shared {subject} could not be restored — feed unavailable', {
|
||||
subject,
|
||||
})
|
||||
: t('Shared {subject} is unavailable', { subject });
|
||||
const showAfterStartupCover = () => {
|
||||
this._lifetime.frame(() => {
|
||||
if (
|
||||
|
||||
+14
-14
@@ -9,7 +9,7 @@ import {
|
||||
reduceTrafficSyncFeedback,
|
||||
} from '../loadingFeedback.js';
|
||||
import { setSplitFlapText, disposeSplitFlap } from '../splitFlap.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
import { t, msg } from '../i18n/index.js';
|
||||
|
||||
// loadingFeedback.js composes the shared status copy inside the portable
|
||||
// node/browser graph (the server-side HUD summary reads the same modules), so
|
||||
@@ -17,17 +17,17 @@ import { t } from '../i18n/index.js';
|
||||
// the closed label vocabulary to the catalog, and pass anything unrecognized
|
||||
// (a live provider error) through verbatim (docs/TRANSLATORS.md).
|
||||
const STATUS_LABEL_KEYS = Object.freeze({
|
||||
'LOADING LIVE DATA': 'shell.status.loadingLiveData',
|
||||
'LOAD COMPLETE': 'shell.status.loadComplete',
|
||||
'LOAD CANCELLED': 'shell.status.loadCancelled',
|
||||
'LOAD FAILED': 'shell.status.loadFailed',
|
||||
'LIVE DATA OFF': 'shell.status.liveDataOff',
|
||||
'MAPPED SITES LOADED': 'shell.status.mappedSitesLoaded',
|
||||
'RETRYING MAPPED SITES': 'shell.status.retryingMappedSites',
|
||||
'FETCHING MAPPED SITES': 'shell.status.fetchingMappedSites',
|
||||
'TURNING OFF LIVE DATA': 'shell.status.turningOffLiveData',
|
||||
'REFRESHING LIVE DATA': 'shell.status.refreshingLiveData',
|
||||
'syncing road network': 'shell.status.trafficSyncing',
|
||||
'LOADING LIVE DATA': msg('LOADING LIVE DATA'),
|
||||
'LOAD COMPLETE': msg('LOAD COMPLETE'),
|
||||
'LOAD CANCELLED': msg('LOAD CANCELLED'),
|
||||
'LOAD FAILED': msg('LOAD FAILED'),
|
||||
'LIVE DATA OFF': msg('LIVE DATA OFF'),
|
||||
'MAPPED SITES LOADED': msg('MAPPED SITES LOADED'),
|
||||
'RETRYING MAPPED SITES': msg('RETRYING MAPPED SITES'),
|
||||
'FETCHING MAPPED SITES': msg('FETCHING MAPPED SITES'),
|
||||
'TURNING OFF LIVE DATA': msg('TURNING OFF LIVE DATA'),
|
||||
'REFRESHING LIVE DATA': msg('REFRESHING LIVE DATA'),
|
||||
'syncing road network': msg('syncing road network'),
|
||||
});
|
||||
|
||||
function localizeStatusLabel(label) {
|
||||
@@ -41,8 +41,8 @@ const RETRY_DETAIL_RE = /^(?:ALPR cameras · )?retrying in (\d+)s$/;
|
||||
function localizeStatusDetail(detail) {
|
||||
if (typeof detail !== 'string' || !detail) return detail;
|
||||
const match = detail.match(RETRY_DETAIL_RE);
|
||||
if (match) return t('shell.status.retryingIn', { seconds: match[1] });
|
||||
if (detail.trim() === 'retry pending') return t('shell.status.retryPending');
|
||||
if (match) return t('retrying in {seconds}s', { seconds: match[1] });
|
||||
if (detail.trim() === 'retry pending') return t('retry pending');
|
||||
return detail;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- Aircraft first-person view. It can be entered from the visible view switcher. -->
|
||||
<section id="cockpit-hud" aria-label="Aircraft cockpit view" hidden data-i18n-aria-label="cockpit.hud.sectionLabel">
|
||||
<section id="cockpit-hud" aria-label="Aircraft cockpit view" hidden data-i18n-aria-label>
|
||||
<!-- One optical coordinate system: every targeting mark shares this center.
|
||||
Decorative only; telemetry and interactive panels remain separate. -->
|
||||
<svg class="cockpit-frame-lines" viewBox="0 0 1200 720" preserveAspectRatio="none" aria-hidden="true" focusable="false">
|
||||
@@ -61,19 +61,19 @@
|
||||
<span>+20</span><span>+10</span><span class="active">00</span><span>−10</span><span>−20</span>
|
||||
</div>
|
||||
<div class="cockpit-horizon-guide">
|
||||
<span data-i18n="cockpit.hud.level"></span><small>LEVEL</small><span></span>
|
||||
<span></span><small data-i18n>LEVEL</small><span></span>
|
||||
</div>
|
||||
<div id="cockpit-route-direction" class="cockpit-route-direction" hidden aria-label="Estimated destination direction" data-i18n-aria-label="cockpit.hud.routeDirectionAriaLabel">
|
||||
<div id="cockpit-route-direction" class="cockpit-route-direction" hidden aria-label="Estimated destination direction" data-i18n-aria-label>
|
||||
<svg class="cockpit-route-chevron" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M12 2 4.58 19.93c-.34.83.51 1.63 1.31 1.27L12 18.47l6.11 2.73c.8.36 1.65-.44 1.31-1.27L12 2Z" />
|
||||
</svg>
|
||||
<small id="cockpit-route-direction-label" data-i18n="cockpit.hud.routeDirectionIdle">DEST ---°</small>
|
||||
<small id="cockpit-route-direction-label" data-i18n>DEST ---°</small>
|
||||
</div>
|
||||
<div class="cockpit-visor-status cockpit-visor-status-left" data-i18n="cockpit.hud.visorPlane">OPTICAL PLANE · 01</div>
|
||||
<div class="cockpit-visor-status cockpit-visor-status-right" data-i18n="cockpit.hud.visorLock">VISOR LOCK · ACTIVE</div>
|
||||
<div class="cockpit-visor-status cockpit-visor-status-left" data-i18n>OPTICAL PLANE · 01</div>
|
||||
<div class="cockpit-visor-status cockpit-visor-status-right" data-i18n>VISOR LOCK · ACTIVE</div>
|
||||
</div>
|
||||
<div id="cockpit-speed-rim" class="cockpit-altitude-rim cockpit-speed-rim">
|
||||
<span class="cockpit-altitude-rim-label" data-i18n="cockpit.readout.rimGroundSpeed">GROUND SPEED · KTS</span>
|
||||
<span class="cockpit-altitude-rim-label" data-i18n>GROUND SPEED · KTS</span>
|
||||
<div class="cockpit-altitude-rim-rail"></div>
|
||||
<div class="cockpit-altitude-rim-scale">
|
||||
<span data-speed-rim-tick><b></b><i></i></span>
|
||||
@@ -92,7 +92,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="cockpit-altitude-rim" class="cockpit-altitude-rim">
|
||||
<span class="cockpit-altitude-rim-label" data-i18n="cockpit.readout.rimAltitude">ALTITUDE · FT</span>
|
||||
<span class="cockpit-altitude-rim-label" data-i18n>ALTITUDE · FT</span>
|
||||
<div class="cockpit-altitude-rim-rail"></div>
|
||||
<div class="cockpit-altitude-rim-scale">
|
||||
<span data-altitude-rim-tick><b></b><i></i></span>
|
||||
@@ -112,36 +112,36 @@
|
||||
</div>
|
||||
<div class="cockpit-topline">
|
||||
<div>
|
||||
<span class="cockpit-kicker" data-i18n="cockpit.hud.firstPerson">FIRST PERSON</span>
|
||||
<span class="cockpit-kicker" data-i18n>FIRST PERSON</span>
|
||||
<strong id="cockpit-callsign">AIRCRAFT</strong>
|
||||
<span id="cockpit-aircraft-meta" class="cockpit-aircraft-meta" data-i18n="cockpit.hud.aircraftMetaInitial">LIVE TRACK · COURSE ALIGNED</span>
|
||||
<span id="cockpit-aircraft-meta" class="cockpit-aircraft-meta" data-i18n>LIVE TRACK · COURSE ALIGNED</span>
|
||||
</div>
|
||||
<div class="cockpit-vision-controls" role="group" aria-label="Cockpit vision style" data-i18n-aria-label="cockpit.hud.visionGroupAriaLabel">
|
||||
<button id="cockpit-vision-previous" class="cockpit-vision-nav" type="button" aria-label="Previous cockpit vision style" title="Previous vision style" data-i18n-aria-label="cockpit.vision.previousAriaLabel" data-i18n-title="cockpit.vision.previousTitle">
|
||||
<div class="cockpit-vision-controls" role="group" aria-label="Cockpit vision style" data-i18n-aria-label>
|
||||
<button id="cockpit-vision-previous" class="cockpit-vision-nav" type="button" aria-label="Previous cockpit vision style" title="Previous vision style" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">chevron_left</span>
|
||||
<small data-i18n="cockpit.vision.previousLabel">PREV</small>
|
||||
<small data-i18n>PREV</small>
|
||||
</button>
|
||||
<button id="cockpit-vision-current" class="cockpit-vision-current" type="button" data-cockpit-vision="optical" aria-label="Current cockpit vision style: NORMAL. Activate for next style." title="Current style: NORMAL — click for next" data-i18n-aria-label="cockpit.vision.currentAriaLabel" data-i18n-title="cockpit.vision.currentTitle">
|
||||
<small data-i18n="cockpit.vision.current">CURRENT</small>
|
||||
<button id="cockpit-vision-current" class="cockpit-vision-current" type="button" data-cockpit-vision="optical" aria-label="Current cockpit vision style: NORMAL. Activate for next style." title="Current style: NORMAL — click for next" data-i18n-aria-label data-i18n-title>
|
||||
<small data-i18n>CURRENT</small>
|
||||
<strong id="cockpit-vision-current-label" aria-live="polite">NORMAL</strong>
|
||||
</button>
|
||||
<button id="cockpit-vision-next" class="cockpit-vision-nav" type="button" aria-label="Next cockpit vision style" title="Next vision style" data-i18n-aria-label="cockpit.vision.nextAriaLabel" data-i18n-title="cockpit.vision.nextTitle">
|
||||
<small data-i18n="cockpit.vision.nextLabel">NEXT</small>
|
||||
<button id="cockpit-vision-next" class="cockpit-vision-nav" type="button" aria-label="Next cockpit vision style" title="Next vision style" data-i18n-aria-label data-i18n-title>
|
||||
<small data-i18n>NEXT</small>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cockpit-reticle material-symbols-outlined" aria-hidden="true">my_location</div>
|
||||
<div class="cockpit-compass" aria-label="Current aircraft heading" data-i18n-aria-label="cockpit.hud.compassAriaLabel">
|
||||
<div class="cockpit-compass" aria-label="Current aircraft heading" data-i18n-aria-label>
|
||||
<div class="cockpit-instrument-row">
|
||||
<div class="cockpit-readout cockpit-speed">
|
||||
<span class="cockpit-readout-label" data-i18n="cockpit.readout.groundSpeed">GROUND SPEED</span>
|
||||
<span class="cockpit-readout-label" data-i18n>GROUND SPEED</span>
|
||||
<strong id="cockpit-speed-value">---</strong>
|
||||
<span class="cockpit-readout-unit">KTS</span>
|
||||
</div>
|
||||
<div class="cockpit-heading-value"><span id="cockpit-heading-value">---</span><small>°</small></div>
|
||||
<div class="cockpit-readout cockpit-altitude">
|
||||
<span class="cockpit-readout-label" data-i18n="cockpit.readout.altitude">ALTITUDE</span>
|
||||
<span class="cockpit-readout-label" data-i18n>ALTITUDE</span>
|
||||
<strong id="cockpit-altitude-value">-----</strong>
|
||||
<span class="cockpit-readout-unit">FT</span>
|
||||
</div>
|
||||
@@ -153,26 +153,26 @@
|
||||
<span id="cockpit-clock">--:--:--Z</span>
|
||||
<span id="cockpit-position">--° · --°</span>
|
||||
</div>
|
||||
<aside id="cockpit-context" class="cockpit-context-window" aria-label="Contact cockpit summary" hidden data-i18n-aria-label="cockpit.context.panelAriaLabel">
|
||||
<aside id="cockpit-context" class="cockpit-context-window" aria-label="Contact cockpit summary" hidden data-i18n-aria-label>
|
||||
<header class="cockpit-context-header">
|
||||
<div>
|
||||
<span class="cockpit-context-kicker">CONTACT</span>
|
||||
<strong id="cockpit-context-subject" data-i18n="cockpit.context.subjectWindow">CONTACTS · 250 KM</strong>
|
||||
<strong id="cockpit-context-subject" data-i18n>CONTACTS · 250 KM</strong>
|
||||
</div>
|
||||
<nav class="cockpit-context-actions" aria-label="Contact navigation" data-i18n-aria-label="cockpit.context.navAriaLabel">
|
||||
<button id="cockpit-context-previous" type="button" aria-label="Previous — prior visited contact in the 250 km window" title="Previous — prior visited contact in the 250 km window" data-i18n-aria-label="cockpit.context.previousAriaLabel" data-i18n-title="cockpit.context.previousTitle">
|
||||
<nav class="cockpit-context-actions" aria-label="Contact navigation" data-i18n-aria-label>
|
||||
<button id="cockpit-context-previous" type="button" aria-label="Previous — prior visited contact in the 250 km window" title="Previous — prior visited contact in the 250 km window" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">skip_previous</span>
|
||||
</button>
|
||||
<button id="cockpit-context-next" type="button" aria-label="Next — nearest unvisited contact in the 250 km window" title="Next — nearest unvisited contact in the 250 km window" data-i18n-aria-label="cockpit.context.nextAriaLabel" data-i18n-title="cockpit.context.nextTitle">
|
||||
<button id="cockpit-context-next" type="button" aria-label="Next — nearest unvisited contact in the 250 km window" title="Next — nearest unvisited contact in the 250 km window" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">skip_next</span>
|
||||
</button>
|
||||
<button id="cockpit-context-toggle" type="button" aria-label="Collapse Contact panel" aria-expanded="true" title="Collapse contact panel" data-i18n-aria-label="cockpit.context.toggleAriaLabel" data-i18n-title="cockpit.context.toggleTitle">
|
||||
<button id="cockpit-context-toggle" type="button" aria-label="Collapse Contact panel" aria-expanded="true" title="Collapse contact panel" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">chevron_left</span>
|
||||
</button>
|
||||
</nav>
|
||||
<span class="cockpit-context-qualifier" data-i18n="cockpit.context.qualifier">CONTEXT ONLY</span>
|
||||
<span class="cockpit-context-qualifier" data-i18n>CONTEXT ONLY</span>
|
||||
</header>
|
||||
<div class="cockpit-context-cohorts" role="list" aria-label="Nearby cohort counts" data-i18n-aria-label="cockpit.context.cohortsAriaLabel">
|
||||
<div class="cockpit-context-cohorts" role="list" aria-label="Nearby cohort counts" data-i18n-aria-label>
|
||||
<div role="listitem" data-context-cohort="flights"><span>FLT</span><strong>—</strong><small>OpenSky</small></div>
|
||||
<div role="listitem" data-context-cohort="military"><span>MIL</span><strong>—</strong><small>adsb.lol</small></div>
|
||||
<div role="listitem" data-context-cohort="ais-live-vessels"><span>AIS</span><strong>—</strong><small>AISStream</small></div>
|
||||
@@ -181,22 +181,21 @@
|
||||
<div class="cockpit-context-nearest">
|
||||
<span id="cockpit-context-direction" class="material-symbols-outlined" aria-hidden="true">navigation</span>
|
||||
<div>
|
||||
<small data-i18n="cockpit.context.nearestLabel">NEAREST OBSERVED / MAPPED</small>
|
||||
<strong id="cockpit-context-nearest-label" data-i18n="cockpit.context.nearestEmpty">NO AVAILABLE EXAMPLE</strong>
|
||||
<small data-i18n>NEAREST OBSERVED / MAPPED</small>
|
||||
<strong id="cockpit-context-nearest-label" data-i18n>NO AVAILABLE EXAMPLE</strong>
|
||||
</div>
|
||||
<span id="cockpit-context-bearing">—</span>
|
||||
<span id="cockpit-context-distance">—</span>
|
||||
</div>
|
||||
<footer class="cockpit-context-footer">
|
||||
<span id="cockpit-context-uncertainty" data-i18n="cockpit.context.uncertaintyNote">AVAILABLE INPUTS ONLY · NOT AN ALL-CLEAR</span>
|
||||
<span id="cockpit-context-uncertainty" data-i18n>AVAILABLE INPUTS ONLY · NOT AN ALL-CLEAR</span>
|
||||
<button
|
||||
id="cockpit-weather-toggle"
|
||||
class="cockpit-context-weather"
|
||||
type="button"
|
||||
aria-label="Enable cockpit weather effects"
|
||||
aria-pressed="false"
|
||||
title="Enable cockpit weather effects"
|
||||
data-i18n-aria-label="cockpit.context.weatherEnableAriaLabel" data-i18n-title="cockpit.context.weatherEnableTitle"
|
||||
title="Enable cockpit weather effects" data-i18n-aria-label data-i18n-title
|
||||
>
|
||||
<span>WX</span>
|
||||
<strong id="cockpit-weather-state">OFF</strong>
|
||||
@@ -204,35 +203,35 @@
|
||||
<time id="cockpit-context-updated">--:--:--Z</time>
|
||||
</footer>
|
||||
</aside>
|
||||
<aside id="cockpit-signal-stream" class="cockpit-signal-window" aria-label="Cockpit briefing carousel" hidden data-i18n-aria-label="cockpit.brief.carouselAriaLabel">
|
||||
<section id="cockpit-route" class="cockpit-route-card" aria-label="Estimated flight plan" hidden data-i18n-aria-label="cockpit.route.cardAriaLabel">
|
||||
<aside id="cockpit-signal-stream" class="cockpit-signal-window" aria-label="Cockpit briefing carousel" hidden data-i18n-aria-label>
|
||||
<section id="cockpit-route" class="cockpit-route-card" aria-label="Estimated flight plan" hidden data-i18n-aria-label>
|
||||
<div class="cockpit-route-heading">
|
||||
<span class="cockpit-route-kicker" data-i18n="cockpit.route.kicker">ESTIMATED FLIGHT PLAN</span>
|
||||
<small id="cockpit-route-status" data-i18n="cockpit.route.statusUnavailable">ROUTE DATA UNAVAILABLE</small>
|
||||
<span class="cockpit-route-kicker" data-i18n>ESTIMATED FLIGHT PLAN</span>
|
||||
<small id="cockpit-route-status" data-i18n>ROUTE DATA UNAVAILABLE</small>
|
||||
</div>
|
||||
<div class="cockpit-route-endpoints">
|
||||
<div class="cockpit-route-airport">
|
||||
<span data-i18n="cockpit.route.fromLabel">FROM</span>
|
||||
<strong id="cockpit-route-from" data-i18n="cockpit.route.unknownEndpoint">UNKNOWN</strong>
|
||||
<span data-i18n>FROM</span>
|
||||
<strong id="cockpit-route-from" data-i18n>UNKNOWN</strong>
|
||||
</div>
|
||||
<span class="cockpit-route-arrow material-symbols-outlined" aria-hidden="true">east</span>
|
||||
<div class="cockpit-route-airport">
|
||||
<span data-i18n="cockpit.route.toLabel">TO</span>
|
||||
<strong id="cockpit-route-to" data-i18n="cockpit.route.unknownEndpoint">UNKNOWN</strong>
|
||||
<span data-i18n>TO</span>
|
||||
<strong id="cockpit-route-to" data-i18n>UNKNOWN</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<header class="cockpit-signal-header">
|
||||
<div class="cockpit-brief-heading">
|
||||
<span id="cockpit-brief-kicker" class="cockpit-signal-kicker"><i aria-hidden="true"></i> LIVE SIGNALS</span>
|
||||
<strong id="cockpit-brief-subtitle" data-i18n="cockpit.brief.subtitle">OBSERVED / MAPPED PINGS</strong>
|
||||
<strong id="cockpit-brief-subtitle" data-i18n>OBSERVED / MAPPED PINGS</strong>
|
||||
</div>
|
||||
<nav class="cockpit-brief-actions" aria-label="Cockpit briefing controls" data-i18n-aria-label="cockpit.brief.actionsAriaLabel">
|
||||
<button id="cockpit-brief-previous" type="button" aria-label="Previous briefing page" title="Previous briefing page" data-i18n-aria-label="cockpit.brief.previousAriaLabel" data-i18n-title="cockpit.brief.previousTitle">
|
||||
<nav class="cockpit-brief-actions" aria-label="Cockpit briefing controls" data-i18n-aria-label>
|
||||
<button id="cockpit-brief-previous" type="button" aria-label="Previous briefing page" title="Previous briefing page" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">chevron_left</span>
|
||||
</button>
|
||||
<span id="cockpit-brief-position" aria-live="polite">1 / 3</span>
|
||||
<button id="cockpit-brief-next" type="button" aria-label="Next briefing page" title="Next briefing page" data-i18n-aria-label="cockpit.brief.nextAriaLabel" data-i18n-title="cockpit.brief.nextTitle">
|
||||
<button id="cockpit-brief-next" type="button" aria-label="Next briefing page" title="Next briefing page" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">chevron_right</span>
|
||||
</button>
|
||||
<button
|
||||
@@ -240,53 +239,52 @@
|
||||
type="button"
|
||||
aria-label="CYCLE OFF"
|
||||
aria-pressed="false"
|
||||
title="Cycle briefing pages automatically every 9 seconds (Signals → News → Local). Pauses while you hover or focus the panel. Live signal data refreshes continuously either way."
|
||||
data-i18n-title="cockpit.brief.autoTitle"
|
||||
title="Cycle briefing pages automatically every 9 seconds (Signals → News → Local). Pauses while you hover or focus the panel. Live signal data refreshes continuously either way." data-i18n-title
|
||||
>CYCLE OFF</button>
|
||||
<button id="cockpit-signal-toggle" type="button" aria-label="Collapse cockpit briefing panel" aria-expanded="true" title="Collapse briefing panel" data-i18n-aria-label="cockpit.brief.collapseAriaLabel" data-i18n-title="cockpit.brief.collapseTitle">
|
||||
<button id="cockpit-signal-toggle" type="button" aria-label="Collapse cockpit briefing panel" aria-expanded="true" title="Collapse briefing panel" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">right_panel_close</span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="cockpit-brief-stage">
|
||||
<section data-cockpit-brief-page="signals" aria-label="Live signals" data-i18n-aria-label="cockpit.brief.signalsAriaLabel">
|
||||
<section data-cockpit-brief-page="signals" aria-label="Live signals" data-i18n-aria-label>
|
||||
<ol id="cockpit-signal-list" class="cockpit-signal-list" aria-live="polite"></ol>
|
||||
</section>
|
||||
<section data-cockpit-brief-page="news" aria-label="Latest regional news" hidden data-i18n-aria-label="cockpit.brief.newsAriaLabel">
|
||||
<div id="cockpit-news-status" class="cockpit-brief-status" data-i18n="cockpit.brief.newsAcquiring">ACQUIRING REGIONAL NEWS</div>
|
||||
<section data-cockpit-brief-page="news" aria-label="Latest regional news" hidden data-i18n-aria-label>
|
||||
<div id="cockpit-news-status" class="cockpit-brief-status" data-i18n>ACQUIRING REGIONAL NEWS</div>
|
||||
<ol id="cockpit-news-list" class="cockpit-news-list" aria-live="polite"></ol>
|
||||
</section>
|
||||
<section data-cockpit-brief-page="local" aria-label="Location-based information" hidden data-i18n-aria-label="cockpit.brief.localAriaLabel">
|
||||
<section data-cockpit-brief-page="local" aria-label="Location-based information" hidden data-i18n-aria-label>
|
||||
<div class="cockpit-local-place">
|
||||
<strong id="cockpit-local-place" data-i18n="cockpit.brief.localResolving">RESOLVING REGION</strong>
|
||||
<strong id="cockpit-local-place" data-i18n>RESOLVING REGION</strong>
|
||||
<small id="cockpit-local-coordinates">--</small>
|
||||
</div>
|
||||
<div class="cockpit-local-grid">
|
||||
<div data-i18n="cockpit.brief.labelTemp"><span>TEMP</span><strong id="cockpit-local-temperature">—</strong><small>OPEN-METEO</small></div>
|
||||
<div data-i18n="cockpit.brief.labelWind"><span>WIND</span><strong id="cockpit-local-wind">—</strong><small id="cockpit-local-wind-direction">—</small></div>
|
||||
<div data-i18n="cockpit.brief.labelSky"><span>SKY</span><strong id="cockpit-local-condition">UNKNOWN</strong><small id="cockpit-local-cloud">—</small></div>
|
||||
<div data-i18n="cockpit.brief.labelPrecip"><span>PRECIP</span><strong id="cockpit-local-precipitation">—</strong><small>MM</small></div>
|
||||
<div><span data-i18n>TEMP</span><strong id="cockpit-local-temperature">—</strong><small>OPEN-METEO</small></div>
|
||||
<div><span data-i18n>WIND</span><strong id="cockpit-local-wind">—</strong><small id="cockpit-local-wind-direction">—</small></div>
|
||||
<div><span data-i18n>SKY</span><strong id="cockpit-local-condition">UNKNOWN</strong><small id="cockpit-local-cloud">—</small></div>
|
||||
<div><span data-i18n>PRECIP</span><strong id="cockpit-local-precipitation">—</strong><small>MM</small></div>
|
||||
</div>
|
||||
<a class="cockpit-weather-credit" href="https://open-meteo.com/" target="_blank" rel="noopener">Weather data by Open-Meteo.com</a>
|
||||
</section>
|
||||
</div>
|
||||
<footer class="cockpit-signal-footer">
|
||||
<span id="cockpit-brief-source" data-i18n="cockpit.brief.sourceNote">SOURCE-BACKED EVENTS · NO SYNTHETIC NEWS</span>
|
||||
<span id="cockpit-brief-source" data-i18n>SOURCE-BACKED EVENTS · NO SYNTHETIC NEWS</span>
|
||||
<nav class="cockpit-brief-tabs" aria-label="Cockpit briefing pages">
|
||||
<button type="button" data-cockpit-brief-index="0" aria-label="Show Live Signals" aria-current="true" data-i18n-aria-label="cockpit.brief.tabSignalsAriaLabel">SIG</button>
|
||||
<button type="button" data-cockpit-brief-index="1" aria-label="Show Regional News" data-i18n-aria-label="cockpit.brief.tabNewsAriaLabel">NEWS</button>
|
||||
<button type="button" data-cockpit-brief-index="2" aria-label="Show Local Info" data-i18n-aria-label="cockpit.brief.tabLocalAriaLabel">LOCAL</button>
|
||||
<button type="button" data-cockpit-brief-index="0" aria-label="Show Live Signals" aria-current="true" data-i18n-aria-label>SIG</button>
|
||||
<button type="button" data-cockpit-brief-index="1" aria-label="Show Regional News" data-i18n-aria-label>NEWS</button>
|
||||
<button type="button" data-cockpit-brief-index="2" aria-label="Show Local Info" data-i18n-aria-label>LOCAL</button>
|
||||
</nav>
|
||||
</footer>
|
||||
</aside>
|
||||
<aside id="cockpit-utility-controls" class="cockpit-utility-controls" aria-label="Cockpit display and Radio controls" data-i18n-aria-label="cockpit.utility.controlsAriaLabel">
|
||||
<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">
|
||||
<div class="cockpit-utility-launcher" data-cockpit-launcher="display">
|
||||
<span data-i18n="cockpit.utility.displayLabel">DISPLAY</span>
|
||||
<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="false" aria-controls="cockpit-display-panel" aria-label="Expand Cockpit display options" title="Expand Cockpit display options" data-i18n-aria-label="cockpit.utility.displayToggleAriaLabel" data-i18n-title="cockpit.utility.displayToggleTitle">◀</button>
|
||||
<button id="cockpit-display-toggle-btn" class="cockpit-utility-glyph" type="button" aria-expanded="false" aria-controls="cockpit-display-panel" aria-label="Expand Cockpit display options" title="Expand Cockpit display options" data-i18n-aria-label data-i18n-title>◀</button>
|
||||
</div>
|
||||
<div id="cockpit-display-panel" class="cockpit-utility-popover" role="group" aria-label="Cockpit display options" hidden data-i18n-aria-label="cockpit.utility.displayPanelAriaLabel">
|
||||
<div id="cockpit-display-panel" class="cockpit-utility-popover" role="group" aria-label="Cockpit display options" hidden 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>
|
||||
@@ -295,21 +293,21 @@
|
||||
</div>
|
||||
<div class="cockpit-utility-control">
|
||||
<div class="cockpit-utility-launcher" data-cockpit-launcher="radio">
|
||||
<span data-i18n="cockpit.utility.radioLabel">RADIO</span>
|
||||
<span data-i18n>RADIO</span>
|
||||
<span class="cockpit-utility-divider" aria-hidden="true"></span>
|
||||
<button id="cockpit-radio-toggle-btn" class="cockpit-utility-glyph" type="button" aria-expanded="false" aria-controls="cockpit-radio-panel" aria-label="Expand Cockpit Radio controls" title="Expand Cockpit Radio controls" data-i18n-aria-label="cockpit.utility.radioToggleAriaLabel" data-i18n-title="cockpit.utility.radioToggleTitle">◀</button>
|
||||
<button id="cockpit-radio-toggle-btn" class="cockpit-utility-glyph" type="button" aria-expanded="false" aria-controls="cockpit-radio-panel" aria-label="Expand Cockpit Radio controls" title="Expand Cockpit Radio controls" data-i18n-aria-label data-i18n-title>◀</button>
|
||||
</div>
|
||||
<div id="cockpit-radio-panel" class="cockpit-utility-popover cockpit-radio-compact" role="group" aria-label="Cockpit compact Radio controls" hidden data-i18n-aria-label="cockpit.utility.radioPanelAriaLabel">
|
||||
<strong id="cockpit-radio-station" class="cockpit-radio-station" data-i18n="cockpit.radio.station.ready">READY</strong>
|
||||
<button id="cockpit-radio-enable-btn" class="panel-layer-toggle" type="button" aria-pressed="false" data-i18n="cockpit.radio.enable">ENABLE</button>
|
||||
<div id="cockpit-radio-panel" class="cockpit-utility-popover cockpit-radio-compact" role="group" aria-label="Cockpit compact Radio controls" hidden data-i18n-aria-label>
|
||||
<strong id="cockpit-radio-station" class="cockpit-radio-station" data-i18n>READY</strong>
|
||||
<button id="cockpit-radio-enable-btn" class="panel-layer-toggle" type="button" aria-pressed="false" data-i18n>ENABLE</button>
|
||||
<div class="cockpit-radio-transport">
|
||||
<button id="cockpit-radio-prev-btn" type="button" aria-label="Previous filtered radio station" disabled data-i18n-aria-label="cockpit.radio.prevAriaLabel">◀◀</button>
|
||||
<button id="cockpit-radio-play-btn" type="button" aria-label="Play selected radio station" disabled data-i18n-aria-label="cockpit.radio.playAriaLabel">▶</button>
|
||||
<button id="cockpit-radio-next-btn" type="button" aria-label="Next filtered radio station" disabled data-i18n-aria-label="cockpit.radio.nextAriaLabel">▶▶</button>
|
||||
<button id="cockpit-radio-prev-btn" type="button" aria-label="Previous filtered radio station" disabled data-i18n-aria-label>◀◀</button>
|
||||
<button id="cockpit-radio-play-btn" type="button" aria-label="Play selected radio station" disabled data-i18n-aria-label>▶</button>
|
||||
<button id="cockpit-radio-next-btn" type="button" aria-label="Next filtered radio station" disabled data-i18n-aria-label>▶▶</button>
|
||||
</div>
|
||||
<label class="cockpit-radio-volume" for="cockpit-radio-volume">
|
||||
<span data-i18n="cockpit.radio.volumeLabel">VOLUME</span>
|
||||
<input id="cockpit-radio-volume" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Cockpit Radio volume" disabled data-i18n-aria-label="cockpit.radio.volumeAriaLabel" />
|
||||
<span data-i18n>VOLUME</span>
|
||||
<input id="cockpit-radio-volume" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Cockpit Radio volume" disabled data-i18n-aria-label />
|
||||
<output id="cockpit-radio-volume-value" class="gev-slider-value" for="cockpit-radio-volume">80%</output>
|
||||
</label>
|
||||
</div>
|
||||
@@ -318,13 +316,13 @@
|
||||
<div class="cockpit-help">ESC EXIT <span>·</span> C TOGGLE</div>
|
||||
</section>
|
||||
|
||||
<nav id="view-switcher" aria-label="View switcher" data-i18n-aria-label="cockpit.exit.navAriaLabel">
|
||||
<button id="cockpit-reset-globe" type="button" aria-label="Reset cockpit to full globe view" title="Exit cockpit and return to full globe view" hidden data-i18n-aria-label="cockpit.exit.resetAriaLabel" data-i18n-title="cockpit.exit.resetTitle">
|
||||
<nav id="view-switcher" aria-label="View switcher" data-i18n-aria-label>
|
||||
<button id="cockpit-reset-globe" type="button" aria-label="Reset cockpit to full globe view" title="Exit cockpit and return to full globe view" hidden data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">public</span>
|
||||
<span data-i18n="cockpit.exit.resetLabel">RESET</span>
|
||||
<span data-i18n>RESET</span>
|
||||
</button>
|
||||
<button id="map-view-switch" type="button" aria-label="Exit cockpit view" aria-keyshortcuts="Escape C" title="Exit cockpit view" hidden data-i18n-aria-label="cockpit.exit.ariaLabel" data-i18n-title="cockpit.exit.title">
|
||||
<button id="map-view-switch" type="button" aria-label="Exit cockpit view" aria-keyshortcuts="Escape C" title="Exit cockpit view" hidden data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">close_fullscreen</span>
|
||||
<span data-i18n="cockpit.exit.label">EXIT COCKPIT</span>
|
||||
<span data-i18n>EXIT COCKPIT</span>
|
||||
</button>
|
||||
</nav>
|
||||
@@ -1,58 +1,53 @@
|
||||
<!-- 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="shell.dock.ariaLabel">
|
||||
<div id="command-dock" 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>
|
||||
<div class="panel-inner">
|
||||
<button id="control-panel-toggle" class="panel-header dock-tray-toggle" type="button"
|
||||
data-dock-toggle-target="control-panel" aria-expanded="false"
|
||||
aria-controls="control-panel-popover" aria-label="Expand Visual Presets" data-i18n-aria-label="cockpit.presets.toggleAriaLabel">
|
||||
aria-controls="control-panel-popover" aria-label="Expand Visual Presets" data-i18n-aria-label>
|
||||
<span class="panel-title"><span class="dock-label-icon dock-label-icon-presets" aria-hidden="true"></span>VISUAL PRESETS</span>
|
||||
<span class="panel-divider"></span>
|
||||
</button>
|
||||
<!-- Language selector: only the group container is static (its
|
||||
data-i18n-aria-label is applied from the catalog at boot). The
|
||||
buttons are rendered at runtime by ui.js _initLocaleSelector(): one
|
||||
per locale in the configured GEV_DEFAULT_LOCALE / GEV_SECONDARY_LOCALE
|
||||
pair plus the always-shipped English fallback, deduped, labels EN/ES/FR
|
||||
with aria-labels from shell.locale.<code>.ariaLabel. -->
|
||||
<div class="dock-locale-switch" role="group" data-i18n-aria-label="shell.locale.groupAriaLabel"></div>
|
||||
<!-- Language selector: buttons are rendered by applicationShell _initLocaleSelector(), one per shipped locale. -->
|
||||
<div class="dock-locale-switch" role="group" data-i18n-aria-label aria-label="Language"></div>
|
||||
<div id="control-panel-popover" class="dock-popover-content">
|
||||
<button class="dock-pin-btn" data-pin-target="control-panel" type="button" aria-label="Pin visual presets" aria-pressed="false" title="Keep visual presets open" data-i18n-aria-label="cockpit.presets.pinAriaLabel" data-i18n-title="cockpit.presets.pinTitle"><img src="/pin.svg" alt="" /></button>
|
||||
<button class="dock-pin-btn" data-pin-target="control-panel" type="button" aria-label="Pin visual presets" aria-pressed="false" title="Keep visual presets open" data-i18n-aria-label data-i18n-title><img src="/pin.svg" alt="" /></button>
|
||||
<div id="style-buttons" class="button-grid">
|
||||
<button class="style-btn active" data-style="normal" title="Show the globe without a visual filter." data-i18n-title="cockpit.presets.styleNormalTitle">
|
||||
<button class="style-btn active" data-style="normal" title="Show the globe without a visual filter." data-i18n-title>
|
||||
<span class="btn-icon">◯</span>
|
||||
<span class="btn-label" data-i18n="cockpit.presets.styleNormalLabel">Normal</span>
|
||||
<span class="btn-label" data-i18n>Normal</span>
|
||||
<span class="btn-key">1</span>
|
||||
</button>
|
||||
<button class="style-btn" data-style="retro" title="Emulate a green phosphor CRT with scanlines and screen curvature." data-i18n-title="cockpit.presets.styleCrtTitle">
|
||||
<button class="style-btn" data-style="retro" title="Emulate a green phosphor CRT with scanlines and screen curvature." data-i18n-title>
|
||||
<span class="btn-icon">▦</span>
|
||||
<span class="btn-label" data-i18n="cockpit.presets.styleCrtLabel">CRT</span>
|
||||
<span class="btn-label" data-i18n>CRT</span>
|
||||
<span class="btn-key">2</span>
|
||||
</button>
|
||||
<button class="style-btn" data-style="surveillance" title="Simulate night-vision goggles with green intensification and a tube vignette." data-i18n-title="cockpit.presets.styleNvgTitle">
|
||||
<button class="style-btn" data-style="surveillance" title="Simulate night-vision goggles with green intensification and a tube vignette." data-i18n-title>
|
||||
<span class="btn-icon">🌙</span>
|
||||
<span class="btn-label" data-i18n="cockpit.presets.styleNvgLabel">NVG</span>
|
||||
<span class="btn-label" data-i18n>NVG</span>
|
||||
<span class="btn-key">3</span>
|
||||
</button>
|
||||
<button class="style-btn" data-style="thermal" title="Simulate FLIR-style thermal contrast. Turn up Ironbow for color." data-i18n-title="cockpit.presets.styleFlirTitle">
|
||||
<button class="style-btn" data-style="thermal" title="Simulate FLIR-style thermal contrast. Turn up Ironbow for color." data-i18n-title>
|
||||
<span class="btn-icon">🌡️</span>
|
||||
<span class="btn-label" data-i18n="cockpit.presets.styleFlirLabel">FLIR</span>
|
||||
<span class="btn-label" data-i18n>FLIR</span>
|
||||
<span class="btn-key">4</span>
|
||||
</button>
|
||||
<button class="style-btn" data-style="anime" title="Apply bright cel-shaded color and illustrated outlines." data-i18n-title="cockpit.presets.styleAnimeTitle">
|
||||
<button class="style-btn" data-style="anime" title="Apply bright cel-shaded color and illustrated outlines." data-i18n-title>
|
||||
<span class="btn-icon">✦</span>
|
||||
<span class="btn-label" data-i18n="cockpit.presets.styleAnimeLabel">Anime</span>
|
||||
<span class="btn-label" data-i18n>Anime</span>
|
||||
<span class="btn-key">5</span>
|
||||
</button>
|
||||
<button class="style-btn" data-style="noir" title="Apply high-contrast monochrome film-noir grading." data-i18n-title="cockpit.presets.styleNoirTitle">
|
||||
<button class="style-btn" data-style="noir" title="Apply high-contrast monochrome film-noir grading." data-i18n-title>
|
||||
<span class="btn-icon">◐</span>
|
||||
<span class="btn-label" data-i18n="cockpit.presets.styleNoirLabel">Noir</span>
|
||||
<span class="btn-label" data-i18n>Noir</span>
|
||||
<span class="btn-key">6</span>
|
||||
</button>
|
||||
<button class="style-btn" data-style="snow" title="Add a cold, snowy whiteout treatment to the scene." data-i18n-title="cockpit.presets.styleSnowTitle">
|
||||
<button class="style-btn" data-style="snow" title="Add a cold, snowy whiteout treatment to the scene." data-i18n-title>
|
||||
<span class="btn-icon">❄</span>
|
||||
<span class="btn-label" data-i18n="cockpit.presets.styleSnowLabel">Snow</span>
|
||||
<span class="btn-label" data-i18n>Snow</span>
|
||||
<span class="btn-key">7</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -65,7 +60,7 @@
|
||||
</section>
|
||||
</div>
|
||||
<div id="style-mini-status" class="panel-mini-status" aria-live="polite">
|
||||
<span class="panel-mini-label" data-i18n="cockpit.presets.miniStyleLabel">Style</span>
|
||||
<span class="panel-mini-label" data-i18n>Style</span>
|
||||
<span id="style-mini-value" class="panel-mini-value">NORMAL</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,26 +71,26 @@
|
||||
<div class="panel-glow"></div>
|
||||
<div class="location-inner">
|
||||
<div class="location-toolbar">
|
||||
<button id="location-bar-toggle" class="dock-tray-toggle" type="button" data-dock-toggle-target="location-bar" aria-controls="location-bar-popover" aria-expanded="false" aria-label="Expand LOCATION" title="Expand LOCATION" data-i18n-aria-label="cockpit.location.toggleAria" data-i18n-title="cockpit.location.toggleAria">
|
||||
<button id="location-bar-toggle" class="dock-tray-toggle" type="button" data-dock-toggle-target="location-bar" aria-controls="location-bar-popover" aria-expanded="false" aria-label="Expand LOCATION" title="Expand LOCATION" data-i18n-aria-label data-i18n-title>
|
||||
<span class="location-toolbar-label"><span class="dock-label-icon dock-label-icon-location" aria-hidden="true"></span>LOCATION</span>
|
||||
</button>
|
||||
<button class="panel-collapse-btn" data-collapse-target="location-bar" title="Collapse panel" data-i18n-title="cockpit.location.collapseTitle">+</button>
|
||||
<button class="panel-collapse-btn" data-collapse-target="location-bar" title="Collapse panel" data-i18n-title>+</button>
|
||||
</div>
|
||||
<div id="location-mini-status" class="location-mini-status" aria-live="polite">
|
||||
<div id="location-mini-city" class="location-mini-city" data-i18n="cockpit.location.miniCityInitial">📍 Location: --</div>
|
||||
<div id="location-mini-poi" class="location-mini-poi" data-i18n="cockpit.location.miniPoiInitial">Landmark: --</div>
|
||||
<div id="location-mini-city" class="location-mini-city" data-i18n>📍 Location: --</div>
|
||||
<div id="location-mini-poi" class="location-mini-poi" data-i18n>Landmark: --</div>
|
||||
</div>
|
||||
<div id="location-bar-popover" class="dock-popover-content">
|
||||
<button class="dock-pin-btn" data-pin-target="location-bar" type="button" aria-label="Pin location tray" aria-pressed="false" title="Keep location tray open" data-i18n-aria-label="cockpit.location.pinAriaLabel" data-i18n-title="cockpit.location.pinTitle"><img src="/pin.svg" alt="" /></button>
|
||||
<button class="dock-pin-btn" data-pin-target="location-bar" type="button" aria-label="Pin location tray" aria-pressed="false" title="Keep location tray open" data-i18n-aria-label data-i18n-title><img src="/pin.svg" alt="" /></button>
|
||||
<div id="poi-row" class="poi-row-container"></div>
|
||||
<div class="location-bar-divider" id="location-bar-divider"></div>
|
||||
<div class="location-city-row">
|
||||
<div id="location-pills" class="location-pills"></div>
|
||||
<div class="location-search-wrap">
|
||||
<button id="search-toggle" class="search-toggle-btn" title="Search any location" data-i18n-title="cockpit.location.searchToggleTitle">
|
||||
<button id="search-toggle" class="search-toggle-btn" title="Search any location" data-i18n-title>
|
||||
<span>🔎</span>
|
||||
</button>
|
||||
<input type="text" id="location-search" placeholder="Search any location..." autocomplete="off" spellcheck="false" aria-label="Search location by name or coordinates" data-i18n-aria-label="cockpit.location.searchAria" data-i18n-placeholder="cockpit.location.searchPlaceholder" />
|
||||
<input type="text" id="location-search" placeholder="Search any location..." autocomplete="off" spellcheck="false" aria-label="Search location by name or coordinates" data-i18n-aria-label data-i18n-placeholder />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,112 +28,112 @@
|
||||
<div class="panel-glow"></div>
|
||||
<div class="global-context-panel-inner" data-rail-scroller>
|
||||
<div class="panel-header">
|
||||
<span class="panel-title" data-i18n="cockpit.context.panelTitle">CONTEXT</span>
|
||||
<span class="panel-title" data-i18n>CONTEXT</span>
|
||||
<span class="panel-divider"></span>
|
||||
<div id="context-radio-dock" class="context-radio-dock">
|
||||
<button id="context-radio-toggle-btn" class="context-radio-toggle" type="button" aria-expanded="false" aria-controls="context-radio-mini" aria-label="Open compact Radio controls" title="Open compact Radio controls" data-i18n-aria-label="cockpit.context.radioToggleAriaLabel" data-i18n-title="cockpit.context.radioToggleTitle">
|
||||
<button id="context-radio-toggle-btn" class="context-radio-toggle" type="button" aria-expanded="false" aria-controls="context-radio-mini" aria-label="Open compact Radio controls" title="Open compact Radio controls" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">radio</span>
|
||||
</button>
|
||||
<div id="context-radio-mini" class="context-radio-mini" role="group" aria-label="Compact Radio controls" hidden data-i18n-aria-label="cockpit.context.radioMiniGroupAriaLabel">
|
||||
<div id="context-radio-mini" class="context-radio-mini" role="group" aria-label="Compact Radio controls" hidden data-i18n-aria-label>
|
||||
<div class="context-radio-mini-heading">
|
||||
<span data-i18n="cockpit.context.radioMiniTitle">RADIO</span>
|
||||
<strong id="context-radio-mini-station" data-i18n="cockpit.context.radioMiniReady">RADIO READY</strong>
|
||||
<span data-i18n>RADIO</span>
|
||||
<strong id="context-radio-mini-station" data-i18n>RADIO READY</strong>
|
||||
<span class="context-radio-mini-actions">
|
||||
<button id="context-radio-details-btn" type="button" aria-label="Open detailed Radio controls" aria-expanded="false" aria-controls="radio-panel" title="Open detailed Radio controls" data-i18n-aria-label="cockpit.context.radioDetailsAriaLabel" data-i18n-title="cockpit.context.radioDetailsTitle">
|
||||
<button id="context-radio-details-btn" type="button" aria-label="Open detailed Radio controls" aria-expanded="false" aria-controls="radio-panel" title="Open detailed Radio controls" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">open_in_full</span>
|
||||
</button>
|
||||
<button id="context-radio-mini-close-btn" type="button" aria-label="Close compact Radio controls" title="Close compact Radio controls" data-i18n-aria-label="cockpit.context.radioCloseAriaLabel" data-i18n-title="cockpit.context.radioCloseTitle">
|
||||
<button id="context-radio-mini-close-btn" type="button" aria-label="Close compact Radio controls" title="Close compact Radio controls" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<button id="context-radio-mini-enable-btn" class="panel-layer-toggle" type="button" aria-pressed="false" data-i18n="cockpit.context.radioMiniEnable">ENABLE</button>
|
||||
<button id="context-radio-mini-enable-btn" class="panel-layer-toggle" type="button" aria-pressed="false" data-i18n>ENABLE</button>
|
||||
<div class="context-radio-mini-transport">
|
||||
<button id="context-radio-mini-prev-btn" type="button" aria-label="Previous filtered radio station" title="Previous station" disabled data-i18n-aria-label="cockpit.context.radioMiniPrevAriaLabel" data-i18n-title="cockpit.context.radioMiniPrevTitle">◀◀</button>
|
||||
<button id="context-radio-mini-play-btn" type="button" aria-label="Play selected radio station" title="Play" disabled data-i18n-aria-label="cockpit.context.radioMiniPlayAriaLabel" data-i18n-title="cockpit.context.radioMiniPlayTitle">▶</button>
|
||||
<button id="context-radio-mini-next-btn" type="button" aria-label="Next filtered radio station" title="Next station" disabled data-i18n-aria-label="cockpit.context.radioMiniNextAriaLabel" data-i18n-title="cockpit.context.radioMiniNextTitle">▶▶</button>
|
||||
<button id="context-radio-mini-prev-btn" type="button" aria-label="Previous filtered radio station" title="Previous station" disabled data-i18n-aria-label data-i18n-title>◀◀</button>
|
||||
<button id="context-radio-mini-play-btn" type="button" aria-label="Play selected radio station" title="Play" disabled data-i18n-aria-label data-i18n-title>▶</button>
|
||||
<button id="context-radio-mini-next-btn" type="button" aria-label="Next filtered radio station" title="Next station" disabled data-i18n-aria-label data-i18n-title>▶▶</button>
|
||||
</div>
|
||||
<label class="context-radio-mini-volume" for="context-radio-mini-volume">
|
||||
<span data-i18n="cockpit.context.radioMiniVolumeLabel">VOLUME</span>
|
||||
<input id="context-radio-mini-volume" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Compact Radio volume" disabled data-i18n-aria-label="cockpit.context.radioMiniVolumeAriaLabel" />
|
||||
<span data-i18n>VOLUME</span>
|
||||
<input id="context-radio-mini-volume" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Compact Radio volume" disabled data-i18n-aria-label />
|
||||
<output id="context-radio-mini-volume-value" class="gev-slider-value" for="context-radio-mini-volume">80%</output>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="panel-collapse-btn" data-collapse-target="global-context-panel" title="Expand panel" data-i18n-title="cockpit.context.collapseTitle">▶</button>
|
||||
<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="global-context-modes" role="tablist" aria-label="Context mode" data-i18n-aria-label="cockpit.context.modesAriaLabel">
|
||||
<button id="global-context-flights-btn" class="context-mode-button" type="button" role="tab" tabindex="0" aria-selected="false" 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="cockpit.context.contactsTabAriaLabel" data-i18n-title="cockpit.context.contactsTabTitle">
|
||||
<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" type="button" role="tab" tabindex="0" aria-selected="false" 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>
|
||||
<span data-i18n="cockpit.context.contactsTabLabel">CONTACTS</span>
|
||||
<span data-i18n>CONTACTS</span>
|
||||
</button>
|
||||
<button id="global-context-missions-btn" class="context-mode-button" type="button" role="tab" tabindex="0" aria-selected="false" aria-controls="context-missions-view">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">rocket_launch</span>
|
||||
<span data-i18n="cockpit.context.missionsTabLabel">SPACE MISSIONS</span>
|
||||
<span data-i18n>SPACE MISSIONS</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="context-mode-standby" class="context-mode-standby">
|
||||
<strong data-i18n="cockpit.context.standbyTitle">SELECT CONTEXT</strong>
|
||||
<strong data-i18n>SELECT CONTEXT</strong>
|
||||
<span>CONTACTS — nearest planes · vessels · sites<br>SPACE MISSIONS — launches & orbital assets</span>
|
||||
</div>
|
||||
<section id="context-flights-view" class="context-mode-view" role="tabpanel" aria-labelledby="global-context-flights-btn" hidden>
|
||||
<div class="global-context-actions" aria-label="Contact Context actions" data-i18n-aria-label="cockpit.context.actionsAriaLabel">
|
||||
<div class="global-context-actions" aria-label="Contact Context actions" data-i18n-aria-label>
|
||||
<button id="cockpit-entry" type="button" hidden>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">flight</span>
|
||||
<span data-i18n="cockpit.context.cockpitEntryLabel">COCKPIT</span>
|
||||
<span data-i18n>COCKPIT</span>
|
||||
</button>
|
||||
<button id="installations-search-btn" class="panel-layer-toggle" type="button" data-i18n="cockpit.context.searchNearbyLabel">SEARCH NEARBY SITES</button>
|
||||
<button id="tr3b-toggle" class="tr3b-toggle" type="button" aria-pressed="false" hidden aria-label="Reclassify tracked contact as TR-3B" title="Reclassify as TR-3B" data-i18n-aria-label="cockpit.context.tr3bAriaLabel" data-i18n-title="cockpit.context.tr3bTitle">🛸</button>
|
||||
<button id="installations-search-btn" class="panel-layer-toggle" type="button" data-i18n>SEARCH NEARBY SITES</button>
|
||||
<button id="tr3b-toggle" class="tr3b-toggle" type="button" aria-pressed="false" hidden aria-label="Reclassify tracked contact as TR-3B" title="Reclassify as TR-3B" data-i18n-aria-label data-i18n-title>🛸</button>
|
||||
</div>
|
||||
<aside id="military-awareness-panel">
|
||||
<div class="military-awareness-standby">
|
||||
<strong data-i18n="cockpit.context.awarenessOffTitle">CONTACTS CONTEXT OFF</strong>
|
||||
<span data-i18n="cockpit.context.awarenessOffHint">SELECT CONTACTS TO LOAD OBSERVED / MAPPED PROXIMITY</span>
|
||||
<strong data-i18n>CONTACTS CONTEXT OFF</strong>
|
||||
<span data-i18n>SELECT CONTACTS TO LOAD OBSERVED / MAPPED PROXIMITY</span>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
<section id="context-missions-view" class="context-mode-view" role="tabpanel" aria-labelledby="global-context-missions-btn" hidden>
|
||||
<div id="space-mission-panel-host">
|
||||
<div id="space-mission-roster" class="space-mission-roster" aria-label="Available Space Missions" data-i18n-aria-label="cockpit.context.rosterAriaLabel">
|
||||
<div id="space-mission-roster" class="space-mission-roster" aria-label="Available Space Missions" data-i18n-aria-label>
|
||||
<div class="space-mission-roster-header">
|
||||
<div>
|
||||
<strong data-i18n="cockpit.context.rosterTitle">AVAILABLE MISSIONS</strong>
|
||||
<span data-i18n="cockpit.context.rosterHint">SELECT A MISSION TO INSPECT</span>
|
||||
<strong data-i18n>AVAILABLE MISSIONS</strong>
|
||||
<span data-i18n>SELECT A MISSION TO INSPECT</span>
|
||||
</div>
|
||||
<output data-mission-roster-count>—</output>
|
||||
</div>
|
||||
<div class="space-mission-roster-list" data-mission-roster-list>
|
||||
<div class="space-mission-roster-empty" data-i18n="cockpit.context.rosterLoading">LOADING 30-DAY MISSION INDEX</div>
|
||||
<div class="space-mission-roster-empty" data-i18n>LOADING 30-DAY MISSION INDEX</div>
|
||||
</div>
|
||||
<p class="space-mission-roster-focus-continuation" tabindex="-1" data-mission-roster-focus-continuation data-i18n="layers.missions.roster.keyboardHint">TAB PREVIEWS · ENTER / SPACE SELECTS</p>
|
||||
<p class="space-mission-roster-focus-continuation" tabindex="-1" data-mission-roster-focus-continuation data-i18n>TAB PREVIEWS · ENTER / SPACE SELECTS</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="radio-panel" class="panel-collapsible collapsed" data-panel-id="radio-panel" aria-label="Internet radio and local SDR" data-i18n-aria-label="layers.radio.panelAriaLabel">
|
||||
<section id="radio-panel" class="panel-collapsible collapsed" data-panel-id="radio-panel" aria-label="Internet radio and local SDR" data-i18n-aria-label>
|
||||
<div class="radio-panel-inner">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title" data-i18n="layers.radio.panelTitle">RADIO</span>
|
||||
<span class="panel-title" data-i18n>RADIO</span>
|
||||
<span id="radio-layer-state" class="radio-layer-state">OFF</span>
|
||||
<span class="panel-divider"></span>
|
||||
<button class="panel-collapse-btn" data-collapse-target="radio-panel" title="Expand Radio" aria-label="Expand Radio section" data-i18n-title="layers.radio.expandTitle" data-i18n-aria-label="layers.radio.expandAriaLabel">+</button>
|
||||
<button class="panel-collapse-btn" data-collapse-target="radio-panel" title="Expand Radio" aria-label="Expand Radio section" data-i18n-title data-i18n-aria-label>+</button>
|
||||
</div>
|
||||
<div class="radio-directory-row">
|
||||
<button id="radio-enable-btn" class="panel-layer-toggle" type="button" aria-pressed="false" data-i18n="layers.radio.enable">ENABLE</button>
|
||||
<label class="radio-filter-label" for="radio-filter" data-i18n="layers.radio.stationTagLabel">STATION TAG</label>
|
||||
<select id="radio-filter" aria-label="Filter stations by station tag" disabled data-i18n-aria-label="layers.radio.filterAriaLabel">
|
||||
<option value="all" data-i18n="layers.radio.filterAll">All</option>
|
||||
<button id="radio-enable-btn" class="panel-layer-toggle" type="button" aria-pressed="false" data-i18n>ENABLE</button>
|
||||
<label class="radio-filter-label" for="radio-filter" data-i18n>STATION TAG</label>
|
||||
<select id="radio-filter" aria-label="Filter stations by station tag" disabled data-i18n-aria-label>
|
||||
<option value="all" data-i18n>All</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="radio-station-card" class="radio-station-card" aria-live="polite">
|
||||
<strong id="radio-station-name" data-i18n="layers.radio.noStation">NO STATION SELECTED</strong>
|
||||
<span id="radio-station-meta" data-i18n="layers.radio.stationHint">Enable Radio, then choose a globe marker or use next.</span>
|
||||
<strong id="radio-station-name" data-i18n>NO STATION SELECTED</strong>
|
||||
<span id="radio-station-meta" data-i18n>Enable Radio, then choose a globe marker or use next.</span>
|
||||
<span id="radio-station-tags"></span>
|
||||
</div>
|
||||
<div id="radio-tuner" class="radio-tuner" hidden>
|
||||
<div class="radio-tuner-header">
|
||||
<span id="radio-tuner-band-label">DIRECTORY BAND</span>
|
||||
<output id="radio-tuner-value" for="radio-tuner-slider" data-i18n="layers.radio.dragToTune">DRAG TO TUNE</output>
|
||||
<output id="radio-tuner-value" for="radio-tuner-slider" data-i18n>DRAG TO TUNE</output>
|
||||
</div>
|
||||
<div class="radio-tuner-dial">
|
||||
<div class="radio-tuner-scale" aria-hidden="true"></div>
|
||||
@@ -142,27 +142,27 @@
|
||||
aria-label="Tune available internet radio stations" aria-valuetext="No station available" disabled />
|
||||
</div>
|
||||
<div class="radio-tuner-readout">
|
||||
<span id="radio-tuner-station" data-i18n="layers.radio.tunerIdle">ALL · DRAG THE NEEDLE</span>
|
||||
<span data-i18n="layers.radio.snapsNote">SNAPS TO AVAILABLE STATIONS</span>
|
||||
<span id="radio-tuner-station" data-i18n>ALL · DRAG THE NEEDLE</span>
|
||||
<span data-i18n>SNAPS TO AVAILABLE STATIONS</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="radio-transport" role="group" aria-label="Radio playback" data-i18n-aria-label="layers.radio.transportAriaLabel">
|
||||
<button id="radio-prev-btn" type="button" aria-label="Previous filtered station" disabled data-i18n-aria-label="layers.radio.prevAriaLabel" data-i18n="layers.radio.prev">PREV</button>
|
||||
<button id="radio-play-btn" type="button" aria-label="Play selected station" disabled data-i18n-aria-label="layers.radio.playAriaLabel" data-i18n="layers.radio.play">PLAY</button>
|
||||
<button id="radio-next-btn" type="button" aria-label="Next filtered station" disabled data-i18n-aria-label="layers.radio.nextAriaLabel" data-i18n="layers.radio.next">NEXT</button>
|
||||
<button id="radio-stop-btn" type="button" aria-label="Stop radio playback" disabled data-i18n-aria-label="layers.radio.stopAriaLabel" data-i18n="layers.radio.stop">STOP</button>
|
||||
<div class="radio-transport" role="group" aria-label="Radio playback" data-i18n-aria-label>
|
||||
<button id="radio-prev-btn" type="button" aria-label="Previous filtered station" disabled data-i18n-aria-label data-i18n>PREV</button>
|
||||
<button id="radio-play-btn" type="button" aria-label="Play selected station" disabled data-i18n-aria-label data-i18n>PLAY</button>
|
||||
<button id="radio-next-btn" type="button" aria-label="Next filtered station" disabled data-i18n-aria-label data-i18n>NEXT</button>
|
||||
<button id="radio-stop-btn" type="button" aria-label="Stop radio playback" disabled data-i18n-aria-label data-i18n>STOP</button>
|
||||
</div>
|
||||
<label class="radio-volume-row" for="radio-volume">
|
||||
<span data-i18n="layers.radio.volumeLabel">VOLUME</span>
|
||||
<input id="radio-volume" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Radio volume" data-i18n-aria-label="layers.radio.volumeAriaLabel" />
|
||||
<span data-i18n>VOLUME</span>
|
||||
<input id="radio-volume" class="gev-quantitative-slider" type="range" min="0" max="100" value="80" aria-label="Radio volume" data-i18n-aria-label />
|
||||
<output id="radio-volume-value" class="gev-slider-value" for="radio-volume">80%</output>
|
||||
</label>
|
||||
<div id="radio-playback-state" class="radio-playback-state" role="status" aria-live="polite" data-i18n="layers.radio.playbackOff">Radio off</div>
|
||||
<div id="radio-playback-state" class="radio-playback-state" role="status" aria-live="polite" data-i18n>Radio off</div>
|
||||
<div class="radio-links">
|
||||
<a id="radio-station-homepage" href="#" target="_blank" rel="noopener noreferrer" hidden data-i18n="layers.radio.stationSite">STATION SITE</a>
|
||||
<a id="radio-station-homepage" href="#" target="_blank" rel="noopener noreferrer" hidden data-i18n>STATION SITE</a>
|
||||
<a href="https://www.radio-browser.info/" target="_blank" rel="noopener noreferrer">DIRECTORY: RADIO BROWSER</a>
|
||||
</div>
|
||||
<p class="radio-privacy" data-i18n="layers.radio.privacyNote">Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.</p>
|
||||
<p class="radio-privacy" data-i18n>Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.</p>
|
||||
<p class="radio-privacy">Audio connects directly to the broadcaster after you press play. Your IP is visible to that broadcaster.</p>
|
||||
<div id="sdr-radio-card" class="sdr-radio-card" role="group" aria-label="Local RTL-SDR receiver">
|
||||
<div class="sdr-radio-heading">
|
||||
|
||||
@@ -3,20 +3,20 @@
|
||||
<div class="compact pp-header-row">
|
||||
<span 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="cockpit.display.collapseTitle">◀</button>
|
||||
<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-toggle-group">
|
||||
<button class="pp-toggle-btn" id="hud-toggle" title="Intelligence HUD (H)" data-i18n-title="cockpit.display.hudToggleTitle">
|
||||
<button class="pp-toggle-btn" id="hud-toggle" title="Intelligence HUD (H)" data-i18n-title>
|
||||
<span class="pp-icon">◎</span>
|
||||
<span class="pp-label">HUD</span>
|
||||
</button>
|
||||
<div class="pp-slider-row" id="hud-layout-row">
|
||||
<span class="pp-slider-mini-label" data-i18n="cockpit.display.hudLayoutLabel">Layout</span>
|
||||
<select id="hud-layout-select" class="pp-select" aria-label="HUD layout" data-i18n-aria-label="cockpit.display.hudLayoutAriaLabel">
|
||||
<option value="tactical" data-i18n="cockpit.display.hudLayoutTactical">Tactical</option>
|
||||
<option value="operator" data-i18n="cockpit.display.hudLayoutOperator">Operator</option>
|
||||
<option value="minimal" data-i18n="cockpit.display.hudLayoutMinimal">Minimal</option>
|
||||
<span class="pp-slider-mini-label" data-i18n>Layout</span>
|
||||
<select id="hud-layout-select" class="pp-select" aria-label="HUD layout" data-i18n-aria-label>
|
||||
<option value="tactical" data-i18n>Tactical</option>
|
||||
<option value="operator" data-i18n>Operator</option>
|
||||
<option value="minimal" data-i18n>Minimal</option>
|
||||
<option value="cyber">Cyber</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -52,39 +52,39 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="pp-toggle-group">
|
||||
<button class="pp-toggle-btn" id="detection-toggle" title="Detection Overlay (D)" aria-label="Detection overlay" aria-pressed="false" data-i18n-title="cockpit.display.detectionToggleTitle" data-i18n-aria-label="cockpit.display.detectionAriaLabel">
|
||||
<button class="pp-toggle-btn" id="detection-toggle" title="Detection Overlay (D)" aria-label="Detection overlay" aria-pressed="false" data-i18n-title data-i18n-aria-label>
|
||||
<span class="pp-icon">▣</span>
|
||||
<span class="pp-label" data-i18n="cockpit.display.detectionLabel">DETECT</span>
|
||||
<span class="pp-label" data-i18n>DETECT</span>
|
||||
</button>
|
||||
<div class="pp-slider-row detection-slider-row" id="detection-slider-row">
|
||||
<label class="pp-slider-mini-label" for="detection-density-slider" data-i18n="cockpit.display.densityLabel">Density</label>
|
||||
<input type="range" class="pp-slider" id="detection-density-slider" min="0" max="100" step="25" value="50" aria-label="Detection label density" data-i18n-aria-label="cockpit.display.densityAriaLabel" />
|
||||
<label class="pp-slider-mini-label" for="detection-density-slider" data-i18n>Density</label>
|
||||
<input type="range" class="pp-slider" id="detection-density-slider" min="0" max="100" step="25" value="50" aria-label="Detection label density" data-i18n-aria-label />
|
||||
<span class="pp-slider-value" id="detection-density-value">50%</span>
|
||||
</div>
|
||||
<div class="pp-slider-row detection-allocation-row" id="detection-allocation-row">
|
||||
<span class="pp-slider-mini-label" data-i18n="cockpit.display.allocationLabel">Allocation</span>
|
||||
<div class="pp-mode-seg detection-allocation-seg" role="radiogroup" aria-label="Detection label allocation" data-i18n-aria-label="cockpit.display.allocationAriaLabel">
|
||||
<button class="pp-mode-btn active" id="detection-allocation-elastic" data-allocation="ELASTIC" role="radio" aria-checked="true" data-i18n="cockpit.display.allocationElastic">Elastic</button>
|
||||
<button class="pp-mode-btn" id="detection-allocation-weighted" data-allocation="WEIGHTED" role="radio" aria-checked="false" data-i18n="cockpit.display.allocationWeighted">Weighted</button>
|
||||
<span class="pp-slider-mini-label" data-i18n>Allocation</span>
|
||||
<div class="pp-mode-seg detection-allocation-seg" role="radiogroup" aria-label="Detection label allocation" data-i18n-aria-label>
|
||||
<button class="pp-mode-btn active" id="detection-allocation-elastic" data-allocation="ELASTIC" role="radio" aria-checked="true" data-i18n>Elastic</button>
|
||||
<button class="pp-mode-btn" id="detection-allocation-weighted" data-allocation="WEIGHTED" role="radio" aria-checked="false" data-i18n>Weighted</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pp-slider-row detection-fade-row" id="detection-fade-row">
|
||||
<label class="pp-slider-mini-label" for="detection-fade-slider" data-i18n="cockpit.display.fadeLabel">Fade</label>
|
||||
<input type="range" class="pp-slider" id="detection-fade-slider" min="0" max="40" step="1" value="7" aria-label="Detection fade distance" title="World-overlay fade distance outside the keyhole as a percentage of its radius" data-i18n-aria-label="cockpit.display.fadeAriaLabel" data-i18n-title="cockpit.display.fadeTitle" />
|
||||
<label class="pp-slider-mini-label" for="detection-fade-slider" data-i18n>Fade</label>
|
||||
<input type="range" class="pp-slider" id="detection-fade-slider" min="0" max="40" step="1" value="7" aria-label="Detection fade distance" title="World-overlay fade distance outside the keyhole as a percentage of its radius" data-i18n-aria-label data-i18n-title />
|
||||
<span class="pp-slider-value" id="detection-fade-value">7%</span>
|
||||
</div>
|
||||
<div class="pp-slider-row detection-opacity-row" id="detection-opacity-row">
|
||||
<label class="pp-slider-mini-label" for="detection-opacity-slider" data-i18n="cockpit.display.outsideLabel">Outside</label>
|
||||
<input type="range" class="pp-slider" id="detection-opacity-slider" min="0" max="100" step="1" value="1" aria-label="Detection opacity outside the keyhole" title="World-overlay label and card opacity beyond the fade distance" data-i18n-aria-label="cockpit.display.outsideAriaLabel" data-i18n-title="cockpit.display.outsideTitle" />
|
||||
<label class="pp-slider-mini-label" for="detection-opacity-slider" data-i18n>Outside</label>
|
||||
<input type="range" class="pp-slider" id="detection-opacity-slider" min="0" max="100" step="1" value="1" aria-label="Detection opacity outside the keyhole" title="World-overlay label and card opacity beyond the fade distance" data-i18n-aria-label data-i18n-title />
|
||||
<span class="pp-slider-value" id="detection-opacity-value">1%</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- The one shared preset-parameter surface is inserted here at runtime. -->
|
||||
<div id="param-slider-panel">
|
||||
<div class="param-panel-header">
|
||||
<span class="param-panel-title" data-i18n="cockpit.display.parametersTitle">PARAMETERS</span>
|
||||
<span class="param-panel-title" data-i18n>PARAMETERS</span>
|
||||
<span class="param-panel-divider"></span>
|
||||
<button class="panel-collapse-btn" id="param-panel-collapse-btn" data-collapse-target="param-slider-panel" title="Collapse panel" data-i18n-title="cockpit.display.parametersCollapseTitle">−</button>
|
||||
<button class="panel-collapse-btn" id="param-panel-collapse-btn" data-collapse-target="param-slider-panel" title="Collapse panel" data-i18n-title>−</button>
|
||||
</div>
|
||||
<div id="param-sliders"></div>
|
||||
</div>
|
||||
@@ -93,31 +93,31 @@
|
||||
button ships lit and the mode row ships open — the same markup-carries-the-default
|
||||
convention as #scope-toggle. ui.js re-syncs both from durable layer state, which
|
||||
is what flips them back for a share link or a stored session carrying 3D off. -->
|
||||
<button class="pp-toggle-btn active" id="models3d-toggle" aria-pressed="true" title="3D aircraft — flat icons zoomed out, 3D models up close" data-i18n-title="cockpit.display.modelsToggleTitle">
|
||||
<button class="pp-toggle-btn active" id="models3d-toggle" aria-pressed="true" title="3D aircraft — flat icons zoomed out, 3D models up close" data-i18n-title>
|
||||
<span class="pp-icon">✈</span>
|
||||
<span class="pp-label">3D</span>
|
||||
</button>
|
||||
<div class="pp-slider-row visible" id="models3d-mode-row">
|
||||
<span class="pp-slider-mini-label" data-i18n="cockpit.display.modelsLabel">Models</span>
|
||||
<div class="pp-mode-seg" role="radiogroup" aria-label="3D model coverage" data-i18n-aria-label="cockpit.display.modelsCoverageAriaLabel">
|
||||
<button class="pp-mode-btn active" id="models3d-mode-proximity" data-mode="proximity" role="radio" aria-checked="true" data-i18n="cockpit.display.modelsModeProximity">Proximity</button>
|
||||
<button class="pp-mode-btn" id="models3d-mode-all" data-mode="all" role="radio" aria-checked="false" data-i18n="cockpit.display.modelsModeAll">All</button>
|
||||
<span class="pp-slider-mini-label" data-i18n>Models</span>
|
||||
<div class="pp-mode-seg" role="radiogroup" aria-label="3D model coverage" data-i18n-aria-label>
|
||||
<button class="pp-mode-btn active" id="models3d-mode-proximity" data-mode="proximity" role="radio" aria-checked="true" data-i18n>Proximity</button>
|
||||
<button class="pp-mode-btn" id="models3d-mode-all" data-mode="all" role="radio" aria-checked="false" data-i18n>All</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pp-toggle-group">
|
||||
<button class="pp-toggle-btn active" id="scope-toggle" title="Scope — the circular viewport mask" aria-pressed="true" data-i18n-title="cockpit.display.scopeToggleTitle">
|
||||
<button class="pp-toggle-btn active" id="scope-toggle" title="Scope — the circular viewport mask" aria-pressed="true" data-i18n-title>
|
||||
<span class="pp-icon material-symbols-outlined" aria-hidden="true">adjust</span>
|
||||
<span class="pp-label" data-i18n="cockpit.display.scopeLabel">Scope</span>
|
||||
<span class="pp-label" data-i18n>Scope</span>
|
||||
</button>
|
||||
<div class="pp-slider-row visible" id="scope-slider-row">
|
||||
<span class="pp-slider-mini-label" data-i18n="cockpit.display.featherLabel">Feather</span>
|
||||
<span class="pp-slider-mini-label" data-i18n>Feather</span>
|
||||
<!-- Feather starts at an 11% soft scope-mask edge (owner final lock
|
||||
2026-08-24, superseding the 08-22 hard-crop and 08-23 8% rulings);
|
||||
mirrors SCOPE_FEATHER_RATIO_DEFAULT in src/scopeMask.js. Feather
|
||||
softens the black mask edge; label fading is the separate Fade
|
||||
slider. -->
|
||||
<input type="range" class="pp-slider" id="scope-feather-slider" min="0" max="100" value="11" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label="cockpit.display.featherAria" data-i18n-title="cockpit.display.featherTitle" />
|
||||
<input type="range" class="pp-slider" id="scope-feather-slider" min="0" max="100" value="11" aria-label="Scope edge feather" title="Scope edge feather as a percentage of the keyhole radius" data-i18n-aria-label data-i18n-title />
|
||||
<span class="pp-slider-value" id="scope-feather-value">11%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,35 +148,35 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="pp-toggle-group">
|
||||
<button class="pp-toggle-btn" id="celestial-toggle" title="Celestial ring — reveal the full globe" aria-pressed="false" data-i18n-title="cockpit.display.celestialToggleTitle">
|
||||
<button class="pp-toggle-btn" id="celestial-toggle" title="Celestial ring — reveal the full globe" aria-pressed="false" data-i18n-title>
|
||||
<span class="pp-icon material-symbols-outlined" aria-hidden="true">flare</span>
|
||||
<span class="pp-label" data-i18n="cockpit.display.celestialLabel">Celestial</span>
|
||||
<span class="pp-label" data-i18n>Celestial</span>
|
||||
</button>
|
||||
</div>
|
||||
<button class="pp-toggle-btn" id="clean-view-toggle" title="Hide UI chrome" data-i18n-title="cockpit.display.cleanViewToggleTitle">
|
||||
<button class="pp-toggle-btn" id="clean-view-toggle" title="Hide UI chrome" data-i18n-title>
|
||||
<span class="pp-icon">□</span>
|
||||
<span class="pp-label" data-i18n="cockpit.display.cleanViewLabel">Clean UI</span>
|
||||
<span class="pp-label" data-i18n>Clean UI</span>
|
||||
</button>
|
||||
<div class="pp-toggle-group">
|
||||
<button class="pp-toggle-btn" id="bloom-toggle" title="Bloom / Glow" data-i18n-title="cockpit.display.bloomToggleTitle">
|
||||
<button class="pp-toggle-btn" id="bloom-toggle" title="Bloom / Glow" data-i18n-title>
|
||||
<span class="pp-icon">✨</span>
|
||||
<span class="pp-label" data-i18n="cockpit.display.bloomLabel">Bloom</span>
|
||||
<span class="pp-label" data-i18n>Bloom</span>
|
||||
</button>
|
||||
<div class="pp-slider-row" id="bloom-slider-row">
|
||||
<input type="range" class="pp-slider" id="bloom-intensity-slider" min="0" max="200" value="0" aria-label="Bloom intensity" data-i18n-aria-label="cockpit.display.bloomAria" />
|
||||
<input type="range" class="pp-slider" id="bloom-intensity-slider" min="0" max="200" value="0" aria-label="Bloom intensity" data-i18n-aria-label />
|
||||
<span class="pp-slider-value" id="bloom-intensity-value">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pp-toggle-group">
|
||||
<button class="pp-toggle-btn" id="sharpen-toggle" title="Sharpening" data-i18n-title="cockpit.display.sharpenToggleTitle">
|
||||
<button class="pp-toggle-btn" id="sharpen-toggle" title="Sharpening" data-i18n-title>
|
||||
<span class="pp-icon">🔍</span>
|
||||
<span class="pp-label" data-i18n="cockpit.display.sharpenLabel">Sharpen</span>
|
||||
<span class="pp-label" data-i18n>Sharpen</span>
|
||||
</button>
|
||||
<div class="pp-slider-row" id="sharpen-slider-row">
|
||||
<input type="range" class="pp-slider" id="sharpen-intensity-slider" min="0" max="100" value="49" aria-label="Sharpen intensity" data-i18n-aria-label="cockpit.display.sharpenAria" />
|
||||
<input type="range" class="pp-slider" id="sharpen-intensity-slider" min="0" max="100" value="49" aria-label="Sharpen intensity" data-i18n-aria-label />
|
||||
<span class="pp-slider-value" id="sharpen-intensity-value">49%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button id="clean-view-exit" title="Return UI controls" data-i18n-title="cockpit.display.cleanViewExitTitle" data-i18n="cockpit.display.cleanViewExitLabel">EXIT CLEAN VIEW</button>
|
||||
<button id="clean-view-exit" title="Return UI controls" data-i18n-title data-i18n>EXIT CLEAN VIEW</button>
|
||||
@@ -6,6 +6,6 @@
|
||||
<div class="loader-content">
|
||||
<span class="loader-logo brand-logo" data-logo-gaze data-logo-src="/logo.svg" aria-hidden="true"><img src="/logo.svg" alt="" /></span>
|
||||
<h2>GOD'S EYE <span class="title-accent">VIEW</span></h2>
|
||||
<p class="loader-status" data-i18n="shell.loading.initialStatus">Initializing photorealistic world...</p>
|
||||
<p class="loader-status" data-i18n>Initializing photorealistic world...</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,9 +4,9 @@
|
||||
<div class="panel-glow"></div>
|
||||
<div class="data-panel-inner">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title" data-i18n="shell.panels.dataLayers">DATA LAYERS</span>
|
||||
<span class="panel-title" data-i18n>DATA LAYERS</span>
|
||||
<span class="panel-divider"></span>
|
||||
<button class="panel-collapse-btn" data-collapse-target="data-panel" title="Collapse panel" data-i18n-title="shell.panels.collapseTitle">+</button>
|
||||
<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>
|
||||
@@ -17,56 +17,56 @@
|
||||
<div class="panel-glow"></div>
|
||||
<div class="cctv-panel-inner" data-rail-scroller>
|
||||
<div class="panel-header">
|
||||
<span class="panel-title" data-i18n="layers.cctv.panelTitle">CCTV</span>
|
||||
<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="layers.cctv.collapseTitle">+</button>
|
||||
<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 id="cctv-frame-wrap" class="cctv-frame-wrap">
|
||||
<img id="cctv-frame" alt="CCTV feed frame" />
|
||||
<canvas id="cctv-video" class="cctv-frame" hidden></canvas>
|
||||
</div>
|
||||
<div id="cctv-source-badge" role="status" aria-live="polite" data-i18n="layers.cctv.sourceUnknown">SOURCE · UNKNOWN</div>
|
||||
<div id="cctv-meta" data-i18n="layers.cctv.metaIdle">Enable CCTV to load camera intersections</div>
|
||||
<div id="cctv-source-badge" role="status" aria-live="polite" data-i18n>SOURCE · UNKNOWN</div>
|
||||
<div id="cctv-meta" data-i18n>Enable CCTV to load camera intersections</div>
|
||||
<div class="cctv-controls">
|
||||
<button id="cctv-enable-btn" class="scene-btn" data-i18n="layers.cctv.toggleOff">CCTV OFF</button>
|
||||
<button id="cctv-nearest-btn" class="scene-btn" data-i18n="layers.cctv.nearest">NEAREST</button>
|
||||
<button id="cctv-enable-btn" class="scene-btn" data-i18n>CCTV OFF</button>
|
||||
<button id="cctv-nearest-btn" class="scene-btn" data-i18n>NEAREST</button>
|
||||
</div>
|
||||
<div class="cctv-controls">
|
||||
<button id="cctv-prev-btn" class="scene-btn" data-i18n="layers.cctv.prev">PREV</button>
|
||||
<select id="cctv-camera-select" aria-label="CCTV camera" data-i18n-aria-label="layers.cctv.cameraAriaLabel"></select>
|
||||
<button id="cctv-next-btn" class="scene-btn" data-i18n="layers.cctv.next">NEXT</button>
|
||||
<button id="cctv-prev-btn" class="scene-btn" data-i18n>PREV</button>
|
||||
<select id="cctv-camera-select" aria-label="CCTV camera" data-i18n-aria-label></select>
|
||||
<button id="cctv-next-btn" class="scene-btn" data-i18n>NEXT</button>
|
||||
</div>
|
||||
<div class="cctv-controls">
|
||||
<button id="cctv-focus-btn" class="scene-btn" data-i18n="layers.cctv.focus">FOCUS</button>
|
||||
<button id="cctv-coverage-btn" class="scene-btn" data-i18n="layers.cctv.coverageOff">COVERAGE OFF</button>
|
||||
<button id="cctv-auto-hop-btn" class="scene-btn" data-i18n="layers.cctv.autoHopOff">AUTO HOP OFF</button>
|
||||
<button id="cctv-focus-btn" class="scene-btn" data-i18n>FOCUS</button>
|
||||
<button id="cctv-coverage-btn" class="scene-btn" data-i18n>COVERAGE OFF</button>
|
||||
<button id="cctv-auto-hop-btn" class="scene-btn" data-i18n>AUTO HOP OFF</button>
|
||||
</div>
|
||||
<div class="cctv-controls">
|
||||
<button id="cctv-projection-btn" class="scene-btn" data-i18n="layers.cctv.projectionOn">PROJECTION ON</button>
|
||||
<button id="cctv-projection-btn" class="scene-btn" data-i18n>PROJECTION ON</button>
|
||||
<span id="cctv-quality-chip">CAL · --</span>
|
||||
</div>
|
||||
<div class="cctv-calibration-block">
|
||||
<div class="cctv-calibration-header">
|
||||
<div class="cctv-summary-label" data-i18n="layers.cctv.calibrationLabel">CALIBRATION</div>
|
||||
<button id="cctv-adjust-btn" class="scene-btn" title="Drag the camera in the world: rings rotate, arrows move, handles set range/FOV" data-i18n-title="layers.cctv.adjustTitle" data-i18n="layers.cctv.adjustLabel">ADJUST</button>
|
||||
<div class="cctv-summary-label" data-i18n>CALIBRATION</div>
|
||||
<button id="cctv-adjust-btn" class="scene-btn" title="Drag the camera in the world: rings rotate, arrows move, handles set range/FOV" data-i18n-title data-i18n>ADJUST</button>
|
||||
</div>
|
||||
<div id="cctv-cal-readout" class="cctv-cal-readout" aria-label="Camera pose — click a value to type" data-i18n-aria-label="layers.cctv.calPoseAriaLabel">
|
||||
<button class="cctv-cal-value" data-cal-field="heading" title="Heading (compass °) — click to type" data-i18n-title="layers.cctv.calHeadingTitle">HDG --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="pitch" title="Pitch (° up/down) — click to type" data-i18n-title="layers.cctv.calPitchTitle">PITCH --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="fov" title="Horizontal FOV (°) — click to type" data-i18n-title="layers.cctv.calFovTitle">FOV --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="range" title="Range / monitor-plane distance (m) — click to type" data-i18n-title="layers.cctv.calRangeTitle">RANGE --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="height" title="Mount height above ground (m) — click to type" data-i18n-title="layers.cctv.calHeightTitle">HGT --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="north" title="North offset from catalog position (m) — click to type" data-i18n-title="layers.cctv.calNorthTitle">ΔN --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="east" title="East offset from catalog position (m) — click to type" data-i18n-title="layers.cctv.calEastTitle">ΔE --</button>
|
||||
<div id="cctv-cal-readout" class="cctv-cal-readout" aria-label="Camera pose — click a value to type" data-i18n-aria-label>
|
||||
<button class="cctv-cal-value" data-cal-field="heading" title="Heading (compass °) — click to type" data-i18n-title>HDG --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="pitch" title="Pitch (° up/down) — click to type" data-i18n-title>PITCH --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="fov" title="Horizontal FOV (°) — click to type" data-i18n-title>FOV --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="range" title="Range / monitor-plane distance (m) — click to type" data-i18n-title>RANGE --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="height" title="Mount height above ground (m) — click to type" data-i18n-title>HGT --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="north" title="North offset from catalog position (m) — click to type" data-i18n-title>ΔN --</button>
|
||||
<button class="cctv-cal-value" data-cal-field="east" title="East offset from catalog position (m) — click to type" data-i18n-title>ΔE --</button>
|
||||
</div>
|
||||
<div class="cctv-controls cctv-calibration-actions">
|
||||
<button id="cctv-calib-save-btn" class="scene-btn" data-i18n="layers.cctv.saveCal">SAVE CAL</button>
|
||||
<button id="cctv-calib-reset-btn" class="scene-btn" data-i18n="layers.cctv.resetCal">RESET CAL</button>
|
||||
<button id="cctv-calib-save-btn" class="scene-btn" data-i18n>SAVE CAL</button>
|
||||
<button id="cctv-calib-reset-btn" class="scene-btn" data-i18n>RESET CAL</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cctv-summary-label" data-i18n="layers.cctv.summaryLabel">SCENE SUMMARY</div>
|
||||
<div id="cctv-summary" data-i18n="layers.cctv.summaryIdle">Enable CCTV to start camera-linked intelligence summaries.</div>
|
||||
<div class="cctv-summary-label" data-i18n>SCENE SUMMARY</div>
|
||||
<div id="cctv-summary" data-i18n>Enable CCTV to start camera-linked intelligence summaries.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,35 +145,35 @@
|
||||
<div class="panel-glow"></div>
|
||||
<div class="scene-panel-inner">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title" data-i18n="setup.scenes.panelTitle">SCENES</span>
|
||||
<span class="panel-title" data-i18n>SCENES</span>
|
||||
<span class="panel-divider"></span>
|
||||
<button class="panel-collapse-btn" data-collapse-target="scene-panel" title="Collapse panel" data-i18n-title="setup.scenes.collapseTitle">+</button>
|
||||
<button class="panel-collapse-btn" data-collapse-target="scene-panel" title="Collapse panel" data-i18n-title>+</button>
|
||||
</div>
|
||||
<div class="scene-controls">
|
||||
<select id="scene-select" aria-label="Scene recipe" data-i18n-aria-label="setup.scenes.recipeAriaLabel"></select>
|
||||
<button id="scene-new-btn" class="scene-btn" data-i18n="setup.scenes.new">NEW</button>
|
||||
<button id="scene-delete-btn" class="scene-btn scene-btn-danger" data-i18n="setup.scenes.delete">DEL</button>
|
||||
<select id="scene-select" aria-label="Scene recipe" data-i18n-aria-label></select>
|
||||
<button id="scene-new-btn" class="scene-btn" data-i18n>NEW</button>
|
||||
<button id="scene-delete-btn" class="scene-btn scene-btn-danger" data-i18n>DEL</button>
|
||||
</div>
|
||||
<div class="scene-controls">
|
||||
<button id="scene-capture-btn" class="scene-btn" data-i18n="setup.scenes.capture">CAPTURE SHOT</button>
|
||||
<button id="scene-update-shot-btn" class="scene-btn" data-i18n="setup.scenes.updateShot">UPDATE SHOT</button>
|
||||
<button id="scene-capture-btn" class="scene-btn" data-i18n>CAPTURE SHOT</button>
|
||||
<button id="scene-update-shot-btn" class="scene-btn" data-i18n>UPDATE SHOT</button>
|
||||
</div>
|
||||
<div id="scene-shot-list" class="scene-shot-list"></div>
|
||||
<div class="scene-controls">
|
||||
<button id="scene-start-btn" class="scene-btn" data-i18n="setup.scenes.start">START</button>
|
||||
<button id="scene-stop-btn" class="scene-btn scene-btn-danger" data-i18n="setup.scenes.stop">STOP</button>
|
||||
<button id="scene-next-btn" class="scene-btn" data-i18n="setup.scenes.next">NEXT</button>
|
||||
<button id="scene-start-btn" class="scene-btn" data-i18n>START</button>
|
||||
<button id="scene-stop-btn" class="scene-btn scene-btn-danger" data-i18n>STOP</button>
|
||||
<button id="scene-next-btn" class="scene-btn" data-i18n>NEXT</button>
|
||||
</div>
|
||||
<div class="scene-controls">
|
||||
<button id="scene-export-btn" class="scene-btn" data-i18n="setup.scenes.exportPresets">EXPORT PRESETS</button>
|
||||
<button id="scene-import-btn" class="scene-btn" data-i18n="setup.scenes.import">IMPORT</button>
|
||||
<button id="scene-download-btn" class="scene-btn" data-i18n="setup.scenes.runLog">RUN LOG</button>
|
||||
<button id="scene-export-btn" class="scene-btn" data-i18n>EXPORT PRESETS</button>
|
||||
<button id="scene-import-btn" class="scene-btn" data-i18n>IMPORT</button>
|
||||
<button id="scene-download-btn" class="scene-btn" data-i18n>RUN LOG</button>
|
||||
<input type="file" id="scene-import-file" accept="application/json,.json" hidden />
|
||||
</div>
|
||||
<div class="scene-progress">
|
||||
<div id="scene-progress-fill">0%</div>
|
||||
</div>
|
||||
<div id="scene-status" data-i18n="setup.scenes.statusReady">Ready</div>
|
||||
<div id="scene-status" data-i18n>Ready</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,22 +4,22 @@
|
||||
that payload, so the key registry lives in src/keySetupCore.mjs alone. -->
|
||||
<button id="key-setup-chip" type="button" hidden aria-haspopup="dialog" aria-controls="key-setup">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">bolt</span>
|
||||
<span data-key-setup-chip-label data-i18n="setup.keySetup.chip">POWER UP</span>
|
||||
<span data-key-setup-chip-label data-i18n>POWER UP</span>
|
||||
</button>
|
||||
<aside id="key-setup" role="dialog" aria-labelledby="key-setup-title" aria-describedby="key-setup-description" hidden>
|
||||
<div class="key-setup-scanline" aria-hidden="true"></div>
|
||||
<header class="key-setup-header">
|
||||
<span class="key-setup-kicker" data-i18n="setup.keySetup.kicker">GROUND STATION · PROVIDER SETTINGS</span>
|
||||
<button type="button" class="key-setup-close" data-key-setup-close aria-label="Close key setup" data-i18n-aria-label="setup.keySetup.closeAriaLabel">
|
||||
<span class="key-setup-kicker" data-i18n>GROUND STATION · PROVIDER SETTINGS</span>
|
||||
<button type="button" class="key-setup-close" data-key-setup-close aria-label="Close key setup" data-i18n-aria-label>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
</header>
|
||||
<h2 id="key-setup-title" data-i18n="setup.keySetup.title">Power up the globe</h2>
|
||||
<p id="key-setup-description" data-i18n="setup.keySetup.description">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>
|
||||
<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-footer">
|
||||
<button type="button" class="key-setup-apply" data-key-setup-apply data-i18n="setup.keySetup.apply">SAVE KEYS</button>
|
||||
<span class="key-setup-hint" data-i18n="setup.keySetup.hint">ESC to close</span>
|
||||
<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>
|
||||
</div>
|
||||
<p class="key-setup-note" data-key-setup-status role="status" aria-live="polite" data-i18n="setup.keySetup.note">The Google Maps key buys the photorealistic planet — everything else stacks on top.</p>
|
||||
<p class="key-setup-note" data-key-setup-status role="status" aria-live="polite" data-i18n>The Google Maps key buys the photorealistic planet — everything else stacks on top.</p>
|
||||
</aside>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div id="world-overlay-root" aria-hidden="true">
|
||||
<canvas id="world-overlay-canvas"></canvas>
|
||||
</div>
|
||||
<div id="world-overlay-actions" class="world-overlay-accessibility" role="region" aria-label="Visible map targets" data-i18n-aria-label="layers.overlay.regionAriaLabel">
|
||||
<div id="world-overlay-actions" class="world-overlay-accessibility" role="region" aria-label="Visible map targets" data-i18n-aria-label>
|
||||
<div id="world-overlay-action-list"></div>
|
||||
<div id="world-overlay-status" role="status" aria-live="polite" aria-atomic="true"></div>
|
||||
</div>
|
||||
@@ -11,21 +11,21 @@
|
||||
<div id="title-bar">
|
||||
<div class="title-glow"></div>
|
||||
<h1><span class="title-logo brand-logo" data-logo-gaze data-logo-src="/logo.svg" aria-hidden="true"><img src="/logo.svg" alt="" /></span> <span>GOD'S EYE <span class="title-accent">VIEW</span></span></h1>
|
||||
<p class="subtitle" data-i18n="shell.title.subtitle">NO PLACE LEFT BEHIND</p>
|
||||
<p class="subtitle" data-i18n>NO PLACE LEFT BEHIND</p>
|
||||
</div>
|
||||
|
||||
<!-- Style indicator -->
|
||||
<div id="style-indicator">
|
||||
<span class="indicator-label" data-i18n="cockpit.presets.activeStyleLabel">ACTIVE STYLE</span>
|
||||
<span class="indicator-label" data-i18n>ACTIVE STYLE</span>
|
||||
<span class="indicator-value" id="active-style-name">NORMAL</span>
|
||||
</div>
|
||||
|
||||
<!-- Top-center camera and share actions -->
|
||||
<nav id="top-center-actions" aria-label="Globe actions" data-i18n-aria-label="shell.actions.navAriaLabel">
|
||||
<button id="clear-selected-layers" type="button" aria-label="Clear selected data layers" title="Turn off all selected data layers" data-i18n-aria-label="shell.actions.clearLayers.ariaLabel" data-i18n-title="shell.actions.clearLayers.title">
|
||||
<nav id="top-center-actions" aria-label="Globe actions" data-i18n-aria-label>
|
||||
<button id="clear-selected-layers" type="button" aria-label="Clear selected data layers" title="Turn off all selected data layers" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">layers_clear</span>
|
||||
</button>
|
||||
<button id="share-btn" type="button" aria-label="Copy share link" title="Copy share link" data-i18n-aria-label="shell.actions.share.ariaLabel" data-i18n-title="shell.actions.share.title">
|
||||
<button id="share-btn" type="button" aria-label="Copy share link" title="Copy share link" data-i18n-aria-label data-i18n-title>
|
||||
<span class="share-icon" aria-hidden="true">🔗</span>
|
||||
</button>
|
||||
<button id="tilt-map-view" type="button" aria-label="Tilt map to oblique view" aria-pressed="false" title="Toggle straight-down and tilted map views">
|
||||
@@ -35,20 +35,20 @@
|
||||
<span class="camera-compass-n" aria-hidden="true">N</span>
|
||||
<span class="camera-compass-needle material-symbols-outlined" aria-hidden="true">navigation</span>
|
||||
</button>
|
||||
<button id="reset-globe-view" type="button" aria-label="Reset to full globe view" title="Reset camera and return to full globe view" data-i18n-aria-label="shell.actions.resetView.ariaLabel" data-i18n-title="shell.actions.resetView.title">
|
||||
<button id="reset-globe-view" type="button" aria-label="Reset to full globe view" title="Reset camera and return to full globe view" data-i18n-aria-label data-i18n-title>
|
||||
<span class="material-symbols-outlined" aria-hidden="true">public</span>
|
||||
</button>
|
||||
</nav>
|
||||
<div id="global-loading-status" role="status" aria-live="polite" aria-atomic="true" hidden>
|
||||
<span id="global-loading-label" data-i18n="shell.status.loadingLiveData">LOADING LIVE DATA</span>
|
||||
<span id="global-loading-label" data-i18n>LOADING LIVE DATA</span>
|
||||
<span id="global-loading-detail"></span>
|
||||
</div>
|
||||
<div id="traffic-sync-chip" aria-live="polite">
|
||||
<span id="traffic-sync-label" data-i18n="shell.status.trafficSyncing">syncing road network</span>
|
||||
<span id="traffic-sync-label" data-i18n>syncing road network</span>
|
||||
<span id="traffic-sync-progress">0%</span>
|
||||
</div>
|
||||
<div id="cctv-sync-chip" aria-live="polite">
|
||||
<span id="cctv-sync-label" data-i18n="shell.status.framesLoading">loading frames</span>
|
||||
<span id="cctv-sync-label" data-i18n>loading frames</span>
|
||||
<span id="cctv-sync-progress">0/0</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<aside id="first-run-launcher" role="dialog" aria-labelledby="first-run-title" aria-describedby="first-run-description" hidden>
|
||||
<div class="first-run-scanline" aria-hidden="true"></div>
|
||||
<header class="first-run-header">
|
||||
<span class="first-run-kicker" data-i18n="setup.firstRun.kicker">MISSION CONTROL · FIRST LAUNCH</span>
|
||||
<span class="first-run-kicker" data-i18n>MISSION CONTROL · FIRST LAUNCH</span>
|
||||
</header>
|
||||
<h2 id="first-run-title" data-i18n="setup.firstRun.title">Choose your first view</h2>
|
||||
<h2 id="first-run-title" data-i18n>Choose your first view</h2>
|
||||
<!-- The card's one persuasive line, owner-authored and VERBATIM (2026-08-23)
|
||||
— including the unspaced em dash. The arc it has to carry: this looks
|
||||
like something you should not have access to → the sources are all
|
||||
@@ -16,12 +16,12 @@
|
||||
<div class="first-run-choices">
|
||||
<button type="button" data-first-run-choice="contacts">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">radar</span>
|
||||
<span><strong data-i18n="setup.firstRun.choice.contacts">LIVE CONTACTS</strong><small data-i18n="setup.firstRun.choice.contactsSub">Aircraft, vessels and nearby intelligence</small></span>
|
||||
<span><strong data-i18n>LIVE CONTACTS</strong><small data-i18n>Aircraft, vessels and nearby intelligence</small></span>
|
||||
<span class="material-symbols-outlined first-run-arrow" aria-hidden="true">arrow_forward</span>
|
||||
</button>
|
||||
<button type="button" data-first-run-choice="space-missions">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">rocket_launch</span>
|
||||
<span><strong data-i18n="setup.firstRun.choice.spaceMissions">SPACE MISSIONS</strong><small data-i18n="setup.firstRun.choice.spaceMissionsSub">Launches, spacecraft and orbital context</small></span>
|
||||
<span><strong data-i18n>SPACE MISSIONS</strong><small data-i18n>Launches, spacecraft and orbital context</small></span>
|
||||
<span class="material-symbols-outlined first-run-arrow" aria-hidden="true">arrow_forward</span>
|
||||
</button>
|
||||
<button type="button" data-first-run-choice="environmental">
|
||||
@@ -35,18 +35,18 @@
|
||||
</button>
|
||||
<button type="button" class="first-run-explore" data-first-run-choice="explore">
|
||||
<span class="material-symbols-outlined" aria-hidden="true">public</span>
|
||||
<span data-i18n="setup.firstRun.choice.explore" data-i18n="setup.firstRun.choice.exploreSub"><strong>EXPLORE MANUALLY</strong><small>Begin with a clean globe</small></span>
|
||||
<span><strong data-i18n>EXPLORE MANUALLY</strong><small data-i18n>Begin with a clean globe</small></span>
|
||||
<span class="material-symbols-outlined first-run-arrow" aria-hidden="true">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="first-run-footer">
|
||||
<label class="first-run-suppress">
|
||||
<input type="checkbox" data-first-run-suppress />
|
||||
<span data-i18n="setup.firstRun.suppress">Don't show this again</span>
|
||||
<span data-i18n>Don't show this again</span>
|
||||
</label>
|
||||
<span data-i18n="setup.firstRun.dismissHint">ESC to dismiss</span>
|
||||
<span data-i18n>ESC to dismiss</span>
|
||||
</div>
|
||||
<!-- Kept out of the footer row: it swaps to mission progress and errors, and
|
||||
a live region that also holds layout makes the row jump as it speaks. -->
|
||||
<p class="first-run-note" data-first-run-status role="status" aria-live="polite" data-i18n="setup.firstRun.note">Tip: the GEV MIC button in the dock lets you talk to the map.</p>
|
||||
<p class="first-run-note" data-first-run-status role="status" aria-live="polite" data-i18n>Tip: the GEV MIC button in the dock lets you talk to the map.</p>
|
||||
</aside>
|
||||
@@ -1191,8 +1191,8 @@ export class VisualSettings {
|
||||
this._celestialBtn.disabled = !styleSupported;
|
||||
this._celestialBtn.setAttribute('aria-disabled', String(!styleSupported));
|
||||
this._celestialBtn.title = styleSupported
|
||||
? t('cockpit.display.celestialToggleTitle')
|
||||
: t('cockpit.display.celestialUnavailableTitle');
|
||||
? t('Celestial ring — reveal the full globe')
|
||||
: t('Celestial ring — available in Normal style');
|
||||
}
|
||||
let cameraFocused = false;
|
||||
if (nextEnabled && focus) {
|
||||
@@ -1649,31 +1649,23 @@ export class VisualSettings {
|
||||
btn.setAttribute(
|
||||
'aria-label',
|
||||
enabled
|
||||
? t('cockpit.display.detectionAriaTemplate', {
|
||||
? t('Detection overlay: {mode}', {
|
||||
mode: String(modeLabel).toLowerCase(),
|
||||
})
|
||||
: t('cockpit.display.detectionAriaOff'),
|
||||
: t('Detection overlay: off'),
|
||||
);
|
||||
btn.classList.remove('active', 'god', 'panoptic');
|
||||
if (modeLabel === 'SPARSE') {
|
||||
btn.querySelector('.pp-label').textContent = t(
|
||||
'cockpit.display.detectionLabelSparse',
|
||||
);
|
||||
btn.querySelector('.pp-label').textContent = t('SPARSE');
|
||||
btn.classList.add('active');
|
||||
} else if (modeLabel === 'BALANCED') {
|
||||
btn.querySelector('.pp-label').textContent = t(
|
||||
'cockpit.display.detectionLabelBalanced',
|
||||
);
|
||||
btn.querySelector('.pp-label').textContent = t('BALANCED');
|
||||
btn.classList.add('active');
|
||||
} else if (modeLabel === 'DENSE') {
|
||||
btn.querySelector('.pp-label').textContent = t(
|
||||
'cockpit.display.detectionLabelDense',
|
||||
);
|
||||
btn.querySelector('.pp-label').textContent = t('DENSE');
|
||||
btn.classList.add('active', 'panoptic');
|
||||
} else {
|
||||
btn.querySelector('.pp-label').textContent = t(
|
||||
'cockpit.display.detectionLabel',
|
||||
);
|
||||
btn.querySelector('.pp-label').textContent = t('DETECT');
|
||||
}
|
||||
|
||||
if (this._detectionSliderRow) {
|
||||
|
||||
Reference in new issue
Block a user