Free the filter menu from its clipping ancestor; square the people-group images

FILTER DROPDOWNS WERE INVISIBLE. Opening a filter rendered its options underneath
the surrounding content, making the dashboard's primary control unusable.

The cause was CLIPPING, not stacking order, and that distinction is why an earlier
attempt failed. The filter band is a one-row horizontal scroller authored as
overflow-x:auto with overflow-y:visible — but CSS Overflow 3 coerces a visible axis
to auto when the other axis is not visible, so it computes to auto/auto and clips
vertically. Clipping happens during painting, before stacking order is consulted, so
raising the band to z-index 55 changed nothing: it won a stacking fight that was never
the problem. You cannot re-order pixels that were never generated.

The menu is now position:fixed, anchored to the trigger's viewport rect, which takes it
out of the clip chain entirely. Fixed coordinates go stale whenever anything scrolls or
resizes, so they are recomputed on open and while open, listening in the capture phase
so the dashboard's inner scroll regions are seen too.

PEOPLE-GROUP IMAGES WERE LETTERBOXED. A fixed 130px height against a 220px column
cropped each person to a band. The image is now a square via aspect-ratio, and the
column floor rises to 240px, roughly doubling the visible area. object-fit:cover is
kept deliberately — stretching to a square would distort faces, which is worse than
the letterbox it replaces.
This commit is contained in:
doxa-maps-88655c70 2026-09-22 13:31:11 +00:00
parent 44dd2d01f3
commit 012c79328a
27 changed files with 365 additions and 157 deletions

View file

