Every animating library component now honours prefers-reduced-motion itself
17 of 25 library components that ship motion carried no reduced-motion rule, so
they animated regardless of the reader's OS setting.
The reason it cannot be fixed once, centrally: a @media (prefers-reduced-motion)
rule in the host document does not cross a shadow boundary, and these render
inside one. Measured while surveying: NO generated leaf page ships such a rule
at all — 0 occurrences across the whole built tree — so there was not even a
page rule to inherit. Each component owns its own.
CSS-ONLY AND ADDITIVE. Nothing but new @media blocks; no existing rule changed.
Verified by diff: 0 real removals across 17 files. The two lines git reports as
removed are a trailing comma moving as a new string was chained onto an existing
concatenation, and both original rules are still present — checked by grepping
for them, not by reading the diff.
WHERE MOTION CARRIES MEANING, it is kept rather than hidden:
· SemanticTreeLegend's spinner MEANS "loading". Stopped dead it is a static
ring that says nothing, so it is slowed to a step crawl (2.4s, steps(8)) —
visibly working, no smooth spin.
· LegendProgressBar's only motion is a hover filter, so it simply stops; the
bar's FILL is geometry, not animation, and still reports its value.
The rule goes where each component's styles actually go, and that varies more
than expected: 13 append into a useShadowStyles template literal, 3 into
string-concatenated sheets, and VerticalTabsLayout into its VTL_SHADOW_CSS
constant AND its duplicate <style> block, because its own header says the two
copies must stay identical.
check:reduced-motion asserts both halves, each mutation-tested to fail before I
trusted a pass: strip the rule from an animating component and it fails; move a
rule from the injected sheet into a <style> block and it fails too, because on a
useShadowStyles component that rule would never reach the shadow root. Both
mutated files restored byte-identical.
Two of my own errors caught by tooling rather than by review, and worth naming:
my first patch asserted useShadowStyles existed in every target and RAISED on
VerticalTabsLayout instead of silently skipping it — the assert-the-anchor habit
from the last registration failure working as intended. Then I appended CSS
after the label argument on two concat-built sheets, corrupting the injection
key; check:shadow-css-parses named both files and the fix was to insert before
the label. A gate I did not write caught what I got wrong.
Registered in all three places and verified by re-reading the parsed
package.json.
This commit is contained in:
parent
094647ebee
commit
36f2818b37
19 changed files with 194 additions and 3 deletions
68
internal/check-reduced-motion.mjs
Normal file
68
internal/check-reduced-motion.mjs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* check-reduced-motion.mjs — a library component that animates must carry its own reduced-motion rule.
|
||||||
|
*
|
||||||
|
* ★ WHY IT CANNOT BE DONE ONCE, AT THE PAGE. A @media (prefers-reduced-motion) rule in the host
|
||||||
|
* document does not cross a shadow boundary, and these components render inside one. Measured while
|
||||||
|
* writing this: NO generated leaf page ships such a rule at all — 0 occurrences across the whole
|
||||||
|
* built tree — so there is not even a page rule to inherit. Every animating component owns its own.
|
||||||
|
*
|
||||||
|
* ★ WHAT COUNTS AS ANIMATING: a transition:, an animation:, or an @keyframes in the component's
|
||||||
|
* source. Motion declared in a template literal, a concatenated string or a <style> block all count
|
||||||
|
* — the mechanism varies across this library and the reader does not care which one was used.
|
||||||
|
*
|
||||||
|
* ★ THE RULE MUST LIVE WHERE THE STYLES DO. A component whose CSS is injected through
|
||||||
|
* useShadowStyles but whose reduced-motion rule sits in a <style> block has written a rule that
|
||||||
|
* never reaches the shadow root. That is the defect this repo already knows about from the other
|
||||||
|
* direction (composables/useShadowStyles.js), so it is asserted rather than trusted.
|
||||||
|
*/
|
||||||
|
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
|
||||||
|
const ROOT = new URL('../library/', import.meta.url).pathname
|
||||||
|
const files = []
|
||||||
|
const walk = (d) => {
|
||||||
|
for (const e of readdirSync(d, { withFileTypes: true })) {
|
||||||
|
if (e.name === 'node_modules' || e.name.startsWith('.')) continue
|
||||||
|
const p = join(d, e.name)
|
||||||
|
if (e.isDirectory()) walk(p)
|
||||||
|
else if (e.name.endsWith('.vue')) files.push(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(ROOT)
|
||||||
|
|
||||||
|
const MOTION = /transition\s*:|animation\s*:|@keyframes/
|
||||||
|
const fail = []
|
||||||
|
let animating = 0
|
||||||
|
|
||||||
|
for (const p of files) {
|
||||||
|
const src = readFileSync(p, 'utf8')
|
||||||
|
if (!MOTION.test(src)) continue
|
||||||
|
// A component whose only "motion" is the reduced-motion rule itself is not animating.
|
||||||
|
const withoutRule = src.replace(/@media\s*\(prefers-reduced-motion[^{]*\{[\s\S]*?\n\}/g, '')
|
||||||
|
if (!MOTION.test(withoutRule)) continue
|
||||||
|
animating++
|
||||||
|
const rel = p.split('/library/')[1]
|
||||||
|
|
||||||
|
if (!src.includes('prefers-reduced-motion')) {
|
||||||
|
fail.push(`${rel}: ships motion and carries NO prefers-reduced-motion rule`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If the component injects through useShadowStyles, the rule must be in the injected CSS —
|
||||||
|
// a rule stranded in a <style> block would never reach the shadow root.
|
||||||
|
const injects = /useShadowStyles\s*\(/.test(src)
|
||||||
|
if (injects) {
|
||||||
|
const styleAt = src.search(/^<style/m)
|
||||||
|
const ruleIdxs = [...src.matchAll(/prefers-reduced-motion/g)].map((m) => m.index)
|
||||||
|
const anyInjected = ruleIdxs.some((i) => styleAt === -1 || i < styleAt)
|
||||||
|
if (!anyInjected) {
|
||||||
|
fail.push(`${rel}: injects via useShadowStyles but its reduced-motion rule is only in a <style> block, which is dropped in a shadow root`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fail.length) {
|
||||||
|
console.error('REDUCED-MOTION: FAIL')
|
||||||
|
for (const f of fail) console.error(` ✗ ${f}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
console.log(`REDUCED-MOTION: PASS — ${animating} animating library component(s), each carrying its own rule where its styles are injected.`)
|
||||||
|
|
@ -171,6 +171,14 @@ useShadowStyles(`
|
||||||
.dark-mode .description-text{color:#d1d5db;}
|
.dark-mode .description-text{color:#d1d5db;}
|
||||||
.description-text.empty{color:#999;font-style:italic;}
|
.description-text.empty{color:#999;font-style:italic;}
|
||||||
.dark-mode .description-text.empty{color:#6b7280;}
|
.dark-mode .description-text.empty{color:#6b7280;}
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.pgd-root, .pgd-card, .pgd-close, .pgd-img { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'legend-people-group-detail')
|
`, 'legend-people-group-detail')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,14 @@ useShadowStyles(`
|
||||||
.rm-btn:hover { background:#9aa6ff; }
|
.rm-btn:hover { background:#9aa6ff; }
|
||||||
.rm-btn.ghost { background:transparent;color:#a8b2bd;border:1px solid #2c343f; }
|
.rm-btn.ghost { background:transparent;color:#a8b2bd;border:1px solid #2c343f; }
|
||||||
.rm-btn.ghost:hover { background:#232a33;color:#e7ebf0; }
|
.rm-btn.ghost:hover { background:#232a33;color:#e7ebf0; }
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.rmsm-root, .rmsm-item { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'research-map-side-menu')
|
`, 'research-map-side-menu')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,14 @@ useShadowStyles(`
|
||||||
.smd-reload-btn:hover { background: rgba(0,0,0,0.09); color: #222; }
|
.smd-reload-btn:hover { background: rgba(0,0,0,0.09); color: #222; }
|
||||||
.smd-drawer.dark .smd-reload-btn { border-color: rgba(255,255,255,0.12); background: rgba(255,255,255,0.06); color: #ccc; }
|
.smd-drawer.dark .smd-reload-btn { border-color: rgba(255,255,255,0.12); background: rgba(255,255,255,0.06); color: #ccc; }
|
||||||
.smd-drawer.dark .smd-reload-btn:hover { background: rgba(255,255,255,0.12); color: #fff; }
|
.smd-drawer.dark .smd-reload-btn:hover { background: rgba(255,255,255,0.12); color: #fff; }
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.smd-drawer, .smd-backdrop, .smd-item { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'side-menu-drawer')
|
`, 'side-menu-drawer')
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,8 @@ useShadowStyles(
|
||||||
'.seg button:hover{ color:var(--ui-text,#3b463d); }' +
|
'.seg button:hover{ color:var(--ui-text,#3b463d); }' +
|
||||||
'.seg button:hover::after{ background:var(--ui-border,#d1d4cd); }' +
|
'.seg button:hover::after{ background:var(--ui-border,#d1d4cd); }' +
|
||||||
'.seg button[aria-pressed="true"]{ color:var(--ui-primary,#92b195); font-weight:600; }' +
|
'.seg button[aria-pressed="true"]{ color:var(--ui-primary,#92b195); font-weight:600; }' +
|
||||||
'.seg button[aria-pressed="true"]::after{ background:var(--ui-primary,#92b195); }',
|
'.seg button[aria-pressed="true"]::after{ background:var(--ui-primary,#92b195); }'
|
||||||
|
+ '/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,\n and no generated leaf ships one anyway (measured: 0 across the whole tree). Additive only. */\n@media (prefers-reduced-motion: reduce) {\n .vs-root, .vs-opt, .vs-menu, .vs-btn { transition: none !important; animation: none !important; }\n}\n',
|
||||||
'view-select'
|
'view-select'
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,14 @@ useShadowStyles(`
|
||||||
.legend-dark .legend-item-selected{outline-color:rgba(255,255,255,0.5)!important;}
|
.legend-dark .legend-item-selected{outline-color:rgba(255,255,255,0.5)!important;}
|
||||||
.legend-dark .legend-tree-caret{color:rgba(243,243,241,0.7)!important;}
|
.legend-dark .legend-tree-caret{color:rgba(243,243,241,0.7)!important;}
|
||||||
.legend-dark .legend-content::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.18);}
|
.legend-dark .legend-content::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.18);}
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.legend-container, .legend-reopen, .legend-collapse-caret, .legend-collapse-caret svg { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'legend-component')
|
`, 'legend-component')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,14 @@ useShadowStyles(`
|
||||||
.sheet-dark .pull-tab-handle{background:rgba(243,243,241,0.3)!important;}
|
.sheet-dark .pull-tab-handle{background:rgba(243,243,241,0.3)!important;}
|
||||||
.sheet-dark .legend-content{background:#3b463d!important;}
|
.sheet-dark .legend-content{background:#3b463d!important;}
|
||||||
.sheet-dark .legend-content::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.2);}
|
.sheet-dark .legend-content::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.2);}
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.legend-sheet, .legend-content, .legend-handle, .legend-toggle { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'legend-mobile-sheet')
|
`, 'legend-mobile-sheet')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,14 @@ useShadowStyles(`
|
||||||
bar must not take a second header line there. Same-shadow-root rule, owned here
|
bar must not take a second header line there. Same-shadow-root rule, owned here
|
||||||
so the sheet needs no knowledge of the bar. */
|
so the sheet needs no knowledge of the bar. */
|
||||||
.legend-mobile-sheet.collapsed .lpb{display:none!important;}
|
.legend-mobile-sheet.collapsed .lpb{display:none!important;}
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.lpb-seg { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'legend-progress-bar')
|
`, 'legend-progress-bar')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,14 @@ useShadowStyles(`
|
||||||
.lrg-dark .lrg-header-col{color:rgba(243,243,241,0.65);}
|
.lrg-dark .lrg-header-col{color:rgba(243,243,241,0.65);}
|
||||||
.lrg-dark .lrg-footer{background:#2a332a;border-top-color:rgba(255,255,255,0.1);color:rgba(243,243,241,0.75);}
|
.lrg-dark .lrg-footer{background:#2a332a;border-top-color:rgba(255,255,255,0.1);color:rgba(243,243,241,0.75);}
|
||||||
.lrg-dark .lrg-footer-label{color:rgba(243,243,241,0.65);}
|
.lrg-dark .lrg-footer-label{color:rgba(243,243,241,0.65);}
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.lr-row, .lr-swatch { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'legend-row-group')
|
`, 'legend-row-group')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,14 @@ useShadowStyles(`
|
||||||
.lt-dark .lt-popup-row{color:rgba(243,243,241,0.85);}
|
.lt-dark .lt-popup-row{color:rgba(243,243,241,0.85);}
|
||||||
.lt-dark .lt-popup-row:hover{background:rgba(255,255,255,0.06);}
|
.lt-dark .lt-popup-row:hover{background:rgba(255,255,255,0.06);}
|
||||||
.lt-dark .lt-popup-row.active{background:rgba(124,140,248,0.18);color:#7c8cf8;}
|
.lt-dark .lt-popup-row.active{background:rgba(124,140,248,0.18);color:#7c8cf8;}
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.legend-tools, .lt-btn, .lt-panel { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'legend-tools')
|
`, 'legend-tools')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
|
||||||
|
|
@ -1180,6 +1180,17 @@ useShadowStyles(`
|
||||||
.stl-panel:not(.legend-mobile-sheet *) .stl-info-pop { font-size: 13px; }
|
.stl-panel:not(.legend-mobile-sheet *) .stl-info-pop { font-size: 13px; }
|
||||||
.stl-panel:not(.legend-mobile-sheet *) .stl-empty,
|
.stl-panel:not(.legend-mobile-sheet *) .stl-empty,
|
||||||
.stl-panel:not(.legend-mobile-sheet *) .stl-overflow { font-size: 12.5px; }
|
.stl-panel:not(.legend-mobile-sheet *) .stl-overflow { font-size: 12.5px; }
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.stl-panel, .stl-collapse-btn svg, .stl-row, .stl-reopen, .stl-chip { transition: none !important; animation: none !important; }
|
||||||
|
/* The spinner MEANS "loading". Stopped dead it is a static ring that says nothing, so it is
|
||||||
|
slowed to a step crawl: still visibly working, no smooth spin. */
|
||||||
|
.stl-spinner { animation: stl-spin 2.4s steps(8, end) infinite !important; }
|
||||||
|
}
|
||||||
`, 'semantic-tree-legend')
|
`, 'semantic-tree-legend')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,8 @@ useShadowStyles(dualEngine(
|
||||||
+ 'color:rgba(243,243,241,0.65)!important;}'
|
+ 'color:rgba(243,243,241,0.65)!important;}'
|
||||||
+ '.geocoder-dark .mapboxgl-ctrl-geocoder--icon,.dsm-dark .mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--icon{'
|
+ '.geocoder-dark .mapboxgl-ctrl-geocoder--icon,.dsm-dark .mapboxgl-ctrl-geocoder .mapboxgl-ctrl-geocoder--icon{'
|
||||||
+ 'fill:rgba(243,243,241,0.75)!important;}'
|
+ 'fill:rgba(243,243,241,0.75)!important;}'
|
||||||
+ '.geocoder-dark .mapboxgl-ctrl-geocoder--button,.dsm-dark .mapboxgl-ctrl-geocoder--button{background:transparent!important;}'),
|
+ '.geocoder-dark .mapboxgl-ctrl-geocoder--button,.dsm-dark .mapboxgl-ctrl-geocoder--button{background:transparent!important;}')
|
||||||
|
+ '/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,\n and no generated leaf ships one anyway (measured: 0 across the whole tree). Additive only. */\n@media (prefers-reduced-motion: reduce) {\n .geo-root, .mapboxgl-ctrl-geocoder, .maplibregl-ctrl-geocoder { transition: none !important; animation: none !important; }\n}\n',
|
||||||
'geocoder-skin'
|
'geocoder-skin'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,14 @@ useShadowStyles(`
|
||||||
.hbg-btn { width: 36px; height: 36px; gap: 4px; }
|
.hbg-btn { width: 36px; height: 36px; gap: 4px; }
|
||||||
.hbg-line { width: 14px; }
|
.hbg-line { width: 14px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.hb-btn, .hb-bar { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'hamburger-button')
|
`, 'hamburger-button')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,14 @@ useShadowStyles(`
|
||||||
/* shape="square" on a STANDALONE button: their 4px corners, our fill and shadow kept.
|
/* shape="square" on a STANDALONE button: their 4px corners, our fill and shadow kept.
|
||||||
A button inside .map-toolbar--square is overridden back to 0 by the toolbar's rules. */
|
A button inside .map-toolbar--square is overridden back to 0 by the toolbar's rules. */
|
||||||
.mcb--square{ border-radius:4px; }
|
.mcb--square{ border-radius:4px; }
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.mcb-btn { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'map-control-button')
|
`, 'map-control-button')
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,14 @@ useShadowStyles(`
|
||||||
.mcm.light .mcm-close{background:rgba(208,215,222,0.4);border-color:#d0d7de;color:#57606a;}
|
.mcm.light .mcm-close{background:rgba(208,215,222,0.4);border-color:#d0d7de;color:#57606a;}
|
||||||
.mcm.light .mcm-close:hover{color:#3b463d;background:rgba(59,70,61,0.12);border-color:#3b463d;}
|
.mcm.light .mcm-close:hover{color:#3b463d;background:rgba(59,70,61,0.12);border-color:#3b463d;}
|
||||||
.mcm-body{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;}
|
.mcm-body{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch;}
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.mcm-menu, .mcm-item { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'map-control-menu')
|
`, 'map-control-menu')
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
|
||||||
|
|
@ -1057,6 +1057,8 @@ useShadowStyles(
|
||||||
'font:inherit;font-size:12.5px;padding:6px 18px;border-radius:8px;cursor:pointer}' +
|
'font:inherit;font-size:12.5px;padding:6px 18px;border-radius:8px;cursor:pointer}' +
|
||||||
'.modal-btn:hover{background:rgba(0,0,0,.06)}',
|
'.modal-btn:hover{background:rgba(0,0,0,.06)}',
|
||||||
'doxa-poster-wall',
|
'doxa-poster-wall',
|
||||||
|
|
||||||
|
+ '/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,\n and no generated leaf ships one anyway (measured: 0 across the whole tree). Additive only. */\n@media (prefers-reduced-motion: reduce) {\n .pw-card, .pw-grid, .pw-overlay, .pw-btn { transition: none !important; animation: none !important; }\n}\n'
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,14 @@ useShadowStyles(`
|
||||||
.qa-note { margin: 10px 0 0; font-size: 11px; color: #8a94a3; }
|
.qa-note { margin: 10px 0 0; font-size: 11px; color: #8a94a3; }
|
||||||
.qa-corner.dark .qa-panel { background: #1b212b; color: #e7ebf0; border-color: #2c3542; }
|
.qa-corner.dark .qa-panel { background: #1b212b; color: #e7ebf0; border-color: #2c3542; }
|
||||||
.qa-corner.dark .qa-sub, .qa-corner.dark .qa-x { color: #aeb7c2; }
|
.qa-corner.dark .qa-sub, .qa-corner.dark .qa-x { color: #aeb7c2; }
|
||||||
|
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree), so a component
|
||||||
|
that animates must carry its own. Additive only: it removes motion when the OS asks and
|
||||||
|
changes nothing otherwise. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.qa-corner, .qa-panel { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`, 'qa-corner')
|
`, 'qa-corner')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -558,6 +558,11 @@ const VTL_SHADOW_CSS = `
|
||||||
.vtl.rail-collapsed:dir(rtl) .vtl-tab { padding-left:12px; padding-right:10px; }
|
.vtl.rail-collapsed:dir(rtl) .vtl-tab { padding-left:12px; padding-right:10px; }
|
||||||
.vtl.rail-collapsed:dir(rtl) .vtl-tier { padding-left:8px; padding-right:8px; }
|
.vtl.rail-collapsed:dir(rtl) .vtl-tier { padding-left:8px; padding-right:8px; }
|
||||||
}
|
}
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree). Additive only. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.vtl-nav, .vtl-collapse-ic, .vtl-tab, .vtl-tier, .vtl-fab, .vtl-scrim { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -1119,4 +1124,9 @@ export default defineComponent({
|
||||||
.vtl.rail-collapsed:dir(rtl) .vtl-tab { padding-left:12px; padding-right:10px; }
|
.vtl.rail-collapsed:dir(rtl) .vtl-tab { padding-left:12px; padding-right:10px; }
|
||||||
.vtl.rail-collapsed:dir(rtl) .vtl-tier { padding-left:8px; padding-right:8px; }
|
.vtl.rail-collapsed:dir(rtl) .vtl-tier { padding-left:8px; padding-right:8px; }
|
||||||
}
|
}
|
||||||
|
/* ★ REDUCED MOTION, OWNED BY THIS COMPONENT. A page-level rule cannot reach into a shadow root,
|
||||||
|
and no generated leaf ships one anyway (measured: 0 across the whole tree). Additive only. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.vtl-nav, .vtl-collapse-ic, .vtl-tab, .vtl-tier, .vtl-fab, .vtl-scrim { transition: none !important; animation: none !important; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,7 @@
|
||||||
"check:public-hygiene": "node internal/public-hygiene-check.mjs",
|
"check:public-hygiene": "node internal/public-hygiene-check.mjs",
|
||||||
"check:published": "node internal/check-published-matches-build.mjs",
|
"check:published": "node internal/check-published-matches-build.mjs",
|
||||||
"check:quoted-scripts": "node internal/check-quoted-scripts.mjs",
|
"check:quoted-scripts": "node internal/check-quoted-scripts.mjs",
|
||||||
|
"check:reduced-motion": "node internal/check-reduced-motion.mjs",
|
||||||
"check:region-fill-repaint": "node internal/check-region-fill-repaint.mjs",
|
"check:region-fill-repaint": "node internal/check-region-fill-repaint.mjs",
|
||||||
"check:rtl-shaping": "node internal/check-gospel-rtl-shaping.mjs",
|
"check:rtl-shaping": "node internal/check-gospel-rtl-shaping.mjs",
|
||||||
"check:rtl-shaping:poison": "node internal/check-gospel-rtl-shaping.mjs --poison",
|
"check:rtl-shaping:poison": "node internal/check-gospel-rtl-shaping.mjs --poison",
|
||||||
|
|
@ -302,7 +303,7 @@
|
||||||
"dev": "bun x vite",
|
"dev": "bun x vite",
|
||||||
"fix:sri": "node internal/fix-sri.mjs",
|
"fix:sri": "node internal/fix-sri.mjs",
|
||||||
"gallery": "node internal/generate-gallery.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:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:poster-print-fallback check:country-names",
|
"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:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:poster-print-fallback check:country-names check:reduced-motion",
|
||||||
"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 check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin",
|
"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 check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin",
|
||||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||||
|
|
@ -494,6 +495,7 @@
|
||||||
"check:provider-switch": "the provider switch must actually SWITCH: counts requests BY HOST (not window globals, which stay defined after a swap) and asserts the maplibre run stays within the leaf HTML's own declared api.mapbox.com preloads. Defaults to the upg-100 leaf; pass a URL or set BASE_URL for another. CONDITIONAL: SKIPs when nothing is served.",
|
"check:provider-switch": "the provider switch must actually SWITCH: counts requests BY HOST (not window globals, which stay defined after a swap) and asserts the maplibre run stays within the leaf HTML's own declared api.mapbox.com preloads. Defaults to the upg-100 leaf; pass a URL or set BASE_URL for another. CONDITIONAL: SKIPs when nothing is served.",
|
||||||
"check:published": "what readers are SERVED must be what we BUILT. Every other gate measures the local tree, so all of them stay green while a stale publish serves something else — and manufactures bug reports that cannot be reproduced. Compares the sha384 per bundle. SKIPs when the published host is unreachable (a network gate that goes red on flaky wifi gets ignored).",
|
"check:published": "what readers are SERVED must be what we BUILT. Every other gate measures the local tree, so all of them stay green while a stale publish serves something else — and manufactures bug reports that cannot be reproduced. Compares the sha384 per bundle. SKIPs when the published host is unreachable (a network gate that goes red on flaky wifi gets ignored).",
|
||||||
"check:quoted-scripts": "documentation/README-SECTION-3-cli-commands.md QUOTES the scripts block verbatim; it had silently drifted TWICE (a stale bunx cap:sync, and build:unlocked missing check:public-hygiene + check:label-joins). package.json is what RUNS — update the DOC.",
|
"check:quoted-scripts": "documentation/README-SECTION-3-cli-commands.md QUOTES the scripts block verbatim; it had silently drifted TWICE (a stale bunx cap:sync, and build:unlocked missing check:public-hygiene + check:label-joins). package.json is what RUNS — update the DOC.",
|
||||||
|
"check:reduced-motion": "a library component that animates must carry its OWN @media (prefers-reduced-motion) rule. A page-level rule does not cross a shadow boundary, and no generated leaf ships one anyway (measured: 0 across the built tree), so there is nothing to inherit. Also asserts the rule lives where the styles do: a component injecting through useShadowStyles whose rule sits only in a <style> block has written a rule that never reaches the shadow root.",
|
||||||
"check:region-fill-repaint": "the region fill must be repainted after its data arrives, in EVERY arrival order. ★ WHAT THIS NOW GUARDS, narrowed by measurement after it landed: the ordering is not merely uncaused-by-the-grey-tab, it is UNCONSTRUCTIBLE today — region-dashboards.vue:1091 gates the whole map subtree behind v-if=\"loading\", and loadRegions assigns points inside the try while loading=false is in the finally, so the map can never mount with empty points (rega: 0 grey in 36 mounts, including the API held back 20s). So this is a guard against a FUTURE refactor that drops that v-if data gate, not a live path. The grey Regions tab stays OPEN and is not this. fill-color is SNAPSHOTTED into the layer when addLayers creates it, from a match-expression built out of props.points — with no points that expression is a flat no-data grey, and applySubject guards every write on getLayer() so a change arriving before the layer exists is dropped and never retried. Models the arrival orders (asserting at least one DOES grey without the repaint, else the model proves nothing) and asserts the source carries the repaint INSIDE the .then() block that received the boundaries, brace-matched rather than within a character distance — a 1600-char window passed on an unrelated applySubject 1200 chars downstream.",
|
"check:region-fill-repaint": "the region fill must be repainted after its data arrives, in EVERY arrival order. ★ WHAT THIS NOW GUARDS, narrowed by measurement after it landed: the ordering is not merely uncaused-by-the-grey-tab, it is UNCONSTRUCTIBLE today — region-dashboards.vue:1091 gates the whole map subtree behind v-if=\"loading\", and loadRegions assigns points inside the try while loading=false is in the finally, so the map can never mount with empty points (rega: 0 grey in 36 mounts, including the API held back 20s). So this is a guard against a FUTURE refactor that drops that v-if data gate, not a live path. The grey Regions tab stays OPEN and is not this. fill-color is SNAPSHOTTED into the layer when addLayers creates it, from a match-expression built out of props.points — with no points that expression is a flat no-data grey, and applySubject guards every write on getLayer() so a change arriving before the layer exists is dropped and never retried. Models the arrival orders (asserting at least one DOES grey without the repaint, else the model proves nothing) and asserts the source carries the repaint INSIDE the .then() block that received the boundaries, brace-matched rather than within a character distance — a 1600-char window passed on an unrelated applySubject 1200 chars downstream.",
|
||||||
"check:religion-label-not-value": "the library's two RELIGION_FAMILIES sites (useLegendData, useSearch) show religionFamilyLabel() to the reader, while the value a filter or search matches on stays the canonical English family name: a translated label must never become a wire value.",
|
"check:religion-label-not-value": "the library's two RELIGION_FAMILIES sites (useLegendData, useSearch) show religionFamilyLabel() to the reader, while the value a filter or search matches on stays the canonical English family name: a translated label must never become a wire value.",
|
||||||
"check:ring-vocabulary": "the ring names the printed packet uses (packet-strings.json) agree with the ones the screen uses (gospelInternet.json) in every locale, so a packet and the map cannot name the same ring two ways. By the UPG-100 owner. Headless; in gates:pre.",
|
"check:ring-vocabulary": "the ring names the printed packet uses (packet-strings.json) agree with the ones the screen uses (gospelInternet.json) in every locale, so a packet and the map cannot name the same ring two ways. By the UPG-100 owner. Headless; in gates:pre.",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue