Pin hint: on a page that scrolls, the pill sits at the map's bottom instead of hanging at the fold
On a partner home page the map is a block in a scrolling page. The clamp floored the pill at the viewport fold, so a map whose bottom was below the fold at load had its pill lifted to mid-map, re-set by the settle timers and resize observers, and left there after a scroll, then dropped. The fold now counts only on a page that does not scroll (a full-screen pane or leaf, where a phone's browser bar can cover the bottom); the legend sheet and the licence credit still lift the pill everywhere. The floor is a pure function (pinHintFloor.js), and check:pin-hint-floor proves both cases; its self-test refuses 5 planted defects.
This commit is contained in:
parent
3f7d489409
commit
c870daa803
4 changed files with 122 additions and 5 deletions
77
internal/check-pin-hint-floor.mjs
Normal file
77
internal/check-pin-hint-floor.mjs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-pin-hint-floor.mjs — the pin-hint pill is placed at the map's bottom on a page that scrolls, and
|
||||
* still kept above the fold on a full-screen app page.
|
||||
*
|
||||
* The defect it guards: PinHint's clamp floored the pill at the viewport fold on EVERY page. On a partner
|
||||
* home page, where the map is a block in a scrolling page, a map whose bottom was below the fold at load
|
||||
* had its pill lifted to mid-map, re-set by the settle timers, and left there after a scroll.
|
||||
*
|
||||
* ASSERTS (headless, on library/components/hints/pinHintFloor.js and PinHint.vue's source):
|
||||
* 1. scrolling page, map bottom below the fold → no lift (the pill stays at the map's bottom);
|
||||
* 2. full-screen page, pill under the fold → lifted above it (today's behaviour kept);
|
||||
* 3. the legend sheet and the licence credit still lift the pill on a scrolling page;
|
||||
* 4. pageScrolls() tells a longer page from a full-screen one, and fails safe (false) on bad input;
|
||||
* 5. PinHint's clampIntoView takes its floor from pillFloor(), not from window.innerHeight.
|
||||
* --self-test plants each defect (the old fold-everywhere floor, a pageScrolls that never says yes, the
|
||||
* old source line) and must see each one refused.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const F = await import(pathToFileURL(join(ROOT, 'library/components/hints/pinHintFloor.js')).href)
|
||||
const SRC = readFileSync(join(ROOT, 'library/components/hints/PinHint.vue'), 'utf8')
|
||||
|
||||
// The clamp's arithmetic, as PinHint.vue does it: lift = overflow + 8 when the pill's bottom passes the floor.
|
||||
const liftFor = (pillBottom, floor) => { const o = Math.round(pillBottom - floor); return o > 0 ? o + 8 : 0 }
|
||||
const doc = (scrollHeight) => ({ scrollingElement: { scrollHeight } })
|
||||
|
||||
function audit({ pillFloor, pageScrolls }, src) {
|
||||
const bad = []
|
||||
// 1. DX Life's measured case at 1440: fold 900, map 450..1230, the pill's bottom 12px above the map's.
|
||||
const scrolls = pageScrolls(doc(3200), { innerHeight: 900 })
|
||||
const l1 = liftFor(1218, pillFloor({ viewportH: 900, scrolls }))
|
||||
if (l1 !== 0) bad.push(`a scrolling page lifts the pill ${l1}px off the map's bottom (the fold is not an obstacle there)`)
|
||||
// 2. A full-screen app page at 390×844: the pill 16px under the fold is lifted clear of it.
|
||||
const s2 = pageScrolls(doc(844), { innerHeight: 844 })
|
||||
const l2 = liftFor(860, pillFloor({ viewportH: 844, scrolls: s2 }))
|
||||
if (l2 !== 24) bad.push(`a full-screen page lifts the pill ${l2}px, expected 24 (16 over the fold + 8)`)
|
||||
// 3. Obstacles still count on a scrolling page.
|
||||
const l3 = liftFor(1218, pillFloor({ viewportH: 900, scrolls: true, sheetTop: 1150 }))
|
||||
if (l3 !== 76) bad.push(`the legend sheet lifts the pill ${l3}px on a scrolling page, expected 76`)
|
||||
const l3b = liftFor(1218, pillFloor({ viewportH: 900, scrolls: true, creditTop: 1200 }))
|
||||
if (l3b !== 26) bad.push(`the licence credit lifts the pill ${l3b}px on a scrolling page, expected 26`)
|
||||
// 4. pageScrolls on its own.
|
||||
if (pageScrolls(doc(900), { innerHeight: 900 }) !== false) bad.push('pageScrolls says a full-screen page scrolls')
|
||||
if (pageScrolls(doc(901), { innerHeight: 900 }) !== false) bad.push('pageScrolls counts a 1px rounding overflow as a scroll')
|
||||
if (pageScrolls(null, null) !== false) bad.push('pageScrolls does not fail safe without a document')
|
||||
// 5. The component uses it.
|
||||
const body = (src.match(/function clampIntoView\(\)\s*\{([\s\S]*?)\n\}/) || [])[1] || ''
|
||||
if (!body) bad.push('PinHint.vue has no clampIntoView() to read')
|
||||
else {
|
||||
if (!/pillFloor\(\{[^}]*scrolls:\s*pageScrolls\(\)/.test(body)) bad.push('clampIntoView does not take its floor from pillFloor({ … scrolls: pageScrolls() … })')
|
||||
if (/Math\.min\(\s*window\.innerHeight/.test(body)) bad.push('clampIntoView still floors on window.innerHeight directly')
|
||||
}
|
||||
return bad
|
||||
}
|
||||
|
||||
const real = { pillFloor: F.pillFloor, pageScrolls: F.pageScrolls }
|
||||
const problems = audit(real, SRC)
|
||||
|
||||
if (process.argv.includes('--self-test')) {
|
||||
if (problems.length) { console.error('PIN-HINT-FLOOR SELF-TEST: FAIL — the real code already fails; fix that first.\n' + problems.map((p) => ' ✗ ' + p).join('\n')); process.exit(1) }
|
||||
const poisons = [
|
||||
['the fold on every page (the old floor)', { ...real, pillFloor: ({ viewportH, sheetTop = Infinity, creditTop = Infinity }) => Math.min(viewportH, sheetTop, creditTop) }, SRC],
|
||||
['a page that never scrolls', { ...real, pageScrolls: () => false }, SRC],
|
||||
['a page that always scrolls', { ...real, pageScrolls: () => true }, SRC],
|
||||
['obstacles dropped on a scrolling page', { ...real, pillFloor: (o) => (o.scrolls ? Infinity : F.pillFloor(o)) }, SRC],
|
||||
['the old source line', real, SRC.replace(/const floor = pillFloor\([^\n]*\n/, 'const floor = Math.min(window.innerHeight, sheetFloor(r), creditFloor(r))\n')],
|
||||
]
|
||||
let missed = 0
|
||||
for (const [name, fns, src] of poisons) { const caught = audit(fns, src).length > 0; console.log(` ${name}: ${caught ? 'caught' : 'NOT CAUGHT'}`); if (!caught) missed++ }
|
||||
if (missed) { console.error(`PIN-HINT-FLOOR SELF-TEST: FAIL — ${missed} poison(s) not caught.`); process.exit(1) }
|
||||
console.log(`PIN-HINT-FLOOR SELF-TEST: PASS — all ${poisons.length} planted defects refused.`); process.exit(0)
|
||||
}
|
||||
if (problems.length) { console.error('PIN-HINT-FLOOR: FAIL\n' + problems.map((p) => ' ✗ ' + p).join('\n')); process.exit(1) }
|
||||
console.log('PIN-HINT-FLOOR: PASS — a scrolling page keeps the pill at the map\'s bottom; a full-screen page still lifts it above the fold; sheet and credit still lift it; PinHint uses pillFloor().')
|
||||
|
|
@ -50,6 +50,7 @@ import { tStatic } from '@map/i18n/index.js'
|
|||
const T = (key, en) => tStatic(key) || en
|
||||
import { useOptionalI18n } from '@map/composables/useOptionalT.js'
|
||||
import { useShadowStyles } from '../../composables/useShadowStyles.js'
|
||||
import { pillFloor, pageScrolls } from './pinHintFloor.js'
|
||||
|
||||
// ★ i18n IS OPTIONAL HERE, NOT REQUIRED. useI18n() THROWS ("Need to install with `app.use`
|
||||
// function") on any host that never installed vue-i18n, and seven bundles in this repo do not --
|
||||
|
|
@ -294,15 +295,16 @@ function clampIntoView() {
|
|||
el.style.removeProperty('--pin-hint-lift')
|
||||
const r = el.getBoundingClientRect()
|
||||
if (!r.height) return
|
||||
// The lowest thing the pill may touch: the fold, the top of the phone legend sheet when one is
|
||||
// open beneath it, or the top of the map's licence credit. Whichever is higher wins.
|
||||
// The lowest thing the pill may touch: the top of the phone legend sheet when one is open beneath
|
||||
// it, the top of the map's licence credit, or the fold, but the fold ONLY on a page that does not
|
||||
// scroll (pinHintFloor.js says why). Whichever is higher wins.
|
||||
// ★ THE CREDIT IS A FLOOR HERE TOO, not only a clearance in syncAttribClearance. That clearance
|
||||
// is re-measured only when the credit RESIZES, and on a phone the credit is also MOVED (lifted
|
||||
// above the legend bar) without resizing. MEASURED at 390 on DX Life's home page: the clearance
|
||||
// stayed at a stale 43px and the pill covered 36px of the 37px credit, while the same page, measured
|
||||
// later, sat at the cap. This check runs from the settle timers and the sheet and host observers,
|
||||
// so it reads where the credit IS, whatever the clearance says.
|
||||
const floor = Math.min(window.innerHeight, sheetFloor(r), creditFloor(r))
|
||||
const floor = pillFloor({ viewportH: window.innerHeight, scrolls: pageScrolls(), sheetTop: sheetFloor(r), creditTop: creditFloor(r) })
|
||||
const overflow = Math.round(r.bottom - floor)
|
||||
if (overflow > 0) el.style.setProperty('--pin-hint-lift', (overflow + 8) + 'px')
|
||||
} catch (_) { /* detached — nothing to clamp */ }
|
||||
|
|
|
|||
35
library/components/hints/pinHintFloor.js
Normal file
35
library/components/hints/pinHintFloor.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* pinHintFloor.js — the lowest point the pin-hint pill may reach (PinHint.vue's clampIntoView).
|
||||
*
|
||||
* The pill sits at the bottom of the map it belongs to. It is lifted only when something covers that
|
||||
* spot: the top of the phone legend sheet, the top of the map's licence credit, or, on an app-like page,
|
||||
* the bottom of the screen.
|
||||
*
|
||||
* ★ THE FOLD IS A FLOOR ONLY WHEN THE PAGE DOES NOT SCROLL. On a full-screen app page (a gallery pane,
|
||||
* a map leaf) the screen's bottom is the map's bottom, and a phone's browser bar can cover the last few
|
||||
* pixels, so the fold still counts there. On a page that SCROLLS (a partner home page with a map block in
|
||||
* it), the fold is only where the reader happens to be: a map whose bottom was below it on load lifted
|
||||
* the pill to mid-map, the settle timers and resize observers re-set it several times in the first
|
||||
* seconds, and a scroll never re-ran the clamp, so the pill hung mid-map and then dropped. There the
|
||||
* pill is placed once, at the map's bottom, whatever the scroll position.
|
||||
*
|
||||
* Pure (numbers in, a number out) so check:pin-hint-floor can prove both cases without a browser.
|
||||
*/
|
||||
|
||||
/** Does the page itself scroll (a map block in a longer page), as opposed to a full-screen app page? */
|
||||
export function pageScrolls(doc = typeof document !== 'undefined' ? document : null,
|
||||
win = typeof window !== 'undefined' ? window : null) {
|
||||
try {
|
||||
const se = doc && (doc.scrollingElement || doc.documentElement)
|
||||
return !!(se && win && se.scrollHeight > win.innerHeight + 1)
|
||||
} catch (_) { return false }
|
||||
}
|
||||
|
||||
/**
|
||||
* The floor in viewport pixels. Infinity means nothing below the pill's own spot is in the way.
|
||||
* @param {{ viewportH:number, scrolls:boolean, sheetTop?:number, creditTop?:number }} o
|
||||
*/
|
||||
export function pillFloor({ viewportH, scrolls, sheetTop = Infinity, creditTop = Infinity }) {
|
||||
const fold = scrolls ? Infinity : viewportH
|
||||
return Math.min(fold, sheetTop, creditTop)
|
||||
}
|
||||
|
|
@ -56,6 +56,8 @@
|
|||
"check:coverage-consistent:poison": "node internal/check-gospel-coverage-consistent.mjs --poison",
|
||||
"check:csv-shape": "node internal/check-csv-export-shape.mjs",
|
||||
"check:csv-shape:self-test": "node internal/check-csv-export-shape.mjs --self-test",
|
||||
"check:pin-hint-floor": "node internal/check-pin-hint-floor.mjs",
|
||||
"check:pin-hint-floor:self-test": "node internal/check-pin-hint-floor.mjs --self-test",
|
||||
"check:dashboard-filter-a11y": "node plugin-designs/research-dashboard/scripts/check-dashboard-filter-a11y.mjs",
|
||||
"check:dashboard-geocoder-provider": "node plugin-designs/research-dashboard/scripts/check-dashboard-geocoder-provider.mjs",
|
||||
"check:deck-click": "node internal/check-deck-click.mjs",
|
||||
|
|
@ -347,8 +349,8 @@
|
|||
"dev": "bun x vite",
|
||||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:pin-hint-floor:self-test",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-design-catalogue check:pin-hint-floor",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -456,6 +458,7 @@
|
|||
"check:country-sheet-one-path:self-test": "plants each defect check:country-sheet-one-path guards (a direct generateCountrySheet call, makeCountrySheet without countries, the old guard that skipped India, a position given to an absent code) and requires each to FAIL; a comment naming generateCountrySheet must NOT.",
|
||||
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
|
||||
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
||||
"check:pin-hint-floor": "the pin-hint pill sits at the map's bottom on a page that scrolls (the fold is not an obstacle there) and is still lifted above the fold on a full-screen page; the legend sheet and the licence credit still lift it; PinHint.vue takes its floor from pinHintFloor.js. Run the `check:pin-hint-floor:self-test` script for the NEGATIVE test",
|
||||
"check:dashboard-filter-a11y": "the dashboard's cross-filter is operable and audible: every filter chip is a <button type=\"button\"> with an aria-label naming the ACTION (a filter that can be set by keyboard must be clearable by keyboard), the shell carries exactly ONE always-rendered polite role=\"status\" region so a brush that changes a number far from the control is announced once rather than six times or not at all, and no resize handle's accessible name is a raw English literal. AST over the parsed templates plus the built bundle — no browser (rega's alone).",
|
||||
"check:dashboard-geocoder-provider": "the research dashboard leaf at ?provider=maplibre makes ZERO requests to api.mapbox.com, and its search bars still suggest (built tree; browser gate). It was kept out of gates:post while the generated leaf hard-linked the Mapbox engine; the leaf now loads only the engine it is asked for.",
|
||||
"check:dep-names": "every declared dep is a REAL package name (a rename once rewrote @capacitor/* and broke bun install in a clone)",
|
||||
|
|
|
|||
Loading…
Reference in a new issue