From f22dc7db00f0bd916f377ebdea2dc104d3072e1d Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Fri, 2 Oct 2026 23:38:26 +0000 Subject: [PATCH] UPG100 packet: points on one sheet, page-1 columns reordered - The twelve prayer points and the themes fit one sheet again, as large as the page allows (points 18.5px, themes 19px); measured in all 11 languages, the tightest keeps 75px under the last point. - Page 1 header columns read: Jesus' Strategy to Feed the 5000 | the facts | What is a UPG100 List? The facts' labels are bold. - The header columns are measured for a fallback font too, so the right column no longer runs to the border when Lato is absent. - check:packet-render expects one points sheet. --- internal/check-packet-render.mjs | 8 ++++---- library/packet/packet-render.js | 34 +++++++++++++------------------- library/packet/poster-render.js | 23 ++++++++++++--------- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/internal/check-packet-render.mjs b/internal/check-packet-render.mjs index ad43cf9cf..3932ff89a 100644 --- a/internal/check-packet-render.mjs +++ b/internal/check-packet-render.mjs @@ -279,7 +279,7 @@ const r = await page.evaluate(() => { return Math.round(Math.max(0, ...[...b.children].map((k) => k.getBoundingClientRect().bottom + parseFloat(getComputedStyle(k).marginBottom))) - top - b.clientHeight) })(), // The points page is ONE sheet by design and fits only because its sizes were tuned to the longest locale; // measure its content against its box (studio's ask), as fdTopOver does for the list's first page. - // The points flow over TWO sheets (2026-10-02); each sheet's body is measured, the worst one reported. + // Each points sheet's body is measured, the worst one reported (one sheet today). pointsOver: (() => { const bs = [...document.querySelectorAll('.points-body')]; if (!bs.length) return null return Math.max(...bs.map((b) => { const top = b.getBoundingClientRect().top return Math.round(Math.max(b.scrollHeight - b.clientHeight, ...[...b.children].map((k) => k.getBoundingClientRect().bottom - top - b.clientHeight))) })) })(), @@ -786,9 +786,9 @@ if (r.fdTopOver > 0) fails.push(`the Full detail's explanation block outgrows it if (r.wsRows && r.wsSheets !== wsPages(r.wsRows)) fails.push(`${r.wsRows} worksheet rows should take ${wsPages(r.wsRows)} page(s) but the packet has ${r.wsSheets}`) // + 2 again (2026-09-30, the second revision): point 4's Mark 5:18-20 and the Father's-heart list are back ON the // points page, so the back matter is the wheel and the points. -// + 3 (2026-10-02): the wheel, and the points on TWO sheets (themes + points 1–6, then 7–12), read at print size. -if (r.pointsSheets !== 2) fails.push(`the prayer points should flow over 2 sheets, found ${r.pointsSheets}`) -const expectedSheets = 1 + lifeSheets + funnelSheets + listSheets + setsSheets + 3 +// + 2: the wheel and the points, the twelve points and the themes on ONE sheet (2026-10-02, at the demo). +if (r.pointsSheets !== 1) fails.push(`the twelve prayer points and the themes should fit ONE sheet, found ${r.pointsSheets}`) +const expectedSheets = 1 + lifeSheets + funnelSheets + listSheets + setsSheets + 2 // ── THE LIFE-CALENDAR PAGES, asserted rather than merely counted ────────────────────────────────── // A page that EXISTS is not a page that is RIGHT: an empty grid, a grid of the wrong size, or a // christian page whose shading silently fell to zero would all still be one sheet. 5,200 is the diff --git a/library/packet/packet-render.js b/library/packet/packet-render.js index 34b900f26..803a0e2a5 100644 --- a/library/packet/packet-render.js +++ b/library/packet/packet-render.js @@ -287,12 +287,12 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de .xb-ex-note{margin-top:4px;font-size:14px;color:#6f6a5c} /* The prayer themes and scenes above the twelve points: a thin numbered list, ruled off below. */ .pt-themes{margin:0 0 10px;padding:0 0 6px;border-bottom:1.5px solid #c9bfa4} - .pt-th-h{font:400 21px/1.3 Georgia,serif;color:#413d35} - .pt-th-h b,.pt-sc-h b{font:700 22px Georgia,serif;color:#1b1a17;letter-spacing:.02em} + .pt-th-h{font:400 19px/1.3 Georgia,serif;color:#413d35} + .pt-th-h b,.pt-sc-h b{font:700 20px Georgia,serif;color:#1b1a17;letter-spacing:.02em} .pt-th-i{font-style:italic;color:#6d766e} - .pt-th-list{display:flex;flex-wrap:wrap;gap:1px 20px;margin:2px 0 0;padding:0;list-style:none;font:400 21px/1.3 Georgia,serif;color:#3b3226} + .pt-th-list{display:flex;flex-wrap:wrap;gap:1px 20px;margin:2px 0 0;padding:0;list-style:none;font:400 19px/1.3 Georgia,serif;color:#3b3226} .pt-n{display:inline-block;min-width:1.3em;font-weight:700;color:#7a3b2e} - .pt-sc-r{font:600 19px Georgia,serif;color:#7a3b2e} + .pt-sc-r{font:600 18px Georgia,serif;color:#7a3b2e} .ws-cols{margin:0;padding:0;list-style:none} .ws-cols li{margin:0 0 6px;font:400 19px/1.45 Georgia,serif;color:#413d35} .ws-cols li b{color:#1b1a17} @@ -1352,13 +1352,11 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de // Mark 5:18-20 and the Father's-heart list share this ONE sheet, in every locale: the sizes below are the // largest that fit the longest locale. class="points-body" is a TEST HOOK: the page must fit ONE sheet in // every locale (its scrollHeight within its clientHeight). - // ★ READABLE ON PAPER, ON TWO SHEETS (2026-10-02): the point text at 22.5px (~9.2pt on letter), the themes at - // 21px (~8.6pt), the Mark 5 passage at 20px; that no longer fits one sheet, so the points FLOW onto a second: the - // themes and points 1–6 (point 4 carries its passage and the Father's-heart list) on the first, 7–12 on the - // second, which carries the credit line. Measured in all 11 locales: the fullest first sheet (fr, de) keeps - // ~350px; check:packet-render fails either sheet's overflow (pointsOver). - const PT = { head: 25, sub: 21, text: 22.5, refs: 18.5, gap: 18, passage: 20 }; - const POINTS_FIRST = 6; + // ★ ONE SHEET AGAIN (2026-10-02): the twelve points and the themes fit ONE page, as large as one page allows: + // the point text 18.5px (~7.5pt on letter), the themes 19px, 5px between points, the Mark 5 passage 16px; the + // credit line stays 0.28in above the edge. Measured in all 11 locales: the tightest (fr 75px, hi 79px) keep + // that much under the last point; check:packet-render fails overflow (pointsOver). + const PT = { head: 22, sub: 19, text: 18.5, refs: 16, gap: 5, passage: 16, top: 82 }; // ⚠ ISOLATE EACH REFERENCE FOR BIDI. Scripture references stay in English // (a reader looks them up in their own Bible), so on an RTL page they are // LTR runs inside an RTL paragraph. Without isolation the Unicode bidi @@ -1408,16 +1406,12 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de ${p.print_passage ? passageBlock() + heartBlock() : ''} `; }); - // Each sheet names its range (1–5, 6–12) beside the title, in the language's digits. - const head = (from, to, intro) => ` + return sheet(`
-
${esc(t('points_title'))} - ${num(from)}–${num(to)}
- ${intro ? `
${esc(t('points_intro'))}
` : ''} -
`; - const body = (html, top) => `
${html}
`; - return sheet(`${head(1, POINTS_FIRST, true)}${body(themesBlock() + rows.slice(0, POINTS_FIRST).join(''), M + 96)}`, 'sheet-points') - + '\n' + sheet(`${head(POINTS_FIRST + 1, POINTS.length, false)}${body(rows.slice(POINTS_FIRST).join(''), M + 70)} +
${esc(t('points_title'))}
+
${esc(t('points_intro'))}
+ +
${themesBlock()}${rows.join('')}
${esc(t('points_attrib'))}
`, 'sheet-points'); } diff --git a/library/packet/poster-render.js b/library/packet/poster-render.js index 748b2fa66..f8fdce093 100644 --- a/library/packet/poster-render.js +++ b/library/packet/poster-render.js @@ -288,7 +288,7 @@ const HEAD_COLUMNS_CSS = ` // series-wide rules above stay as they were, so the series' SPARSE posters are byte-identical. // Print size = sheet px x 0.92 (the packet's margins) x 0.408 (1500px = 8.5in): 22px ≈ 8.3pt, 19px ≈ 7.1pt. const HEAD_COLUMNS_FULL_CSS = ` - .hcolk{font-size:19px;fill:#6b6553} + .hcolk{font-size:19px;fill:#2f2a22;font-weight:800;letter-spacing:.1em} .hcolt{font-size:22px;fill:#3a362c} .hcolh{font-family:Georgia,'Times New Roman',serif;font-size:23px;font-weight:700;fill:#2f2a22;letter-spacing:.04em;font-variant:small-caps} .hvref{font-size:19px} @@ -827,23 +827,28 @@ function paraInto(push, text, { x, y, w, fs, lh, cls, lead }) { // its text. A second copy of the arithmetic is how the full header's prediction once ran 53px short. // Built like the footer's three networks: equal columns, a text measure narrower than the column, each block // CENTRED on its column with its lines sharing one left edge, and short rules between the columns. -// 1 the facts: a small key above each value (countries, administrative areas, people groups, series) -// 2 Jesus' strategy to feed the 5000 +// 1 Jesus' strategy to feed the 5000 +// 2 the facts: a bold key above each value (countries, administrative areas, people groups, series) // 3 what the series is const HC = { gut: 32, fs: 22, lh: 27, keyFs: 19, keyLh: 24, headLh: 30, gap: 8, headFs: 23 }; +// textW is calibrated for Lato. Without Lato the browser falls back to a system sans that sets +// these columns 2-6% wider (measured 1.05-1.06x in DejaVu Sans). Each block is centred on its +// width at 1.03x, halfway, so any overrun splits across both sides instead of all running right. +const HC_FALLBACK = 1.03; function headColumnsBand(top, facts, approxLists) { const GRID_L = BAND_MARGIN_G, GRID_W = PW - 2 * BAND_MARGIN_G; const CW = (GRID_W - 2 * HC.gut) / 3; - // The measure nearly fills the column (its inset is BAND_TEXT_PAD), so the larger type takes fewer lines. - const TW = (CW - 10 - BAND_TEXT_PAD / 2) * 0.96; + // The measure leaves room for a fallback font (see HC_FALLBACK): a block set to it stays inside its column. + const TW = (CW - 10 - BAND_TEXT_PAD / 2) * 0.92; // Uppercase, letterspaced keys: measured as capitals plus the .14em tracking. const keyW = (k) => textW(k.toUpperCase(), HC.keyFs) + k.length * HC.keyFs * 0.14; + // Column order (2026-10-02): Jesus' strategy | the facts | what the series is. const cols = [ - facts.map(([key, value]) => ({ key, lines: wrapPx(value, HC.fs, TW) })), [{ head: 'Jesus’ Strategy to Feed the 5000', paras: [ 'Jesus’ disciples faced a similar challenge: “How can we feed so many with only five loaves and two fish?” Jesus fed the 5,000 by supernaturally multiplying that small offering — and His answer holds the key to our people-groups problem.', 'Through radical faith and miracle-expectant prayer, He focused on the few in front of Him — dividing the crowd and entrusting faithful men and women to serve the most strategic groups of fifties and hundreds (Mark 6:40).', ] }], + facts.map(([key, value]) => ({ key, lines: wrapPx(value, HC.fs, TW) })), [{ head: POSTER_TEXT.sideBoxHeading || 'What is a UPG100 List?', paras: [ (POSTER_TEXT.sideBoxBody || 'This map is one of a {approxLists}-map set called the {seriesName} — a large, overwhelming target in the thousands, organised into clear groupings and displayed on maps and prayer lists.') @@ -854,9 +859,9 @@ function headColumnsBand(top, facts, approxLists) { let frags = '', bottom = top; cols.forEach((col, i) => { const centre = GRID_L + i * (CW + HC.gut) + CW / 2; - if (i === 0) { + if (col[0] && col[0].key !== undefined) { // the facts column: a bold key above each value const blockW = Math.min(TW, Math.max(...col.map((r) => Math.max(keyW(r.key), ...r.lines.map((ln) => textW(ln, HC.fs)))))); - const x = (centre - blockW / 2).toFixed(1); + const x = (centre - blockW * HC_FALLBACK / 2).toFixed(1); let y = top; col.forEach((r, k) => { if (k) y += HC.gap; @@ -869,7 +874,7 @@ function headColumnsBand(top, facts, approxLists) { const { head, paras } = col[0]; const wrapped = paras.map((t) => wrapPx(t, HC.fs, TW)); const blockW = Math.min(TW, Math.max(textW(head, HC.headFs), ...wrapped.flat().map((ln) => textW(ln, HC.fs)))); - const x = (centre - blockW / 2).toFixed(1); + const x = (centre - blockW * HC_FALLBACK / 2).toFixed(1); frags += `${esc(head)}`; let y = top + HC.headLh; wrapped.forEach((lines, k) => {