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 ( + + ); +} 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 ( + + ); +} + interface MetricRowProps { metric: ThresholdMetric; effective: EffectiveRange | undefined;