doxa-maps/plugin-designs/research-dashboard/index.js
Doxa Maps 6263c250ec C1a: the affinity-block and doxa-region palettes move to library/colors/
First palette migration from SPEC-LIBRARY-CURATION.md. Source only; studio rebuilds
(rule 5B — library/ plus four other owners' plugins).

WHY THIS ONE FIRST: the duplication was not idle. doxa-research-map's
affinity-block.js carried A018 'Tibetan Peoples' and A019 'Himalayan Peoples' —
live on the pray-tools API — with colours measured to WCAG 1.4.11's 3.0:1 non-text
floor (A013 5.28 unchanged, A018 7.69, A019 3.53; sibling separations 2.61/1.46/1.79).
country-posters' copy covered only A001–A017, so 134 rows (6.4% of 2,107) fell to the
grey fallback #95a5a6 — the "bunch of grey colours" report. exp-shepherd confirmed
that code path headlessly. So the correct copy wins with no judgement call, and the
move FIXES country-posters rather than changing it.

FOUR CONSUMERS, NOT THE TWO THE SPEC COUNTED, and finding that is the sharpest
argument for the migration: research-dashboard/index.js and dashboard-plus-grid/index.js
each imported `../doxa-research-map/src/colors/affinity-block.js` — a cross-plugin
RELATIVE import into another plugin's PRIVATE folder. Both now import @map/colors/.
Only the build found this; no gate names cross-plugin reach.

THE GLOB STAYS in both plugins' index.js, deliberately, and is documented there:
resource.js is still plugin-local pending C1b, so deleting the glob would unregister
it and colour those pins by the soft fallback — the same silent wrong-colour class
being fixed here. Deleting the two STALE FILES is what stops the stale copy winning.

THE REJECTED-CANDIDATE NOTE MOVED ABOVE `export const PALETTE`, and this is a real
constraint rather than tidying: check:one-palette slices from that export to END OF
FILE and matches every hex, despite its own comment claiming it reads "only the
PALETTE export". #AF7AC5 was rejected for A019 at 2.95 — under the accessibility
floor — and mentioned below the export it would have been DEMANDED of every consumer
bundle. The note keeps the reasoning without creating the obligation; the gate bug
itself is reported separately since internal/ is not mine to edit.

The legend needed no change and that is by design: useAffinityBlockLegendData resolves
via getColorStrategy(COLOR_MODES.AFFINITY_BLOCK) — the same strategy the pins use — and
reads labels from the data, so map and legend gain A018/A019 together. Its comments
claiming the strategy lives in a plugin folder were corrected; they now describe where
it actually is.

GATES: check:one-palette PASS — 8 strategies, 16 bundle/palette pairs (baselined at 6
and 12 before the move, so a red would have been mine). check:library-index PASS after
regenerating the derived menu. check:no-free-identifiers PASS. All four consumer
bundles build; their artifacts reverted for studio.
2026-09-26 08:39:53 +00:00

100 lines
5.5 KiB
JavaScript

/**
* research-dashboard / index.js — Bundle entry (DOXA-LIB-2)
*
* Vite multi-entry build target. Emits app/research-dashboard.js.
*
* Mirrors plugin-designs/doxa-research-map/index.js (same ProfileLoader plumbing).
* Registers a SINGLE tag (UNCHANGED from the original profile — only the bundle folder
* is now plural so it can host N dashboard profiles tomorrow):
* - <research-dashboard-map> config-driven people-group research dashboard
*
* The dashboard mounts a Mapbox map in its center cell (Mapbox GL is loaded globally by
* index.html and the token arrives via profile-config.tk), so the RTL-text plugin is
* loaded once here, exactly like the map bundles.
*
* Profile registry — same Q3 contract as the map bundles: ProfileLoader lives in @map and
* cannot import.meta.glob across the package boundary, so the bundle evaluates
* import.meta.glob('./*.vue') HERE and hands the result to the loader via
* app.provide('profileModules'). The library panels live in @map/components/dashboards and
* are imported by the profile .vue — the glob only registers the profile screen(s).
*/
import { defineCustomElement } from 'vue'
import { createPinia } from 'pinia'
import ProfileLoader from '@map/ProfileLoader.vue'
import { createAppI18n } from '@map/i18n/index.js'
import { registerStrategies } from '@map/colors/_registry.js'
import { installRtlTextPlugin } from '@map/maps/rtl-text.js'
// ★ WAS A CROSS-PLUGIN RELATIVE IMPORT into another plugin's PRIVATE folder
// ('../doxa-research-map/src/colors/...'). Both modules now live in library/colors/ (C1), so this
// bundle no longer depends on doxa-research-map's internal layout -- which is the coupling the
// library exists to remove, and the reason this migration had four consumers rather than the two
// the spec counted.
import affinityBlock from '@map/colors/affinity-block.js'
import doxaRegion from '@map/colors/doxa-region.js'
// ─── Bundle-private profile registry ─────────────────────────────────────────
const profileModules = import.meta.glob('./*.vue')
// ─── Bundle-private color strategies (card BUILD-STACKED-TREE-LEGEND) ────────
// The stacked tree legend colours its People and Place trees from the cap-3 research map's
// palettes, so the legend can never drift from the map's pins. Those two strategies are
// PROFILE-LOCAL to doxa-research-map (registered there by its own src/colors glob — see
// plugin-designs/doxa-research-map/index.js:38), so the dashboard bundle's registry does not
// contain them unless we register them here too.
//
// This MATTERS because the failure is silent: getColorStrategy() falls back to language-family
// for an unregistered mode, and useAffinityBlockLegendData.js:57 guards on propertyKey and
// returns {} — so without this, every People swatch would quietly be neutral grey and nothing
// would error. Registering the SAME modules means one palette, not a copied one.
//
// Imported EXPLICITLY rather than via import.meta.glob: a glob is resolved relative to this
// file by Vite and a cross-bundle relative glob that stops matching fails silently (zero
// strategies registered, grey swatches), whereas a named import fails loudly at build time.
registerStrategies({
'./affinity-block.js': { default: affinityBlock },
'./doxa-region.js': { default: doxaRegion },
})
// ─── Mapbox RTL text plugin — load ONCE before any map instance ──────────────
installRtlTextPlugin()
/**
* Build a fresh defineCustomElement-wrapped class. Each instance gets its own Pinia + i18n
* so multiple embeds on the same page don't bleed state.
*/
function buildDoxaResearchDashboardElement() {
return defineCustomElement(ProfileLoader, {
configureApp(app) {
app.use(createPinia())
// ★ HAND THE INSTANCE TO ProfileLoader, DON'T JUST INSTALL IT.
// ProfileLoader is the root of this custom element, and vue-i18n's useI18n() cannot resolve
// its injection at a custom-element root — so ProfileLoader takes the i18n INSTANCE by
// reference through inject('appI18n') and drives its global locale from the host's
// profile-config. With nothing provided under that key the inject returns null and the
// watchEffect that applies the locale quietly does nothing.
//
// MEASURED with check:host-locale-contract: this leaf answered 0 Arabic characters against a
// reference of 1,891 for the same page asked for in Arabic by URL. Not a missing catalogue and
// not a missing i18n install — the install was here all along, one line short of being usable.
// The sibling map entry (plugin-designs/doxa-research-map/index.js:66) has always provided it;
// this entry's own header calls itself a mirror of that file, which is how the divergence
// survived: the two read as the same plumbing until you diff them.
const i18n = createAppI18n()
app.use(i18n)
app.provide('appI18n', i18n)
app.provide('profileModules', profileModules)
}
})
}
const DoxaResearchDashboardElement = buildDoxaResearchDashboardElement()
// ─── Register ONE tag — <research-dashboard-map> ────────────────────────────
if (typeof customElements !== 'undefined') {
if (!customElements.get('research-dashboard-map')) {
customElements.define('research-dashboard-map', DoxaResearchDashboardElement)
}
}
export default DoxaResearchDashboardElement