From 91d5db5853161b590ee3e616d9cabf4bf1fe1bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 15:09:16 +0200 Subject: [PATCH] fix(admin): align Coach-Feedback header with adjacent sections to prevent layout shift The Coach-Feedback admin section previously rendered four distinct top- level structures across its loading / error / empty / data branches. The loading branch was a thin flex-row stub without the heading, the error branch was a p-4 alert with no card chrome at all, and only the empty + data branches mounted the Sparkles icon + 'Coach Feedback' title inside the bg-card border-border rounded-xl border p-6 wrapper. The result: when the user clicked into /admin/coach-feedback the section first measured at ~52 px (loading) and then snapped down to its full height once the query resolved, shifting everything below it. Mirror the canonical pattern used by SystemStatusSection: render the outer card + heading unconditionally, and switch only the body between spinner / alert / empty / table. The header now sits at a constant y-offset from the section root regardless of fetch state. Adds a regression test that asserts the 'Coach Feedback' heading and the bg-card rounded-xl outer chrome are present in every render state. --- .../__tests__/coach-feedback-section.test.tsx | 30 +++ .../admin/coach-feedback-section.tsx | 205 +++++++++--------- 2 files changed, 129 insertions(+), 106 deletions(-) diff --git a/src/components/admin/__tests__/coach-feedback-section.test.tsx b/src/components/admin/__tests__/coach-feedback-section.test.tsx index 47b9d88e8..3d21af81b 100644 --- a/src/components/admin/__tests__/coach-feedback-section.test.tsx +++ b/src/components/admin/__tests__/coach-feedback-section.test.tsx @@ -107,4 +107,34 @@ describe("CoachFeedbackSection", () => { expect(html).toContain("Failed to load Coach feedback summary"); expect(html).toContain('role="alert"'); }); + + // v1.4.25 W8 regression — Marc reported a layout shift on + // /admin/coach-feedback. Root cause was the title only rendering in the + // empty + data branches, so the section grew taller (loading → data) + // and snapped down. Lock the heading + outer-card structure across + // every render state so the section maintains a constant top-of-card + // baseline. + it("keeps the section heading rendered across loading / error / empty states", () => { + for (const queryState of [ + { isLoading: true, isError: false, data: null }, + { isLoading: false, isError: true, data: null }, + { + isLoading: false, + isError: false, + data: { + generatedAt: "2026-05-10T04:00:00Z", + windowDays: 30, + coachBuckets: [], + }, + }, + ]) { + useQueryMock.mockReturnValue(queryState); + const html = render(); + // Sparkles icon + h2 title rendered in every state ⇒ constant + // header height ⇒ no layout shift when the query resolves. + expect(html).toContain("Coach Feedback"); + expect(html).toContain("bg-card"); + expect(html).toContain("rounded-xl"); + } + }); }); diff --git a/src/components/admin/coach-feedback-section.tsx b/src/components/admin/coach-feedback-section.tsx index 3bf76b386..4121c4fd6 100644 --- a/src/components/admin/coach-feedback-section.tsx +++ b/src/components/admin/coach-feedback-section.tsx @@ -64,125 +64,118 @@ export function CoachFeedbackSection() { }, }); - if (query.isLoading) { - return ( -
- - - {t("admin.coachFeedback.loading")} - -
- ); - } - - if (query.isError) { - return ( -
- {t("admin.coachFeedback.loadError")} -
- ); - } - - const summary = query.data; + const summary = query.data ?? null; const buckets = summary?.coachBuckets ?? []; + const hasData = !!summary && buckets.length > 0; - if (!summary || buckets.length === 0) { - return ( -
-
- -
- {t("admin.coachFeedback.title")} -
-
-

- {t("admin.coachFeedback.empty")} -

-
- ); - } - + // v1.4.25 W8 — keep the section's outer card + header rendered across + // every query state (loading / error / empty / data) so the heading + // baseline stays at a constant Y-offset. Previously the loading branch + // returned a thin flex-row stub and the error branch a p-4 alert with + // NO heading, which made the entire section snap downward when the + // fetched data resolved — Marc reported the visible layout shift on + // /admin/coach-feedback. Mirrors the canonical structure used by + // (header outside the fetch-state branch). return (
-
+
{t("admin.coachFeedback.title")}
-

- {t("admin.coachFeedback.windowLabel", { - days: String(summary.windowDays), - })} - {" · "} - {t("admin.coachFeedback.generatedAtLabel")}{" "} - {fmt.dateTime(summary.generatedAt)} -

+ {hasData && summary && ( +

+ {t("admin.coachFeedback.windowLabel", { + days: String(summary.windowDays), + })} + {" · "} + {t("admin.coachFeedback.generatedAtLabel")}{" "} + {fmt.dateTime(summary.generatedAt)} +

+ )}
-
- - - - - - - - - - - - - - {buckets.map((bucket, idx) => { - const ratePct = Math.round(bucket.helpfulRate * 100); - return ( - - - - - - - - + + + + + + + + ); + })} + +
- {t("admin.coachFeedback.colPromptVersion")} - - {t("admin.coachFeedback.colTone")} - - {t("admin.coachFeedback.colVerbosity")} - - {t("admin.coachFeedback.colHelpful")} - - {t("admin.coachFeedback.colNotHelpful")} - - {t("admin.coachFeedback.colN")} - - {t("admin.coachFeedback.colHelpfulRate")} -
- {bucket.promptVersion} - {bucket.tone}{bucket.verbosity}{bucket.helpful} - {bucket.notHelpful} - {bucket.total} + + + {t("admin.coachFeedback.loading")} + + + ) : query.isError ? ( +
+ {t("admin.coachFeedback.loadError")} +
+ ) : !hasData ? ( +

+ {t("admin.coachFeedback.empty")} +

+ ) : ( +
+ + + + + + + + + + + + + + {buckets.map((bucket, idx) => { + const ratePct = Math.round(bucket.helpfulRate * 100); + return ( + - {ratePct}% - - - ); - })} - -
+ {t("admin.coachFeedback.colPromptVersion")} + + {t("admin.coachFeedback.colTone")} + + {t("admin.coachFeedback.colVerbosity")} + + {t("admin.coachFeedback.colHelpful")} + + {t("admin.coachFeedback.colNotHelpful")} + + {t("admin.coachFeedback.colN")} + + {t("admin.coachFeedback.colHelpfulRate")} +
-
+
+ {bucket.promptVersion} + {bucket.tone}{bucket.verbosity}{bucket.helpful} + {bucket.notHelpful} + {bucket.total} + {ratePct}% +
+
+ )}
); }