diff --git a/app/(dashboard)/bookkeeping/periodiseringar/page.tsx b/app/(dashboard)/bookkeeping/periodiseringar/page.tsx
index 4ca38264..3b09436f 100644
--- a/app/(dashboard)/bookkeeping/periodiseringar/page.tsx
+++ b/app/(dashboard)/bookkeeping/periodiseringar/page.tsx
@@ -343,7 +343,7 @@ export default function AccrualSchedulesPage() {
{isOpen && (
-
diff --git a/app/globals.css b/app/globals.css
index 433eabfc..748234ba 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -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 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 {
diff --git a/components/agent/AgentChat.tsx b/components/agent/AgentChat.tsx
index 6a1742cd..4bd95a4c 100644
--- a/components/agent/AgentChat.tsx
+++ b/components/agent/AgentChat.tsx
@@ -28,6 +28,13 @@ import type { AgentStatusEvent } from './agent-status'
import { sendFeedback, type FeedbackSentiment } from './feedback-client'
import { Skeleton } from '@/components/ui/skeleton'
+// New messages arrive one at a time, so they enter on the short bubble curve.
+// The whole loaded history must NOT: `.animate-slide-up` is the 500ms
+// once-per-navigation page-entry animation, so resuming a 20-message thread
+// used to fire 20 simultaneous 500ms slides.
+const MESSAGE_ENTER_CLASS =
+ 'animate-in fade-in-0 slide-in-from-bottom-2 duration-200 ease-[cubic-bezier(0.23,1,0.32,1)]'
+
// Markdown parser loads separately from the chat surface: react-markdown +
// remark-gfm pull in the whole unified/remark tree.
//
@@ -198,6 +205,9 @@ export default function AgentChat({
const firstTurnFiredRef = useRef(false)
const conversationIdRef = useRef(initialConversationId ?? null)
const [messages, setMessages] = useState(initialMessages ?? [])
+ // How many messages were already on screen when this thread mounted. Anything
+ // at or past this index is new and animates in; the resumed history does not.
+ const historyBaselineRef = useRef((initialMessages ?? []).length)
// Read by the announcement effect, which must not re-run on every token: a
// `messages` dependency would fire it hundreds of times per turn. Written in
// an effect rather than during render: React may replay a render, and a
@@ -775,7 +785,7 @@ export default function AgentChat({
{messages.length === 0 && streaming && }
{messages.map((m, i) => (
-
+ = historyBaselineRef.current ? MESSAGE_ENTER_CLASS : undefined}>
{
+ const t = setTimeout(() => setEntering(false), 320)
+ return () => clearTimeout(t)
+ }, [])
const [conversationId, setConversationId] = useState(null)
// 'chat' shows the conversation; 'list' shows the session picker.
const [view, setView] = useState<'chat' | 'list'>('chat')
@@ -227,7 +234,13 @@ export default function AgentSheet({
// conversation state in AgentChat survives) while removing it from view
// and layout entirely (no stray horizontal scroll from an off-screen box).
className={cn(
- 'fixed inset-y-0 right-0 z-[60] flex w-full flex-col border-l border-border bg-background shadow-lg transition-[max-width] duration-200 ease-out',
+ 'fixed inset-y-0 right-0 z-[60] flex w-full flex-col border-l border-border bg-background shadow-lg transition-[max-width] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]',
+ // Arrive along the same edge, on the same curve and duration, as the
+ // page panel that animates its margin to make room (layout.tsx). Gated
+ // on first mount only: the panel stays mounted while collapsed, and
+ // display:none -> visible would otherwise replay the slide every time
+ // the user re-expands the same session.
+ entering && 'animate-in slide-in-from-right-full fade-in-0',
collapsed && 'hidden',
// Expanded grows the panel leftward over the page (still non-modal: the
// page stays interactive); normal is the compact side sheet.
diff --git a/components/bookkeeping/JournalEntryList.tsx b/components/bookkeeping/JournalEntryList.tsx
index 18a7138b..bbfd192c 100644
--- a/components/bookkeeping/JournalEntryList.tsx
+++ b/components/bookkeeping/JournalEntryList.tsx
@@ -1231,7 +1231,7 @@ export default function JournalEntryList() {
{isExpanded && (
-
+
diff --git a/components/dashboard/AttGoraSection.tsx b/components/dashboard/AttGoraSection.tsx
index 574c7b07..6cb328fa 100644
--- a/components/dashboard/AttGoraSection.tsx
+++ b/components/dashboard/AttGoraSection.tsx
@@ -168,7 +168,7 @@ export default function AttGoraSection({
next.delete(match.transaction_id)
return next
})
- }, 300)
+ }, 200)
void refetchCounts()
} catch {
toast({ title: t('suggested_failed_toast'), variant: 'destructive' })
@@ -237,7 +237,7 @@ export default function AttGoraSection({
{t('suggested_title')}
-
+
{matches.map((match) => {
const isLeaving = leavingIds.has(match.transaction_id)
const isConfirming = confirmingId === match.transaction_id
@@ -245,10 +245,12 @@ export default function AttGoraSection({
+
+
{match.transaction_description}
@@ -294,6 +296,8 @@ export default function AttGoraSection({
t('suggested_confirm')
)}
+
+
)
})}
diff --git a/components/salary/run/RunProgressBar.tsx b/components/salary/run/RunProgressBar.tsx
index c5e5183b..04bda922 100644
--- a/components/salary/run/RunProgressBar.tsx
+++ b/components/salary/run/RunProgressBar.tsx
@@ -244,7 +244,7 @@ export function RunProgressBar(props: RunProgressBarProps) {
{steps.map(step => (
-
+
))}
@@ -275,7 +275,7 @@ export function RunProgressBar(props: RunProgressBarProps) {
{steps.map(step => (
-
-
+
|
{expanded && (
-
+
|
| |