feat(ui): unified tab-switch animation - enter-only fade in TabPanel

One 0.2s enter-only fade (opacity + 6px rise) in the kit TabPanel covers
every tab switch (M3 clean-fade pattern; CSS animation so the global
reduced-motion kill switch applies). Orders and Invoices view switchers
now render through TabPanel (also fixes their dangling aria-controls).
Removed the inconsistent bespoke framer entrances inside tab content
(CompanySettings cascade, ReceivedInvoices card). Filter-style status
tabs (Projects/Offers/Invoices) deliberately keep the refetch dim only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-12 18:04:32 +02:00
parent bf33d0a543
commit b9103c59f0
5 changed files with 878 additions and 919 deletions

View File

@@ -2,6 +2,8 @@ import type { ReactNode } from "react";
import MuiTabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Box from "@mui/material/Box";
import { keyframes } from "@mui/system";
import type { SxProps, Theme } from "@mui/material/styles";
export interface TabDef {
value: string;
@@ -47,15 +49,26 @@ export function Tabs({
);
}
// The ONE tab-switch animation, app-wide: enter-only fade of the incoming
// panel (M3 "clean fade" — old content drops instantly, no exit/slide; tabs
// are too high-frequency for anything longer). Kept as a CSS animation so the
// global prefers-reduced-motion kill switch in GlobalStyles neutralizes it.
const panelEnter = keyframes({
from: { opacity: 0, transform: "translateY(6px)" },
to: { opacity: 1, transform: "none" },
});
/** Renders children only when its value matches the current tab. */
export function TabPanel({
value,
current,
children,
sx,
}: {
value: string;
current: string;
children: ReactNode;
sx?: SxProps<Theme>;
}) {
if (value !== current) return null;
return (
@@ -63,7 +76,13 @@ export function TabPanel({
role="tabpanel"
id={panelId(value)}
aria-labelledby={tabId(value)}
sx={{ pt: 2.5 }}
sx={[
{
pt: 2.5,
animation: `${panelEnter} 0.2s cubic-bezier(0.16, 1, 0.3, 1)`,
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
{children}
</Box>