diff --git a/library/utils/viewportScrim.js b/library/utils/viewportScrim.js index edc6753bf..72e04368c 100644 --- a/library/utils/viewportScrim.js +++ b/library/utils/viewportScrim.js @@ -27,8 +27,7 @@ * lives, not where the containment does. * ✗ width:100vw; height:100vh. MEASURED: fixes the SIZE but NOT the offset — the box still starts * at (256,64), so it overflows off-screen right and bottom while the rail stays uncovered. A - * viewport-sized scrim in the wrong place is still the wrong scrim. (And the size is not even - * reliable: in one pane those units measured 1324x944 inside a 1280x900 window — see below.) + * viewport-sized scrim in the wrong place is still the wrong scrim. * ✗ . These components render INSIDE SHADOW ROOTS (measured: .dt-wrap's root * is a ShadowRoot whose host is ). Teleporting to document.body leaves the * shadow-scoped CSS behind and the modal renders UNSTYLED. @@ -103,11 +102,10 @@ export function useViewportScrim(openRef) { // without ever naming the shorthand. // No capture (o is 0,0): this collapses to a box at 0,0 sized to the viewport — i.e. exactly // what inset:0 meant in the first place. - // ★ PIXELS FROM A MEASUREMENT, NOT vw/vh. 100vw/100vh are not reliably "the viewport" here: - // in the upg poster-wall pane the same declarations produced a rect of 1324x944 inside a - // 1280x900 window (computed width WAS 1280px, so the units resolved against a different - // viewport, not a transform — the ancestor chain had none). documentElement.clientWidth/ - // Height is the actual laid-out viewport box and matched the window in every pane measured. + // PIXELS FROM A MEASUREMENT rather than vw/vh: documentElement.clientWidth/Height is the + // laid-out viewport box and excludes any classic scrollbar gutter, which is what a scrim + // should cover. (Both forms measured identically once box-sizing was fixed below — the + // 1324x944 reading that first looked like a unit problem was padding, not units.) const doc = el.ownerDocument || (typeof document !== 'undefined' ? document : null) const vw = doc?.documentElement?.clientWidth || 0 const vh = doc?.documentElement?.clientHeight || 0 @@ -119,6 +117,13 @@ export function useViewportScrim(openRef) { bottom: 'auto', width: `${vw}px`, height: `${vh}px`, + // ★ REQUIRED, because box-sizing is NOT inherited into a shadow root from the host page's + // reset (poster-wall.vue's own CSS says so at .tape). Several scrims carry padding — the upg + // .modal-scrim has padding:22px to inset its dialog — so under the default content-box the + // border box became width+44 x height+44: MEASURED 1324x944 for an explicit 1280px/900px. + // inset:0 never exposed this because pinning all four edges sizes the BORDER box directly; + // switching to width/height is what made box-sizing start to matter. + boxSizing: 'border-box', } } diff --git a/plugin-designs/research-dashboard/dashboard.config.js b/plugin-designs/research-dashboard/dashboard.config.js index 0c7a0419a..66341fd36 100644 --- a/plugin-designs/research-dashboard/dashboard.config.js +++ b/plugin-designs/research-dashboard/dashboard.config.js @@ -12,6 +12,7 @@ import { lbl } from './src/composables/useDashboardAggregations.js' import { RELIGION_FAMILIES } from '@map/colors/religion.js' import { canonicalFamilyName } from '@map/colors/language-family.js' import langFamilyByIso from '@map/data/langFamilyByIso.json' +import { getPrayerLevel, LABELS as PRAYER_LABELS } from '@map/colors/prayer-progress.js' // same numeric coercion the DataTable uses for its num columns const num = (v) => { if (typeof v === 'number') return v; const n = parseFloat(String(v ?? '').replace(/[^0-9.\-]/g, '')); return isNaN(n) ? null : n } @@ -43,6 +44,36 @@ export default { // ── facet accessors ── THE single former-duplicated FACET_ACC, declared ONCE. facets: { + // ── PRAYER (card BUILD-ADD-A-PRAYER-FILTER-FIRST-IN-THE-ROW) ── + // `people_praying` is a LIVE per-row pray-tools field, already read by three shipping + // consumers in this very config: the KPI sum (kpis 'praying'), the table column + // ('Praying', num(r.people_praying)) and detail.fields. No derived boolean was invented. + // + // It is a COUNT, not a flag, and the app already splits that count THREE ways in + // library/colors/prayer-progress.js (the prayer map's colour strategy): + // none (0/null) · partial (0 < n < 144) · full (n >= 144) + // Three levels => a DROPDOWN. We reuse getPrayerLevel + its LABELS instead of re-deriving + // the split, so the filter can never disagree with the colour of the pin it filters to — + // one threshold, one owner. + // + // ★ WHY THIS ACCESSOR MUST LIVE IN THE CONFIG AND NOT IN THE ADAPTER. useDashboardShell + // builds FACET_ACC from `config.facets` ALONE, and filteredRows FAILS OPEN on a key it + // cannot resolve (`return a ? a(r) === filterSet[k] : true`). A facet defined only in + // DashboardResearchFilters.vue therefore renders a working dropdown, lands a chip, and + // filters NOTHING — silently. A filter that filters nothing is worse than no filter, + // because the user reads the chip as proof the scope narrowed. The adapter keeps a local + // default and merges config-first, so THIS definition is the one that takes effect. + // + // Returns the human LABEL, which is both the display text and the bus value: the same + // value-is-label contract every non-People axis follows. + prayer_level: { + label: 'Prayer', + get: (r) => { + const n = r?.people_praying + const raw = n && typeof n === 'object' ? n.value : n + return PRAYER_LABELS[getPrayerLevel({ peoplePraying: raw ?? null })] + }, + }, religion: { label: 'Religion', get: (r) => lbl(r.religion) }, // ── Religion TIER 1 (card BUILD-STACKED-TREE-LEGEND) ── // The Doxa API's religion code is 1-3 chars whose FIRST LETTER is the family @@ -267,7 +298,10 @@ export default { // People = `affinity_bloc`, the SAME key the tree's top tier filters on, so a pick here and // a click there are one selection (card BUG-PEOPLE-AND-AFFINITY-BLOC-ARE-THE-SAME-AXIS-TWICE). // It stores the code and displays the name via the facet's `optionLabel`. - dropdowns: ['affinity_bloc', 'wagf_region', 'wagf_block', 'country_code', 'primary_language', 'religion'], + // `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. + dropdowns: ['prayer_level', 'affinity_bloc', 'wagf_region', 'wagf_block', 'country_code', 'primary_language', 'religion'], searchPlaceholder: 'Search people groups, place, language, religion…', }, // `rop1_name` (not `affinity_bloc`) so free-text search matches the People NAME — DX-life's