Merge branch 'main' into feat/import-geo-files

This commit is contained in:
kapturner committed 2026-10-11 21:18:37 -05:00
commit d8b4b0d80f
80 files changed
+3829 -184

No files matched your search

+15 -5
View File
@@ -588,7 +588,12 @@ export class LayerPanel {
}
_syncToggleButton(button, layer) {
const feedState = layer.enabled ? layerFeedState(layer.stats) : 'off';
const unavailable = layer.stats?.sourceUnavailable === true;
const feedState = unavailable
? 'unavailable'
: layer.enabled
? layerFeedState(layer.stats)
: 'off';
const transitioning =
layer.lifecycleState === 'enabling' ||
layer.lifecycleState === 'disabling';
@@ -601,7 +606,7 @@ export class LayerPanel {
for (const state of Object.keys(FEED_STATE_LABELS)) {
button.classList.toggle(
`feed-${state}`,
layer.enabled && !uncertain && feedState === state,
(layer.enabled || unavailable) && !uncertain && feedState === state,
);
}
button.dataset.feedState = transitioning
@@ -613,16 +618,21 @@ export class LayerPanel {
// click guard above prevents repeat activation without the focus loss caused
// by native `disabled`.
button.disabled = false;
button.setAttribute('aria-disabled', String(transitioning));
button.setAttribute(
'aria-disabled',
String(transitioning || (unavailable && !layer.enabled)),
);
button.setAttribute('aria-busy', String(transitioning));
button.textContent = transitioning
? layer.lifecycleState.toUpperCase()
: uncertain
? 'UNCERTAIN'
: layer.enabled
: layer.enabled || unavailable
? FEED_STATE_LABELS[feedState]
: 'OFF';
const keyGuidance = layerKeyRequirementTooltip(layer);
const keyGuidance = unavailable
? String(layer.stats.error || '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.
+30
View File
@@ -228,3 +228,33 @@ test('the Recent Imagery readout mounts in its rail body like the weather readou
globalThis.document = previousDocument;
}
});
test('an omitted source is visibly unavailable and cannot be toggled on', async () => {
const { LayerPanel } = await import('./layerPanel.js');
const attrs = new Map();
const button = {
classList: { toggle() {} },
dataset: {},
setAttribute: (key, value) => attrs.set(key, value),
};
const layer = {
id: 'flights',
name: 'Flights',
enabled: false,
stats: {
sourceUnavailable: true,
status: 'unavailable',
error: 'Data source not configured: flights',
},
};
LayerPanel.prototype._syncToggleButton(button, layer);
assert.equal(button.textContent, 'UNAVAILABLE');
assert.equal(attrs.get('aria-disabled'), 'true');
assert.match(button.title, /not configured/);
LayerPanel.prototype._syncToggleButton(button, { ...layer, enabled: true });
assert.equal(
attrs.get('aria-disabled'),
'false',
'an active layer can always be switched off',
);
});
+2 -1
View File
@@ -125,6 +125,7 @@ function presentViewer(state) {
}
function presentMeta(state) {
if (state.providers.length === 0) return state.coverage.hint || '';
if (!state.enabled) return 'Switch a provider on to draw its coverage.';
if (state.sequence.selectedId)
return state.sequence.loading
@@ -147,7 +148,7 @@ export function presentStreetLevelPanel(state, { now = Date.now() } = {}) {
enabled,
keyRequired,
status: presentStatus(state),
controlsDisabled: keyRequired,
controlsDisabled: keyRequired || state.providers.length === 0,
providers: presentProviders(state),
error: keyMissing
? null
+17
View File
@@ -9,6 +9,23 @@ import { providerSnapshot } from '../testSupport/streetLevelFakes.mjs';
const provider = providerSnapshot;
test('an empty composition explains unavailable imagery even while disabled', () => {
for (const enabled of [false, true]) {
const view = presentStreetLevelPanel(
snapshot({
enabled,
providers: [],
coverage: { hint: 'No street-level imagery providers configured' },
legend: [],
}),
);
assert.equal(view.controlsDisabled, true);
assert.equal(view.keyRequired, false);
assert.deepEqual(view.providers, []);
assert.equal(view.meta, 'No street-level imagery providers configured');
}
});
function snapshot(overrides = {}) {
const base = {
enabled: false,
+37
View File
@@ -300,6 +300,43 @@
color: rgba(148, 178, 188, 0.42);
}
.key-setup-cloud-auth {
display: flex;
align-items: center;
gap: 0.5rem;
}
.key-setup-cloud-auth-state {
color: rgba(190, 214, 222, 0.7);
font-family: var(--font-mono);
font-size: 0.56rem;
letter-spacing: 0.05rem;
}
.key-setup-cloud-auth-toggle {
margin-left: auto;
padding: 0.28rem 0.55rem;
color: rgba(54, 220, 255, 0.9);
font-family: var(--font-mono);
font-size: 0.55rem;
letter-spacing: 0.06rem;
background: rgba(54, 220, 255, 0.08);
border: 1px solid rgba(54, 220, 255, 0.28);
border-radius: 0.32rem;
cursor: pointer;
}
.key-setup-cloud-auth-toggle:hover,
.key-setup-cloud-auth-toggle:focus-visible {
border-color: rgba(54, 220, 255, 0.65);
outline: none;
}
.key-setup-cloud-auth-toggle[aria-disabled='true'] {
cursor: wait;
opacity: 0.5;
}
.key-setup-footer {
margin-top: 0.85rem;
}