From 2dda933d77e173403813d0c3b77fd0ea5f03735d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 8 Oct 2026 20:20:03 +0200 Subject: [PATCH] Align layout settings cards and the insights arrange editor The insights arrange list becomes a Settings card with its own header and a Save that waits for a change; the self-link back to the page it sits on and the focus grab on load are gone. The medications view card takes the header shape the labs and documents cards use, the order card shares their title and loads with row placeholders, and the documents view card uses their icon. --- .../insights-edit-mode-manager.test.tsx | 22 +++ .../insights/insights-edit-mode.tsx | 176 ++++++++++++------ .../__tests__/medications-section.test.tsx | 3 +- src/components/settings/documents-section.tsx | 4 +- .../insights-overview-arrange-section.tsx | 33 ++-- .../settings/medications-section.tsx | 39 ++-- 6 files changed, 188 insertions(+), 89 deletions(-) diff --git a/src/components/insights/__tests__/insights-edit-mode-manager.test.tsx b/src/components/insights/__tests__/insights-edit-mode-manager.test.tsx index d39d4fad5..0b4a4d599 100644 --- a/src/components/insights/__tests__/insights-edit-mode-manager.test.tsx +++ b/src/components/insights/__tests__/insights-edit-mode-manager.test.tsx @@ -60,4 +60,26 @@ describe(" — detail-page manager retired (v1.15.20)", () => expect(html).toContain('data-slot="insights-edit-section-row"'); expect(html).toContain('data-slot="insights-edit-section-eye"'); }); + + it("as a Settings card: settings header, one action row last, no self-link", () => { + // On /settings/layout/insights the editor is a standing card. The + // pointer "Manage chips in Settings" would link to the page it is on, + // and Reset + Done beside the title broke the one-action-row rule. + const html = render( + {}} + />, + ); + expect(html).not.toContain('data-slot="insights-edit-manage-link"'); + expect(html).toContain('id="insights-overview-arrange-title"'); + const row = html.indexOf('data-slot="settings-card-actions"'); + expect(row).toBeGreaterThan( + html.lastIndexOf('data-slot="insights-edit-section-row"'), + ); + expect(html.slice(row)).toContain('data-slot="insights-edit-done"'); + expect(html.slice(row)).toContain(">Save"); + }); }); diff --git a/src/components/insights/insights-edit-mode.tsx b/src/components/insights/insights-edit-mode.tsx index e16191a35..7ccf96c92 100644 --- a/src/components/insights/insights-edit-mode.tsx +++ b/src/components/insights/insights-edit-mode.tsx @@ -4,7 +4,14 @@ import { useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; -import { Eye, EyeOff, GripVertical, Loader2, RotateCcw } from "lucide-react"; +import { + Eye, + EyeOff, + GripVertical, + LayoutGrid, + Loader2, + RotateCcw, +} from "lucide-react"; import { DndContext, KeyboardSensor, @@ -24,6 +31,8 @@ import { CSS } from "@dnd-kit/utilities"; import { Button } from "@/components/ui/button"; import { SettingsCard } from "@/components/settings/settings-card"; +import { SettingsCardActions } from "@/components/settings/_card-actions"; +import { SettingsCardHeader } from "@/components/settings/_card-header"; import { ConfirmButton } from "@/components/ui/confirm-button"; import { cn } from "@/lib/utils"; import { prefersReducedMotion } from "@/lib/charts/reduced-motion"; @@ -88,13 +97,24 @@ interface InsightsEditModeProps { gatedOffSectionIds: ReadonlySet; /** Close edit mode (the "Fertig" / save-success path calls this). */ onClose: () => void; + /** + * `page` (default) is the inline editor the Insights overview opens: its own + * title, Reset + Done beside it, and a pointer to the Settings pill manager. + * `settings` is the same editor as a standing Settings card: a settings + * header, the actions in one row at the bottom (design standards §12), no + * pointer back to the page it already sits on, and no focus grab on mount — + * the card is part of the page, not a surface the user just opened. + */ + variant?: "page" | "settings"; } export function InsightsEditMode({ layout, gatedOffSectionIds, onClose, + variant = "page", }: InsightsEditModeProps) { + const inSettings = variant === "settings"; const { t } = useTranslations(); const queryClient = useQueryClient(); @@ -115,8 +135,9 @@ export function InsightsEditMode({ // "Anpassen" toggle on close via the page's onClose handler. const headingRef = useRef(null); useEffect(() => { + if (inSettings) return; headingRef.current?.focus(); - }, []); + }, [inSettings]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), @@ -186,6 +207,15 @@ export function InsightsEditMode({ }); const busy = saveMutation.isPending || resetMutation.isPending; + // The Settings card's Save waits for a change, like the pill-order Save + // beside it; the overview's inline editor keeps "Done" live because it + // also closes the editor. + const arrangement = (rows: readonly InsightsSectionConfig[]) => + [...rows] + .sort((a, b) => a.order - b.order) + .map((r) => `${r.id}:${r.visible ? 1 : 0}`) + .join(","); + const dirty = arrangement(draft.sections) !== arrangement(layout.sections); const sections = useMemo( () => [...draft.sections].sort((a, b) => a.order - b.order), @@ -215,46 +245,55 @@ export function InsightsEditMode({ return ( -
-
-

- {t("insights.editMode.title")} -

-

- {t("insights.editMode.description")} -

+ {inSettings ? ( + + ) : ( +
+
+

+ {t("insights.editMode.title")} +

+

+ {t("insights.editMode.description")} +

+
+
+ } + label={t("insights.editMode.reset")} + title={t("insights.editMode.resetTitle")} + body={t("insights.editMode.resetBody")} + confirmLabel={t("insights.editMode.resetConfirm")} + disabled={busy && !resetMutation.isPending} + pending={resetMutation.isPending} + onConfirm={() => resetMutation.mutate()} + /> + +
-
- } - label={t("insights.editMode.reset")} - title={t("insights.editMode.resetTitle")} - body={t("insights.editMode.resetBody")} - confirmLabel={t("insights.editMode.resetConfirm")} - disabled={busy && !resetMutation.isPending} - pending={resetMutation.isPending} - onConfirm={() => resetMutation.mutate()} - /> - -
-
+ )} {allHidden && (

- {/* v1.15.20 — the per-detail-page manager (sort + show/hide) lives on - Settings → Insights; the disclosure this card used to nest under - the Vitals row duplicated it. Keep a quiet pointer instead. */} -

- - {t("insights.editMode.manageInSettings")} - -

+ {inSettings ? ( + + } + label={t("insights.editMode.reset")} + title={t("insights.editMode.resetTitle")} + body={t("insights.editMode.resetBody")} + confirmLabel={t("insights.editMode.resetConfirm")} + disabled={busy && !resetMutation.isPending} + pending={resetMutation.isPending} + onConfirm={() => resetMutation.mutate()} + /> + + + ) : ( + // v1.15.20 — the per-detail-page manager (sort + show/hide) lives on + // Settings → Insights; the disclosure this card used to nest under + // the Vitals row duplicated it. Keep a quiet pointer instead. +

+ + {t("insights.editMode.manageInSettings")} + +

+ )} ); } diff --git a/src/components/settings/__tests__/medications-section.test.tsx b/src/components/settings/__tests__/medications-section.test.tsx index f44a9f36d..8704f98e9 100644 --- a/src/components/settings/__tests__/medications-section.test.tsx +++ b/src/components/settings/__tests__/medications-section.test.tsx @@ -97,7 +97,8 @@ describe(" — SSR smoke", () => { it("renders the German copy end-to-end", () => { const html = render("de"); - expect(html).toContain("Reihenfolge anpassen"); + // The order card shares its title with the Labs order card. + expect(html).toContain(">Reihenfolge<"); expect(html).toContain( "Wähle, ob deine Medikamente als Karten oder als Tabelle angezeigt werden.", ); diff --git a/src/components/settings/documents-section.tsx b/src/components/settings/documents-section.tsx index f1e9b4f12..35f60b49b 100644 --- a/src/components/settings/documents-section.tsx +++ b/src/components/settings/documents-section.tsx @@ -9,7 +9,7 @@ * shape): the card/list view and the month arrangement. Both persist per user * through `/api/documents/inbound/layout`. */ -import { Eye, Rows3, WrapText } from "lucide-react"; +import { LayoutGrid, Rows3, WrapText } from "lucide-react"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { SettingsCard } from "@/components/settings/settings-card"; @@ -26,7 +26,7 @@ export function DocumentsSection() {
} diff --git a/src/components/settings/insights-overview-arrange-section.tsx b/src/components/settings/insights-overview-arrange-section.tsx index 5acb424cb..5ed574433 100644 --- a/src/components/settings/insights-overview-arrange-section.tsx +++ b/src/components/settings/insights-overview-arrange-section.tsx @@ -9,8 +9,8 @@ import { useAuth } from "@/hooks/use-auth"; import { useAiCapability } from "@/hooks/use-ai-capability"; import { useInsightsLayoutQuery } from "@/hooks/use-insights-layout"; import { InsightsEditMode } from "@/components/insights/insights-edit-mode"; -import { SectionHeading } from "@/components/ui/section-heading"; import { SettingsCard } from "@/components/settings/settings-card"; +import { SettingsCardHeader } from "@/components/settings/_card-header"; import { INSIGHTS_SECTION_IDS, type InsightsSectionId, @@ -59,29 +59,28 @@ export function InsightsOverviewArrangeSection({ id }: { id?: string }) { id={id} data-slot="insights-overview-arrange-section" aria-labelledby="insights-overview-arrange-title" - className="space-y-3" + className="scroll-mt-28" > - - {/* Gate the editor mount until the layout GET settles. The editor seeds its draft once from `layout` on mount, which is the canonical default - while in flight; mounting it early would let a "Fertig" save flush - defaults over the user's real saved layout (the same QA-L1 gate the - mother page applies to its "Anpassen" toggle). */} + while in flight; mounting it early would let a Save flush defaults + over the user's real saved layout (the same QA-L1 gate the mother + page applies to its "Anpassen" toggle). The loading card paints the + same header the loaded card carries (design standards §13), so the + page does not reflow under the cursor when the list arrives. */} {isLoading ? ( - - + + + ) : ( {}} diff --git a/src/components/settings/medications-section.tsx b/src/components/settings/medications-section.tsx index 6fc8ec40f..d02f97ed4 100644 --- a/src/components/settings/medications-section.tsx +++ b/src/components/settings/medications-section.tsx @@ -1,7 +1,7 @@ "use client"; import { useQuery } from "@tanstack/react-query"; -import { LayoutGrid, Loader2, Pill } from "lucide-react"; +import { LayoutGrid, ListOrdered } from "lucide-react"; import { MedicationOrderEditor, @@ -9,6 +9,7 @@ import { } from "@/components/medications/medication-order-editor"; import { MedicationViewToggle } from "@/components/medications/medication-view-toggle"; import { SettingsCard } from "@/components/settings/settings-card"; +import { Skeleton } from "@/components/ui/skeleton"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { InjectionSitesCard } from "@/components/settings/injection-sites-card"; import { useAuth } from "@/hooks/use-auth"; @@ -81,33 +82,39 @@ export function MedicationsSection() { {/* View preference — cards vs table. The shared header toggle writes optimistically, so there is no Save button here. */} + {/* Same shape as the Labs and Documents view cards: the hint is the + header description, the toggle sits in the status slot. */} -
-

- {t("medications.viewToggleHint")} -

- {isLayoutLoading ? ( - - ) : ( + description={t("medications.viewToggleHint")} + status={ + // Painted from the first frame, like the Labs and Documents + // toggles: a spinner the width of one icon swapped for a + // two-button toggle moved the cards below it. - )} -
+ } + />
{/* Manual order — applies to both list views. */} {isLoading || isLayoutLoading ? ( -
- - {t("common.loading")} + // Row-shaped placeholders instead of a one-line spinner, so the + // card below moves less when the list arrives. +
+ {Array.from({ length: 3 }, (_, i) => ( + + ))}
) : (