From 697b2c8897b70b5444456864be6c4b6a34e573f0 Mon Sep 17 00:00:00 2001 From: thepra Date: Mon, 9 Nov 2026 20:48:47 +0100 Subject: [PATCH] Animations, GIFs and the globe now move for every reader 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 Claude-Session: https://claude.ai/code/session_01W4YtEKqxKjKyHkFzuRVMNA --- CLAUDE.md | 25 +++++++++---- Components/Content.razor | 3 +- Components/EmojiPicker.razor | 5 +-- Services/AppStatusService.cs | 5 --- Styles/feed.css | 6 --- decePubClient.csproj | 3 +- tools/css/motion.py | 71 ++++++++++++++++++++++++++++++++++++ tools/tailwind.sh | 7 +++- wwwroot/js/feed.js | 2 +- wwwroot/js/globe.js | 27 ++++++-------- wwwroot/main.js | 14 ++----- 11 files changed, 113 insertions(+), 55 deletions(-) create mode 100644 tools/css/motion.py diff --git a/CLAUDE.md b/CLAUDE.md index 7590083..2437050 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,6 +80,10 @@ without network access. NuGet restore still uses nuget.org. - `IncludeTailwindOutput` adds the generated file to `Content` before static web assets are resolved. Without that, a clean-clone publish would ship `app.css` without its `.gz`/`.br` copies and outside `service-worker-assets.js`. The deploy workflow checks for `css/app.css.gz`. +- Right after Tailwind writes `app.css`, `tools/css/motion.py` (run by `TailwindCompile` and by `tailwind.sh build` and + `watch`) deletes the blocks daisyUI emits for `prefers-reduced-motion: reduce` and unwraps its `no-preference` ones, + so its transitions and animations play for everyone. It fails the build on a query it cannot rewrite. It needs + `python3`. - `bash tools/tailwind.sh watch` rebuilds the CSS live next to `dotnet watch`. - Tailwind scans every file `.gitignore` doesn't exclude (minus `tools/` and `wwwroot/vendor/`), and only generates the classes it finds written out **whole**. Never build a class name by concatenation or interpolation (`"btn-" + size`, `$"btn-{size}"`). Write @@ -355,7 +359,9 @@ Never redeclare the base's members in a component. paints any `[data-blurhash]`: a picture's until it loads, again when its source changes, and for good behind the sensitive-media button), a picture's focal point (`object-position`), and an event's online link, participants and status. A picture, video or sound that fails to load says "Media unavailable" with "Open original" - (`data-media-unavailable`), and a gifv plays only on a tap under `prefers-reduced-motion`. + (`data-media-unavailable`), and a gifv plays by itself, muted and looping, for every reader. Blazor writes `muted` + only as an attribute, which leaves the video unmuted and Chromium refuses to autoplay it before the reader's first + tap, so `main.js` sets the `muted` property of every video carrying the attribute on its `loadstart`. - **About** (`Pages/About.razor` at `/about`, no sign-in, `data-about`; linked from NavMenu as `data-nav-about` and from Login as `data-about-offer`): the server's name (`data-about-title`), short description and thumbnail, its long description (`data-about-description`, `/api/v1/instance/extended_description`), contact (`data-about-contact`), rules @@ -612,11 +618,11 @@ Never redeclare the base's members in a component. quick choices (`Quick`), the server's emoji by category (none first) and common unicode ones by group (`Helpers/UnicodeEmojis.cs`, single emoji only, searched by their Mastodon/GitHub shortcode and a few words). Each choice is `data-emoji-choice` (the shortcode, or the character) and goes to `OnPick` as `:shortcode:` or the - character; it closes the picker first. A GIF shows its still picture under `prefers-reduced-motion`. The card's React - button (`data-react`) and the announcements banner's (`data-announcement-react`) react with it, after their quick - reactions; the composer's emoji button writes it where the caret is, replacing a selection, else at the end, a - shortcode with a space on each side where a letter would touch it (`AppStatusService.CaretOf` / `PlaceCaret`, - `main.js`'s `caretOf` and `placeCaret`). + character; it closes the picker first. An animated emoji moves in the picker for every reader (its `url`, never its + `static_url`). The card's React button (`data-react`) and the announcements banner's (`data-announcement-react`) + react with it, after their quick reactions; the composer's emoji button writes it where the caret is, replacing a + selection, else at the end, a shortcode with a space on each side where a letter would touch it + (`AppStatusService.CaretOf` / `PlaceCaret`, `main.js`'s `caretOf` and `placeCaret`). - **Suggestions:** in the composer, `:` and two letters or more at the end of the words offer the matching emoji (shortcodes starting with it first, eight at most) among the mention and hashtag suggestions, as `data-emoji-suggestion="{shortcode}"`; one picked is written `:shortcode: `. @@ -765,8 +771,8 @@ Never redeclare the base's members in a component. - **The globe** (`wwwroot/js/globe.js`, an ES module imported by `GlobePanel`) runs on globe.gl, vendored in `wwwroot/vendor/globe/` with its textures and `countries.json` (`SOURCES.md` has versions, licences and hashes; the service worker never precaches it). It looks at the spherical mean of the located posts in view, pulses and draws an - arc to a hovered post, honours `prefers-reduced-motion`, and draws only while something moves (idle, it costs no GPU - time; `wake()` before anything that animates). + arc to a hovered post, moves the same way for every reader (camera flights, the Sun, pulses, arcs, auto-rotation), + and draws only while something moves (idle, it costs no GPU time; `wake()` before anything that animates). - **Map.** Each pixel computes its own map coordinates from its point on the sphere (`sharpen`), so the poles don't pinch; the maps wrap horizontally and get 8× anisotropic filtering (`keepSharp`). The day texture is NASA's, its seam repaired by `tools/globe/prepare-day-texture.py` (dev-only, needs Pillow). @@ -978,6 +984,9 @@ The nginx vhost and headers are in `deploy/nginx/` (it sends `X-Robots-Tag: noin ## Conventions +- **Motion plays for everyone** (owner, 9 November 2026): nothing reads `prefers-reduced-motion` or any equivalent + (`matchMedia`, `motion-safe:`/`motion-reduce:`, a flag fed from it), so animations, GIFs and gifvs, smooth scrolling + and the globe behave the same for every reader. - Indent with tabs. - `Nullable` is disabled and `ImplicitUsings` is enabled. - Global Razor usings are in `_Imports.razor`. diff --git a/Components/Content.razor b/Components/Content.razor index cee0dd0..9a387db 100644 --- a/Components/Content.razor +++ b/Components/Content.razor @@ -907,8 +907,7 @@ @media.Description , - // under reduced motion a gifv waits for a tap (its controls) - "gifv" => @