doxa-maps/internal/check-square-map-controls.mjs
Doxa Maps d888b20d8b Gate the square controls against the engine's own stylesheet
check:square-map-controls, registered with a gates:post token.

It reads node_modules/maplibre-gl/dist/maplibre-gl.css and compares our
ported values against THAT FILE rather than against a remembered number,
which is possible only because the look the Driver asked for is the engine
default: DX-life adds NavigationControl and overrides nothing, so the target
is published.

It asserts the three deliberate deviations as deviations -- the engine's
29px is not adopted, there is no overflow:hidden, and a forced-colors block
ships -- so each stays a stated choice rather than decaying into a mistake
nobody can see.

And it asserts the blast radius: round is the default and none of the nine
MapToolbar consumers passes controls=. Opting demo-map in turns it red,
which is what makes the silence mean something.

Its CSS assertions read the text passed to useShadowStyles, not the file.
The first run went red on "we do not adopt 29px" because the prop's own doc
comment quotes the engine's rule, 29px and all -- a checker that cannot tell
a comment from a declaration keeps finding the documentation it stands on.
2026-09-27 06:04:08 +00:00

155 lines
11 KiB
JavaScript

#!/usr/bin/env node
// check-square-map-controls.mjs — `bun run check:square-map-controls`. Source-only, headless, no network.
//
// The square map-control variant: is it the ENGINE'S card, and does nothing else move?
//
// The Driver asked for square controls as an OPTION beside the round ones. The key fact that shaped
// the implementation is that the square look is not a design of DX-life's at all: their CountryMap.vue
// adds mapboxgl.NavigationControl + FullscreenControl and overrides NOTHING, so what he admired is the
// ENGINE'S DEFAULT grouped card. That makes the target published and exact rather than a matter of
// taste -- so this check reads the engine's own stylesheet out of node_modules and compares our ported
// values against it. A port checked against its SOURCE cannot drift with my memory of it.
//
// AND IT ASSERTS THE BLAST RADIUS, which is the half a visual review cannot see: round is the default
// and no consumer opts in, so all nine maps that use MapToolbar render exactly as before.
import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
// Each copy of this repo grades ITS OWN tree (check:no-abs-paths, 2026-09-24).
const REPO = process.env.DOXA_REPO || resolve(dirname(fileURLToPath(import.meta.url)), '..')
const rd = (p) => readFileSync(join(REPO, p), 'utf8')
let fails = 0
const ok = (c, m) => { console.log(` ${c ? 'ok ' : 'FAIL'} - ${m}`); if (!c) fails++ }
const TOOLBAR_SRC = rd('library/components/map-controls/MapToolbar.vue')
const BUTTON = rd('library/components/map-controls/MapControlButton.vue')
// ★ CSS ASSERTIONS READ THE STYLESHEET, NOT THE FILE. The first run of this check went red on
// "we do not adopt 29px" -- because the prop's own doc comment QUOTES the engine's rule, 29px and all.
// A comment is not a declaration, and a checker that cannot tell them apart will keep finding the
// documentation it is standing on. So the CSS tests run against the text actually passed to
// useShadowStyles; the prop and template tests still read the whole file, which is where they live.
function shadowCss(src) {
const at = src.indexOf('useShadowStyles(')
const open = src.indexOf('`', at)
const close = src.indexOf('`', open + 1)
return open < 0 || close < 0 ? '' : src.slice(open + 1, close)
}
const TOOLBAR = shadowCss(TOOLBAR_SRC)
if (!TOOLBAR.trim()) { console.log(' FAIL - could not extract MapToolbar\'s stylesheet; nothing below was measured'); process.exit(1) }
// ── 1. the ENGINE is the oracle, read from node_modules ─────────────────────────────────────────
const ENGINE = 'node_modules/maplibre-gl/dist/maplibre-gl.css'
if (existsSync(join(REPO, ENGINE))) {
const css = rd(ENGINE).replace(/\/\*[\s\S]*?\*\//g, ' ')
const rule = (sel) => {
const m = css.match(new RegExp(sel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*\\{([^}]*)\\}'))
return m ? m[1].replace(/\s+/g, ' ').trim() : null
}
const group = rule('.maplibregl-ctrl-group')
const filled = rule('.maplibregl-ctrl-group:not(:empty)')
const divider = rule('.maplibregl-ctrl-group button+button')
console.log(`\n the engine's own rules, read from ${ENGINE}:`)
console.log(` group { ${group} }`)
console.log(` filled { ${filled} }`)
console.log(` divider { ${divider} }`)
const radius = (group || '').match(/border-radius:\s*([^;]+)/)
ok(!!radius && radius[1].trim() === '4px', `the engine's group radius is 4px, and ours is too`)
ok(/border-radius:\s*4px;/.test(TOOLBAR), 'our .map-toolbar--square carries border-radius:4px')
const ring = (filled || '').match(/box-shadow:\s*([^;]+)/)
ok(!!ring, 'the engine states a ring on a non-empty group')
ok(/box-shadow:\s*0 0 0 2px rgba\(0, 0, 0, 0\.1\)/.test(TOOLBAR),
`our ring matches the engine's 0 0 0 2px rgba(0,0,0,.1) (engine: ${ring && ring[1]})`)
ok(/border-top:\s*1px solid #ddd/.test(TOOLBAR),
`our divider matches the engine's 1px solid #ddd (engine: ${divider})`)
// ★ the ONE value deliberately NOT ported
const btn = rule('.maplibregl-ctrl-group button')
const eng = (btn || '').match(/height:\s*(\d+)px/)
ok(!!eng && eng[1] === '29', `the engine sizes its buttons ${eng && eng[1]}px`)
ok(!new RegExp('height:\\s*29px').test(TOOLBAR) && !new RegExp('width:\\s*29px').test(TOOLBAR),
'and we DO NOT adopt 29px -- the variant takes the SHAPE and leaves the SIZE, so the touch target is not cut')
ok(/--map-btn-size/.test(TOOLBAR), 'our size still comes from --map-btn-size')
} else {
console.log(`\n NOT MEASURED against the engine: ${ENGINE} is absent, so the ported values were not compared to their source.`)
fails++ // a missing oracle is not a pass
}
// ── 2. the grouped card is a CONTAINER, not a radius swap ───────────────────────────────────────
ok(/\.map-toolbar--square\s*\{[^}]*gap:\s*0/.test(TOOLBAR), 'the square toolbar sets gap:0 -- one panel, not a spaced column')
ok(/\.map-toolbar--square\s*\{[^}]*background:\s*#fff/.test(TOOLBAR), 'the PANEL owns the background')
ok(/\.map-toolbar--square \.mcb[^{]*\{[^}]*background:\s*transparent/.test(TOOLBAR), 'and the buttons own none of it')
ok(/\.map-toolbar--square \.mcb[^{]*\{[^}]*box-shadow:\s*none/.test(TOOLBAR), 'the buttons drop their own shadow, so there is one ring not N')
ok(/\.map-toolbar--square > \* \+ \*\s*\{[^}]*border-top/.test(TOOLBAR),
'the divider is on the toolbar CHILDREN, so it still lands where two different control types meet')
ok(/\.map-toolbar--square \.hbg-ctrl\s*\{[^}]*margin:\s*0/.test(TOOLBAR),
"the hamburger wrapper's 10px margin is zeroed -- it would otherwise tear the panel apart")
// ★ no overflow:hidden, and the reason is a focus ring
ok(!/\.map-toolbar--square\s*\{[^}]*overflow:\s*hidden/.test(TOOLBAR),
'the square panel does NOT use overflow:hidden -- it would clip a focus ring on any inner button')
ok(/:first-child[^{]*\{[^}]*border-radius:\s*4px 4px 0 0/.test(TOOLBAR) &&
/:last-child[^{]*\{[^}]*border-radius:\s*0 0 4px 4px/.test(TOOLBAR),
'outer corners come from first/last child rounding instead, which is the engine\'s own mechanism')
ok(/:only-child[^{]*\{[^}]*border-radius:\s*4px/.test(TOOLBAR), 'and a lone control is rounded on all four corners')
// ── 3. forced colours, which the round buttons never had ────────────────────────────────────────
ok(/@media \(forced-colors: active\)/.test(TOOLBAR), 'the square variant ships a forced-colors block')
ok(/ButtonText/.test(TOOLBAR), 'using ButtonText, so the ring and dividers survive a replaced palette')
// ── 4. the standalone per-button override ───────────────────────────────────────────────────────
ok(/shape:\s*\{ type: String,\s*default: 'round' \}/.test(BUTTON), "MapControlButton's shape defaults to 'round'")
ok(/\.mcb--square\{ border-radius:4px; \}/.test(BUTTON), 'a standalone square takes the engine\'s 4px corners')
ok(/'mcb--square': shape === 'square'/.test(BUTTON), 'and the class is bound to the prop')
// ── 5. BLAST RADIUS: round is the default and nobody opted in ────────────────────────────────────
ok(/controls:\s*\{ type: String, default: 'round' \}/.test(TOOLBAR_SRC), "MapToolbar's controls prop defaults to 'round'")
const CONSUMERS = [
['doxa-simple-map', 'plugin-designs/doxa-simple-map/doxa-simple-map.vue', 8],
['resources-map', 'plugin-designs/gospel-resources/resources-map.vue', 5],
['upg-100-list', 'plugin-designs/my-upg-100-list/upg-100-list.vue', 6],
['countries-map', 'plugin-designs/country-posters/countries-map.vue', 5],
['DashboardMapPanel', 'plugin-designs/research-dashboard/src/components/dashboards/DashboardMapPanel.vue', 2],
['demo-map', 'plugin-designs/demo-map/demo-map.vue', 5],
['connectivity-map', 'plugin-designs/cities-internet/connectivity-map.vue', 5],
['research-map', 'plugin-designs/doxa-research-map/research-map.vue', 7],
['all-people-groups', 'plugin-designs/all-people-groups/all-people-groups.vue', 1],
]
let optedIn = 0
for (const [name, path] of CONSUMERS) {
const src = rd(path)
const m = src.match(/<MapToolbar[^>]*controls=/)
if (m) { optedIn++; console.log(` ${name} opts in`) }
}
ok(optedIn === 0, `none of the ${CONSUMERS.length} MapToolbar consumers passes controls=, so every one renders exactly as before (${optedIn} opted in)`)
// ── 6. THE GEOMETRY, for rega to confirm with eyes ──────────────────────────────────────────────
// A square toolbar is SHORTER, because the gaps become dividers. That is the one measurable knock-on:
// MapControlMenu's matchToolbar limits a menu to the toolbar column's height.
const BTN = 32, GAP_DESKTOP = 8, GAP_MOBILE = 6
console.log('\n toolbar column height, round vs square (button 32px; ring is box-shadow, so outside layout):')
for (const [name, , n] of CONSUMERS) {
const round = n * BTN + (n - 1) * GAP_DESKTOP
const square = n * BTN + (n - 1) * 1 // 1px dividers replace the 8px gaps
console.log(` ${String(n).padStart(2)} control(s) ${name.padEnd(18)} round ${String(round).padStart(3)}px square ${String(square).padStart(3)}px shorter by ${round - square}px`)
}
const five = { round: 5 * BTN + 4 * GAP_DESKTOP, square: 5 * BTN + 4 }
ok(five.square < five.round,
`a 5-control toolbar goes ${five.round}px -> ${five.square}px, so a matchToolbar menu gets ${five.round - five.square}px shorter -- rega should confirm the menu still fits its content`)
console.log(` at mobile gap ${GAP_MOBILE}px a 5-control toolbar is ${5 * BTN + 4 * GAP_MOBILE}px round vs ${five.square}px square`)
// ── 7. NEG CONTROLS ─────────────────────────────────────────────────────────────────────────────
ok(!/\.map-toolbar\s*\{[^}]*border-radius/.test(TOOLBAR),
'NEG CONTROL: the DEFAULT .map-toolbar has no border-radius, so the square rules are genuinely scoped to the variant')
ok(/border-radius:\s*50%/.test(BUTTON),
'NEG CONTROL: the round button is still a 50% circle -- the default was not quietly changed')
ok(!/controls === 'round'/.test(TOOLBAR_SRC),
'NEG CONTROL: nothing branches on round, so round is the ABSENCE of the square class rather than a second code path')
console.log(fails ? `\nSQUARE-CONTROLS: ${fails} FAIL(s)` : '\nSQUARE-CONTROLS: the variant matches the engine\'s own rules, keeps our touch target, and changes no consumer')
process.exit(fails ? 1 : 0)