@ -40,6 +40,68 @@ const searchText = ref('')
const highlightedIndex = ref(-1)
const rootRef = ref(null)
const searchInputRef = ref(null)
const triggerRef = ref(null)
/**
* ── ★ WHY THE MENU IS position:fixed AND NOT JUST A BIGGER z-index ──
* (card BUG-FILTER-DROPDOWN-HIDDEN-BEHIND-EVERYTHING-ZINDEX)
*
* MEASURED, not guessed: the menu's parent row (.sfh__filters-row in SearchFilterHeader)
* is the one-row horizontal scroller. It authors `overflow-x:auto; overflow-y:visible`,
* but CSS Overflow §3 COERCES a `visible` axis to `auto` when the other axis is not
* visible — so it computes to `overflow: auto auto` and CLIPS VERTICALLY. Live probe at
* 1440x900: the row's content box ended at y=95 while the open menu wanted y=62..343, so
* ~248px of the menu was never painted. A deep (shadow-piercing) hit test below y=95 hit
* .rd-stats and the facet rail — i.e. the content BEHIND the menu — because the menu did
* not exist there at all.
*
* Raising z-index CANNOT fix that, and a previous fix that raised the band to z-index:55
* changed nothing: clipping happens during PAINTING, before stacking order is consulted.
* You cannot re-order pixels that were never generated. (z-index:55 was already winning
* the stacking fight it was sent to fight — which is why the bug survived that "fix".)
*
* position:fixed re-parents the menu to the VIEWPORT's containing block, which also takes
* it out of the scroller's clip chain. That is the whole fix; the accompanying high
* z-index only preserves paint order once the menu is free. No ancestor here sets
* transform/filter/perspective (the properties that WOULD capture a fixed descendant),
* verified live across the chain up to <html>, so this is safe in the shadow-root/custom-
* element embedding all three consumers use.
*
* Because fixed coords are viewport-relative they go stale when anything scrolls or
* resizes, so they are recomputed on open and on scroll/resize while open.
*/
const menuStyle = ref({})
function positionMenu() {
const t = triggerRef.value
if (!t) return
const r = t.getBoundingClientRect()
const vw = window.innerWidth
const vh = window.innerHeight
const MENU_MAX = 320
const GAP = 4
const EDGE = 8
// Horizontal CLAMP. A fixed box is viewport-relative, so it does NOT follow the filters
// row's horizontal scroll the way an absolute child did — a trigger scrolled past the
// right edge (measured at 520px wide: three of six triggers sit at x=524..1024) would
// otherwise place the menu completely off-screen. Clamp into the viewport so the menu is
// always reachable, matching the trigger width when it fits.
const width = Math.round(Math.min(r.width, vw - EDGE * 2))
const left = Math.round(Math.min(Math.max(r.left, EDGE), vw - width - EDGE))
// Flip above the trigger when there is not enough room below (keeps the list usable in
// a short band near the bottom of the viewport).
const below = vh - r.bottom
const flipUp = below < 180 && r.top > below
menuStyle.value = flipUp
? { top: 'auto', bottom: `${Math.round(vh - r.top + GAP)}px`, left: `${left}px`, width: `${width}px`, maxHeight: `${Math.round(Math.min(MENU_MAX, r.top - GAP - EDGE))}px` }
: { top: `${Math.round(r.bottom + GAP)}px`, bottom: 'auto', left: `${left}px`, width: `${width}px`, maxHeight: `${Math.round(Math.min(MENU_MAX, below - GAP - EDGE))}px` }
}
// `true` = capture phase, so this also sees scrolls inside the dashboard's inner scroll
// regions (they do not bubble to window).
function onReflow() { if (isOpen.value) positionMenu() }
const filteredOptions = computed(() => {
if (!searchText.value) return props.options
@ -60,8 +122,11 @@ function open() {
isOpen.value = true
searchText.value = ''
highlightedIndex.value = -1
positionMenu()
document.addEventListener('click', onClickOutside)
nextTick(() => searchInputRef.value?.focus())
window.addEventListener('scroll', onReflow, true)
window.addEventListener('resize', onReflow)
nextTick(() => { positionMenu(); searchInputRef.value?.focus() })
}
function close() {
@ -69,6 +134,8 @@ function close() {
searchText.value = ''
highlightedIndex.value = -1
document.removeEventListener('click', onClickOutside)
window.removeEventListener('scroll', onReflow, true)
window.removeEventListener('resize', onReflow)
}
function onSearchInput(e) {
@ -119,12 +186,17 @@ function clear() {
close()
}
onBeforeUnmount(() => document.removeEventListener('click', onClickOutside))
onBeforeUnmount(() => {
document.removeEventListener('click', onClickOutside)
window.removeEventListener('scroll', onReflow, true)
window.removeEventListener('resize', onReflow)
})
</script>
<template>
<div ref="rootRef" class="filter-dropdown" :class="{ 'is-open': isOpen }">
<button
ref="triggerRef"
class="filter-dropdown__trigger input"
:data-active="value ? '' : undefined"
type="button"
@ -140,7 +212,7 @@ onBeforeUnmount(() => document.removeEventListener('click', onClickOutside))
<path d="M2 4l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<div v-if="isOpen" class="filter-dropdown__menu" role="listbox">
<div v-if="isOpen" class="filter-dropdown__menu" role="listbox" :style="menuStyle">
<input
ref="searchInputRef"
class="filter-dropdown__search"
@ -205,10 +277,16 @@ onBeforeUnmount(() => document.removeEventListener('click', onClickOutside))
It must declare its own foreground instead of inheriting: the dark header passes down
a near-white colour, which on this white surface measured ~1.2:1 — invisible option
labels. A popover never inherits text colour across a light/dark boundary. */
position: absolute; top: 100%; left: 0; right: 0; z-index: 10; background-color: var(--dx-elevated, #fff);
/* ★ position:FIXED, not absolute — see the long note in <script setup>. An absolute menu
is clipped away by the filters row's computed `overflow:auto auto`; no z-index can
recover pixels that were never painted. Fixed escapes that clip chain; the z-index
below only keeps paint order once it is free. Coordinates (top/left/width/max-height)
are set inline by positionMenu() because a fixed box is viewport-relative. */
position: fixed; z-index: 2147483000; background-color: var(--dx-elevated, #fff);
color: var(--dx-text, #3b463d);
border: 1px solid var(--dx-line, #d1d4cd); border-radius: 8px; margin-top: 4px;
border: 1px solid var(--dx-line, #d1d4cd); border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); overflow: hidden;
display: flex; flex-direction: column; min-width: 160px;
}
.filter-dropdown__search {
border: none; border-bottom: 1px solid var(--dx-line, #d1d4cd); border-radius: 0; padding: 6px 12px; width: 100%;
@ -221,7 +299,11 @@ onBeforeUnmount(() => document.removeEventListener('click', onClickOutside))
text-decoration: underline; text-decoration-color: var(--dx-accent, #92b195); text-underline-offset: 3px;
}
.filter-dropdown__clear:hover { background-color: var(--dx-accent-weak, rgba(146, 177, 149, .18)); }
.filter-dropdown__options { max-height: 250px; overflow-y: auto; }
/* flex:1 1 auto + min-height:0 so the list is what shrinks when positionMenu() caps the
menu's height to the room actually available below (or above) the trigger — the search
box and Clear row stay pinned and reachable instead of being pushed out of view. */
.filter-dropdown__options { flex: 1 1 auto; min-height: 0; max-height: 250px; overflow-y: auto; }
.filter-dropdown__search, .filter-dropdown__clear { flex: 0 0 auto; }
.filter-dropdown__option {
padding: 5px 12px; cursor: pointer; display: flex; justify-content: space-between; align-items: center;
font-size: 0.875rem; transition: background-color 100ms ease;

View file

@ -207,10 +207,19 @@ function onClearAll() { emit('clear-all') }
.sfh__toggle-chev { transition: transform 200ms ease; }
.sfh__toggle[aria-expanded="true"] .sfh__toggle-chev { transform: rotate(180deg); }
/* ★ ONE ROW, NEVER WRAP — horizontal-scroll on overflow. */
/* ★ ONE ROW, NEVER WRAP — horizontal-scroll on overflow.
NOTE the missing `overflow-y: visible`: it was there, and it was a LIE. CSS Overflow §3
coerces a `visible` axis to `auto` whenever the other axis is not visible, so
`overflow-x:auto; overflow-y:visible` COMPUTES to `overflow: auto auto` — a vertical clip
that reads, in source, as the exact opposite. That clip is what ate the open FilterDropdown
menu (card BUG-FILTER-DROPDOWN-HIDDEN-BEHIND-EVERYTHING-ZINDEX); measured live, ~248px of a
281px menu was never painted. Writing only `overflow-x` is honest about what the box does.
This row therefore still clips vertically ON PURPOSE (it must, to scroll horizontally) —
which is why FilterDropdown's menu is position:FIXED and escapes the clip chain entirely
rather than trying to out-z-index it. Do not "restore" an overflow-y here; it cannot work. */
.sfh__filters-row {
display: flex; flex-wrap: nowrap; align-items: center; gap: 10px;
overflow-x: auto; overflow-y: visible; padding-bottom: 4px;
overflow-x: auto; padding-bottom: 4px;
scrollbar-width: thin; -webkit-overflow-scrolling: touch;
}
/* keep each control at its width instead of collapsing into a 2nd line */

View file

@ -180,6 +180,17 @@ const activeFilterCount = computed(() => Object.values(activeFilters.value || {}
// Active dropdown chips the user can see/remove (only meaningful when filters are shown).
const visibleFilterCount = computed(() => Object.keys(activeFilters.value).length)
// IS THIS GRID CURRENTLY SHOWING A SUBSET? Drives the count line's third state (see the markup
// note at .pgg-body): a host-embedded grid stays silent while it merely echoes the dashboard's
// total, and speaks up as soon as it is actually narrowed. Reuses activeFilterCount rather than
// Object.keys() because a CLEARED filter still occupies a key — counting keys would report
// "narrowed" for a grid showing everything, which is the precise lie the comment above guards.
// The search term counts too: typing narrows the grid just as a facet does — and it reads
// effSearchTerm, not the local ref, because that computed already resolves the local-control-else-
// incoming-prop precedence. In the composite the search lives in the HOST's header, so the local
// ref is always empty there and only the prop is real.
const isNarrowed = computed(() => activeFilterCount.value > 0 || !!(effSearchTerm.value || '').trim())
function escapeHtml(s) {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;')
@ -576,9 +587,28 @@ onMounted(getUUPGs)
</div>
<div class="pgg-body">
<div v-if="useTotal" class="repel">
<!-- THE COUNT LINE, AND WHY IT HAS THREE STATES RATHER THAN TWO.
Two Driver requests point in opposite directions and BOTH are right:
(a) earlier — "the same 2,107 renders five times down one page", so the host-embedded
grid was given show-total=false and went silent;
(b) 2026-09-22 — "the people groups grid underneath should tell me how many people
groups are currently displaying, and that number should change based on the
selections."
Those only LOOK contradictory. (a) is about a number that merely ECHOES the dashboard's
own total; (b) is about a number that DESCRIBES THIS GRID. Before today they were always
the same value, so the echo was pure noise. After the label-vs-value fix the grid really
does narrow — a region selection takes it well below the dashboard's 2,107 — so the two
numbers now differ, and a differing number is information rather than repetition.
Hence: show it when it SAYS something (a filter is narrowing this grid), stay silent when
it would only repeat the host. showTotal=true is unchanged for the standalone element,
where this line is the only row-total and must always render. -->
<div v-if="useTotal || isNarrowed" class="repel">
<div class="font-size-sm">
<template v-if="!loading">{{ t.total || 'Total' }}: {{ total.toLocaleString('en-US') }}</template>
<template v-if="!loading">
{{ isNarrowed && !useTotal
? (t.showing || 'Showing') + ' ' + total.toLocaleString('en-US')
: (t.total || 'Total') + ': ' + total.toLocaleString('en-US') }}
</template>
<span v-else>&nbsp;</span>
</div>
</div>
@ -697,12 +727,34 @@ onMounted(getUUPGs)
.filters__clear-all { background: none; border: none; cursor: pointer; font-size: 0.875rem; color: var(--ink); text-decoration: underline; text-decoration-color: var(--brand); text-underline-offset: 4px; }
.repel { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.font-size-sm { font-size: 0.875rem; color: var(--mut); }
.grid.uupgs-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; }
/* 240px floor (was 220px) so the square image below is genuinely LARGER, not just squarer — the
Driver asked for both. auto-fill keeps the column count responsive, so this only widens cards when
there is room; on a narrow viewport the track still collapses to one full-width column rather than
overflowing. Paired with .uupg__image's aspect-ratio:1/1 this takes the image from a 220x130
letterbox to a 240x240 square: ~2x the visible area, and the face is no longer cropped to a band. */
.grid.uupgs-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
.card.uupg__card {
display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--line);
border-radius: 12px; overflow: hidden; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.uupg__image { width: 100%; height: 130px; object-fit: cover; background: var(--line); }
/* SQUARE, NOT A LETTERBOX — the image is the point of the card.
Driver-reported 2026-09-22: "the images need to be larger. They need to be more like SQUARES and
less like RECTANGLES so that you can see the people image better." The old rule was a fixed
height:130px against a column that is at least 220px wide (see .grid.uupgs-list above), i.e. a
220x130 letterbox — roughly 1.7:1 — which crops a standing person to a band and renders faces small.
aspect-ratio:1/1 rather than a taller fixed height: the columns are fluid (auto-fill minmax(220px,
1fr)), so ANY fixed pixel height re-letterboxes at a different viewport width. A ratio holds the
square at every column width, which is the actual requirement.
object-fit:cover is KEPT and is load-bearing — it was already correct. Switching to a square while
letting the image stretch would distort faces, which is worse than the letterbox it replaces; cover
crops instead of squashing. object-position defaults to centre, which suits these portraits.
Consequence accepted: taller cards make the page longer. That is fine here — the grid is a
scroll-page section, and in the cap-4 composite it sits BELOW a fixed-height dashboard band, so the
page simply grows downward rather than fighting an inner-scroll region. */
.uupg__image { width: 100%; aspect-ratio: 1 / 1; object-fit: cover; background: var(--line); }
.uupg__header { padding: 12px 14px 6px; }
.uupg__name { font-size: 1rem; font-weight: 700; line-height: 1.2; margin: 0 0 4px; }
.uupg__country { font-size: 0.85rem; color: var(--mut); margin: 0; }

View file

@ -92,7 +92,14 @@ useShadowStyles(`
.rd-legend::-webkit-scrollbar-thumb:hover{ background:#9aa197; }
.rd-li{ display:flex; align-items:center; gap:5px; cursor:pointer; white-space:nowrap; padding:1px 2px; border-radius:3px; }
.rd-li:hover{ background:var(--dx-surface, #f3f3f1); } .rd-li.on{ background:#fff4ea; font-weight:700; color:var(--rd-ink, #3b463d); }
.rd-li i{ width:9px; height:9px; border-radius:2px; flex:0 0 auto; } .rd-li b{ margin-left:auto; color:var(--rd-muted, #6d766e); font-variant-numeric:tabular-nums; }
.rd-li i{ width:9px; height:9px; border-radius:2px; flex:0 0 auto; } .rd-li b{ margin-left:auto; color:var(--rd-muted, #6d766e); font-variant-numeric:tabular-nums; flex:0 0 auto; padding-left:4px; }
/* The label is the ONLY item allowed to shrink. It used to be a bare text node, which in a
nowrap flex row cannot shrink at all: the row grew past .rd-legend (overflow-x:hidden) and
carried the VALUE out of sight — "Ethnoreligion - Chinese Folk Religion" rendered with no
number at all. Wrapping it in .rd-lt with min-width:0 makes it yield instead, so the count
(flex:0 0 auto) is always the last thing on screen. Ellipsis, not wrap: the legend sits in a
max-height:108px scroller beside a donut, so growing rows would push items out of view. */
.rd-lt{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.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); }
@ -119,7 +126,7 @@ useShadowStyles(`
</svg>
<div class="rd-legend">
<span v-for="s in chart.geometry" :key="s.key" class="rd-li" :class="{ on: isActive(chart.facet, s.key) }"
:data-facet="chart.facet" :data-key="s.key"><i :style="{ background: s.color }"></i>{{ s.key }}<b>{{ s.value }}</b></span>
:data-facet="chart.facet" :data-key="s.key" :title="s.key + ': ' + s.value"><i :style="{ background: s.color }"></i><span class="rd-lt">{{ s.key }}</span><b>{{ s.value }}</b></span>
</div>
</div>
</div>

View file

@ -221,8 +221,58 @@ function addLayers() {
m.addLayer({ id: 'rd-choropleth', type: 'fill', source: 'rd-countries', 'source-layer': 'country_boundaries',
paint: { 'fill-color': fillColor, 'fill-opacity': fillOpacity, 'fill-outline-color': '#ffffff' } })
m.addSource('rd-bubbles', { type: 'geojson', data: bubbleGeo() })
// ── PG pins: NAVY fill + light halo (BUG-DEAF-PIN-COLOUR-ORANGE-IS-UNREADABLE) ──────
// WHAT WAS WRONG. The pins were '#e67e22' at circle-opacity 0.6 with a 1px '#fff' ring.
// Two independent faults, and the opacity one is the reason the Driver could not see them:
// 1) 0.6 alpha means the pin is NOT #e67e22 on screen — it composites with whatever
// choropleth bin is underneath. Measured effective colour + contrast-vs-background
// across the actual BINS ramp above (#e0e0e0/#b0b0b0/#808080/#404040/#000000):
// on #e0e0e0 -> #e4a56e (1.61) on #b0b0b0 -> #d0925b (1.22)
// on #808080 -> #bd7f48 (1.18) on #404040 -> #a4652e (2.21)
// on #000000 -> #8a4c14 (3.12)
// Over the mid greys the pin sat at 1.18–1.22 contrast — essentially INVISIBLE, and
// the same pin rendered a different colour per country. That is the reported bug.
// 2) The '#fff' ring is only 1.12 contrast against the '#f2f2f2' no-data fill and 1.32
// against the lightest bin, so on light countries the ring vanished too.
//
// WHAT THIS IS NOW. Driver's instruction: navy blue fill, dark outline on light mode.
// Fill '#1a237e' (Material Indigo 900 — the darkest navy in the Material palette this
// product already standardises on) at FULL opacity, so a pin is one fixed colour
// everywhere instead of five. Measured vs the same ramp: 10.03 / 6.11 / 3.35 / 1.28 /
// 1.59, and 11.83 against the '#f2f2f2' no-data fill — i.e. strong everywhere the map is
// light, which is where these pins actually live.
//
// WHY A HALO AND NOT JUST A DARK OUTLINE. A MapLibre circle layer has exactly ONE
// stroke, so a single layer cannot be readable against BOTH ends of a grey-to-black
// ramp. Measured: a dark ring ('#3b463d') scores min 1.05 across the ramp and only 1.34
// against the navy fill — over the dark bins that is a dark dot inside a dark ring, the
// original bug in a new hue. So the pin is drawn as TWO stacked layers: a light halo
// ('#f3f3f1', the Material surface token) under a navy dot whose own ring is the dark
// outline the Driver asked for. Navy-vs-halo separation is 13.24. Same construction the
// repo already uses for black pins (see library/composables/useMapLayers.js, which keeps
// a thick white ring so near-black pins survive a dark basemap) and the same conclusion
// as BUG-UPG100-DOTS-HARD-TO-SEE: raise the radius floor, drop the alpha, add a halo.
//
// REJECTED. '#0d47a1' (Blue 900) — too light, only 2.19 over the #808080 bin. '#001f5b'
// scored marginally better than #1a237e but is an off-palette one-off; the Material token
// is worth more than 0.2 of contrast. Keeping a single layer with a white ring — fails on
// the light bins, which is half the map. Tinting the pin by bin — reintroduces fault (1).
//
// NOTE FOR DARK STYLES. The Driver also asked for navy + WHITE outline in dark mode. This
// product is Material LIGHT only (no prefers-color-scheme:dark anywhere in this bundle),
// so only the light pairing ships here. The halo is already near-white, so a genuinely
// dark style would want the two stroke colours swapped, not a new fill.
// HIT-TESTING: 'rd-bubble-layer' keeps its id and stays the TOP layer — onBubbleClick,
// onMapClick's pin-guard and the __rdashPinAt verify hook all query it by name. The halo
// is a separate, NON-interactive layer beneath it and must not be queried.
// Halo radii are the pin radius + its 1px ring + a >=1px visible band. MapLibre draws
// circle-stroke-width OUTSIDE circle-radius, so the dot's true outer edge is r+1; a first
// pass used 3.6 at z1, which left only a 0.2px band that would not survive rasterisation
// at the zoomed-out view. Measured: z1 band 1.1px, z6 band 1.6px.
m.addLayer({ id: 'rd-bubble-halo', type: 'circle', source: 'rd-bubbles',
paint: { 'circle-radius': ['interpolate', ['linear'], ['zoom'], 1, 4.5, 6, 8], 'circle-color': '#f3f3f1', 'circle-opacity': 0.95 } })
m.addLayer({ id: 'rd-bubble-layer', type: 'circle', source: 'rd-bubbles',
paint: { 'circle-radius': ['interpolate', ['linear'], ['zoom'], 1, 2, 6, 5], 'circle-color': '#e67e22', 'circle-opacity': 0.6, 'circle-stroke-width': 1, 'circle-stroke-color': '#fff' } })
paint: { 'circle-radius': ['interpolate', ['linear'], ['zoom'], 1, 2.4, 6, 5.4], 'circle-color': '#1a237e', 'circle-opacity': 1, 'circle-stroke-width': 1, 'circle-stroke-color': '#0d1330' } })
// selection pen — a prominent neon ring over the selected PG pin(s), drawn ABOVE the normal bubbles.
m.addSource('rd-bubble-selected', { type: 'geojson', data: selectedGeo() })
m.addLayer({ id: 'rd-bubble-selected', type: 'circle', source: 'rd-bubble-selected',

View file

@ -2,7 +2,38 @@
/**
* DashboardTopBar — RDASH P4 Command Bar (topbar grid region, 52px).
* One cohesive header surface holding, on ONE row:
* title · the SHARED filter search bar · inline Breadcrumb + FilterChips · Align.
* title · inline Breadcrumb · the SHARED filter search bar · Align · Share.
*
* ── ACTIVE-FILTER CHIPS ARE NOT HERE (card BUG-DUPLICATE-FILTER-BADGES-IN-BOTH-DASHBOARD-HEADERS) ──
* DRIVER, 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 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
* over the same shared state:
* 1. THIS topbar (grid-area 'topbar', 52px, hosts the search bar) rendered <DashboardFilterChips/>,
* which emits the chips AND its own "Clear all" span.
* 2. DashboardResearchFilters (grid-area 'filters', the SECOND header) passes :active-chips into
* the shared @map/components/search/SearchFilterHeader.vue, which renders its own chip row and
* its own "Clear all" button.
* Both read the SAME bus.filterSet, so they could only ever render identical content — hence two of
* everything, every time. Grep found the literal "Clear all" only once because copy 2's text lives
* in the shared library component, not in this design folder; that is why the duplication survived
* several passes.
*
* The SECOND header is now the SINGLE SOURCE OF TRUTH for showing and clearing active filters. It
* is the right home on the merits, not just by decree: it is the band that OWNS the filter
* dropdowns, so the chips sit with the controls that created them, and it is a free-height flow row
* rather than this fixed 52px bar — the chip strip here had already forced a bounded, scrollable,
* ellipsised left group (see .rd-left below) purely to avoid a second header row.
*
* NOTHING was re-homed to make this work: the second header's clear is entirely self-contained
* (SearchFilterHeader emits 'clear-all' -> DashboardResearchFilters.onClearAll -> bus.clearFilters),
* with no dependency on this component. DashboardFilterChips.vue is left in the kit and still
* exported from ./index.js — it is a working, label-resolving chip strip another dashboard may
* mount; it simply must not be mounted ALONGSIDE a SearchFilterHeader that is already showing chips.
*
* CSV export is NOT here — it belongs to the people-groups table it exports, so it lives on
* DashboardDataTable's header bar below the map (card
@ -27,7 +58,6 @@
import { inject, computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import SearchBar from '@map/components/search/SearchBar.vue'
import DashboardFilterChips from './DashboardFilterChips.vue'
import DashboardBreadcrumb from './DashboardBreadcrumb.vue'
import ShareButton from '@map/components/map-controls/ShareButton.vue'
import { lbl } from '../../composables/useDashboardAggregations.js'
@ -131,14 +161,15 @@ useShadowStyles(`
when two rects are rounded independently, and the narrow case where the search's percentage
clamp binds rather than its 360px (that one measured -7px, not -2px). */
max-width:calc(50% - (var(--rd-search-w) / 2) - 16px); }
/* The CHIPS are the live query state, so they get first claim on the group's space. The TITLE and
BREADCRUMB yield first and truncate with an ellipsis. Measured at 1024x768 the left zone is only
~334px while the brand alone was 248px, so WITHOUT letting the brand shrink the chip strip
collapsed to an unreadable 8px sliver. A chip the user cannot read is not a fix.
min-width:0 + the nested overflow/ellipsis is what actually lets them give way: a flex item will
not shrink past its min-content width on shrink-factor alone. */
/* The BREADCRUMB gets first claim on the group's space (it is the live drill-down state); the
TITLE yields first and truncates with an ellipsis. Measured at 1024x768 the left zone is only
~334px while the brand alone was 248px, so WITHOUT letting the brand shrink its neighbour
collapsed to an unreadable sliver. min-width:0 + the nested overflow/ellipsis is what actually
lets them give way: a flex item will not shrink past its min-content width on shrink-factor
alone.
The .rd-chips sizing rule that sat here is GONE with the chip strip itself — a rule matching
nothing is not free, it reads as a live constraint to the next person and invites a phantom fix. */
.rd-topbar .rd-brand{ display:flex; align-items:baseline; gap:7px; flex:0 0 auto; }
.rd-topbar .rd-left .rd-chips{ flex:0 1 auto; min-width:0; }
.rd-topbar .rd-left .rd-breadcrumb{ flex:0 1 auto; min-width:0; }
/* AFTER the .rd-brand rule above on purpose — that rule is flex:0 0 auto (never shrink) and is
equally specific, so it would win on source order and keep the brand at its full 248px. */
@ -206,16 +237,15 @@ useShadowStyles(`
<template>
<header ref="rootEl" class="rd-topbar" data-region="topbar">
<!-- LEFT GROUP — brand + breadcrumb + chips share ONE bounded box so they collectively stop
short of the absolutely-centred search. A max-width on the chips ALONE cannot fix the
overlap: measured, the chips' natural width (297px) is already under any sane cap, while
the brand (248px) + breadcrumb (196px) push the chips' START to x=482 — past the search's
left edge at 1280 (460) and 1024 (332). A cap limits how far a box extends from where it
starts; it cannot move the start. Bounding the GROUP is what actually reserves the centre. -->
<!-- LEFT GROUP — brand + breadcrumb share ONE bounded box so they collectively stop short of
the absolutely-centred search. The bound is KEPT even though the chips (its widest and most
variable member) are gone: the search is still absolutely positioned and therefore still out
of flow, so the flex row still cannot see that the centre strip is occupied. The breadcrumb
is data-driven and grows with the drill-down path, so an unbounded left group would slide
back under the search on a long path — the same bug class, just slower to reproduce. -->
<div class="rd-left">
<span class="rd-brand"><span class="rd-ttl">{{ cfg.title || 'Research Dashboard' }}<span class="dot">.</span></span></span>
<DashboardBreadcrumb />
<DashboardFilterChips />
</div>
<div class="rd-filter-search" data-filter-search>
<SearchBar

View file

@ -1 +1 @@
pid=3840928 started=2026-09-22T12:45:39Z by=lk10x
pid=4080791 started=2026-09-22T13:25:31Z by=lk10x

File diff suppressed because one or more lines are too long

View file

@ -37,7 +37,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../../big-picture.js" integrity="sha384-T+pRaZkssXjuFk1ehjya7I73/AmZzZs41RPQ8PvujPFGJuVI53fcJu9+3Zk28n02" crossorigin="anonymous"></script>
<script src="../../big-picture.js" integrity="sha384-/lIKKEQ7JSyz7FchUyMD0/A2uYI8U2+yHd9kRjBeXxrszB+0ptTnKhYp36ARLHqE" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -37,7 +37,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../../big-picture.js" integrity="sha384-T+pRaZkssXjuFk1ehjya7I73/AmZzZs41RPQ8PvujPFGJuVI53fcJu9+3Zk28n02" crossorigin="anonymous"></script>
<script src="../../big-picture.js" integrity="sha384-/lIKKEQ7JSyz7FchUyMD0/A2uYI8U2+yHd9kRjBeXxrszB+0ptTnKhYp36ARLHqE" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -37,7 +37,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <bp-big-picture> custom element. -->
<script src="../big-picture.js" integrity="sha384-T+pRaZkssXjuFk1ehjya7I73/AmZzZs41RPQ8PvujPFGJuVI53fcJu9+3Zk28n02" crossorigin="anonymous"></script>
<script src="../big-picture.js" integrity="sha384-/lIKKEQ7JSyz7FchUyMD0/A2uYI8U2+yHd9kRjBeXxrszB+0ptTnKhYp36ARLHqE" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"big-picture","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"plugin-designs/*/*.vue","embedBase":".","bundleCount":19,"profileCount":28,"bundles":[{"bundle":"big-picture","scriptUrl":"./big-picture/big-picture.js","output":"doxa-maps/big-picture/big-picture.js","integrity":"sha384-T+pRaZkssXjuFk1ehjya7I73/AmZzZs41RPQ8PvujPFGJuVI53fcJu9+3Zk28n02","elements":["bp-big-picture","bp-research-header","people-groups-grid"],"profiles":[{"profile":"big-picture","file":"big-picture.vue","description":"The Cat-4 composite that PAIRS the two real capability elements of the","element":"bp-big-picture","type":"nested-elements-mfe","isMap":false,"iframeUrl":"./big-picture/big-picture/index.html","scriptUrl":"./big-picture/big-picture.js","embed":{"iframe":"\u003ciframe src=\"./big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./big-picture/big-picture.js\" integrity=\"sha384-T+pRaZkssXjuFk1ehjya7I73/AmZzZs41RPQ8PvujPFGJuVI53fcJu9+3Zk28n02\" crossorigin=\"anonymous\">\u003c/script>\n\u003cbp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/bp-big-picture>"},"instances":[{"id":"big-picture","title":"Big Picture — research header ↔ people-groups grid (composite)","apiBaseUrl":"","props":{"profile":"big-picture","provider":"maplibre","instanceId":"bp-big-picture"},"element":"bp-big-picture","iframeUrl":"./big-picture/big-picture/big-picture/index.html","embed":{"iframe":"\u003ciframe src=\"./big-picture/big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"frontier","title":"Big Picture — frontier accent","apiBaseUrl":"","props":{"profile":"big-picture","provider":"maplibre","instanceId":"bp-big-picture-frontier","accent":"#22d3ee","title":"Big Picture — Frontier Focus"},"element":"bp-big-picture","iframeUrl":"./big-picture/big-picture/frontier/index.html","embed":{"iframe":"\u003ciframe src=\"./big-picture/big-picture/frontier/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}],"bundle":"big-picture"}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"plugin-designs/*/*.vue","embedBase":".","bundleCount":19,"profileCount":28,"bundles":[{"bundle":"big-picture","scriptUrl":"./big-picture/big-picture.js","output":"doxa-maps/big-picture/big-picture.js","integrity":"sha384-/lIKKEQ7JSyz7FchUyMD0/A2uYI8U2+yHd9kRjBeXxrszB+0ptTnKhYp36ARLHqE","elements":["bp-big-picture","bp-research-header","people-groups-grid"],"profiles":[{"profile":"big-picture","file":"big-picture.vue","description":"The Cat-4 composite that PAIRS the two real capability elements of the","element":"bp-big-picture","type":"nested-elements-mfe","isMap":false,"iframeUrl":"./big-picture/big-picture/index.html","scriptUrl":"./big-picture/big-picture.js","embed":{"iframe":"\u003ciframe src=\"./big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./big-picture/big-picture.js\" integrity=\"sha384-/lIKKEQ7JSyz7FchUyMD0/A2uYI8U2+yHd9kRjBeXxrszB+0ptTnKhYp36ARLHqE\" crossorigin=\"anonymous\">\u003c/script>\n\u003cbp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/bp-big-picture>"},"instances":[{"id":"big-picture","title":"Big Picture — research header ↔ people-groups grid (composite)","apiBaseUrl":"","props":{"profile":"big-picture","provider":"maplibre","instanceId":"bp-big-picture"},"element":"bp-big-picture","iframeUrl":"./big-picture/big-picture/big-picture/index.html","embed":{"iframe":"\u003ciframe src=\"./big-picture/big-picture/big-picture/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"frontier","title":"Big Picture — frontier accent","apiBaseUrl":"","props":{"profile":"big-picture","provider":"maplibre","instanceId":"bp-big-picture-frontier","accent":"#22d3ee","title":"Big Picture — Frontier Focus"},"element":"bp-big-picture","iframeUrl":"./big-picture/big-picture/frontier/index.html","embed":{"iframe":"\u003ciframe src=\"./big-picture/big-picture/frontier/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}],"bundle":"big-picture"}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*

File diff suppressed because one or more lines are too long

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <dashboard-plus-grid> custom element. -->
<script src="../../dashboard-plus-grid.js" integrity="sha384-9BYJSq79mr0iTDOZUk48P35+bpXCVJke9xD5W2xYvKPnQAE/rbpNdxaZzkiZjnSj" crossorigin="anonymous"></script>
<script src="../../dashboard-plus-grid.js" integrity="sha384-CCvQQO2RbFqM6pcOHtuEFQChCDqC+Hj1ibetczRCpEJysk3iMslvBDp6Ki2xV2oI" 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 <dashboard-plus-grid> custom element. -->
<script src="../dashboard-plus-grid.js" integrity="sha384-9BYJSq79mr0iTDOZUk48P35+bpXCVJke9xD5W2xYvKPnQAE/rbpNdxaZzkiZjnSj" crossorigin="anonymous"></script>
<script src="../dashboard-plus-grid.js" integrity="sha384-CCvQQO2RbFqM6pcOHtuEFQChCDqC+Hj1ibetczRCpEJysk3iMslvBDp6Ki2xV2oI" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"dashboard-plus-grid","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"plugin-designs/*/*.vue","embedBase":".","bundleCount":19,"profileCount":28,"bundles":[{"bundle":"dashboard-plus-grid","scriptUrl":"./dashboard-plus-grid/dashboard-plus-grid.js","output":"doxa-maps/dashboard-plus-grid/dashboard-plus-grid.js","integrity":"sha384-9BYJSq79mr0iTDOZUk48P35+bpXCVJke9xD5W2xYvKPnQAE/rbpNdxaZzkiZjnSj","elements":["dashboard-plus-grid"],"profiles":[{"profile":"dashboard-plus-grid","file":"dashboard-plus-grid.vue","description":"Both children are IMPORTED DIRECTLY (compiled into this ONE bundle — no runtime bundle","element":"dashboard-plus-grid","type":"nested-elements-mfe","isMap":true,"iframeUrl":"./dashboard-plus-grid/dashboard-plus-grid/index.html","scriptUrl":"./dashboard-plus-grid/dashboard-plus-grid.js","embed":{"iframe":"\u003ciframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-9BYJSq79mr0iTDOZUk48P35+bpXCVJke9xD5W2xYvKPnQAE/rbpNdxaZzkiZjnSj\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/dashboard-plus-grid>"},"instances":[{"id":"default","title":"default","apiBaseUrl":"","props":{"profile":"dashboard-plus-grid"},"element":"dashboard-plus-grid","iframeUrl":"./dashboard-plus-grid/dashboard-plus-grid/default/index.html","embed":{"iframe":"\u003ciframe src=\"./dashboard-plus-grid/dashboard-plus-grid/default/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"plugin-designs/*/*.vue","embedBase":".","bundleCount":19,"profileCount":28,"bundles":[{"bundle":"dashboard-plus-grid","scriptUrl":"./dashboard-plus-grid/dashboard-plus-grid.js","output":"doxa-maps/dashboard-plus-grid/dashboard-plus-grid.js","integrity":"sha384-CCvQQO2RbFqM6pcOHtuEFQChCDqC+Hj1ibetczRCpEJysk3iMslvBDp6Ki2xV2oI","elements":["dashboard-plus-grid"],"profiles":[{"profile":"dashboard-plus-grid","file":"dashboard-plus-grid.vue","description":"Both children are IMPORTED DIRECTLY (compiled into this ONE bundle — no runtime bundle","element":"dashboard-plus-grid","type":"nested-elements-mfe","isMap":true,"iframeUrl":"./dashboard-plus-grid/dashboard-plus-grid/index.html","scriptUrl":"./dashboard-plus-grid/dashboard-plus-grid.js","embed":{"iframe":"\u003ciframe src=\"./dashboard-plus-grid/dashboard-plus-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-CCvQQO2RbFqM6pcOHtuEFQChCDqC+Hj1ibetczRCpEJysk3iMslvBDp6Ki2xV2oI\" crossorigin=\"anonymous\">\u003c/script>\n\u003cdashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/dashboard-plus-grid>"},"instances":[{"id":"default","title":"default","apiBaseUrl":"","props":{"profile":"dashboard-plus-grid"},"element":"dashboard-plus-grid","iframeUrl":"./dashboard-plus-grid/dashboard-plus-grid/default/index.html","embed":{"iframe":"\u003ciframe src=\"./dashboard-plus-grid/dashboard-plus-grid/default/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*

File diff suppressed because one or more lines are too long

View file

@ -35,7 +35,7 @@
"bundle": "big-picture",
"scriptUrl": "./big-picture/big-picture.js",
"output": "doxa-maps/big-picture/big-picture.js",
"integrity": "sha384-T+pRaZkssXjuFk1ehjya7I73/AmZzZs41RPQ8PvujPFGJuVI53fcJu9+3Zk28n02",
"integrity": "sha384-/lIKKEQ7JSyz7FchUyMD0/A2uYI8U2+yHd9kRjBeXxrszB+0ptTnKhYp36ARLHqE",
"elements": [
"bp-big-picture",
"bp-research-header",
@ -53,7 +53,7 @@
"scriptUrl": "./big-picture/big-picture.js",
"embed": {
"iframe": "<iframe src=\"./big-picture/big-picture/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=\"./big-picture/big-picture.js\" integrity=\"sha384-T+pRaZkssXjuFk1ehjya7I73/AmZzZs41RPQ8PvujPFGJuVI53fcJu9+3Zk28n02\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./big-picture/big-picture.js\" integrity=\"sha384-/lIKKEQ7JSyz7FchUyMD0/A2uYI8U2+yHd9kRjBeXxrszB+0ptTnKhYp36ARLHqE\" crossorigin=\"anonymous\"></script>\n<bp-big-picture profile-config='{\"profile\":\"big-picture\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></bp-big-picture>"
},
"instances": [
{
@ -189,7 +189,7 @@
"bundle": "dashboard-plus-grid",
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"output": "doxa-maps/dashboard-plus-grid/dashboard-plus-grid.js",
"integrity": "sha384-9BYJSq79mr0iTDOZUk48P35+bpXCVJke9xD5W2xYvKPnQAE/rbpNdxaZzkiZjnSj",
"integrity": "sha384-CCvQQO2RbFqM6pcOHtuEFQChCDqC+Hj1ibetczRCpEJysk3iMslvBDp6Ki2xV2oI",
"elements": [
"dashboard-plus-grid"
],
@ -205,7 +205,7 @@
"scriptUrl": "./dashboard-plus-grid/dashboard-plus-grid.js",
"embed": {
"iframe": "<iframe src=\"./dashboard-plus-grid/dashboard-plus-grid/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=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-9BYJSq79mr0iTDOZUk48P35+bpXCVJke9xD5W2xYvKPnQAE/rbpNdxaZzkiZjnSj\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./dashboard-plus-grid/dashboard-plus-grid.js\" integrity=\"sha384-CCvQQO2RbFqM6pcOHtuEFQChCDqC+Hj1ibetczRCpEJysk3iMslvBDp6Ki2xV2oI\" crossorigin=\"anonymous\"></script>\n<dashboard-plus-grid profile-config='{\"profile\":\"dashboard-plus-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></dashboard-plus-grid>"
},
"instances": [
{
@ -660,7 +660,7 @@
"bundle": "people-groups-grid",
"scriptUrl": "./people-groups-grid/people-groups-grid.js",
"output": "doxa-maps/people-groups-grid/people-groups-grid.js",
"integrity": "sha384-iZt0B4Frfb7tLlAdWioHoQ6m0sXu+SbVqXC1tcbJifuCQmR8zwMQ6STlFLD6MHcU",
"integrity": "sha384-GL/OqFHBSpmgMGehNKdgFO1uOIdNb2hX4ycIxQDUwxa9mWLOfrs6TmPkA0zDojkO",
"elements": [
"people-groups-grid"
],
@ -676,7 +676,7 @@
"scriptUrl": "./people-groups-grid/people-groups-grid.js",
"embed": {
"iframe": "<iframe src=\"./people-groups-grid/people-groups-grid/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=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-iZt0B4Frfb7tLlAdWioHoQ6m0sXu+SbVqXC1tcbJifuCQmR8zwMQ6STlFLD6MHcU\" crossorigin=\"anonymous\"></script>\n<people-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></people-groups-grid>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-GL/OqFHBSpmgMGehNKdgFO1uOIdNb2hX4ycIxQDUwxa9mWLOfrs6TmPkA0zDojkO\" crossorigin=\"anonymous\"></script>\n<people-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></people-groups-grid>"
},
"instances": [
{
@ -764,7 +764,7 @@
"bundle": "research-dashboard",
"scriptUrl": "./research-dashboard/research-dashboard.js",
"output": "doxa-maps/research-dashboard/research-dashboard.js",
"integrity": "sha384-bkHqLMT1g4ABMX7BhgqM2DEw4luXhZpmCQ/Ya5wDnrzT0bTMJErJ7g5cDITPvCVb",
"integrity": "sha384-SO6+6xne0Wm3yJh5mV6V9Ct3SH2BxZFgkPWNIRmUAx2+73i4QzTrg1bGZ6WBTtms",
"elements": [
"research-dashboard-map"
],
@ -780,7 +780,7 @@
"scriptUrl": "./research-dashboard/research-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./research-dashboard/research-dashboard/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=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-bkHqLMT1g4ABMX7BhgqM2DEw4luXhZpmCQ/Ya5wDnrzT0bTMJErJ7g5cDITPvCVb\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-SO6+6xne0Wm3yJh5mV6V9Ct3SH2BxZFgkPWNIRmUAx2+73i4QzTrg1bGZ6WBTtms\" crossorigin=\"anonymous\"></script>\n<research-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></research-dashboard-map>"
},
"bundle": "research-dashboard"
}

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"people-groups-grid","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"plugin-designs/*/*.vue","embedBase":".","bundleCount":19,"profileCount":28,"bundles":[{"bundle":"people-groups-grid","scriptUrl":"./people-groups-grid/people-groups-grid.js","output":"doxa-maps/people-groups-grid/people-groups-grid.js","integrity":"sha384-iZt0B4Frfb7tLlAdWioHoQ6m0sXu+SbVqXC1tcbJifuCQmR8zwMQ6STlFLD6MHcU","elements":["people-groups-grid"],"profiles":[{"profile":"people-groups-grid","file":"people-groups-grid.vue","description":"The people-groups grid as an owned custom element: card list, search and four","element":"people-groups-grid","type":"parameterized","isMap":false,"iframeUrl":"./people-groups-grid/people-groups-grid/index.html","scriptUrl":"./people-groups-grid/people-groups-grid.js","embed":{"iframe":"\u003ciframe src=\"./people-groups-grid/people-groups-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-iZt0B4Frfb7tLlAdWioHoQ6m0sXu+SbVqXC1tcbJifuCQmR8zwMQ6STlFLD6MHcU\" crossorigin=\"anonymous\">\u003c/script>\n\u003cpeople-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/people-groups-grid>"},"instances":[{"id":"people-groups-grid","title":"People Groups Grid — standalone (own search + 4 dropdowns)","apiBaseUrl":"","props":{"profile":"people-groups-grid","provider":"maplibre","instanceId":"people-groups-grid","pray-base-url":"https://pray.doxa.life"},"element":"people-groups-grid","iframeUrl":"./people-groups-grid/people-groups-grid/people-groups-grid/index.html","embed":{"iframe":"\u003ciframe src=\"./people-groups-grid/people-groups-grid/people-groups-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"driven","title":"People Groups Grid — externally driven (controls off, props/events)","apiBaseUrl":"","props":{"profile":"people-groups-grid","provider":"maplibre","instanceId":"people-groups-grid-driven","pray-base-url":"https://pray.doxa.life","show-search":"false","show-filters":"false"},"element":"people-groups-grid","iframeUrl":"./people-groups-grid/people-groups-grid/driven/index.html","embed":{"iframe":"\u003ciframe src=\"./people-groups-grid/people-groups-grid/driven/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"plugin-designs/*/*.vue","embedBase":".","bundleCount":19,"profileCount":28,"bundles":[{"bundle":"people-groups-grid","scriptUrl":"./people-groups-grid/people-groups-grid.js","output":"doxa-maps/people-groups-grid/people-groups-grid.js","integrity":"sha384-GL/OqFHBSpmgMGehNKdgFO1uOIdNb2hX4ycIxQDUwxa9mWLOfrs6TmPkA0zDojkO","elements":["people-groups-grid"],"profiles":[{"profile":"people-groups-grid","file":"people-groups-grid.vue","description":"The people-groups grid as an owned custom element: card list, search and four","element":"people-groups-grid","type":"parameterized","isMap":false,"iframeUrl":"./people-groups-grid/people-groups-grid/index.html","scriptUrl":"./people-groups-grid/people-groups-grid.js","embed":{"iframe":"\u003ciframe src=\"./people-groups-grid/people-groups-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./people-groups-grid/people-groups-grid.js\" integrity=\"sha384-GL/OqFHBSpmgMGehNKdgFO1uOIdNb2hX4ycIxQDUwxa9mWLOfrs6TmPkA0zDojkO\" crossorigin=\"anonymous\">\u003c/script>\n\u003cpeople-groups-grid profile-config='{\"profile\":\"people-groups-grid\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/people-groups-grid>"},"instances":[{"id":"people-groups-grid","title":"People Groups Grid — standalone (own search + 4 dropdowns)","apiBaseUrl":"","props":{"profile":"people-groups-grid","provider":"maplibre","instanceId":"people-groups-grid","pray-base-url":"https://pray.doxa.life"},"element":"people-groups-grid","iframeUrl":"./people-groups-grid/people-groups-grid/people-groups-grid/index.html","embed":{"iframe":"\u003ciframe src=\"./people-groups-grid/people-groups-grid/people-groups-grid/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}},{"id":"driven","title":"People Groups Grid — externally driven (controls off, props/events)","apiBaseUrl":"","props":{"profile":"people-groups-grid","provider":"maplibre","instanceId":"people-groups-grid-driven","pray-base-url":"https://pray.doxa.life","show-search":"false","show-filters":"false"},"element":"people-groups-grid","iframeUrl":"./people-groups-grid/people-groups-grid/driven/index.html","embed":{"iframe":"\u003ciframe src=\"./people-groups-grid/people-groups-grid/driven/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>"}}]}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <people-groups-grid> custom element. -->
<script src="../../people-groups-grid.js" integrity="sha384-iZt0B4Frfb7tLlAdWioHoQ6m0sXu+SbVqXC1tcbJifuCQmR8zwMQ6STlFLD6MHcU" crossorigin="anonymous"></script>
<script src="../../people-groups-grid.js" integrity="sha384-GL/OqFHBSpmgMGehNKdgFO1uOIdNb2hX4ycIxQDUwxa9mWLOfrs6TmPkA0zDojkO" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <people-groups-grid> custom element. -->
<script src="../people-groups-grid.js" integrity="sha384-iZt0B4Frfb7tLlAdWioHoQ6m0sXu+SbVqXC1tcbJifuCQmR8zwMQ6STlFLD6MHcU" crossorigin="anonymous"></script>
<script src="../people-groups-grid.js" integrity="sha384-GL/OqFHBSpmgMGehNKdgFO1uOIdNb2hX4ycIxQDUwxa9mWLOfrs6TmPkA0zDojkO" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -26,7 +26,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <people-groups-grid> custom element. -->
<script src="../../people-groups-grid.js" integrity="sha384-iZt0B4Frfb7tLlAdWioHoQ6m0sXu+SbVqXC1tcbJifuCQmR8zwMQ6STlFLD6MHcU" crossorigin="anonymous"></script>
<script src="../../people-groups-grid.js" integrity="sha384-GL/OqFHBSpmgMGehNKdgFO1uOIdNb2hX4ycIxQDUwxa9mWLOfrs6TmPkA0zDojkO" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -130,7 +130,7 @@ header.top .sub { margin-top: 3px; font: 12px/1.4 var(--sans); color: var(--mut)
<script>window.FORGE = {"mode":"built","scope":"bundle","manifestUrl":"../manifest.json","bundle":"research-dashboard","hrefBase":"../"};
/* file:// contract: manifest inlined at build — no fetch needed to render. */
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"plugin-designs/*/*.vue","embedBase":".","bundleCount":19,"profileCount":28,"bundles":[{"bundle":"research-dashboard","scriptUrl":"./research-dashboard/research-dashboard.js","output":"doxa-maps/research-dashboard/research-dashboard.js","integrity":"sha384-bkHqLMT1g4ABMX7BhgqM2DEw4luXhZpmCQ/Ya5wDnrzT0bTMJErJ7g5cDITPvCVb","elements":["research-dashboard-map"],"profiles":[{"profile":"research-dashboard","file":"research-dashboard.vue","description":"Coordinated-Multiple-Views research dashboard for 10/40-window UPGs (live pray.doxa.life): docked facet rail + map-as-hero + linked table/charts, brushing-and-linking over one in-memory store.","element":"research-dashboard-map","type":"monolithic","isMap":true,"iframeUrl":"./research-dashboard/research-dashboard/index.html","scriptUrl":"./research-dashboard/research-dashboard.js","embed":{"iframe":"\u003ciframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-bkHqLMT1g4ABMX7BhgqM2DEw4luXhZpmCQ/Ya5wDnrzT0bTMJErJ7g5cDITPvCVb\" crossorigin=\"anonymous\">\u003c/script>\n\u003cresearch-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/research-dashboard-map>"},"bundle":"research-dashboard"}]}]};</script>
window.FORGE.manifest = {"schema":"doxa-maps-manifest@1","generatedFrom":"plugin-designs/*/*.vue","embedBase":".","bundleCount":19,"profileCount":28,"bundles":[{"bundle":"research-dashboard","scriptUrl":"./research-dashboard/research-dashboard.js","output":"doxa-maps/research-dashboard/research-dashboard.js","integrity":"sha384-SO6+6xne0Wm3yJh5mV6V9Ct3SH2BxZFgkPWNIRmUAx2+73i4QzTrg1bGZ6WBTtms","elements":["research-dashboard-map"],"profiles":[{"profile":"research-dashboard","file":"research-dashboard.vue","description":"Coordinated-Multiple-Views research dashboard for 10/40-window UPGs (live pray.doxa.life): docked facet rail + map-as-hero + linked table/charts, brushing-and-linking over one in-memory store.","element":"research-dashboard-map","type":"monolithic","isMap":true,"iframeUrl":"./research-dashboard/research-dashboard/index.html","scriptUrl":"./research-dashboard/research-dashboard.js","embed":{"iframe":"\u003ciframe src=\"./research-dashboard/research-dashboard/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen>\u003c/iframe>","script":"\u003cscript>window.MAP_APP_API_URL=\"https://pray.doxa.life\";\u003c/script>\n\u003cscript src=\"./research-dashboard/research-dashboard.js\" integrity=\"sha384-SO6+6xne0Wm3yJh5mV6V9Ct3SH2BxZFgkPWNIRmUAx2+73i4QzTrg1bGZ6WBTtms\" crossorigin=\"anonymous\">\u003c/script>\n\u003cresearch-dashboard-map profile-config='{\"profile\":\"research-dashboard\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'>\u003c/research-dashboard-map>"},"bundle":"research-dashboard"}]}]};</script>
<script>/**
* staging-forge.js — the ONE-PAGE semantic staging environment (root + bundle pages).
*

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 <research-dashboard-map> custom element. -->
<script src="../research-dashboard.js" integrity="sha384-bkHqLMT1g4ABMX7BhgqM2DEw4luXhZpmCQ/Ya5wDnrzT0bTMJErJ7g5cDITPvCVb" crossorigin="anonymous"></script>
<script src="../research-dashboard.js" integrity="sha384-SO6+6xne0Wm3yJh5mV6V9Ct3SH2BxZFgkPWNIRmUAx2+73i4QzTrg1bGZ6WBTtms" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long