The share menu's tabs mirrored the wrong way, and the check could not see them

Five physical CSS declarations in the chrome every production map renders, so
under Arabic each of them lands on the wrong side for every map at once:
SemanticTreeLegend's active tab chip drew its border on the left, the share
menu's step list padded from the left, its tab spacing and its auto-spacer both
pushed from the right, and the geocoder's search icon sat at left:7px inside an
input that mirrors. All five become logical properties, which in a left-to-right
reading resolve to exactly what they resolved to before: four insertions against
four deletions in the components, one property each, and the English view cannot
move.

The reason they survived is the more useful half. check-legend-logical-insets
existed and passed, but it read only library/components/legends -- the share menu
is one directory away, and a reader in Arabic does not know which folder a control
came from. So the scan now covers map-controls as well.

Widening it was not enough. It still passed, because its CSS extractor matched a
template literal with the pattern `([^`]*\{[^`]*\}[^`]*)`, which describes a
literal containing ONE brace pair. ShareButton keeps its styles in a 600-line
useShadowStyles literal with hundreds, so the scan captured seven fragments
totalling 7,003 characters of a 629-line file and never saw .share-tab at all. It
was reporting PASS on a directory it was barely reading, which is worse than
having no check: a green gate is read as evidence. Template literals are now
taken whole and kept only when they look like CSS -- 2 blocks, 8,462 characters,
and the rule is finally in scope.

The self-test poisons BOTH directories. Poisoning only a legend would have proved
the check works on the half it always covered, and passed unchanged the day the
wider scan silently narrowed again. Its map-controls arm puts back the exact
declaration this commit removes, and its failure message says what a failure
means.
This commit is contained in:
Doxa Maps 2026-09-29 03:25:22 +00:00
parent 9996bb6fde
commit 1b2c596011
4 changed files with 62 additions and 16 deletions

View file

@ -22,14 +22,26 @@ import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
const DIR = join(ROOT, 'library/components/legends')
// ★ THE SHARED CHROME, NOT ONLY THE LEGENDS. This check began as a legends check, and that scope is
// exactly why three physical declarations survived in ShareButton — the share menu is chrome every
// production map renders, but it lives one directory away and was never read. A reader in Arabic
// does not know which folder a control came from. Audited 2026-09-29: ShareButton had padding-left
// on the step list and margin-right on both the tab spacing and the auto-spacer, so under `ar` the
// gap fell on the wrong side of every share tab.
const DIRS = [
join(ROOT, 'library/components/legends'),
join(ROOT, 'library/components/map-controls'),
]
// Files are addressed as "<dir-name>/<file>" so two directories cannot collide on a base name and so
// the ALLOWED keys below stay unambiguous.
const shortOf = (dir, f) => `${dir.split('/').pop()}/${f}`
// file -> substrings of declarations that may stay physical, each with its reason.
const ALLOWED = {
'LegendMobile.vue': [
'legends/LegendMobile.vue': [
['.mapboxgl-ctrl-attrib.mapboxgl-compact{margin-right', "the engine's own attribution control, which the engine places"],
],
'LegendProgressBar.vue': [
'legends/LegendProgressBar.vue': [
['.lpb-pop{', 'the popover is placed by JS (popLeft) over the bar segment it describes'],
],
}
@ -38,7 +50,19 @@ const PHYSICAL = /(^|[;{\s])((?:padding|margin)-(?:left|right)|left|right)\s*:\s
function findings(file, src) {
const out = []
const styles = [...src.matchAll(/<style[^>]*>([\s\S]*?)<\/style>|`([^`]*\{[^`]*\}[^`]*)`/g)].map((m) => m[1] ?? m[2])
// ★ THE BACKTICK ARM USED TO MISS MOST OF A BIG SHADOW-CSS LITERAL, AND THAT IS HOW THREE PHYSICAL
// DECLARATIONS HID IN ShareButton. `([^`]*\{[^`]*\}[^`]*)` matches a literal containing ONE brace
// pair; a 600-line useShadowStyles(`…`) block has hundreds, so the scan captured 7 fragments
// totalling 7,003 chars of a 629-line file and never saw .share-tab's margin-right at all. The
// gate then reported PASS on a directory it was barely reading — the worst kind of green.
// A <style> block is still matched as before; a template literal is now taken WHOLE, from its
// opening backtick to its closing one, and only kept when it actually looks like CSS.
const styles = []
for (const m of src.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) styles.push(m[1])
for (const m of src.matchAll(/`([\s\S]*?)`/g)) {
const body = m[1]
if (/\{[\s\S]*:[\s\S]*\}/.test(body)) styles.push(body)
}
const allowed = ALLOWED[file] || []
for (const css of styles) {
const clean = css.replace(/\/\*[\s\S]*?\*\//g, '')
@ -59,25 +83,47 @@ function findings(file, src) {
return out
}
function eachFile() {
const out = []
for (const dir of DIRS) {
for (const f of readdirSync(dir).filter((x) => x.endsWith('.vue')).sort()) out.push([dir, f])
}
return out
}
function run(read) {
const all = []
for (const f of readdirSync(DIR).filter((f) => f.endsWith('.vue')).sort()) all.push(...findings(f, read(f)))
for (const [dir, f] of eachFile()) all.push(...findings(shortOf(dir, f), read(dir, f)))
return all
}
const readReal = (f) => readFileSync(join(DIR, f), 'utf8')
const readReal = (dir, f) => readFileSync(join(dir, f), 'utf8')
if (process.argv.includes('--self-test')) {
const poisoned = (f) => {
const s = readReal(f)
return f === 'SemanticTreeLegend.vue' ? s.replace('inset-inline-start: 8px; top: var(--stl-top', 'left: 8px; top: var(--stl-top') : s
const poisoned = (dir, f) => {
const s = readReal(dir, f)
if (f === 'SemanticTreeLegend.vue') return s.replace('inset-inline-start: 8px; top: var(--stl-top', 'left: 8px; top: var(--stl-top')
// ★ POISON THE NEW DIRECTORY TOO. The legends arm alone would still pass with map-controls
// unscanned, so a self-test that only poisons a legend proves the check works on the half it
// always covered. This one puts back the exact declaration the 2026-09-29 audit found live in
// the share menu — if the widened scan ever silently narrows again, this fails.
if (f === 'ShareButton.vue') return s.replace('margin-inline-end:20px;', 'margin-right:20px;')
return s
}
const hit = run(poisoned)
if (!hit.some((h) => h.includes('.stl-panel') && h.includes('left: 8px'))) {
const legendCaught = hit.some((h) => h.includes('.stl-panel') && h.includes('left: 8px'))
const controlCaught = hit.some((h) => h.includes('ShareButton.vue') && h.includes('margin-right'))
if (!legendCaught) {
console.error('LEGEND-LOGICAL-INSETS self-test: FAIL — a poisoned `.stl-panel { left: 8px }` was not caught')
process.exit(1)
}
console.log('LEGEND-LOGICAL-INSETS self-test: PASS — a physical `left: 8px` on .stl-panel is caught')
if (!controlCaught) {
console.error('LEGEND-LOGICAL-INSETS self-test: FAIL — a poisoned `margin-right:20px` in ShareButton was not caught.\n' +
' That means map-controls/ is not being scanned, which is the exact gap that let three physical\n' +
' declarations live in the share menu until 2026-09-29.')
process.exit(1)
}
console.log('LEGEND-LOGICAL-INSETS self-test: PASS — a physical inset is caught in BOTH scanned directories (legends and map-controls)')
process.exit(0)
}

View file

@ -777,7 +777,7 @@ useShadowStyles(`
}
.stl-tab-info:hover { background: rgba(59,70,61,0.2); color: #73A17F; }
.stl-tab-chip.active .stl-tab-info {
border-left-color: rgba(0,0,0,0.3); color: #fff;
border-inline-start-color: rgba(0,0,0,0.3); color: #fff;
background: rgba(0,0,0,0.2);
}
.stl-info-pop {

View file

@ -110,7 +110,7 @@ useShadowStyles(dualEngine(
'.mapboxgl-ctrl-geocoder{height:35px!important;min-height:35px!important;box-sizing:border-box!important;}'
+ '.mapboxgl-ctrl-geocoder--input{height:35px!important;min-height:35px!important;box-sizing:border-box!important;'
+ 'padding-top:0!important;padding-bottom:0!important;line-height:35px!important;}'
+ '.mapboxgl-ctrl-geocoder--icon-search{top:50%!important;transform:translateY(-50%)!important;margin-top:0!important;left:7px!important;}'),
+ '.mapboxgl-ctrl-geocoder--icon-search{top:50%!important;transform:translateY(-50%)!important;margin-top:0!important;inset-inline-start:7px!important;}'),
'geocoder-height-35'
)

View file

@ -49,7 +49,7 @@ useShadowStyles(`
@keyframes share-ring-turn{to{transform:rotate(360deg);}}
@media(prefers-reduced-motion:reduce){.share-wrap::before{animation:none;}}
.share-steps{margin:10px 14px 6px;padding:0 0 0 15px;list-style:'- ';font:14px/1.55 system-ui,sans-serif;color:#c9d1d9;}
.share-steps li{margin:0 0 3px;padding-left:3px;}
.share-steps li{margin:0 0 3px;padding-inline-start:3px;}
.share-steps li:last-child{font-weight:600;color:#92b195;}
.share-pop.light .share-steps{color:#3b463d;}
.share-pop.light .share-steps li:last-child{color:#3b463d;}
@ -73,7 +73,7 @@ useShadowStyles(`
.share-pop.light .share-tabs{border-bottom:1px solid rgba(0,0,0,0.08);}
.share-tab{
padding:6px 0;
margin-right:20px;
margin-inline-end:20px;
font-size:12.5px;
font-weight:600;
background:none;border:none;
@ -175,7 +175,7 @@ useShadowStyles(`
font-size:12px;
opacity:0.45;
font-weight:500;
margin-right:auto;
margin-inline-end:auto;
white-space:nowrap;
}
.share-social-btn{