refactor(ui): redesign profile and table loading skeletons to mirror real layouts

This commit is contained in:
alejandrobailo
2026-06-17 20:40:22 +02:00
parent 0ed43a7c3a
commit ed68dbbfa9
5 changed files with 530 additions and 198 deletions
@@ -1,37 +1,102 @@
import React from "react";
import { Card } from "@/components/shadcn/card/card";
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
export const SkeletonTableInvitation = () => {
const SkeletonTableRow = () => {
return (
<Card variant="base" padding="md" className="flex flex-col gap-4">
{/* Table headers */}
<div className="hidden gap-4 md:flex">
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-1/12" />
</div>
{/* Table body */}
<div className="flex flex-col gap-3">
{[...Array(10)].map((_, index) => (
<div
key={index}
className="flex flex-col gap-4 md:flex-row md:items-center"
>
<Skeleton className="h-12 w-full md:w-2/12" />
<Skeleton className="h-12 w-full md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-1/12" />
</div>
))}
</div>
</Card>
<tr className="border-border-neutral-secondary border-b last:border-b-0">
{/* Email */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-32 rounded" />
</td>
{/* State - text */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-16 rounded" />
</td>
{/* Role - text */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-20 rounded" />
</td>
{/* Inserted At - date */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-24 rounded" />
</td>
{/* Expires At - date */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-24 rounded" />
</td>
{/* Actions */}
<td className="px-2 py-4">
<Skeleton className="size-6 rounded" />
</td>
</tr>
);
};
export const SkeletonTableInvitation = () => {
const rows = 10;
return (
<div className="border-border-neutral-secondary bg-bg-neutral-secondary flex w-full flex-col gap-4 overflow-hidden rounded-[14px] border p-4 shadow-sm">
{/* Toolbar: Search + Total entries */}
<div className="flex items-center justify-between">
{/* Search icon button */}
<Skeleton className="size-10 rounded-md" />
{/* Total entries */}
<Skeleton className="h-4 w-28 rounded" />
</div>
{/* Table */}
<table className="w-full">
<thead>
<tr className="border-border-neutral-secondary border-b">
{/* Email */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-16 rounded" />
</th>
{/* State */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-12 rounded" />
</th>
{/* Role */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-12 rounded" />
</th>
{/* Inserted At */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-24 rounded" />
</th>
{/* Expires At */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-24 rounded" />
</th>
{/* Actions - empty header */}
<th className="w-10 py-3" />
</tr>
</thead>
<tbody>
{Array.from({ length: rows }).map((_, i) => (
<SkeletonTableRow key={i} />
))}
</tbody>
</table>
{/* Pagination */}
<div className="flex items-center justify-between pt-2">
{/* Rows per page */}
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-24 rounded" />
<Skeleton className="h-9 w-16 rounded-md" />
</div>
{/* Page info + navigation */}
<div className="flex items-center gap-4">
<Skeleton className="h-4 w-24 rounded" />
<div className="flex gap-1">
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
</div>
</div>
</div>
</div>
);
};
@@ -1,39 +1,94 @@
import React from "react";
import { Card } from "@/components/shadcn/card/card";
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
export const SkeletonTableGroups = () => {
const SkeletonTableRow = () => {
return (
<Card variant="base" padding="md" className="flex flex-col gap-4">
{/* Table headers */}
<div className="hidden gap-4 md:flex">
<Skeleton className="h-8 w-1/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-1/12" />
<Skeleton className="h-8 w-1/12" />
</div>
{/* Table body */}
<div className="flex flex-col gap-3">
{[...Array(3)].map((_, index) => (
<div
key={index}
className="flex flex-col gap-4 md:flex-row md:items-center"
>
<Skeleton className="h-12 w-full md:w-1/12" />
<Skeleton className="h-12 w-full md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-1/12" />
<Skeleton className="hidden h-12 md:block md:w-1/12" />
</div>
))}
</div>
</Card>
<tr className="border-border-neutral-secondary border-b last:border-b-0">
{/* Name - text */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-32 rounded" />
</td>
{/* Providers - count in circle */}
<td className="px-3 py-4">
<Skeleton className="size-8 rounded-full" />
</td>
{/* Roles - count in circle */}
<td className="px-3 py-4">
<Skeleton className="size-8 rounded-full" />
</td>
{/* Added - date */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-24 rounded" />
</td>
{/* Actions */}
<td className="px-2 py-4">
<Skeleton className="size-6 rounded" />
</td>
</tr>
);
};
export const SkeletonTableGroups = () => {
const rows = 10;
return (
<div className="border-border-neutral-secondary bg-bg-neutral-secondary flex w-full flex-col gap-4 overflow-hidden rounded-[14px] border p-4 shadow-sm">
{/* Toolbar: Search + Total entries */}
<div className="flex items-center justify-between">
{/* Search icon button */}
<Skeleton className="size-10 rounded-md" />
{/* Total entries */}
<Skeleton className="h-4 w-28 rounded" />
</div>
{/* Table */}
<table className="w-full">
<thead>
<tr className="border-border-neutral-secondary border-b">
{/* Name */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-12 rounded" />
</th>
{/* Providers */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-20 rounded" />
</th>
{/* Roles */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-16 rounded" />
</th>
{/* Added */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-14 rounded" />
</th>
{/* Actions - empty header */}
<th className="w-10 py-3" />
</tr>
</thead>
<tbody>
{Array.from({ length: rows }).map((_, i) => (
<SkeletonTableRow key={i} />
))}
</tbody>
</table>
{/* Pagination */}
<div className="flex items-center justify-between pt-2">
{/* Rows per page */}
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-24 rounded" />
<Skeleton className="h-9 w-16 rounded-md" />
</div>
{/* Page info + navigation */}
<div className="flex items-center gap-4">
<Skeleton className="h-4 w-24 rounded" />
<div className="flex gap-1">
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
</div>
</div>
</div>
</div>
);
};
@@ -1,37 +1,102 @@
import React from "react";
import { Card } from "@/components/shadcn/card/card";
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
export const SkeletonTableRoles = () => {
const SkeletonTableRow = () => {
return (
<Card variant="base" padding="md" className="flex flex-col gap-4">
{/* Table headers */}
<div className="hidden gap-4 md:flex">
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-1/12" />
</div>
{/* Table body */}
<div className="flex flex-col gap-3">
{[...Array(10)].map((_, index) => (
<div
key={index}
className="flex flex-col gap-4 md:flex-row md:items-center"
>
<Skeleton className="h-12 w-full md:w-2/12" />
<Skeleton className="h-12 w-full md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-1/12" />
</div>
))}
</div>
</Card>
<tr className="border-border-neutral-secondary border-b last:border-b-0">
{/* Role - bold name text */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-32 rounded" />
</td>
{/* Users - count text */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-16 rounded" />
</td>
{/* Invitations - count text */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-20 rounded" />
</td>
{/* Permissions - state text */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-20 rounded" />
</td>
{/* Added - date */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-24 rounded" />
</td>
{/* Actions */}
<td className="px-2 py-4">
<Skeleton className="size-6 rounded" />
</td>
</tr>
);
};
export const SkeletonTableRoles = () => {
const rows = 10;
return (
<div className="border-border-neutral-secondary bg-bg-neutral-secondary flex w-full flex-col gap-4 overflow-hidden rounded-[14px] border p-4 shadow-sm">
{/* Toolbar: Search + Total entries */}
<div className="flex items-center justify-between">
{/* Search icon button */}
{/* <Skeleton className="size-10 rounded-md" /> */}
{/* Total entries */}
<Skeleton className="h-4 w-28 rounded" />
</div>
{/* Table */}
<table className="w-full">
<thead>
<tr className="border-border-neutral-secondary border-b">
{/* Role */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-12 rounded" />
</th>
{/* Users */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-14 rounded" />
</th>
{/* Invitations */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-20 rounded" />
</th>
{/* Permissions */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-24 rounded" />
</th>
{/* Added */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-14 rounded" />
</th>
{/* Actions - empty header */}
<th className="w-10 py-3" />
</tr>
</thead>
<tbody>
{Array.from({ length: rows }).map((_, i) => (
<SkeletonTableRow key={i} />
))}
</tbody>
</table>
{/* Pagination */}
<div className="flex items-center justify-between pt-2">
{/* Rows per page */}
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-24 rounded" />
<Skeleton className="h-9 w-16 rounded-md" />
</div>
{/* Page info + navigation */}
<div className="flex items-center gap-4">
<Skeleton className="h-4 w-24 rounded" />
<div className="flex gap-1">
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
</div>
</div>
</div>
</div>
);
};
@@ -1,74 +1,156 @@
import { Card, CardContent, Skeleton } from "@/components/shadcn";
import {
Card,
CardAction,
CardContent,
CardHeader,
Separator,
Skeleton,
} from "@/components/shadcn";
// Mirrors the real profile layout (UserBasicInfoCard full-width + a two-column
// grid with RolesCard and Organizations) so the page does not shift when the
// data resolves. Conditional cards (SAML / API keys) depend on permissions not
// known while loading, so we mirror only the always-present structure.
export const SkeletonUserInfo = () => {
return (
<div className="flex flex-col gap-6">
{/* User Information */}
<Card>
<CardContent className="p-3">
<div className="flex flex-col gap-3">
{/* Name */}
<div className="flex items-center justify-between">
<p className="text-text-neutral-secondary text-sm font-semibold">
Name:
</p>
<Skeleton className="h-5 w-24 rounded-lg" />
</div>
{/* Email */}
<div className="flex items-center justify-between">
<p className="text-text-neutral-secondary text-sm font-semibold">
Email:
</p>
<Skeleton className="h-5 w-32 rounded-lg" />
</div>
{/* Company */}
<div className="flex items-center justify-between">
<p className="text-text-neutral-secondary text-sm font-semibold">
Company:
</p>
<Skeleton className="h-5 w-28 rounded-lg" />
</div>
{/* Date Joined */}
<div className="flex items-center justify-between">
<p className="text-text-neutral-secondary text-sm font-semibold">
Date Joined:
</p>
<Skeleton className="h-5 w-36 rounded-lg" />
</div>
{/* Tenant ID */}
<div className="flex items-center justify-between">
<p className="text-text-neutral-secondary text-sm font-semibold">
Tenant ID:
</p>
<Skeleton className="h-5 w-32 rounded-lg" />
</div>
</div>
</CardContent>
</Card>
{/* Roles */}
<Card>
<CardContent className="p-3">
<h4 className="mb-3 text-sm font-semibold">Roles</h4>
<div className="flex flex-wrap gap-2">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-6 w-20 rounded-full" />
))}
</div>
</CardContent>
</Card>
{/* Memberships */}
<Card>
<CardContent className="p-3">
<h4 className="mb-3 text-sm font-semibold">Memberships</h4>
<div className="flex flex-col gap-2">
{[1, 2].map((i) => (
<Skeleton key={i} className="h-16 w-full rounded-md" />
))}
</div>
</CardContent>
</Card>
<div className="flex w-full flex-col gap-6">
<UserBasicInfoSkeleton />
<div className="flex flex-col gap-6 xl:flex-row">
<div className="flex w-full flex-col gap-6 xl:max-w-[50%]">
<RolesCardSkeleton />
</div>
<div className="flex w-full flex-col gap-6 xl:max-w-[50%]">
<OrganizationsCardSkeleton />
</div>
</div>
</div>
);
};
const UserBasicInfoSkeleton = () => {
return (
<Card variant="base" padding="none" className="p-4">
<CardContent>
{/* Avatar + name / email */}
<div className="flex items-center gap-4">
<Skeleton className="size-10 rounded-full" />
<div className="flex flex-col gap-1.5">
<Skeleton className="h-4 w-32 rounded" />
<Skeleton className="h-3 w-48 rounded" />
</div>
</div>
<Separator className="my-4" />
{/* Date Joined + Organization ID */}
<div className="flex flex-row gap-4 md:gap-8">
<FieldSkeleton labelWidth="w-20" valueWidth="w-24" />
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
<Skeleton className="h-3 w-28 rounded" />
<Skeleton className="h-8 w-full max-w-xs rounded-md" />
</div>
</div>
</CardContent>
</Card>
);
};
const RolesCardSkeleton = () => {
return (
<Card variant="base" padding="none" className="p-4">
<CardContent>
{/* Header: "Active roles" + subtitle */}
<div className="mb-6 flex flex-col gap-1.5">
<Skeleton className="h-6 w-28 rounded" />
<Skeleton className="h-3 w-48 rounded" />
</div>
<div className="flex flex-col gap-2">
<RoleItemSkeleton />
<RoleItemSkeleton />
</div>
</CardContent>
</Card>
);
};
// Mirrors <RoleItem>: inner card with a role badge + state on the left, a
// details toggle on the right, and a grid of permission rows below.
const RoleItemSkeleton = () => {
return (
<Card variant="inner">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Skeleton className="h-5 w-20 rounded-full" />
<Skeleton className="h-3 w-16 rounded" />
</div>
<Skeleton className="h-5 w-20 rounded" />
</div>
<div className="border-border-neutral-primary mt-4 grid grid-cols-1 gap-3 border-t pt-4 md:grid-cols-2">
{[0, 1, 2, 3].map((i) => (
<div key={i} className="flex items-center gap-2">
<Skeleton className="size-4 rounded-full" />
<Skeleton className="h-3 w-24 rounded" />
</div>
))}
</div>
</Card>
);
};
const OrganizationsCardSkeleton = () => {
return (
<Card variant="base" padding="none" className="p-4">
<CardHeader>
<div className="flex flex-col gap-1.5">
<Skeleton className="h-5 w-32 rounded" />
<Skeleton className="h-3 w-52 rounded" />
</div>
<CardAction>
<Skeleton className="h-8 w-36 rounded-md" />
</CardAction>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-2">
<MembershipItemSkeleton />
<MembershipItemSkeleton />
</div>
</CardContent>
</Card>
);
};
// Mirrors <MembershipItem>: role badge (left), Name / Joined fields, and the
// "Active" badge (right).
const MembershipItemSkeleton = () => {
return (
<Card variant="inner" className="p-2">
<div className="flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
<Skeleton className="h-5 w-16 rounded-full" />
<div className="flex flex-row flex-wrap gap-1 gap-x-4">
<FieldSkeleton inline labelWidth="w-10" valueWidth="w-24" />
<FieldSkeleton inline labelWidth="w-14" valueWidth="w-20" />
</div>
<div className="ml-auto">
<Skeleton className="h-5 w-14 rounded-full" />
</div>
</div>
</Card>
);
};
// Label + value pair. `inline` lays them side by side (info fields inside a
// row); otherwise stacked (a standalone field).
const FieldSkeleton = ({
labelWidth,
valueWidth,
inline = false,
}: {
labelWidth: string;
valueWidth: string;
inline?: boolean;
}) => {
return (
<div className={inline ? "flex items-center gap-2" : "flex flex-col gap-1.5"}>
<Skeleton className={`h-3 ${labelWidth} rounded`} />
<Skeleton className={`h-4 ${valueWidth} rounded`} />
</div>
);
};
@@ -1,37 +1,102 @@
import React from "react";
import { Card } from "@/components/shadcn/card/card";
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
export const SkeletonTableUser = () => {
const SkeletonTableRow = () => {
return (
<Card variant="base" padding="md" className="flex flex-col gap-4">
{/* Table headers */}
<div className="hidden gap-4 md:flex">
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-2/12" />
<Skeleton className="h-8 w-1/12" />
</div>
{/* Table body */}
<div className="flex flex-col gap-3">
{[...Array(10)].map((_, index) => (
<div
key={index}
className="flex flex-col gap-4 md:flex-row md:items-center"
>
<Skeleton className="h-12 w-full md:w-2/12" />
<Skeleton className="h-12 w-full md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-2/12" />
<Skeleton className="hidden h-12 md:block md:w-1/12" />
</div>
))}
</div>
</Card>
<tr className="border-border-neutral-secondary border-b last:border-b-0">
{/* Name */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-28 rounded" />
</td>
{/* Email */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-32 rounded" />
</td>
{/* Role */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-20 rounded" />
</td>
{/* Company name */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-24 rounded" />
</td>
{/* Joined - date */}
<td className="px-3 py-4">
<Skeleton className="h-4 w-24 rounded" />
</td>
{/* Actions */}
<td className="px-2 py-4">
<Skeleton className="size-6 rounded" />
</td>
</tr>
);
};
export const SkeletonTableUser = () => {
const rows = 10;
return (
<div className="border-border-neutral-secondary bg-bg-neutral-secondary flex w-full flex-col gap-4 overflow-hidden rounded-[14px] border p-4 shadow-sm">
{/* Toolbar: Search + Total entries */}
<div className="flex items-center justify-between">
{/* Search icon button */}
<Skeleton className="size-10 rounded-md" />
{/* Total entries */}
<Skeleton className="h-4 w-28 rounded" />
</div>
{/* Table */}
<table className="w-full">
<thead>
<tr className="border-border-neutral-secondary border-b">
{/* Name */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-12 rounded" />
</th>
{/* Email */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-14 rounded" />
</th>
{/* Role */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-10 rounded" />
</th>
{/* Company name */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-28 rounded" />
</th>
{/* Joined */}
<th className="px-3 py-3 text-left">
<Skeleton className="h-4 w-16 rounded" />
</th>
{/* Actions - empty header */}
<th className="w-10 py-3" />
</tr>
</thead>
<tbody>
{Array.from({ length: rows }).map((_, i) => (
<SkeletonTableRow key={i} />
))}
</tbody>
</table>
{/* Pagination */}
<div className="flex items-center justify-between pt-2">
{/* Rows per page */}
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-24 rounded" />
<Skeleton className="h-9 w-16 rounded-md" />
</div>
{/* Page info + navigation */}
<div className="flex items-center gap-4">
<Skeleton className="h-4 w-24 rounded" />
<div className="flex gap-1">
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
<Skeleton className="size-9 rounded-md" />
</div>
</div>
</div>
</div>
);
};