fix(site): hide table headers during loading and empty states on workspaces page (#23446)

## Problem

When the workspaces table transitions between states (loading →
populated, or populated → empty search results), the table column
headers visibly jump. This happens because the Actions column's width is
content-driven: when workspace rows are present, the action buttons give
it intrinsic width, shrinking the Name/Template/Status columns. When the
body is empty or loading, the Actions column collapses to zero, and the
other columns expand to fill the space.

## Solution

Hide the header content during loading and empty states using
`visibility: hidden` (Tailwind's `invisible` class), which preserves the
row's layout height but hides the text. This prevents the visual jump
since headers aren't visible during the states where column widths
differ.

- **Loading**: first column shows a skeleton bar matching the body
skeleton aesthetic; other columns are invisible
- **Empty search results**: all header content is invisible
- **Populated**: headers display normally

---------

Co-authored-by: Jaayden Halko <jaayden@coder.com>
This commit is contained in:
TJ
2026-03-29 23:28:30 -07:00
committed by GitHub
co-authored by Jaayden Halko
parent 4d2b0a2f82
commit 78986efed8
2 changed files with 49 additions and 33 deletions
@@ -195,6 +195,13 @@ export const AllStates: Story = {
},
};
export const Loading: Story = {
args: {
workspaces: undefined,
count: undefined,
},
};
export const AllStatesWithFavorites: Story = {
args: {
workspaces: allWorkspaces.map((workspace, i) => ({
@@ -1,4 +1,3 @@
import Skeleton from "@mui/material/Skeleton";
import {
BanIcon,
CloudIcon,
@@ -43,8 +42,8 @@ import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog"
import { ExternalImage } from "#/components/ExternalImage/ExternalImage";
import { VSCodeIcon } from "#/components/Icons/VSCodeIcon";
import { VSCodeInsidersIcon } from "#/components/Icons/VSCodeInsidersIcon";
import { Skeleton } from "#/components/Skeleton/Skeleton";
import { Spinner } from "#/components/Spinner/Spinner";
import { Stack } from "#/components/Stack/Stack";
import {
Table,
TableBody,
@@ -111,47 +110,57 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
chatsByWorkspace,
}) => {
const dashboard = useDashboard();
const isLoading = !workspaces;
const isEmpty = workspaces && workspaces.length === 0;
const hideHeaders = isLoading || isEmpty;
return (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-1/3">
<div className="flex items-center gap-5">
<Checkbox
disabled={!workspaces || workspaces.length === 0}
checked={
workspaces &&
workspaces.length > 0 &&
checkedWorkspaces.length === workspaces.length
}
onCheckedChange={(checked) => {
if (!workspaces) {
return;
{isLoading ? (
<Skeleton className="h-4 w-40" />
) : (
<div
className={cn(
"flex items-center gap-5",
isEmpty && "invisible",
)}
>
<Checkbox
disabled={isEmpty}
checked={
!isEmpty && checkedWorkspaces.length === workspaces.length
}
if (!checked) {
onCheckChange([]);
} else {
onCheckChange(workspaces);
}
}}
aria-label="Select all workspaces"
className="my-0"
/>
Name
</div>
onCheckedChange={(checked) => {
if (!checked) {
onCheckChange([]);
} else {
onCheckChange(workspaces);
}
}}
aria-label="Select all workspaces"
className="my-0"
/>
Name
</div>
)}
</TableHead>
<TableHead className={cn("w-1/3", hideHeaders && "invisible")}>
Template
</TableHead>
<TableHead className={cn("w-1/3", hideHeaders && "invisible")}>
Status
</TableHead>
<TableHead className="w-1/3">Template</TableHead>
<TableHead className="w-1/3">Status</TableHead>
<TableHead className="w-0">
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody className="[&_td]:h-[72px]">
{!workspaces && <TableLoader />}
{workspaces && workspaces.length === 0 && (
{isLoading && <TableLoader />}
{isEmpty && (
<TableRow>
<TableCell colSpan={999}>
<WorkspacesEmpty
@@ -198,7 +207,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
/>
<AvatarData
title={
<Stack direction="row" spacing={0.5} alignItems="center">
<div className="flex items-center gap-0.5">
<span className="whitespace-nowrap">
{workspace.name}
</span>
@@ -224,7 +233,7 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
</Link>
</Badge>
)}
</Stack>
</div>
}
subtitle={
<div className="flex items-center gap-1">
@@ -355,11 +364,11 @@ const TableLoader: FC = () => {
<AvatarDataSkeleton />
</TableCell>
<TableCell className="w-2/6">
<Skeleton variant="text" width="50%" />
<Skeleton className="h-4 w-1/2" />
</TableCell>
<TableCell className="w-0 ">
<div className="flex gap-1 justify-end">
<Skeleton variant="rounded" width={40} height={40} />
<Skeleton className="h-10 w-10" />
<Button size="icon-lg" variant="subtle" disabled>
<EllipsisVertical aria-hidden="true" />
</Button>