From 53505aa03db64db049f9eba5b42183414ef0860e Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 18:17:55 +0000 Subject: [PATCH] Separate the backdrop's layout floor from its styling, and assert the defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sr caught a layout change hiding inside a styling one. dx-life's root treatment carries min-height:400px, and shipping that as part of backdrop:'root' was tempting because it came with it — but in a scroll stack a min-height is a FLOOR on the grid band, not a backdrop rule. An empty or heavily-filtered grid that collapses to a short band today would have become 400px tall with a world map behind it. That is the shape of thing a reader finds before we do. The floor is now its own opt-in prop, named for what it does. And the gate sr asked for. "Every default reproduces today's grid exactly" is the entire safety argument for putting three consumers on one component, and it is exactly the kind of claim that is true when written and false three props later — the grid ships in Big Picture, Simple Research and the studio preview, so a drifted default moves all three at once, silently, because none of them passes the prop that changed. It reads the defaults through the SFC and JS parsers rather than by grep, since a default is an AST node and a regex cannot tokenise JavaScript. It also asserts the 400px is not hardcoded anywhere in the backdrop, which is the specific defect above rather than a general rule. Falsified: flipping the backdrop default to 'root' turns it red. A negative control pins that the reader really sees defaults, so the arms are not vacuous. Simple Research's own answer is recorded in the shape of the code: their filters stay non-collapsible by default, because a collapsed panel on a map-plus-grid hides the control a reader came to use. --- internal/check-grid-merge-defaults.mjs | 66 +++++++++++++++++++ package.json | 3 +- .../people-groups-grid/people-groups-grid.vue | 9 ++- 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 internal/check-grid-merge-defaults.mjs diff --git a/internal/check-grid-merge-defaults.mjs b/internal/check-grid-merge-defaults.mjs new file mode 100644 index 000000000..eb91b7a3d --- /dev/null +++ b/internal/check-grid-merge-defaults.mjs @@ -0,0 +1,66 @@ +#!/usr/bin/env node +/** + * check-grid-merge-defaults — the people-group grid's merge props must DEFAULT to today's grid. + * + * ★ WHY THIS EXISTS, in sr's words: "every default reproduces today's grid exactly" is the whole + * safety argument for merging three consumers onto one component, and it is exactly the kind of + * claim that is true when written and false three props later. The grid ships in Big Picture, + * Simple Research and the studio preview; a default that drifts moves all three at once, silently, + * because nothing in those hosts passes the prop that changed. + * + * Parser-based, not grep: a regex cannot tokenise JavaScript, and a default is an AST node. + */ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' + +const GRID = join(dirname(fileURLToPath(import.meta.url)), '..', + 'plugin-designs', 'people-groups-grid', 'people-groups-grid.vue') +let fails = 0 +const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ } + +const { parse: parseSfc } = await import('@vue/compiler-sfc') +const { parse: parseJs } = await import('@babel/parser') +const sfc = parseSfc(readFileSync(GRID, 'utf8')) +const ast = parseJs((sfc.descriptor.scriptSetup || sfc.descriptor.script).content, { sourceType: 'module' }) + +const defaults = new Map() +const walk = (n) => { + if (!n || typeof n !== 'object') return + if (n.type === 'CallExpression' && n.callee?.name === 'defineProps' && n.arguments[0]?.type === 'ObjectExpression') { + for (const pr of n.arguments[0].properties) { + const key = pr.key?.name || pr.key?.value + const def = pr.value?.properties?.find((q) => (q.key?.name || q.key?.value) === 'default') + defaults.set(key, def ? def.value : undefined) + } + } + for (const k of Object.keys(n)) { + const v = n[k] + if (Array.isArray(v)) v.forEach(walk); else if (v && typeof v.type === 'string') walk(v) + } +} +walk(ast.program) +const lit = (name) => { const n = defaults.get(name); return n && 'value' in n ? n.value : undefined } + +ok(defaults.size > 0, `the grid declares ${defaults.size} props (the reader is not vacuous)`) + +// ── the merge props, each defaulting to what the grid did BEFORE the merge ────────────────────── +ok(defaults.has('filtersToggle') && lit('filtersToggle') === 'button', + "filtersToggle defaults to 'button' — the bordered pill with its count badge, which is what the three shipping hosts render today") +ok(defaults.has('backdrop') && lit('backdrop') === 'filters', + "backdrop defaults to 'filters' — the watermark stays scoped to the filters block, as today") +ok(defaults.has('backdropFloor') && lit('backdropFloor') === '', + 'backdropFloor defaults to EMPTY, so no min-height is imposed: a floor is a LAYOUT change and must be asked for by name, never arrive inside a styling one (sr)') + +// ★ THE FLOOR MUST NOT BE HARDCODED ANYWHERE IN THE BACKDROP. This is the defect sr caught: the +// regions treatment carries min-height:400px, and shipping it as part of 'root' would make an +// empty or heavily-filtered grid 400px tall with a world map behind it, inside a scroll stack. +const src = readFileSync(GRID, 'utf8') +ok(!/minHeight:\s*'400px'/.test(src), + 'the 400px floor is not hardcoded into the root backdrop') + +// NEG CONTROL: the reader really does see defaults, so the arms above can fail. +ok(lit('profileConfig') === '', "NEG CONTROL: a known existing default still reads correctly (profileConfig '')") + +console.log(fails ? `\nGRID-MERGE-DEFAULTS: ${fails} FAIL(s)` : '\nGRID-MERGE-DEFAULTS: every merge prop defaults to the pre-merge grid') +process.exit(fails ? 1 : 0) diff --git a/package.json b/package.json index 1fdb573e1..8652ae745 100644 --- a/package.json +++ b/package.json @@ -149,6 +149,7 @@ "check:link-rot": "node internal/check-link-rot-sample.mjs", "check:linked-vocabulary": "node plugin-designs/research-map-plus-grid/scripts/check-linked-vocabulary.mjs", "check:list-prop-unit": "node internal/check-list-prop-unit.mjs", + "check:grid-merge-defaults": "node internal/check-grid-merge-defaults.mjs", "check:list-selection": "node internal/check-list-selection.mjs", "check:locale-reachable": "node internal/check-locale-reachable.mjs", "check:manifest-covers": "node internal/check-manifest-covers-discovery.mjs", @@ -283,7 +284,7 @@ "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: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:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:frozen-labels", - "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:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist 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", + "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:grid-merge-defaults check:selector-fields check:upg-zone-joins check:gospel-set-charts check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist 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", "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", diff --git a/plugin-designs/people-groups-grid/people-groups-grid.vue b/plugin-designs/people-groups-grid/people-groups-grid.vue index 52d21072a..1abecdb0f 100644 --- a/plugin-designs/people-groups-grid/people-groups-grid.vue +++ b/plugin-designs/people-groups-grid/people-groups-grid.vue @@ -91,6 +91,13 @@ const props = defineProps({ // the LIST ROOT at background-size 60%, background-position top, min-height 400px. 'none' = off. // Same asset either way — assets/images/worldmap.svg — so this is placement, not a second copy. backdrop: { type: String, default: 'filters' }, + // ★ THE FLOOR IS SEPARATE FROM THE BACKDROP, and sr caught why. dx-life's root treatment carries + // min-height:400px, and it is tempting to ship that as part of 'root' since it came with it. + // But in a scroll stack a min-height is a LAYOUT FLOOR on the grid band, not a backdrop rule: an + // empty or heavily-filtered grid that collapses to a short band today would become 400px tall + // with a world map behind it. That is a layout change arriving inside a styling change — the + // shape of thing a reader finds before we do. So it is opt-in and named for what it does. + backdropFloor: { type: String, default: '' }, prayBaseUrl: { type: String, default: '' }, languageCode: { type: String, default: '' }, // ★ default '' NOT '/research/': a non-empty prop default is ALWAYS truthy, so the computed @@ -364,7 +371,7 @@ const rootBackdrop = computed(() => { backgroundSize: '60%', backgroundPosition: 'top', backgroundRepeat: 'no-repeat', - minHeight: '400px', + ...(props.backdropFloor ? { minHeight: props.backdropFloor } : null), } }) const useSearch = computed(() => truthy(props.showSearch))