Regions: their second filters panel was missing, and it carries two of their bugs

The expanded filters panel was the largest interactive surface never
compared to theirs, and comparing it found a whole panel absent.
Measured on their live country page: TWO .filters__panel blocks and
THREE .filter-toggle buttons — Adopted, Engaged, Exact Search — against
our one panel and zero toggles. The dropdowns already matched exactly,
four of them, with their country dropdown correctly hidden by
lockCountryCode.

A tell I had missed: the search already read active.exact for the Fuse
threshold, so Exact Search's BEHAVIOUR was ported while the control
that sets it never was.

Ported their panel, their three toggles, their toggle semantics (a
second click removes the filter), their activeFilters shape so each
toggle also renders as a chip, and .filter-toggle styled from the
MEASURED computed values of their live button rather than by chasing
tokens. The labels needed no new keys — Adopted and Engaged were
already translated in all eight locales.

Two of their bugs are cloned deliberately, both measured on their live
/regions/india at 855 rows:

- ENGAGED RETURNS ZERO. Their branch filters on u.people_praying, and
  people_praying is not in the field list they request — they fetch
  people_committed. The field is always undefined, so the filter
  matches nothing: their page goes 855 to 0. Reading people_committed
  instead would make us differ, and the ruling is that what exists on
  DX-life must match. The upstream fix is one word in their file.
- THE ACTIVE STATE IS WHITE ON WHITE. Their rule sets background-color
  var(--color-surface-brand-lighter, rgba(0,80,160,0.05)) with color
  var(--color-white), and --color-surface-brand-lighter is not defined
  in their token files. Their active button computes background
  rgb(255,255,255) and color rgb(255,255,255) — an invisible label,
  now visible in our screenshot too. Defining that one token upstream
  fixes both sides.

Both are flagged for the Driver rather than quietly corrected: a bar
chart was already removed under this ruling for the opposite reason,
being in ours and not theirs.

Proof: _probe-filter-toggles.mjs, all assertions pass — two panels,
three toggles, nine measured style properties, Adopted narrowing 24 to
2 and appearing as a chip, a second click restoring, Engaged emptying
to 0 exactly as theirs does, Exact Search changing nothing alone.

Regression: 22 suites green.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 06:53:22 +00:00
parent 81cb8ad235
commit a32097751b
7 changed files with 131 additions and 60 deletions

View file

