Packet: the lightweight worksheet shows every group's photo and short description too (a photo cell and a two-line description per row, 16 + 18 rows a page); the variant gate asserts both headless, the render gate counts the rows' photos and follows the new pagination
This commit is contained in:
parent
f76475de50
commit
f963d90bc3
3 changed files with 64 additions and 16 deletions
|
|
@ -183,7 +183,7 @@ await page.evaluate((words) => { window.__EXPECTED_RHYTHMS = words }, expected)
|
|||
|
||||
const r = await page.evaluate(() => {
|
||||
const body = document.body.innerText
|
||||
const imgs = [...document.querySelectorAll('.card img')]
|
||||
const imgs = [...document.querySelectorAll('.card img, img.ws-ph')] // profile cards, or lightweight rows (2026-09-30)
|
||||
// ⚠ The expected rhythm words come from the STRINGS CATALOG FOR THIS PACKET'S
|
||||
// LANGUAGE, never a hardcoded English list. A fixed list tests the opposite of
|
||||
// what i18n means: it passed a packet whose translation silently did nothing and
|
||||
|
|
@ -519,6 +519,16 @@ if (r.cards && expectPhotos && r.imgs === 0) {
|
|||
+ 'failed photo fetch, not a zone without pictures. Re-run without NOPHOTOS=1, or set '
|
||||
+ 'PACKET_EXPECT_PHOTOS=0 to declare this a deliberately photo-less packet.')
|
||||
}
|
||||
// ★ THE LIGHTWEIGHT ROWS SHOW PEOPLE TOO (2026-09-30): every worksheet row carries exactly one photo cell, a
|
||||
// photo or the "no photo" box, and a photo-expecting packet carries at least one photo, as the cards above.
|
||||
if (r.wsRows && r.imgs + r.nophoto !== r.wsRows) {
|
||||
fails.push(`${r.wsRows} worksheet rows but ${r.imgs} photo + ${r.nophoto} "no photo" = ${r.imgs + r.nophoto} — `
|
||||
+ 'every row must carry exactly one of the two')
|
||||
}
|
||||
if (r.wsRows && expectPhotos && r.imgs === 0) {
|
||||
fails.push(`${r.wsRows} worksheet rows and NOT ONE photo — the lightweight packet shows every group's photo `
|
||||
+ 'too, so this is a skipped or failed photo fetch. Set PACKET_EXPECT_PHOTOS=0 to declare a photo-less packet.')
|
||||
}
|
||||
if (r.wheelSegs !== 12) fails.push(`prayer wheel has ${r.wheelSegs} segments, expected 12`)
|
||||
// A SEGMENT WITHOUT ITS WORD IS A COLOUR. 12 labels, one per segment, on the wheel
|
||||
// sheet itself — not merely somewhere in the document.
|
||||
|
|
@ -718,8 +728,9 @@ const lifeSheets = (r.lifeSheets || 0) + (r.christianSheets || 0)
|
|||
// ⚠ AND THE SETS PAGES (bounded, centered): back matter, DETECTED for the same reason as the rest.
|
||||
// Before they were counted the arithmetic only held when a fixture omitted set data.
|
||||
const setsSheets = r.setsSheets || 0
|
||||
// The worksheet paginates 20 rows on its first page and 24 after (packet-render WS_FIRST / WS_REST).
|
||||
const wsPages = (n) => (n <= 0 ? 0 : 1 + Math.ceil(Math.max(0, n - 20) / 24))
|
||||
// The worksheet paginates 16 rows on its first page and 18 after (packet-render WS_FIRST / WS_REST; 20 and 24
|
||||
// before its rows gained a photo and a description, 2026-09-30).
|
||||
const wsPages = (n) => (n <= 0 ? 0 : 1 + Math.ceil(Math.max(0, n - 16) / 18))
|
||||
const listSheets = r.cards ? Math.ceil(r.cards / PER_PAGE) : wsPages(r.wsRows)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -102,6 +102,18 @@ ok(count(light, /sheet-sets-(bounded|centered)/g) === 0, 'lightweight still carr
|
|||
ok(count(full, /<div class="sheet/g) - count(light, /<div class="sheet/g) === want,
|
||||
`lightweight should be exactly ${want} sheets shorter, got ${count(full, /<div class="sheet/g)} vs ${count(light, /<div class="sheet/g)}`)
|
||||
}
|
||||
// ★ THE LIGHTWEIGHT ROWS SHOW PEOPLE TOO (2026-09-30): every worksheet row carries the group's photo (or the
|
||||
// "no photo" box when the record has none) and its description, as the full packet's profiles do. Built here
|
||||
// WITH photos and a describer, because build() above passes neither and so cannot see them.
|
||||
{
|
||||
const RD = createPacketRenderer({ strings, lang: 'en', describe: (g) => `${g.name} is a people group in Malaysia.` })
|
||||
const pics = new Map(groups.filter((_, i) => i % 5).map((g) => [g.slug, `https://img.test/${g.slug}.jpg`]))
|
||||
const lw = RD.buildPacket(zone, groups, pics, '<svg/>', funnel, christian, 'lightweight', sets)
|
||||
const rows = count(lw, /class="ws-row"/g), imgs = count(lw, /<img class="ws-ph"/g), none = count(lw, /class="ws-ph nophoto"/g)
|
||||
ok(rows === 45 && imgs === pics.size && imgs + none === rows,
|
||||
`each lightweight row should carry ONE photo cell (${pics.size} photos + ${45 - pics.size} "no photo"), got ${imgs} + ${none} in ${rows} rows`)
|
||||
ok(count(lw, /class="ws-desc"/g) === 45, `each lightweight row should carry its group's description, got ${count(lw, /class="ws-desc"/g)} of 45`)
|
||||
}
|
||||
// ★ THE LIFE CALENDAR, HEADLESS — the grid's CONTENT, not just its page. An adversarial review mutated the
|
||||
// renderer to shade the LAST 33 years, to shade nothing, and to draw 90 rows; every packet gate stayed green
|
||||
// because none looked inside the grid. Read the cells of each frame, in order (the legend's sample boxes
|
||||
|
|
|
|||
|
|
@ -228,14 +228,19 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
|||
.ws-top{display:flex;justify-content:space-between;margin:0 0 14px;font:400 15px Georgia,serif;color:#8a8577}
|
||||
.ws-h1{font-size:34px}
|
||||
.ws-intro{margin:8px 0 20px;max-width:1180px;font:400 22px/1.5 Georgia,serif;color:#413d35}
|
||||
.ws-head,.ws-row{display:grid;grid-template-columns:58px 1fr repeat(${PRAY_COLS.length},120px);align-items:center;column-gap:8px}
|
||||
.ws-head,.ws-row{display:grid;grid-template-columns:58px ${WS_PHOTO}px 1fr repeat(${PRAY_COLS.length},120px);align-items:center;column-gap:8px}
|
||||
.ws-head{padding:0 0 8px;border-bottom:2px solid #3b463d;font:700 16px Georgia,serif;color:#6f6a5c;text-align:center}
|
||||
.ws-head span:nth-child(2){text-align:start}
|
||||
.ws-head span:nth-child(3){text-align:start}
|
||||
.ws-ph{display:block;width:${WS_PHOTO}px;height:${WS_PHOTO}px;box-sizing:border-box;border-radius:3px;object-fit:cover}
|
||||
.ws-ph.nophoto{border:1px solid #d6d0c0;background:#f2efe6;display:flex;align-items:center;justify-content:center;
|
||||
font:italic 11px Georgia,serif;color:#a89f8c;text-align:center;padding:3px}
|
||||
.ws-row{height:${WS_ROW}px;box-sizing:border-box;border-bottom:1px solid #e2ddd0}
|
||||
.ws-n{font:700 19px Georgia,serif;color:#8a8577;text-align:end;padding-inline-end:10px}
|
||||
.ws-name{min-width:0}
|
||||
.ws-name b{display:block;font:700 26px/1.2 Georgia,serif;color:#1b1a17;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.ws-name small{display:block;font:400 18px/1.3 Georgia,serif;color:#6f6a5c;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.ws-name small.ws-desc{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;white-space:normal;
|
||||
font:400 ${WS_DESC_PX}px/1.3 Georgia,serif;margin-top:2px}
|
||||
.ws-box{justify-self:center;width:28px;height:28px;box-sizing:border-box;border:2px solid #8f8a78;border-radius:4px;background:#fff}
|
||||
.ws-foot{position:absolute;left:0;right:0;bottom:22px;text-align:center;font:400 12px Georgia,serif;color:#a89f8c}
|
||||
|
||||
|
|
@ -1111,23 +1116,43 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
|||
// 2026-09-29: the lightweight packet is less about fitting information in and more a
|
||||
// printable, instructive sheet — every people group, less text, a kind rather than commanding
|
||||
// voice, and type and spacing sized for paper. Modelled on the Module 1 list-of-100 worksheet: a
|
||||
// short note on the first page, one column header per page, numbered rows with room to tick. No
|
||||
// photos and no descriptions; those are what the full packet is for.
|
||||
// short note on the first page, one column header per page, numbered rows with room to tick.
|
||||
// ★ EVERY ROW ALSO SHOWS THE GROUP'S PHOTO AND SHORT DESCRIPTION (2026-09-30): both packets show people,
|
||||
// not just names. The worksheet keeps its shape; each row gains the photo and a two-line description
|
||||
// (the same describeGroup sentence the full packet prints, cut at a word), so rows grow 70 → 92px.
|
||||
// Without a describer (the Node gates) the row keeps its country · population line.
|
||||
// ⚠ STILL EVERY GROUP. A variant may change HOW people are listed, never WHETHER they are.
|
||||
// Sizes in sheet px (1500px = 8.5in, so 1px ~ 0.41pt): names 26px ~ 10.6pt, rows 70px ~ 0.4in.
|
||||
const WS_ROW = 70, WS_FIRST = 20, WS_REST = 24;
|
||||
function worksheetPages(zone, groups) {
|
||||
// Sizes in sheet px (1500px = 8.5in, so 1px ~ 0.41pt): names 26px ~ 10.6pt, rows 92px ~ 0.52in.
|
||||
// 16 + 18 rows: measured in all 11 locales, the tightest first page (fr, its longer note) keeps 100px spare and
|
||||
// the tightest later page 73px; a 17th or 19th row would not fit.
|
||||
const WS_ROW = 92, WS_FIRST = 16, WS_REST = 18, WS_PHOTO = 76, WS_DESC_PX = 17;
|
||||
// The description cell's width: the sheet less its padding, the number, photo and tick columns, and
|
||||
// the five column gaps (the .ws-row grid below). Budgeted like the full packet's (budgetFor), and the
|
||||
// CSS clamps it to two lines too, for the scripts whose glyphs run wider than the estimate.
|
||||
const WS_DESC_W = W - 2 * M - 58 - WS_PHOTO - PRAY_COLS.length * 120 - (PRAY_COLS.length + 2) * 8;
|
||||
const WS_DESC_BUDGET = Math.floor(WS_DESC_W / (WS_DESC_PX * AVG_ADVANCE)) * 2;
|
||||
function worksheetLine(g) {
|
||||
const sentence = describe ? String(describe(g) || '') : '';
|
||||
if (sentence) return `<small class="ws-desc">${esc(cutAtWord(sentence, WS_DESC_BUDGET))}</small>`;
|
||||
const where = [flat(g.country_code_label) || flat(g.country) || flat(g.country_code), // the name, not the ISO3
|
||||
Number(g.population) > 0 ? `${num(g.population)} ${t('people_suffix')}` : ''].filter(Boolean).join(' · ');
|
||||
return `<small>${esc(where)}</small>`;
|
||||
}
|
||||
function worksheetPages(zone, groups, photos = new Map()) {
|
||||
const pages = [];
|
||||
const head = `<div class="ws-head"><span></span><span>${esc(t('light_col_group'))}</span>${
|
||||
const head = `<div class="ws-head"><span></span><span></span><span>${esc(t('light_col_group'))}</span>${
|
||||
PRAY_COLS.map((key) => `<span>${esc(t(key))}</span>`).join('')}</div>`;
|
||||
for (let start = 0, page = 0; start < groups.length; page += 1) {
|
||||
const per = page === 0 ? WS_FIRST : WS_REST;
|
||||
const slice = groups.slice(start, start + per);
|
||||
const rows = slice.map((g, k) => {
|
||||
const where = [flat(g.country_code_label) || flat(g.country) || flat(g.country_code), // the name, not the ISO3
|
||||
Number(g.population) > 0 ? `${num(g.population)} ${t('people_suffix')}` : ''].filter(Boolean).join(' · ');
|
||||
return `<div class="ws-row"><span class="ws-n">${num(start + k + 1)}</span>`
|
||||
+ `<span class="ws-name"><b>${esc(g.name)}</b><small>${esc(where)}</small></span>`
|
||||
const src = photos.get(g.slug);
|
||||
// esc() on the URL, as the profile cards do: a quote in an API image_url must not become markup.
|
||||
const photo = src
|
||||
? `<img class="ws-ph" src="${esc(src)}" alt="">`
|
||||
: `<span class="ws-ph nophoto">${esc(t('no_photo'))}</span>`;
|
||||
return `<div class="ws-row"><span class="ws-n">${num(start + k + 1)}</span>${photo}`
|
||||
+ `<span class="ws-name"><b>${esc(g.name)}</b>${worksheetLine(g)}</span>`
|
||||
+ PRAY_COLS.map(() => '<i class="ws-box"></i>').join('') + '</div>';
|
||||
}).join('');
|
||||
const top = page === 0
|
||||
|
|
@ -1527,7 +1552,7 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
|||
keep('christian') && christianPage(zone, christian),
|
||||
// NOT behind keep() — see the VARIANTS comment: dropping list pages drops people. The lightweight
|
||||
// packet lists the SAME groups as a worksheet (LIST_STYLE); the full packet as profiles.
|
||||
...(LIST_STYLE[variant] ? LIST_STYLE[variant](zone, groups) : listPages(zone, groups, photos)),
|
||||
...(LIST_STYLE[variant] ? LIST_STYLE[variant](zone, groups, photos) : listPages(zone, groups, photos)),
|
||||
// BACK MATTER: after the profiles (brief A1). Full-detail only.
|
||||
keep('sets') && setsPage(zone, sets),
|
||||
keep('wheel') && wheelPage(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue