mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(responsive): align medication-row and side-effects at narrow viewports
Two narrow-viewport regressions surface on 320 px phones. The medication-list row's category badge and state badges share line 2 via flex flex-wrap; any state badge pushes the row to three lines and breaks the FB-G1 two-line contract for ~20 % of configured drugs. State badges now ride their own row below the category badge so the canonical row stays two lines. The side-effects timeline pinned its date column at 88 px; the longest short-date variant fits inside 56 px with slack, so the column narrows and the category + entry + severity left slot recovers 32 px of wrap headroom.
This commit is contained in:
1 parent
f91b4732ea
commit
786fbde6a5
4 files changed
+151
-3
No files matched your search
@@ -48,12 +48,27 @@ export function MedicationCardHeader({
|
||||
<CardTitle className="text-lg">
|
||||
{name} {dose}
|
||||
</CardTitle>
|
||||
<div className="text-muted-foreground flex flex-wrap items-center gap-2 text-sm">
|
||||
{/* D-H6 — state badges (without-notification, paused-since,
|
||||
inactive) used to share line 2 with the category badge
|
||||
via `flex flex-wrap`. At 320 px any state badge pushed
|
||||
the row to three lines and broke the FB-G1 "two-line,
|
||||
no exceptions" contract for ~20 % of configured drugs.
|
||||
State badges now ride their own row below the category
|
||||
badge so the canonical row stays two lines on narrow
|
||||
viewports. */}
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{categoryLabel}
|
||||
</Badge>
|
||||
{stateBadges}
|
||||
</div>
|
||||
{stateBadges ? (
|
||||
<div
|
||||
data-slot="medication-card-header-state-badges"
|
||||
className="text-muted-foreground flex flex-wrap items-center gap-2 text-sm"
|
||||
>
|
||||
{stateBadges}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{actions ? (
|
||||
<div className="flex items-center gap-0.5">{actions}</div>
|
||||
|
||||
@@ -303,7 +303,14 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
|
||||
</Badge>
|
||||
</div>
|
||||
<p
|
||||
className="text-muted-foreground w-[5.5rem] shrink-0 text-right text-xs tabular-nums"
|
||||
// D-H7 — narrowed from `w-[5.5rem]` (88 px) to
|
||||
// `w-14` (56 px). The longest short-date variant
|
||||
// ("15. Mai") fits inside 56 px with slack;
|
||||
// recovering 32 px relieves the left slot's
|
||||
// wrap rhythm at 320 px so the category badge +
|
||||
// entry label + severity chip stop pushing the
|
||||
// free-text notes off-screen.
|
||||
className="text-muted-foreground w-14 shrink-0 text-right text-xs tabular-nums"
|
||||
data-slot="side-effect-row-date"
|
||||
>
|
||||
{fmt.dateShort(new Date(row.occurredAt))}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader";
|
||||
|
||||
/**
|
||||
* v1.4.28 — narrow-viewport contract for the shared medication-list
|
||||
* row primitive (D-H6).
|
||||
*
|
||||
* FB-G1 calls for a two-line shape: `{name} {dose}` on line 1 and the
|
||||
* category badge on line 2. State badges (without-notification, paused,
|
||||
* inactive) used to share line 2 via `flex flex-wrap`; on a 320 px
|
||||
* viewport that pushed the row to three lines for ~20 % of configured
|
||||
* drugs. State badges now ride their own row below the category badge
|
||||
* so the canonical row stays two lines on narrow viewports.
|
||||
*
|
||||
* The tests assert the structural seams, not the visual measurement
|
||||
* (no DOM measurement layer in SSR). The data-slot on the state-badges
|
||||
* row plus the absence of state badges inside the category row are the
|
||||
* load-bearing contracts.
|
||||
*/
|
||||
|
||||
function render(node: React.ReactNode) {
|
||||
return renderToStaticMarkup(<Card>{node}</Card>);
|
||||
}
|
||||
|
||||
describe("<MedicationCardHeader>", () => {
|
||||
it("paints `{name} {dose}` on the title row", () => {
|
||||
const html = render(
|
||||
<MedicationCardHeader
|
||||
name="Ramipril"
|
||||
dose="5 mg"
|
||||
categoryLabel="Blood Pressure"
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("Ramipril 5 mg");
|
||||
expect(html).toContain("Blood Pressure");
|
||||
});
|
||||
|
||||
it("does not paint the state-badges row when no badges are supplied", () => {
|
||||
const html = render(
|
||||
<MedicationCardHeader
|
||||
name="Ramipril"
|
||||
dose="5 mg"
|
||||
categoryLabel="Blood Pressure"
|
||||
/>,
|
||||
);
|
||||
// Pin the absence so a future refactor can't paint an empty
|
||||
// wrapper that eats vertical space.
|
||||
expect(html).not.toContain(
|
||||
'data-slot="medication-card-header-state-badges"',
|
||||
);
|
||||
});
|
||||
|
||||
it("breaks state badges onto their own row below the category badge (D-H6)", () => {
|
||||
// The narrow-viewport contract: state badges live on their own
|
||||
// row so the canonical row stays two lines at 320 px even when
|
||||
// a drug carries one of the state badges. Pinning the seam.
|
||||
const html = render(
|
||||
<MedicationCardHeader
|
||||
name="Ramipril"
|
||||
dose="5 mg"
|
||||
categoryLabel="Blood Pressure"
|
||||
stateBadges={
|
||||
<>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Without notification
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Paused since 12.05
|
||||
</Badge>
|
||||
</>
|
||||
}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain(
|
||||
'data-slot="medication-card-header-state-badges"',
|
||||
);
|
||||
// Both badges land inside the state row, not adjacent to the
|
||||
// category badge. Verifying via document order: the state-badges
|
||||
// wrapper must paint AFTER the category badge.
|
||||
const categoryIdx = html.indexOf("Blood Pressure");
|
||||
const stateRowIdx = html.indexOf(
|
||||
'data-slot="medication-card-header-state-badges"',
|
||||
);
|
||||
expect(categoryIdx).toBeGreaterThan(-1);
|
||||
expect(stateRowIdx).toBeGreaterThan(categoryIdx);
|
||||
});
|
||||
});
|
||||
@@ -194,6 +194,41 @@ describe("<SideEffectsSection> — timeline rows", () => {
|
||||
expect(html).toContain("Very severe");
|
||||
});
|
||||
|
||||
it("narrows the date column to w-14 at narrow viewports (D-H7)", () => {
|
||||
// The earlier `w-[5.5rem]` (88 px) date column overspec pushed
|
||||
// the category badge + entry label + severity chip's left slot
|
||||
// into a wrap-prone shape at 320 px. The narrowed `w-14` (56 px)
|
||||
// fits the longest short-date variant ("15. Mai") with slack
|
||||
// and recovers 32 px for the free-text notes.
|
||||
const client = makeClient();
|
||||
seedSideEffects(client, "med-1", [
|
||||
{
|
||||
id: "se-1",
|
||||
category: "GI",
|
||||
entry: "NAUSEA",
|
||||
severity: 2,
|
||||
occurredAt: "2026-05-13T08:00:00.000Z",
|
||||
notes: null,
|
||||
},
|
||||
]);
|
||||
const html = render(<SideEffectsSection medicationId="med-1" />, client);
|
||||
// Class attribute order in SSR depends on JSX prop order; match
|
||||
// either order so the assertion stays stable across React minor
|
||||
// versions.
|
||||
const dateCell = html.match(
|
||||
/<p[^>]*data-slot="side-effect-row-date"[^>]*>|<p[^>]*class="[^"]*w-14[^"]*"[^>]*data-slot="side-effect-row-date"/,
|
||||
);
|
||||
expect(dateCell).not.toBeNull();
|
||||
// Source-of-truth check via the rendered HTML — pin both the
|
||||
// adoption (`w-14`) and the absence (`w-[5.5rem]`).
|
||||
expect(html).toContain('data-slot="side-effect-row-date"');
|
||||
const tagWithSlot = html.match(
|
||||
/<p[^>]*data-slot="side-effect-row-date"[^>]*>/,
|
||||
);
|
||||
expect(tagWithSlot?.[0]).toContain("w-14");
|
||||
expect(tagWithSlot?.[0]).not.toContain("w-[5.5rem]");
|
||||
});
|
||||
|
||||
it("renders German entry and category labels when locale=de", () => {
|
||||
const client = makeClient();
|
||||
seedSideEffects(client, "med-1", [
|
||||
|
||||
Reference in new issue
Block a user