- {isLoading &&
}
+ {isLoading &&
}
{!isLoading && workspace && (
@@ -342,3 +342,42 @@ export function WorkspaceDetail() {
);
}
+
+function WorkspaceDetailSkeleton() {
+ return (
+
+
+ {Array.from({ length: 4 }).map((_, index) => (
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ {Array.from({ length: 4 }).map((_, index) => (
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/web/src/components/workspaces/WorkspaceList.tsx b/packages/web/src/components/workspaces/WorkspaceList.tsx
index 64789d2..b15f597 100644
--- a/packages/web/src/components/workspaces/WorkspaceList.tsx
+++ b/packages/web/src/components/workspaces/WorkspaceList.tsx
@@ -6,8 +6,8 @@ import { useWorkspaces } from "@/api/queries";
import type { components } from "@/api/schema.d.ts";
import { EmptyState } from "@/components/shared/EmptyState";
import { ErrorAlert } from "@/components/shared/ErrorAlert";
-import { PageLoader } from "@/components/shared/LoadingSpinner";
import { Pagination } from "@/components/shared/Pagination";
+import { Skeleton } from "@/components/shared/Skeleton";
import { SortControl, type SortDir } from "@/components/shared/SortControl";
import { MonoCaption, Muted, PageTitle } from "@/components/ui/typography";
import { useDemo } from "@/hooks/useDemo";
@@ -94,7 +94,7 @@ export function WorkspaceList() {
- {isLoading &&
}
+ {isLoading &&
}
{!isLoading && workspaces.length === 0 && (
);
}
+
+function WorkspaceListSkeleton() {
+ return (
+
+
+ {Array.from({ length: 5 }).map((_, index) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/web/src/index.css b/packages/web/src/index.css
index 21ca813..7f91ec0 100644
--- a/packages/web/src/index.css
+++ b/packages/web/src/index.css
@@ -196,6 +196,44 @@ body::after {
outline: none;
}
+@keyframes skeleton-shimmer {
+ 0% {
+ background-position: 200% 0;
+ }
+
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+.theme-skeleton {
+ background-image: linear-gradient(
+ 90deg,
+ var(--skeleton-base) 0%,
+ var(--skeleton-highlight) 50%,
+ var(--skeleton-base) 100%
+ );
+ background-size: 200% 100%;
+ animation: skeleton-shimmer 1.6s ease-in-out infinite;
+}
+
+.theme-skeleton--accent {
+ background-image: linear-gradient(
+ 90deg,
+ var(--skeleton-accent-base) 0%,
+ var(--skeleton-accent-highlight) 50%,
+ var(--skeleton-accent-base) 100%
+ );
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .theme-skeleton,
+ .theme-skeleton--accent {
+ animation: none;
+ background-position: 50% 0;
+ }
+}
+
/* ─── Responsive container ─── */
/* Base: padding + centering only. Width is a CSS variable so modifiers cascade. */
.page-container {