@ -86,6 +86,8 @@ const T = computed(() => ({
total: t('Total'),
alternate_name: t('Alternate Names'),
exact_filter: t('Exact Search'),
adopted_filter: t('Adopted'),
engaged_filter: t('Engaged'),
}))
const rows = ref([])
@ -95,7 +97,11 @@ const searchTerm = ref('')
const filtersExpanded = ref(false)
const shown = ref(props.perPage)
// Their filter set, minus `country` — a locked country hides that one (their own rule).
const active = ref({ wagf_region: null, wagf_block: null, rop1: null, religion: null })
// adopted / engaged / exact live in the SAME activeFilters object theirs uses, not a separate
// flag set, because their applyDropdownFilters() iterates that object and branches on the
// field name — and their chips row renders whatever is in it, toggles included.
const active = ref({ wagf_region: null, wagf_block: null, rop1: null, religion: null,
adopted: null, engaged: null, exact: null })
const label = (f) => (f && typeof f === 'object' ? f.label : f) || ''
const value = (f) => (f && typeof f === 'object' ? f.value : f) || ''
@ -166,7 +172,22 @@ const byPath = (o, path) => path.split('.').reduce((a, k) => (a == null ? undefi
// Their dropdown filters AND first, then the term. (Their filterUUPGs, same order.)
const preFiltered = computed(() => rows.value.filter((p) => {
for (const [k, v] of Object.entries(active.value)) {
if (v && value(p[k]) !== v.value) return false
if (!v) continue
// THEIR applyDropdownFilters branches, verbatim (UupgsList.vue:190-205).
if (k === 'adopted') { if (!((p.adopted_by_churches ?? 0) > 0)) return false; continue }
// ★ THEIR BUG, CLONED ON PURPOSE. Their branch is
// filtered.filter(u => (u.people_praying ?? 0) > 0)
// and `people_praying` is NOT in the field list they request — they fetch
// people_committed. So the field is always undefined and the filter matches nothing.
// MEASURED on their live /regions/india: 855 rows before, 0 after clicking Engaged.
// The scope ruling is "it exists on DX-life and must MATCH", so this reproduces their
// result rather than quietly correcting it. Reading people_committed instead would make
// us DIFFER. Flagged for the Driver: the upstream fix is one word, in their file.
if (k === 'engaged') { if (!((p.people_praying ?? 0) > 0)) return false; continue }
// Their `exact` branch is `filtered = filtered` — a no-op here. It only changes the FUSE
// threshold once a search term exists, which is where we already read active.exact.
if (k === 'exact') continue
if (value(p[k]) !== v.value) return false
}
return true
}))
@ -244,6 +265,12 @@ const showMore = computed(() => filtered.value.length > shown.value)
function loadMore() { shown.value += (props.morePerPage || props.perPage) }
watch([searchTerm, active], () => { shown.value = props.perPage }, { deep: true })
// Their toggleAdopted / toggleEngaged / toggleExact (UupgsList.vue:169-188): a second click
// REMOVES the filter, and each sets a {value:'yes', label} pair so it also appears as a chip.
function toggleFlag(name, label) {
active.value = { ...active.value, [name]: active.value[name] ? null : { value: 'yes', label } }
}
function onFilterChange(e) { if (e && e.name) active.value = { ...active.value, [e.name]: { value: e.value, label: e.label } } }
function onFilterClear(e) { if (e && e.name) active.value = { ...active.value, [e.name]: null } }
function clearAll() { active.value = { wagf_region: null, wagf_block: null, rop1: null, religion: null } }
@ -289,6 +316,25 @@ useShadowStyles(
' display:inline-flex; align-items:center; gap:7.5188px; margin-inline:auto; color:var(--dx-text); }' +
'.filters__toggle svg{ transition:transform 200ms ease; }' +
'.filters__toggle[aria-expanded="true"] svg{ transform:rotate(180deg); }' +
// .filter-toggle — their _filters.scss:218 composed with .input and .fit-content, taken
// from the MEASURED computed style of their live button rather than by chasing tokens:
// padding 5.65323px 13.3px, font-size 15.625px, radius 8px, 1px solid rgb(128,128,128),
// white ground, ink rgb(42,51,42), width fit-content, and a 150ms border/background
// transition.
'.filter-toggle{ padding:5.65323px 13.3px; cursor:pointer; font:inherit;' +
' font-size:15.625px; color:#2a332a; border:1px solid rgb(128,128,128); border-radius:8px;' +
' background-color:#fff; width:fit-content;' +
' transition:border-color 150ms ease, background-color 150ms ease; }' +
'.filter-toggle:hover{ border-color:#92b195; }' +
// ★ THEIR ACTIVE STATE IS WHITE-ON-WHITE, CLONED ON PURPOSE. Their rule is
// background-color: var(--color-surface-brand-lighter, rgba(0,80,160,0.05));
// color: var(--color-white);
// and --color-surface-brand-lighter is NOT DEFINED in their token files, so the 5%-alpha
// fallback lands on white while the ink turns white. MEASURED on their live page: the
// active button computes backgroundColor rgb(255,255,255) and color rgb(255,255,255) —
// an invisible label. Reproduced so the clone matches; flagged for the Driver, because
// defining that one token upstream fixes it for them and for us at once.
'.filter-toggle[data-active]{ background-color:rgba(0, 80, 160, 0.05); color:#fff; }' +
'.filters__panel{ display:flex; flex-wrap:wrap; gap:13.3px; justify-content:center;' +
' align-items:flex-start; }' +
'.filters__active{ display:flex; flex-wrap:wrap; gap:7.5188px; align-items:center;' +
@ -409,6 +455,31 @@ useShadowStyles(
/>
</div>
<!-- THEIR SECOND PANEL (UupgsList.vue:541-560). We shipped only the dropdown panel, so
three of their filter controls were simply absent — measured on their live country
page: two .filters__panel blocks and three .filter-toggle buttons against our one
and zero. -->
<div v-if="filtersExpanded" class="filters__panel">
<button
type="button"
class="filter-toggle"
:data-active="active.adopted ? '' : undefined"
@click="toggleFlag('adopted', T.adopted_filter)"
>{{ T.adopted_filter }}</button>
<button
type="button"
class="filter-toggle"
:data-active="active.engaged ? '' : undefined"
@click="toggleFlag('engaged', T.engaged_filter)"
>{{ T.engaged_filter }}</button>
<button
type="button"
class="filter-toggle"
:data-active="active.exact ? '' : undefined"
@click="toggleFlag('exact', T.exact_filter)"
>{{ T.exact_filter }}</button>
</div>
<div v-if="activeChips.length" class="filters__active">
<span v-for="c in activeChips" :key="c.name" class="filter-chip">
{{ c.label }}

File diff suppressed because one or more lines are too long

View file

@ -856,7 +856,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-f2Bf2Le3VRKjVFXcXhuRzEpHcuIXiCQDY2MSdKQG/ZKsRP2+a7I2/82myqQQb6aa",
"integrity": "sha384-/0xJsdPMt2izpGpElmMftHWA/ckwtU/VwVbrUrXMaSPeZ0EK86HfggXZmp/vHtGi",
"elements": [
"regions-dashboard"
],
@ -876,7 +876,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-f2Bf2Le3VRKjVFXcXhuRzEpHcuIXiCQDY2MSdKQG/ZKsRP2+a7I2/82myqQQb6aa\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-/0xJsdPMt2izpGpElmMftHWA/ckwtU/VwVbrUrXMaSPeZ0EK86HfggXZmp/vHtGi\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -890,7 +890,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-f2Bf2Le3VRKjVFXcXhuRzEpHcuIXiCQDY2MSdKQG/ZKsRP2+a7I2/82myqQQb6aa\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-/0xJsdPMt2izpGpElmMftHWA/ckwtU/VwVbrUrXMaSPeZ0EK86HfggXZmp/vHtGi\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -904,7 +904,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-f2Bf2Le3VRKjVFXcXhuRzEpHcuIXiCQDY2MSdKQG/ZKsRP2+a7I2/82myqQQb6aa\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-/0xJsdPMt2izpGpElmMftHWA/ckwtU/VwVbrUrXMaSPeZ0EK86HfggXZmp/vHtGi\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-f2Bf2Le3VRKjVFXcXhuRzEpHcuIXiCQDY2MSdKQG/ZKsRP2+a7I2/82myqQQb6aa" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-/0xJsdPMt2izpGpElmMftHWA/ckwtU/VwVbrUrXMaSPeZ0EK86HfggXZmp/vHtGi" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-f2Bf2Le3VRKjVFXcXhuRzEpHcuIXiCQDY2MSdKQG/ZKsRP2+a7I2/82myqQQb6aa" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-/0xJsdPMt2izpGpElmMftHWA/ckwtU/VwVbrUrXMaSPeZ0EK86HfggXZmp/vHtGi" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-f2Bf2Le3VRKjVFXcXhuRzEpHcuIXiCQDY2MSdKQG/ZKsRP2+a7I2/82myqQQb6aa" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-/0xJsdPMt2izpGpElmMftHWA/ckwtU/VwVbrUrXMaSPeZ0EK86HfggXZmp/vHtGi" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: