refactor(motion): retime content entry, stop the double and one-frame animations (#1282)

The stagger is the standard content entry on every migrated page
(convention 11), so its budget matters more than any single surface. It ran
500ms per item in 80ms steps, putting the 10th row at 1220ms against a
300ms UI budget, and it only defined delays for children 1-10: on any list
longer than ten rows, children 11+ inherited delay 0 and arrived BEFORE the
middle of the list. Now 300ms per item in 40ms steps with the delay capped
at 360ms, so the tail lands at ~660ms instead of ~1220ms.

The cap is on the delay, never on the animation: the `both` fill holds a
child at opacity 0 until its delay elapses, so excluding rows 11+ from the
animation would paint them while rows 1-10 were still invisible.

Adds --ease-emphasized (the strong ease-out) rather than retiming
--ease-out, which is unlayered in :root and therefore shadows Tailwind's
own token: changing it would retime every ease-out utility in the app.

Foldout rows in JournalEntryList, periodiseringar and TransactionInboxCard
sit directly inside a staggered tbody, so expanding a verifikat fired the
inherited slideUp (with an invisible pre-roll of up to 320ms) on top of the
foldout's own transition. They opt out via data-no-stagger.

Also: the confirmed match on Hem faded at full height and then vanished in
one frame, jumping everything below it 52px; it now grid-collapses over
200ms with the gap inside the collapsing area. The assistant sheet slid
nothing while the page panel animated 300ms to make room for it; it now
arrives along the same edge on the same curve, gated to first mount so
re-expanding a collapsed session stays instant. Chat history no longer
replays 20 simultaneous 500ms page-entry slides on resume: only genuinely
new messages animate. Payroll wizard segments transition their colour.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-29 12:35:32 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 3d02a74147
commit 108f348c84
8 changed files with 68 additions and 23 deletions
@@ -343,7 +343,7 @@ export default function AccrualSchedulesPage() {
</td>
</tr>
{isOpen && (
<tr className="hover:bg-transparent">
<tr data-no-stagger className="hover:bg-transparent">
<td colSpan={7} className="border-b border-border bg-muted/30 p-0">
<RowFoldout>
<div className="px-6 py-4">
+30 -12
View File
@@ -91,6 +91,12 @@
/* Animation */
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
/* Strong ease-out for content entry. The --ease-out above is easeOutQuad,
the weak built-in shape; it is deliberately left alone because this :root
block is unlayered and therefore shadows Tailwind's own `ease-out` token,
so retiming it would retime every ease-out utility in the app. New entry
animations should use this one. */
--ease-emphasized: cubic-bezier(0.23, 1, 0.32, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--duration-fast: 150ms;
--duration-base: 300ms;
@@ -374,20 +380,32 @@ body {
@utility slide-out-to-right { --tw-exit-translate-x: 100%; }
@utility slide-out-to-right-full { --tw-exit-translate-x: 100%; }
/* Staggered entrance animation */
.stagger-enter > * {
animation: slideUp var(--duration-slow) var(--ease-out) both;
/* Staggered entrance animation (convention 11). This is the standard content
entry on every migrated page, so its budget matters more than any single
surface: 300ms per item on the emphasized curve, in 40ms steps.
It used to be 500ms per item in 80ms steps, which put the 10th row at
1220ms, and it only defined delays for children 1-10, so on any list longer
than ten rows children 11+ inherited delay 0 and arrived BEFORE the middle
of the list.
The cap is on the delay, never on the animation: `both` holds a child at
opacity 0 until its delay elapses, so excluding rows 11+ from the animation
(nth-child(-n+10)) would paint them at full opacity while rows 1-10 were
still invisible. Rows carrying data-no-stagger opt out entirely, for the
case of a foldout <tr> that runs its own transition inside a staggered
tbody and would otherwise animate twice. */
.stagger-enter > *:not([data-no-stagger]) {
animation: slideUp var(--duration-base) var(--ease-emphasized) both;
}
.stagger-enter > *:nth-child(1) { animation-delay: 0ms; }
.stagger-enter > *:nth-child(2) { animation-delay: 80ms; }
.stagger-enter > *:nth-child(3) { animation-delay: 160ms; }
.stagger-enter > *:nth-child(4) { animation-delay: 240ms; }
.stagger-enter > *:nth-child(5) { animation-delay: 320ms; }
.stagger-enter > *:nth-child(6) { animation-delay: 400ms; }
.stagger-enter > *:nth-child(7) { animation-delay: 480ms; }
.stagger-enter > *:nth-child(8) { animation-delay: 560ms; }
.stagger-enter > *:nth-child(9) { animation-delay: 640ms; }
.stagger-enter > *:nth-child(10) { animation-delay: 720ms; }
.stagger-enter > *:nth-child(2) { animation-delay: 40ms; }
.stagger-enter > *:nth-child(3) { animation-delay: 80ms; }
.stagger-enter > *:nth-child(4) { animation-delay: 120ms; }
.stagger-enter > *:nth-child(5) { animation-delay: 160ms; }
.stagger-enter > *:nth-child(6) { animation-delay: 200ms; }
.stagger-enter > *:nth-child(7) { animation-delay: 240ms; }
.stagger-enter > *:nth-child(8) { animation-delay: 280ms; }
.stagger-enter > *:nth-child(9) { animation-delay: 320ms; }
.stagger-enter > *:nth-child(n+10) { animation-delay: 360ms; }
/* Utility animations */
.animate-fade-in {