From a331f1f20eb88928ec2d05cd5c8b8c18d8954d62 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 23:33:54 +0000 Subject: [PATCH] Wall of People Groups: nothing on the poster is cut off, and the size list reads the same in Arabic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chart's rotated labels are sized from their own text and wrap; its theme headers fit their row. A long group name shrinks to fit (never below 6 pt) or takes two lines, and the photo gives way so the card's content never outgrows the card. The verse reference prints at 6 pt. The size labels are isolated left to right, so an RTL page no longer reorders '5 × 8 ft · 3×'. --- .../wall-of-people-groups/src/blessPoster.js | 52 ++++++++++++++----- .../src/components/PeopleGroupsWall.vue | 9 ++-- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/plugin-designs/wall-of-people-groups/src/blessPoster.js b/plugin-designs/wall-of-people-groups/src/blessPoster.js index b9a7402cb..96da8d649 100644 --- a/plugin-designs/wall-of-people-groups/src/blessPoster.js +++ b/plugin-designs/wall-of-people-groups/src/blessPoster.js @@ -108,14 +108,38 @@ export function buildBlessPosterHtml(groups, presetId = DEFAULT_PRESET, texts = const upright = /^(zh|ja|ko)/.test(String(texts.lang || '')) // CJK sidebar labels stand upright const glueHere = texts.glueHere || 'glue here' const f = (v) => v.toFixed(3) + 'in' + const W = plan.contentW const vPt = plan.text.versePt, nPt = plan.text.namePt + // The reference prints at the verse's size, never below MIN_READABLE_PT (a reference that can't be read + // is noise); it is only printed on sizes whose verses are readable. + const rPt = Math.max(MIN_READABLE_PT, vPt * 0.8) + // A long name shrinks to fit its line (bold ≈ 0.68 em a character, measured wide on purpose), down to MIN_READABLE_PT (or the + // preset's own name size if that is smaller); a name still too long wraps to two lines. + const nameInnerPt = plan.cw * 0.93 / PT + const nameFit = (name) => { + const want = nameInnerPt / (Math.max(1, name.length) * 0.68) + const pt = Math.max(Math.min(nPt, MIN_READABLE_PT), Math.min(nPt, want)) + return { pt: +pt.toFixed(1), wrap: want < pt } + } + // The rotated sidebar labels of the chart are sized from their text: the longest word must fit the row's + // height (a CJK label stands upright, one em a character; other scripts ≈ 0.72 em a capital with the + // letter spacing), and the wrapped lines must fit the sidebar's width. + const railH = plan.tableH * 0.33 - W * 0.006, railW = W * 0.06 - W * 0.004 + const railFs = (text) => { + const words = String(text).split(/\s+/), em = upright ? 1.02 : 0.72 + const longest = Math.max(...words.map((w) => w.length)) * em + const byH = railH / longest, byW = railW / (Math.min(words.length, 3) * 1.2) + return Math.min(W * 0.0105, byH, byW) + } + const railLabel = (text) => `${esc(text)}` const card = (c) => { const g = groups[c.i], v = blessingFor(c.i) const img = g.imageUrl ? `` : '
' // Below MIN_READABLE_PT the verse would print as grey noise: that size shows name + photo only. const verse = plan.text.readable ? `
${esc(v.text)}
${esc(v.ref)}
` : '' + const nf = nameFit(g.name || '') return `
` - + `
${esc(g.name || '')}
${img}${verse}
` + + `
${esc(g.name || '')}
${img}${verse}` } const pages = plan.bands.map((bd) => { const s = bd.s, last = s === plan.bands.length - 1 @@ -127,11 +151,11 @@ export function buildBlessPosterHtml(groups, presetId = DEFAULT_PRESET, texts = // every scripture, name and reference on the sheet (cards, chart, blessing, closing). const cell = (v) => `

${esc(v.text)}

${esc(v.ref)}

` const table = `
` - + `
${esc(lab('themesLabel', 'Prayer themes'))}
` + + `
${railLabel(lab('themesLabel', 'Prayer themes'))}
` + THEMES.map((t) => `
${ICONS[t.id]}
${esc(lab('theme_' + t.id, THEME_EN[t.id]))}
`).join('') - + `
${esc(lab('oldTestament', 'Old Testament'))}
` + + `
${railLabel(lab('oldTestament', 'Old Testament'))}
` + THEMES.map((t) => `
${cell(t.ot)}
`).join('') - + `
${esc(lab('newTestament', 'New Testament'))}
` + + `
${railLabel(lab('newTestament', 'New Testament'))}
` + THEMES.map((t) => `
${cell(t.nt)}
`).join('') + '
' const head = bd.bannerY != null @@ -149,7 +173,6 @@ export function buildBlessPosterHtml(groups, presetId = DEFAULT_PRESET, texts = return `
${glue}${head}${plan.cards.filter((c) => c.s === s).map(card).join('')}${foot}${trim}` + `
${s + 1} / ${P.sheets}
` }) - const W = plan.contentW const css = `@page{size:${P.sheetW}in ${P.sheetH}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} @@ -160,11 +183,11 @@ body{font-family:Georgia,'Times New Roman',serif;color:#1c1c1c;-webkit-print-col .hd .subt{margin:${f(plan.headH * 0.06)} auto 0;max-width:78%;font:${f(plan.headH * 0.085)}/1.35 -apple-system,'Segoe UI',Arial,sans-serif;color:#333} .themes{display:grid;grid-template-columns:${f(W * 0.06)} repeat(3,1fr);grid-template-rows:34% 33% 33%;height:100%;border:.03in solid #1c1c1c;border-radius:${f(W * 0.006)};overflow:hidden} .themes > div{border:.015in solid #cfcfcf;padding:${f(W * 0.006)} ${f(W * 0.008)};overflow:hidden} -.rl{display:flex;align-items:center;justify-content:center;background:#1c1c1c;color:#fff} -.rl span{writing-mode:vertical-rl;${upright ? 'text-orientation:upright;' : 'transform:rotate(180deg);'}font:700 ${f(W * 0.0105)} -apple-system,'Segoe UI',Arial,sans-serif;letter-spacing:.06em;text-transform:uppercase;white-space:nowrap} -.th{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:${f(W * 0.004)};background:#faf8f2} -.th .ic{width:${f(W * 0.05)};height:${f(W * 0.05)};color:#6b4f1d}.th .ic svg{width:100%;height:100%} -.th .tn{font:700 ${f(W * 0.014)} Georgia,serif;letter-spacing:.04em;text-transform:uppercase} +.themes > .rl{display:flex;align-items:center;justify-content:center;background:#1c1c1c;color:#fff;padding:${f(W * 0.002)}} +.rl span{writing-mode:vertical-rl;${upright ? 'text-orientation:upright;' : 'transform:rotate(180deg);'}font-weight:700;font-family:-apple-system,'Segoe UI',Arial,sans-serif;line-height:1.15;letter-spacing:.06em;text-transform:uppercase;text-align:center} +.themes > .th{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:${f(W * 0.003)};background:#faf8f2;padding-top:${f(W * 0.003)};padding-bottom:${f(W * 0.003)}} +.th .ic{width:${f(W * 0.028)};height:${f(W * 0.028)};color:#6b4f1d;flex:none}.th .ic svg{width:100%;height:100%} +.th .tn{font:700 ${f(W * 0.012)}/1.1 Georgia,serif;letter-spacing:.04em;text-transform:uppercase} .td{display:flex;flex-direction:column;justify-content:center;text-align:center} .td .tv{margin:0;font-size:${f(W * 0.0078)};line-height:1.3;font-style:italic} .td .tr{margin:${f(W * 0.002)} 0 0;font-size:${f(W * 0.0062)};color:#666} @@ -179,10 +202,13 @@ body{font-family:Georgia,'Times New Roman',serif;color:#1c1c1c;-webkit-print-col .st.ctr{background:#faf8f2;border:.03in solid #1c1c1c}.st.ctr .sv{font-size:${f(W * 0.0064)};font-weight:600}.st.ctr .sn{color:#1c1c1c} .close{margin:${f(W * 0.008)} auto 0;max-width:80%;font:700 ${f(W * 0.0085)}/1.35 -apple-system,'Segoe UI',Arial,sans-serif} .card{position:absolute;padding:${f(plan.cw * 0.035)};display:flex;flex-direction:column;align-items:center;text-align:center;overflow:hidden} -.card .nm{font:700 ${nPt}pt/1.1 -apple-system,'Segoe UI',Arial,sans-serif;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:none} -.card img,.card .noimg{width:100%;height:${plan.text.readable ? 52 : 78}%;object-fit:cover;margin:${f(plan.cw * 0.02)} 0;background:#eee;flex:none} +.card .nm{font-weight:700;line-height:1.1;font-family:-apple-system,'Segoe UI',Arial,sans-serif;width:100%;white-space:nowrap;overflow:hidden;flex:none} +.card .nm.w2{white-space:normal;overflow-wrap:anywhere;hyphens:auto;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical} +/* The photo takes whatever height the text leaves, so a card's content never outgrows the card. */ +.card img,.card .noimg{width:100%;flex:1 1 0;min-height:0;object-fit:cover;margin:${f(plan.cw * 0.02)} 0;background:#eee} .card .vs{font-size:${vPt}pt;line-height:1.15;font-style:italic} -.card .rf{font-size:${(vPt * 0.8).toFixed(1)}pt;color:#777;margin-top:1pt} +.card .vs,.card .rf{flex:none} +.card .rf{font-size:${rPt.toFixed(1)}pt;color:#777;margin-top:1pt} .glue{position:absolute;left:0;width:100%;border-bottom:.02in dashed #b00;display:flex;align-items:center;justify-content:center;font:700 ${f(Math.min(0.5, b * 0.08))} sans-serif;color:#b00;background:rgba(176,0,0,.035);letter-spacing:.05em;text-transform:uppercase} .trim{position:absolute;left:0;bottom:0;width:100%;border-bottom:.02in solid #999} .tag{position:absolute;right:${f(Math.max(0.15, b * 0.1))};top:${f(Math.max(0.1, b * 0.08))};font:600 ${f(Math.max(0.12, b * 0.035))} sans-serif;color:#aaa}` 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 d6f3e4f96..ac8337779 100644 --- a/plugin-designs/wall-of-people-groups/src/components/PeopleGroupsWall.vue +++ b/plugin-designs/wall-of-people-groups/src/components/PeopleGroupsWall.vue @@ -59,7 +59,9 @@ export default { return { mode: 'bless', // 'bless' (default) | 'social' posterPreset: DEFAULT_PRESET, // blessPoster.js PRESETS: 5×8 ft (3 sheets) by default - posterPresets: Object.entries(PRESETS).map(([id, p]) => ({ id, label: p.label + (p.sheets > 1 ? ' · ' + p.sheets + '×' : '') })), + // Each label is isolated left to right (U+2066 … U+2069): "5 × 8 ft · 3×" would otherwise be reordered + // by an RTL page into "×ft · 3 8 × 5" (an