Merge branch 'upg100/p6-card-row-dev' into upg100/land-pair

# Conflicts:
#	public/dx-maps/country-posters/countries-map/index.html
#	public/dx-maps/country-posters/country-poster-wall/index.html
#	public/dx-maps/country-posters/country-posters-mfe/index.html
#	public/dx-maps/country-posters/country-posters.js
#	public/dx-maps/doxa-maps-spa/manifest.json
#	public/dx-maps/manifest.json
#	public/dx-maps/my-upg-100-list/my-upg-100-list.js
#	public/dx-maps/my-upg-100-list/poster-wall/index.html
#	public/dx-maps/my-upg-100-list/upg-100-list/index.html
#	public/dx-maps/my-upg-100-list/upg-100-mfe/index.html
#	public/dx-maps/regions-dashboard/region-dashboards/index.html
#	public/dx-maps/regions-dashboard/regions-dashboard.js
#	public/dx-maps/regions-dashboard/regions-list/index.html
#	public/dx-maps/regions-dashboard/regions-mfe/index.html
This commit is contained in:
Doxa Maps 2026-09-30 08:33:01 +00:00
commit fa2a7fc0e7
2 changed files with 55 additions and 3 deletions

View file

@ -208,6 +208,7 @@ async function measure(width, lang) {
let previewsPainted = 0, previewsBroken = 0, noPreview = 0, unnamedPreview = 0
let notTaped = 0, worstTape = 0, selectsInRow = 0
let headNotTaped = 0, worstHead = 0, mapCovered = 0, worstCover = 0
let uncontained = 0, worstBelow = 0
for (const { c, b } of boxes) {
const row = c.querySelector('.pfs')
if (!row) continue
@ -216,6 +217,14 @@ async function measure(width, lang) {
overflowing++
worst = Math.max(worst, Math.round(Math.max(rb.right - b.right, b.left - rb.left)))
}
// ★ AND THE CARD CONTAINS ITS ROW (2026-09-30, from a phone at two posters per row). The row
// wrapped to three lines and hung about 45px below the card's white box, into the row gap:
// horizontally inside, so the check above stayed green. PosterWall now fits each card to its
// own row (--pfs-room); this is the assertion that keeps that true in every locale x width.
if (rb.bottom > b.bottom + 0.5) {
uncontained++
worstBelow = Math.max(worstBelow, Math.round(rb.bottom - b.bottom))
}
// ⚠ THE VERTICAL AXIS TOO — this measured only left/right, which is exactly how it passed a
// visible regression. Fixing the horizontal overflow with flex-wrap grew the row
// 35px -> 78px, and because it sits at bottom:-14px the extra height pushed its TOP from
@ -364,7 +373,8 @@ async function measure(width, lang) {
overflowing, worst, intrusions, coveringPreview, worstY,
previewsPainted, previewsBroken, noPreview, unnamedPreview, clipped: clipped.length,
controls: ctrls.length, tiny: tiny.length, perRow, cardW,
notTaped, worstTape, selectsInRow, headNotTaped, worstHead, mapCovered, worstCover }
notTaped, worstTape, selectsInRow, headNotTaped, worstHead, mapCovered, worstCover,
uncontained, worstBelow }
})()`)
await page.close()
return { width, lang, hover, reach, ...out }
@ -382,6 +392,7 @@ console.log(` leaf : ${LEAF}`)
for (const r of results) {
console.log(` ${(r.lang || 'en').padEnd(2)} ${String(r.width).padStart(4)}px cards ${String(r.cards).padStart(2)}`
+ ` · overflow ${r.overflowing}${r.worst ? ` (${r.worst}px)` : ''}`
+ ` · below-card ${r.uncontained}${r.worstBelow ? ` (${r.worstBelow}px)` : ''}`
+ ` · neighbour ${r.intrusions}`
+ ` · over-preview ${r.coveringPreview}${r.worstY ? ` (${r.worstY}px)` : ''}`
+ ` · off-frame ${r.notTaped}${r.worstTape ? ` (${r.worstTape > 0 ? '+' : ''}${r.worstTape}px)` : ''}`
@ -408,6 +419,7 @@ for (const r of results) {
if (!r.rows) { fails.push(`${at}: ${r.cards} card(s) but none carry a .pfs print-options row`); continue }
if (r.intrusions) fails.push(`${at}: ${r.intrusions} print row(s) overlap a NEIGHBOURING card`)
if (r.overflowing) fails.push(`${at}: ${r.overflowing} print row(s) extend past their own card (worst ${r.worst}px)`)
if (r.uncontained) fails.push(`${at}: ${r.uncontained} print row(s) hang below their own card's box (worst ${r.worstBelow}px): the card must contain its row`)
// ⚠ "Nothing overflows" is trivially satisfied by a row whose buttons collapsed to nothing.
if (r.tiny) {
fails.push(`${at}: ${r.tiny} print control(s) shrank below a usable size — fitting the row by `

View file

@ -39,7 +39,7 @@
* CROSS-PANE COMMS — this wall NEVER imports a map. It dispatches a window
* CustomEvent scoped by instanceId, so several embeds on one page never cross-talk.
*/
import { computed, ref, watch, onBeforeUnmount, onMounted } from 'vue'
import { computed, ref, watch, onBeforeUnmount, onMounted, onUpdated } from 'vue'
import { tStatic } from '../../i18n/index.js'
// This component's chrome, read out of the catalogue. tStatic and not useI18n: these mount inside
@ -365,6 +365,44 @@ onMounted(() => {
})
onBeforeUnmount(() => { for (const mo of paneMOs) mo.disconnect(); paneMOs = [] })
// ★ THE CARD CONTAINS ITS PRINT ROW (2026-09-30, from a phone at two posters per row). The row is a sticker
// pinned --tape-overlap over the banner's bottom edge that grows DOWNWARD as its labels wrap. One line
// fits inside the card's 26px bottom padding; on a ~162px two-up card it wraps to three lines and hung
// about 45px below the card's white box. No fixed padding fits every locale x width (the reason the row
// is pinned by its overlap and not its centre), so each card is fitted to its own row: a ResizeObserver
// on every .pfs writes --pfs-room on the card: for a row that hangs past the white box (the 26px padding
// and the 1px border), the part below the banner plus ROW_DAYLIGHT_PX that the 26px does not cover. A row
// that fits writes 0 (a one-line row ends ~5px inside the box), so every card that fit is unchanged.
// The revealed packet/size rows stay overlays on purpose (.has-tape-open lifts the card above its
// neighbours); only the print row itself is contained.
const CARD_PAD_BOTTOM_PX = 26
const CARD_BORDER_PX = 1
const ROW_DAYLIGHT_PX = 8
let rowRO = null
function fitCardToRow(pfs) {
const card = pfs.closest('.card')
const banner = pfs.parentElement
if (!card || !banner) return
// Rect DIFFERENCES inside one card: the hover lift (a translateY on the card) moves both edges alike.
const below = pfs.getBoundingClientRect().bottom - banner.getBoundingClientRect().bottom
const hangs = below > CARD_PAD_BOTTOM_PX + CARD_BORDER_PX
const room = `${hangs ? Math.ceil(below + ROW_DAYLIGHT_PX - CARD_PAD_BOTTOM_PX) : 0}px`
if (card.style.getPropertyValue('--pfs-room') !== room) card.style.setProperty('--pfs-room', room)
}
// observe() is idempotent, so re-scanning after every render only adds the rows that are new (items,
// sections opened, the per-row choice); a hidden wall reports 0 and writes 0 until it is shown.
function observeRows() {
if (!rowRO || !wallEl.value) return
for (const pfs of wallEl.value.querySelectorAll('.pfs')) rowRO.observe(pfs)
}
onMounted(() => {
if (typeof ResizeObserver === 'undefined') return
rowRO = new ResizeObserver((entries) => { for (const e of entries) fitCardToRow(e.target) })
observeRows()
})
onUpdated(observeRows)
onBeforeUnmount(() => { if (rowRO) rowRO.disconnect(); rowRO = null })
function showOnMap(item) {
// TOGGLE, not a one-way set: a control the user can turn on but not off reads as
// broken even when the selection is correct.
@ -758,7 +796,9 @@ useShadowStyles(
// hand-typed 24px would.
'.card{--card-pad-top:24px;--tape-overlap:16px;position:relative;background:#fff;border:1px solid #e3e6ea;' +
'border-radius:3px;' +
'padding:var(--card-pad-top) 12px 26px;' +
// --pfs-room: the extra bottom padding a card needs to CONTAIN its print row once that row wraps
// (fitCardToRow in the script). 0 wherever the row fits the 26px, so those cards are unchanged.
'padding:var(--card-pad-top) 12px calc(26px + var(--pfs-room,0px));' +
'box-shadow:0 1px 3px rgba(0,0,0,.10),0 6px 18px rgba(0,0,0,.07);' +
'transition:transform .16s ease,box-shadow .16s ease}' +
// ★ THE BLUE OUTLINE IS A HOVER STATE, NOT A MISSING STATIC BORDER. Several reports have asked