feat(street-level): one colour per imagery source

As more sources arrive, each one reads the same everywhere it appears: its
PROVIDERS chip, coverage lines, overview points, image cones and a single
legend swatch. The palette lives in one place, PROVIDER_COLORS in
src/layers/streetLevel/policy.js: Mapillary green (#05cb63), Panoramax
purple, Google Street View blue.

Mapillary drops its age dimming and magenta panoramas: lines and overview
points are Mapillary green, cones are green too and 360° images keep their
ring shape; the 360°/FLAT filter still separates them, and the selected
sequence stays GEV cyan.

The provider contract loses `legend`: `colors.coverage` is the source's one
colour, and the core composes the legend as one swatch per active source
(its name, its colour) followed by Selected. The chip group takes an
optional per-chip colour as a `--chip-color` CSS variable, written only
when it changes; Street Level chips show it as a swatch dot and, while on,
as the chip's border, text and a faint fill.

The feature gate checks the legend has one swatch per source plus Selected
and that the Mapillary chip carries #05cb63.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-09-29 00:03:00 +01:00
1 parent d5da45eacb
commit 0f2cac2ca2
21 files changed
+188 -99

No files matched your search

+11
View File
@@ -28,6 +28,17 @@ export function syncChipGroup(container, chips = [], { before = null } = {}) {
`data-toggle-chip chip-${state}${chip.active ? ' active' : ''}`,
);
set(button, 'textContent', chip.label);
// Optional per-chip colour (a Street Level source); CSS decides its use.
const color = chip.color || '';
if ((button.dataset.chipColor || '') !== color) {
if (color) {
button.style.setProperty('--chip-color', color);
button.dataset.chipColor = color;
} else {
button.style.removeProperty('--chip-color');
delete button.dataset.chipColor;
}
}
set(button, 'title', chip.title || '');
set(button, 'disabled', Boolean(chip.disabled));
attribute(button, 'aria-pressed', String(Boolean(chip.active)));
+21
View File
@@ -31,3 +31,24 @@ test('keyed chips retain focus, reflect state, reorder and remove without redund
syncChipGroup(f.container, []);
assert.deepEqual(f.container.children, [legend]);
});
test('a chip colour is set once, changed, and cleared through a CSS variable', () => {
const f = railFixture(() => {});
const calls = [];
syncChipGroup(f.container, [{ id: 'm', label: 'M' }]);
const chip = f.container.children[0];
chip.style.setProperty = (name, value) => calls.push(['set', name, value]);
chip.style.removeProperty = (name) => calls.push(['remove', name]);
syncChipGroup(f.container, [{ id: 'm', label: 'M' }]);
assert.deepEqual(calls, [], 'a chip without a colour never touches style');
syncChipGroup(f.container, [{ id: 'm', label: 'M', color: '#05cb63' }]);
syncChipGroup(f.container, [{ id: 'm', label: 'M', color: '#05cb63' }]);
syncChipGroup(f.container, [{ id: 'm', label: 'M', color: '#a66bff' }]);
syncChipGroup(f.container, [{ id: 'm', label: 'M' }]);
assert.deepEqual(calls, [
['set', '--chip-color', '#05cb63'],
['set', '--chip-color', '#a66bff'],
['remove', '--chip-color'],
]);
assert.equal(chip.dataset.chipColor, undefined);
});
+1
View File
@@ -91,6 +91,7 @@ function presentProviders(state) {
return {
id: provider.id,
label: provider.label,
color: provider.color || null,
title,
active: on,
disabled: false,
+21 -5
View File
@@ -18,7 +18,7 @@ const provider = (overrides = {}) => ({
count: 0,
hint: '',
error: null,
legend: [],
color: '#05cb63',
...overrides,
});
@@ -30,9 +30,7 @@ function snapshot(overrides = {}) {
providers: [provider()],
coverage: { loading: false, count: 0, hint: '', error: null },
legend: [
{ key: 'mapillary:recent', label: 'Recent (≤2 yr)', color: '#05cb63' },
{ key: 'mapillary:older', label: 'Older', color: '#2e7d5b' },
{ key: 'mapillary:pano', label: '360°', color: '#ff4fd8' },
{ key: 'mapillary', label: 'Mapillary', color: '#05cb63' },
{ key: 'selected', label: 'Selected', color: '#00d4ff' },
],
sequence: { providerId: null, selectedId: null, images: 0, loading: false },
@@ -239,7 +237,25 @@ test('legend passes through in the layer’s order', () => {
const view = presentStreetLevelPanel(snapshot());
assert.deepEqual(
view.legend.map((entry) => entry.key),
['mapillary:recent', 'mapillary:older', 'mapillary:pano', 'selected'],
['mapillary', 'selected'],
);
});
test('each provider chip carries its source colour', () => {
const view = presentStreetLevelPanel(
snapshot({
providers: [
provider(),
provider({ id: 'panoramax', label: 'PANORAMAX', color: '#a66bff' }),
],
}),
);
assert.deepEqual(
view.providers.map((chip) => [chip.id, chip.color]),
[
['mapillary', '#05cb63'],
['panoramax', '#a66bff'],
],
);
});
+32
View File
@@ -139,6 +139,38 @@ button.sl-status:focus-visible {
gap: 4px;
}
/* One colour per source: a swatch dot, and the whole chip in that colour
while the source is on. `--chip-color` comes from the provider. */
.sl-chips .data-toggle-chip {
display: inline-flex;
align-items: center;
gap: 5px;
}
.sl-chips .data-toggle-chip::before {
content: '';
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--chip-color, currentColor);
opacity: 0.45;
flex: none;
}
.sl-chips .data-toggle-chip.active {
color: var(--chip-color, var(--accent));
border-color: var(--chip-color, var(--accent));
background: color-mix(
in srgb,
var(--chip-color, var(--accent)) 14%,
transparent
);
}
.sl-chips .data-toggle-chip.active::before {
opacity: 1;
}
.sl-chips .data-toggle-chip.chip-error {
color: #ffb300;
border-color: rgba(255, 179, 0, 0.55);