diff --git a/plugin-designs/wall-of-people-groups/src/blessPoster.js b/plugin-designs/wall-of-people-groups/src/blessPoster.js
new file mode 100644
index 000000000..63a32b1ea
--- /dev/null
+++ b/plugin-designs/wall-of-people-groups/src/blessPoster.js
@@ -0,0 +1,79 @@
+/**
+ * blessPoster.js — the "Bless the nations in Jesus' name" poster: every people group (photo, name and a
+ * blessing verse) on ONE 8 × 5 ft poster, printed as THREE 3 × 5 ft sheets that overlap by 6 inches.
+ *
+ * 3 + 2.5 + 2.5 = 8 ft wide, 5 ft tall: sheet 2 is glued over the right-hand 6 in of sheet 1, and sheet 3
+ * over the right-hand 6 in of sheet 2. Each sheet is a 36-inch WINDOW onto one 96 × 60 in layout, so the
+ * content in an overlap strip is identical on both sheets and registers when glued.
+ *
+ * Pure: layout maths + an HTML string for the shared in-place print frame (library printInPlace). No Vue.
+ * Only the cards that touch a sheet are placed on it, so the print document holds ~2,300 cards in total,
+ * not 3 × 2,107.
+ */
+import { blessingFor, POSTER } from './blessings.js'
+
+export const SHEET = { w: 36, h: 60, overlap: 6, count: 3 } // inches
+export const POSTER_W = SHEET.w * SHEET.count - SHEET.overlap * (SHEET.count - 1) // 96
+export const POSTER_H = SHEET.h // 60
+
+const MARGIN = 0.5, BANNER_H = 5.2, FOOTER_H = 3
+
+/** Where each of n cards goes, in inches on the 96 × 60 poster. */
+export function posterLayout(n, { aspect = 1.18 } = {}) {
+ const gx = MARGIN, gy = MARGIN + BANNER_H, gw = POSTER_W - 2 * MARGIN, gh = POSTER_H - 2 * MARGIN - BANNER_H - FOOTER_H
+ // Columns so the cells come out near the target width/height aspect, then enough rows for n.
+ let cols = Math.max(1, Math.round(Math.sqrt((n * gw) / (gh * aspect))))
+ let rows = Math.ceil(n / cols)
+ while (rows * (gw / cols / aspect) > gh && cols < n) { cols++; rows = Math.ceil(n / cols) }
+ const cw = gw / cols, ch = Math.min(gh / rows, cw / aspect * 1.25)
+ const cards = []
+ for (let i = 0; i < n; i++) cards.push({ i, x: gx + (i % cols) * cw, y: gy + Math.floor(i / cols) * ch, w: cw, h: ch })
+ return { cols, rows, cw, ch, grid: { x: gx, y: gy, w: gw, h: gh }, cards }
+}
+
+/** The x-window (in poster inches) that sheet s prints. */
+export const sheetX = (s) => s * (SHEET.w - SHEET.overlap)
+
+const esc = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
+
+/** The full print document: 3 pages of 36 × 60 in. groups: [{ name, imageUrl }]. */
+export function buildBlessPosterHtml(groups, { footer = POSTER.footer, banner = POSTER.banner, subBanner = POSTER.subBanner } = {}) {
+ const L = posterLayout(groups.length)
+ const f = (v) => v.toFixed(3) + 'in'
+ const card = (c) => {
+ const g = groups[c.i], b = blessingFor(c.i)
+ const img = g.imageUrl ? `
` : '
'
+ return ``
+ + `
${esc(g.name || '')}
${img}
${esc(b.text)}
${esc(b.ref)}
`
+ }
+ const sheets = []
+ for (let s = 0; s < SHEET.count; s++) {
+ const x0 = sheetX(s), x1 = x0 + SHEET.w
+ const onSheet = L.cards.filter((c) => c.x < x1 && c.x + c.w > x0)
+ const glue = s < SHEET.count - 1
+ ? `▶ ${s + 2} / ${SHEET.count}
` : ''
+ sheets.push(``
+ + `
`
+ + onSheet.map(card).join('')
+ + `
`
+ + glue + `
${s + 1} / ${SHEET.count}
`)
+ }
+ const css = `@page{size:${SHEET.w}in ${SHEET.h}in;margin:0}
+*{box-sizing:border-box}html,body{margin:0;padding:0}
+body{font-family:Georgia,'Times New Roman',serif;color:#1c1c1c;-webkit-print-color-adjust:exact;print-color-adjust:exact}
+.sheet{position:relative;width:${SHEET.w}in;height:${SHEET.h}in;overflow:hidden;page-break-after:always;break-after:page}
+.poster{position:absolute;top:0;width:${POSTER_W}in;height:${POSTER_H}in}
+.ban{position:absolute;left:${MARGIN}in;top:${MARGIN}in;width:${POSTER_W - 2 * MARGIN}in;height:${BANNER_H}in;text-align:center}
+.ban h1{margin:0;font-size:2.35in;line-height:1.05;letter-spacing:.02em;font-weight:700}
+.ban p{margin:.25in 0 0;font-size:.62in;font-style:italic;color:#444}.ban p span,.ftr p span{font-style:normal;font-size:.7em;color:#777}
+.card{position:absolute;padding:.04in;display:flex;flex-direction:column;align-items:center;text-align:center;overflow:hidden}
+.card .nm{font:700 .105in/1.1 -apple-system,'Segoe UI',Arial,sans-serif;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.card img,.card .noimg{width:100%;height:58%;object-fit:cover;margin:.02in 0;background:#eee;flex:none}
+.card .vs{font-size:.068in;line-height:1.15;font-style:italic}
+.card .rf{font-size:.056in;color:#777;margin-top:.01in}
+.ftr{position:absolute;left:${MARGIN}in;bottom:${MARGIN}in;width:${POSTER_W - 2 * MARGIN}in;height:${FOOTER_H - 0.4}in;display:flex;align-items:center;justify-content:center;text-align:center}
+.ftr p{margin:0;font-size:.95in;font-style:italic}
+.glue{position:absolute;top:0;width:${SHEET.overlap}in;height:${POSTER_H}in;border-left:.02in dashed #b00;display:flex;align-items:flex-end;justify-content:center;padding-bottom:.2in;font:700 .3in sans-serif;color:#b00;background:rgba(176,0,0,.03)}
+.tag{position:absolute;right:.15in;bottom:.1in;font:600 .18in sans-serif;color:#999}`
+ return `${esc(banner)}${sheets.join('')}`
+}
diff --git a/plugin-designs/wall-of-people-groups/src/components/PeopleGroupsWall.vue b/plugin-designs/wall-of-people-groups/src/components/PeopleGroupsWall.vue
index 33b205f9c..7db8dbb54 100644
--- a/plugin-designs/wall-of-people-groups/src/components/PeopleGroupsWall.vue
+++ b/plugin-designs/wall-of-people-groups/src/components/PeopleGroupsWall.vue
@@ -9,7 +9,7 @@
-
+
@@ -44,6 +44,8 @@
import { lodPlan } from '../wallLod.js'
import { wallD3 } from '../wallD3.js'
import { blessingFor, wrapText } from '../blessings.js'
+import { buildBlessPosterHtml } from '../blessPoster.js'
+import { openPrintFrame } from '@map/composables/printInPlace.js'
// Wall of People Groups - D3 Zoomable Whiteboard
export default {
name: 'PeopleGroupsWall',
@@ -472,6 +474,20 @@ export default {
},
+ // BLESS prints the 8 × 5 ft poster (3 sheets of 3 × 5 ft, 6-inch overlaps: blessPoster.js);
+ // SOCIAL keeps the HD print of the wall as it is on screen.
+ onPrint() { return this.mode === 'bless' ? this.printBlessPoster() : this.printWall(); },
+ printBlessPoster() {
+ if (this.printing) return;
+ const groups = (this.peopleGroups || []).map((d) => ({ name: d.name || d.post_title || '', imageUrl: d.imageUrl || '' }));
+ if (!groups.length) return;
+ this.printing = true;
+ const win = openPrintFrame(buildBlessPosterHtml(groups), { autoprint: true });
+ // Busy until the photos have loaded and the print dialog is up (the frame prints on its own 'load').
+ const done = () => { this.printing = false; };
+ if (win) { try { win.addEventListener('load', () => setTimeout(done, 300), { once: true }); } catch (_) { done(); } setTimeout(done, 120000); }
+ else done();
+ },
setMode(m) {
if (this.mode === m) return;
this.mode = m;