Build / Build (push) Successful in 33s
decePub no longer looks at the system's "reduce motion" setting, so everything moves the same way whatever it says: - a GIF video in a post plays by itself, muted and looping, instead of waiting for a tap; it now also starts on a page just opened, before the first tap, where the browser used to hold it still; - animated custom emoji move in the emoji picker instead of showing a still picture; - the globe turns by itself while no located post is in view, flies to the posts, and its pulses, arcs and Sun move; - going back to the top of a feed scrolls smoothly; - tooltips, dropdowns, menus, folding sections, toasts, loading spinners, placeholders, progress bars, radio buttons and ratings keep their animations: the build now takes daisyUI's reduce-motion rules out of the stylesheet (tools/css/motion.py, run after every Tailwind build, in watch mode too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4YtEKqxKjKyHkFzuRVMNA
72 lines
2.3 KiB
Python
72 lines
2.3 KiB
Python
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
MEDIA = re.compile(r'@media([^{;]*prefers-reduced-motion[^{;]*)\{')
|
|
MOTION = re.compile(r'\(\s*prefers-reduced-motion\s*:\s*(reduce|no-preference)\s*\)')
|
|
|
|
|
|
def block_end(css, start):
|
|
depth, i, quote = 1, start, None
|
|
while depth:
|
|
ch = css[i]
|
|
if quote:
|
|
if ch == '\\':
|
|
i += 1
|
|
elif ch == quote:
|
|
quote = None
|
|
elif ch in '"\'':
|
|
quote = ch
|
|
elif ch == '{':
|
|
depth += 1
|
|
elif ch == '}':
|
|
depth -= 1
|
|
i += 1
|
|
return i
|
|
|
|
|
|
def without_motion(prelude):
|
|
if ',' in prelude or re.search(r'\b(or|not|only)\b', prelude):
|
|
raise ValueError(f'@media{prelude}')
|
|
parts = [part.strip() for part in re.split(r'\band\b', prelude)]
|
|
wanted = [MOTION.fullmatch(part).group(1) for part in parts if MOTION.fullmatch(part)]
|
|
if len(wanted) != 1 or any('prefers-reduced-motion' in part and not MOTION.fullmatch(part) for part in parts):
|
|
raise ValueError(f'@media{prelude}')
|
|
return wanted[0], ' and '.join(part for part in parts if not MOTION.fullmatch(part))
|
|
|
|
|
|
def everyone_moves(css):
|
|
out, i = [], 0
|
|
while match := MEDIA.search(css, i):
|
|
out.append(css[i:match.start()])
|
|
end = block_end(css, match.end())
|
|
wanted, rest = without_motion(match.group(1))
|
|
if wanted == 'no-preference':
|
|
inner = everyone_moves(css[match.end():end - 1])
|
|
if not rest and inner.strip() and not inner.rstrip().endswith((';', '}')):
|
|
inner += ';'
|
|
out.append(f'@media {rest}{{{inner}}}' if rest else inner)
|
|
i = end
|
|
out.append(css[i:])
|
|
return ''.join(out)
|
|
|
|
|
|
failed = False
|
|
for name in sys.argv[1:]:
|
|
path = Path(name)
|
|
before = path.read_text(encoding='utf-8')
|
|
try:
|
|
after = everyone_moves(before)
|
|
except ValueError as query:
|
|
print(f'{name}: cannot rewrite {query}', file=sys.stderr)
|
|
failed = True
|
|
continue
|
|
if 'prefers-reduced-motion' in after:
|
|
print(f'{name}: a prefers-reduced-motion query is still there', file=sys.stderr)
|
|
failed = True
|
|
continue
|
|
if after != before:
|
|
path.write_text(after, encoding='utf-8')
|
|
print(f'{name}: motion now plays for everyone')
|
|
sys.exit(1 if failed else 0)
|