diff --git a/platform/src/app/[tenant]/ai-exposure/loading.tsx b/platform/src/app/[tenant]/ai-exposure/loading.tsx
new file mode 100644
index 0000000..417df4a
--- /dev/null
+++ b/platform/src/app/[tenant]/ai-exposure/loading.tsx
@@ -0,0 +1,25 @@
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function AIExposureLoading() {
+ return (
+
+
+
+
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/platform/src/app/[tenant]/audit-log/loading.tsx b/platform/src/app/[tenant]/audit-log/loading.tsx
new file mode 100644
index 0000000..eb78dd3
--- /dev/null
+++ b/platform/src/app/[tenant]/audit-log/loading.tsx
@@ -0,0 +1,18 @@
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function AuditLogLoading() {
+ return (
+
+
+
+
+
+
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/platform/src/app/[tenant]/compare/loading.tsx b/platform/src/app/[tenant]/compare/loading.tsx
new file mode 100644
index 0000000..f184171
--- /dev/null
+++ b/platform/src/app/[tenant]/compare/loading.tsx
@@ -0,0 +1,22 @@
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function CompareLoading() {
+ return (
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/platform/src/app/[tenant]/connect/loading.tsx b/platform/src/app/[tenant]/connect/loading.tsx
new file mode 100644
index 0000000..26bafb4
--- /dev/null
+++ b/platform/src/app/[tenant]/connect/loading.tsx
@@ -0,0 +1,21 @@
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function ConnectLoading() {
+ return (
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+
+
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/platform/src/app/[tenant]/dashboard/loading.tsx b/platform/src/app/[tenant]/dashboard/loading.tsx
new file mode 100644
index 0000000..d171abd
--- /dev/null
+++ b/platform/src/app/[tenant]/dashboard/loading.tsx
@@ -0,0 +1,34 @@
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function DashboardLoading() {
+ return (
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/platform/src/app/[tenant]/repos/[repoName]/loading.tsx b/platform/src/app/[tenant]/repos/[repoName]/loading.tsx
new file mode 100644
index 0000000..e5f8d39
--- /dev/null
+++ b/platform/src/app/[tenant]/repos/[repoName]/loading.tsx
@@ -0,0 +1,27 @@
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function RepoDetailLoading() {
+ return (
+
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/platform/src/app/[tenant]/repos/loading.tsx b/platform/src/app/[tenant]/repos/loading.tsx
new file mode 100644
index 0000000..9a35d5b
--- /dev/null
+++ b/platform/src/app/[tenant]/repos/loading.tsx
@@ -0,0 +1,20 @@
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function ReposLoading() {
+ return (
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/platform/src/app/[tenant]/settings/integrations/[provider]/loading.tsx b/platform/src/app/[tenant]/settings/integrations/[provider]/loading.tsx
new file mode 100644
index 0000000..41b9ad5
--- /dev/null
+++ b/platform/src/app/[tenant]/settings/integrations/[provider]/loading.tsx
@@ -0,0 +1,22 @@
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function IntegrationDetailLoading() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/platform/src/app/[tenant]/settings/integrations/loading.tsx b/platform/src/app/[tenant]/settings/integrations/loading.tsx
new file mode 100644
index 0000000..c43f308
--- /dev/null
+++ b/platform/src/app/[tenant]/settings/integrations/loading.tsx
@@ -0,0 +1,15 @@
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function IntegrationsLoading() {
+ return (
+
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/platform/src/app/[tenant]/settings/loading.tsx b/platform/src/app/[tenant]/settings/loading.tsx
new file mode 100644
index 0000000..4e2fd56
--- /dev/null
+++ b/platform/src/app/[tenant]/settings/loading.tsx
@@ -0,0 +1,22 @@
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function SettingsLoading() {
+ return (
+
+
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+
+
+
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/platform/src/app/[tenant]/team/loading.tsx b/platform/src/app/[tenant]/team/loading.tsx
new file mode 100644
index 0000000..093d40b
--- /dev/null
+++ b/platform/src/app/[tenant]/team/loading.tsx
@@ -0,0 +1,25 @@
+import { Card, CardContent, CardHeader } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export default function TeamLoading() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/platform/src/components/WindowSelector.tsx b/platform/src/components/WindowSelector.tsx
index 7bf9085..87a0749 100644
--- a/platform/src/components/WindowSelector.tsx
+++ b/platform/src/components/WindowSelector.tsx
@@ -1,7 +1,11 @@
"use client";
+import { useTransition } from "react";
+
import { useRouter, usePathname, useSearchParams } from "next/navigation";
+import { Loader2 } from "lucide-react";
+
import {
Select,
SelectContent,
@@ -32,13 +36,21 @@ export function WindowSelector({
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
+ const [isPending, startTransition] = useTransition();
if (options.length < 2) return null;
function handleChange(value: string) {
const params = new URLSearchParams(searchParams.toString());
params.set(paramKey, value);
- router.replace(`${pathname}?${params.toString()}`, { scroll: false });
+ // Wrapping the navigation in a transition keeps this page visible with
+ // isPending=true instead of unmounting to the route's loading.tsx — the
+ // right feel for a filter (instant feedback on the control itself)
+ // rather than a full-page flash. Also blocks a second click from firing
+ // another replace() before the first one resolves.
+ startTransition(() => {
+ router.replace(`${pathname}?${params.toString()}`, { scroll: false });
+ });
}
return (
@@ -46,9 +58,17 @@ export function WindowSelector({
{t("common.windowSelector.label")}
-
+
-
+ {isPending ? (
+
+ ) : (
+
+ )}
{options.map((days) => (
diff --git a/platform/src/components/ui/skeleton.tsx b/platform/src/components/ui/skeleton.tsx
new file mode 100644
index 0000000..67c3229
--- /dev/null
+++ b/platform/src/components/ui/skeleton.tsx
@@ -0,0 +1,12 @@
+import { cn } from "@/lib/utils";
+
+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ );
+}
+
+export { Skeleton };