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) => ( + + ))}
) : (