From 493bbf45b83dd76311251593623ad2010e83bfa1 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 14:24:17 +0000 Subject: [PATCH 01/15] Set an alarm on the zone/region id collision, which data currently hides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gospel found a latent ambiguity in the emit contract while wiring, and checked it instead of building past it. selectedId emits one bare string with no kind, and region ids and zone ids share a namespace. Re-measured here rather than taken on report: 33 zone ids, 7 region ids, and the intersection is exactly europe, whose region is itself. So the string "europe" cannot tell a consumer whether the Europe region row was clicked, meaning every European zone, or the Europe zone leaf, meaning that one. It is not live today, and only for a reason that is a coincidence rather than a design: region europe contains exactly one zone, so both readings give the same set. It goes live the day a second European zone is filed, and then it fails the quiet way this whole file exists to catch — a smaller result, no error, no empty state. So this is an alarm, not a fix. The consumer builds the tree, so only the consumer knows which rows are regions, and gospel is namespacing theirs. Two arms: the colliding set is pinned and says re-derive if it changes, and each colliding id's region must still contain exactly one zone. The second fails with the instruction rather than the symptom, naming the zones and telling whoever hits it to namespace their region rows. Both falsified before trusting them: filing a second European zone fires the ambiguity arm with both zones named, and adding a second colliding id fires the drift arm with both ids listed. Data restored, clean diff. The same note is on selectedId, because the next consumer to build a tree from this zone data inherits the collision, and the doc as written invited it. --- internal/check-list-selection.mjs | 27 +++++++++++++++++++++ library/components/legends/listSelection.js | 16 ++++++++++++ 2 files changed, 43 insertions(+) diff --git a/internal/check-list-selection.mjs b/internal/check-list-selection.mjs index be60492f7..c64ea78c8 100644 --- a/internal/check-list-selection.mjs +++ b/internal/check-list-selection.mjs @@ -126,5 +126,32 @@ ok(realGap.join(',') === MISSING.join(','), ok(joinable.has('asia-china-xinjiang-uyghur-autonomous-region') && !filed.has('asia-china-xinjiang-uyghur-autonomous-region'), 'xinjiang specifically: joinable in admin-to-list, absent from zone-regions — its 100 groups are reachable ONLY through the bucket') +// ── the id ambiguity that is masked by a data coincidence (gospel, END-GOS-VLGC) ──────────────── +// ★ REGION IDS AND ZONE IDS SHARE ONE NAMESPACE. selectedId emits one bare string with no kind, and +// the two sets OVERLAP in the shipped data. gospel found it while wiring and I re-measured it here +// rather than taking the report: 33 zone ids, 7 region ids, intersection exactly {europe}, whose +// region is itself. So `europe` cannot tell a consumer whether the Europe REGION row was clicked +// (expand to every European zone) or the Europe ZONE leaf (filter to that one). +// +// It is not live today ONLY because region `europe` contains exactly one zone, so both readings give +// the same set. That is a coincidence in the data, not a property of the design, and it is the same +// shape as the inert arm above: something green because reality currently happens to agree with it. +// The arm below is therefore the alarm, not the fix — the consumer builds the tree, so only the +// consumer can namespace its region rows, and gospel is doing that. +const zonesAll = new Set(Object.keys(regionOf)) +const regionsAll = new Set(Object.values(regionOf).map(String)) +const collide = [...zonesAll].filter((id) => regionsAll.has(id)).sort() +ok(collide.join(',') === 'europe', + collide.join(',') === 'europe' + ? 'region ids and zone ids share one namespace and collide on exactly {europe} — measured, not assumed' + : `THE COLLIDING IDS CHANGED — re-derive. shipped now: [${collide.join(', ') || 'none'}], this check asserts: [europe]. Every id in both sets is one a consumer cannot disambiguate from the emitted string alone.`) +for (const id of collide) { + const members = Object.keys(regionOf).filter((z) => String(regionOf[z]) === id) + ok(members.length === 1, + members.length === 1 + ? `and region '${id}' still contains exactly one zone, so both readings of '${id}' give the same set — the ambiguity is MASKED, not absent` + : `THE '${id}' AMBIGUITY IS NOW LIVE: region '${id}' contains ${members.length} zones (${members.join(', ')}), so emitting the bare id '${id}' means either that whole region or the single zone of the same name, and the two now differ. Nothing on screen would say which — the reader just gets a smaller set. The consumer that BUILDS the tree must namespace its region rows (region:${id}) and leave leaves bare; this component cannot know which rows are regions.`) +} + console.log(fails ? `\nLIST-SELECTION: ${fails} FAIL(s)` : '\nLIST-SELECTION: serialisable ids out, and no joinable zone can be dropped') process.exit(fails ? 1 : 0) diff --git a/library/components/legends/listSelection.js b/library/components/legends/listSelection.js index 14a79e795..5758dc3e5 100644 --- a/library/components/legends/listSelection.js +++ b/library/components/legends/listSelection.js @@ -64,6 +64,22 @@ export function withUnassigned(nodes, allZoneIds, label = 'Unassigned') { * the union of its zones). The consumer expands a region id to its zones using the zone data it * already holds — which is also where the diacritic-folding join lives, so there is no second copy * of it here to drift. + * + * ★ REGION IDS AND ZONE IDS SHARE ONE NAMESPACE, SO NAMESPACE YOUR REGION ROWS (e.g. `region:europe`, + * leaves left bare). One bare string on one channel carries no kind, and the two id sets overlap in + * the shipped data: measured against zone-regions.json there are 33 zone ids and 7 region ids, and + * the intersection is exactly {europe}, whose region is itself. So `europe` alone cannot tell a + * consumer whether the Europe REGION was clicked (expand to every European zone) or the Europe ZONE + * leaf (filter to that one). + * + * It is not live TODAY only because region `europe` currently contains exactly one zone, so both + * readings produce the same set — the bug is masked by a data coincidence, not absent. It goes live + * the day a second European zone is filed, and then it fails the quiet way: a smaller result, no + * error, no empty state. check:list-selection FAILS ON PURPOSE at that moment. + * + * This component cannot fix it: the CONSUMER builds the tree, so only the consumer knows which rows + * are regions. Found by gospel-plugin while wiring, who is namespacing their region rows; written + * here because the next consumer to build a tree from the same zone data inherits the same collision. */ export function selectedId(node) { return node && node.id != null ? String(node.id) : null From f3c050aed13ab12b7dd2158475013226ad1f67f8 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 14:28:56 +0000 Subject: [PATCH 02/15] Move the pinned gap, and assert the filing that closed it upg100 filed asia-india-madhya-pradesh and this check went red, which is the arm working. They reported it instead of deleting my expectation to make their own change green, and they were right that doing so is how a guard gets hollowed out. dx-shepherd ruled the two land together, which is the only ordering where neither of us is red in between. THIS COMMIT IS RED ON MAIN BY ITSELF. It expects three unfiled zones and today's data has four. It goes green with upg100's data change and not before, so the two must land in the same landing. The gap pin drops to guizhou, xinjiang and bihar. Filing is the good direction but it is exactly as revertible as any other data edit, so the three newly filed India zones now get an arm of their own: drop a key from zone-regions and those groups quietly leave the region tree again, with the bucket the only thing still carrying them. Two of the three were never visible to the gap arms at all, so without this they could be unfiled with nothing to say so. Also recorded what the gap number actually counts. It is admin-to-list's values, which is not every printable zone, so it is a lower bound rather than a census. upg100 proved that costs something: checking the property instead of the four names they were handed, they found four more unfiled zones, none of them an admin-to-list value, so none of them ever appeared in this number. A count that is quietly a lower bound reads exactly like a complete one. And the fixture arm no longer hardcodes the number four. The gap changes; a literal there turns every legitimate data fix into a confusing red in an arm that is not even about the data. It counts the pin instead. --- internal/check-list-selection.mjs | 37 +++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/internal/check-list-selection.mjs b/internal/check-list-selection.mjs index c64ea78c8..4d90bc7e0 100644 --- a/internal/check-list-selection.mjs +++ b/internal/check-list-selection.mjs @@ -35,8 +35,13 @@ const MISSING = [ 'asia-china-guizhou-province', 'asia-china-xinjiang-uyghur-autonomous-region', 'asia-india-bihar', - 'asia-india-madhya-pradesh', ] +// asia-india-madhya-pradesh WAS here and is now FILED. upg100 hit this arm while filing it, and +// did the right thing with it: they reported the red instead of editing my expectation to make +// their change green — "editing someone else's pinned expectation to make my own change green is +// precisely the move that hollows out a guard". dx-shepherd then ruled that their data and this +// fixture land TOGETHER, which is the only ordering where neither of us is red in between. + ok(leavesOf(TREE[0]).map((l) => l.id).join(',') === 'z1,z2,z3', 'leavesOf walks NESTED children, not one level') ok([...zoneIdsIn(TREE)].sort().join(',') === 'z1,z2,z3', 'zoneIdsIn collects every leaf id in the tree') @@ -53,11 +58,15 @@ const all = ['z1', 'z2', 'z3', ...MISSING] const withBucket = withUnassigned(TREE, all, 'Unassigned') ok(withBucket.length === TREE.length + 1, 'zones absent from the tree get an explicit bucket') const bucket = withBucket[withBucket.length - 1] -ok(bucket.id === UNASSIGNED_ID && bucket.children.length === 4, 'and all four missing zones are in it') +// Counted from MISSING, never written as a literal: the gap CHANGES (upg100 filed one while I was +// writing this), and a hardcoded 4 here turns every legitimate data fix into a puzzling red in an +// arm that is not even about the data. +ok(bucket.id === UNASSIGNED_ID && bucket.children.length === MISSING.length, + `and all ${MISSING.length} missing zones are in it`) ok(bucket.children.map((c) => c.id).includes('asia-china-xinjiang-uyghur-autonomous-region'), 'including xinjiang — the consumer\'s largest zone at 100 groups, which a region tree loses silently') ok(bucket.children.every((c) => c.id && c.zoneKey), - 'each is a SELECTABLE leaf, not a lump — a reader wants xinjiang, not "the four nobody filed"') + 'each is a SELECTABLE leaf, not a lump — a reader wants xinjiang, not "the ones nobody filed"') ok(withUnassigned(TREE, ['z1', 'z2', 'z3']).length === TREE.length, 'CONTROL: no bucket appears when nothing is missing') ok(withUnassigned(TREE, null).length === TREE.length && withUnassigned(TREE, []).length === TREE.length, @@ -70,7 +79,7 @@ ok(!zoneIdsIn(naive).has('asia-china-xinjiang-uyghur-autonomous-region'), // ── a region sweep is NOT a zone sweep (gospel, END-GOS-VLG2) ─────────────────────────────────── // ★ The consequence of the bucket that neither of us should leave implicit: ticking every REGION -// still does not select every ZONE, because the four unfiled zones have no region BY DEFINITION. +// still does not select every ZONE, because the unfiled zones have no region BY DEFINITION. // Nothing on screen would say so — the reader just gets a smaller total, which is the exact // silent-wrong-answer shape the bucket exists to end. So if a "select all" is ever added it must // sweep ZONES, not regions, or it inherits the bug. Pinned here so the wrong one cannot ship @@ -114,6 +123,14 @@ const adminToList = readJson('admin-to-list.json') // admin name -> zone id const filed = new Set(Object.keys(regionOf)) const joinable = new Set(Object.values(adminToList).map(String)) const realGap = [...joinable].filter((id) => !filed.has(id)).sort() +// ★ WHAT THIS GAP COUNTS, AND WHAT IT DOES NOT. `joinable` is admin-to-list's VALUES — zones +// reachable by an admin-area join. That is NOT the same as every printable zone, so this arm can +// only ever report a LOWER BOUND on unfiled zones. upg100 proved the difference costs something: +// checking the property rather than the four names they were handed, they found deaf-old-world, +// deaf-new-world-asia-pacific, asia-india-uttarakhand and asia-india-west-bengal also unfiled, +// and none of the four is an admin-to-list value, so none of them appears in realGap. Two were +// then filed; the deaf pair look deliberate (there is a "deaf": "exempt" umbrella key). Stated +// here because a gap number that is quietly a lower bound reads exactly like a complete one. ok(realGap.length > 0, realGap.length > 0 @@ -123,6 +140,18 @@ ok(realGap.join(',') === MISSING.join(','), realGap.join(',') === MISSING.join(',') ? 'and it is exactly the four this check names, so the fixture above models the shipped data rather than a story about it' : `THE GAP MOVED — re-derive. shipped now: [${realGap.join(', ')}] but this check asserts: [${MISSING.join(', ')}]. A zone ADDED here is groups that just became unreachable; a zone REMOVED got filed and the fixture is stale. Either way the numbers in the commit body are no longer true.`) +// ── the zones that got FILED stay filed (dx-shepherd RG12) ───────────────────────────────────── +// The gap arms watch the BAD direction. Filing is the good one, and it is exactly as revertible: +// drop a key from zone-regions and those groups leave the region tree again, silently, with the +// bucket the only thing still carrying them. Two of these three were never visible to the gap arms +// at all (see the coverage note below), so without this they could be unfiled with nothing to say so. +const FILED = ['asia-india-madhya-pradesh', 'asia-india-uttarakhand', 'asia-india-west-bengal'] +for (const z of FILED) { + ok(filed.has(z), filed.has(z) + ? `${z} is filed under a region, so it reaches readers through the tree and not only the bucket` + : `${z} IS NO LONGER FILED — it was given a region and has lost it, so its groups are back to being reachable only through the unassigned bucket. That is the silent direction: no error, just a shorter region tree.`) +} + ok(joinable.has('asia-china-xinjiang-uyghur-autonomous-region') && !filed.has('asia-china-xinjiang-uyghur-autonomous-region'), 'xinjiang specifically: joinable in admin-to-list, absent from zone-regions — its 100 groups are reachable ONLY through the bucket') From 7e99159cb8707cce2ff22114dec13a0f79f57a8f Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 13:58:20 +0000 Subject: [PATCH 03/15] Stop the dimmed pins reading as white rings, and lift the chooser map rega shot the hover dim at 1440 and called the non-hovered pins pale rings on the pastel fills. That is what they were, and the cause is a property that was never set: circle-opacity dims the FILL only, and circle-stroke-opacity is a separate property defaulting to 1. So a pin dimmed to 0.25 kept a fully opaque 1px white stroke, and at the dimmed radius that ring is most of the pin. Worth saying plainly, because the obvious fix was to keep the stroke opaque while dimming the fill: that was already the behaviour, and it was the cause rather than the cure. The stroke now dims with the fill, so a non-hovered pin recedes as a dot instead of becoming an outline. The fill dims less as well, 0.25 to 0.45, and the dimmed radius goes 2.5 to 3 at low zoom for the same reason the resting state was raised earlier: a stroke is a fixed px cost, so at radius 2.5 the coloured core carrying the region's identity is about 1.5px. dx-life's two modes keep an opaque stroke; only the chooser's dim is ours. The property is set in both places. A paint property added at layer creation but not re-applied where the mode is re-applied never updates on hover, which is the bug this file already carries a warning about. The map below the fold, measured at 1440 with the container at 1200: the map's top sits at about 583px and the map is 780px tall, because min-height beats what 16/9 asks for at that width. A reader sees roughly its top 190px. Tightening the three gaps above it and dropping the outsized title one step recovers about 99px, so about 286px shows on load. It does not put the whole map above the fold and nothing leaving the map 780px tall can; that needs the map bounded by viewport height, which changes its rendered size and is on the Driver's sheet. Scoped to the all-regions page and to 1200px and up, because the region and country pages are the clone and their rhythm is measured against dx-life. Rebuilt on current main rather than merged, so the shared manifests carry every other bundle's current hash and only this bundle's moved: four lines, one integrity plus the three script tags that embed it. --- .../regions-dashboard/region-dashboards.vue | 26 ++++++- .../src/RegionCountryMap.vue | 20 ++++- public/dx-maps/doxa-maps-spa/manifest.json | 8 +- public/dx-maps/manifest.json | 8 +- .../region-dashboards/index.html | 2 +- .../regions-dashboard/regions-dashboard.js | 76 +++++++++---------- .../regions-dashboard/regions-list/index.html | 2 +- .../regions-dashboard/regions-mfe/index.html | 2 +- 8 files changed, 90 insertions(+), 54 deletions(-) diff --git a/plugin-designs/regions-dashboard/region-dashboards.vue b/plugin-designs/regions-dashboard/region-dashboards.vue index acec9233d..1b7465d99 100644 --- a/plugin-designs/regions-dashboard/region-dashboards.vue +++ b/plugin-designs/regions-dashboard/region-dashboards.vue @@ -848,6 +848,30 @@ useShadowStyles( '.rd-chooser{ display:flex; flex-wrap:wrap; gap:13.3px; align-items:flex-end;' + ' justify-content:center; }' + // .stack .stack--sm — --spacing-sm, 1.33rem. + // ── the map sat below the fold at 1440, and this is the part of that I can fix without + // ── touching the clone or the order ──────────────────────────────────────────────────────── + // ARITHMETIC AT 1440, container 1200px, tokens resolved: padding-top 55.3 + head 147 + 3 gaps of + // 55.3 + chooser ~62 + stats ~110 + note 29 + 13.3 puts the map's TOP at ~583px, and the map is + // 780px tall because min-height:780px beats what 16/9 asks for at 1200 wide (675). Bottom ~1363. + // So a reader at 1440x900 sees roughly the top 190px of an 780px map: a sliver, which is what + // "below the fold" meant. + // + // WHAT THIS DOES AND DOES NOT DO. Tightening the three gaps above the map from --spacing-3xl to + // --spacing-xl and dropping the outsized title one step recovers ~99px, so ~286px of map shows + // instead of ~190 — about 50% more map on load. It does NOT get the whole map above the fold, + // and nothing that leaves the map at 780px can: you would need the map under ~320px tall. That + // fix exists (bound .rcm by viewport height) but it changes the map's rendered SIZE, which is a + // change to what the Campaign Server shows, so it is the Driver's call and not mine. + // + // SCOPED TO THE ALL-REGIONS PAGE ONLY (.rd-root--all), and at >=1200px only. That page is ours; + // the region and country pages are the clone, and their vertical rhythm is measured against + // dx-life, so a global trim here would quietly un-clone three pages to improve one. + '@media (min-width:1200px){' + + ' .rd-root--all .rd-scroll > .rd-chooser,' + + ' .rd-root--all .rd-scroll > .rd-stats,' + + ' .rd-root--all .rd-scroll > .rd-mapblock{ margin-top:var(--spacing-xl); }' + + ' .rd-root--all .rd-title{ font-size:var(--font-size-3xl); }' + + '}' + '.rd-mapblock{ display:flex; flex-direction:column; }' + '.rd-mapblock > * + *{ margin-top:13.3px; }' + '.rd-mapnote{ margin:0; text-align:center; color:#92b195; font-weight:700; font-size:var(--font-size-md); }' + @@ -990,7 +1014,7 @@ useShadowStyles( see the host document's attribute, so the switch their global sheet performs on [lang="ru-RU"] can only fire if the attribute is INSIDE the root. It is also simply correct HTML — screen readers and hyphenation read it. --> -
+
diff --git a/plugin-designs/regions-dashboard/src/RegionCountryMap.vue b/plugin-designs/regions-dashboard/src/RegionCountryMap.vue index d8394334d..c9e008b1e 100644 --- a/plugin-designs/regions-dashboard/src/RegionCountryMap.vue +++ b/plugin-designs/regions-dashboard/src/RegionCountryMap.vue @@ -228,7 +228,16 @@ function pinExpressions() { return { color: regionColorExpression(), sortKey: lit ? ['case', isLit, 2, 1] : 1, - opacity: lit ? ['case', isLit, 1, 0.25] : 1, + opacity: lit ? ['case', isLit, 1, 0.45] : 1, + // ★ THE DIM DIMS THE STROKE TOO, AND THAT IS THE WHOLE FIX. circle-opacity applies to the + // FILL only; circle-stroke-opacity is a separate property that defaults to 1. So the old + // 0.25 dim left the 1px white stroke fully opaque, and at the dimmed radius that ring is + // most of the pin: a reader saw a crisp white O with a washed-out centre, not a receded dot. + // rega shot it (pins-a5eb9f6fa/hover2/maplibre-1440-hover.png) and called them pale rings, + // which is exactly what an opaque stroke around a faint core looks like on a pastel fill. + // Worth being precise, because "keep the stroke opaque while dimming the fill" was the + // proposed fix and it was already the behaviour — it was the CAUSE, not the cure. + strokeOpacity: lit ? ['case', isLit, 1, 0.3] : 1, // THE RESTING STATE IS BIGGER THAN THE CLONE'S, and only here. The Driver reported the pins // as hard to see; this is the state that answers that, because it is what the page shows on // load, before any hover. Two things make a pin harder to read on THIS map than on a cloned @@ -244,7 +253,7 @@ function pinExpressions() { // The HOVER state is deliberately untouched — dimming the rest to 0.25 is the design saying // "these pins are one group", and that is a Driver taste call, not a legibility defect. radius: lit - ? ['case', isLit, ['interpolate', ['linear'], ['zoom'], 2, 4.5, 6, 8], ['interpolate', ['linear'], ['zoom'], 2, 2.5, 6, 5]] + ? ['case', isLit, ['interpolate', ['linear'], ['zoom'], 2, 4.5, 6, 8], ['interpolate', ['linear'], ['zoom'], 2, 3, 6, 5.5]] : ['interpolate', ['linear'], ['zoom'], 2, 4, 6, 7.5], } } @@ -257,6 +266,7 @@ function pinExpressions() { ? ['case', outsideFocus, 0, full, 3, some, 2, 1] : ['case', ['get', 'inCountry'], 1, 0], opacity: ['case', ['get', 'inCountry'], 1, 0.75], + strokeOpacity: 1, // theirs, unchanged (CountryMap.vue) — only the chooser's dim is ours radius: ['interpolate', ['linear'], ['zoom'], 2, 3, 6, 6.5], } } @@ -264,7 +274,7 @@ function pinExpressions() { function addLayers() { const m = map.value if (!m) return - const { color: pinColor, sortKey: pinSortKey, opacity: pinOpacity, radius: pinRadius } = pinExpressions() + const { color: pinColor, sortKey: pinSortKey, opacity: pinOpacity, radius: pinRadius, strokeOpacity: pinStrokeOpacity } = pinExpressions() if (!m.getSource(SRC_COUNTRIES)) { // ── COUNTRY POLYGONS, PER PROVIDER ──────────────────────────────────────────────────────── @@ -329,6 +339,7 @@ function addLayers() { 'circle-opacity': pinOpacity, 'circle-stroke-width': 1, 'circle-stroke-color': '#ffffff', + 'circle-stroke-opacity': pinStrokeOpacity, }, }) m.on('click', 'rcm-pins', onPinClick) @@ -596,9 +607,10 @@ function applySubject() { if (src) src.setData(geojson.value) // Re-apply the MODE, not just the data — see pinExpressions(). if (m.getLayer('rcm-pins')) { - const { color, sortKey, opacity, radius } = pinExpressions() + const { color, sortKey, opacity, radius, strokeOpacity } = pinExpressions() m.setPaintProperty('rcm-pins', 'circle-color', color) m.setPaintProperty('rcm-pins', 'circle-opacity', opacity) + m.setPaintProperty('rcm-pins', 'circle-stroke-opacity', strokeOpacity) m.setPaintProperty('rcm-pins', 'circle-radius', radius) m.setLayoutProperty('rcm-pins', 'circle-sort-key', sortKey) } diff --git a/public/dx-maps/doxa-maps-spa/manifest.json b/public/dx-maps/doxa-maps-spa/manifest.json index 8570f494d..894a736ac 100644 --- a/public/dx-maps/doxa-maps-spa/manifest.json +++ b/public/dx-maps/doxa-maps-spa/manifest.json @@ -1080,7 +1080,7 @@ "bundle": "regions-dashboard", "scriptUrl": "./regions-dashboard/regions-dashboard.js", "output": "dx-maps/regions-dashboard/regions-dashboard.js", - "integrity": "sha384-JNfHBIQiXgHSUg0i/wd43Q0XMPGuo2GNOm0jRdD7AQvWg+TZUdpRHKtwqkqhRCNo", + "integrity": "sha384-/RARe73nUORH8xOqfu+fHBot96aJ9qN0lASLexQE5kaAHBQrC5JGBFOzVlexWphH", "elements": [ "regions-dashboard" ], @@ -1103,7 +1103,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n" + "script": "\n\n\n\n" } }, { @@ -1119,7 +1119,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n" + "script": "\n\n" } }, { @@ -1135,7 +1135,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n" + "script": "\n\n\n\n" } } ] diff --git a/public/dx-maps/manifest.json b/public/dx-maps/manifest.json index 8570f494d..894a736ac 100644 --- a/public/dx-maps/manifest.json +++ b/public/dx-maps/manifest.json @@ -1080,7 +1080,7 @@ "bundle": "regions-dashboard", "scriptUrl": "./regions-dashboard/regions-dashboard.js", "output": "dx-maps/regions-dashboard/regions-dashboard.js", - "integrity": "sha384-JNfHBIQiXgHSUg0i/wd43Q0XMPGuo2GNOm0jRdD7AQvWg+TZUdpRHKtwqkqhRCNo", + "integrity": "sha384-/RARe73nUORH8xOqfu+fHBot96aJ9qN0lASLexQE5kaAHBQrC5JGBFOzVlexWphH", "elements": [ "regions-dashboard" ], @@ -1103,7 +1103,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n" + "script": "\n\n\n\n" } }, { @@ -1119,7 +1119,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n" + "script": "\n\n" } }, { @@ -1135,7 +1135,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n" + "script": "\n\n\n\n" } } ] diff --git a/public/dx-maps/regions-dashboard/region-dashboards/index.html b/public/dx-maps/regions-dashboard/region-dashboards/index.html index 3c9e9780b..20754b322 100644 --- a/public/dx-maps/regions-dashboard/region-dashboards/index.html +++ b/public/dx-maps/regions-dashboard/region-dashboards/index.html @@ -36,7 +36,7 @@ - + - + - + \n\n\n" + "script": "\n\n\n\n\n" } }, { @@ -1119,7 +1119,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1135,7 +1135,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n" + "script": "\n\n\n\n\n" } } ] diff --git a/public/dx-maps/manifest.json b/public/dx-maps/manifest.json index d7feb7f0a..f8100e5a9 100644 --- a/public/dx-maps/manifest.json +++ b/public/dx-maps/manifest.json @@ -1080,7 +1080,7 @@ "bundle": "regions-dashboard", "scriptUrl": "./regions-dashboard/regions-dashboard.js", "output": "dx-maps/regions-dashboard/regions-dashboard.js", - "integrity": "sha384-Y2zDvJvo/WXHjIvwdvxG0ir3G1mdXatHdyi6EvHWdr+/8yTl4D8TxWQtT9TYF5Pz", + "integrity": "sha384-vBU9kwruSBwjsHpUaw/esnUnJQKv2mbVbVLaZFHF2t7O9ZnLmR8r6MrbYATplAPC", "elements": [ "regions-dashboard" ], @@ -1103,7 +1103,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n" + "script": "\n\n\n\n\n" } }, { @@ -1119,7 +1119,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n" + "script": "\n\n\n" } }, { @@ -1135,7 +1135,7 @@ "scriptUrl": "./regions-dashboard/regions-dashboard.js", "embed": { "iframe": "", - "script": "\n\n\n\n\n" + "script": "\n\n\n\n\n" } } ] diff --git a/public/dx-maps/regions-dashboard/region-dashboards/index.html b/public/dx-maps/regions-dashboard/region-dashboards/index.html index f634fe911..1986399a8 100644 --- a/public/dx-maps/regions-dashboard/region-dashboards/index.html +++ b/public/dx-maps/regions-dashboard/region-dashboards/index.html @@ -36,7 +36,7 @@ - + - + - + + + + + diff --git a/library/components/legends/LegendDesktop.vue b/library/components/legends/LegendDesktop.vue index 1c8ee13f1..3db1899fb 100644 --- a/library/components/legends/LegendDesktop.vue +++ b/library/components/legends/LegendDesktop.vue @@ -13,6 +13,7 @@ import { retryableAsyncComponent } from '../../composables/retryableAsyncCompone import LegendRows from './LegendRows.vue' import PeopleGroupDetail from '../PeopleGroupDetail.vue' import ApproxPinNote from './ApproxPinNote.vue' +import LegendCollapseCaret from './LegendCollapseCaret.vue' import { buildColocationIndex, colocatedWith } from '../../utils/colocation.js' // Async-import the heavier branches so the boot bundle stays lean. const LegendMobile = retryableAsyncComponent(() => import('./LegendMobile.vue')) @@ -68,11 +69,6 @@ useShadowStyles(` LegendRows. Matches SemanticTreeLegend's .stl-collapse-btn look: 22x20px, rounded-rect, subtle dark bg + border, right-chevron SVG that rotates 180deg when expanded. */ -.legend-collapse-caret{background:rgba(110,118,129,0.15);border:1px solid #30363d;border-radius:5px;color:#8b949e;cursor:pointer;width:22px;height:20px;flex-shrink:0;display:flex;align-items:center;justify-content:center;transition:color 0.1s,background 0.1s,transform 0.18s;outline:none;-webkit-tap-highlight-color:transparent;padding:0;} -.legend-collapse-caret:focus{outline:none;} -.legend-collapse-caret:hover{color:#c9d1d9;background:rgba(59,70,61,0.18);border-color:#73A17F;} -.legend-collapse-caret svg{transition:transform 0.18s;transform:rotate(180deg);} -.legend-collapse-caret.rotated svg{transform:rotate(0deg);} /* Light mode caret — mirrors STL's [data-theme="light"] .stl-collapse-btn */ .legend-container:not(.legend-dark) .legend-collapse-caret{background:rgba(208,215,222,0.4);border-color:#d0d7de;color:#57606a;} .legend-container:not(.legend-dark) .legend-collapse-caret:hover{background:rgba(59,70,61,0.12);border-color:#3b463d;color:#3b463d;} @@ -86,8 +82,14 @@ useShadowStyles(` above the PeopleGroupDetail sticky header (.detail-header z-index:10) so it is always tappable even when the detail panel scrolls. The button is only rendered in the detail-mode template branch (see