mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
refactor(export): highlight the health-record export, demote the doctor report, collapse the included-data list
The Settings export page over-emphasised the doctor-report PDF: it owned the page hero while the broader health-record export (PDF + FHIR R4 + zip package) sat below it as a plain card. The relative prominence is now the other way round. - The health-record export panel takes the page hero, with the same hero-gradient + glow-purple treatment the Insights hero strip uses. - The doctor-report card drops the hero styling and moves to the bottom of the page as a small secondary card. It stays fully functional — the same dialog + /api/doctor-report flow runs from there. - The health-record "included data" checklist is now a disclosure, collapsed by default, so the panel opens compact instead of as a long always-expanded list. It uses the inline aria-expanded/aria-controls pattern already used elsewhere, with no new dependency. Pure presentation/IA change: export routes, payloads, and the exported data are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
68cad9d11c
commit
d4801d4646
6 files changed
+196
-129
No files matched your search
+33
-11
@@ -3,12 +3,16 @@ import { expect, test } from "@playwright/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B7 — Settings → Export consolidated UI smoke.
|
||||
* Settings → Export consolidated UI smoke.
|
||||
*
|
||||
* Validates that:
|
||||
* 1. The five export cards are all rendered with their stable testids.
|
||||
* 2. Clicking the Measurements CSV download button fires a real
|
||||
* browser download — proving the new `/api/export/measurements`
|
||||
* 1. The export surfaces are all rendered with their stable testids:
|
||||
* the health-record export hero, the four CSV/JSON tiles, and the
|
||||
* demoted doctor-report card (v1.12).
|
||||
* 2. The health-record "included data" checklist is a disclosure,
|
||||
* collapsed by default, and expands on demand.
|
||||
* 3. Clicking the Measurements CSV download button fires a real
|
||||
* browser download — proving the `/api/export/measurements`
|
||||
* endpoint is reachable from the browser end-to-end.
|
||||
*
|
||||
* The other CSV / JSON cards share the same code path; one happy-path
|
||||
@@ -18,24 +22,42 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
test.describe("Settings → Export", () => {
|
||||
test.use({ storageState: STORAGE_STATE_PATH });
|
||||
|
||||
test("renders all five export cards with stable testids", async ({
|
||||
page,
|
||||
}) => {
|
||||
test("renders all export surfaces with stable testids", async ({ page }) => {
|
||||
await page.goto("/settings/export", { waitUntil: "domcontentloaded" });
|
||||
// v1.4.37 lifted the doctor-report card to a hero block with the
|
||||
// `export-hero-*` testid prefix; the four CSV/backup tiles keep
|
||||
// the `export-card-*` shape.
|
||||
// v1.12 — the health-record export is the page hero; the
|
||||
// doctor-report card (`export-hero-*` prefix) is demoted to the
|
||||
// bottom; the four CSV/backup tiles keep the `export-card-*` shape.
|
||||
for (const id of [
|
||||
"export-hero-doctor-report",
|
||||
"health-record-export-panel",
|
||||
"export-card-measurements-csv",
|
||||
"export-card-medications-csv",
|
||||
"export-card-mood-csv",
|
||||
"export-card-full-backup",
|
||||
"export-hero-doctor-report",
|
||||
]) {
|
||||
await expect(page.getByTestId(id)).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
});
|
||||
|
||||
test("included-data checklist is collapsed by default and expands on demand", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/settings/export", { waitUntil: "domcontentloaded" });
|
||||
const toggle = page.getByTestId("health-record-included-data-toggle");
|
||||
await expect(toggle).toBeVisible({ timeout: 10_000 });
|
||||
// Collapsed on first render — the checklist panel is absent.
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(
|
||||
page.getByTestId("health-record-included-data-panel"),
|
||||
).toHaveCount(0);
|
||||
// Expands on demand.
|
||||
await toggle.click();
|
||||
await expect(toggle).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(
|
||||
page.getByTestId("health-record-included-data-panel"),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("Measurements CSV download fires a real download event", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* v1.4.37 W7a — Arztbericht hero card contract.
|
||||
* Arztbericht (doctor-report) card contract.
|
||||
*
|
||||
* The flagship doctor-report PDF surface was promoted from one of five
|
||||
* grid cards into a hero card at the top of `/settings/export`. This
|
||||
* suite pins the SSR contract:
|
||||
* v1.4.37 W7a promoted this surface to the page hero; v1.12 demotes it
|
||||
* back to a small secondary card at the bottom of `/settings/export`
|
||||
* (the health-record export now owns the hero). The card stays fully
|
||||
* functional — only the framing and visual weight changed. This suite
|
||||
* pins the SSR contract:
|
||||
*
|
||||
* 1. The hero renders with the expected `data-testid` slot.
|
||||
* 1. The card renders with the expected `data-testid` slot.
|
||||
* 2. The value statement is present (Marc-Voice copy, no AI mention).
|
||||
* 3. The CTA button is present, clears the 44 px touch floor, and
|
||||
* carries an `aria-describedby` link to the value statement.
|
||||
@@ -37,7 +39,7 @@ function renderSSR(node: React.ReactElement, locale: "en" | "de" = "en") {
|
||||
}
|
||||
|
||||
describe("<ArztberichtHeroCard> — SSR contract", () => {
|
||||
it("renders the hero slot with eyebrow + title", () => {
|
||||
it("renders the card slot with eyebrow + title", () => {
|
||||
const html = renderSSR(<ArztberichtHeroCard />);
|
||||
expect(html).toContain('data-testid="export-hero-doctor-report"');
|
||||
// Title falls back to the existing doctorReport.title key — keeps
|
||||
@@ -59,10 +61,10 @@ describe("<ArztberichtHeroCard> — SSR contract", () => {
|
||||
it("renders the CTA with min-h-11 touch target + aria-describedby", () => {
|
||||
const html = renderSSR(<ArztberichtHeroCard />);
|
||||
expect(html).toContain('data-testid="export-hero-doctor-report-action"');
|
||||
// 44 px touch floor (WCAG 2.5.5) — the hero CTA pins `h-11` on
|
||||
// mobile and falls back to the compact `sm:h-10` on pointer
|
||||
// 44 px touch floor (WCAG 2.5.5) — the demoted CTA keeps `min-h-11`
|
||||
// on mobile and falls back to the compact `sm:min-h-9` on pointer
|
||||
// devices.
|
||||
expect(html).toContain("h-11");
|
||||
expect(html).toContain("min-h-11");
|
||||
// Aria-describedby links the CTA to the value statement so screen
|
||||
// readers announce the framing alongside the action label.
|
||||
expect(html).toMatch(/aria-describedby="[^"]+"/);
|
||||
@@ -84,12 +86,15 @@ describe("<ArztberichtHeroCard> — SSR contract", () => {
|
||||
expect(html).not.toContain("settings.sections.export.hero");
|
||||
});
|
||||
|
||||
it("uses the hero-gradient + glow-purple visual treatment", () => {
|
||||
it("renders as a plain secondary card, not a hero", () => {
|
||||
const html = renderSSR(<ArztberichtHeroCard />);
|
||||
// Same surface utilities as the Insights hero strip — keeps the
|
||||
// app's hero language consistent.
|
||||
expect(html).toContain("hero-gradient");
|
||||
expect(html).toContain("glow-purple");
|
||||
// v1.12 demoted the doctor-report from the page hero — the
|
||||
// health-record export now owns the hero treatment, so this card
|
||||
// must NOT carry the hero gradient/glow utilities.
|
||||
expect(html).not.toContain("hero-gradient");
|
||||
expect(html).not.toContain("glow-purple");
|
||||
// It renders with the standard secondary-card surface.
|
||||
expect(html).toContain("bg-card");
|
||||
});
|
||||
|
||||
it("does not paint dialog markup in the closed-state SSR pass", () => {
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
/**
|
||||
* v1.4.16 Phase B7 — Settings → Export section.
|
||||
*
|
||||
* The acceptance criterion for the consolidated Export menu was "five
|
||||
* cards":
|
||||
* 1. Doctor Report (PDF) — promoted to the hero in v1.4.37 W7a;
|
||||
* now lives at the top of the page, NOT in the grid
|
||||
* 2. Measurements CSV
|
||||
* 3. Medications CSV (with optional intake-history toggle)
|
||||
* 4. Mood CSV
|
||||
* 5. Full JSON Backup
|
||||
* The consolidated Export page surfaces:
|
||||
* - Health Record export panel (the page hero, v1.12)
|
||||
* - Measurements CSV
|
||||
* - Medications CSV (with optional intake-history toggle)
|
||||
* - Mood CSV
|
||||
* - Full JSON Backup
|
||||
* - Doctor Report (PDF) — small secondary card at the bottom (v1.12)
|
||||
*
|
||||
* Each card must surface a title, a 1-line description, and a
|
||||
* download/generate button. SSR-only smoke test — interaction is
|
||||
* exercised by the e2e suite.
|
||||
*
|
||||
* v1.4.37 W7a — the doctor-report card was extracted into
|
||||
* `<ArztberichtHeroCard>` and rendered above the grid. The remaining
|
||||
* four cards stay under a "Weitere Export-Optionen" / "Other export
|
||||
* options" sub-heading. The hero owns its own contract test; this
|
||||
* suite pins the page-level shape.
|
||||
* v1.12 — the health-record export takes the page hero; the
|
||||
* doctor-report card (`<ArztberichtHeroCard>`) is demoted to a small
|
||||
* secondary card at the bottom of the page. The four CSV/JSON tiles
|
||||
* stay under a "Weitere Export-Optionen" / "Other export options"
|
||||
* sub-heading. Each component owns its own contract test; this suite
|
||||
* pins the page-level shape.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
@@ -60,11 +60,19 @@ describe("<ExportSection> — SSR smoke", () => {
|
||||
expect(html).not.toContain("settings.sections.export.");
|
||||
});
|
||||
|
||||
it("renders the Arztbericht hero card at the top of the page", () => {
|
||||
it("mounts the health-record export panel as the page hero", () => {
|
||||
const html = render(<ExportSection />);
|
||||
// The hero owns its own contract suite; here we pin that the page
|
||||
// v1.12 — the health-record export owns the hero treatment.
|
||||
expect(html).toContain('data-testid="health-record-export-panel"');
|
||||
expect(html).toContain("hero-gradient");
|
||||
expect(html).toContain("glow-purple");
|
||||
});
|
||||
|
||||
it("mounts the demoted doctor-report card", () => {
|
||||
const html = render(<ExportSection />);
|
||||
// The card owns its own contract suite; here we pin that the page
|
||||
// mounts it, and that the legacy in-grid doctor-report card is
|
||||
// gone (v1.4.37 W7a moved it into the hero).
|
||||
// gone. The card no longer carries the hero gradient/glow (v1.12).
|
||||
expect(html).toContain('data-testid="export-hero-doctor-report"');
|
||||
expect(html).not.toContain('data-testid="export-card-doctor-report"');
|
||||
});
|
||||
|
||||
@@ -1,46 +1,30 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* v1.4.37 W7a — Settings → Export Arztbericht hero card.
|
||||
* Settings → Export Arztbericht (doctor-report) card.
|
||||
*
|
||||
* The doctor-report PDF is HealthLog's flagship export — the artefact a
|
||||
* user actually carries into a doctor's appointment. The v1.4.16 phase
|
||||
* B7 layout treated it as one of five equal cards in the grid, which
|
||||
* undersold the feature. Marc 2026-05-17:
|
||||
* The doctor-report PDF is the artefact a user carries into a doctor's
|
||||
* appointment. v1.4.37 W7a promoted it to the page hero; v1.12 demotes
|
||||
* it back to a small secondary card at the bottom of the export page —
|
||||
* the health-record export now owns the page hero. The card is fully
|
||||
* functional: the CTA routes through the existing `<DoctorReportDialog>`
|
||||
* + `/api/doctor-report` flow exactly as before. Only the framing and
|
||||
* visual weight changed.
|
||||
*
|
||||
* "Können wir unter Einstellungen > Export den Arztbericht als Hero
|
||||
* Card machen?"
|
||||
* Layout (compact card, no hero gradient):
|
||||
*
|
||||
* This component lifts the doctor-report card out of the grid and
|
||||
* paints it with the same `hero-gradient + glow-purple` visual treatment
|
||||
* as the Insights hero strip (`src/components/insights/hero-strip.tsx`),
|
||||
* so the surface feels consistent with the rest of the app. The CTA
|
||||
* routes through the existing `<DoctorReportDialog>` + `/api/doctor-
|
||||
* report` flow — only the framing changed.
|
||||
*
|
||||
* Layout:
|
||||
*
|
||||
* ┌─ hero ────────────────────────────────────────────┐
|
||||
* │ [icon] eyebrow │
|
||||
* │ H2 title (settings.sections.export.cards. │
|
||||
* │ doctorReport.title) │
|
||||
* ┌─ card ────────────────────────────────────────────┐
|
||||
* │ [icon] H2 title eyebrow │
|
||||
* │ one-line value statement │
|
||||
* │ ───── │
|
||||
* │ [Configure & generate] PDF · printable │
|
||||
* └───────────────────────────────────────────────────┘
|
||||
*
|
||||
* Mobile-first: at < sm the CTA stacks below the value statement (the
|
||||
* action row is `flex-wrap`), the hero pads down to `px-4 py-5`, and
|
||||
* the title clamps to a single line.
|
||||
*
|
||||
* a11y:
|
||||
* - The CTA button clears the 44 px touch floor (`min-h-11`).
|
||||
* - `focus-visible:ring` ships via the default `<Button>` variant.
|
||||
* - The CTA is `aria-describedby={valueStatementId}` so a screen
|
||||
* reader announces the value statement alongside the action.
|
||||
* - The hero's `<h2>` slots beneath the page-level `<h1>` so the
|
||||
* outline reads h1 (Export) → h2 (Arztbericht hero) → h2
|
||||
* (Weitere Export-Optionen) → h2 (each remaining card).
|
||||
* - The card's `<h2>` slots beneath the page-level `<h1>`.
|
||||
*/
|
||||
|
||||
import { useEffect, useId, useState } from "react";
|
||||
@@ -52,7 +36,6 @@ import {
|
||||
type DoctorReportSubmitPayload,
|
||||
} from "@/components/doctor-report/doctor-report-dialog";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function ArztberichtHeroCard() {
|
||||
const { t, locale } = useTranslations();
|
||||
@@ -122,52 +105,47 @@ export function ArztberichtHeroCard() {
|
||||
<section
|
||||
data-testid="export-hero-doctor-report"
|
||||
aria-labelledby="export-hero-doctor-report-title"
|
||||
className={cn(
|
||||
"hero-gradient glow-purple animate-insight-in",
|
||||
// `isolate` traps the purple glow inside the hero so the
|
||||
// shadow doesn't leak through the cards below — same trick
|
||||
// the Insights `<HeroStrip>` uses.
|
||||
"relative isolate overflow-hidden rounded-xl px-4 py-5 sm:px-6 sm:py-6",
|
||||
)}
|
||||
className="bg-card border-border rounded-xl border p-5 sm:p-6"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Stethoscope
|
||||
className="text-dracula-purple h-5 w-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Stethoscope
|
||||
className="text-muted-foreground h-5 w-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h2
|
||||
id="export-hero-doctor-report-title"
|
||||
className="text-base font-semibold tracking-tight"
|
||||
>
|
||||
{t("settings.sections.export.cards.doctorReport.title")}
|
||||
</h2>
|
||||
</div>
|
||||
<span className="text-muted-foreground text-[11px] font-medium tracking-wide uppercase">
|
||||
{t("settings.sections.export.hero.eyebrow")}
|
||||
</span>
|
||||
</div>
|
||||
<h2
|
||||
id="export-hero-doctor-report-title"
|
||||
className="text-2xl leading-tight font-semibold tracking-tight sm:text-[28px]"
|
||||
>
|
||||
{t("settings.sections.export.cards.doctorReport.title")}
|
||||
</h2>
|
||||
<p
|
||||
id={valueStatementId}
|
||||
data-testid="export-hero-doctor-report-value"
|
||||
className="text-muted-foreground max-w-2xl text-sm leading-relaxed"
|
||||
className="text-muted-foreground max-w-2xl text-xs leading-relaxed"
|
||||
>
|
||||
{t("settings.sections.export.hero.valueStatement")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 border-t border-border/50 pt-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button
|
||||
data-testid="export-hero-doctor-report-action"
|
||||
variant="default"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
// `min-h-11` (44 px) is the WCAG 2.5.5 touch-target floor;
|
||||
// we override the default `<Button>` height on mobile and
|
||||
// fall back to the compact `sm:h-10` on pointer devices.
|
||||
// v1.4.38 W-D P3-2 — pair the explicit `h-11` with a
|
||||
// `min-h-11` floor (and a `sm:min-h-9` desktop floor) so a
|
||||
// future className override can't accidentally re-lift the
|
||||
// mobile minimum below the touch-target contract.
|
||||
className="h-11 min-h-11 px-5 text-sm font-medium sm:h-10 sm:min-h-9"
|
||||
// we keep it on mobile and fall back to the compact
|
||||
// `sm:min-h-9` on pointer devices so a future className
|
||||
// override can't re-lift the mobile minimum below the
|
||||
// touch-target contract.
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => setOpen(true)}
|
||||
disabled={busy}
|
||||
aria-describedby={valueStatementId}
|
||||
|
||||
@@ -9,18 +9,20 @@
|
||||
* per export type, each with its own filter inputs and a clear
|
||||
* "Download" / "Generate" button.
|
||||
*
|
||||
* 1. Doctor Report (PDF) — hero card at the top of the page
|
||||
* (v1.4.37 W7a, see
|
||||
* `<ArztberichtHeroCard>`)
|
||||
* 1. Health Record export — hero panel at the top of the page
|
||||
* (PDF + FHIR R4 + zip package, see
|
||||
* `<HealthRecordExportPanel>`)
|
||||
* 2. Measurements CSV — optional `since`/`until`
|
||||
* 3. Medications CSV — optional intake-history toggle
|
||||
* 4. Mood CSV — optional `since`/`until`
|
||||
* 5. Full JSON Backup — single-file user-scoped dump
|
||||
* 6. Doctor Report (PDF) — small secondary card at the bottom
|
||||
* (`<ArztberichtHeroCard>`)
|
||||
*
|
||||
* v1.4.37 W7a — the doctor-report card was promoted to the page hero so
|
||||
* the flagship "PDF for the doctor" flow no longer competes for visual
|
||||
* weight with the secondary CSV/JSON exports. The remaining four cards
|
||||
* live under a "Weitere Export-Optionen" sub-heading.
|
||||
* v1.12 — the health-record export is the headline "data out" path; it
|
||||
* takes the page hero. The doctor-report PDF is demoted to a small
|
||||
* secondary card at the bottom of the page — fully functional, just no
|
||||
* longer the flagship surface it was in v1.4.37 W7a.
|
||||
*
|
||||
* Mobile-first: cards stack on `<md`, two-column grid on `>=md`.
|
||||
*/
|
||||
@@ -86,20 +88,14 @@ export function ExportSection() {
|
||||
</p>
|
||||
</header>
|
||||
|
||||
{/* v1.4.37 W7a — Arztbericht hero. The doctor-report PDF is the
|
||||
flagship "data out" path; the previous grid layout buried it
|
||||
alongside the four CSV/JSON exports. The hero now sits at the
|
||||
top of the page; the same generation flow runs underneath. */}
|
||||
<ArztberichtHeroCard />
|
||||
|
||||
{/* v1.7.0 — full health-record export. PDF + FHIR R4 + a combined
|
||||
zip package, with grouped data-section toggles and a format
|
||||
switch. Sits above the secondary CSV/JSON cards. */}
|
||||
{/* v1.12 — full health-record export is now the page hero. PDF +
|
||||
FHIR R4 + a combined zip package, with grouped data-section
|
||||
toggles (collapsed by default) and a format switch. This is the
|
||||
headline "data out" path; it carries the primary visual weight. */}
|
||||
<HealthRecordExportPanel />
|
||||
|
||||
{/* v1.4.37 W7a — secondary export options. The doctor-report
|
||||
card moved into the hero above, so this group renders the
|
||||
four remaining destinations under a dedicated sub-heading.
|
||||
{/* v1.12 — secondary export options. The four CSV/JSON
|
||||
destinations live under a dedicated sub-heading.
|
||||
Mobile: single column. Desktop (md+): 2-column grid. */}
|
||||
<section
|
||||
aria-labelledby="settings-section-export-other-title"
|
||||
@@ -118,6 +114,11 @@ export function ExportSection() {
|
||||
<FullBackupCard />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* v1.12 — doctor-report PDF, demoted from the former page hero to a
|
||||
small secondary card at the bottom. Fully functional — the same
|
||||
`<DoctorReportDialog>` + `/api/doctor-report` flow runs from here. */}
|
||||
<ArztberichtHeroCard />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,10 +12,17 @@
|
||||
* Mood stays default-OFF everywhere (privacy). PDF-only fields (practice
|
||||
* name, charts) hide when the format is FHIR. The ePA-compat note shows
|
||||
* for the FHIR + package formats.
|
||||
*
|
||||
* v1.12 — this is the export page's hero. It carries the same
|
||||
* `hero-gradient + glow-purple` treatment as the Insights hero strip so
|
||||
* the headline "data out" path owns the primary visual weight. The
|
||||
* grouped data-section toggles live behind a collapsible disclosure,
|
||||
* collapsed by default, so the panel opens compact instead of as a long
|
||||
* always-expanded checklist.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Download, FileText, Loader2 } from "lucide-react";
|
||||
import { useId, useState } from "react";
|
||||
import { ChevronDown, Download, FileText, Loader2 } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -24,6 +31,7 @@ import { NativeSelect } from "@/components/ui/native-select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useRovingRadioGroup } from "@/hooks/use-roving-radio-group";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ExportFormat = "pdf" | "fhir" | "package";
|
||||
|
||||
@@ -106,6 +114,10 @@ export function HealthRecordExportPanel() {
|
||||
const [sections, setSections] = useState<SectionState>(DEFAULT_SECTIONS);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// The included-data checklist is a disclosure, collapsed by default —
|
||||
// the panel opens compact instead of as a long always-expanded list.
|
||||
const [includedDataOpen, setIncludedDataOpen] = useState(false);
|
||||
const includedDataPanelId = useId();
|
||||
|
||||
const isPdfLike = format === "pdf" || format === "package";
|
||||
const isFhirLike = format === "fhir" || format === "package";
|
||||
@@ -179,15 +191,27 @@ export function HealthRecordExportPanel() {
|
||||
<section
|
||||
aria-labelledby="health-record-export-title"
|
||||
data-testid="health-record-export-panel"
|
||||
className="bg-card border-border rounded-xl border p-6"
|
||||
className={cn(
|
||||
"hero-gradient glow-purple animate-insight-in",
|
||||
// `isolate` traps the purple glow inside the hero so the shadow
|
||||
// doesn't leak through the cards below — same trick the Insights
|
||||
// `<HeroStrip>` uses.
|
||||
"relative isolate overflow-hidden rounded-xl p-5 sm:p-6",
|
||||
)}
|
||||
>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<FileText className="text-primary h-5 w-5" aria-hidden="true" />
|
||||
<h2 id="health-record-export-title" className="text-lg font-semibold">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<FileText
|
||||
className="text-dracula-purple h-5 w-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h2
|
||||
id="health-record-export-title"
|
||||
className="text-xl font-semibold tracking-tight sm:text-2xl"
|
||||
>
|
||||
{t("settings.healthRecord.title")}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-muted-foreground mb-4 text-sm">
|
||||
<p className="text-muted-foreground mb-5 max-w-2xl text-sm leading-relaxed">
|
||||
{t("settings.healthRecord.description")}
|
||||
</p>
|
||||
|
||||
@@ -245,12 +269,39 @@ export function HealthRecordExportPanel() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section groups */}
|
||||
{/* Section groups — collapsible, collapsed by default so the
|
||||
panel opens compact. The disclosure follows the inline
|
||||
aria-expanded/aria-controls pattern used elsewhere in the app
|
||||
(e.g. the insights recommendation cards). */}
|
||||
<fieldset className="space-y-3">
|
||||
<legend className="text-sm font-medium">
|
||||
<legend className="sr-only">
|
||||
{t("settings.healthRecord.includedData")}
|
||||
</legend>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="health-record-included-data-toggle"
|
||||
aria-expanded={includedDataOpen}
|
||||
aria-controls={includedDataPanelId}
|
||||
onClick={() => setIncludedDataOpen((v) => !v)}
|
||||
className="text-foreground hover:bg-muted/40 focus-visible:ring-ring/50 flex min-h-11 w-full items-center justify-between gap-3 rounded-lg px-1 py-1 text-left text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
||||
>
|
||||
<span>{t("settings.healthRecord.includedData")}</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"text-muted-foreground h-4 w-4 shrink-0 transition-transform",
|
||||
includedDataOpen && "rotate-180",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{includedDataOpen && (
|
||||
<div
|
||||
id={includedDataPanelId}
|
||||
data-testid="health-record-included-data-panel"
|
||||
className="animate-insight-in space-y-3"
|
||||
style={{ animationDuration: "200ms" }}
|
||||
>
|
||||
<div className="border-border space-y-2 rounded-lg border p-3">
|
||||
<p className="text-xs font-semibold">
|
||||
{t("settings.healthRecord.groupVitals")}
|
||||
@@ -372,6 +423,8 @@ export function HealthRecordExportPanel() {
|
||||
onToggle={() => setIncludeAiSummary((v) => !v)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
{isFhirLike && (
|
||||
|
||||
Reference in new issue
Block a user