diff --git a/web/app/loading.tsx b/web/app/loading.tsx new file mode 100644 index 0000000..86e2072 --- /dev/null +++ b/web/app/loading.tsx @@ -0,0 +1,5 @@ +import TableSkeleton from "@/components/table-skeleton"; + +export default function Loading() { + return ; +} diff --git a/web/app/users/loading.tsx b/web/app/users/loading.tsx new file mode 100644 index 0000000..db948f6 --- /dev/null +++ b/web/app/users/loading.tsx @@ -0,0 +1,5 @@ +import TableSkeleton from "@/components/table-skeleton"; + +export default function Loading() { + return ; +} diff --git a/web/components/table-skeleton.tsx b/web/components/table-skeleton.tsx new file mode 100644 index 0000000..810916f --- /dev/null +++ b/web/components/table-skeleton.tsx @@ -0,0 +1,50 @@ +type Props = { eyebrow: string; title: string; rows?: number }; + +/** + * Lightweight skeleton that mimics the AccountsTable / UsersTable shell. + * Rendered by app/loading.tsx and app/users/loading.tsx — Next.js shows + * this immediately on tab navigation, then streams in the real Server + * Component once its data fetch resolves. Without it, the previous + * route's UI freezes until the fetch finishes (the "tab switch is + * laggy" symptom). + * + * The pulse animation comes from Tailwind's animate-pulse on each + * placeholder bar; no JS, no layout shift when the real content swaps in. + */ +export default function TableSkeleton({ eyebrow, title, rows = 8 }: Props) { + return ( +
+
+
+

+ {eyebrow} +

+

+ {title} + +

+
+
+ + +
+
+
    + {Array.from({ length: rows }).map((_, i) => ( +
  • +
    +
    + + +
    + +
    +
  • + ))} +
+
+ ); +}