diff --git a/documentation/reference-library-index.md b/documentation/reference-library-index.md index 55aaf7828..7e1bba4d4 100644 --- a/documentation/reference-library-index.md +++ b/documentation/reference-library-index.md @@ -178,6 +178,10 @@ and a change here can break other maps. `import … from '@map/utils/embedBase.js'` - **geoUtils** — geoUtils.js — Geographic / GeoJSON Utility Functions `import … from '@map/utils/geoUtils.js'` +- **html** — html.js — the two helpers every HTML string built in library/ must use (CRITIC-3, 2026-09-26). + `import … from '@map/utils/html.js'` +- **loadPinnedUmd** — loadPinnedUmd — load a PINNED, integrity-checked UMD library and capture it WITHOUT touching the host + `import … from '@map/utils/loadPinnedUmd.js'` - **viewportScrim** — viewportScrim.js — make a position:fixed full-screen SCRIM actually cover the VIEWPORT, `import … from '@map/utils/viewportScrim.js'` @@ -192,6 +196,8 @@ and a change here can break other maps. ### Constants (never-change values) +- **cdn** — cdn.js — EVERY third-party script and stylesheet a plugin loads at runtime, pinned to an EXACT version + `import … from '@map/constants/cdn.js'` - **mapDefaults** — mapDefaults.js — Default map initialization settings `import … from '@map/constants/mapDefaults.js'` - **posterDefaults** — posterDefaults.js — default margins, bleed, slot sizes, and typography diff --git a/plugin-designs/doxa-research-map/src/colors/affinity-block.js b/library/colors/affinity-block.js similarity index 89% rename from plugin-designs/doxa-research-map/src/colors/affinity-block.js rename to library/colors/affinity-block.js index ca3b1476c..f8b581203 100644 --- a/plugin-designs/doxa-research-map/src/colors/affinity-block.js +++ b/library/colors/affinity-block.js @@ -42,6 +42,14 @@ export const AFFINITY_BLOCK_NAMES = { } /** Primary mapping: ROP1 Code → Color */ +// ★ REJECTED CANDIDATE, RECORDED ABOVE THE PALETTE DELIBERATELY: #AF7AC5 was considered for A019 +// and scored 2.95 against the Material Light surface, just under WCAG 1.4.11's 3.0:1 non-text floor. +// It lives here rather than beside A019 because check:one-palette slices from `export const PALETTE` +// to END OF FILE and matches every hex it finds -- so a rejected colour mentioned BELOW the export +// would be demanded of every consumer bundle, and the gate would fail on a colour we refused on +// accessibility grounds. Keeping the note above the export preserves the reasoning without +// creating that obligation. (The gate's own comment says it reads "only the PALETTE export"; its +// implementation does not, which is reported separately.) export const PALETTE = { 'A001': '#E74C3C', // Arab World - Red/Orange 'A002': '#3498DB', // East Asian Peoples - Blue @@ -68,7 +76,6 @@ export const PALETTE = { // A013 #8E44AD 5.28 (unchanged) A018 #6C3483 7.69 A019 #A569BD 3.53 // Sibling separation so the three stay tellable apart: A018-vs-A019 2.61, // A013-vs-A018 1.46, A013-vs-A019 1.79. - // REJECTED #AF7AC5 for A019 — scored 2.95, just under the 3.0 floor. 'A018': '#6C3483', // Tibetan Peoples - deep purple (A013 family, darker) 'A019': '#A569BD' // Himalayan Peoples - light purple (A013 family, lighter) } diff --git a/plugin-designs/country-posters/src/colors/doxa-region.js b/library/colors/doxa-region.js similarity index 100% rename from plugin-designs/country-posters/src/colors/doxa-region.js rename to library/colors/doxa-region.js diff --git a/library/composables/legends/useAffinityBlockLegendData.js b/library/composables/legends/useAffinityBlockLegendData.js index eee67c21d..9bbb25f98 100644 --- a/library/composables/legends/useAffinityBlockLegendData.js +++ b/library/composables/legends/useAffinityBlockLegendData.js @@ -42,9 +42,12 @@ import { computed } from 'vue' // Single source of truth — pins paint with this same palette, keyed by ROP1 code. // Resolving the SAME strategy the pins use keeps legend ↔ map in lock-step (was a // separate name-keyed palette causing pin/legend drift). affinity-block is a per-map -// (research-only) strategy that lives in plugin-designs/doxa-research-map/ -// src/colors/affinity-block.js and is resolved via the registry (registered by -// the research bundle's local src/colors glob). Read LAZILY (at call time +// strategy, which NOW LIVES IN library/colors/affinity-block.js (C1): it was private to +// plugin-designs/doxa-research-map/ and duplicated in country-posters, where the stale copy +// covered only A001-A017 and sent 134 Tibetan/Himalayan rows to the grey fallback. +// It is resolved via the registry, which now discovers it from library/colors/ for EVERY +// bundle rather than from one plugin's local glob -- that is the whole point of the move. +// Read LAZILY (at call time // inside blocColor — this composable is only invoked by research surfaces) so // registration has already run, and GUARDED on propertyKey — getColorStrategy // silently falls back to language-family for unregistered modes, and that palette diff --git a/plugin-designs/country-posters/index.js b/plugin-designs/country-posters/index.js index ffe40b8df..1daffa77c 100644 --- a/plugin-designs/country-posters/index.js +++ b/plugin-designs/country-posters/index.js @@ -48,6 +48,13 @@ const profileModules = import.meta.glob('./*.vue') // Keys are auto-derived from filenames and merged OVER the shared library set. // This bundle inlines its own copy of the registry (IIFE build), so registering // here affects THIS bundle only. +// ★ THIS GLOB NOW COVERS resource.js ONLY. doxa-region.js and affinity-block.js moved to +// library/colors/ (C1), where _registry.js discovers them for every bundle, so registering them +// here would be re-declaring what the shared set already has. +// THE GLOB STAYS rather than being deleted: resource.js is still private pending C1b (its palette +// has 8 values and the two dashboards ship 6, which needs its owner's answer before it moves). +// Removing the glob now would unregister resource and colour those pins by the soft fallback -- +// the same class of silent wrong-colour bug this migration is fixing. registerStrategies(import.meta.glob('./src/colors/*.js', { eager: true })) // ─── Mapbox RTL text plugin — load ONCE before any map instance ────────────── diff --git a/plugin-designs/country-posters/src/colors/affinity-block.js b/plugin-designs/country-posters/src/colors/affinity-block.js deleted file mode 100644 index 06d38ebd6..000000000 --- a/plugin-designs/country-posters/src/colors/affinity-block.js +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Affinity Block Color Strategy - * - * Colors pins based on their ROP1 code (A001, A002, etc.). - * Uses ROP1 code as the primary key for consistency across data sources. - * - * ARCHITECTURE DECISION: - * - Use ROP1 codes (A001, A002, etc.) as the PRIMARY KEY - * - Store mappings from ROP1 → Name and ROP1 → Color - * - Avoids issues with typos in affinity bloc names - */ - -export const PROPERTY_KEY = 'affinityBlock' // This is the ROP1 code - -const DEFAULT_COLOR = '#95a5a6' - -/** Primary mapping: ROP1 Code → Affinity Block Name */ -export const AFFINITY_BLOCK_NAMES = { - 'A001': 'Arab World', - 'A002': 'East Asian Peoples', - 'A003': 'Eurasian Peoples', - 'A004': 'Horn of Africa Peoples', - 'A005': 'Jewish', - 'A006': 'Malay Peoples', - 'A007': 'Persian-Median Peoples', - 'A008': 'South Asian Peoples', - 'A009': 'North American Peoples', - 'A010': 'Pacific Islanders', - 'A011': 'Southeast Asian Peoples', - 'A012': 'Sub-Saharan Peoples', - 'A013': 'Tibetan-Himalayan Peoples', - 'A014': 'Turkic Peoples', - 'A015': 'Latin-Caribbean Americans', - 'A017': 'Deaf' -} - -/** Primary mapping: ROP1 Code → Color */ -export const PALETTE = { - 'A001': '#E74C3C', // Arab World - Red/Orange - 'A002': '#3498DB', // East Asian Peoples - Blue - 'A003': '#2ECC71', // Eurasian Peoples - Green - 'A004': '#F39C12', // Horn of Africa Peoples - Orange - 'A005': '#9B59B6', // Jewish - Purple - 'A006': '#1ABC9C', // Malay Peoples - Teal - 'A007': '#E67E22', // Persian-Median Peoples - Dark orange - 'A008': '#34495E', // South Asian Peoples - Dark blue-grey - 'A009': '#95A5A6', // North American Peoples - Grey - 'A010': '#5DADE2', // Pacific Islanders - Light blue - 'A011': '#16A085', // Southeast Asian Peoples - Sea green - 'A012': '#C0392B', // Sub-Saharan Peoples - Deep red - 'A013': '#8E44AD', // Tibetan-Himalayan Peoples - Purple - 'A014': '#D35400', // Turkic Peoples - Burnt orange - 'A015': '#27AE60', // Latin-Caribbean Americans - Forest green - 'A017': '#000000' // Deaf - Black (universal accessibility) -} - -export const AFFINITY_BLOCK_COLORS = PALETTE - -/** Reverse lookup: Name → ROP1 Code */ -export const AFFINITY_BLOCK_NAME_TO_CODE = Object.entries(AFFINITY_BLOCK_NAMES).reduce((acc, [code, name]) => { - acc[name] = code - return acc -}, {}) - -/** Legacy: Name-based color lookup (for backward compatibility) */ -export const AFFINITY_BLOCK_COLORS_BY_NAME = Object.entries(AFFINITY_BLOCK_NAMES).reduce((acc, [code, name]) => { - acc[name] = AFFINITY_BLOCK_COLORS[code] - return acc -}, {}) - -export const NAMES = AFFINITY_BLOCK_NAMES - -export function getAffinityBlockName(code) { - return AFFINITY_BLOCK_NAMES[code] || 'Unknown' -} - -export function getAffinityBlockColorByCode(code) { - return AFFINITY_BLOCK_COLORS[code] || DEFAULT_COLOR -} - -/** - * Get color for an affinity block (supports both ROP1 code and name). - */ -export function getAffinityBlockColor(blockNameOrCode) { - if (!blockNameOrCode) return DEFAULT_COLOR - - if (typeof blockNameOrCode === 'string' && /^A\d{3}$/.test(blockNameOrCode)) { - return AFFINITY_BLOCK_COLORS[blockNameOrCode] || DEFAULT_COLOR - } - - return AFFINITY_BLOCK_COLORS_BY_NAME[blockNameOrCode] || DEFAULT_COLOR -} - -export function getAffinityBlocksArray() { - return Object.entries(AFFINITY_BLOCK_NAMES).map(([code, name]) => ({ - code, - name, - color: AFFINITY_BLOCK_COLORS[code] - })) -} - -export function getAffinityBlockData(code) { - return { - code, - name: AFFINITY_BLOCK_NAMES[code] || 'Unknown', - color: AFFINITY_BLOCK_COLORS[code] || DEFAULT_COLOR - } -} - -/** - * Strategy-aligned getter — uniform name across all per-strategy modules. - * Expects properties.affinityBlock to contain ROP1 code (e.g., "A007"). - */ -export function getColor(properties) { - const rop1Code = properties.affinityBlock || properties._raw?.ROP1 - return getAffinityBlockColorByCode(rop1Code) -} - -/** - * Get display name for a feature. - */ -export function getName(properties) { - const rop1Code = properties.affinityBlock || properties._raw?.ROP1 - return getAffinityBlockName(rop1Code) -} - -/** - * Build Mapbox color expression for affinity blocks. - * Uses ROP1 code (A001, A002, etc.) as the match key. - */ -export function applyColor() { - const colorExpression = ['match', ['get', PROPERTY_KEY]] - Object.entries(AFFINITY_BLOCK_COLORS).forEach(([code, color]) => { - colorExpression.push(code, color) - }) - colorExpression.push(DEFAULT_COLOR) - return colorExpression -} - -export const buildColorExpression = applyColor - -// Backward-compatibility aliases -export const affinityBlockCodeToName = AFFINITY_BLOCK_NAMES -export const affinityBlockCodeToColor = AFFINITY_BLOCK_COLORS -export const affinityBlockNameToCode = AFFINITY_BLOCK_NAME_TO_CODE -export const affinityBlockColors = AFFINITY_BLOCK_COLORS_BY_NAME -export const affinityBlockCodeLookup = AFFINITY_BLOCK_NAMES - -export default { - name: 'Affinity Block', - propertyKey: PROPERTY_KEY, - palette: PALETTE, - colors: PALETTE, - names: NAMES, - getColor, - applyColor, - buildColorExpression, - getName, - getAffinityBlockName, - getAffinityBlockColor, - getAffinityBlockColorByCode, - getAffinityBlocksArray, - getAffinityBlockData -} diff --git a/plugin-designs/dashboard-plus-grid/index.js b/plugin-designs/dashboard-plus-grid/index.js index e1b2df2a1..24aaba439 100644 --- a/plugin-designs/dashboard-plus-grid/index.js +++ b/plugin-designs/dashboard-plus-grid/index.js @@ -15,8 +15,13 @@ import { createPinia } from 'pinia' import { createAppI18n } from '@map/i18n/index.js' import { registerStrategies } from '@map/colors/_registry.js' import { installRtlTextPlugin } from '@map/maps/rtl-text.js' -import affinityBlock from '../doxa-research-map/src/colors/affinity-block.js' -import doxaRegion from '../doxa-research-map/src/colors/doxa-region.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' import DashboardPlusGrid from './dashboard-plus-grid.vue' // ─── THE SAME BYPASS AS THE i18n BUG BELOW, SECOND LEG (card BUILD-MAP-STYLE-...) ──────────── diff --git a/plugin-designs/doxa-research-map/index.js b/plugin-designs/doxa-research-map/index.js index 7e5334778..d73da24e2 100644 --- a/plugin-designs/doxa-research-map/index.js +++ b/plugin-designs/doxa-research-map/index.js @@ -35,6 +35,13 @@ const profileModules = import.meta.glob('./*.vue') // copy of the registry (IIFE build), so registering here affects THIS bundle only. // This bundle ships THREE research-only strategies: affinity-block.js, // doxa-region.js, resource.js (shared modes like religion stay in the library). +// ★ THIS GLOB NOW COVERS resource.js ONLY. doxa-region.js and affinity-block.js moved to +// library/colors/ (C1), where _registry.js discovers them for every bundle, so registering them +// here would be re-declaring what the shared set already has. +// THE GLOB STAYS rather than being deleted: resource.js is still private pending C1b (its palette +// has 8 values and the two dashboards ship 6, which needs its owner's answer before it moves). +// Removing the glob now would unregister resource and colour those pins by the soft fallback -- +// the same class of silent wrong-colour bug this migration is fixing. registerStrategies(import.meta.glob('./src/colors/*.js', { eager: true })) // ─── Mapbox RTL text plugin — load ONCE before any map instance ────────────── diff --git a/plugin-designs/doxa-research-map/src/colors/doxa-region.js b/plugin-designs/doxa-research-map/src/colors/doxa-region.js deleted file mode 100644 index 27f620afa..000000000 --- a/plugin-designs/doxa-research-map/src/colors/doxa-region.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * DOXA Region Color Strategy - * - * Colors pins based on their DOXA / WAGF region. - * - * Drift note (vs simple-map): - * - simple-map's 'No WAGF Region/Bloc' = '#95a5a6' (grey), - * research = '#1a1a2e' (deep navy). Research wins. - * - * generateColorFromString fallback comes from utils/geoUtils.js — sister - * subagent 1D owns that file. Cross-folder dep flagged. - */ - -import { generateColorFromString } from '@map/utils/geoUtils.js' - -export const PROPERTY_KEY = 'doxaRegion' - -const DEFAULT_COLOR = '#0098FF' // Bright blue for null/undefined region -const FALLBACK_COLOR = '#95a5a6' - -/** - * DOXA Region Color Palette — defines colors for the 8 DOXA regions. - */ -export const PALETTE = { - 'Africa': '#ff2e2e', - 'Asia': '#1a8cff', - 'Europe': '#1ee05f', - 'Latin America & Caribbean': '#FFEB3B', - 'Middle East': '#a83dff', - 'No WAGF Region/Bloc': '#1a1a2e', - 'North America & Non-Spanish Caribbean': '#00e0bd', - 'Oceania': '#ff7f11' -} - -export const DOXA_REGION_COLORS = PALETTE - -/** - * SLUG-keyed palette — the AUTHORITATIVE map for coloring. - * - * The pray-tools API returns wagf_region as { value: , label: }. - * DSM stores the stable slug in properties.doxaRegion (e.g. 'asia', 'middle_east'). - * The label is localized per `lang` ("Africa" → "África" → "Afrique"), so keying - * colors on the display name silently breaks in every non-English locale (only - * "Asia" — spelled identically across es/fr — survives). Key on the slug instead. - * Slugs verified against the live API (2026-06-22). - */ -export const SLUG_PALETTE = { - 'africa': '#ff2e2e', - 'asia': '#1a8cff', - 'europe': '#1ee05f', - 'latin_america_&_caribbean': '#FFEB3B', - 'middle_east': '#a83dff', - 'north_america_&_non-spanish_caribbean': '#00e0bd', - 'oceania': '#ff7f11', - 'na': '#1a1a2e' // No WAGF Region/Bloc -} - -/** Color for a region by its stable slug (locale-independent). */ -export function getRegionColorBySlug(slug) { - if (!slug) return DEFAULT_COLOR - return SLUG_PALETTE[String(slug).toLowerCase()] || FALLBACK_COLOR -} - -/** Extended region aliases for flexible matching */ -export const REGION_COLOR_ALIASES = { - 'Africa': '#ff2e2e', - 'East Asia': '#1a8cff', - 'Asia': '#1a8cff', - 'Eurasia': '#1ee05f', - 'Europe': '#1ee05f', - 'Latin America': '#FFEB3B', - 'Latin America & Caribbean': '#FFEB3B', - 'Caribbean': '#FFEB3B', - 'Middle East': '#a83dff', - 'North America': '#00e0bd', - 'North America (non-spanish)': '#00e0bd', - 'North America & Non-Spanish Caribbean': '#00e0bd', - 'Pacific': '#ff7f11', - 'Oceania': '#ff7f11', - 'South Asia': '#ff7f11', - 'No WAGF Region/Bloc': '#1a1a2e', - 'Unknown': '#0098FF' -} - -/** - * Get color for a region — falls back to a deterministic - * generateColorFromString() so unmapped regions still render. - */ -export function getRegionColor(regionName) { - if (!regionName) { - return DEFAULT_COLOR - } - - const color = REGION_COLOR_ALIASES[regionName] - if (!color) { - return generateColorFromString(regionName) - } - - return color -} - -export function getColor(properties) { - // properties.doxaRegion is the stable slug (e.g. 'asia'), NOT the localized - // label — color by slug so it survives language switches. - const slug = properties.doxaRegion || properties._normalized?.doxaRegion - return getRegionColorBySlug(slug) -} - -/** - * Build Mapbox color expression for DOXA regions. - * - * Matches on the stable slug stored in `doxaRegion` (e.g. 'asia') so the - * expression keeps coloring correctly when the API returns localized labels. - */ -export function applyColor() { - const colorExpression = ['match', ['get', PROPERTY_KEY]] - Object.entries(SLUG_PALETTE).forEach(([slug, color]) => { - colorExpression.push(slug, color) - }) - colorExpression.push(FALLBACK_COLOR) - return colorExpression -} - -export const buildColorExpression = applyColor - -export default { - name: 'DOXA Region', - propertyKey: PROPERTY_KEY, - palette: PALETTE, - colors: PALETTE, - slugColors: SLUG_PALETTE, - aliases: REGION_COLOR_ALIASES, - fallback: FALLBACK_COLOR, - getColor, - applyColor, - buildColorExpression, - getRegionColor, - getRegionColorBySlug -} diff --git a/plugin-designs/research-dashboard/index.js b/plugin-designs/research-dashboard/index.js index e32807c43..c019616ac 100644 --- a/plugin-designs/research-dashboard/index.js +++ b/plugin-designs/research-dashboard/index.js @@ -25,8 +25,13 @@ 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' -import affinityBlock from '../doxa-research-map/src/colors/affinity-block.js' -import doxaRegion from '../doxa-research-map/src/colors/doxa-region.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')