P: merge bba382d9a
This commit is contained in:
commit
0f7d69918c
26 changed files with 155 additions and 184 deletions
|
|
@ -183,7 +183,7 @@
|
|||
|
||||
<!-- ★ BACK TO TOP, BESIDE THE VIEW-LIST CIRCLE. The gallery owns both the circle
|
||||
and the scroller, so this needs no shadow-root walking: the target is the ACTIVE .dms-pane,
|
||||
which rega measured as the real scroller (clientHeight 712, scrollHeight 12,373) while the
|
||||
which measured as the real scroller (clientHeight 712, scrollHeight 12,373) while the
|
||||
document does not scroll and no .vtl-host exists in this composition.
|
||||
Horizontal placement comes from --map-fab-beside, which VerticalTabsLayout publishes ONLY
|
||||
while its circle renders (12px inset + 44px button + 8px gap), so the arrow sits next to the
|
||||
|
|
@ -904,7 +904,7 @@ export default defineComponent({
|
|||
return tStatic('messages.largerScreenNotice', this.uiLocale)
|
||||
|| 'This experimental map is built for larger screens. It opens here, but parts of it may not fit or work well on a phone.'
|
||||
},
|
||||
// ★ THE SCROLLER IS THIS PANE, MEASURED, NOT VTL'S. rega at 390x844 on the live build:
|
||||
// ★ THE SCROLLER IS THIS PANE, MEASURED, NOT VTL'S. At 390x844 on the live build:
|
||||
// section.dms-pane has clientHeight 712 and scrollHeight 12,373 with overflow-y:auto, while
|
||||
// the document does not scroll at all and there is NO .vtl-host element in the tree.
|
||||
// VerticalTabsLayout does render a BackToTopButton, but it binds the active .vtl-host, which
|
||||
|
|
@ -1684,7 +1684,7 @@ export default defineComponent({
|
|||
That floor measures against the VIEWPORT while this pane measures against its HOST, so on any page
|
||||
with chrome above the gallery the profile is taller than the pane by exactly the height of that
|
||||
chrome -- and .dms-pane's overflow-y:auto then scrolls the whole plugin, tab strip included.
|
||||
MEASURED by rega on DX Life: 1440x900 viewport, 794px pane, content exactly 900 (106px over, the
|
||||
MEASURED on DX Life: 1440x900 viewport, 794px pane, content exactly 900 (106px over, the
|
||||
header); 390x844, 712px pane, content exactly 844 (132px). On 12 of 16 tabs, INCLUDING plugins with
|
||||
no min-height of their own -- which is what proves the floor is ProfileLoader's, not theirs.
|
||||
It cannot be fixed from outside: the floor is in the element's SHADOW ROOT where no host-page
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
*
|
||||
* ★ pos PLACES THE BUTTON CLEAR OF THE MAP'S OWN CONTROLS (--map-info-top / --map-info-side, default
|
||||
* 12px each). The button sits at the END of the line (right in English, left in Arabic, see MapInfo),
|
||||
* which is also where most of these maps keep their zoom column; measured on rega's 1440 captures in
|
||||
* which is also where most of these maps keep their zoom column; measured on 1440 captures in
|
||||
* en and ar (2026-09-29), 12px covered the zoom-in button on Prayer Progress, Cities and Gospel, Big
|
||||
* Picture's header buttons and Simple Research's tab bar. side 56px sits beside a zoom column; top
|
||||
* 46px / 56px clears Big Picture's 38px header bar / Simple Research's tab bar. Regions and UPG-100
|
||||
|
|
@ -75,7 +75,7 @@ export const TAB_INFO = {
|
|||
// groups, too many to fit one poster legibly (library/data/country-posters.json carries that
|
||||
// reason itself). China prints (a 3x4 sheet, 154 groups), so the note must not list it.
|
||||
noteKey: 'mapInfo.tab.regions-dashboard.note',
|
||||
// The Sudan poster, packet pages and card deck join when rega's shots are in the tree.
|
||||
// The Sudan poster, packet pages and card deck join when their shots are in the tree.
|
||||
examples: [],
|
||||
},
|
||||
'dashboard-plus-grid': {
|
||||
|
|
@ -89,7 +89,7 @@ export const TAB_INFO = {
|
|||
trigger: 'control',
|
||||
// top 46px is the empty END of Big Picture's filter area at every width (1 row at 1440, 2 at 1024,
|
||||
// 3 at 800), but that bar is a positioned layer (z-index 55) painted above the button, which left it
|
||||
// unclickable (rega, 1440). raise lifts the BUTTON over the bar; the pointer stays under it.
|
||||
// unclickable (1440). raise lifts the BUTTON over the bar; the pointer stays under it.
|
||||
pos: { top: '46px', raise: true },
|
||||
leadKey: 'mapInfo.tab.dashboard-plus-grid.lead',
|
||||
strategyKey: 'mapStrategy.tab.dashboard-plus-grid',
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@
|
|||
"config": {
|
||||
"pinHint": false
|
||||
},
|
||||
"note": "Monolithic composite (capability 3): ONE root .vue nests ten internal views of the PPLR people-group data (religion, ROP tree, language families, semantic trees, PG name locales, ROP sunburst, all pins, cross-system ID rosetta, translation coverage, index) behind a view-switcher. index.js registers exactly ONE tag (pplr-data-maps); no view is independently embeddable. pinHint:false because the shared coach-mark says 'Tap a coloured dot — someone still needs prayer', the prayer map's message, not this research tool's; on a phone it also sat on top of the view's own bottom bar and attribution (rega, 390)."
|
||||
"note": "Monolithic composite (capability 3): ONE root .vue nests ten internal views of the PPLR people-group data (religion, ROP tree, language families, semantic trees, PG name locales, ROP sunburst, all pins, cross-system ID rosetta, translation coverage, index) behind a view-switcher. index.js registers exactly ONE tag (pplr-data-maps); no view is independently embeddable. pinHint:false because the shared coach-mark says 'Tap a coloured dot — someone still needs prayer', the prayer map's message, not this research tool's; on a phone it also sat on top of the view's own bottom bar and attribution (390)."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,8 +79,8 @@ useShadowStyles(
|
|||
'.pdm-seg button.on{background:#92b195;border-color:#92b195;color:#fff}' +
|
||||
'.pdm-seg button:hover:not(.on){border-color:#92b195}' +
|
||||
'.pdm-stage{flex:1 1 0;position:relative;min-height:0}' +
|
||||
// PHONE (rega, 390×844): the view tabs ran past the right edge with no scroll cue ("Semantic Trees" at
|
||||
// x=460). Wrapped, the ten tabs took five rows (160 px of an 844 px phone, rega). On a phone the brand
|
||||
// PHONE (390×844): the view tabs ran past the right edge with no scroll cue ("Semantic Trees" at
|
||||
// x=460). Wrapped, the ten tabs took five rows (160 px of an 844 px phone). On a phone the brand
|
||||
// steps aside and the views are ONE select in one row; a wide screen keeps the tabs.
|
||||
'.pdm-select{display:none}' +
|
||||
'@media (max-width:640px){.pdm-bar{padding:6px 8px;gap:8px}.pdm-brand{display:none}.pdm-seg{display:none}' +
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
/**
|
||||
* MapboxGeocoder.vue — wraps mapbox-gl-geocoder npm plugin (loaded via CDN).
|
||||
*
|
||||
* DOXA's pattern, ported per user request 2026-05-02 ("your competitor is
|
||||
* beating you in that area. Their geocoder's directly mapped to language-
|
||||
* family legend"). Replaces the bespoke PplrGeocoder.vue dropdown with
|
||||
* DOXA's pattern, ported here: a geocoder mapped directly to the language-family
|
||||
* legend, as comparable tools offer. Replaces the bespoke PplrGeocoder.vue dropdown with
|
||||
* Mapbox's vetted UI/UX (keyboard nav, debouncing, ARIA, mobile-friendly)
|
||||
* while keeping local-data search via the localGeocoder callback.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@
|
|||
/**
|
||||
* SemanticSunburst — D3 sunburst with mouse-wheel zoom + drag pan.
|
||||
*
|
||||
* Per user 2026-05-02: replaced the original click-to-expand-arc behavior
|
||||
* with map-style scroll-wheel zoom + click-and-drag pan. Click-to-expand was
|
||||
* "kind of slow" and "I don't like it"; users want the same gesture they get
|
||||
* on a Mapbox map.
|
||||
* The original click-to-expand-arc behavior is replaced with map-style
|
||||
* scroll-wheel zoom + click-and-drag pan. Click-to-expand was slow, and readers
|
||||
* expect the same gesture they get on a Mapbox map.
|
||||
*
|
||||
* Labels are now visibility-tuned: only render labels for arcs that occupy
|
||||
* at least `LABEL_MIN_RADIANS` of arc, and font scales up at higher zoom
|
||||
|
|
@ -142,8 +141,8 @@ function build() {
|
|||
.style('paint-order', 'stroke').style('stroke', 'rgba(13,17,23,0.65)').style('stroke-width', '2.5px')
|
||||
.text(d => (d.data.name || '').replace(' Peoples', '').replace(' People', ''))
|
||||
|
||||
// Hover tooltip only — arc clicks no longer trigger zoom (per user request:
|
||||
// wheel-zoom + drag-pan instead of click-to-expand).
|
||||
// Hover tooltip only — arc clicks no longer trigger zoom (wheel-zoom + drag-pan
|
||||
// instead of click-to-expand).
|
||||
path.on('mousemove', (event, d) => {
|
||||
const depth = props.depthLabels[d.depth] || ''
|
||||
tooltip.style('display', 'block')
|
||||
|
|
@ -153,7 +152,7 @@ function build() {
|
|||
})
|
||||
.on('mouseleave', () => tooltip.style('display', 'none'))
|
||||
|
||||
// ── Wheel zoom + drag pan (Mapbox-style gesture, per user 2026-05-02) ──
|
||||
// ── Wheel zoom + drag pan (Mapbox-style gesture) ──
|
||||
// wheelDelta: tweak default for smoother small-step zooming.
|
||||
const zoomBehavior = d3.zoom()
|
||||
.scaleExtent([0.5, 12])
|
||||
|
|
|
|||
|
|
@ -126,8 +126,8 @@ export function exposeMap(map) {
|
|||
}
|
||||
}
|
||||
|
||||
// Release the hook on unmount (a closure on window pins the whole app after the view is gone; rega
|
||||
// measured ~10 MB per mount). Only if it is still THIS map's: another view may have exposed its own since.
|
||||
// Release the hook on unmount (a closure on window pins the whole app after the view is gone; measured
|
||||
// at ~10 MB per mount). Only if it is still THIS map's: another view may have exposed its own since.
|
||||
export function releaseMap(map) {
|
||||
if (typeof window === 'undefined' || !map || _exposedMap !== map) return
|
||||
delete window.__pplrMap
|
||||
|
|
|
|||
|
|
@ -122,9 +122,9 @@ function fetchCountryPolygons() {
|
|||
* Returns { sourceId, dataReady: Promise<{ fc, sourceId }> }.
|
||||
* For blocking callers, await the returned promise directly.
|
||||
*/
|
||||
// Theme-aware paint presets. Light mode renders polygons fully invisible
|
||||
// (per user spec 2026-05-02: "I prefer on light mode not to even show the
|
||||
// Polygon blue borders"). Dark mode keeps the dark-gray fill + blue line.
|
||||
// Theme-aware paint presets. Light mode renders polygons fully invisible: on a light basemap the
|
||||
// polygons' blue borders only add clutter, so neither fill nor line is drawn. Dark mode keeps the
|
||||
// dark-gray fill + blue line.
|
||||
export const POLYGON_PAINT = {
|
||||
dark: { fillColor: '#1f2937', fillOpacity: 0.35, lineColor: '#3b82f6', lineOpacity: 0.5 },
|
||||
light: { fillColor: 'rgba(0,0,0,0)', fillOpacity: 0, lineColor: 'rgba(0,0,0,0)', lineOpacity: 0 },
|
||||
|
|
@ -224,7 +224,7 @@ export function setCountryPolygonTheme(map, theme, sourceId = 'pplr-countries')
|
|||
* touch + crowded zooms. We instead listen on the whole map and use
|
||||
* queryRenderedFeatures with a small bbox around the click point so the
|
||||
* effective hit-target is `tolerance × 2` pixels wide. No visual change to
|
||||
* the pin itself (per user 2026-05-02: don't restyle base pins).
|
||||
* the pin itself (base pins are deliberately not restyled).
|
||||
*
|
||||
* Cursor change still uses the direct mouseenter/mouseleave on the layer so
|
||||
* the cursor only goes pointer when the user is actually over a pin —
|
||||
|
|
@ -442,7 +442,7 @@ export class FullscreenToggleControl {
|
|||
this._btn.type = 'button'
|
||||
this._btn.className = 'mapboxgl-ctrl-fullscreen'
|
||||
// The shared, translated name (buttons.toggleFullscreen), in the page's language when the map is
|
||||
// built. rega saw the English "Toggle fullscreen" in ar and zh on every PPLR map view.
|
||||
// built. a browser check saw the English "Toggle fullscreen" in ar and zh on every PPLR map view.
|
||||
const label = tStatic('buttons.toggleFullscreen') || 'Toggle fullscreen'
|
||||
this._btn.setAttribute('aria-label', label)
|
||||
this._btn.title = label
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ function buildColorExpr() {
|
|||
}
|
||||
|
||||
// ── Projection + basemap ──────────────────────────────────────────────────────
|
||||
const projection = ref('mercator') // default mercator per user spec
|
||||
const projection = ref('mercator') // default: mercator
|
||||
const showBasemap = ref(false)
|
||||
const currentBasemap = ref('dark')
|
||||
const BASEMAPS = [
|
||||
|
|
@ -446,7 +446,7 @@ onMounted(async () => {
|
|||
map.value.on('load', async () => {
|
||||
// The constructor's projection:'mercator' is only a request: on Mapbox the style's own projection
|
||||
// (globe) wins, so the map booted as a globe while this view's state and button said Mercator, and
|
||||
// the first "Switch to Globe" click did nothing (rega, C2). The style is ready here, on both
|
||||
// the first "Switch to Globe" click did nothing (C2). The style is ready here, on both
|
||||
// engines, so apply the state to the map.
|
||||
setProjectionFor(map.value, projection.value)
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -72,9 +72,8 @@ function setBasemap(key) {
|
|||
|
||||
function setupLayers() {
|
||||
if (!pgData.value || !map.value) return
|
||||
// Religion profile: polygons hidden on BOTH themes (per user 2026-05-02 "I
|
||||
// kind of just want you on the religion map to remove the blue borders on
|
||||
// dark mode... remove the polygons"). Only pins + basemap show.
|
||||
// Religion profile: polygons hidden on BOTH themes (no blue borders, in dark
|
||||
// mode too). Only pins + basemap show.
|
||||
addCountryPolygons(map.value, {
|
||||
fillColor: 'rgba(0,0,0,0)', fillOpacity: 0,
|
||||
lineColor: 'rgba(0,0,0,0)', lineOpacity: 0,
|
||||
|
|
@ -124,7 +123,7 @@ const { map, isMapReady, initializeMap, destroy } = useMapInstance({
|
|||
projection: 'mercator' // pinned at construct time — no globe→mercator reproject flash
|
||||
})
|
||||
|
||||
// Color palette per user spec 2026-05-02:
|
||||
// Color palette:
|
||||
// - Christianity is the ONLY blue group (navy-ish)
|
||||
// - Ethnoreligion → pink (was purple)
|
||||
// - Judaism → purple (was cyan-blue; cannot share Christianity's hue)
|
||||
|
|
@ -270,7 +269,7 @@ onMounted(async () => {
|
|||
map.value.on('load', async () => {
|
||||
// The constructor's projection:'mercator' is only a request: on Mapbox the style's own projection
|
||||
// (globe) wins, so the map booted as a globe while this view's state and button said Mercator, and
|
||||
// the first "Switch to Globe" click did nothing (rega, C2). The style is ready here, on both
|
||||
// the first "Switch to Globe" click did nothing (C2). The style is ready here, on both
|
||||
// engines, so apply the state to the map.
|
||||
setProjectionFor(map.value, projection.value)
|
||||
try {
|
||||
|
|
@ -366,7 +365,7 @@ const REL_CSS = POPUP_CSS + `
|
|||
<button v-for="b in BASEMAPS" :key="b.key" class="basemap-opt" :class="{ active: currentBasemap === b.key }" @click="setBasemap(b.key)">{{ b.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Data sources — moved into the toolbar to match ROP-tree (per user 2026-05-02) -->
|
||||
<!-- Data sources — moved into the toolbar to match ROP-tree -->
|
||||
<div class="settings-wrap">
|
||||
<button class="toolbar-btn round" :title="t('pplr.ui.dataSources')" @click="showSettings = !showSettings">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ const hasPop = ref(false)
|
|||
const pgicLookup = ref({})
|
||||
|
||||
// ── Projection + basemap ──────────────────────────────────────────────────────
|
||||
const projection = ref('mercator') // default mercator per user spec
|
||||
const projection = ref('mercator') // default: mercator
|
||||
const showBasemap = ref(false)
|
||||
const currentBasemap = ref('dark')
|
||||
const BASEMAPS = [
|
||||
|
|
@ -149,8 +149,7 @@ function buildRopTreeOnce() {
|
|||
// Some rop-tree.json entries don't have matching rows in
|
||||
// pg-points.geojson — show them as "Unmapped · <code>" so the
|
||||
// user sees the rop25 ID with context, not a bare number
|
||||
// (per user 2026-05-02: "currently it's returning like these
|
||||
// numbers which is weird").
|
||||
// (a bare number reads as an error).
|
||||
const info = lookup[geoId]
|
||||
const label = info
|
||||
? (info.country ? `${info.name} · ${info.country}` : info.name)
|
||||
|
|
@ -422,7 +421,7 @@ onMounted(async () => {
|
|||
map.value.on('load', async () => {
|
||||
// The constructor's projection:'mercator' is only a request: on Mapbox the style's own projection
|
||||
// (globe) wins, so the map booted as a globe while this view's state and button said Mercator, and
|
||||
// the first "Switch to Globe" click did nothing (rega, C2). The style is ready here, on both
|
||||
// the first "Switch to Globe" click did nothing (C2). The style is ready here, on both
|
||||
// engines, so apply the state to the map.
|
||||
setProjectionFor(map.value, projection.value)
|
||||
try {
|
||||
|
|
@ -492,7 +491,7 @@ const TREE_POPUP_CSS = POPUP_CSS + `
|
|||
<template>
|
||||
<div ref="profileRoot" class="profile" :data-theme="pplrInstance.theme.value">
|
||||
<!-- Stays mounted until BOTH the basemap canvas AND the PG pin layer are
|
||||
ready (per user 2026-05-02: don't disappear before pins populate). -->
|
||||
ready, so it never disappears before the pins populate. -->
|
||||
<LoadingGlobe v-if="!isMapReady || !mapLoaded" :label="status" @retry="reload" />
|
||||
|
||||
<!-- Decoupled geocoder, top-left, sits above the legend; shares instance store -->
|
||||
|
|
|
|||
|
|
@ -99,9 +99,8 @@ onMounted(async () => {
|
|||
|
||||
// Rosetta sample (5 stars — slightly larger than base pins so they
|
||||
// stand out, but zoom-interpolated so they don't dominate the whole
|
||||
// map at low zoom levels (per user 2026-05-02: "yellow pens are way
|
||||
// bigger than all the other pins... when you zoom out it makes the
|
||||
// whole map just yellow").
|
||||
// map at low zoom levels: at full size they were far bigger than every
|
||||
// other pin and turned the zoomed-out map yellow.
|
||||
map.value.addLayer({
|
||||
id: 'rosetta-pin',
|
||||
type: 'circle',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { inject, ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue
|
|||
const profileConfig = inject('profileConfig', null)
|
||||
const treeType = computed(() => profileConfig?.value?.treeType || null)
|
||||
|
||||
// One tree per category (per user spec). Each parent has exactly 2 children to
|
||||
// One tree per category. Each parent has exactly 2 children to
|
||||
// keep the visual pattern obvious. Lang tree intentionally shows one language
|
||||
// WITH dialects and one WITHOUT (to convey "not every language has dialects").
|
||||
const TREES = [
|
||||
|
|
@ -337,8 +337,8 @@ onBeforeUnmount(() => resizeObs?.disconnect())
|
|||
.tree-cell::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.tree-cell::-webkit-scrollbar-thumb { background: #484f58; border-radius: 3px; }
|
||||
|
||||
/* ── Table+SVG container: scrolls horizontally if columns don't fit (per user
|
||||
2026-05-02: don't hide field columns just to keep cells slim) ── */
|
||||
/* ── Table+SVG container: scrolls horizontally if columns don't fit (field
|
||||
columns are never hidden just to keep cells slim) ── */
|
||||
.tree-table-wrap {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
|
|
|
|||
|
|
@ -183,9 +183,8 @@ export default {
|
|||
// "Affinity Bloc" tree tier) that could never recognise each other: the tree stored
|
||||
// affinity_bloc='A012' while the dropdown stored rop1='South Asian Peoples', and
|
||||
// filteredRows byte-matches each key against its OWN accessor, so neither ever saw the
|
||||
// other's selection. Observed from behaviour: "there are duplicate affinity blocks, one
|
||||
// called People, one called Affinity Bloc… the People filter IS by affinity block. So they
|
||||
// are the same thing." He is right — the People filter always WAS the affinity-bloc filter.
|
||||
// other's selection. Observed from behaviour: two filters, "People" and "Affinity Bloc", that are
|
||||
// the same axis — the People filter always WAS the affinity-bloc filter.
|
||||
//
|
||||
// THE RULE THIS RESTORES: one axis = one filterable facet key. The CODE won as canonical
|
||||
// because every other People tier (cluster / people_group) already keys by code, and
|
||||
|
|
@ -219,9 +218,9 @@ export default {
|
|||
// `tree:'affinity'` tells the rail to resolve the People codes to human names via the shared
|
||||
// useAffinityBlockLegendData composable (single source of truth with the map's semantic tree).
|
||||
// ── THE FOUR STACKED TREES (card BUILD-STACKED-TREE-LEGEND) ──
|
||||
// The requirement: "if you had a team of people trying to fill the globe with the gospel… they need to be
|
||||
// able to search by people, place, language, religion, and find who has the gospel resources and
|
||||
// who doesn't." The legend is how a researcher navigates those FOUR axes, in THIS order.
|
||||
// The purpose: a team working to bring the gospel to every place needs to search by people, place,
|
||||
// language and religion, and to find who has gospel resources and who does not. The legend is how a
|
||||
// researcher navigates those FOUR axes, in THIS order.
|
||||
//
|
||||
// `axis` names which cap-3 colour strategy paints the tree (see useTreeLegendColors.js) — the
|
||||
// legend reads the map's palettes, so the two can never drift. Status is NOT one of the four
|
||||
|
|
@ -329,11 +328,10 @@ export default {
|
|||
//
|
||||
// WHY IT COSTS NOTHING TO STAY DUAL: MapLibre ignores accessToken rather than rejecting it,
|
||||
// so the Mapbox credential stays threaded through untouched while MapLibre is live.
|
||||
// ★ MAPBOX FOR THIS MAP, by the 2026-09-23 description. Every wording of the
|
||||
// ask pointed at one thing: "like the Mapbox map", "a mirror or clone of Mapbox's globe space
|
||||
// style with the stars", "water like Mapbox style". Mapbox Standard's globe NATIVELY renders
|
||||
// ★ MAPBOX FOR THIS MAP. The target look is Mapbox's own: its globe, the space style with the
|
||||
// stars, and its water. Mapbox Standard's globe NATIVELY renders
|
||||
// the starfield, the atmosphere and that water -- no clone can beat the original, and we hold
|
||||
// a working baked token (the explicit ruling accepted it as public). MapLibre support
|
||||
// a working baked token (accepted as a public token). MapLibre support
|
||||
// stays fully built and reversible: flip this one key back and the keyless VersaTiles path
|
||||
// returns. Interoperability was the requirement; it is intact.
|
||||
provider: 'mapbox',
|
||||
|
|
@ -357,8 +355,8 @@ export default {
|
|||
// dataset of 200 groups, a 150 cutoff puts EVERY country in the bottom bin and the map renders
|
||||
// uniformly pale: not an error, not a crash, just a dashboard that says nothing. That failure
|
||||
// is invisible to every existing gate, because the code is perfectly correct — the ASSUMPTION
|
||||
// is what moved. The report: "that filter of 150 ... should be a variable. It shouldn't be a
|
||||
// static ... I need to be able to reuse this dashboard for other maps, other data sets."
|
||||
// is what moved. The cutoff of 150 must be a variable, not a static, so this dashboard can be
|
||||
// reused for other maps and other data sets.
|
||||
//
|
||||
// So the thresholds are declared HERE, in the dashboard's own config, where every other
|
||||
// per-dataset fact already lives (facets, columns, kpis). Re-pointing this dashboard at a new
|
||||
|
|
@ -390,13 +388,10 @@ export default {
|
|||
// ── COLOUR RAMP — a NAME, resolved in DashboardMapPanel.vue's RAMP_PRESETS ──────────────
|
||||
// Same pattern as `mapStyle` above: a name in config, not a URL/hex-array a host has to
|
||||
// construct. 'heartbeat-red' is the shipped default (unchanged if this key is omitted).
|
||||
// 'greyscale' is the requested swappable alternative ("grey scale as a backdrop
|
||||
// from grey to black, just in case I need to swap from red to black scale") — set this to
|
||||
// 'greyscale' is the swappable alternative, a grey-to-black backdrop — set this to
|
||||
// 'greyscale' to swap the whole choropleth, legend included, with no other edit required.
|
||||
// ★ SWITCHED TO GREYSCALE 2026-09-23 on the ruling: "instead of the red, we need to
|
||||
// use the grey scale from grey to black". He had asked for this preset to exist earlier as a
|
||||
// swappable alternative; this is him taking the swap. One key, and the choropleth AND its
|
||||
// legend both move — which is why it was built as config rather than scattered hex.
|
||||
// ★ THE SHIPPED VALUE IS GREYSCALE: the grey-to-black ramp replaced the red one. One key, and the
|
||||
// choropleth AND its legend both move — which is why it was built as config rather than scattered hex.
|
||||
// The ramp is ['#e8e8e8','#a9a9a9','#6f6f6f','#3a3a3a','#060606'] (DashboardMapPanel.vue:467),
|
||||
// measured worst-case pin visibility 1.96 vs red's 1.84. THE DURABLE FINDING: the real fix for
|
||||
// pin contrast is the lightness-conditional STROKE, which holds whichever ramp is selected —
|
||||
|
|
@ -406,9 +401,8 @@ export default {
|
|||
|
||||
// ── charts ── (DashboardChartsRail)
|
||||
charts: [
|
||||
// ★ HIGH-LEVEL RELIGIONS ONLY (reported 2026-09-23: "it should not mention Ethnoreligion and
|
||||
// Islam multiple times, just once -- just the high-level religions, not broken down into
|
||||
// smaller categories"). The rollup ALREADY EXISTED as the religion_family facet ("Islam",
|
||||
// ★ HIGH-LEVEL RELIGIONS ONLY: each religion appears once, at the high level, not broken down into
|
||||
// smaller categories (Islam once, not "Islam - Sunni" and "Islam - Folk"). The rollup ALREADY EXISTED as the religion_family facet ("Islam",
|
||||
// "Hinduism", "Ethnoreligion" -- one row each); the chart was simply pointed at the
|
||||
// fine-grained `religion` facet ("Islam - Sunni", "Islam - Folk", ...). One word moves it.
|
||||
// religion_family is dashboardOnly by design (a derived value no row field carries), so a
|
||||
|
|
@ -446,8 +440,8 @@ export default {
|
|||
// `prayer_level` listed FIRST so the config owns the order rather than the adapter forcing
|
||||
// it. DashboardResearchFilters de-duplicates on that key, so naming it here cannot render
|
||||
// it twice — and if the adapter's forcing is ever removed, the order still holds.
|
||||
// ★ EIGHT dropdowns. The spec named them: "people, region, block, country, language, religion,
|
||||
// prayer, engagement" — and separately asked for prayer FIRST. engagement_status was the one
|
||||
// ★ EIGHT dropdowns: people, region, bloc, country, language, religion, prayer and engagement,
|
||||
// with prayer FIRST. engagement_status was the one
|
||||
// missing: it was already fully built (label + getter at :112, the Status tree section at :201,
|
||||
// the Engagement table column at :256) and simply never registered here, so the rail rendered
|
||||
// SEVEN. Measured live before this change: 7 filter buttons (Prayer, People, Region, Bloc,
|
||||
|
|
|
|||
|
|
@ -321,8 +321,8 @@ useShadowStyles(`
|
|||
|
||||
★ SCOPE: this law governs UI CHROME — panels, cards, type, borders, controls; everything
|
||||
whose colour comes from a token above. It does NOT govern BASEMAP TILES. The map panel
|
||||
renders a dark basemap ('galaxy' — black ground, blue water, set in dashboard.config.js on
|
||||
the 2026-09-23 ruling), and that is DELIBERATE, not drift: a basemap is imagery the
|
||||
renders a dark basemap ('galaxy' — black ground, blue water, set in dashboard.config.js),
|
||||
and that is DELIBERATE, not drift: a basemap is imagery the
|
||||
data is drawn ON, chosen for choropleth contrast, not a surface the UI is themed with. A
|
||||
dark map inside light chrome is the intended composition. Do not "fix" it to match the
|
||||
palette, and do not cite it as precedent for darkening any chrome.
|
||||
|
|
@ -342,19 +342,19 @@ useShadowStyles(`
|
|||
--rd-primary:var(--dx-accent);
|
||||
/* ── RAIL BALANCE (card BUILD-REBALANCE-THE-TWO-SIDEBARS) ────────────────────────────
|
||||
The charts rail used to be 312px against the tree rail's 248px — the CHARTS rail was
|
||||
64px WIDER than the tree, which is exactly what was reported ("it looks like the
|
||||
CHART sidebar is LARGER than the TREE sidebar"). That was backwards for the content:
|
||||
64px WIDER than the tree, so the CHART sidebar looked LARGER than the TREE sidebar.
|
||||
That was backwards for the content:
|
||||
the tree carries long multi-word labels (bloc/cluster/region names) that must read on
|
||||
ONE line, while the charts carry a donut plus a legend that can wrap.
|
||||
So the widths SWAP ROLES: the tree takes the wider column, the charts the narrower.
|
||||
The pair still sums to 560px, so the centre map cell is unchanged. */
|
||||
/* ★ SYMMETRIC SIDEBARS (reported 2026-09-23: "the charts sidebar can be wider so it is the
|
||||
same width as the left sidebar, which will put the map in the exact center"). rail
|
||||
264 -> 296 = facet width; equal flanks centre the middle column by construction. */
|
||||
/* ★ SYMMETRIC SIDEBARS: the charts rail is as wide as the tree rail, which puts the map in the
|
||||
exact centre. rail 264 -> 296 = facet width; equal flanks centre the middle column by
|
||||
construction. */
|
||||
--facet-w:296px; --rail-w:296px;
|
||||
/* ── ★ ONE EDGE GUTTER FOR THE WHOLE HEADER STACK ────────────────────────────────────
|
||||
(card BUILD-THE-FOUR-HEADERS-ARE-ONE-CENTRED-STACK: "on the edges there should be padding
|
||||
so that everything lines up perfectly.")
|
||||
(card BUILD-THE-FOUR-HEADERS-ARE-ONE-CENTRED-STACK: one edge padding for every band, so
|
||||
everything lines up.)
|
||||
MEASURED BEFORE at 1600x900: topbar padding 14px, filters band 14px, KPI strip 16px. The
|
||||
KPI band was inset 2px further than the two bands above it, so the stack's left edge was a
|
||||
2px staircase rather than a line. Nobody chose that — each band picked its own number
|
||||
|
|
@ -443,7 +443,7 @@ useShadowStyles(`
|
|||
.rd-rz::after{ content:''; position:absolute; background:transparent; border-radius:var(--r-pill,999px); transition:background .15s; }
|
||||
/* Logical margins: the grid mirrors under dir=rtl (grid-template-columns + justify-self both
|
||||
follow the inline direction), so a physical margin-right put the handle's 4px overhang on the
|
||||
wrong side of its own column in Arabic. go-int's board already used the logical pair. */
|
||||
wrong side of its own column in Arabic. the gospel-internet board already used the logical pair. */
|
||||
.rd-rz-facet{ grid-area:facets; justify-self:end; align-self:stretch; width:8px; margin-inline-end:-4px; cursor:col-resize; }
|
||||
.rd-rz-rail{ grid-area:rail; justify-self:start; align-self:stretch; width:8px; margin-inline-start:-4px; cursor:col-resize; }
|
||||
.rd-rz-table{ grid-area:map; align-self:end; justify-self:stretch; height:8px; margin-bottom:-4px; cursor:row-resize; }
|
||||
|
|
@ -451,7 +451,7 @@ useShadowStyles(`
|
|||
.rd-rz-table::after{ top:50%; left:50%; transform:translate(-50%,-50%); width:34px; height:3px; }
|
||||
/* ── SHELL HANDLE (page-scroll mode only) — the boundary between the WHOLE research-dashboard
|
||||
element and whatever a host stacks below it (BUG-SHELL-HANDLE-BOUND-TO-WRONG-DIVIDER: this is
|
||||
the control actually asked for — "resize the height of the page" — which a previous
|
||||
the control that resizes the height of the page, which a previous
|
||||
pass mistakenly bound to .rd-rz-table above instead). Positioned OUTSIDE the grid (the shell is
|
||||
already position:relative) rather than given a grid-area, because it must span all THREE
|
||||
columns (facets + map/table + rail) regardless of their widths, and no single grid-area does
|
||||
|
|
@ -546,9 +546,8 @@ useShadowStyles(`
|
|||
SERVER, not this CSS. Verified this time against a live server with a screenshot. */
|
||||
.rd-shell.rd-page-scroll{ height:auto; --table-h:auto;
|
||||
grid-template-rows: var(--rd-topbar-h,38px) auto var(--rd-stats-h,22px) var(--rd-map-h,340px) auto; }
|
||||
/* ★ COLLAPSE GIVES ITS SPACE TO THE MAP (reported 2026-09-23: "collapsing the table is supposed to
|
||||
collapse down and increase the size of the map -- instead it collapsed up and introduced white
|
||||
space under the collapsed table"). The base .rd-table-collapsed template still says 1fr for the
|
||||
/* ★ COLLAPSE GIVES ITS SPACE TO THE MAP: collapsing the table must collapse DOWN and grow the map.
|
||||
It used to collapse UP and leave white space under the collapsed table. The base .rd-table-collapsed template still says 1fr for the
|
||||
map row, and 1fr cannot grow in an auto-height shell -- there is no fixed total to divide -- so
|
||||
the freed space pooled under the bar as white nothing. In page-scroll the collapsed map takes
|
||||
the SAME shell budget the open state uses, minus only the bar: the one-owner derivation again,
|
||||
|
|
@ -614,11 +613,9 @@ useShadowStyles(`
|
|||
table's real bottom edge, i.e. the sidebar grey-gap bug this card is fixing. --rd-table-foot-h
|
||||
is a var (not a literal) for the same reason --rd-table-bar-h is: DashboardDataTable can correct
|
||||
it if the footer's height ever changes, without this rule going stale. */
|
||||
/* ★ SEVEN DATA ROWS BY DEFAULT (2026-09-23, with the arithmetic done explicitly):
|
||||
"the table needs to only show SEVEN rows by default height. 7 people group rows. That's NOT
|
||||
including the header. If you include the field, there'll be 8 rows. If you include the footer,
|
||||
that'd be 9. If you include the table's header, 10. But 7 actual data people groups."
|
||||
That also settles an earlier ambiguity: he had said 77 and 7 in one breath; 7 is DATA ROWS.
|
||||
/* ★ SEVEN DATA ROWS BY DEFAULT, with the arithmetic done explicitly: the table shows SEVEN
|
||||
people-group rows at its default height, chrome NOT counted: adding the filter field makes 8 rows'
|
||||
height, the footer 9, and the table's header 10. 7 is DATA ROWS.
|
||||
★ STATED IN ROWS, NOT PIXELS, deliberately. The old formula derived the keyhole by subtraction
|
||||
(shell - map - bar - foot), so the row count was whatever fell out — MEASURED 300px = 11 rows,
|
||||
not 7. Deriving from --rd-table-rows x --rd-table-row-h means the promise is the declaration:
|
||||
|
|
@ -713,9 +710,9 @@ useShadowStyles(`
|
|||
ONE QUANTITY, ONE OWNER applies to fallbacks too: same var, same fallback, everywhere. */
|
||||
max-height:calc(var(--rd-shell-h, 562px) + 1px); }
|
||||
|
||||
/* ★ THE ELEMENT MUST SIZE TO ITS CONTENT IN PAGE-SCROLL (reported 2026-09-23: "massive grey gap
|
||||
between map and table and the map overflows into the people-group grid page — the dashboard
|
||||
element is not one unified element above the grid element, it is overflowing").
|
||||
/* ★ THE ELEMENT MUST SIZE TO ITS CONTENT IN PAGE-SCROLL. The symptom: a large grey gap between the
|
||||
map and the table, with the map overflowing into the people-group grid, so the dashboard did not
|
||||
read as one element above the grid.
|
||||
MEASURED in the cap-5 shell at 1440x900: INSIDE the dashboard everything was tight (map bottom
|
||||
536, table top 537, gap 1px, table flush with .rd-shell at 899) while the HOST element measured
|
||||
3510px tall — 2611px of empty element below the content. That empty tail is the grey gap, and
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
* failure here (check-composite-interop carries the same part B for the same reason): a gate that
|
||||
* only reads source will pass while the reader gets last week's element.
|
||||
*
|
||||
* NO BROWSER. Playwright is rega's alone (fleet policy). Nothing here launches one.
|
||||
* NO BROWSER. Playwright runs only in the browser QA step (project policy). Nothing here launches one.
|
||||
*
|
||||
* NEGATIVE-TESTED 2026-09-26 — each of these makes it FAIL, naming the file and the reason:
|
||||
* chips back to <span> -> A1 "chip is a <span>, not a <button>"
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
* look identical: the bars appear, suggestions work, nothing errors. Only the request log tells the
|
||||
* truth, which is why this counts requests rather than looking at the page.
|
||||
*
|
||||
* ★ CORRECTED 2026-09-26, after rega reported "0 search bars on both engines" and it was the CHECK
|
||||
* ★ CORRECTED 2026-09-26, after a browser check reported "0 search bars on both engines" and it was the CHECK
|
||||
* that was wrong, not the product. Two separate mistakes, both of them mine:
|
||||
*
|
||||
* 1. UNDER MAPLIBRE, NO SEARCH BAR IS THE CONTRACT — not a hollow pass and not a failure.
|
||||
|
|
@ -57,14 +57,14 @@
|
|||
* gate unless the runner says so.
|
||||
*
|
||||
* ★ WHY THE MAP PROBE ASKS THE ENGINE AND NOT THE DOM (2026-09-26, the second correction in a
|
||||
* day). This gate reported "NO map constructed" on a leaf where rega then measured 1 Mapbox map, 2
|
||||
* day). This gate reported "NO map constructed" on a leaf where a browser check then measured 1 Mapbox map, 2
|
||||
* GL canvases and 3 search inputs, on BOTH token rungs. The product was never at fault; the
|
||||
* detection was. A DOM walk for a canvas is a chain of assumptions — every shadow root open, the
|
||||
* canvas carrying the class expected, existing by the time it is looked for — and when any link
|
||||
* breaks it reports ABSENCE, which is indistinguishable from a broken product. It now hooks the
|
||||
* engine's own Map.prototype.setStyle through both globals, which cannot be wrong about the DOM
|
||||
* because it never looks at it, and widens the bar selector to what a reader would call a search
|
||||
* box. Both borrowed from rega's tokrung.mjs.
|
||||
* box. Both borrowed from the token-rung probe (tokrung.mjs).
|
||||
*
|
||||
* THE LESSON, recorded because it is the second time in two days that a probe accused a working
|
||||
* product: a measurement that yields ZERO when its own assumption is wrong will always blame the
|
||||
|
|
@ -137,8 +137,8 @@ const fails = []
|
|||
const lines = []
|
||||
try {
|
||||
// ★ THE MAPBOX HALF IS MEASURED ON BOTH TOKEN RUNGS, AND THAT IS A DELIBERATE EXPERIMENT.
|
||||
// rega, gates:post at 1a761741 WITH a token: the mapbox half reported "NO map constructed", 21
|
||||
// requests to api.mapbox.com, 0 bars — while rega's own probe of THIS leaf at
|
||||
// gates:post at 1a761741 WITH a token: the mapbox half reported "NO map constructed", 21
|
||||
// requests to api.mapbox.com, 0 bars — while a separate probe of THIS leaf at
|
||||
// ?provider=mapbox&tk=<same token> constructed a Mapbox map. The suggested cause was that the
|
||||
// generated leaf overwrites the window.MAP_TOKEN this gate sets in addInitScript.
|
||||
//
|
||||
|
|
@ -172,7 +172,7 @@ try {
|
|||
page.on('response', (r) => { if (r.url().includes('api.mapbox.com') && r.status() >= 400) mapboxBad.push(r.status()) })
|
||||
if (c.rung === 'global') await page.addInitScript((tk) => { window.MAP_TOKEN = tk }, TOKEN)
|
||||
// ★ ASK THE ENGINE, NOT THE DOM, WHETHER A MAP EXISTS. See the header note; borrowed from
|
||||
// rega's tokrung.mjs, which measured this leaf correctly while this gate did not. Both globals
|
||||
// the token-rung probe (tokrung.mjs), which measured this leaf correctly while this gate did not. Both globals
|
||||
// are hooked because the leaf loads only the engine its provider asks for.
|
||||
await page.addInitScript(() => {
|
||||
window.__mapsBuilt = { mapbox: 0, maplibre: 0 }
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ if (!existsSync(ENGINE)) { console.log(`${TAG}: SKIP — no ${ENGINE.replace(ROO
|
|||
}
|
||||
|
||||
// ── D. the drag mirrors under RTL, and the keyboard agrees with it ───────────────────────────
|
||||
// go-int, 2026-09-27: the x delta was computed once, in LTR. Under dir=rtl the grid mirrors — the
|
||||
// Found 2026-09-27: the x delta was computed once, in LTR. Under dir=rtl the grid mirrors — the
|
||||
// facet rail sits on the RIGHT — so dragging its handle toward the map moves clientX the other way,
|
||||
// and in Arabic the facet handle shrank the CHARTS rail. It reaches Big Picture in ar, not only a
|
||||
// new consumer. The keyboard path is asserted too: if only the pointer mirrored, the two input
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
/**
|
||||
* check-legend-mirrors.mjs — the legends must MIRROR, and their labels must not become values.
|
||||
*
|
||||
* rega's Arabic survey on main 52eeb0db: the research-dashboard legends sat on the wrong edge with
|
||||
* the Arabic survey on main 52eeb0db: the research-dashboard legends sat on the wrong edge with
|
||||
* ENGLISH text — 70 of 85 letters Latin — in Big Picture and in the Research Gallery. Two faults
|
||||
* that look like one, and they fail in opposite directions if you fix them carelessly.
|
||||
*
|
||||
|
|
@ -174,7 +174,7 @@ if (!fails.some((f) => f.startsWith('1'))) notes.push(`insets: ${SURFACES.length
|
|||
// component: mounting an SFC here needs the Vite/no-op-renderer harness check:composites-mount
|
||||
// carries, and a no-op renderer cannot show a colour anyway, so a mount would prove only "it did
|
||||
// not throw" at the cost of a dev server in a gate that otherwise runs in milliseconds. The
|
||||
// rendered result is rega's, on the NEEDS-EYES row for this landing.
|
||||
// rendered result is the browser QA step's, on the NEEDS-EYES row for this landing.
|
||||
{
|
||||
const RAIL = 'library/components/dashboards/DashboardFacetRail.vue' // moved by kit landing 4c-3
|
||||
const CHIP = 'library/components/dashboards/DashboardFilterChips.vue' // moved by kit landing 4c-2
|
||||
|
|
@ -263,10 +263,10 @@ if (!fails.some((f) => f.startsWith('1'))) notes.push(`insets: ${SURFACES.length
|
|||
// (a) the old copy survives, so the tree holds two chip bars that drift apart — the failure this
|
||||
// repo has already paid for with useNeighborT/usePplrT at Jaccard 0.82;
|
||||
// (b) the kit's UNCONFIGURED behaviour is wrong, which nobody notices because every dashboard in
|
||||
// this tree supplies a resolver. go-int is the consumer that will not, at first.
|
||||
// this tree supplies a resolver. the gospel-internet board is the consumer that will not, at first.
|
||||
// So the arm asserts the move is complete and then LIFTS the default out of the source and RUNS it,
|
||||
// the same way arm 3 drives the rail's fallbacks. Not a mount: a no-op renderer cannot show a chip,
|
||||
// and the rendered result is rega's.
|
||||
// and the rendered result is the browser QA step's.
|
||||
{
|
||||
const NEW = 'library/components/dashboards/DashboardFilterChips.vue'
|
||||
const OLD = 'plugin-designs/research-dashboard/src/components/dashboards/DashboardFilterChips.vue'
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ const isActive = (facet, key) => bus.isFilterActive(facet, key)
|
|||
// Two projections, one function, and both follow the same rule: the key crosses, the label is read.
|
||||
// OTHER_KEY is the rollup sentinel aggregate() produces; it must stay English on the wire (this
|
||||
// component refuses to filter on it by testing `key === 'Other'`) and be localized for a reader.
|
||||
// ★ TWO MORE FACETS CARRY ENGLISH DATA LABELS, and rega read both on ?lang=ar (KA3, 3d64bcad).
|
||||
// ★ TWO MORE FACETS CARRY ENGLISH DATA LABELS, and both were read on ?lang=ar (KA3, 3d64bcad).
|
||||
// They are not chrome: no catalogue held them, so every i18n gate passed while the chart spoke English.
|
||||
// · wagf_region — the prayer API's seven region names. A closed set, so they are CATALOGUED, five of
|
||||
// them from vendored CLDR (internal/generate-wagf-region-names.mjs); middle_east and
|
||||
|
|
@ -128,7 +128,7 @@ const regionLabel = (key) => {
|
|||
const k = slug && REGION_KEY[slug]
|
||||
return (k && tStatic(k)) || key
|
||||
}
|
||||
// ★ THE CODE COMES FROM THE ROW, NOT FROM THE NAME (rega, ?lang=ar after the first fix: "People's
|
||||
// ★ THE CODE COMES FROM THE ROW, NOT FROM THE NAME (?lang=ar after the first fix: "People's
|
||||
// Republic of China" and "Lao People's Democratic Republic" were still English). The API labels some
|
||||
// countries with their FORMAL name, which countries.json does not carry — it says "China" and "Laos" —
|
||||
// so reversing the English name missed exactly the countries whose names are longest. Every row already
|
||||
|
|
@ -256,9 +256,8 @@ useShadowStyles(`
|
|||
The 108px cap existed to match the donut's height, so the legend never made the card
|
||||
taller than the chart beside it. That cap is what forced .rd-lt to ELLIPSISE rather than
|
||||
wrap — a wrapping row is ~2x tall, and in a 108px box that pushes items out of view.
|
||||
It is now ruled READABILITY OVER COMPACTNESS (2026-09-22): "on the religion and
|
||||
region charts I cannot scroll right to read the rest of the text." He is right that there
|
||||
was nothing to scroll to — .rd-legend is overflow-x:hidden by design, so an ellipsised
|
||||
READABILITY NOW WINS OVER COMPACTNESS: on the religion and region charts a reader could not
|
||||
scroll right to read the rest of a label, and there was indeed nothing to scroll to — .rd-legend is overflow-x:hidden by design, so an ellipsised
|
||||
label was simply unrecoverable text.
|
||||
So the legend is allowed to grow PAST the donut (176px, vs the donut's 84px) and .rd-lt
|
||||
wraps to at most 3 lines. The card gets taller; that is the accepted cost. If you need to
|
||||
|
|
@ -276,10 +275,9 @@ useShadowStyles(`
|
|||
now two lines tall, and centring would float the swatch and the count in the middle of that
|
||||
block, detached from the first line of text they label. The count is top-aligned with a 1px
|
||||
nudge (.rd-li b) so it reads as belonging to the row's first line. */
|
||||
/* ★ THE CONVENTIONAL LEGEND ROW, on the final ruling (2026-09-23): "I am not
|
||||
impressed with that design -- use conventional designs only", after rejecting both the
|
||||
scroll-to-read variant ("I cannot scroll right") and the frozen column's painted backing
|
||||
("super white background, it looks odd; it was to be invisible"). This is the pattern every
|
||||
/* ★ THE CONVENTIONAL LEGEND ROW. Two earlier variants were replaced: scroll-to-read (a reader
|
||||
could not scroll right) and a frozen count column on a painted backing (an opaque white block
|
||||
where the backing was meant to be invisible). This is the pattern every
|
||||
chart library ships: swatch, label that ELLIPSIZES at the available width, count right-
|
||||
aligned in a fixed-width slot. No horizontal scroll, no sticky, no background -- the number
|
||||
needs no backing because nothing can ever slide beneath it. The full label lives in the
|
||||
|
|
@ -292,9 +290,8 @@ useShadowStyles(`
|
|||
/* margin-top nudges keep the swatch and the count optically on the label's FIRST line now that
|
||||
a row can be two lines tall. */
|
||||
.rd-li i{ width:9px; height:9px; border-radius:2px; flex:0 0 auto; margin-top:3px; }
|
||||
/* ★ THE COUNT IS A FROZEN COLUMN (decided 2026-09-23): "the number column was supposed to be
|
||||
FROZEN with the z-index topped with a WHITE BACKGROUND so that the text doesn't underlap it, so
|
||||
that whenever you scroll to the right you can read the text but the numbers stay visible."
|
||||
/* ★ THE COUNT IS A FROZEN COLUMN: pinned, on an opaque background, so the label never slides under
|
||||
it and the numbers stay visible while the label scrolls.
|
||||
This is the other half of the horizontal-scroll change and shipping without it was incomplete:
|
||||
nowrap + overflow-x:auto made the long label reachable, but the label then slides UNDER the
|
||||
count and the two overlap mid-scroll. A scrollable row needs a pinned column, or the thing you
|
||||
|
|
@ -305,9 +302,8 @@ useShadowStyles(`
|
|||
Background is --rd-panel (the card's own surface) so the pin matches its host rather than
|
||||
assuming white; the card is #fff today but a host that re-skins the panel should not get a
|
||||
white rectangle floating on a tinted card. */
|
||||
/* ★ THE COUNT COLUMN IS UNIFORM, RIGHT-JUSTIFIED AND IMMOVABLE (reported 2026-09-23: "set the
|
||||
number column to be justified right, same width, uniform, immovable... so it is easier to
|
||||
use -- and the same goes for the region chart"). One fixed width for every row means the
|
||||
/* ★ THE COUNT COLUMN IS UNIFORM, RIGHT-JUSTIFIED AND IMMOVABLE, in this chart and the region chart
|
||||
alike, so it is easier to read. One fixed width for every row means the
|
||||
digits align into a scannable column instead of each number sitting wherever its label ended;
|
||||
right-justified plus the existing tabular-nums makes magnitudes comparable at a glance (541
|
||||
visibly longer than 72). 3.4em clears the widest live value (1,309 in the region chart) with
|
||||
|
|
@ -324,8 +320,8 @@ useShadowStyles(`
|
|||
── WRAP, NOT ELLIPSIS (card BUILD-REBALANCE-THE-TWO-SIDEBARS) ───────────────────────────
|
||||
This previously read overflow:hidden + text-overflow:ellipsis + white-space:nowrap, a
|
||||
DELIBERATE earlier choice to protect the 108px scroller (see the .rd-legend note above).
|
||||
It is reversed on the explicit ruling: measured before this change, 4 of 16 legend
|
||||
labels were ellipsised, including the two he named — "Ethnoreligion - Chinese Folk Religion"
|
||||
It is reversed for readability: measured before this change, 4 of 16 legend
|
||||
labels were ellipsised, including these two — "Ethnoreligion - Chinese Folk Religion"
|
||||
(191px of text in a 130px box) and "North America & Non-Spanish Caribbean" (214px in 130px).
|
||||
Because the scroller is overflow-x:hidden, that hidden text could not be scrolled to; it was
|
||||
simply GONE. Two lines at this rail width fit both in full.
|
||||
|
|
@ -340,16 +336,15 @@ useShadowStyles(`
|
|||
overflow-wrap:anywhere breaks only words that genuinely cannot fit, so ordinary labels still
|
||||
break at spaces. Keep .rd-li's count at flex:0 0 auto — the number must never be the thing
|
||||
that yields. */
|
||||
/* ★ THIRD AND RESOLVING PASS ON THIS TRADE (2026-09-23) — read the block above first.
|
||||
History: (1) capped at 108px to match the donut, which forced ELLIPSIS; (2) it was rejected
|
||||
that on 2026-09-22 — "I cannot SCROLL RIGHT to read the rest of the text" — so wrapping was
|
||||
enabled, the cap raised to 176px, and a taller card was accepted as the cost.
|
||||
(3) He has now asked for the thing that was missing from BOTH: "add horizontal scrolling to the
|
||||
charts, we can remove line wrapping and save a lot of space in terms of height... we are trying
|
||||
to RECLAIM HEIGHT."
|
||||
/* ★ THIRD AND RESOLVING PASS ON THIS TRADE — read the block above first.
|
||||
History: (1) capped at 108px to match the donut, which forced ELLIPSIS; (2) that was rejected
|
||||
because a truncated label could not be scrolled to and read, so wrapping was enabled, the cap
|
||||
raised to 176px, and a taller card was accepted as the cost.
|
||||
(3) The thing missing from BOTH: horizontal scrolling on the charts, so line wrapping can go and
|
||||
the height it cost is reclaimed.
|
||||
★ THIS IS NOT A FLIP-FLOP BACK TO PASS 1, and the difference is the whole point. Pass 1 was
|
||||
nowrap + ellipsis + overflow-x:HIDDEN — the label was truncated and UNRECOVERABLE, which is the
|
||||
defect he rejected. This is nowrap + overflow-x:AUTO on the scroller: the full label still
|
||||
defect that was rejected. This is nowrap + overflow-x:AUTO on the scroller: the full label still
|
||||
exists and is reachable by scrolling right. The old warning said "do NOT restore nowrap", and it
|
||||
was right given hidden overflow; with a real horizontal scrollbar its precondition is gone.
|
||||
The vertical saving is the reason: a 3-line clamp makes every long row ~2.5x tall. */
|
||||
|
|
@ -357,7 +352,7 @@ useShadowStyles(`
|
|||
.rd-bars{ display:flex; flex-direction:column; gap:3px; }
|
||||
.rd-bar{ display:flex; align-items:center; gap:6px; cursor:pointer; font-size:10.5px; padding:1px 2px; border-radius:3px; }
|
||||
.rd-bar:hover{ background:var(--dx-surface, #f3f3f1); } .rd-bar.on{ background:#fff4ea; font-weight:700; color:var(--rd-ink, #3b463d); }
|
||||
/* Same wrap ruling as .rd-lt, applied to the bar chart's country labels: measured before this
|
||||
/* Same wrap rule as .rd-lt, applied to the bar chart's country labels: measured before this
|
||||
change, 2 of 13 clipped — "People's Republic of China" (138px of text in 84px) and "Lao
|
||||
People's Democratic Republic" (177px in 84px) — and like the donut legend there was no way
|
||||
to scroll to the hidden half. Clamped at 3 lines — same measured reason as .rd-lt above, and
|
||||
|
|
@ -375,9 +370,8 @@ useShadowStyles(`
|
|||
<template>
|
||||
<aside class="rd-charts" data-region="charts-rail" @click="onChartClick">
|
||||
<!--
|
||||
★ THE RAIL IS A TAB STRIP, NOT A HEADING (the report: "I asked a long time ago that the chart
|
||||
becomes a tab, so that there are tabs, so that you can have the Gospel Resources tab next to
|
||||
Charts. Charts will be a selected tab that will show the charts.")
|
||||
★ THE RAIL IS A TAB STRIP, NOT A HEADING: Charts is one tab (selected by default, showing the
|
||||
charts), so a Gospel Resources tab can sit next to it.
|
||||
|
||||
THIS IS HALF THE CARD, DELIBERATELY. The container is buildable now; what the Gospel Resources
|
||||
tab ENCODES is not, because it waits on three open product decisions (the official lostness
|
||||
|
|
@ -411,7 +405,7 @@ useShadowStyles(`
|
|||
</path>
|
||||
</svg>
|
||||
<div class="rd-legend">
|
||||
<!-- ★ THE LABEL IS PROJECTED; THE KEY IS NOT (rega's Arabic survey: 70 of 85 letters in
|
||||
<!-- ★ THE LABEL IS PROJECTED; THE KEY IS NOT (the Arabic survey: 70 of 85 letters in
|
||||
this legend were Latin). `s.key` is a facet VALUE, and for the religion donut it is a
|
||||
DERIVED English family name — "Islam", "Hinduism" — that library/colors/religion.js
|
||||
produces from a religion code. It is English in every locale because it is not a
|
||||
|
|
|
|||
|
|
@ -482,8 +482,8 @@ function toggleDock() { collapsed.value = !collapsed.value; bus && bus.emit('tog
|
|||
*/
|
||||
const isFull = ref(false)
|
||||
const wrapEl = ref(null)
|
||||
// ★ NATIVE FULLSCREEN, NOT A FIXED OVERLAY (reported 2026-09-23: "there is no way to exit full
|
||||
// screen or export csv when you enter full screen"). The bar with BOTH buttons is inside this
|
||||
// ★ NATIVE FULLSCREEN, NOT A FIXED OVERLAY: in full screen there was no way to exit it or to export
|
||||
// the CSV. The bar with BOTH buttons is inside this
|
||||
// wrap, so structurally they should have been visible -- but position:fixed inside a shadow root
|
||||
// resolves against any transformed/contained ANCESTOR, not the viewport, and in the cap-5 shell
|
||||
// that pinned the "fullscreen" wrap to a pane whose top sat above the visible area: the bar (and
|
||||
|
|
@ -709,8 +709,8 @@ useShadowStyles(`
|
|||
HONEST LIMIT: this IMPROVES 1280, it does not solve it — 8 text columns genuinely do not fit there.
|
||||
Closing that gap is a product call (drop a column at narrow widths, or scroll horizontally), not a
|
||||
CSS one. At 1440 the table already fits with 0 of 8 truncated. */
|
||||
/* ★ NO ELLIPSIS ON THE HEADER (the report: "instead of ABBREVIATING the headers, we can have the
|
||||
FULL headers in the table"). text-overflow:ellipsis DESTROYS information with no way to
|
||||
/* ★ NO ELLIPSIS ON THE HEADER: the table shows FULL headers, never abbreviated ones.
|
||||
text-overflow:ellipsis DESTROYS information with no way to
|
||||
recover it — the reader cannot widen a truncated word, and 'Engageme…' is not a column name.
|
||||
Horizontal scroll PRESERVES it: the full text always exists and is always reachable. This is
|
||||
the same trade already applied to the chart legends (nowrap + overflow-x:auto) in the commit
|
||||
|
|
@ -928,7 +928,7 @@ useShadowStyles(`
|
|||
<div class="rd-td chk"></div>
|
||||
<div v-for="(col, i) in COLUMNS" :key="'f-' + col.key" class="rd-td"
|
||||
:class="{ num: col.type === 'num' }" :style="colFlex(col)">
|
||||
<!-- ★ "groups" WAS THE LAST ENGLISH WORD IN THIS TABLE (rega KA3, ?lang=ar): the footer read
|
||||
<!-- ★ "groups" WAS THE LAST ENGLISH WORD IN THIS TABLE (KA3, ?lang=ar): the footer read
|
||||
"Σ 2,107 groups" on an Arabic page. dashboard.groups already ships in all eleven locales
|
||||
(ar "مجموعة"), so this is a call site that never read its own catalogue -- no new
|
||||
string, no new key. Singular/plural is written the same way as the header count at
|
||||
|
|
|
|||
|
|
@ -80,9 +80,8 @@ const countryCounts = computed(() => {
|
|||
|
||||
const mapContainer = ref(null)
|
||||
// ── MAP PROVIDER: choosable, Mapbox by default ──────────────────────────────
|
||||
// Reported 2026-09-22: "keep every Mapbox component working so we don't break Mapbox
|
||||
// compatibility — we just want interoperability with MapLibre... start on the unified research
|
||||
// dashboard MapLibre swap while maintaining Mapbox dependencies so that both work."
|
||||
// The aim is interoperability: every Mapbox component keeps working, so Mapbox compatibility never
|
||||
// breaks, while the unified research dashboard also runs on MapLibre. Both engines must work.
|
||||
//
|
||||
// So this is DUAL-SUPPORT, not a migration, and the change is deliberately ADDITIVE: `provider`
|
||||
// defaults to 'mapbox', so with no config the behaviour is byte-for-byte what it was. Nothing
|
||||
|
|
@ -112,13 +111,13 @@ const mapContainer = ref(null)
|
|||
// This used to be `cfg.provider === 'maplibre' ? 'maplibre' : 'mapbox'`, which collapsed every
|
||||
// other answer to Mapbox and therefore silently OUTRANKED the page-level switch that the rest of
|
||||
// the tree honours: a partner flipping one value watched this dashboard stay on Mapbox with no
|
||||
// indication why. But dashboard.config.js pins provider:'mapbox' DELIBERATELY — the
|
||||
// 2026-09-23 ruling, for Mapbox Standard's native starfield globe — so that default must survive
|
||||
// indication why. But dashboard.config.js pins provider:'mapbox' DELIBERATELY — for
|
||||
// Mapbox Standard's native starfield globe — so that default must survive
|
||||
// untouched. Both hold if the page switch sits BETWEEN the per-element config and the bundle's
|
||||
// shipped preference:
|
||||
// 1. profile-config.provider — this embed, decided by whoever mounted the element
|
||||
// 2. ?provider= / window.MAP_PROVIDER — the page, i.e. "swap every map on this site"
|
||||
// 3. dashboard.config.js — this bundle's shipped default (Mapbox, by the ruling above)
|
||||
// 3. dashboard.config.js — this bundle's shipped default (Mapbox, as set above)
|
||||
// With no switch set, resolveMapProvider decides 'auto': with a token step 2 does not fire and the
|
||||
// shipped default holds; with NO token it answers 'maplibre', because Mapbox cannot draw without one.
|
||||
const mapProvider = computed(() => {
|
||||
|
|
@ -134,7 +133,7 @@ const isMaplibreProvider = computed(() => mapProvider.value === 'maplibre')
|
|||
|
||||
// The country boundaries (384 KB) are fetched as soon as the panel exists, not from the style-load
|
||||
// handler: there the download waited for the whole basemap style and so ran after it, not beside it
|
||||
// (rega's waterfall: requested at 10.4 s of a 12.5 s load). The handler awaits this same promise.
|
||||
// (the network waterfall: requested at 10.4 s of a 12.5 s load). The handler awaits this same promise.
|
||||
let boundariesPromise = null
|
||||
function countryBoundaries() {
|
||||
if (!boundariesPromise) {
|
||||
|
|
@ -153,9 +152,8 @@ function countryBoundaries() {
|
|||
|
||||
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
|
||||
containerRef: mapContainer, accessToken: unref(mapboxToken) || (mapboxToken && mapboxToken.value) || '',
|
||||
// ★ ZOOMED OUT BY DEFAULT, WITH REAL ZOOM-OUT RANGE (reported 2026-09-23: "I don't want the map
|
||||
// zoomed in by default but zoomed out... the edges of the map need to be truly around the edges
|
||||
// so you have more zoom control and zoom function distance"). zoom 1.45 -> 1.1 opens on the
|
||||
// ★ ZOOMED OUT BY DEFAULT, WITH REAL ZOOM-OUT RANGE: the map opens zoomed out, with its edges truly
|
||||
// at the edges, so there is more zoom-out distance to work with. zoom 1.45 -> 1.1 opens on the
|
||||
// whole sphere; minZoom 1 -> 0.5 plus skipWorldFloor gives the zoom-out gesture somewhere to
|
||||
// go. The floor being skipped is the mercator "no duplicate world" rule, which is meaningless
|
||||
// on a globe -- a sphere HAS no second world to show. Flat mercator keeps the floor.
|
||||
|
|
@ -175,7 +173,7 @@ const mapFly = useMapFly({
|
|||
defaultCenter: [28, 18],
|
||||
defaultZoom: 1.45,
|
||||
})
|
||||
// ── Table selection -> camera (the requirement: "select in the table, the map zooms there") ──
|
||||
// ── Table selection -> camera (selecting a row in the table zooms the map there) ──
|
||||
// Frames the selection off the FULL row store, not filteredRows — matching selectedGeo()
|
||||
// below, so the camera always frames exactly the pins the selection pen draws (a row can be
|
||||
// selected and then filtered out of scope; the pen deliberately keeps showing it).
|
||||
|
|
@ -208,7 +206,7 @@ let offToggleTable = null
|
|||
// Holders for the three verify hooks planted on window when the map is built, so the unmount path
|
||||
// can release ONLY the ones this instance installed. Two dashboards can share a page (the Big
|
||||
// Picture composite beside a gallery instance); B overwrites A's hooks, then A unmounts, and an
|
||||
// unconditional delete would strip the hooks of the dashboard still on screen — rega would see a
|
||||
// unconditional delete would strip the hooks of the dashboard still on screen — a browser check would see a
|
||||
// missing hook on a healthy map. Declared here because the assignments live inside the map-build
|
||||
// function and a const there is invisible to onBeforeUnmount.
|
||||
let _hookRdashMap = null
|
||||
|
|
@ -552,8 +550,7 @@ function setLiveResize(on) {
|
|||
const BIN_COLORS = (() => {
|
||||
const RAMP_PRESETS = {
|
||||
'heartbeat-red': ['#f4cec4', '#ecaea1', '#e08678', '#ce544b', '#b10010'],
|
||||
// ★ BIN 1 DARKENED #e8e8e8 -> #c9c9c9 (reported 2026-09-23: "bangladesh is not showing up as a
|
||||
// polygon"). It WAS rendering -- queryRenderedFeatures returned the BGD polygon painted
|
||||
// ★ BIN 1 DARKENED #e8e8e8 -> #c9c9c9: Bangladesh appeared not to show as a polygon. It WAS rendering -- queryRenderedFeatures returned the BGD polygon painted
|
||||
// #e8e8e8 -- but against the #f3f3f1 no-data surface that is a 1.06 contrast ratio at 0.78
|
||||
// opacity: a 1-4-group country was pixel-identical to an empty one. The ramp's first step
|
||||
// must be separated from NO-DATA at least as clearly as from bin 2, or the lowest bin
|
||||
|
|
@ -911,9 +908,8 @@ function addLayers() {
|
|||
// longer ships. Kept only so the NAVY-ERA rejections stay readable:
|
||||
// '#0d47a1' (Blue 900) - too light.
|
||||
// Tinting the pin per bin - reintroduces the original alpha fault. ★ STILL BINDING.
|
||||
// "Reverting to orange - it was ruled explicitly against it." ★ NO LONGER TRUE. That
|
||||
// ruling was given while the ramp was PALE and the pin navy; it has since been asked for
|
||||
// orange against the new red ramp ("I think it might be better orange"), which supersedes it.
|
||||
// Reverting to orange (once ruled out) - ★ NO LONGER TRUE. That held while the ramp was PALE
|
||||
// and the pin navy; against the new red ramp the pin moved to orange, which supersedes it.
|
||||
// Navy did not lose on contrast - it was freed to become the SELECTION ring, which had to
|
||||
// move because an orange ring on an orange pin measured 1.07. See SEL_COLOR.
|
||||
//
|
||||
|
|
@ -941,7 +937,7 @@ function addLayers() {
|
|||
// z6: navy r=5.0 (+0 ring) inside a white r=7.4 -> band 2.4px, footprint 14.8px
|
||||
// The band is now ABOVE 1px at every zoom in the ramp, so it survives rasterisation at the
|
||||
// zoomed-out view where the heat map was seen. The footprint at z1 drops 24%, which
|
||||
// is the "a little bit smaller when you zoom out" spread that was asked for.
|
||||
// gives the pins a slightly smaller spread when zoomed out.
|
||||
//
|
||||
// The dark 1px ring on the navy dot is REMOVED: its job was to separate navy from the
|
||||
// near-white halo, but a true white ring already does that at 13.2:1, and at r=1.6 a 1px
|
||||
|
|
@ -970,15 +966,14 @@ function addLayers() {
|
|||
// palest and the darkest bin, and on any basemap a host swaps in. Nothing about the pin
|
||||
// depends on the choropleth any more, which is what ends the recurring pin-colour argument.
|
||||
//
|
||||
// Blue dots + white rings are the settled ruling ("I like the blue dots. Do not
|
||||
// revert it to orange, just add WHITE RINGS") — unchanged here. Radii are unchanged too, so
|
||||
// the zoomed-out spacing that was asked for is preserved: z1 navy r=1.6 inside white r=3.4
|
||||
// Blue dots + white rings are the settled design (blue dots, not orange, each with a WHITE RING) —
|
||||
// unchanged here. Radii are unchanged too, so the zoomed-out spacing is preserved: z1 navy r=1.6 inside white r=3.4
|
||||
// (1.8px band), z6 navy r=5.0 inside white r=7.4 (2.4px band).
|
||||
//
|
||||
// HIT-TESTING unchanged: 'rd-bubble-layer' keeps its id and stays the TOP layer; the halo
|
||||
// is a separate NON-interactive layer beneath it and must not be queried.
|
||||
// ── ★ ONE CIRCLE WITH A WHITE STROKE — NOT A NAVY/WHITE/NAVY SANDWICH ──────────────────────
|
||||
// (reported 2026-09-22: "the pins look like eyes, they look kind of creepy")
|
||||
// (the symptom: the pins looked like eyes, which read as unsettling)
|
||||
//
|
||||
// THE SANDWICH WAS THE BUG, and the comment above described it accurately without noticing what
|
||||
// it draws. Read outward from the centre at z1 the old construction was:
|
||||
|
|
@ -1096,11 +1091,10 @@ function addLayers() {
|
|||
}
|
||||
} catch (_) { /* verify hook */ }
|
||||
applyProjection()
|
||||
// ★ THE GALAXY, THE WAY MAPBOX DOES IT (the report: "find out how Mapbox does the galaxy stars and
|
||||
// see if we can use that"). Mapbox GL renders space and the starfield ENGINE-SIDE via the fog
|
||||
// ★ THE GALAXY, THE WAY MAPBOX DOES IT: the starfield follows Mapbox's own method. Mapbox GL renders space and the starfield ENGINE-SIDE via the fog
|
||||
// API -- documented at docs.mapbox.com/mapbox-gl-js/guides/globe/ ("stars and atmosphere are
|
||||
// customized with the fog property"). These are their documented example values with one
|
||||
// change: space-color is pure black on the instruction ("not grey"). star-intensity
|
||||
// change: space-color is pure black, not grey. star-intensity
|
||||
// 0..1 is the engine's own star layer -- no canvas, no custom code, the actual Mapbox stars.
|
||||
// Guarded: MapLibre has no setFog (its equivalent is the sky spec), so on that provider this is
|
||||
// a silent no-op and the plain black container behind the transparent sphere carries the void.
|
||||
|
|
@ -1288,7 +1282,7 @@ onMounted(async () => {
|
|||
})
|
||||
onBeforeUnmount(() => {
|
||||
// These hooks are closures over this component (and one is the Map itself), so leaving any of them
|
||||
// on window pins the whole app after unmount — rega measured exactly that (leak-722ac7789).
|
||||
// on window pins the whole app after unmount — measured exactly that (leak-722ac7789).
|
||||
// Released as a set: __rdashPinAt was the one that showed up in the snapshot, but __rdashMap holds
|
||||
// the map and retains strictly more, and clearing only the reported one would have left the larger
|
||||
// retainer in place and looked fixed.
|
||||
|
|
@ -1397,8 +1391,8 @@ function syncAttribClearance() {
|
|||
|
||||
// ── RDASH-ENHANCE: GLOBE (default) <-> MERCATOR projection toggle ──
|
||||
const projection = ref('globe')
|
||||
// ★ THE TWO ENGINES DISAGREE ON THIS ONE CALL, AND THE CATCH HID IT (reported 2026-09-23: "the map
|
||||
// style is not a globe"). Mapbox GL takes setProjection('globe') -- a bare string. MapLibre v5 takes
|
||||
// ★ THE TWO ENGINES DISAGREE ON THIS ONE CALL, AND THE CATCH HID IT: the symptom was a flat map where
|
||||
// a globe was expected. Mapbox GL takes setProjection('globe') -- a bare string. MapLibre v5 takes
|
||||
// setProjection({ type: 'globe' }) -- an object. Passing a string to MapLibre throws, the empty
|
||||
// catch below swallowed it, and the map silently fell back to flat mercator with NO console error.
|
||||
// That is why this appeared the moment the dashboard moved to provider:'maplibre': the default was
|
||||
|
|
@ -1600,14 +1594,14 @@ watch(() => JSON.stringify(filterSet), () => { clearTimeout(_flyT); _flyT = setT
|
|||
|
||||
useShadowStyles(`
|
||||
.rd-mapwrap{ position:relative; width:100%; height:100%; min-height:0; background:var(--dx-line, #d1d4cd); }
|
||||
/* ★ THE GALAXY IS THE CONTAINER, NOT THE TILES (2026-09-23, correcting the first attempt:
|
||||
"I asked for a spherical globe with a black background — the galaxy — with blue oceans like the
|
||||
Mapbox map, and the land was NOT to be black"). On a globe projection the sphere does not fill
|
||||
/* ★ THE GALAXY IS THE CONTAINER, NOT THE TILES (correcting the first attempt): the target is a
|
||||
spherical globe on a black background — the galaxy — with blue oceans like the Mapbox map, and
|
||||
land that is NOT black. On a globe projection the sphere does not fill
|
||||
the canvas; the corners show the CONTAINER's background. So space lives here: a deep radial
|
||||
wash behind the sphere, while the tiles keep their normal daylight colours. Scoped to
|
||||
[data-projection="globe"] so flat mercator keeps the neutral ground — space behind a flat
|
||||
rectangle would read as a mistake. */
|
||||
/* Plain BLACK behind the sphere (the report: "if not, then just do black background, not grey").
|
||||
/* Plain BLACK behind the sphere, not grey.
|
||||
On Mapbox the engine paints its own space + stars over the whole canvas via fog, so this only
|
||||
shows on engines without fog support (MapLibre), where black is the honest fallback. */
|
||||
.rd-mapwrap[data-projection="globe"]{ background:#000; }
|
||||
|
|
@ -1676,7 +1670,7 @@ useShadowStyles(`
|
|||
been removed, so the corner is free if a future control needs it).
|
||||
Stays on the right so the legend sits beside the choropleth it explains. */
|
||||
.rd-mapwrap{ --rd-attrib-clear:26px; --rd-proj-h:24px; --rd-proj-top:8px; --rd-ctl-gap:6px; }
|
||||
/* ★ LOGICAL, NOT PHYSICAL (rega's Arabic survey). A physical right:8px pins this to the right
|
||||
/* ★ LOGICAL, NOT PHYSICAL (the Arabic survey). A physical right:8px pins this to the right
|
||||
edge in every locale, so in Arabic the legend stayed where an English reader expects it while
|
||||
everything around it mirrored. inset-inline-end puts it at the END of the reading direction —
|
||||
right in English, left in Arabic — which is the pattern cities-internet already uses for its
|
||||
|
|
@ -1742,7 +1736,7 @@ useShadowStyles(`
|
|||
<ZoomOutButton :map="map" :disabled="!canZoomOut" />
|
||||
</MapToolbar>
|
||||
<!-- ★ THE ICON IS THE LABEL, because the word could not be translated without inventing one.
|
||||
rega saw "🌐 Globe" in Arabic on ?lang=ar (KA3, 3d64bcad): hardcoded English chrome on a
|
||||
a browser check saw "🌐 Globe" in Arabic on ?lang=ar (KA3, 3d64bcad): hardcoded English chrome on a
|
||||
localized map. "Globe" has no catalogue entry and "Flat" has none in any of the eleven
|
||||
locales, so translating the visible text meant either authoring ten new strings for a
|
||||
two-state toggle or leaving nine locales reading English. The catalogue DOES already ship
|
||||
|
|
|
|||
|
|
@ -195,8 +195,8 @@ const activeChips = computed(() => {
|
|||
const shown = k === PRAYER_KEY ? (Array.isArray(v) ? formatList(v, { type: 'disjunction' }) : v) : labelFor(k, v)
|
||||
return { name: k, label: `${FACETS.value[k]?.label || k}: ${shown}` }
|
||||
})
|
||||
// ★ THE TYPED SEARCH IS A FILTER AND MUST LOOK LIKE ONE (reported 2026-09-23: "I searched Bhutan
|
||||
// and it did not switch the toggle to show the country was selected"). MEASURED: typing narrows
|
||||
// ★ THE TYPED SEARCH IS A FILTER AND MUST LOOK LIKE ONE: searching "Bhutan" did not show that a
|
||||
// country was now selected. MEASURED: typing narrows
|
||||
// the scope live via bus.searchTerm (2,107 -> 13) while filterSet stays empty, so the data
|
||||
// changed and no control anywhere said why -- invisible state, the same class as the silent
|
||||
// grey pane. The conventional faceted-search answer is that free text is its OWN chip beside
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const L = (key, en) => tStatic('dashboard.' + key) || en
|
|||
* - <DashboardBreadcrumb/> STAYED, and it is ALSO a view over bus.filterSet — it rendered
|
||||
* "World ▸ South Asian Peoples" in bold white, the most prominent selection echo on screen.
|
||||
* Every verification after round 1 searched for CHIPS — chip classes, the literal "Clear all" — and
|
||||
* came back green while the reader was plainly still looking at his selection in the top bar. A
|
||||
* came back green while the reader was plainly still looking at their selection in the top bar. A
|
||||
* numeric probe for the wrong noun is indistinguishable from a fixed bug. MEASURED 2026-09-22 on
|
||||
* BOTH the leaf and the cap-4 composite: chips/Clear-All were already correctly in region 'filters'
|
||||
* (1 of each, 0 in topbar) while the topbar read "…World ▸ South Asian Peoples ⤢ Align Dashboard".
|
||||
|
|
@ -39,10 +39,9 @@ const L = (key, en) => tStatic('dashboard.' + key) || en
|
|||
*
|
||||
* ── (round 1, retained) ACTIVE-FILTER CHIPS ARE NOT HERE (card
|
||||
* BUG-DUPLICATE-FILTER-BADGES-IN-BOTH-DASHBOARD-HEADERS) ──
|
||||
* Reported 2026-09-22: in the header of the Unified Research dashboard it says Africa, Region
|
||||
* Africa Clear All... I have two duplicate clear alls, two duplicate banner badges... I want the
|
||||
* SECOND header to be the spot to do that. I want to REMOVE that functionality from the TOP header
|
||||
* where the search bar is."
|
||||
* The symptom: the Unified Research dashboard showed the same selection twice ("Africa", "Region
|
||||
* Africa", "Clear All"), with two Clear Alls and two sets of badges. The SECOND header is the one
|
||||
* place for them; the TOP header, where the search bar is, carries none.
|
||||
*
|
||||
* The duplication was ARCHITECTURAL, not a copy-paste: the dashboard mounts TWO headers in this
|
||||
* order (research-dashboard.vue:265-266) and each had grown its OWN independent chip implementation
|
||||
|
|
@ -254,8 +253,7 @@ useShadowStyles(`
|
|||
.rd-topbar .rd-filter-search{ position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
|
||||
z-index:2; width:360px; max-width:var(--rd-search-w); min-width:150px; }
|
||||
.rd-topbar .rd-filter-search .search-box{ margin:0; color:var(--rd-header-muted,#cbc5b9); }
|
||||
/* ★ FULL PILL, not a bigger number picked by eye (the report: "I want the search bar in the top
|
||||
header to be more rounded"). The input is a MEASURED 32px tall, so 16px is exactly half its
|
||||
/* ★ FULL PILL, not a bigger number picked by eye: the header's search bar is fully rounded. The input is a MEASURED 32px tall, so 16px is exactly half its
|
||||
height — the radius at which the ends become true semicircles. Any larger value renders
|
||||
identically (the browser clamps to height/2), so this is the actual maximum, and it stays
|
||||
correct if the height changes only in the sense that it is derived from a value stated right
|
||||
|
|
@ -350,8 +348,7 @@ useShadowStyles(`
|
|||
DOM order [ⓘ, Share] gives ⓘ at the logical START — left in English, and RIGHT of Share in
|
||||
Arabic, because this flex row reverses its main axis under dir=rtl. Both are the SAME reusable
|
||||
header-button form, shape="rounded-square" (radius = the vertical tabs' --vtl-radius).
|
||||
SHARE the WHOLE dashboard (decided 2026-09-20): "if we could actually embed this on a partner
|
||||
site from one share button." Reuses the SAME ShareButton the upg-100 MFE ships (no hand-rolled
|
||||
SHARE the WHOLE dashboard: one share button gives the embed for a partner site. Reuses the SAME ShareButton the upg-100 MFE ships (no hand-rolled
|
||||
variant); the embed path is THIS profile's tree-relative leaf, which ShareButton joins to the
|
||||
served tree root. The ⓘ reuses MapInfoButton — it renders ONLY once the tab's MapInfo answers
|
||||
(fail-open: no host, no ⓘ, same as before), so a standalone leaf with no gallery is unchanged. -->
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ function religionLetterFromLabel(key, families) {
|
|||
|
||||
|
||||
/**
|
||||
* COUNTRY -> REGION HUE. The requirement: "countries should match the colour of their regions."
|
||||
* COUNTRY -> REGION HUE: each country takes the colour of its region.
|
||||
* A country has no palette of its own, so the Top-countries bar inherits its REGION's hue —
|
||||
* which makes the bar chart, the Region donut and the map legend agree at a glance.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* useTreeLegendColors.js — the STACKED TREE LEGEND's colour resolver (card BUILD-TREE-LEGEND-COLOURS).
|
||||
*
|
||||
* The ruling: the legend's colours must come from the cap-3 research map's semantic tree, so
|
||||
* The rule: the legend's colours must come from the cap-3 research map's semantic tree, so
|
||||
* the legend can NEVER drift from the map. This module therefore declares NO palette of its own —
|
||||
* every hex is read from the SAME colour-strategy modules the map's pins paint with:
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue