mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge upstream/main into pr/google-street-view
Conflicts with upstream's Street Level provider composition and share-state codec, resolved: - Street View's switch moves from its own share option (`g`) to upstream's provider-switch field (`r`, e.g. `0.r.google-1`); a link without it keeps Street View off. Docs and the browser gate follow. - createApplicationStreetLevel keeps upstream's injectable provider list; its default, once the application supplies its Street Level source, is Mapillary plus Google Street View. Without that source there are none, as upstream intends. - The coverage hint keeps upstream's empty-composition message and joins every switched-on provider's hint otherwise. - CHANGELOG, README and SECURITY keep both sides' entries. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
76db4114ff
80 files changed
+3845
-201
No files matched your search
+15
-5
@@ -595,7 +595,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';
|
||||
@@ -608,7 +613,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
|
||||
@@ -620,16 +625,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.
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
@@ -184,6 +184,7 @@ function presentNearest(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.';
|
||||
// Key-gated with another source ready: say how to use it.
|
||||
if (state.keyRequired && state.keyAlternatives?.length)
|
||||
@@ -215,7 +216,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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user