poster cards: a blue outline on hover, and a gate that can see it
Some checks are pending
build-gate / build (push) Waiting to run
Some checks are pending
build-gate / build (push) Waiting to run
The card asks for the Pinterest/Instagram behaviour: hovering a poster outlines it in blue so a reader knows which one their click is about to act on. ★ THIS IS NOT A RESTORED BORDER. Several reports asked to bring back "the blue outline around the poster", and MEASURED 2026-09-24 there has never been one: all 136 elements painting a frame on this wall compute outline:none, and every border is grey (.card #e3e6ea, .main #dfe3e7). Nothing regressed -- the affordance was never built. The reports kept recurring because a hover state is invisible to a screenshot, so each check confirmed "no outline" and concluded the sighting was mistaken. outline, NOT border, and that is load-bearing. An outline paints OUTSIDE the box and takes no space, so it cannot move anything. The rest of this wall's gate asserts where the print row sits at five widths; a hover that grew a border would re-lay-out the card and push that row, so a cosmetic state could break a gate about print controls. :focus-within carries it to the keyboard, and @media (hover:none) drops the whole state on touch -- with no pointer it would either never appear or stick after a tap, leaving a card looking permanently selected. THE GATE NOW MOVES THE POINTER. Everything it measured before was of a loaded, untouched page, which is precisely why this affordance could go unbuilt while being repeatedly requested. ⚠ AND ONE OF MY OWN ASSERTIONS WAS DECORATION UNTIL I MUTATION-TESTED IT. I first asserted the card's OUTER width is unchanged by hover. It can't change: the card is a grid item, so the track fixes its width and a border grown on hover eats inward. MEASURED with border-width:4px on hover -- outer 224->224px, unfalsifiable, while the CONTENT box went 222->216px. It now asserts clientWidth, which is what actually repositions the print row. MUTATION-TESTED: remove the outline -> FAIL at all four pointer widths (exit 1); grow a border alongside it -> FAIL naming the content-box shift (exit 1); restore -> PASS (exit 0), reporting "hover 2px rgb(47, 111, 219)" at 768-1920 and "no outline (touch, correct)" at 390. The print/open controls the card also asks for are already there and were verified clickable in the same pass: Prayer List, Card Deck, the stock select and Poster Map, all four with full labels and real handlers, 0 clipped.
This commit is contained in:
parent
31b934bdf5
commit
a7f57c117d
8 changed files with 117 additions and 12 deletions
|
|
@ -49,6 +49,55 @@ async function measure(width) {
|
|||
const page = await browser.newPage({ viewport: { width, height: 1000 }, isMobile: mobile, hasTouch: mobile })
|
||||
await page.goto(LEAF, { waitUntil: 'load', timeout: 45000 })
|
||||
await page.waitForTimeout(10000)
|
||||
|
||||
// ★ HOVER IS INVISIBLE TO A STATIC PROBE, which is exactly how the blue outline stayed
|
||||
// unbuilt for so long. Repeated reports asked to "restore the blue outline around the poster";
|
||||
// measuring the loaded page found `outline:none` on all 136 framed elements and concluded it had
|
||||
// never existed — true, and useless, because the affordance being asked for only exists WHILE
|
||||
// THE POINTER IS OVER THE CARD. The pointer has to actually move.
|
||||
//
|
||||
// ⚠ AND IT MUST NOT MOVE ANYTHING. The whole rest of this gate asserts where the print row sits.
|
||||
// A hover that grew a border would re-lay-out the card and push that row, so a cosmetic state
|
||||
// could break a gate about print controls. Outline paints outside the box; this asserts the box
|
||||
// WIDTH is identical hovered and not, which is the property that keeps the two independent.
|
||||
let hover = null
|
||||
try {
|
||||
const target = await page.evaluate(`(() => {
|
||||
const all = (${WALK})(document)
|
||||
const c = all.filter((e) => (e.className||'').toString().split(' ').includes('card'))
|
||||
.filter((e) => { const b = e.getBoundingClientRect(); return b.width > 2 && b.height > 2 })[0]
|
||||
if (!c) return null
|
||||
c.scrollIntoView({ block: 'center' })
|
||||
const r = c.getBoundingClientRect()
|
||||
const cs = getComputedStyle(c)
|
||||
// ⚠ MEASURE clientWidth, NOT THE BORDER BOX. The card is a GRID ITEM, so its outer width is
|
||||
// fixed by the track (minmax(210px,1fr)) and CANNOT change no matter what the hover rule
|
||||
// does — a border grown on hover eats INWARD and leaves getBoundingClientRect().width
|
||||
// identical. Asserting on the outer width is therefore unfalsifiable here, which is a
|
||||
// decorative assertion, not a real one. MEASURED: growing border-width to 4px on hover left
|
||||
// the outer width at 212px and moved nothing this gate was watching. clientWidth is the
|
||||
// CONTENT box — it shrinks when the border grows, and it is what actually repositions the
|
||||
// print row, which is the property worth protecting.
|
||||
return { x: r.x + r.width / 2, y: r.y + r.height / 2, w: Math.round(r.width),
|
||||
inner: c.clientWidth, outlineStyle: cs.outlineStyle }
|
||||
})()`)
|
||||
if (target) {
|
||||
await page.mouse.move(target.x, target.y)
|
||||
await page.waitForTimeout(500)
|
||||
const hovered = await page.evaluate(`(() => {
|
||||
const all = (${WALK})(document)
|
||||
const c = all.filter((e) => (e.className||'').toString().split(' ').includes('card'))
|
||||
.filter((e) => { const b = e.getBoundingClientRect(); return b.width > 2 && b.height > 2 })[0]
|
||||
if (!c) return null
|
||||
const cs = getComputedStyle(c)
|
||||
return { outlineStyle: cs.outlineStyle, outlineColor: cs.outlineColor,
|
||||
outlineWidth: cs.outlineWidth, w: Math.round(c.getBoundingClientRect().width),
|
||||
inner: c.clientWidth }
|
||||
})()`)
|
||||
if (hovered) hover = { before: target, after: hovered }
|
||||
}
|
||||
} catch (_) { /* hover is an enhancement; never let it break the geometry run */ }
|
||||
|
||||
const out = await page.evaluate(`(() => {
|
||||
const all = (${WALK})(document)
|
||||
const vis = (e) => { const b = e.getBoundingClientRect(); return b.width > 2 && b.height > 2 }
|
||||
|
|
@ -115,7 +164,7 @@ async function measure(width) {
|
|||
controls: ctrls.length, tiny: tiny.length }
|
||||
})()`)
|
||||
await page.close()
|
||||
return { width, ...out }
|
||||
return { width, hover, ...out }
|
||||
}
|
||||
|
||||
const results = []
|
||||
|
|
@ -134,7 +183,12 @@ for (const r of results) {
|
|||
+ ` · over-preview ${r.coveringPreview}${r.worstY ? ` (${r.worstY}px)` : ''}`
|
||||
+ ` · previews ${r.previewsPainted} painted`
|
||||
+ `${r.previewsBroken ? `/${r.previewsBroken} BROKEN` : ''}${r.noPreview ? `/${r.noPreview} none` : ''}`
|
||||
+ ` · ${r.controls} control(s), ${r.tiny} too small, ${r.clipped} clipped`)
|
||||
+ ` · ${r.controls} control(s), ${r.tiny} too small, ${r.clipped} clipped`
|
||||
+ (r.hover
|
||||
? ` · hover ${r.hover.after.outlineStyle === 'none'
|
||||
? 'no outline' + (r.width < 500 ? ' (touch, correct)' : '')
|
||||
: `${r.hover.after.outlineWidth} ${r.hover.after.outlineColor}`}`
|
||||
: ''))
|
||||
}
|
||||
|
||||
const fails = []
|
||||
|
|
@ -185,6 +239,34 @@ for (const r of results) {
|
|||
+ `(top sits ${r.worstY}px above the image's bottom edge) — growing the row taller to fit it `
|
||||
+ 'horizontally is not a fix')
|
||||
}
|
||||
// HOVER: the card must answer the pointer, and must not move when it does.
|
||||
// Only asserted where a pointer exists — a touch viewport has `@media (hover:none)` suppressing
|
||||
// the state deliberately, so demanding an outline there would fail the correct behaviour.
|
||||
if (r.hover) {
|
||||
const { before, after } = r.hover
|
||||
const pointer = r.width >= 500
|
||||
if (pointer) {
|
||||
if (after.outlineStyle === 'none') {
|
||||
fails.push(`${at}: hovering a card paints NO outline — the card does not answer the `
|
||||
+ 'pointer, so a reader cannot tell which poster their click is about to act on')
|
||||
} else if (before.outlineStyle !== 'none') {
|
||||
fails.push(`${at}: the card carries an outline even when NOT hovered `
|
||||
+ `(${before.outlineStyle}) — the outline is meant to be the hover affordance, not a `
|
||||
+ 'permanent frame')
|
||||
}
|
||||
if (after.w !== before.w || after.inner !== before.inner) {
|
||||
fails.push(`${at}: hovering RESIZED the card `
|
||||
+ `(outer ${before.w}->${after.w}px, content ${before.inner}->${after.inner}px). The `
|
||||
+ 'hover state must not re-lay-out anything: every print-row assertion above is about '
|
||||
+ 'where that row sits, and a cosmetic hover that moves the box can break them. Use '
|
||||
+ 'outline (painted OUTSIDE the box and taking no space), never a border change.')
|
||||
}
|
||||
} else if (after.outlineStyle !== 'none') {
|
||||
fails.push(`${at}: a TOUCH viewport painted a hover outline (${after.outlineStyle}) — with `
|
||||
+ 'no pointer this either never appears or sticks after a tap, leaving a card looking '
|
||||
+ 'permanently selected')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fails.length) {
|
||||
|
|
|
|||
|
|
@ -477,7 +477,30 @@ useShadowStyles(
|
|||
'.card{position:relative;background:#fff;border:1px solid #e3e6ea;border-radius:3px;' +
|
||||
'padding:24px 12px 26px;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}' +
|
||||
'.card:hover{transform:translateY(-3px);box-shadow:0 3px 8px rgba(0,0,0,.14),0 12px 28px rgba(0,0,0,.10)}' +
|
||||
// ★ THE BLUE OUTLINE IS A HOVER STATE, NOT A MISSING STATIC BORDER. Several reports have asked
|
||||
// for "the blue outline around the poster" to be restored, and MEASURED 2026-09-24 there has
|
||||
// never been one: all 136 elements painting a frame on this wall compute `outline:none`, and
|
||||
// every border is grey (.card #e3e6ea, .main #dfe3e7). Nothing regressed — the affordance was
|
||||
// never built. Asked for as Pinterest/Instagram do it: the card answers the pointer.
|
||||
//
|
||||
// ⚠ `outline`, NOT `border`, and this is the load-bearing choice. An outline is painted OUTSIDE
|
||||
// the box and takes no space, so it cannot move anything. Growing the border on hover would
|
||||
// re-lay-out the card by 2px and push the print row — whose geometry is asserted at five widths
|
||||
// by check:poster-card-fits (0 overflow / 0 over-preview / 0 neighbour intrusions) — meaning a
|
||||
// cosmetic hover would break a gate about print controls. outline-offset keeps it clear of the
|
||||
// rounded corner instead of cutting across it.
|
||||
//
|
||||
// ⚠ :focus-visible CARRIES IT TO THE KEYBOARD. A hover-only affordance tells a mouse user which
|
||||
// card they are about to act on and tells a keyboard user nothing, on a card whose buttons this
|
||||
// same wall already had to be fixed to name (791/791 accessible names).
|
||||
'.card:hover,.card:focus-within{transform:translateY(-3px);' +
|
||||
'box-shadow:0 3px 8px rgba(0,0,0,.14),0 12px 28px rgba(0,0,0,.10);' +
|
||||
'outline:2px solid #2f6fdb;outline-offset:2px}' +
|
||||
'.is-dark .card:hover,.is-dark .card:focus-within{outline-color:#5aa0ff}' +
|
||||
// ⚠ A TOUCH READER NEVER HOVERS. On a phone the lift and the outline would either never appear
|
||||
// or, worse, stick after a tap and leave a card looking selected. The print row on this wall is
|
||||
// always visible, so touch loses no affordance by dropping the hover state entirely.
|
||||
'@media (hover:none){.card:hover,.card:focus-within{transform:none;outline:none}}' +
|
||||
'.is-dark .card{background:#132019;border-color:rgba(110,231,183,.20)}' +
|
||||
// box-sizing is NOT inherited into a shadow root from the host page's reset.
|
||||
'.tape{box-sizing:border-box;position:absolute;left:50%;transform:translateX(-50%);z-index:2;' +
|
||||
|
|
|
|||
2
public/doxa-maps/index.html
generated
2
public/doxa-maps/index.html
generated
File diff suppressed because one or more lines are too long
8
public/doxa-maps/manifest.json
generated
8
public/doxa-maps/manifest.json
generated
|
|
@ -681,7 +681,7 @@
|
|||
"bundle": "my-upg-100-list",
|
||||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"output": "doxa-maps/my-upg-100-list/my-upg-100-list.js",
|
||||
"integrity": "sha384-p29J0vBYB023wjm27/6NkyLanFQot5B0C48ZZnnyxC9yQTGXva9kRPfjdJeUOj6y",
|
||||
"integrity": "sha384-NlPYE8Ka9bbUPBeUKZ8tppvBrvF9xdMXZlHtaUym05SxsppfTes/YtnAbr1wq4es",
|
||||
"elements": [
|
||||
"my-upg-100-list-map"
|
||||
],
|
||||
|
|
@ -701,7 +701,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/poster-wall/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-p29J0vBYB023wjm27/6NkyLanFQot5B0C48ZZnnyxC9yQTGXva9kRPfjdJeUOj6y\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-NlPYE8Ka9bbUPBeUKZ8tppvBrvF9xdMXZlHtaUym05SxsppfTes/YtnAbr1wq4es\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"poster-wall\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -715,7 +715,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-p29J0vBYB023wjm27/6NkyLanFQot5B0C48ZZnnyxC9yQTGXva9kRPfjdJeUOj6y\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-NlPYE8Ka9bbUPBeUKZ8tppvBrvF9xdMXZlHtaUym05SxsppfTes/YtnAbr1wq4es\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -729,7 +729,7 @@
|
|||
"scriptUrl": "./my-upg-100-list/my-upg-100-list.js",
|
||||
"embed": {
|
||||
"iframe": "<iframe src=\"./my-upg-100-list/upg-100-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-p29J0vBYB023wjm27/6NkyLanFQot5B0C48ZZnnyxC9yQTGXva9kRPfjdJeUOj6y\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./my-upg-100-list/my-upg-100-list.js\" integrity=\"sha384-NlPYE8Ka9bbUPBeUKZ8tppvBrvF9xdMXZlHtaUym05SxsppfTes/YtnAbr1wq4es\" crossorigin=\"anonymous\"></script>\n<my-upg-100-list-map profile-config='{\"profile\":\"upg-100-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></my-upg-100-list-map>"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-p29J0vBYB023wjm27/6NkyLanFQot5B0C48ZZnnyxC9yQTGXva9kRPfjdJeUOj6y" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-NlPYE8Ka9bbUPBeUKZ8tppvBrvF9xdMXZlHtaUym05SxsppfTes/YtnAbr1wq4es" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-p29J0vBYB023wjm27/6NkyLanFQot5B0C48ZZnnyxC9yQTGXva9kRPfjdJeUOj6y" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-NlPYE8Ka9bbUPBeUKZ8tppvBrvF9xdMXZlHtaUym05SxsppfTes/YtnAbr1wq4es" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
|
||||
<!-- Bundle IIFE — registers the <my-upg-100-list-map> custom element. -->
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-p29J0vBYB023wjm27/6NkyLanFQot5B0C48ZZnnyxC9yQTGXva9kRPfjdJeUOj6y" crossorigin="anonymous"></script>
|
||||
<script src="../my-upg-100-list.js" integrity="sha384-NlPYE8Ka9bbUPBeUKZ8tppvBrvF9xdMXZlHtaUym05SxsppfTes/YtnAbr1wq4es" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
// Mapbox token resolution — the A3 ladder, in precedence order:
|
||||
|
|
|
|||
Loading…
Reference in a new issue