style: format Layers panel components

This commit is contained in:
Sameh Khamis committed 2026-09-12 22:36:06 -07:00
1 parent 07cf1695ff
commit e34d619949
3 files changed
+158 -205

No files matched your search

+51 -156
View File
@@ -10,47 +10,27 @@
"src/data/localGeojsonCore.js",
"src/data/localGeojsonLod.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"application": {
"exports": [
"./application"
],
"modules": [
"src/app/application.js"
],
"exports": ["./application"],
"modules": ["src/app/application.js"],
"external": []
},
"viewer": {
"exports": [
"./application/viewer"
],
"modules": [
"src/app/viewer.js"
],
"external": [
"cesium"
]
"exports": ["./application/viewer"],
"modules": ["src/app/viewer.js"],
"external": ["cesium"]
},
"vite-build": {
"runtime": "node",
"exports": [
"./build/vite"
],
"modules": [
"build/vite.js"
],
"external": [
"vite-plugin-cesium"
]
"exports": ["./build/vite"],
"modules": ["build/vite.js"],
"external": ["vite-plugin-cesium"]
},
"live-providers": {
"runtime": "node",
"exports": [
"./server/providers/live"
],
"exports": ["./server/providers/live"],
"modules": [
"server/providers/live.js",
"server/providers/common/http.js",
@@ -65,24 +45,16 @@
"src/data/aisStreamAdapter.js",
"src/data/aisWatchdog.js"
],
"external": [
"ws"
]
"external": ["ws"]
},
"adsb-lol-source": {
"exports": [
"./sources/adsb-lol"
],
"modules": [
"src/data/adsbLolFallback.js"
],
"exports": ["./sources/adsb-lol"],
"modules": ["src/data/adsbLolFallback.js"],
"external": []
},
"place-providers": {
"runtime": "node",
"exports": [
"./server/providers/places"
],
"exports": ["./server/providers/places"],
"modules": [
"server/providers/places.js",
"server/providers/places/google.js",
@@ -97,19 +69,13 @@
"external": []
},
"place-payloads": {
"exports": [
"./sources/places"
],
"modules": [
"src/data/placeProviderPayloads.js"
],
"exports": ["./sources/places"],
"modules": ["src/data/placeProviderPayloads.js"],
"external": []
},
"space-providers": {
"runtime": "node",
"exports": [
"./server/providers/space"
],
"exports": ["./server/providers/space"],
"modules": [
"server/providers/space.js",
"server/providers/space/celestrak.js",
@@ -120,19 +86,13 @@
"external": []
},
"space-requests": {
"exports": [
"./sources/space"
],
"modules": [
"src/data/spaceProviderRequests.js"
],
"exports": ["./sources/space"],
"modules": ["src/data/spaceProviderRequests.js"],
"external": []
},
"terrain-provider": {
"runtime": "node",
"exports": [
"./server/providers/terrain"
],
"exports": ["./server/providers/terrain"],
"modules": [
"server/providers/terrain.js",
"src/data/terrainHeightsProxy.js"
@@ -140,50 +100,30 @@
"external": []
},
"terrain-source": {
"exports": [
"./sources/terrain"
],
"modules": [
"src/data/terrainHeightsProxy.js"
],
"exports": ["./sources/terrain"],
"modules": ["src/data/terrainHeightsProxy.js"],
"external": []
},
"traffic-provider": {
"runtime": "node",
"exports": [
"./server/providers/traffic"
],
"modules": [
"server/providers/traffic.js",
"src/data/tomtomTiles.js"
],
"exports": ["./server/providers/traffic"],
"modules": ["server/providers/traffic.js", "src/data/tomtomTiles.js"],
"external": []
},
"traffic-source": {
"exports": [
"./sources/traffic"
],
"modules": [
"src/data/tomtomTiles.js"
],
"exports": ["./sources/traffic"],
"modules": ["src/data/tomtomTiles.js"],
"external": []
},
"firms-provider": {
"runtime": "node",
"exports": [
"./server/providers/firms"
],
"modules": [
"server/providers/firms.js",
"src/data/firmsCsv.js"
],
"exports": ["./server/providers/firms"],
"modules": ["server/providers/firms.js", "src/data/firmsCsv.js"],
"external": []
},
"gbfs-provider": {
"runtime": "node",
"exports": [
"./server/providers/gbfs"
],
"exports": ["./server/providers/gbfs"],
"modules": [
"server/providers/gbfs.js",
"server/providers/common/http.js",
@@ -192,19 +132,13 @@
"external": []
},
"gbfs-source": {
"exports": [
"./sources/gbfs"
],
"modules": [
"src/data/gbfsSource.js"
],
"exports": ["./sources/gbfs"],
"modules": ["src/data/gbfsSource.js"],
"external": []
},
"cctv-provider": {
"runtime": "node",
"exports": [
"./server/providers/cctv"
],
"exports": ["./server/providers/cctv"],
"modules": [
"server/providers/cctv.js",
"server/providers/cctv/catalog.js",
@@ -221,9 +155,7 @@
},
"radio-provider": {
"runtime": "node",
"exports": [
"./server/providers/radio"
],
"exports": ["./server/providers/radio"],
"modules": [
"server/providers/radio.js",
"server/providers/radio/catalog.js",
@@ -237,9 +169,7 @@
},
"overpass-provider": {
"runtime": "node",
"exports": [
"./server/providers/overpass"
],
"exports": ["./server/providers/overpass"],
"modules": [
"server/providers/common/geo.js",
"server/providers/common/http.js",
@@ -258,9 +188,7 @@
},
"military-installations-provider": {
"runtime": "node",
"exports": [
"./server/providers/military-installations"
],
"exports": ["./server/providers/military-installations"],
"modules": [
"server/providers/common/http.js",
"server/providers/common/query.js",
@@ -277,9 +205,7 @@
},
"regional-provider": {
"runtime": "node",
"exports": [
"./server/providers/regional"
],
"exports": ["./server/providers/regional"],
"modules": [
"server/providers/common/http.js",
"server/providers/common/query.js",
@@ -298,9 +224,7 @@
},
"openai-provider": {
"runtime": "node",
"exports": [
"./server/providers/openai"
],
"exports": ["./server/providers/openai"],
"modules": [
"server/providers/common/rate-limit.js",
"server/providers/common/request.js",
@@ -320,9 +244,7 @@
},
"standalone-key-setup": {
"runtime": "node",
"exports": [
"./server/standalone/key-setup"
],
"exports": ["./server/standalone/key-setup"],
"modules": [
"scripts/pinokio-environment.mjs",
"server/providers/common/source-root.js",
@@ -333,9 +255,7 @@
"external": []
},
"place-search": {
"exports": [
"./search"
],
"exports": ["./search"],
"modules": [
"src/search/index.js",
"src/search/placeSearch.js",
@@ -345,27 +265,17 @@
"external": []
},
"panel-disclosure": {
"exports": [
"./ui/panels"
],
"modules": [
"src/ui/panelDisclosure.js"
],
"exports": ["./ui/panels"],
"modules": ["src/ui/panelDisclosure.js"],
"external": []
},
"surface-keyboard": {
"exports": [
"./ui/surfaces"
],
"modules": [
"src/ui/surfaceKeyboard.js"
],
"exports": ["./ui/surfaces"],
"modules": ["src/ui/surfaceKeyboard.js"],
"external": []
},
"panel-rail-layout": {
"exports": [
"./ui/layout"
],
"exports": ["./ui/layout"],
"modules": [
"src/ui/panelRails.js",
"src/ui/leftPanelRail.js",
@@ -377,9 +287,7 @@
"external": []
},
"visual-input": {
"exports": [
"./ui/input"
],
"exports": ["./ui/input"],
"modules": [
"src/ui/visualInput.js",
"src/ui/applicationShortcuts.js",
@@ -388,19 +296,12 @@
"external": []
},
"display-controls": {
"exports": [
"./ui/display"
],
"modules": [
"src/ui/displayControls.js"
],
"exports": ["./ui/display"],
"modules": ["src/ui/displayControls.js"],
"external": []
},
"visual-effects": {
"exports": [
"./ui/effects",
"./ui/effects/bloom"
],
"exports": ["./ui/effects", "./ui/effects/bloom"],
"modules": [
"src/ui/effects.js",
"src/ui/visualEffects.js",
@@ -413,14 +314,10 @@
"src/styles/noir.js",
"src/styles/snow.js"
],
"external": [
"cesium"
]
"external": ["cesium"]
},
"map-source-controls": {
"exports": [
"./ui/maps"
],
"exports": ["./ui/maps"],
"modules": [
"src/ui/mapSource.js",
"src/ui/mapSourceControls.js",
@@ -430,9 +327,7 @@
"external": []
},
"layer-panel": {
"exports": [
"./ui/layers"
],
"exports": ["./ui/layers"],
"modules": [
"src/ui/layers.js",
"src/ui/layerPanel.js",
+7 -2
View File
@@ -1,14 +1,19 @@
/** Keep the clear action focusable while busy; the caller owns its transaction. */
export function bindClearLayersControl(button, clear) {
let destroyed = false;
const click = () => { if (!destroyed) void clear(); };
const click = () => {
if (!destroyed) void clear();
};
button?.addEventListener('click', click);
return {
setBusy(busy) {
if (destroyed || !button) return;
button.setAttribute('aria-disabled', String(busy));
button.setAttribute('aria-busy', String(busy));
button.setAttribute('aria-label', busy ? 'Clearing selected data layers' : 'Clear selected data layers');
button.setAttribute(
'aria-label',
busy ? 'Clearing selected data layers' : 'Clear selected data layers',
);
},
destroy() {
destroyed = true;
+100 -47
View File
@@ -15,16 +15,21 @@ const FEED_STATE_LABELS = Object.freeze({
*/
export function layerFeedState(stats = {}) {
const state = stats || {};
const status = typeof state.status === 'string' ? state.status.toLowerCase() : '';
const status =
typeof state.status === 'string' ? state.status.toLowerCase() : '';
const source = `${state.source || ''} ${state.coverage || ''}`;
const hasExplicitFallback = typeof state.fallback === 'boolean';
const hasPriorData = Number(state.count) > 0 || Boolean(state.lastUpdate);
const presentedError = state.error || state.lastError || state.managerRefreshError;
if (['unavailable', 'offline', 'down', 'error'].includes(status)) return 'unavailable';
const presentedError =
state.error || state.lastError || state.managerRefreshError;
if (['unavailable', 'offline', 'down', 'error'].includes(status))
return 'unavailable';
if (
(presentedError || state.unavailable === true || state.available === false)
&& !hasPriorData
&& !GUIDANCE_STATUSES.includes(status)
(presentedError ||
state.unavailable === true ||
state.available === false) &&
!hasPriorData &&
!GUIDANCE_STATUSES.includes(status)
) {
return 'unavailable';
}
@@ -37,29 +42,37 @@ export function layerFeedState(stats = {}) {
return state.stale ? 'stale' : 'nominal';
}
if (
state.fallback === true
|| status === 'fallback'
|| state.mode === 'sim'
|| /\bfallback\b/i.test(source)
|| (!hasExplicitFallback && /\badsb\.lol\b/i.test(source))
state.fallback === true ||
status === 'fallback' ||
state.mode === 'sim' ||
/\bfallback\b/i.test(source) ||
(!hasExplicitFallback && /\badsb\.lol\b/i.test(source))
) {
return 'fallback';
}
if (state.stale || status === 'stale') return 'stale';
if (
state.degraded
|| presentedError
|| state.unavailable === true
|| state.available === false
) return 'degraded';
state.degraded ||
presentedError ||
state.unavailable === true ||
state.available === false
)
return 'degraded';
return 'nominal';
}
/** Layer row presentation over supplied state and actions; no layer imports. */
export class LayerPanel {
constructor({ getLayers, isEnabled, setEnabled, setLayerParams, getRowControls,
hasRowControls, subscribeRowControls, onHiddenRefresh = () => {} }) {
constructor({
getLayers,
isEnabled,
setEnabled,
setLayerParams,
getRowControls,
hasRowControls,
subscribeRowControls,
onHiddenRefresh = () => {},
}) {
this.getAll = getLayers;
this.isEnabled = isEnabled;
this.setEnabled = setEnabled;
@@ -123,7 +136,9 @@ export class LayerPanel {
const count = document.createElement('span');
count.className = 'data-count';
count.textContent = layer.stats.count ? this._formatCount(layer.stats.count) : '—';
count.textContent = layer.stats.count
? this._formatCount(layer.stats.count)
: '—';
const toggle = document.createElement('button');
toggle.type = 'button';
@@ -133,16 +148,24 @@ export class LayerPanel {
// Native `disabled` immediately evicts keyboard focus in Chromium. Keep
// the lifecycle control focusable while it is busy, and enforce the
// same single-flight interaction contract through ARIA instead.
if (this._destroyed || this._generation !== generation || toggle.getAttribute('aria-disabled') === 'true') return;
if (
this._destroyed ||
this._generation !== generation ||
toggle.getAttribute('aria-disabled') === 'true'
)
return;
toggle.setAttribute('aria-disabled', 'true');
toggle.setAttribute('aria-busy', 'true');
try {
await this.setEnabled(layer.id, !this.isEnabled(layer.id), { origin: 'user' });
await this.setEnabled(layer.id, !this.isEnabled(layer.id), {
origin: 'user',
});
} catch (error) {
console.warn(`[Data] ${layer.id} toggle error:`, error);
} finally {
const current = this.getAll().find(({ id }) => id === layer.id);
if (!this._destroyed && current && this._generation === generation) this._syncToggleButton(toggle, current);
if (!this._destroyed && current && this._generation === generation)
this._syncToggleButton(toggle, current);
}
});
@@ -165,7 +188,9 @@ export class LayerPanel {
// A layer whose controls settle asynchronously (a chunked catalog load
// that can also fail) pushes a re-render through this; nothing else
// would repaint the row before its next scheduled refresh.
const unsubscribe = this.subscribeRowControls(layer.id, () => this._refreshTogglePanel());
const unsubscribe = this.subscribeRowControls(layer.id, () =>
this._refreshTogglePanel(),
);
if (unsubscribe) this._removers.push(unsubscribe);
const controls = document.createElement('div');
controls.className = 'data-toggle-controls';
@@ -174,9 +199,11 @@ export class LayerPanel {
if (!button || button.disabled) return;
// Re-read the live descriptor rather than trusting the rendered
// chip, so a stale row can never apply an inverted toggle.
const chip = this._rowControlsFor(layer.id)?.chips
?.find((entry) => entry.id === button.dataset.chipId);
if (chip?.params) this.setLayerParams(layer.id, chip.params, { origin: 'user' });
const chip = this._rowControlsFor(layer.id)?.chips?.find(
(entry) => entry.id === button.dataset.chipId,
);
if (chip?.params)
this.setLayerParams(layer.id, chip.params, { origin: 'user' });
});
row.appendChild(controls);
this._syncRowControls(controls, layer);
@@ -215,7 +242,10 @@ export class LayerPanel {
container.hidden = chips.length === 0 && legend.length === 0;
for (const node of [...container.children]) {
if (String(node.className).split(/\s+/).includes('data-toggle-legend-item')) node.remove();
if (
String(node.className).split(/\s+/).includes('data-toggle-legend-item')
)
node.remove();
}
const stale = new Map();
@@ -265,7 +295,9 @@ export class LayerPanel {
return;
}
for (const layer of this.getAll()) {
const row = this._toggleContainer.querySelector(`[data-layer-id="${layer.id}"]`);
const row = this._toggleContainer.querySelector(
`[data-layer-id="${layer.id}"]`,
);
if (!row) continue;
const btn = row.querySelector('.data-toggle-btn');
@@ -275,7 +307,9 @@ export class LayerPanel {
const count = row.querySelector('.data-count');
if (count) {
count.textContent = layer.stats.count ? this._formatCount(layer.stats.count) : '—';
count.textContent = layer.stats.count
? this._formatCount(layer.stats.count)
: '—';
}
const meta = row.querySelector('.data-toggle-meta');
@@ -292,14 +326,16 @@ export class LayerPanel {
const feedState = layerFeedState(stats);
const stateLabel = FEED_STATE_LABELS[feedState];
const source = stats.source || layer.source;
const lifecycleState = layer.lifecycleState || (layer.enabled ? 'enabled' : 'disabled');
const lifecycleState =
layer.lifecycleState || (layer.enabled ? 'enabled' : 'disabled');
if (lifecycleState === 'enabling' || lifecycleState === 'disabling') {
return `${lifecycleState.toUpperCase()} · ${source}`;
}
if (layer.lifecycleUncertain) {
return `UNCERTAIN · ${source} · lifecycle state requires reconciliation`;
}
const presentedError = stats.error || stats.lastError || stats.managerRefreshError;
const presentedError =
stats.error || stats.lastError || stats.managerRefreshError;
if (presentedError) {
if (typeof stats.retryInSec === 'number' && stats.retryInSec > 0) {
return `${stateLabel} · ${source} · ${presentedError} · retry ${stats.retryInSec}s`;
@@ -308,27 +344,33 @@ export class LayerPanel {
}
// A guidance status carries its prompt in `statusMessage`, not `error`, so
// the row still tells the operator what to do without reporting a fault.
if (GUIDANCE_STATUSES.includes(String(stats.status || '').toLowerCase())
&& typeof stats.statusMessage === 'string' && stats.statusMessage.trim()) {
if (
GUIDANCE_STATUSES.includes(String(stats.status || '').toLowerCase()) &&
typeof stats.statusMessage === 'string' &&
stats.statusMessage.trim()
) {
return `${source} · ${stats.statusMessage.trim()}`;
}
const ago = stats.lastUpdate ? this._timeAgo(stats.lastUpdate) : 'never';
if (stats.loading) {
const loadingLabel = typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()
? stats.loadingLabel.trim()
: 'loading...';
const loadingLabel =
typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()
? stats.loadingLabel.trim()
: 'loading...';
return `${source} · ${loadingLabel}`;
}
if (feedState === 'fallback') {
const detail = typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()
? stats.loadingLabel.trim()
: (stats.coverage || ago);
const detail =
typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()
? stats.loadingLabel.trim()
: stats.coverage || ago;
return `${stateLabel} · ${source} · ${detail}`;
}
if (feedState === 'stale') {
const retry = typeof stats.retryInSec === 'number' && stats.retryInSec > 0
? ` · retrying in ${stats.retryInSec}s`
: '';
const retry =
typeof stats.retryInSec === 'number' && stats.retryInSec > 0
? ` · retrying in ${stats.retryInSec}s`
: '';
return `${stateLabel} · ${source} · ${ago}${retry}`;
}
if (typeof stats.loadingLabel === 'string' && stats.loadingLabel.trim()) {
@@ -339,7 +381,9 @@ export class LayerPanel {
_syncToggleButton(button, layer) {
const feedState = layer.enabled ? layerFeedState(layer.stats) : 'off';
const transitioning = layer.lifecycleState === 'enabling' || layer.lifecycleState === 'disabling';
const transitioning =
layer.lifecycleState === 'enabling' ||
layer.lifecycleState === 'disabling';
const uncertain = Boolean(layer.lifecycleUncertain);
button.classList.toggle('active', layer.enabled);
button.classList.toggle('transitioning', transitioning);
@@ -347,11 +391,16 @@ export class LayerPanel {
button.classList.toggle('disabling', layer.lifecycleState === 'disabling');
button.classList.toggle('lifecycle-uncertain', uncertain);
for (const state of Object.keys(FEED_STATE_LABELS)) {
button.classList.toggle(`feed-${state}`, layer.enabled && !uncertain && feedState === state);
button.classList.toggle(
`feed-${state}`,
layer.enabled && !uncertain && feedState === state,
);
}
button.dataset.feedState = transitioning
? layer.lifecycleState
: (uncertain ? 'uncertain' : feedState);
: uncertain
? 'uncertain'
: feedState;
// A busy toggle remains the keyboard focus owner. `aria-disabled` plus the
// click guard above prevents repeat activation without the focus loss caused
// by native `disabled`.
@@ -360,7 +409,11 @@ export class LayerPanel {
button.setAttribute('aria-busy', String(transitioning));
button.textContent = transitioning
? layer.lifecycleState.toUpperCase()
: (uncertain ? 'UNCERTAIN' : (layer.enabled ? FEED_STATE_LABELS[feedState] : 'OFF'));
: uncertain
? 'UNCERTAIN'
: layer.enabled
? FEED_STATE_LABELS[feedState]
: 'OFF';
button.setAttribute('aria-label', `${layer.name}: ${button.textContent}`);
}