The legend mirrors in Arabic: logical insets instead of left/right
The shared legend was placed with physical left/right, while the map controls use inset-inline-end. In Arabic the controls mirrored to the left, onto a legend that stayed left: on Simple Research, Prayer Progress and UPG-100 (rega's RTL survey at 52eeb0db; traced by upg100-plugin and repo). SemanticTreeLegend: the panel, its reopen button, the tree-guide hairlines and the export menu use inset-inline-start/end; the closed panel slides off to the right and its collapse arrow flips under :dir(rtl) (additive overrides, as VerticalTabsLayout does). LegendDesktop, LegendMobile, LegendRows, LegendTools and LegendProgressBar: the card, reopen, detail close/collapse buttons, the name gutters that clear them, indents and spacing are logical; LegendTools re-centres under RTL. LTR renders the same. check:legend-logical-insets (with :self-test, both in gates:post) fails on any physical left/right in the legend components unless it is symmetric, an auto reset, or allowed with a reason.
This commit is contained in:
parent
29f4ab7097
commit
2449098a68
8 changed files with 132 additions and 30 deletions
93
internal/check-legend-logical-insets.mjs
Normal file
93
internal/check-legend-logical-insets.mjs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-legend-logical-insets.mjs — the legend components position themselves with LOGICAL insets.
|
||||
*
|
||||
* WHY: the shared legend (.stl-panel, its reopen button, the detail buttons, the tree-guide hairlines) used
|
||||
* `left: 8px`. MapToolbar uses `inset-inline-end`, which mirrors under dir="rtl"; a physical `left` does not.
|
||||
* So in Arabic the controls moved left onto a legend that stayed left, on three production maps at once
|
||||
* (Simple Research, Prayer Progress, UPG-100; rega's survey at 52eeb0db). The fix is inset-inline-start /
|
||||
* inset-inline-end; this gate keeps a physical left/right from coming back.
|
||||
*
|
||||
* WHAT IT READS: every CSS declaration in library/components/legends/*.vue (comments stripped). A
|
||||
* declaration `left:` / `right:` / `padding-left|right:` / `margin-left|right:` fails unless it is:
|
||||
* - a centring or full-width rule, which is symmetric under RTL (`left:50%`, `left:0` beside `right:0`);
|
||||
* - a reset to auto (`left:auto`), which undoes rather than places;
|
||||
* - listed in ALLOWED below with the reason it may stay physical.
|
||||
* Inline :style bindings computed in JS (the progress bar's popover) are not CSS declarations and are not read.
|
||||
*
|
||||
* --self-test poisons a copy of one rule and asserts the gate notices: a gate that cannot fail proves nothing.
|
||||
*/
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
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')
|
||||
|
||||
// file -> substrings of declarations that may stay physical, each with its reason.
|
||||
const ALLOWED = {
|
||||
'LegendMobile.vue': [
|
||||
['.mapboxgl-ctrl-attrib.mapboxgl-compact{margin-right', "the engine's own attribution control, which the engine places"],
|
||||
],
|
||||
'LegendProgressBar.vue': [
|
||||
['.lpb-pop{', 'the popover is placed by JS (popLeft) over the bar segment it describes'],
|
||||
],
|
||||
}
|
||||
|
||||
const PHYSICAL = /(^|[;{\s])((?:padding|margin)-(?:left|right)|left|right)\s*:\s*([^;}]+)/g
|
||||
|
||||
function findings(file, src) {
|
||||
const out = []
|
||||
const styles = [...src.matchAll(/<style[^>]*>([\s\S]*?)<\/style>|`([^`]*\{[^`]*\}[^`]*)`/g)].map((m) => m[1] ?? m[2])
|
||||
const allowed = ALLOWED[file] || []
|
||||
for (const css of styles) {
|
||||
const clean = css.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
for (const rule of clean.split('}')) {
|
||||
const [sel = '', body = ''] = rule.split('{')
|
||||
const decl = sel.trim() + '{' + body
|
||||
for (const m of body.matchAll(PHYSICAL)) {
|
||||
const prop = m[2], val = m[3].trim().replace(/!important$/, '').trim()
|
||||
if (val === 'auto') continue
|
||||
if ((prop === 'left' || prop === 'right') && /^50%$/.test(val)) continue
|
||||
if (prop === 'left' && val === '0' && /right\s*:\s*0\b/.test(body)) continue
|
||||
if (prop === 'right' && val === '0' && /left\s*:\s*0\b/.test(body)) continue
|
||||
if (allowed.some(([s]) => decl.replace(/\s+/g, '').includes(s.replace(/\s+/g, '')))) continue
|
||||
out.push(`${file}: ${sel.trim().replace(/\s+/g, ' ').slice(0, 70)} { ${prop}: ${val} }`)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)))
|
||||
return all
|
||||
}
|
||||
|
||||
const readReal = (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 hit = run(poisoned)
|
||||
if (!hit.some((h) => h.includes('.stl-panel') && h.includes('left: 8px'))) {
|
||||
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')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const bad = run(readReal)
|
||||
if (bad.length) {
|
||||
console.error(`LEGEND-LOGICAL-INSETS: FAIL — ${bad.length} physical left/right declaration(s) in the legend components:`)
|
||||
for (const b of bad) console.error(' ✗ ' + b)
|
||||
console.error('\n A physical inset does not mirror under dir="rtl" while the map controls (inset-inline-end) do,')
|
||||
console.error(' so in Arabic they overlap. Use inset-inline-start / inset-inline-end (padding-/margin-inline-*),')
|
||||
console.error(' or add the rule to ALLOWED in this file with the reason it must stay physical.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('LEGEND-LOGICAL-INSETS: PASS — every legend inset is logical, symmetric, or allowed with a reason.')
|
||||
|
|
@ -38,7 +38,7 @@ useShadowStyles(`
|
|||
sit at exactly the same position+size — no visible jump when the user
|
||||
clicks a pin and the detail panel swaps in over the language-family legend
|
||||
(qa: 2026-05-02). */
|
||||
.legend-container{position:absolute;top:60px;left:8px;width:var(--map-legend-width,380px);max-height:calc(100vh - 80px);background:#fff;border-radius:var(--map-radius,16px);box-shadow:0 2px 8px rgba(0,0,0,0.12),0 1px 3px rgba(0,0,0,0.08);z-index:1000;overflow:hidden;display:flex;flex-direction:column;transition:width 200ms cubic-bezier(0.2,0,0,1),max-height 200ms cubic-bezier(0.2,0,0,1),min-width 200ms cubic-bezier(0.2,0,0,1),border-radius 200ms linear,box-shadow 200ms linear;will-change:width,max-height;}
|
||||
.legend-container{position:absolute;top:60px;inset-inline-start:8px;width:var(--map-legend-width,380px);max-height:calc(100vh - 80px);background:#fff;border-radius:var(--map-radius,16px);box-shadow:0 2px 8px rgba(0,0,0,0.12),0 1px 3px rgba(0,0,0,0.08);z-index:1000;overflow:hidden;display:flex;flex-direction:column;transition:width 200ms cubic-bezier(0.2,0,0,1),max-height 200ms cubic-bezier(0.2,0,0,1),min-width 200ms cubic-bezier(0.2,0,0,1),border-radius 200ms linear,box-shadow 200ms linear;will-change:width,max-height;}
|
||||
/* ── DESKTOP TYPE SCALE for the LegendRows-based card (Driver, 2026-09-16: larger, more
|
||||
readable desktop legends). The semantic-tree panel's scale lives in SemanticTreeLegend.vue
|
||||
(it renders without this card on some maps). Size only, never layout. ── */
|
||||
|
|
@ -52,7 +52,7 @@ useShadowStyles(`
|
|||
floating shadow, uppercase monospace font. Hover lifts + glows.
|
||||
Name track auto-sizes to fit title text. */
|
||||
/* ── Reopen pill (collapsed state) — mirrors STL .stl-reopen ── */
|
||||
.legend-reopen{position:absolute;left:8px;top:60px;z-index:1000;display:inline-flex;align-items:center;gap:8px;background:#0d1117;border:1px solid #30363d;border-radius:999px;color:#c9d1d9;cursor:pointer;height:36px;padding:0 14px 0 12px;box-shadow:0 4px 14px rgba(0,0,0,0.25);font:600 11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:0.06em;max-width:320px;transition:color 0.12s,background 0.12s,border-color 0.12s,transform 0.12s,box-shadow 0.12s;}
|
||||
.legend-reopen{position:absolute;inset-inline-start:8px;top:60px;z-index:1000;display:inline-flex;align-items:center;gap:8px;background:#0d1117;border:1px solid #30363d;border-radius:999px;color:#c9d1d9;cursor:pointer;height:36px;padding:0 14px 0 12px;box-shadow:0 4px 14px rgba(0,0,0,0.25);font:600 11px ui-monospace,monospace;text-transform:uppercase;letter-spacing:0.06em;max-width:320px;transition:color 0.12s,background 0.12s,border-color 0.12s,transform 0.12s,box-shadow 0.12s;}
|
||||
.legend-reopen:hover{color:#fff;background:#1c2333;border-color:#73A17F;box-shadow:0 6px 20px rgba(59,70,61,0.25);transform:translateY(-1px);}
|
||||
.legend-reopen-icon{color:#73A17F;flex-shrink:0;}
|
||||
.legend-reopen-label{display:inline-flex;align-items:center;gap:6px;min-width:0;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-transform:none;letter-spacing:0;font:600 12px system-ui,sans-serif;}
|
||||
|
|
@ -78,7 +78,7 @@ useShadowStyles(`
|
|||
.legend-container:not(.legend-dark) .legend-collapse-caret:hover{background:rgba(59,70,61,0.12);border-color:#3b463d;color:#3b463d;}
|
||||
|
||||
/* Detail-mode close button — top-right of the card */
|
||||
.detail-close-btn{position:absolute;top:10px;right:10px;background:none;border:none;padding:4px;cursor:pointer;color:#888;display:flex;align-items:center;justify-content:center;z-index:20;transition:color 0.2s ease;}
|
||||
.detail-close-btn{position:absolute;top:10px;inset-inline-end:10px;background:none;border:none;padding:4px;cursor:pointer;color:#888;display:flex;align-items:center;justify-content:center;z-index:20;transition:color 0.2s ease;}
|
||||
.detail-close-btn:hover{color:#333;}
|
||||
|
||||
/* Detail-mode collapse caret — top-left of the card, mirrors the inline
|
||||
|
|
@ -86,14 +86,14 @@ useShadowStyles(`
|
|||
above the PeopleGroupDetail sticky header (.detail-header z-index:10) so
|
||||
it is always tappable even when the detail panel scrolls. The button is
|
||||
only rendered in the detail-mode template branch (see <template> below). */
|
||||
.detail-collapse-btn{position:absolute;top:10px;left:10px;z-index:20;background:transparent;border:none;color:transparent;cursor:pointer;width:28px;height:28px;padding:0;outline:none;-webkit-tap-highlight-color:transparent;}
|
||||
.detail-collapse-btn{position:absolute;top:10px;inset-inline-start:10px;z-index:20;background:transparent;border:none;color:transparent;cursor:pointer;width:28px;height:28px;padding:0;outline:none;-webkit-tap-highlight-color:transparent;}
|
||||
.detail-collapse-btn:focus{outline:none;}
|
||||
/* Inside the detail panel: leave a 32px left gutter on the title text so the
|
||||
absolute-positioned .detail-collapse-btn at top:10px/left:10px does not
|
||||
overlap the people-group name. Targets the .pg-name-lead inside any
|
||||
.people-group-detail (which itself is rendered as a child of
|
||||
.legend-content). Right gutter clears the .detail-close-btn the same way. */
|
||||
.legend-container .people-group-detail .pg-name-lead{padding-left:28px;padding-right:32px;}
|
||||
.legend-container .people-group-detail .pg-name-lead{padding-inline-start:28px;padding-inline-end:32px;}
|
||||
|
||||
/* ── Content — no padding; left/right AND top/bottom are handled by the
|
||||
LegendRows grid (caret + trailing columns + .lrg-items padding 12px 0).
|
||||
|
|
@ -135,14 +135,14 @@ useShadowStyles(`
|
|||
.legend-item-clickable:hover{transform:translateX(4px);box-shadow:0 2px 4px rgba(0,0,0,0.1);}
|
||||
.legend-item-selected{outline:2px solid #333;outline-offset:2px;}
|
||||
.legend-item-text{flex:1;font-weight:500;color:#333;}
|
||||
.legend-item-count{font-size:var(--map-font-sm,11px);color:#555;background:rgba(0,0,0,0.08);padding:2px 7px;border-radius:10px;min-width:40px;text-align:right;margin-right:4px;}
|
||||
.legend-item-population{font-size:var(--map-font-sm,11px);color:#555;background:rgba(0,0,0,0.08);padding:2px 7px;border-radius:10px;min-width:52px;text-align:right;margin-right:4px;}
|
||||
.legend-item-count{font-size:var(--map-font-sm,11px);color:#555;background:rgba(0,0,0,0.08);padding:2px 7px;border-radius:10px;min-width:40px;text-align:right;margin-inline-end:4px;}
|
||||
.legend-item-population{font-size:var(--map-font-sm,11px);color:#555;background:rgba(0,0,0,0.08);padding:2px 7px;border-radius:10px;min-width:52px;text-align:right;margin-inline-end:4px;}
|
||||
.legend-export-btn{background:none;border:none;cursor:pointer;font-size:15px;padding:0 4px;}
|
||||
.legend-tree-item{display:flex;flex-direction:column;gap:4px;}
|
||||
.legend-tree-parent{display:flex;align-items:center;gap:6px;}
|
||||
.legend-tree-caret{width:16px;height:16px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform 0.2s;font-size:12px;color:#666;}
|
||||
.legend-tree-caret.expanded{transform:rotate(90deg);}
|
||||
.legend-tree-child{margin-left:20px;display:flex;gap:6px;}
|
||||
.legend-tree-child{margin-inline-start:20px;display:flex;gap:6px;}
|
||||
.legend-tree-caret-placeholder{width:16px;}
|
||||
.legend-item-child{font-size:12px;}
|
||||
|
||||
|
|
@ -151,10 +151,10 @@ useShadowStyles(`
|
|||
.legend-stats-label{flex:1;min-width:0;padding:0 10px;}
|
||||
/* Stats column values — 10px right padding aligns with header's last-of-type gap
|
||||
and gives a clean breathing space against the card's right edge. */
|
||||
.legend-stats-value{width:58px;text-align:right;flex-shrink:0;font-family:'Courier New',monospace;padding-right:10px;white-space:pre;}
|
||||
.legend-stats-value{width:58px;text-align:right;flex-shrink:0;font-family:'Courier New',monospace;padding-inline-end:10px;white-space:pre;}
|
||||
.legend-footer{display:flex;align-items:center;padding:5px 0;border-top:1px solid rgba(0,0,0,0.07);font-size:11px;font-weight:600;color:#555;background:#fff;}
|
||||
.legend-footer-label{flex:1;padding-left:10px;color:#777;}
|
||||
.legend-footer-value{width:56px;text-align:right;padding-right:10px;font-family:'Courier New',monospace;}
|
||||
.legend-footer-label{flex:1;padding-inline-start:10px;color:#777;}
|
||||
.legend-footer-value{width:56px;text-align:right;padding-inline-end:10px;font-family:'Courier New',monospace;}
|
||||
|
||||
/* ── Scrollbar ── */
|
||||
.legend-content::-webkit-scrollbar{width:4px;}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ useShadowStyles(`
|
|||
mobile chrome (pull tab, collapse/expand caret, touch drag). ── */
|
||||
/* bottom = 30% (the sheet's open height) + 16px gap. The Mapbox watermark lives bottom-right
|
||||
and only rises by the collapsed strip, so nothing else needs room under this image. */
|
||||
.floating-mini-image{position:absolute;left:16px;bottom:calc(30% + 16px);z-index:999;pointer-events:auto;cursor:pointer;}
|
||||
.floating-mini-image{position:absolute;inset-inline-start:16px;bottom:calc(30% + 16px);z-index:999;pointer-events:auto;cursor:pointer;}
|
||||
.mini-image{width:120px;height:150px;object-fit:cover;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.25);display:block;}
|
||||
|
||||
/* Containment root: clamps absolute children to the map area. */
|
||||
|
|
@ -57,7 +57,7 @@ useShadowStyles(`
|
|||
/* In the sheet the legend's rows fill the full width: the clear "x" column does not reserve
|
||||
space on the right — it floats over the row's right edge only when a row is selected. */
|
||||
.legend-mobile-sheet .stl-row{position:relative;}
|
||||
.legend-mobile-sheet .stl-row .stl-x-slot{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:auto;}
|
||||
.legend-mobile-sheet .stl-row .stl-x-slot{position:absolute;inset-inline-end:6px;top:50%;transform:translateY(-50%);width:auto;}
|
||||
.legend-mobile-sheet .stl-col-hdr .stl-x-slot{display:none;}
|
||||
|
||||
/* Sheet shell — no padding; drag strip and caret are absolutely
|
||||
|
|
@ -103,16 +103,16 @@ useShadowStyles(`
|
|||
on the right; gives the user an always-visible, finger-sized tap target
|
||||
to collapse the sheet without having to find the 12px drag-strip. z-index
|
||||
above .sheet-drag-strip (z:2) so the strip's @click doesn't steal taps. */
|
||||
.detail-collapse-btn-mobile{position:absolute;top:10px;left:12px;background:transparent;border:none;padding:0;cursor:pointer;color:transparent;width:28px;height:28px;z-index:3;outline:none;-webkit-tap-highlight-color:transparent;}
|
||||
.detail-collapse-btn-mobile{position:absolute;top:10px;inset-inline-start:12px;background:transparent;border:none;padding:0;cursor:pointer;color:transparent;width:28px;height:28px;z-index:3;outline:none;-webkit-tap-highlight-color:transparent;}
|
||||
.detail-collapse-btn-mobile:focus{outline:none;}
|
||||
|
||||
/* Detail-mode close X — top-right corner */
|
||||
.detail-close-btn{position:absolute;top:10px;right:12px;background:none;border:none;padding:4px;cursor:pointer;color:#666;display:flex;align-items:center;justify-content:center;z-index:3;transition:color 0.2s ease;}
|
||||
.detail-close-btn{position:absolute;top:10px;inset-inline-end:12px;background:none;border:none;padding:4px;cursor:pointer;color:#666;display:flex;align-items:center;justify-content:center;z-index:3;transition:color 0.2s ease;}
|
||||
.detail-close-btn:hover{color:#333;}
|
||||
/* The people-group name lead sits on the same line as that absolute close X
|
||||
(right:12px, 26px wide): reserve its width so a long name ellipsizes
|
||||
before the X instead of running underneath it. */
|
||||
.legend-mobile-sheet .pg-name-lead{padding-right:32px;}
|
||||
.legend-mobile-sheet .pg-name-lead{padding-inline-end:32px;}
|
||||
|
||||
/* Collapsed-detail footer — mimics LegendRows collapsed-state footer
|
||||
(caret + centered title) but for PeopleGroupDetail mode. Renders only
|
||||
|
|
@ -256,7 +256,7 @@ useShadowStyles(`
|
|||
keeps its desktop look.
|
||||
No backticks anywhere in this comment block — see the earlier note. */
|
||||
.legend-mobile-sheet .lft-row{min-height:44px;column-gap:8px;}
|
||||
.legend-mobile-sheet .lft-row-child{margin-left:0;}
|
||||
.legend-mobile-sheet .lft-row-child{margin-inline-start:0;}
|
||||
.legend-mobile-sheet .lft-item-inner{min-height:36px;padding:6px 8px;}
|
||||
.legend-mobile-sheet .lft-name{font-size:13px;}
|
||||
/* Collapsed-state row hiding lives in the unified block above (handles
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ useShadowStyles(`
|
|||
/* Popover — imitates .stl-info-pop (same font, colours, radius, border). */
|
||||
.lpb-pop{position:absolute;top:calc(100% + 6px);left:0;z-index:10;padding:7px 10px;background:#0d1117;border:1px solid #30363d;border-radius:6px;font:11px system-ui;color:#c9d1d9;line-height:1.45;white-space:nowrap;pointer-events:none;box-shadow:0 8px 24px rgba(0,0,0,0.5);}
|
||||
.lpb-light .lpb-pop{background:#f6f8fa;border-color:#d0d7de;color:#1f2328;box-shadow:0 8px 24px rgba(31,35,40,0.18);}
|
||||
.lpb-pop-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;vertical-align:-1px;box-shadow:0 0 0 1px rgba(0,0,0,0.4);}
|
||||
.lpb-pop-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-inline-end:6px;vertical-align:-1px;box-shadow:0 0 0 1px rgba(0,0,0,0.4);}
|
||||
/* Collapsed mobile sheet (LegendMobile.vue) shows only the 48px title strip — the
|
||||
bar must not take a second header line there. Same-shadow-root rule, owned here
|
||||
so the sheet needs no knowledge of the bar. */
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ useShadowStyles(`
|
|||
not absolute); the pill spans cols 2 to -2 so the trailing column stays
|
||||
empty as right padding. */
|
||||
.lrg-row{display:grid;grid-template-columns:subgrid;grid-column:1 / -1;align-items:center;}
|
||||
.lrg-row-child{margin-left:0;}
|
||||
.lrg-row-child{margin-inline-start:0;}
|
||||
|
||||
/* .lrg-item and .lrg-item-inner are layout-transparent on desktop so the
|
||||
subgrid stays flat. LegendMobile.vue overrides these at higher specificity
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@ useShadowStyles(`
|
|||
Pill-shaped wrapper, absolutely positioned ABOVE the legend card. Horizontally
|
||||
centered over the legend card by anchoring the left edge to the card origin
|
||||
(10px) + half the legend width, then translating back -50%. ── */
|
||||
.lt-root{position:absolute;top:14px;left:calc(10px + var(--map-legend-width,340px) / 2);transform:translateX(-50%);z-index:1050;display:flex;gap:8px;align-items:center;flex-wrap:nowrap;background:transparent;pointer-events:auto;}
|
||||
.lt-root{position:absolute;top:14px;inset-inline-start:calc(10px + var(--map-legend-width,340px) / 2);transform:translateX(-50%);z-index:1050;display:flex;gap:8px;align-items:center;flex-wrap:nowrap;background:transparent;pointer-events:auto;}
|
||||
.lt-root:dir(rtl){transform:translateX(50%);}
|
||||
@media(max-width:767px){
|
||||
/* Mobile uses the bottom-sheet legend; toolbar is hidden on phones. */
|
||||
.lt-root{display:none;}
|
||||
|
|
|
|||
|
|
@ -616,13 +616,17 @@ useShadowStyles(`
|
|||
rises into the vacated space instead of keeping a dead gap (Driver, 2026-09-23, UPG-100:
|
||||
"the search bar is removed, that means the legend needs to be moved up"). The max-height
|
||||
mirrors it so a raised panel gains the same room at the bottom it gained at the top. */
|
||||
position: absolute; left: 8px; top: var(--stl-top, 60px); width: var(--map-legend-width, 380px); z-index: 4;
|
||||
position: absolute; inset-inline-start: 8px; top: var(--stl-top, 60px); width: var(--map-legend-width, 380px); z-index: 4;
|
||||
display: flex; flex-direction: column;
|
||||
max-height: calc(100% - var(--stl-top, 60px) - 12px);
|
||||
transition: transform 0.25s cubic-bezier(0.2, 0, 0, 1), opacity 0.2s;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.stl-panel.closed { transform: translateX(-100%); opacity: 0; pointer-events: none; }
|
||||
/* RTL: the panel sits at the inline START (the right), so it slides off to the right and its collapse
|
||||
arrow points the other way. Additive :dir(rtl) overrides, as in VerticalTabsLayout. */
|
||||
.stl-panel.closed:dir(rtl) { transform: translateX(100%); }
|
||||
.stl-collapse-btn:dir(rtl) svg { transform: rotate(0deg); }
|
||||
/* Derived from --stl-top for the same reason as the base rule above: a fixed 72px here overflowed
|
||||
whenever the profile's real offset was larger (UPG-100 sets 58px + a 12px bottom margin = 70px of
|
||||
required clearance, against 72px only by luck; at --stl-top:8px it wasted 64px of panel height). */
|
||||
|
|
@ -831,31 +835,31 @@ useShadowStyles(`
|
|||
.stl-trunk { position: relative; flex-shrink: 0; width: 12px; align-self: stretch; }
|
||||
.stl-trunk-pipe::before {
|
||||
content: ''; position: absolute;
|
||||
left: 5px; top: 0; bottom: 0; width: 1px;
|
||||
inset-inline-start: 5px; top: 0; bottom: 0; width: 1px;
|
||||
background: var(--trunk-color, #6e7681);
|
||||
opacity: 0.45;
|
||||
}
|
||||
.stl-trunk-tee::before {
|
||||
content: ''; position: absolute;
|
||||
left: 5px; top: 0; bottom: 0; width: 1px;
|
||||
inset-inline-start: 5px; top: 0; bottom: 0; width: 1px;
|
||||
background: var(--trunk-color, #6e7681);
|
||||
opacity: 0.45;
|
||||
}
|
||||
.stl-trunk-tee::after {
|
||||
content: ''; position: absolute;
|
||||
left: 5px; top: 50%; height: 1px; width: 7px;
|
||||
inset-inline-start: 5px; top: 50%; height: 1px; width: 7px;
|
||||
background: var(--trunk-color, #6e7681);
|
||||
opacity: 0.55;
|
||||
}
|
||||
.stl-trunk-elbow::before {
|
||||
content: ''; position: absolute;
|
||||
left: 5px; top: 0; height: 50%; width: 1px;
|
||||
inset-inline-start: 5px; top: 0; height: 50%; width: 1px;
|
||||
background: var(--trunk-color, #6e7681);
|
||||
opacity: 0.45;
|
||||
}
|
||||
.stl-trunk-elbow::after {
|
||||
content: ''; position: absolute;
|
||||
left: 5px; top: calc(50% - 0.5px); height: 1px; width: 7px;
|
||||
inset-inline-start: 5px; top: calc(50% - 0.5px); height: 1px; width: 7px;
|
||||
background: var(--trunk-color, #6e7681);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
|
@ -918,7 +922,7 @@ useShadowStyles(`
|
|||
.stl-collapse-btn:hover { color: #c9d1d9; background: rgba(59,70,61,0.18); border-color: #73A17F; }
|
||||
.stl-collapse-btn svg { transition: transform 0.18s; transform: rotate(180deg); }
|
||||
.stl-reopen {
|
||||
position: absolute; left: 8px; top: 60px; z-index: 3;
|
||||
position: absolute; inset-inline-start: 8px; top: 60px; z-index: 3;
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
background: #0d1117; border: 1px solid #30363d;
|
||||
border-radius: 999px;
|
||||
|
|
@ -1032,7 +1036,7 @@ useShadowStyles(`
|
|||
/* ── Export affordance (top-right of titlebar) ──────────────────────────────
|
||||
margin-left:auto pins it to the right edge whether the titlebar shows the
|
||||
plain title (flex-shrink:0) or the flex:1 breadcrumb button. */
|
||||
.stl-export { margin-left: auto; position: relative; flex-shrink: 0; }
|
||||
.stl-export { margin-inline-start: auto; position: relative; flex-shrink: 0; }
|
||||
.stl-export-btn {
|
||||
display: inline-flex; align-items: center; gap: 5px;
|
||||
background: rgba(110,118,129,0.15); border: 1px solid #30363d; border-radius: 6px;
|
||||
|
|
@ -1048,7 +1052,7 @@ useShadowStyles(`
|
|||
/* Backdrop sits below the menu, above the panel — captures the next click. */
|
||||
.stl-export-backdrop { position: fixed; inset: 0; z-index: 9; }
|
||||
.stl-export-menu {
|
||||
position: absolute; right: 0; top: calc(100% + 6px); z-index: 10;
|
||||
position: absolute; inset-inline-end: 0; top: calc(100% + 6px); z-index: 10;
|
||||
min-width: 168px;
|
||||
display: flex; flex-direction: column;
|
||||
background: #0d1117; border: 1px solid #30363d; border-radius: 8px;
|
||||
|
|
|
|||
|
|
@ -115,6 +115,8 @@
|
|||
"check:leaves-mount": "node internal/check-gospel-leaves-mount.mjs",
|
||||
"check:leaves-mount:poison": "node internal/check-gospel-leaves-mount.mjs --poison",
|
||||
"check:legend-export-reachable": "node internal/check-legend-export-reachable.mjs",
|
||||
"check:legend-logical-insets": "node internal/check-legend-logical-insets.mjs",
|
||||
"check:legend-logical-insets:self-test": "node internal/check-legend-logical-insets.mjs --self-test",
|
||||
"check:legend-source": "node internal/check-legend-matches-source.mjs",
|
||||
"check:library-index": "node internal/check-library-index.mjs",
|
||||
"check:link-rot": "node internal/check-link-rot-sample.mjs",
|
||||
|
|
@ -221,7 +223,7 @@
|
|||
"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: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: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",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- 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: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",
|
||||
"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",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -332,6 +334,8 @@
|
|||
"check:leaf-engine-per-provider": "every leaf in the manifest at ?provider=maplibre makes ZERO requests to any *.mapbox.com host and a map leaf still draws; at its default every map leaf draws with the engine it drew with before (read from the canvas that drew). --baseline <tree> compares against another tree (e.g. main). Browser gate, @browser-slot-max-hold 2700.",
|
||||
"check:leaves-mount": "DID THE ELEMENT ACTUALLY MOUNT, on all five gospel leaves. Exists because a ReferenceError thrown during setup dies INSIDE the custom-element boundary: the page loads, every asset returns 200, pageerror fires zero times, and the element sits there with an empty shadow root. A healthy-looking dead page. This asks the cheap unambiguous question the rich gates are too slow to ask on every leaf — is anything there — in five steps that each name their own failure: tag defined, host present, shadowRoot attached, shadow root non-empty, visible text rendered. It does NOT trust the absence of console errors; zero errors is not the same as working. --poison for the negative test.",
|
||||
"check:legend-export-reachable": "the Export affordance must be reachable at BOTH widths. Selects a leaf row first (the control is gated on selectedNode) and opens the mobile sheet first (it starts closed). Asserts a REAL BOX, not CSS visibility — on mobile the elements report visibility:visible at 0x0. PASSES at both widths since the mobile sheet was given :export-enabled and :tabs. SKIPs when nothing is served.",
|
||||
"check:legend-logical-insets": "the legend components position themselves with LOGICAL insets (inset-inline-start/end, padding-/margin-inline-*). A physical left/right does not mirror under dir=\"rtl\" while the map controls (inset-inline-end) do, so in Arabic the controls overlapped a legend that stayed left, on Simple Research, Prayer Progress and UPG-100 at once. Symmetric rules (left:50%, left:0 with right:0), auto resets and a short ALLOWED list with reasons pass.",
|
||||
"check:legend-logical-insets:self-test": "poisons .stl-panel back to left:8px and asserts check:legend-logical-insets notices.",
|
||||
"check:legend-source": "the numbers the maps SHOW must match the numbers the live SOURCE has. Every other gate checks INTERNAL consistency — a page can agree with itself perfectly while its legend has stopped describing reality. Offline SKIPs (exit 0); only a reachable endpoint that disagrees fails.",
|
||||
"check:library-index": "the DERIVED library menu must not DRIFT from library/. REGENERATES the index into a throwaway tree (the generator, unmodified, over a symlinked library/) and diffs the BYTES — it used to compare mtimes, so `touch` or a rebase turned it red on a byte-identical doc. Fix: bun run library-index",
|
||||
"check:link-rot": "re-tests a stratified sample of links we already VERIFIED, because nothing else ever re-tests one: a publisher can retire a page and the map keeps offering it, still labelled 'link checked'. THREE BANDS, each measured rather than assumed, each with its controls in band every run: plain STATUS where a status is evidence (jesusfilm.org among them — it does 404 on the /watch/jesus/<slug> shape the map ships, and the old exclusion was inherited from the retired ?languageId= URL); API-MAPPED for live.bible.is, whose page answers 200 for any fileset while /api/bibles/<id> answers 404; and REDIRECT-LANDING for fivefish.org, where a published code lands on its numbered language page and an unknown one is swept to /regions. If a control misbehaves it reports PROVED NOTHING rather than a pass. Only knowgod.com is left out — byte-identical page for any URL; the verifier proves those through Cru's API. ~3 minutes, network.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue