The table row is a border-box, and the offline packet gets its description
Two fixes from measuring what I had only modelled. 1. THE ROW RENDERED 81px AGAINST A 73.25px MODEL. rega measured it: no box-sizing, so the declared height was the CONTENT box and the 6px padding plus 1px border were added outside it. Because `top` still advanced by the modelled height, every row overlapped its successor by the difference. The stack still fitted the sheet -- it ended 8px inside the 60px bottom margin instead of on it -- so nothing clipped, nothing ran off the page, and no headless check could see it. box-sizing:border-box makes the declared height the rendered height. The ladder still fits: the content box is 66.25px and a name plus two lines at the default 17px needs 61.8px. 2. THE OFFLINE GENERATOR WOULD HAVE PRINTED A ONE-CLAUSE DESCRIPTION. Its fetch never asked for primary_language or imb_gsec -- measured on the cached records, 0 of 2107 carried either, while country_code_label was present on all 2107. So the browser packet printed two full sentences and the offline packet printed "The Amahuaca live in Peru." from the same data. Same defect class as the comment already above that field list: it reads as missing DATA and is missing FIELDS. The generator now injects the describer too, deliberately WITHOUT a country resolver: library/utils/countryName.js cannot be imported under node at all (it imports JSON without an import attribute -- measured), which is the constraint that made this seam an injection. It is not needed, because describeGroup falls back to country_code_label and every cached record has one. Verified end to end: 28 packets, 63 sentences on the first page, zero fallbacks, country localized correctly. The gate now budgets against the CONTENT box rather than the declared height, asserts the DEFAULT 17px step fits and not merely the smallest, and pins box-sizing in the source -- mutation-tested: removing border-box fails with its own message.
This commit is contained in:
parent
ca5ce4acd5
commit
aedc517cec
3 changed files with 51 additions and 5 deletions
|
|
@ -60,8 +60,25 @@ const CH = (H - 2 * M - HEAD - GUT * (PER_PAGE - 1)) / PER_PAGE
|
|||
// the SMALLEST ladder size. Below that, no ladder step fits and every long row overflows. Verified
|
||||
// to fire: 20 rows (73px) and 21 rows (69px) pass because the content genuinely fits; 24 rows
|
||||
// (60px) and 28 fail. That is the boundary this is meant to guard.
|
||||
ok(CH >= 15 * 1.15 + 2 + 2 * 14 * 1.25 + 6,
|
||||
`the row is ${CH.toFixed(1)}px at PER_PAGE=${PER_PAGE} — too short for a name plus two description lines even at the smallest ladder size, so long rows will overflow onto the row below`)
|
||||
// ⚠ THE ROW IS A BORDER-BOX, so its padding and border come OUT of the declared height rather than
|
||||
// being added to it. rega measured 81px against a 73.25px model before `box-sizing:border-box` was
|
||||
// set — the rows were rendering 7.75px taller than the geometry that positions them, overlapping
|
||||
// their successors, while the stack still fitted the sheet (ending 8px INSIDE the bottom margin
|
||||
// rather than on it). Nothing clipped and nothing ran off the page, which is exactly why only a
|
||||
// MEASUREMENT could find it. Budget against the CONTENT box that survives the chrome.
|
||||
const ROW_PAD = 6, ROW_BORDER = 1
|
||||
const CONTENT_H = CH - ROW_PAD - ROW_BORDER
|
||||
ok(CONTENT_H >= 15 * 1.15 + 2 + 2 * 14 * 1.25,
|
||||
`the row's content box is ${CONTENT_H.toFixed(1)}px at PER_PAGE=${PER_PAGE} — too short for a name plus two description lines even at the smallest ladder size, so long rows will overflow onto the row below`)
|
||||
// ★ AND THE BIGGEST LADDER STEP MUST FIT TOO, not just the smallest. Asserting only the 14px case
|
||||
// would let the 17px default overflow on every row while the gate stayed green — the ladder's TOP
|
||||
// is what almost every row actually renders at (all 11 locales fit at 17px today).
|
||||
ok(CONTENT_H >= 15 * 1.15 + 2 + 2 * 17 * 1.25,
|
||||
`the row's content box is ${CONTENT_H.toFixed(1)}px — too short for a name plus two lines at the DEFAULT 17px, which is what nearly every row renders at`)
|
||||
// The box model itself, asserted in the source: without border-box the declared height is the
|
||||
// CONTENT box and the row silently renders ${ROW_PAD + ROW_BORDER}px taller than it is positioned for.
|
||||
ok(/\.card\{[^}]*box-sizing:border-box/.test(SRC),
|
||||
'the .card row is not box-sizing:border-box — its padding and border will be ADDED to the declared height, so every row renders taller than the geometry that positions it')
|
||||
|
||||
// ── 2. EVERY GROUP GETS A ROW, ACROSS THE PAGE BOUNDARY ────────────────────────────────────────
|
||||
// ★ THE LOAD-BEARING ONE. A layout rewrite is exactly where people get silently dropped: an
|
||||
|
|
|
|||
|
|
@ -194,7 +194,14 @@ export function createPacketRenderer({ strings, lang = 'en', fontData = null, de
|
|||
.pray-box{display:inline-block;width:17px;height:17px;border:1.5px solid #b0a993;border-radius:3px;background:#fff;flex:0 0 auto}
|
||||
.pray-col em{font-style:normal}
|
||||
.hd-r{position:absolute;right:${M}px;top:54px;font:400 19px Georgia,serif;color:#8c8170}
|
||||
.card{position:absolute;display:flex;gap:${GUT}px;align-items:center;
|
||||
/* ⚠ box-sizing:border-box IS LOAD-BEARING HERE, NOT HOUSEKEEPING. Without it the row's height
|
||||
is the CONTENT box and the padding + border are added OUTSIDE it, so the row renders taller
|
||||
than the geometry that positions it — rega measured 81px against a 73.25px model, and because
|
||||
the row's top still advances by CH the rows then overlap their successors by the difference. The stack
|
||||
still fitted the sheet (it ended 8px INTO the 60px bottom margin rather than on it), which is
|
||||
exactly why this was invisible without a measurement: nothing clipped and nothing overflowed
|
||||
the page. With border-box the declared height IS the rendered height. */
|
||||
.card{position:absolute;box-sizing:border-box;display:flex;gap:${GUT}px;align-items:center;
|
||||
border-bottom:1px solid #e2ddd0;padding-bottom:6px;overflow:hidden}
|
||||
.card img,.card .nophoto{width:${PHOTO.toFixed(0)}px;height:${PHOTO.toFixed(0)}px;flex:0 0 auto;border-radius:3px}
|
||||
.card img{object-fit:cover}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,11 @@ const fill = (s, vars) => String(s).replace(/\{(\w+)\}/g, (_, k) => (vars[k] ??
|
|||
// the SAME packet from the SAME code. Changing a page here changes it in both.
|
||||
const { createPacketRenderer } = await import(
|
||||
join(__dirname, '..', '..', 'packet', 'packet-render.js'));
|
||||
// describeGroup generates the profile table's description column. It is imported directly (it is
|
||||
// pure and has no imports of its own); its COUNTRY RESOLVER is not, and cannot be — see the
|
||||
// `describe` seam below.
|
||||
const { describeGroup } = await import(
|
||||
join(__dirname, '..', '..', 'packets', 'describeGroup.js'));
|
||||
// SCRIPT FONT, inlined. Some languages need a face the system stack does not
|
||||
// carry — Hindi renders as tofu boxes in Georgia/Palatino/serif. The packet is a
|
||||
// standalone file meant to print offline, so the font is base64'd INTO it rather
|
||||
|
|
@ -154,7 +159,16 @@ if (fontRel) {
|
|||
+ 'the packet will render as tofu boxes. Vendor the font first.');
|
||||
}
|
||||
}
|
||||
const R = createPacketRenderer({ strings: STRINGS, lang: LANG, fontData });
|
||||
// ★ THE DESCRIBER, INJECTED — and deliberately WITHOUT a country resolver, which is not an omission.
|
||||
// library/utils/countryName.js cannot be imported here at all: it imports JSON without an import
|
||||
// attribute, so `node` refuses it outright (measured — "needs an import attribute"). That is the very
|
||||
// constraint that made this seam an injection rather than an import.
|
||||
// It is not needed. describeGroup falls back from a resolver to `country_code_label`, and the cached
|
||||
// records carry that on 2107 of 2107 — verified, not assumed — so the offline sentence localizes the
|
||||
// country exactly as the browser's does. A record somehow lacking the label degrades to its alpha-3
|
||||
// code, which is describeGroup's own documented ladder, not a new failure mode.
|
||||
const describe = (g) => describeGroup(g, LANG, { strings: STRINGS });
|
||||
const R = createPacketRenderer({ strings: STRINGS, lang: LANG, fontData, describe });
|
||||
const { mapPage, listPages, pointsPage, wheelPage, css, mapCss, PER_PAGE, W, H } = R;
|
||||
|
||||
// ── DATA ────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -257,8 +271,16 @@ async function loadRecords() {
|
|||
// evaluator's own requirements, so adding a selector type that reads a new field is a change in ONE
|
||||
// place. A hand-maintained superset drifts the moment someone adds a selector — which is exactly
|
||||
// what happened here.
|
||||
// ★ primary_language + imb_gsec FEED THE GENERATED PROFILE DESCRIPTION — the widest column of the
|
||||
// profile table. Without them describeGroup drops BOTH the language clause and the reachedness
|
||||
// sentence, so every offline row reads "The X live in Malaysia." and nothing more. MEASURED on the
|
||||
// cached records before adding them: 0 of 2107 carried either field, while country_code_label was
|
||||
// present on all 2107 — so the offline packet would have printed a one-clause description while
|
||||
// the BROWSER packet printed two full sentences from the same data. Same defect class as the
|
||||
// comment above: it reads as missing DATA and is missing FIELDS.
|
||||
const PRESENTATION_FIELDS = ['slug', 'name', 'country_code', 'population', 'religion',
|
||||
'image_url', 'has_photo', 'picture_credit', 'latitude', 'longitude'];
|
||||
'image_url', 'has_photo', 'picture_credit', 'latitude', 'longitude',
|
||||
'primary_language', 'imb_gsec'];
|
||||
const SELECTOR_FIELDS = ['country_code', 'wagf_block', 'wagf_region', 'rop1'];
|
||||
const fields = [...new Set([...PRESENTATION_FIELDS, ...SELECTOR_FIELDS])].join(',');
|
||||
const r = await fetch(`${API}/api/people-groups/list?fields=${fields}&lang=${LANG}`,
|
||||
|
|
|
|||
Loading…
Reference in a new issue