176 lines
9.7 KiB
Vue
176 lines
9.7 KiB
Vue
<script setup>
|
|
/**
|
|
* big-picture.vue — Plugin: "Big Picture" (Capability-4 nested-elements-mfe).
|
|
*
|
|
* @description The Cat-4 composite that PAIRS the two real capability elements of the
|
|
* research page via **props + events** (the deploy-method-3 contract — no shared
|
|
* JS bus, no Pinia store; the parent is the only wire):
|
|
*
|
|
* TOP = <BpResearchHeader> (Cat-1) — the Unified Research Dashboard header.
|
|
* Its hand-rolled search/select controls are GONE; it now uses the
|
|
* SHARED <SearchBar> + FOUR shared <FilterDropdown>s (Region / Block /
|
|
* People-Group / Religion). Emits `research-filter`
|
|
* { query, wagfRegion, wagfBlock, rop1, religion } on change.
|
|
* BELOW= the REAL cap-2 <PeopleGroupsGrid> (plugin-designs/people-groups-grid),
|
|
* the live pray.doxa.life people-groups grid, with its OWN controls
|
|
* TURNED OFF (:show-search / :show-filters = false) so the header
|
|
* drives it purely through props.
|
|
*
|
|
* WIRING (the load-bearing proof):
|
|
* 1. The header emits `research-filter` (@research-filter) → this parent stores
|
|
* { query, wagfRegion, wagfBlock, rop1, religion } in reactive state.
|
|
* 2. This parent passes that state DOWN to the grid as
|
|
* :query / :wagf-region / :wagf-block / :rop1 / :religion props → the real
|
|
* grid re-filters against its live data. (Header control → grid filter.)
|
|
* 3. The grid emits `groups-filtered` { total, … } (@groups-filtered) → this
|
|
* parent stores the total and passes it back UP into the header's :count prop.
|
|
* => Searching / picking a dropdown in the dashboard header filters the REAL
|
|
* people-groups grid below it, and the header shows the live matched count.
|
|
* Data down, events up.
|
|
*
|
|
* Both inner elements stay INDIVIDUALLY embeddable: <bp-research-header> and the
|
|
* cap-2 <people-groups-grid> are also registered as standalone custom elements
|
|
* (see index.js) — one code path, two mount modes.
|
|
*
|
|
* @profile big-picture
|
|
* @bundle big-picture
|
|
* @element bp-big-picture
|
|
*
|
|
* ★ @element MUST BE A BARE TAG — ONE VALID CSS SELECTOR, NOTHING ELSE.
|
|
* It was previously prose: "bp-big-picture (this composite) + bp-research-header +
|
|
* people-groups-grid (nested)". generate-tree copies @element VERBATIM into manifest.json, and
|
|
* consumers feed that value straight to document.querySelector(). The prose threw
|
|
* "SyntaxError: Failed to execute 'querySelector'", which embed-smoke's catch-all reported as
|
|
* "mount timeout" — so a DATA defect in this comment masqueraded as a BROKEN PLUGIN, on the one
|
|
* bundle that is tab-1 of the cap-5 SPA. big-picture mounts fine; only this string was invalid.
|
|
* Every other bundle in the repo declares a single bare tag; this was the lone outlier.
|
|
* The nested elements are documented above and in @composes — they do not belong in @element.
|
|
*
|
|
* Becomes tab-1 of the Cat-5 SPA.
|
|
*/
|
|
import { computed, reactive, ref, watchEffect } from 'vue'
|
|
import { SUPPORTED_LOCALES, setRuntimeLocale } from '@map/i18n/index.js'
|
|
import BpResearchHeader from './src/components/BpResearchHeader.vue'
|
|
// The REAL cap-2 grid — same component the standalone <people-groups-grid> ships (no fork).
|
|
import PeopleGroupsGrid from '../people-groups-grid/people-groups-grid.vue'
|
|
|
|
// ── profile-config prop (optional overrides) ─────────────────────────────────
|
|
const props = defineProps({
|
|
profileConfig: { type: String, default: '' },
|
|
})
|
|
const config = computed(() => {
|
|
if (!props.profileConfig) return {}
|
|
try { return JSON.parse(props.profileConfig) || {} } catch { return {} }
|
|
})
|
|
const accent = computed(() => config.value.accent || '#8b5cf6')
|
|
const heading = computed(() => config.value.title || 'Big Picture — Unified Research Dashboard')
|
|
// ★ THE LOCALE THE HOST ASKED FOR, AND THE ONE ELEMENT ON THE PAGE THAT CAN ACT ON IT.
|
|
// MEASURED with check:host-locale-contract: this leaf drew 0 Arabic characters for
|
|
// profile-config {"locale":"ar"} against 273 for the same page reached with ?lang=ar. Its siblings
|
|
// were fixed two different ways and neither applies here — this bundle's entry calls
|
|
// defineCustomElement(BigPicture) with no configureApp at all, so there is no i18n instance for
|
|
// provide-on-install to reach and no ProfileLoader anywhere beneath to drive one. Every string this
|
|
// element renders comes through tStatic (the grid's `t` proxy, the header's labels), and tStatic
|
|
// resolves the locale from the URL and the document — neither of which a host's profile-config
|
|
// touches. So the only thing missing was somebody to say which language this page is being read in.
|
|
const hostLang = computed(() => {
|
|
const l = String(config.value.locale || config.value.lang || '').split('-')[0].toLowerCase()
|
|
return SUPPORTED_LOCALES.includes(l) ? l : ''
|
|
})
|
|
watchEffect(() => { if (hostLang.value) setRuntimeLocale(hostLang.value) })
|
|
|
|
// The host's people-group LIST (library/lists/), handed to both children as the only key of their
|
|
// profile-config, so nothing else about them changes. Absent = the default list.
|
|
const listConfig = computed(() => (config.value.list ? JSON.stringify({ list: config.value.list }) : ''))
|
|
|
|
const prayBaseUrl = computed(() => config.value.prayBaseUrl || '')
|
|
const languageCode = computed(() => config.value.languageCode || '')
|
|
|
|
// ── Cross-element wiring state (props/events, no shared bus) ──────────────────
|
|
// filter: set by the HEADER via its `research-filter` event, read by the GRID as props.
|
|
// The FOUR real grid axes (Country excluded per the agent).
|
|
const filter = reactive({ query: '', wagfRegion: '', wagfBlock: '', rop1: '', religion: '' })
|
|
// matched: set by the GRID via its `groups-filtered` event, read by the HEADER as :count.
|
|
const matched = ref(null)
|
|
|
|
function onResearchFilter(payload) {
|
|
const d = payload && typeof payload === 'object' ? payload : {}
|
|
filter.query = d.query || ''
|
|
filter.wagfRegion = d.wagfRegion || ''
|
|
filter.wagfBlock = d.wagfBlock || ''
|
|
filter.rop1 = d.rop1 || ''
|
|
filter.religion = d.religion || ''
|
|
}
|
|
function onGroupsFiltered(payload) {
|
|
const d = payload && typeof payload === 'object' ? payload : {}
|
|
matched.value = typeof d.total === 'number' ? d.total : null
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="bp-root" :style="{ '--accent': accent }">
|
|
<!-- TOP: the Cat-1 dashboard header (SHARED SearchBar + 4 FilterDropdowns). @research-filter
|
|
is the events half of the contract — the header tells this parent what to filter by. -->
|
|
<BpResearchHeader
|
|
:title="heading"
|
|
:accent="accent"
|
|
:count="matched"
|
|
:pray-base-url="prayBaseUrl"
|
|
:language-code="languageCode"
|
|
:profile-config="listConfig"
|
|
@research-filter="onResearchFilter"
|
|
/>
|
|
|
|
<!-- BELOW: the REAL cap-2 people-groups grid, its OWN controls TURNED OFF so it is driven
|
|
entirely by the props this parent pushes down. @groups-filtered flows the live total
|
|
back up into the header's count. -->
|
|
<!-- show-total=false: the header above already shows "<N> shown" (bound to this same
|
|
@groups-filtered total, see onGroupsFiltered), 139px above the grid's own "Total:"
|
|
line. Measured live 2026-09-23: both rendered "2107 shown" / "Total: 2,107" — the exact
|
|
same number twice, above the fold, which is the redundant-header shape the agent's
|
|
standing rule (hint lives in the EXISTING header, never a second one) forbids. The grid
|
|
keeps its own line when embedded ALONE, where it is the only total — same pattern
|
|
already applied in dashboard-plus-grid.vue. -->
|
|
<div class="bp-body">
|
|
<PeopleGroupsGrid
|
|
:query="filter.query"
|
|
:wagf-region="filter.wagfRegion"
|
|
:wagf-block="filter.wagfBlock"
|
|
:rop1="filter.rop1"
|
|
:religion="filter.religion"
|
|
:show-search="false"
|
|
:show-filters="false"
|
|
:show-total="false"
|
|
:pray-base-url="prayBaseUrl"
|
|
:language-code="languageCode"
|
|
:profile-config="listConfig"
|
|
@groups-filtered="onGroupsFiltered"
|
|
/>
|
|
</div>
|
|
|
|
<!-- User-facing footer: an unreached-people-groups research dashboard. The Cap-4
|
|
architecture note (header↔grid via props/events, no shared bus, tab-1 of the SPA)
|
|
lives in this file's header block, not in the UI where an end user reads it. -->
|
|
<footer class="bp-foot">
|
|
Search and filter the world's people groups · least-reached shown first
|
|
</footer>
|
|
</div>
|
|
</template>
|
|
|
|
<style>
|
|
/* Self-size regardless of how a parent mounts us: a host that isn't a sized block makes
|
|
.bp-root's height:100% resolve against nothing → 0-height collapse (the black-screen class).
|
|
Declaring :host here means this element fills its slot even if the mounter forces no display
|
|
(or the wrong one). See BUG-EMBEDDED-PLUGIN-STUDIO-BLACK-SCREEN-DISPLAY-BLOCK. */
|
|
:host { display: block; width: 100%; height: 100%; }
|
|
.bp-root {
|
|
--bg: #f3f3f1; --line: #d1d4cd; --ink: #3b463d; --mut: #6d766e;
|
|
display: flex; flex-direction: column;
|
|
width: 100%; height: 100%; min-height: 480px; overflow: hidden;
|
|
background: var(--bg); color: var(--ink); box-sizing: border-box;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
}
|
|
.bp-body { flex: 1; min-height: 0; position: relative; overflow: auto; }
|
|
.bp-foot { flex: none; padding: 8px 12px; font-size: 11px; color: var(--mut); text-align: center;
|
|
border-top: 1px solid var(--line); }
|
|
</style>
|