feat(ui): motion & micro-interactions, prefers-reduced-motion aware (LC-196) #247

Merged
nrupard merged 2 commits from feat/lc-196-motion into main 2026-05-28 15:43:18 +02:00
Owner

LC-196: tasteful motion + a prefers-reduced-motion: reduce kill switch.

CSS (server/assets/main.css)

Keyframes: lc-fade-in, lc-scale-in, lc-slide-up, lc-shimmer, lc-spin.

Helpers:

  • .lc-anim-swap - opt-in fade-in via htmx's .htmx-added settle class. Adding the class to a server-rendered element does NOT animate on initial page render (no htmx-added present); it animates only on OOB-swapped re-arrival, so backfill stays calm and live updates are gently announced.
  • .lc-skeleton - shimmering placeholder block, sized by parent.
  • .lc-spinner - small rotating ring; pairs with htmx's .htmx-indicator (parent gains .htmx-request while in-flight, indicator becomes visible).

Component motion baked into LC-195 vocabulary: .toast slides up on insert, .modal-panel scales in, .modal-backdrop fades in. Existing places that use these classes (poll_modal.html, scheduled_modal.html, reminders/picker.html) animate for free, no template change.

Reduced-motion

A single universal @media (prefers-reduced-motion: reduce) rule sets all transition-duration and animation-duration to 0.001ms !important and pins scroll-behavior: auto. Kept non-zero (per the a11y-project convention) so JS listeners on transitionend / animationend still fire. Tailwind's motion-safe: / motion-reduce: variants still compose on top.

Adopted

  • server/templates/room/message.html: both message rows (system + regular) get lc-anim-swap, so new messages arriving over the WS fade in.
  • Auth submit buttons (login / register / forgot / reset): added <span class="htmx-indicator lc-spinner" aria-hidden="true"> inside the button so the spinner appears beside the label while the POST is in flight. The .btn gap-1.5 from LC-195 spaces it correctly.

Tests

  • LC-77 webhook + email-inbox render fixtures regenerated to pick up the new lc-anim-swap class on the message div (only delta: one extra class token in the row's class=).
  • just test and just test-saas both green.

Pre-existing CI noise on main (not introduced here): clippy clippy::too_many_arguments on resolve_msg_author in server/src/routes/mod.rs:154, and cargo fmt drift in server/tests/spike_image_decoder_hostile_corpus.rs:138. Goes to LC-187 (the standing Check-workflow issue).

Still to come under LC-189

  • LC-197 a11y polish (focus-visible audit, keyboard nav, skip-link).
  • LC-198 iconography / avatars / presence.
  • LC-199 responsive + mobile (drawer slide will use these motion primitives).

🤖 Generated with Claude Code

LC-196: tasteful motion + a `prefers-reduced-motion: reduce` kill switch. ### CSS (server/assets/main.css) Keyframes: `lc-fade-in`, `lc-scale-in`, `lc-slide-up`, `lc-shimmer`, `lc-spin`. Helpers: - `.lc-anim-swap` - opt-in fade-in via htmx's `.htmx-added` settle class. Adding the class to a server-rendered element does NOT animate on initial page render (no htmx-added present); it animates only on OOB-swapped re-arrival, so backfill stays calm and live updates are gently announced. - `.lc-skeleton` - shimmering placeholder block, sized by parent. - `.lc-spinner` - small rotating ring; pairs with htmx's `.htmx-indicator` (parent gains `.htmx-request` while in-flight, indicator becomes visible). Component motion baked into LC-195 vocabulary: `.toast` slides up on insert, `.modal-panel` scales in, `.modal-backdrop` fades in. Existing places that use these classes (`poll_modal.html`, `scheduled_modal.html`, `reminders/picker.html`) animate for free, no template change. ### Reduced-motion A single universal `@media (prefers-reduced-motion: reduce)` rule sets all `transition-duration` and `animation-duration` to `0.001ms !important` and pins `scroll-behavior: auto`. Kept non-zero (per the a11y-project convention) so JS listeners on `transitionend` / `animationend` still fire. Tailwind's `motion-safe:` / `motion-reduce:` variants still compose on top. ### Adopted - `server/templates/room/message.html`: both message rows (system + regular) get `lc-anim-swap`, so new messages arriving over the WS fade in. - Auth submit buttons (login / register / forgot / reset): added `<span class="htmx-indicator lc-spinner" aria-hidden="true">` inside the button so the spinner appears beside the label while the POST is in flight. The `.btn` `gap-1.5` from LC-195 spaces it correctly. ### Tests - LC-77 webhook + email-inbox render fixtures regenerated to pick up the new `lc-anim-swap` class on the message div (only delta: one extra class token in the row's `class=`). - `just test` and `just test-saas` both green. Pre-existing CI noise on `main` (not introduced here): clippy `clippy::too_many_arguments` on `resolve_msg_author` in `server/src/routes/mod.rs:154`, and `cargo fmt` drift in `server/tests/spike_image_decoder_hostile_corpus.rs:138`. Goes to LC-187 (the standing Check-workflow issue). ### Still to come under LC-189 - LC-197 a11y polish (focus-visible audit, keyboard nav, skip-link). - LC-198 iconography / avatars / presence. - LC-199 responsive + mobile (drawer slide will use these motion primitives). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(ui): motion & micro-interactions, prefers-reduced-motion aware (LC-196)
Some checks failed
check-secrets / TruffleHog (push) Successful in 3s
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 6s
Check / clippy + fmt + tests (pull_request) Failing after 15s
7d4aeb398e
assets/main.css: keyframes (lc-fade-in, lc-scale-in, lc-slide-up, lc-shimmer, lc-spin) + helper classes (lc-anim-swap, lc-skeleton, lc-spinner). Component motion baked into .toast / .modal-panel / .modal-backdrop (LC-195 vocabulary auto-picks this up). All wrapped by a universal `@media (prefers-reduced-motion: reduce)` reset that pins transition + animation duration to ~0 (kept non-zero so JS transitionend / animationend listeners still fire).

Adopted: room/message.html gets `lc-anim-swap` so OOB-arriving messages fade in via the htmx-added settle class (initial page render has no htmx-added, so the backfill does not animate). Auth submit buttons (login/register/forgot/reset) gain a `<span class="htmx-indicator lc-spinner">` so the button shows a spinner while POST is in flight.

Fixtures: LC-77 webhook/email-inbox render fixtures regenerated to pick up the `lc-anim-swap` class on the message div.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(ui): .lc-anim-swap fade uses transition, not animation (LC-196)
Some checks failed
check-secrets / TruffleHog (pull_request) Successful in 3s
check-secrets / Nosey parker (pull_request) Successful in 4s
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / Kingfisher (pull_request) Successful in 6s
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / TruffleHog (push) Successful in 5s
Check / clippy + fmt + tests (pull_request) Failing after 13s
Create release / Create release from merged PR (pull_request) Has been skipped
f04e662a87
Self-review on PR #247 caught it: tying a keyframe animation to `.htmx-added` doesn't work, because htmx removes that class at `config.defaultSettleDelay` (20ms by default), which cancels any in-flight CSS animation matched only by that selector. The fade ran for ~20ms instead of 180ms, barely visible.

Switched to the canonical htmx fade pattern (per the htmx Animations docs): pin `opacity: 0` while `.htmx-added` is present, and let a CSS transition on the bare `.lc-anim-swap` carry opacity back up to 1 once the class is removed. Transitions don't depend on the matching selector to keep going.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-196-motion 2026-05-28 15:43:18 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!247
No description provided.