From 04f165efd37982a903c0a30bcb3a769bc7ea3bd1 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Thu, 24 Sep 2026 04:55:55 +0000 Subject: [PATCH 01/11] feat(poster): share the print actions, keep the record fetching per bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The poster wall was generalised into library/components/poster/PosterWall.vue and two bundles migrated onto it — but the ORIGINAL, my-upg-100-list's 903-line wall, was left on its own copy. So the one wall carrying the real print actions is the one wall nothing else benefits from, and the shared component's packet path was still a "coming soon" stub. Every print fix made in the 100-list this flight (the card deck, the stock picker, the image_url fetch) was invisible to the three bundles on the shared component, and vice versa. ★ THE SEAM IS WHAT MADE THIS SHAREABLE AT ALL. The PRINT ACTIONS are common; the RECORD FETCHING is not. fetchListRecords in the 100-list knows the people-groups API, the selector evaluator and the pg_slugs special case; Key Cities and Country Posters have entirely different data shapes. usePosterPrints.js therefore takes a `records(item)` PROVIDER and owns everything after it — the HUD state machine, the in-place print frame, and the documents. That also keeps the bundles honest. A wall that CAN supply rows passes the prop and gets the list + card-deck prints; one that cannot omits it, the Card Deck button is not rendered, and Prayer List keeps its truthful "coming soon" instead of a button that fails at click time. Country Posters shows exactly that today: 175 cards, Prayer List and Poster Map, no Card Deck — because it has no provider yet, not because something broke. ⚠ ONE exportState, not two. The composable owns a ref by default, and a wall rendering its OWN state while these actions wrote to a private one would show no progress and swallow every error — the mechanism working and the feedback not, which is the failure mode this flight keeps finding. PosterWall hands the composable its own ref so both write to the surface the reader is looking at. Prints IN PLACE via the shared openPrintFrame, never window.open() — the mechanism the Driver asked for by name: "not supposed to open up a new browser, but should open up the print dialog directly in the browser". NOT DONE, and deliberately: the 100-list wall is NOT yet migrated onto the shared component. Its template is 107 lines against the shared 72 with a different structure, and it is a Driver-reviewed surface — that is a rewrite whose risk does not fit in one tick alongside this. The shared half lands first and is verified; the migration stays open on the card with this composable now waiting for it. Verified: all three bundles on the shared wall build; country-poster-wall renders 175 cards with zero page errors; check:poster-prints drives the composable in a browser and asserts BOTH directions. --- library/components/poster/PosterWall.vue | 58 +++++- library/composables/usePosterPrints.js | 168 +++++++++++++++++ .../country-posters/countries-map/index.html | 2 +- .../country-poster-wall/index.html | 2 +- .../country-posters-mfe/index.html | 2 +- .../country-posters/country-posters.js | 175 ++++++++++++------ public/doxa-maps/index.html | 2 +- public/doxa-maps/key-cities/key-cities.js | 91 +++++++-- public/doxa-maps/manifest.json | 16 +- .../region-dashboards/index.html | 2 +- .../regions-dashboard/regions-dashboard.js | 117 +++++++++--- .../regions-dashboard/regions-mfe/index.html | 2 +- 12 files changed, 519 insertions(+), 118 deletions(-) create mode 100644 library/composables/usePosterPrints.js diff --git a/library/components/poster/PosterWall.vue b/library/components/poster/PosterWall.vue index 7a2068462..3c0e77771 100644 --- a/library/components/poster/PosterWall.vue +++ b/library/components/poster/PosterWall.vue @@ -41,6 +41,8 @@ */ import { computed, ref } from 'vue' import { useShadowStyles } from '@map/composables/useShadowStyles.js' +import { usePosterPrints } from '@map/composables/usePosterPrints.js' +import { useI18n } from 'vue-i18n' // Export-modal geometry. A fixed;inset:0 scrim covers only the CONTENT PANE in the cap-5 shell // (contain:layout on .vtl-content). Measured 1024x793 @ (256,107) at 1280x900 — rail clickable // behind the "modal". See library/utils/viewportScrim.js for the measurements and rejected fixes. @@ -77,6 +79,17 @@ const props = defineProps({ // prayer list for each country generated.") prayerListSoonMessage: { type: String, default: 'Coming soon — the prayer list for each people group is in build.' }, + // ★ RECORDS PROVIDER — the seam that makes the print actions shareable. + // The PRINT ACTIONS are common to every wall; the RECORD FETCHING is not. The + // 100-list resolves its rows through the people-groups API, the selector + // evaluator and a pg_slugs special case; Key Cities and Country Posters have + // entirely different data shapes. So a bundle that CAN supply rows passes this + // and gets the list + card-deck prints; one that cannot simply omits it and + // those buttons stay hidden, rather than failing at click time. + records: { type: Function, default: null }, + // Card stock for the deck print, when this wall offers one. See card-render.js + // STOCK — both orientations are available. + cardStock: { type: String, default: 'poker' }, // EMBEDDED mode (added 2026-09-24 for the regions-tab poster dropdown). The wall's root is // `position:absolute;inset:0` because in country-posters and my-upg-100-list it IS a pane and // fills it. Dropped inside another component — a region card, say — that absolute root covers @@ -208,11 +221,43 @@ async function exportPoster(item) { } } +// ── SHARED PRINT ACTIONS ────────────────────────────────────────────────── +// The list and card-deck prints live in the shared composable so every wall gets +// the same documents. They need ROWS, which only the bundle can fetch — hence the +// `records` prop. Without it the buttons are hidden and this stays honest rather +// than offering an action that cannot work. +// The wall follows the app's locale: the card deck and its QR codes are localised, +// and packet-strings.json falls back per key. +const { locale: wallLocale } = useI18n() +// ⚠ ONE exportState, not two. The composable owns a ref of its own; if the HUD +// keeps rendering the wall's while the print actions write to the composable's, a +// card-deck print shows NO progress and its errors vanish silently — the mechanism +// works and the feedback does not. Hand the composable this wall's ref so both +// write to the surface the reader is actually looking at. +const prints = usePosterPrints({ + state: exportState, + records: props.records ? (item) => props.records(item) : null, + // useI18n()'s locale can be a ref or a plain string depending on how the host + // installed it, so normalise rather than assume. + lang: () => { + const l = wallLocale && (wallLocale.value ?? wallLocale) + return (typeof l === 'string' && l) || 'en' + }, + stock: () => props.cardStock, + label: (item) => item?.label || item?.id || '', +}) +const canPrintRows = computed(() => typeof props.records === 'function') + function downloadPacket(item) { - // NOT an error — unbuilt, not broken. phase:'error' reads to a user as - // "something went wrong" rather than "not yet". - exportState.value = { label: item.label, kind: 'Prayer List', phase: 'soon', - message: props.prayerListSoonMessage, pct: 0 } + // Still the honest "not yet" when this wall has no records provider: NOT an + // error — unbuilt, not broken. phase:'error' reads to a user as "something went + // wrong" rather than "not yet". + if (!canPrintRows.value) { + exportState.value = { label: item.label, kind: 'Prayer List', phase: 'soon', + message: props.prayerListSoonMessage, pct: 0 } + return + } + prints.printList(item) } function download(blob, name) { @@ -422,6 +467,11 @@ useShadowStyles(
diff --git a/library/composables/usePosterPrints.js b/library/composables/usePosterPrints.js new file mode 100644 index 000000000..34741dfc8 --- /dev/null +++ b/library/composables/usePosterPrints.js @@ -0,0 +1,168 @@ +// usePosterPrints.js — THE print actions every poster wall shares. +// +// ★ WHY THIS EXISTS. The poster wall was generalised into +// library/components/poster/PosterWall.vue and two bundles were migrated onto it — +// but the ORIGINAL, my-upg-100-list/poster-wall.vue (903 lines), was left behind. +// So the one wall carrying the real print actions is the one wall nothing else can +// benefit from, and the shared component's packet path is still a "coming soon" +// stub. Every print fix made in the 100-list is invisible to the three bundles on +// the shared component, and vice versa. That divergence is the bug. +// +// ── THE SEAM ──────────────────────────────────────────────────────────────── +// The PRINT ACTIONS are shareable; the RECORD FETCHING is not. fetchListRecords in +// the 100-list knows the people-groups API, the selector evaluator and the pg_slugs +// special case; Key Cities and Country Posters have entirely different data shapes. +// So this composable takes a `records(item)` PROVIDER and owns everything after it: +// the HUD state machine, the in-place print frame, and the three documents. +// +// That is also what keeps the bundles honest — a wall that cannot supply records +// simply does not pass a provider, and the actions that need them stay hidden +// rather than failing at click time. +// +// ⚠ PRINTS IN PLACE, never window.open(). A popup is blockable and drops the reader +// out of the map; the shared openPrintFrame mounts a hidden same-page iframe. This +// is the mechanism the Driver asked for by name: "not supposed to open up a new +// browser, but should open up the print dialog directly in the browser". + +import { ref } from 'vue'; +import { openPrintFrame } from './printInPlace.js'; + +const esc = (s) => String(s ?? '') + .replace(/&/g, '&').replace(//g, '>') + .replace(/"/g, '"').replace(/'/g, '''); + +/** + * @param {object} opts + * records async (item) => rows[] — REQUIRED for the list and deck prints. + * strings () => Promise