doxa-maps/plugin-designs/research-map-plus-grid/research-map-plus-grid.vue
doxa-maps b57aef457c
Some checks are pending
build-gate / build (push) Waiting to run
Add the mobile capability-4 composite: research map above the people-groups grid
The desktop composite pairs the research dashboard with the grid. This is its
mobile sibling: the monolithic 7-tab research map on top, the same grid beneath,
stacked to scroll. Which one a shell shows is an aspect-ratio choice made at the
SPA level, so neither plugin knows about the other and both stay embeddable.

The grid's own search and filters are ON here. The desktop composite turns them
off because the dashboard header owns those controls and drives the grid through
props; the research map has no such header, so switching them off would leave the
grid with no way to be searched at all.

Composes its children by tag and bundle URL rather than importing their .vue
files. Importing the research map directly would bypass its bundle entry, which
is where its three bundle-private colour strategies, its profile registry, its
Pinia and i18n pair and the RTL text plugin are installed — and losing those is a
runtime defect in a green build, because a thrown setup() removes the subtree
with an empty error.

Add check:composite-srcs, which resolves every nested src against the built tree
and asserts every nested tag is registered. A wrong relative depth is a 404, and
the stack layout swallows a failed section on purpose so one broken child cannot
take the whole stack down — which means the cost of that resilience is a silently
empty band. Negative-tested both ways: breaking the depth and renaming a tag each
fail with the file and path named; restoring returns to pass.
2026-09-22 07:34:02 +00:00

112 lines
7.3 KiB
Vue

