diff --git a/plugin-designs/poster-design/poster-styles.vue b/plugin-designs/poster-design/poster-styles.vue
index d1552bb19..e27a14009 100644
--- a/plugin-designs/poster-design/poster-styles.vue
+++ b/plugin-designs/poster-design/poster-styles.vue
@@ -25,6 +25,8 @@ import countryData from '@map/data/country-posters.json'
import { makeCountrySheet } from '@map/packet/countrySheet.js'
import { loadPacketStrings } from '@map/packet/packetStrings.js'
import { firstPageImage } from './src/pageThumb.js'
+import { panelsSvgUrl } from './src/panelsSvg.js'
+import { resolvePosterSize } from '@map/constants/posterSizes.js'
import { listConfigurations, FAMILY_KEYS, SAMPLE_COUNTRY_ID } from './src/templates.js'
import FullDetailsDialog from './src/FullDetailsDialog.vue'
@@ -96,7 +98,12 @@ async function packetDocument(variant) {
const doc = R.buildPacket(sample, rows, new Map(), mapSvg, null, null, variant, null)
return { html: doc, page: '.sheet', width: R.W, height: R.H, firstPageOnly: true }
}
-const ctx = { countrySheet, cardDeck, packetDocument }
+// A multi-sheet poster as its panels, from the shared ladder's sheets for that size.
+function panels(sizeKey) {
+ const size = resolvePosterSize(sizeKey)
+ return size ? { image: panelsSvgUrl(size.sheets) } : null
+}
+const ctx = { countrySheet, cardDeck, packetDocument, panels }
// The printed document's size, measured when it is drawn: what "file size" means in the details.
// render() answers a poster's HTML (a map sheet) or a document { html, page, width, height } (a deck, a packet).
@@ -105,6 +112,7 @@ async function renderDocument(name) {
const c = byName.get(name)
if (!c || !c.render) return null
const out = await c.render(ctx)
+ if (out && out.image) return out
const html = typeof out === 'string' ? out : out && out.html
if (!html) return null
if (!(out && out.firstPageOnly)) fileBytes.value = { ...fileBytes.value, [name]: new Blob([html]).size }
@@ -117,6 +125,7 @@ async function renderDocument(name) {
async function previewSrc(item) {
const out = await renderDocument(item.id)
if (!out) return null
+ if (out.image) return out.image
if (typeof out === 'object') return firstPageImage(out.html, out)
const { rasterPoster, looksRendered } = await import('@map/packet/poster-raster.js')
const blob = await rasterPoster(out, { scale: 0.25 })
@@ -130,7 +139,7 @@ async function previewSrc(item) {
// packet missing its later pages would misrepresent the template.
async function sheet(item) {
const out = await renderDocument(item.id)
- if (!out || (typeof out === 'object' && out.firstPageOnly)) return ''
+ if (!out || out.image || (typeof out === 'object' && out.firstPageOnly)) return ''
return typeof out === 'string' ? out : out.html
}
diff --git a/plugin-designs/poster-design/src/panelsSvg.js b/plugin-designs/poster-design/src/panelsSvg.js
new file mode 100644
index 000000000..21723cdd7
--- /dev/null
+++ b/plugin-designs/poster-design/src/panelsSvg.js
@@ -0,0 +1,69 @@
+/**
+ * panelsSvg.js: a poster printed on several sheets, drawn as the sheets pulled apart, each join a red glue
+ * band, as one SVG image for a poster card's thumbnail.
+ *
+ * The geometry is the shared panel plan (library/components/poster/panelSheets.js planPanels + screenLayout):
+ * the same sheets, joins and content boxes the print uses. Inside each sheet's content box sit rows of cards,
+ * the way the Bless wall fills its 5 × 8 ft panels: a banner on the first sheet, a footer on the last, and
+ * card rows that never cross a join. The cards are a LAYOUT (blocks, no words): the Bless wall draws its real
+ * cards in its own plugin, which a library-only gallery cannot import.
+ */
+import { planPanels, screenLayout } from '@map/components/poster/panelSheets.js'
+
+const PAPER = '#f7f4ec'
+const INK = '#3b463d'
+const ACCENT = '#6B4A78' // the Bless wall's purple
+const PHOTO = '#e9e3d4'
+const LINE = '#c9c3b3'
+
+/**
+ * @param {{ sheetW, sheetH, count, direction, joinIn }} sheets a size's `sheets` from the shared ladder
+ * @param {{ borderIn?: number, gapIn?: number, cols?: number }} o
+ * @returns {string} an image URL (a data: URL of an SVG), drawn in inches
+ */
+export function panelsSvgUrl(sheets, { borderIn = sheets.joinIn || 2, gapIn = 0.75, cols = 14 } = {}) {
+ const plan = planPanels({ ...sheets, borderIn, joinFill: 'glue' })
+ const L = screenLayout(plan, { gapIn, pxPerIn: 1 })
+ const W = L.worldW, H = L.worldH
+ const s1 = Math.min(plan.sheetW, plan.sheetH)
+ const stroke = s1 / 200
+ let out = ''
+ for (const sh of plan.sheets) {
+ const at = L.sheets[sh.s]
+ const ox = at.x, oy = at.y
+ out += ``
+ const c = sh.content
+ let top = oy + c.y0, bottom = oy + c.y1
+ const cw0 = c.x1 - c.x0
+ if (sh.first) { // the banner: a title band and the prayer-themes table
+ const bh = cw0 * 0.075, th = cw0 * 0.17
+ out += ``
+ out += ``
+ top += bh + th + cw0 * 0.025
+ }
+ if (sh.last) { // the footer: the blessing and the closing line
+ const fh = cw0 * 0.09
+ out += ``
+ bottom -= fh + cw0 * 0.015
+ }
+ // rows of portrait cards (0.8 aspect), centred in the band, never crossing a join
+ const cw = cw0 / cols, ch = cw / 0.8
+ const rows = Math.max(0, Math.floor((bottom - top) / ch))
+ const y0 = top + Math.max(0, (bottom - top) - rows * ch) / 2
+ for (let r = 0; r < rows; r++) {
+ for (let k = 0; k < cols; k++) {
+ const x = ox + c.x0 + k * cw, y = y0 + r * ch
+ out += ``
+ out += ``
+ }
+ }
+ if (sh.band) { // the glue band: red, dashed at its edge, as PanelSheets draws it
+ const b = sh.band
+ const ex = plan.direction === 'vertical' ? `x1="${ox}" y1="${oy + b.h}" x2="${ox + b.w}" y2="${oy + b.h}"` : `x1="${ox + b.w}" y1="${oy}" x2="${ox + b.w}" y2="${oy + b.h}"`
+ out += ``
+ out += ``
+ }
+ }
+ const svg = ``
+ return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
+}
diff --git a/plugin-designs/poster-design/src/templates.js b/plugin-designs/poster-design/src/templates.js
index d29fd613d..c00cd5da1 100644
--- a/plugin-designs/poster-design/src/templates.js
+++ b/plugin-designs/poster-design/src/templates.js
@@ -10,7 +10,8 @@
* render(ctx) draws the template from the LIBRARY with the sample data, so a library change shows up in
* the gallery the next time it opens. It returns either a poster's HTML (a map sheet: an