From e65cdc97b2b4d41951741999590b200a8e4a33da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 16 May 2026 17:49:46 +0200 Subject: [PATCH] fix(ui): reserve Button loader space to eliminate CLS during in-flight requests Lighthouse v1.4.33 run flagged a 0.186 CLS attached to a `div.space-y-2 > Button > Loader-Circle` element. Root cause: the two heavy submit buttons in `/bugreport` and the medications import dialog drew `{loading && }` -- rendering BOTH icons during the in-flight request and growing the button by ~24 px. The fix is to swap (not append) the leading icon, so Loader2 occupies the same 16x16 slot the static icon would. - src/app/bugreport/page.tsx: ternary swap on `loading`, pass `aria-busy` for screen-reader narration. - src/app/medications/page.tsx (Import dialog): same shape. - src/components/ui/button.tsx: document the loading-state contract on the primitive so future contributors apply the swap convention by default. (Callsites inside `src/components/settings/**` are owned by IW4 -- left untouched, the doc-comment makes the convention visible from the primitive.) --- src/app/bugreport/page.tsx | 19 ++++++++++++++++--- src/app/medications/page.tsx | 16 ++++++++++++++-- src/components/ui/button.tsx | 25 +++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/app/bugreport/page.tsx b/src/app/bugreport/page.tsx index 21dce49c4..c9c920058 100644 --- a/src/app/bugreport/page.tsx +++ b/src/app/bugreport/page.tsx @@ -227,9 +227,22 @@ export default function BugReportPage() { )}
-
diff --git a/src/app/medications/page.tsx b/src/app/medications/page.tsx index 690c103c7..6e52ebbbc 100644 --- a/src/app/medications/page.tsx +++ b/src/app/medications/page.tsx @@ -505,9 +505,21 @@ function IntakeImportDialog({ diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 642a7a72f..0e3df88f6 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -4,6 +4,31 @@ import { Slot } from "radix-ui"; import { cn } from "@/lib/utils"; +/** + * v1.4.33 IW9 — loading-state convention. + * + * Callers driving a transient loading state on a button must SWAP the + * leading icon for `` rather than APPENDING the spinner + * next to the existing icon. The legacy `{loading && }` + * pattern grew the button by ~24 px during the request, which produced + * the 0.186 CLS hit flagged on the v1.4.33 Lighthouse run. The + * spinner and the icon are both 16 px (`h-4 w-4`); swapping keeps the + * intrinsic button width stable. + * + * ```tsx + * + * ``` + * + * Also: pass `aria-busy={loading || undefined}` so screen readers + * announce the in-flight state. + */ const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", {