<script setup>
/**
* research-map-plus-grid.vue — Capability-4 composite: doxa-research-map + people-groups-grid
* stacked to scroll. THE MOBILE SIBLING of dashboard-plus-grid.
*
* @element research-map-plus-grid
* @description The monolithic 7-tab research map (cap-3) on top, the People Groups grid beneath,
* stacked via the shared StackLayout so the host page scrolls through both. Each child is
* composed by TAG + BUNDLE URL (the stack-demo `element`/`src` contract), so each one boots
* through its OWN bundle entry and stays individually embeddable.
*
* ── WHY THIS EXISTS AS A SEPARATE PLUGIN (Driver 2026-09-22) ─────────────────────────────────
* The first idea was ONE cap-4 plugin that SWAPS its top element by aspect ratio — research
* dashboard on desktop, research map on mobile. The Driver reversed that mid-message, verbatim:
* "We made the wrong choice of saying that we should swap elements based on mobile and desktop...
* That would be really complex. It'd be easier just to have two separate capability-4 plugins."
*
* So there are two plain composites and NEITHER knows about the other:
* - dashboard-plus-grid = DESKTOP — research-dashboard + grid
* - research-map-plus-grid = MOBILE — research map + grid (this file)
* Which one appears is chosen by aspect ratio AT THE SPA LEVEL, never inside a plugin.
*
* ★ WHY THE SPLIT IS ALSO THE BETTER ENGINEERING, not just the simpler one. A self-swapping
* element would be un-embeddable in any fixed context: deploy methods 1 (iframe) and 3
* (direct-JS) both place an element at a size the HOST controls, so an element that silently
* changes what it IS based on its own width gives an embedder no way to ask for the one they
* want. Two elements keep both independently embeddable.
*
* ★★ WHY THIS COMPOSES BY src AND DOES **NOT** BAKE ITS CHILDREN — the non-obvious part.
* dashboard-plus-grid IMPORTS its two children's .vue files directly and compiles them in. That
* works there because research-dashboard owns NO MAP (it is SVG charts). Doing the same with the
* research map would SILENTLY SHIP A BROKEN ELEMENT, because importing a child .vue BYPASSES its
* index.js — and doxa-research-map/index.js is where four load-bearing things happen:
* 1. registerStrategies(import.meta.glob('./src/colors/*.js')) — its THREE bundle-private colour
* strategies (affinity-block, doxa-region, resource). Without them the map's colour modes are
* missing.
* 2. app.provide('profileModules', import.meta.glob('./*.vue')) — the map is mounted through
* ProfileLoader, NOT as a bare component; with no registry it cannot resolve its own profile.
* 3. createPinia() + createAppI18n() + provide('appI18n') — the same appContext pair whose
* absence caused BUG-VUE-I18N-CODE27 (a thrown setup() removes the subtree with an EMPTY
* SyntaxError, which cost the desktop sibling its Share / Zoom controls before it was found).
* 4. mapboxgl.setRTLTextPlugin, installed once per page.
* The failure mode is the dangerous one: the build stays GREEN and the defect is a blank or
* half-coloured band at runtime. So: compose by tag+src, let the real bundle entry run.
* This also matches the standing rule for nesting maps (avoid the Mapbox peer + the one-shot
* customElements.define collision). Measured 2026-09-22: NO composite in this repo bakes a map
* component directly — every map is composed via its own bundle.
*
* ── THE ONE REAL DIFFERENCE FROM THE DESKTOP SIBLING ─────────────────────────────────────────
* The grid's OWN search bar and filters are ENABLED here. In dashboard-plus-grid they are turned
* OFF (show-search=false, show-filters=false) because the research dashboard's header owns those
* controls and drives the grid through props — two search bars would be a duplicate. The research
* map has NO such header, so switching them off here would leave the grid with no way to be
* searched or filtered at all. Driver, verbatim: the grid's search bar + filters must be toggled
* ON via prop. This is why the swap-inside-one-element idea was genuinely complex: the two top
* elements do not merely look different, they imply OPPOSITE control ownership below.
*/
import StackLayout from '@map/layouts/StackLayout.vue'
// One-screen height for the fixed-viewport research-map band. Same reasoning as
// dashboard-plus-grid's DASHBOARD_H: the research map is a FIXED-VIEWPORT app whose shell is a CSS
// grid with height:100% and a 1fr map hero plus inner-scroll regions. Rendered in a scroll-page
// section with NO definite height, height:100% collapses to its floor and its inner-scroll regions
// swallow the wheel, so the OUTER page never grows past it and the grid below is unreachable.
// Pinning min === max gives it a definite height to fill while the page keeps growing below.
// svh (small viewport height) avoids the mobile URL-bar jump — which matters MORE here than on the
// desktop sibling, because this composite is the mobile one.
const MAP_H = '88svh'
// src is resolved by the browser relative to THIS leaf page, which is served two levels deep
// (doxa-maps/research-map-plus-grid/research-map-plus-grid/index.html) — so go UP TWO to reach a
// sibling bundle's tree (doxa-maps/<bundle>/<bundle>.js). An off-by-one here is a 404 and a
// SILENTLY EMPTY section: StackLayout's .catch() deliberately leaves a failed section's host empty
// so the rest of the stack still renders, which means a wrong path costs you a band with NO error.
// `bun run check:composite-srcs` resolves every composite src against the built tree to catch it.
const sections = [
// No section title: the research map self-titles through its own chrome, so a StackLayout title
// here would render the name twice — the duplicate-header bug the Driver rejected on the desktop
// sibling (BUG-RESEARCH-DASHBOARD-HEADER-STACK-WRONG). One header per element.
{
key: 'map',
element: 'doxa-research-map',
src: '../../doxa-research-map/doxa-research-map.js',
minHeight: MAP_H,
maxHeight: MAP_H
},
// No title/sub on the grid either (BUILD-BIGPIC-DROP-REDUNDANT-SECTION-HEADERS): StackLayout only
// renders .stl-sec-head when `title` is set, so omitting it removes the strip entirely.
//
// attrs = "props in advance" — set on the element BEFORE it mounts, per the StackLayout contract.
// These three are written out EXPLICITLY even though true is the component's own default, so the
// contrast with the desktop sibling is readable at the call site and a future default change
// cannot silently strip the mobile controls. show-total stays on because, unlike the desktop
// composite, no table above reports the count — this is the only total on the page.
{
key: 'grid',
element: 'people-groups-grid',
src: '../../people-groups-grid/people-groups-grid.js',
attrs: { 'show-search': 'true', 'show-filters': 'true', 'show-total': 'true' }
}
]
</script>
<template>
<!-- No `title` and no #meta: StackLayout omits its own top bar entirely when neither is used, so
the composite shows ONLY the nested elements' own chrome. -->
<StackLayout :sections="sections" />
</template>
<style>
:host { display:block; width:100%; }
</style>