From 9c8660a4e05358607d1d4cb88783c84031a5951b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Fri, 15 May 2026 16:31:42 +0200
Subject: [PATCH] feat(settings): replace single-spinner loading with skeleton
rows on Thresholds and Sources
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replaces the centred ` Loading` line in
`` and `` with a skeleton
list whose row count matches the loaded UI (14 rows each — one per
`METRIC_ORDER` entry and one per `SOURCE_PRIORITY_METRIC_KEYS` entry
respectively). The skeleton rows reuse the live UI's container
classes (`border-border space-y-3 rounded-lg border p-4` on
Thresholds; `border-border bg-background/30 space-y-2 rounded-md
border p-3` on Sources) so the post-fetch swap lands in place. The
`` primitive's `motion-reduce:animate-none` covers the
reduced-motion preference. Combined with the `min-h` shell reserve
from the previous commit, this removes the page-height jump that
landed under the sticky sidebar when these two sections finished
loading.
---
src/components/settings/sources-section.tsx | 36 ++++++++++++--
.../settings/thresholds-editor-section.tsx | 48 +++++++++++++++----
2 files changed, 70 insertions(+), 14 deletions(-)
diff --git a/src/components/settings/sources-section.tsx b/src/components/settings/sources-section.tsx
index b4c07876e..fb0f48f19 100644
--- a/src/components/settings/sources-section.tsx
+++ b/src/components/settings/sources-section.tsx
@@ -40,6 +40,7 @@ import {
} from "lucide-react";
import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import {
@@ -314,10 +315,7 @@ export function SourcesSection() {
{isLoading || !priority ? (
-
-
- {t("common.loading")}
-
+
) : (
{SOURCE_PRIORITY_METRIC_KEYS.map((metric) => {
@@ -564,3 +562,33 @@ export function SourcesSection() {
);
}
+
+/**
+ * Skeleton placeholder rendered while `/api/auth/me/source-priority`
+ * is in flight. Reserves one row per `SOURCE_PRIORITY_METRIC_KEYS`
+ * entry at roughly the loaded height so the page does not jump when
+ * the fetched ladder list swaps in. The pulsing animation honours
+ * `prefers-reduced-motion` via Tailwind's `motion-reduce:animate-none`.
+ */
+function SourcesSkeletonList() {
+ return (
+
+ {SOURCE_PRIORITY_METRIC_KEYS.map((metric) => (
+
+ ))}
+
+ );
+}
diff --git a/src/components/settings/thresholds-editor-section.tsx b/src/components/settings/thresholds-editor-section.tsx
index 036142a11..2e337bf00 100644
--- a/src/components/settings/thresholds-editor-section.tsx
+++ b/src/components/settings/thresholds-editor-section.tsx
@@ -3,15 +3,11 @@
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
-import {
- SlidersHorizontal,
- RotateCcw,
- Loader2,
- AlertTriangle,
-} from "lucide-react";
+import { SlidersHorizontal, RotateCcw, AlertTriangle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
@@ -147,10 +143,7 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
{isLoading || !data ? (
-
-
- {t("common.loading")}
-
+
) : (
{METRIC_ORDER.map((metric) => (
@@ -170,6 +163,41 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
);
}
+/**
+ * Skeleton placeholder rendered while `/api/user/thresholds` is in
+ * flight. Reserves one row per `METRIC_ORDER` entry at roughly the
+ * loaded height so the page does not jump when the fetched list
+ * swaps in. The pulsing animation honours `prefers-reduced-motion`
+ * via Tailwind's `motion-reduce:animate-none`.
+ */
+function ThresholdsSkeletonList() {
+ return (
+
+ {METRIC_ORDER.map((metric) => (
+
+ ))}
+
+ );
+}
+
interface MetricRowProps {
metric: ThresholdMetric;
effective: EffectiveRange | undefined;