From 9fc2a5047b00c8349e68e9be6ba4a1e09dcf5cf3 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Fri, 2 Oct 2026 19:55:05 +0000 Subject: [PATCH] India's states as a library poster type: shows the 36 states and union territories, each with its state poster, prayer packet and cards, through the same engine as Country Posters' India section (stateCards + makeIndiaStateSheet, now in @map/packet/indiaStateSheet.js). A type may load its table before its cards are read. The whole-India card stays out of the type. --- .../components/poster/PosterSectionView.vue | 6 +++ library/components/poster/posterTypes.js | 4 ++ .../poster/types/indiaStatePosterType.js | 42 +++++++++++++++++++ .../src => library/packet}/indiaStateSheet.js | 15 ++++++- package.json | 2 +- .../country-posters/country-poster-wall.vue | 10 ++--- .../scripts/check-country-sheet-one-path.mjs | 23 +++++++++- 7 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 library/components/poster/types/indiaStatePosterType.js rename {plugin-designs/country-posters/src => library/packet}/indiaStateSheet.js (82%) diff --git a/library/components/poster/PosterSectionView.vue b/library/components/poster/PosterSectionView.vue index 19c8548f7..1ec96ca63 100644 --- a/library/components/poster/PosterSectionView.vue +++ b/library/components/poster/PosterSectionView.vue @@ -21,6 +21,12 @@ const unknownType = ref(false) async function resolveType(name) { const a = await posterType(name) if (name !== typeName.value) return // the type changed while this one loaded + // A type whose cards come from a FETCHED table (indiaState) loads it first; items() then reads it. A failed load + // leaves items() empty, so the section says it has none rather than guessing. + if (a && typeof a.load === 'function') { + try { await a.load({ assetsBase: assetsBase.value }) } catch (_) { /* items() stays empty: the none line */ } + if (name !== typeName.value) return + } adapter.value = a; unknownType.value = !a } watch(typeName, resolveType, { immediate: true }) diff --git a/library/components/poster/posterTypes.js b/library/components/poster/posterTypes.js index aedbfa236..90d604395 100644 --- a/library/components/poster/posterTypes.js +++ b/library/components/poster/posterTypes.js @@ -64,3 +64,7 @@ if (!registry().loaders.has('country')) { registry().loaders.set('country', () => import('./types/countryPosterType.js')) announce('country') } +if (!registry().loaders.has('indiaState')) { + registry().loaders.set('indiaState', () => import('./types/indiaStatePosterType.js')) + announce('indiaState') +} diff --git a/library/components/poster/types/indiaStatePosterType.js b/library/components/poster/types/indiaStatePosterType.js new file mode 100644 index 000000000..22a069532 --- /dev/null +++ b/library/components/poster/types/indiaStatePosterType.js @@ -0,0 +1,42 @@ +/** + * The 'indiaState' poster type: one poster per Indian state or union territory, from the shared state table + * (library/data/indiaStates.js, served + pinned) and the ONE state-sheet path (@map/packet/indiaStateSheet.js), the + * same cards and sheets as Country Posters' India section, so a host embeds them with + * + * Each card prints its state poster, its prayer list/packet and its card deck. The WHOLE-INDIA card (country-ind) + * is not in this type: it stays withheld wherever it appears (countrySheet.js WITHHELD). + * The table is fetched, so the type has a load(): awaits it before reading items(). + * scope = 'all' (the only one). Order: most people groups first, as on the wall, matching each sheet's "map N of M". + */ +import { loadIndiaStates } from '@map/data/indiaStates.js' +import { makeIndiaStateSheet, stateCards } from '@map/packet/indiaStateSheet.js' +import { createPeopleGroupRecords } from '@map/lists/peopleGroupRecords.js' +import { tStatic, getRuntimeLocale, detectLocale } from '@map/i18n/index.js' +import { countryName } from '@map/utils/countryName.js' +import data from '@map/data/country-posters.json' + +const REGION = (data.countries.find((c) => c.iso3 === 'IND') || {}).region || 'Asia' // India's hue, as on the wall +let table = null + +export default { + async load({ assetsBase }) { if (!table) table = (await loadIndiaStates(assetsBase)).states || [] }, + scopes: () => ['all'], + items(scope, locale) { + const cards = stateCards(table, REGION) + if (/^en\b/i.test(locale || 'en')) return cards + // named in the reader's language from the catalogue; the printed sheet stays English, like a country's + return cards.map((st) => ({ ...st, label: tStatic(`countryPosters.stateNames.${st.iso}`, locale) || st.label })) + }, + // the section is India's: its name in the reader's language (a host adds its own heading if it wants one) + title() { const l = getRuntimeLocale() || detectLocale() || 'en'; return countryName('IND', l) || 'India' }, + actions({ assetsBase, apiBase, lang, profileConfig, t }) { + const recordsFor = createPeopleGroupRecords({ apiBase, lang, profileConfig }) + const sheet = makeIndiaStateSheet({ records: recordsFor, assetsBase, states: () => table, region: REGION }) + return { + records: (c) => (c.slugs && c.slugs.length ? recordsFor({ type: 'pg_slugs', values: c.slugs }) : Promise.resolve([])), + sheet: (card, format) => sheet(card, format), + unsupported: (c) => (c.noGroups ? t('countryPosters.indiaStateNoGroups') : null), + unit: 'UPG', itemCountKey: 'countryPosters.indiaItemCount', eventName: 'doxa-country-posters:show-on-map', downloadPrefix: 'doxa-india-state', cardStocks: true, + } + }, +} diff --git a/plugin-designs/country-posters/src/indiaStateSheet.js b/library/packet/indiaStateSheet.js similarity index 82% rename from plugin-designs/country-posters/src/indiaStateSheet.js rename to library/packet/indiaStateSheet.js index 10f3d7f42..565256a74 100644 --- a/plugin-designs/country-posters/src/indiaStateSheet.js +++ b/library/packet/indiaStateSheet.js @@ -10,7 +10,9 @@ * const sheet = makeIndiaStateSheet({ records, assetsBase: () => base, states }) * // (item, format) -> html, '' when it cannot be drawn * - * ONE PATH: the wall's state cards draw through this and nothing else (check:country-sheet-one-path). + * ONE PATH: the state cards draw through this and nothing else: Country Posters' wall and the library 'indiaState' + * poster type () both build their cards with stateCards() and their sheets + * with makeIndiaStateSheet() (check:country-sheet-one-path). */ // The states that get a poster: those with people groups listed, in the wall's own order (most groups first, @@ -20,6 +22,17 @@ export function statesWithPosters(states) { .slice().sort((a, b) => b.count - a.count || String(a.name).localeCompare(String(b.name), 'en')) } +// The cards, in the wall's order: the states with people groups (a poster each), then those with none listed +// (Nagaland, Mizoram), named with a card that says so rather than silently left out. +export function stateCards(states, region = 'Asia') { + const all = states || [] + const withGroups = statesWithPosters(all).map((st) => ({ + id: st.id, iso: st.iso, label: st.name, count: st.count, sheet: '3x3', region, indiaState: true, slugs: st.slugs, + })) + const none = all.filter((st) => !st.count).map((st) => ({ id: st.id, iso: st.iso, label: st.name, count: 0, indiaState: true, noGroups: true, slugs: [] })) + return [...withGroups, ...none] +} + export function stateSeries(states, state) { const tier = statesWithPosters(states) const index = tier.findIndex((s) => s.id === state?.id) + 1 diff --git a/package.json b/package.json index c7069314b..c66d41de0 100644 --- a/package.json +++ b/package.json @@ -483,7 +483,7 @@ "check:composite-token-url": "a composite embedded BARE (no outer ProfileLoader) must honour a tokenUrl-only config: it reads tokenUrl, forwards it, and asks the token service only when no token is given. The expectation is derived from ProfileLoader, not restated. Headless; in gates:pre.", "check:composites-mount": "every capability-4/5 composite's OWN setup() and mounted hooks run without throwing, in en and ar, headless (no browser): vite loads each composite as its bundle entry defines it (configureApp, and ProfileLoader's provides where the entry mounts ProfileLoader), and @vue/runtime-core mounts it on a no-op renderer. Exists because 08f89f55 shipped two composites calling getRuntimeLocale/detectLocale/RTL_LOCALES with no import: the build was green, setup threw a ReferenceError in every locale, and Big Picture and Simple Research rendered nothing. Proves setup runs; cannot prove the children render.", "check:country-names": "every ISO 3166-1 alpha-3 our data uses must resolve to a localized country name. Guards the JOIN, not Intl: library/data/iso3-to-alpha2.json drifting from library/data/countries.json makes a country render as a bare code, which still looks like a value. Checks en/ar/zh (ar is our only RTL locale; zh makes an untranslated Latin code unmistakable) and asserts BOTH directions of drift. Carries two negative arms — an unassigned code and a malformed one — and FAILS if either resolves, because a coverage count cannot otherwise tell a hit from a lookup that returns its input.", - "check:country-sheet-one-path": "a country sheet, and an Indian state or union territory sheet, each draw through ONE path (@map/packet/countrySheet.js; country-posters/src/indiaStateSheet.js), so one place cannot print two posters. Parses (comments cannot count) plugin sources: a direct generateCountrySheet FAILS anywhere, a direct generateSheet FAILS inside country-posters outside the state helper, every makeCountrySheet call must pass `countries`, and the known callers must be found. Named data answers: seriesFor over country-posters.json (India 1 of 1, each 3x3 country N of 173, an absent code no series, exactly one no-sheet country); over the served india-states.json + gb-IND-ADM1.geojson: 36 divisions, the 34 with groups numbered 1..34 each with an outline, the rest named, the shared ADM1 never mutated.", + "check:country-sheet-one-path": "a country sheet, and an Indian state or union territory sheet, each draw through ONE path (@map/packet/countrySheet.js; library/packet/indiaStateSheet.js), so one place cannot print two posters. Parses (comments cannot count) plugin sources: a direct generateCountrySheet FAILS anywhere, a direct generateSheet FAILS inside country-posters outside the state helper, every makeCountrySheet call must pass `countries`, and the known callers must be found. Named data answers: seriesFor over country-posters.json (India 1 of 1, each 3x3 country N of 173, an absent code no series, exactly one no-sheet country); over the served india-states.json + gb-IND-ADM1.geojson: 36 divisions, the 34 with groups numbered 1..34 each with an outline, the rest named, the shared ADM1 never mutated.", "check:country-sheet-one-path:self-test": "plants each defect check:country-sheet-one-path guards (a direct generateCountrySheet call, makeCountrySheet without countries, the old guard that skipped India, a position given to an absent code) and requires each to FAIL; a comment naming generateCountrySheet must NOT.", "check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.", "check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.", diff --git a/plugin-designs/country-posters/country-poster-wall.vue b/plugin-designs/country-posters/country-poster-wall.vue index 77617bbbc..8131141c3 100644 --- a/plugin-designs/country-posters/country-poster-wall.vue +++ b/plugin-designs/country-posters/country-poster-wall.vue @@ -49,7 +49,7 @@ import { isPhonePrint } from '@map/composables/printInPlace.js' import { keyedError } from '@map/i18n/keyedError.js' // India's states and union territories: the shared table (fetched when the wall shows them) and their sheet. import { loadIndiaStates } from '@map/data/indiaStates.js' -import { makeIndiaStateSheet, statesWithPosters } from './src/indiaStateSheet.js' +import { makeIndiaStateSheet, stateCards } from '@map/packet/indiaStateSheet.js' // The regions catalogue holds the not-yet sentence a host's country gets when no poster exists (all eleven locales); // its keys are English sentences with dots, so it is read by literal key, the way regions-list.vue reads it. import { useRegionsI18n } from '@map/i18n/useRegionsI18n.js' @@ -128,11 +128,7 @@ const INDIA_REGION = (byIso3.get('IND') || {}).region || 'Asia' const stateItems = computed(() => { const all = indiaStates.value if (filtered.value || !all) return [] - const withGroups = statesWithPosters(all).map((st) => ({ - id: st.id, iso: st.iso, label: st.name, count: st.count, sheet: '3x3', region: INDIA_REGION, indiaState: true, slugs: st.slugs, - })) - const none = all.filter((st) => !st.count).map((st) => ({ id: st.id, iso: st.iso, label: st.name, count: 0, indiaState: true, noGroups: true, slugs: [] })) - return [...withGroups, ...none] + return stateCards(all, INDIA_REGION) }) const items = computed(() => { @@ -187,7 +183,7 @@ const sheetForRecord = makeCountrySheet({ countries: data.countries || [], }) const countryOnlySheet = (card, format) => sheetForRecord(byId.get(card.id) || card, format) -// A state's sheet through its own ONE path (src/indiaStateSheet.js), in India's hue and the indiaState series. +// A state's sheet through its own ONE path (@map/packet/indiaStateSheet.js), in India's hue and the indiaState series. const stateSheet = makeIndiaStateSheet({ records: recordsFor, assetsBase: () => assetsBase.value, states: () => indiaStates.value, region: INDIA_REGION, }) diff --git a/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs b/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs index 8bc30174e..5b5eb4f05 100644 --- a/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs +++ b/plugin-designs/country-posters/scripts/check-country-sheet-one-path.mjs @@ -31,6 +31,21 @@ const REGIONS_LIST = 'plugin-designs/regions-dashboard/regions-list.vue' const COUNTRY_TYPE = 'library/components/poster/types/countryPosterType.js' // ARM H: the library 'country' type (the element's caller) withholds like the wall: rows reject with the sentence, // a withheld poster's card shows the sentence. +// ARM I: the library 'indiaState' type (, Bless India's states) is the SAME +// engine as the wall's India section: both build their cards with stateCards() and their sheets with +// makeIndiaStateSheet() from @map/packet/indiaStateSheet.js; the type never reaches the whole-India card or its sheet. +const STATE_TYPE = 'library/components/poster/types/indiaStatePosterType.js' +export function armI(H, states, typeSrc, wallSrc) { + const fails = [] + if (!/import \{[^}]*\bmakeIndiaStateSheet\b[^}]*\bstateCards\b[^}]*\} from '@map\/packet\/indiaStateSheet\.js'/.test(typeSrc)) fails.push(`${STATE_TYPE}: does not build through @map/packet/indiaStateSheet.js (stateCards + makeIndiaStateSheet): a second engine`) + if (/makeCountrySheet|country-ind|'IND'\s*\)\s*\?/.test(typeSrc.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '').replace(/c\.iso3 === 'IND'/, ''))) fails.push(`${STATE_TYPE}: reaches the whole-India card or its sheet (it must stay withheld)`) + if (!/stateCards\(/.test(wallSrc)) fails.push('country-poster-wall.vue builds its own state cards: it must use stateCards() (one engine with the indiaState type)') + const cards = H.stateCards ? H.stateCards(states, 'Asia') : [] + if (cards.length !== states.length) fails.push(`stateCards() gives ${cards.length} cards for ${states.length} states/UTs`) + if (cards.some((c) => c.id === 'country-ind' || !c.indiaState)) fails.push('stateCards() returns a non-state card') + return fails +} + export function armH(typeSrc) { const fails = [] if (!/makeCountrySheet\(/.test(typeSrc)) fails.push(`${COUNTRY_TYPE}: no longer draws through makeCountrySheet`) @@ -40,7 +55,7 @@ export function armH(typeSrc) { } // India's states and union territories draw through their own ONE path; inside this plugin only that helper may // call the generator directly. -const STATE_HELPER = 'plugin-designs/country-posters/src/indiaStateSheet.js' +const STATE_HELPER = 'library/packet/indiaStateSheet.js' const STATE_CALLER = 'plugin-designs/country-posters/country-poster-wall.vue' function scriptsOf(file, src) { @@ -271,6 +286,7 @@ async function main() { const REG = readFileSync(join(ROOT, 'plugin-designs/regions-dashboard/regions-list.vue'), 'utf8') const MFE = readFileSync(join(ROOT, 'plugin-designs/country-posters/country-posters-mfe.vue'), 'utf8') const TYPE = readFileSync(join(ROOT, COUNTRY_TYPE), 'utf8') + const TYPE_STATE = readFileSync(join(ROOT, STATE_TYPE), 'utf8') const regIsCaller = /makeCountrySheet\(/.test(REG) const lists = (wallSrc, regSrc = REG) => (regIsCaller ? { wall: wallSrc, regions: regSrc } : { wall: wallSrc }) if (selfTest) { @@ -301,6 +317,9 @@ async function main() { ['the India tab shown without the preview flag', async () => armG(CS, lists(files[wall]), MFE.replace('...(import.meta.env && import.meta.env.DEV && globalThis.DXM_PREVIEW_UNRELEASED === true ? [INDIA_PANE] : []),', 'INDIA_PANE,'))], ['the wall prints the list of a country whose rows are withheld', async () => armG(CS, lists(files[wall].replace('rowsWithheld(c) ? withheld() : ', '')))], ['the library type prints withheld rows', () => armH(TYPE.replace('rowsWithheld(c) ? ', 'false ? '))], + ['the indiaState type draws a second way', () => armI(H, states, TYPE_STATE.replace("from '@map/packet/indiaStateSheet.js'", "from './myOwnStateSheet.js'"), files[wall])], + ['the indiaState type reaches the whole-India sheet', () => armI(H, states, TYPE_STATE + "\nimport { makeCountrySheet } from '@map/packet/countrySheet.js'", files[wall])], + ['the wall builds its own state cards again', () => armI(H, states, TYPE_STATE, files[wall].replace('stateCards(all, INDIA_REGION)', 'all.map((st) => st)'))], ['the library type shows a withheld poster', () => armH(TYPE.replace('posterWithheld(c) ||', 'false ||'))], ['a helper that mutates the shared ADM1', () => armC({ ...H, stateGeometry: (z, a, n) => { (a.features || []).forEach((f) => { if (f.properties.shapeName === n) f.properties.listId = z.id }); return H.stateGeometry(z, a, n) } }, states, JSON.parse(JSON.stringify(adm1Ind)))], ] @@ -321,7 +340,7 @@ async function main() { const callers = [] const E = armE(countries, states, files['plugin-designs/country-posters/country-poster-wall.vue'], readFileSync(join(ROOT, 'library/components/poster/PosterWall.vue'), 'utf8'), H) - const G = [...await armG(CS, lists(files['plugin-designs/country-posters/country-poster-wall.vue']), MFE), ...armH(TYPE)] + const G = [...await armG(CS, lists(files['plugin-designs/country-posters/country-poster-wall.vue']), MFE), ...armH(TYPE), ...armI(H, states, TYPE_STATE, files['plugin-designs/country-posters/country-poster-wall.vue'])] const fails = [...armA(files, callers), ...armB(seriesFor, countries, sheetFormatFor), ...armC(H, states, adm1Ind), ...G, ...E.fails, ...armD(states, areas, meta), ...armF(lists(files['plugin-designs/country-posters/country-poster-wall.vue']))] const missing = [...KNOWN_CALLERS, `${STATE_CALLER}#state`].filter((k) => !callers.includes(k)) if (missing.length) fails.push(`known callers not found calling makeCountrySheet: ${missing.join(', ')}`)