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))