diff --git a/src/components/medications/__tests__/medication-card-glp1.test.tsx b/src/components/medications/__tests__/medication-card-glp1.test.tsx index b3980c448..34ebb131a 100644 --- a/src/components/medications/__tests__/medication-card-glp1.test.tsx +++ b/src/components/medications/__tests__/medication-card-glp1.test.tsx @@ -362,13 +362,15 @@ describe(" — GLP-1 variant rendering", () => { expect(html).not.toContain("Low stock"); }); - it("side-effect quick-log button hands off the medication object", () => { - // SSR can't fire DOM clicks, so we smoke-check the contract: - // - the button renders when onLogSideEffect is supplied - // - invoking the handler synchronously delivers the GLP-1 - // medication object the parent will then prefill MoodEntry with - // (the parent's MoodEntry mutation reads med.name to build the - // pre-tagged side-effect entry). + it("side-effect quick-log moves into the header actions overflow", () => { + // v1.4.37 W4b — the side-effect quick-log no longer sits in the + // primary actions row (which used to read as a three-button row + // and broke symmetry with the generic Ramipril card). It now + // lives behind a kebab overflow in the header `actions` slot. + // SSR doesn't open Radix Portal content, so we assert the trigger + // surfaces with the localised `common.moreOptions` aria-label and + // smoke-check that invoking the handler delivers the GLP-1 + // medication object the parent prefills MoodEntry with. const client = makeClient(); seedCompliance(client, med7p5.id); seedGlp1Details(client, med7p5.id, {}); @@ -383,21 +385,27 @@ describe(" — GLP-1 variant rendering", () => { client, ); - // Button renders with the localised label. - expect(html).toContain("Log side effect"); + // Kebab trigger renders with the localised aria-label. + expect(html).toContain('aria-label="More options"'); + // The side-effect button must no longer ride alongside + // Eingenommen / Übersprungen in the primary actions row. + expect(html).not.toMatch( + /class="[^"]*flex[^"]*gap-2[^"]*"[^>]*>[\s\S]*?Log side effect/, + ); - // Invoke the supplied handler the way the button's onClick would — - // pins the prefill payload contract for MoodEntry. + // Invoke the supplied handler the way the menu item's onClick + // would — pins the prefill payload contract for MoodEntry. handler(med7p5); expect(handler).toHaveBeenCalledWith( expect.objectContaining({ name: "Mounjaro", treatmentClass: "GLP1" }), ); }); - it("omits the side-effect button when onLogSideEffect is not supplied", () => { - // Back-compat: the button is opt-in via the prop. Pages that - // haven't wired the MoodEntry hand-off yet should still render - // the card without a dead button. + it("omits the overflow kebab when onLogSideEffect is not supplied", () => { + // Back-compat: the overflow is opt-in via the prop. Pages that + // haven't wired the MoodEntry hand-off yet render Mounjaro with + // exactly the same header-actions shape as Ramipril (history + + // edit, no kebab) so the medications list stays symmetric. const client = makeClient(); seedCompliance(client, med7p5.id); seedGlp1Details(client, med7p5.id, {}); @@ -407,7 +415,7 @@ describe(" — GLP-1 variant rendering", () => { client, ); - expect(html).not.toContain("Log side effect"); + expect(html).not.toContain('aria-label="More options"'); }); it("AI-disabled state: no Coach hand-off button is rendered today", () => { @@ -451,8 +459,11 @@ describe(" — GLP-1 variant rendering", () => { expect(html).toContain("opacity-60"); expect(html).toContain("Paused since"); // Primary actions ("Taken" / "Skipped") are suppressed for - // inactive medications. - expect(html).not.toContain("Log side effect"); + // inactive medications. The header-actions overflow trigger is + // unconditionally absent when `onLogSideEffect` is not supplied + // (this test fixture doesn't wire it), so Mounjaro and Ramipril + // headers stay shape-equivalent in the paused state. + expect(html).not.toContain('aria-label="More options"'); }); it("renders German copy under the 'de' locale", () => { diff --git a/src/components/medications/glp1-medication-card.tsx b/src/components/medications/glp1-medication-card.tsx index 755684cc8..0179da3c2 100644 --- a/src/components/medications/glp1-medication-card.tsx +++ b/src/components/medications/glp1-medication-card.tsx @@ -8,6 +8,7 @@ import { Flame, History, Loader2, + MoreVertical, Pencil, SkipForward, Stethoscope, @@ -16,6 +17,12 @@ import { import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader"; import { Progress } from "@/components/ui/progress"; import { useTranslations, useFormatters } from "@/lib/i18n/context"; @@ -332,6 +339,35 @@ export function Glp1MedicationCard({ > + {/* v1.4.37 W4b — GLP-1 specifics (side-effect quick-log etc.) + live in the header actions overflow so the primary action + row stays the canonical two-button shape (Eingenommen / + Übersprungen) shared with the generic medication card. The + kebab only renders when at least one overflow item is wired + for this medication kind. */} + {onLogSideEffect && ( + + + + + + onLogSideEffect(medication)} + className="whitespace-nowrap" + > + + {t("medications.glp1LogSideEffect")} + + + + )} ); @@ -482,18 +518,20 @@ export function Glp1MedicationCard({ )} - {/* Primary actions row — matches the generic card's - min-h-11 tap-target rule. The third button opens the - side-effect log pre-tagged with the GLP-1 drug name. */} + {/* Primary actions row — byte-equivalent with the generic + medication card. v1.4.37 W4b moved the GLP-1-specific + side-effect quick-log out of this row and into the + header-actions overflow (kebab) so Mounjaro and Ramipril + share the canonical two-button primary row. */} {medication.active && ( -
+
- {onLogSideEffect && ( - - )}
)}