mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(medications): rebuild the advanced-settings sheet as a single column
Drop the lg:grid-cols-2 data|reminders grid that read as overloaded. The sheet now narrows from 4xl to 2xl and stacks four bordered section cards in most-used-first order: Reminders, Lifecycle, Data, Danger. One spacing rule throughout — space-y-4 between cards, divide-y plus py-3 rows inside each card, with the SettingsGroup card owning the dividers so no row interleaves its own separator.
This commit is contained in:
1 parent
a3b34bc0cf
commit
795898bb0a
9 files changed
+113
-127
No files matched your search
+1
-2
@@ -1281,8 +1281,7 @@
|
||||
"group": {
|
||||
"data": "Daten",
|
||||
"reminders": "Erinnerungen",
|
||||
"lifecycle": "Lebenszyklus",
|
||||
"danger": "Gefahrenzone"
|
||||
"lifecycle": "Lebenszyklus"
|
||||
},
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
|
||||
+1
-2
@@ -1281,8 +1281,7 @@
|
||||
"group": {
|
||||
"data": "Data",
|
||||
"reminders": "Reminders",
|
||||
"lifecycle": "Lifecycle",
|
||||
"danger": "Danger zone"
|
||||
"lifecycle": "Lifecycle"
|
||||
},
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
|
||||
+1
-2
@@ -1281,8 +1281,7 @@
|
||||
"group": {
|
||||
"data": "Datos",
|
||||
"reminders": "Recordatorios",
|
||||
"lifecycle": "Ciclo de vida",
|
||||
"danger": "Zona de peligro"
|
||||
"lifecycle": "Ciclo de vida"
|
||||
},
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
|
||||
+1
-2
@@ -1281,8 +1281,7 @@
|
||||
"group": {
|
||||
"data": "Données",
|
||||
"reminders": "Rappels",
|
||||
"lifecycle": "Cycle de vie",
|
||||
"danger": "Zone de danger"
|
||||
"lifecycle": "Cycle de vie"
|
||||
},
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
|
||||
+1
-2
@@ -1281,8 +1281,7 @@
|
||||
"group": {
|
||||
"data": "Dati",
|
||||
"reminders": "Promemoria",
|
||||
"lifecycle": "Ciclo di vita",
|
||||
"danger": "Zona pericolosa"
|
||||
"lifecycle": "Ciclo di vita"
|
||||
},
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
|
||||
+1
-2
@@ -1281,8 +1281,7 @@
|
||||
"group": {
|
||||
"data": "Dane",
|
||||
"reminders": "Przypomnienia",
|
||||
"lifecycle": "Cykl życia",
|
||||
"danger": "Strefa zagrożenia"
|
||||
"lifecycle": "Cykl życia"
|
||||
},
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/**
|
||||
* v1.7.0 — AdvancedSettingsSheet composition contract.
|
||||
*
|
||||
* The sheet hosts four labelled groups — Data → Reminders → Lifecycle →
|
||||
* Danger zone — composed from bare section bodies. `<ResponsiveSheet>`
|
||||
* The sheet hosts four blocks — Reminders → Lifecycle → Data → Danger
|
||||
* zone — composed from bare section bodies. `<ResponsiveSheet>`
|
||||
* wraps Radix portals that `renderToStaticMarkup` does not materialise,
|
||||
* so we mock it to a passthrough that renders children only when `open`
|
||||
* and echoes `contentWidth`. The bodies are mocked to ordered markers.
|
||||
* The test pins:
|
||||
* - the four groups render in the documented order;
|
||||
* - the sheet opens at the `4xl` width token;
|
||||
* - the four blocks render in the documented order;
|
||||
* - the sheet opens at the `2xl` width token;
|
||||
* - `onRequestPhaseSheet` threads through to the phases row;
|
||||
* - `onOpenImport` wires the import button (co-located with export);
|
||||
* - the export control sits beside the import control;
|
||||
@@ -107,27 +107,27 @@ function baseProps(
|
||||
}
|
||||
|
||||
describe("AdvancedSettingsSheet (v1.7.0)", () => {
|
||||
it("renders the four groups in Data → Reminders → Lifecycle → Danger order", () => {
|
||||
it("renders the four blocks in Reminders → Lifecycle → Data → Danger order", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<AdvancedSettingsSheet
|
||||
{...baseProps(true, { onRequestPhaseSheet: () => {} })}
|
||||
/>,
|
||||
);
|
||||
const data = html.indexOf("advanced-group-data");
|
||||
const reminders = html.indexOf("advanced-group-reminders");
|
||||
const lifecycle = html.indexOf("advanced-group-lifecycle");
|
||||
const danger = html.indexOf("advanced-group-danger");
|
||||
expect(data).toBeGreaterThan(-1);
|
||||
expect(reminders).toBeGreaterThan(data);
|
||||
const data = html.indexOf("advanced-group-data");
|
||||
const danger = html.indexOf("mock-danger");
|
||||
expect(reminders).toBeGreaterThan(-1);
|
||||
expect(lifecycle).toBeGreaterThan(reminders);
|
||||
expect(danger).toBeGreaterThan(lifecycle);
|
||||
expect(data).toBeGreaterThan(lifecycle);
|
||||
expect(danger).toBeGreaterThan(data);
|
||||
});
|
||||
|
||||
it("opens at the 4xl width token", () => {
|
||||
it("opens at the 2xl width token", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<AdvancedSettingsSheet {...baseProps(true)} />,
|
||||
);
|
||||
expect(html).toContain('data-content-width="4xl"');
|
||||
expect(html).toContain('data-content-width="2xl"');
|
||||
});
|
||||
|
||||
it("threads onRequestPhaseSheet through to the phases row", () => {
|
||||
|
||||
@@ -3,24 +3,21 @@
|
||||
/**
|
||||
* v1.7.0 — Advanced settings sheet.
|
||||
* v1.7.2 — widened to `4xl` and re-laid into a responsive two-column
|
||||
* grid so a normal desktop viewport shows every group without forcing
|
||||
* a long single-column scroll.
|
||||
* grid.
|
||||
* v1.8.6 — rebuilt as a single column. The two-column data|reminders
|
||||
* grid read as overloaded; the sheet now narrows to `2xl` (~672px) and
|
||||
* stacks four bordered section cards in most-used-first order:
|
||||
* Reminders → Lifecycle → Data → Danger. One spacing rule throughout —
|
||||
* `space-y-4` between cards, `divide-y` + `py-3` rows inside each card
|
||||
* (the `<SettingsGroup>` card owns the dividers).
|
||||
*
|
||||
* Four labelled groups under one consistent visual system (R-medui §5):
|
||||
*
|
||||
* - Data: intake Import + medications CSV Export (co-located) +
|
||||
* external-API endpoint / token.
|
||||
* - Reminders: notifications switch + reminder window (grace).
|
||||
* - Lifecycle: pause/resume + end course + phases.
|
||||
* - Danger zone: purge intake history + delete medication.
|
||||
*
|
||||
* Layout: on `lg+` the first three groups flow into a two-column grid —
|
||||
* the Data group (tallest, with the export toggle + token snippets)
|
||||
* holds the left column; Reminders + Lifecycle stack down the right.
|
||||
* The Danger zone always spans the full width at the bottom, set off by
|
||||
* its own destructive-tinted card, so an irreversible action never
|
||||
* shares a column with a routine toggle. On `<lg` everything stacks in
|
||||
* the documented Data → Reminders → Lifecycle → Danger order.
|
||||
* - Data: intake Import + medications CSV Export (co-located) +
|
||||
* external-API endpoint / token.
|
||||
* - Danger zone: purge intake history + delete medication, set off by
|
||||
* its own destructive-tinted card so an irreversible action never
|
||||
* sits next to a routine toggle.
|
||||
*
|
||||
* Button styling is consistent: reversible / neutral actions are
|
||||
* `outline`, destructive actions are `destructive font-semibold`,
|
||||
@@ -32,7 +29,6 @@
|
||||
* self-saves, so the footer slot stays empty.
|
||||
*/
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { SettingsGroup } from "@/components/medications/settings-group";
|
||||
import { ApiTokensRow } from "@/components/medications/sections/api-tokens-row";
|
||||
@@ -95,96 +91,86 @@ export function AdvancedSettingsSheet({
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t("medications.detail.advanced.title")}
|
||||
contentWidth="4xl"
|
||||
bodyClassName="gap-6"
|
||||
contentWidth="2xl"
|
||||
bodyClassName="gap-4"
|
||||
>
|
||||
<div className="space-y-6" data-slot="advanced-settings-sheet-body">
|
||||
{/* Top region: Data + Reminders + Lifecycle in a two-column grid
|
||||
on lg+, single column below. `items-start` keeps each group
|
||||
top-aligned so the shorter right column never stretches. */}
|
||||
<div className="grid items-start gap-6 lg:grid-cols-2">
|
||||
{/* DATA — import + export + external API. Tallest group → left
|
||||
column on lg+ so the right column's two shorter groups
|
||||
balance against it. */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.data")}
|
||||
dataSlot="advanced-group-data"
|
||||
>
|
||||
<div className="space-y-4" data-slot="advanced-settings-sheet-body">
|
||||
{/* REMINDERS — notifications + reminder window. Most-used group
|
||||
first; each row is a padded `<div>` so the group card's
|
||||
`divide-y` draws the hairline between them. */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.reminders")}
|
||||
dataSlot="advanced-group-reminders"
|
||||
>
|
||||
<div className="py-3">
|
||||
<NotificationsBody
|
||||
medicationId={medicationId}
|
||||
notificationsEnabled={notificationsEnabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="py-3">
|
||||
<GraceRow
|
||||
medicationId={medicationId}
|
||||
reminderGraceMinutes={reminderGraceMinutes}
|
||||
/>
|
||||
</div>
|
||||
</SettingsGroup>
|
||||
|
||||
{/* LIFECYCLE — pause/resume + end course + phases */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.lifecycle")}
|
||||
dataSlot="advanced-group-lifecycle"
|
||||
>
|
||||
<div className="py-3">
|
||||
<LifecycleManageBody
|
||||
medicationId={medicationId}
|
||||
medicationName={medicationName}
|
||||
active={active}
|
||||
onAfterAction={() => onOpenChange(false)}
|
||||
/>
|
||||
</div>
|
||||
{isGlp1 && (
|
||||
<div className="py-3">
|
||||
<PhasesRow
|
||||
medicationId={medicationId}
|
||||
treatmentClass={treatmentClass}
|
||||
startsOn={startsOn}
|
||||
endsOn={endsOn}
|
||||
onRequestPhaseSheet={onRequestPhaseSheet}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SettingsGroup>
|
||||
|
||||
{/* DATA — import + export + external API */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.data")}
|
||||
dataSlot="advanced-group-data"
|
||||
>
|
||||
<div className="py-3">
|
||||
<DataPortabilityRow
|
||||
medicationId={medicationId}
|
||||
onOpenImport={onOpenImport}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
</div>
|
||||
<div className="py-3">
|
||||
<ApiTokensRow
|
||||
medicationId={medicationId}
|
||||
medicationName={medicationName}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
|
||||
{/* Right column: Reminders + Lifecycle stacked. */}
|
||||
<div className="space-y-6">
|
||||
{/* REMINDERS — notifications + reminder window */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.reminders")}
|
||||
dataSlot="advanced-group-reminders"
|
||||
>
|
||||
<NotificationsBody
|
||||
medicationId={medicationId}
|
||||
notificationsEnabled={notificationsEnabled}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<GraceRow
|
||||
medicationId={medicationId}
|
||||
reminderGraceMinutes={reminderGraceMinutes}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
|
||||
{/* LIFECYCLE — pause/resume + end course + phases */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.lifecycle")}
|
||||
dataSlot="advanced-group-lifecycle"
|
||||
>
|
||||
<LifecycleManageBody
|
||||
medicationId={medicationId}
|
||||
medicationName={medicationName}
|
||||
active={active}
|
||||
onAfterAction={() => onOpenChange(false)}
|
||||
/>
|
||||
|
||||
{isGlp1 && (
|
||||
<>
|
||||
<Separator />
|
||||
<PhasesRow
|
||||
medicationId={medicationId}
|
||||
treatmentClass={treatmentClass}
|
||||
startsOn={startsOn}
|
||||
endsOn={endsOn}
|
||||
onRequestPhaseSheet={onRequestPhaseSheet}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DANGER ZONE — purge + delete. Full-width at the bottom, set
|
||||
off by the destructive-tinted card inside the body so an
|
||||
irreversible action never shares a column with a toggle. */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.danger")}
|
||||
dataSlot="advanced-group-danger"
|
||||
>
|
||||
<DangerZoneBody
|
||||
medicationId={medicationId}
|
||||
medicationName={medicationName}
|
||||
intakeCount={intakeCount}
|
||||
onAfterAction={() => onOpenChange(false)}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
|
||||
{/* DANGER ZONE — purge + delete. Set off by the destructive-
|
||||
tinted card so an irreversible action never sits next to a
|
||||
routine toggle. The body supplies its own matched-pair card,
|
||||
so it slots in without the neutral group frame. */}
|
||||
<DangerZoneBody
|
||||
medicationId={medicationId}
|
||||
medicationName={medicationName}
|
||||
intakeCount={intakeCount}
|
||||
onAfterAction={() => onOpenChange(false)}
|
||||
/>
|
||||
</div>
|
||||
</ResponsiveSheet>
|
||||
);
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
/**
|
||||
* v1.7.0 — advanced-settings group header.
|
||||
*
|
||||
* Lightweight presentational grouping for the redesigned
|
||||
* `<AdvancedSettingsSheet>`: an uppercase micro-label over a hairline
|
||||
* rule, then the group body. Reads as "one settings document, four
|
||||
* parts" rather than three bordered section cards.
|
||||
* v1.8.6 — rebuilt as a bordered section card. The sheet now stacks
|
||||
* every group in a single column (Reminders → Lifecycle → Data →
|
||||
* Danger), so each group needs its own frame to read as a distinct
|
||||
* block rather than a hairline-separated run. The card owns one spacing
|
||||
* rule: an uppercase micro-label header, then a `divide-y` body where
|
||||
* each row carries `py-3`. Callers pass one `<div>` per row; the card
|
||||
* supplies the dividers so no row interleaves its own `<Separator>`.
|
||||
*
|
||||
* The label is a plain `<p>` (not a heading with an id) so it stays out
|
||||
* of the `aria-labelledby` graph — the sheet already owns the dialog
|
||||
@@ -17,26 +19,30 @@
|
||||
|
||||
import { type ReactNode } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface SettingsGroupProps {
|
||||
label: ReactNode;
|
||||
children: ReactNode;
|
||||
dataSlot?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SettingsGroup({
|
||||
label,
|
||||
children,
|
||||
dataSlot,
|
||||
className,
|
||||
}: SettingsGroupProps) {
|
||||
return (
|
||||
<section
|
||||
className="space-y-4"
|
||||
className={cn("bg-card rounded-lg border px-4 py-1", className)}
|
||||
{...(dataSlot ? { "data-slot": dataSlot } : {})}
|
||||
>
|
||||
<p className="border-border/60 text-muted-foreground border-t pt-1 text-xs font-medium tracking-wide uppercase">
|
||||
<p className="text-muted-foreground border-border/60 border-b py-2.5 text-xs font-medium tracking-wide uppercase">
|
||||
{label}
|
||||
</p>
|
||||
{children}
|
||||
<div className="divide-border/60 divide-y">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user