diff --git a/docs/widgets.md b/docs/widgets.md index fd810e79..d7d11347 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -614,7 +614,8 @@ success was. `sinceLabel(ts)` gives that "3m ago" label on its own. **Visuals** (self-contained inline SVG/DOM, no extra CSS) - `sparkline(values, opts?)` returns an `` area+line chart element. -- `barFill(percent, opts?)` returns a track+fill bar element. +- `barFill(percent, opts?)` returns a track+fill bar element. It skips its width + transition when the browser asks for reduced motion. - `smoothPath(points)` returns a smoothed SVG path string through `[[x,y], ...]`. Check the toolbox before building a new visual by hand. diff --git a/ui/js/widget-toolbox.js b/ui/js/widget-toolbox.js index de878d25..ae62b56e 100644 --- a/ui/js/widget-toolbox.js +++ b/ui/js/widget-toolbox.js @@ -152,6 +152,16 @@ export function sparkline(values, opts = {}) { return svg; } +/* A widget page loads no shared stylesheet, so a reduced-motion rule cannot + reach a style set here. Ask for the preference instead. */ +function reducedMotion() { + try { + return !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; + } catch { + return false; + } +} + /* opts: { color='#0a84ff', track='rgba(255,255,255,0.10)', height=6, radius=3 } */ export function barFill(percent, opts = {}) { const pct = Math.max(0, Math.min(100, Number(percent) || 0)); @@ -164,7 +174,8 @@ export function barFill(percent, opts = {}) { const fill = document.createElement('div'); fill.style.cssText = `position:absolute;left:0;top:0;bottom:0;width:${pct}%;border-radius:${radius}px;` + - `background:${opts.color || '#0a84ff'};transition:width .4s ease`; + `background:${opts.color || '#0a84ff'}` + + (reducedMotion() ? '' : ';transition:width .4s ease'); track.appendChild(fill); return track; } diff --git a/ui/test/widget-toolbox-motion.test.mjs b/ui/test/widget-toolbox-motion.test.mjs new file mode 100644 index 00000000..a0ba9c99 --- /dev/null +++ b/ui/test/widget-toolbox-motion.test.mjs @@ -0,0 +1,75 @@ +/* A widget is an iframe that loads no shared stylesheet, so the reduced-motion + rule in tokens.css cannot reach it and a rule in the widget's own