Comments only: neutral wording in research-dashboard, pplr-data-maps and the Research Gallery
Quoted remarks, attributions and person-tied dates in comments become plain statements of the same technical fact, and the names of internal tools and roles become neutral descriptions of the check that was run. No code, markup or style changes: the rebuilt bundles are byte-identical apart from Vue's scoped-style ids, which hash each component's source, comments included.
This commit is contained in:
parent
3f7d489409
commit
bba382d9af
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