From 6d83115d1632ffecb196015bee3d3005e6d55189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 16 Jul 2026 03:24:16 +0200 Subject: [PATCH] =?UTF-8?q?chore(release):=20v1.28.43=20=E2=80=94=20visual?= =?UTF-8?q?=20consistency=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified UI-consistency findings from the campaign's design audit, each pixel-faithful except the intended correction: - insights hero band: off-scale py-5/gap-5 → the standard scale; H1 font-semibold → font-bold; sm:text-[28px] → sm:text-3xl. - medication-compliance-chart + recent-achievements-card: hand-rolled muted h-4 icon + text-sm headers → the standard TileHeader primitive so they match sibling tiles. - achievements stat tiles + clinician-view shells: p-5 → p-4 md:p-6. - two text-primary header glyphs (research-mode dialog, enroll-mfa) → text-foreground (never accent on a header glyph); the deliberate badge-boxed icons left untouched. - SheetHeader now reserves the trailing close-X gutter like DialogHeader, closing the latent overlap class for future raw-Sheet consumers. - five raw Dialogs on phone-reachable surfaces → ResponsiveSheet. - new bg-card-scoped healthlog/spacing-scale lint (flags p-5/gap-5 etc. only on card shells; list-marker insets + half-steps excluded) at error, with the five offenders it flagged swept. - dropped the stale CLAUDE.md '18 PascalCase outliers' line (0 remain). Every data-slot preserved. No migration. --- CHANGELOG.md | 15 + CLAUDE.md | 2 +- docs/api/openapi.yaml | 2 +- .../__tests__/spacing-scale.test.mjs | 55 ++ eslint-plugins/healthlog/index.js | 3 +- eslint-plugins/healthlog/spacing-scale.js | 109 +++- eslint.config.mjs | 11 +- package.json | 2 +- public/sw.js | 2 +- src/app/achievements/page.tsx | 6 +- src/app/enroll-mfa/page.tsx | 5 +- .../medication-chart-polish.test.tsx | 18 + .../charts/medication-compliance-chart.tsx | 7 +- src/components/clinician/clinician-view.tsx | 4 +- .../recent-achievements-card.test.tsx | 12 + .../gamification/recent-achievements-card.tsx | 35 +- .../insights/__tests__/hero-strip.test.tsx | 21 + src/components/insights/hero-strip.tsx | 8 +- .../research-mode-acknowledgment-dialog.tsx | 2 +- .../scheduling/natural-language-extractor.tsx | 171 +++-- .../sections/inventory-section.tsx | 586 +++++++++--------- src/components/onboarding/done-screen.tsx | 2 +- .../settings/account-section/index.tsx | 144 ++--- .../ui/__tests__/sheet-header.test.tsx | 30 + src/components/ui/sheet.tsx | 9 +- 25 files changed, 717 insertions(+), 544 deletions(-) create mode 100644 src/components/ui/__tests__/sheet-header.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 79df444fe..34cc6ad81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ ## [Unreleased] +## [1.28.43] — 2026-07-16 — Visual consistency polish + +A pass over small visual inconsistencies the design audit surfaced: + +- The insights header now uses the standard heading weight and spacing scale. +- Two dashboard tiles that hand-rolled a dimmer header now use the same tile + header as their neighbours, so the cards read as one family. +- A few tiles sat a step denser than their siblings; they now match. +- Two header icons that used the accent colour are back to the standard. +- Several edit dialogs on phone-reachable surfaces now open as the same bottom + sheet the rest of the app uses. + +A new lint rule keeps card spacing on the standard scale so this drift can't +quietly return. + ## [1.28.42] — 2026-07-16 — Snappier lists, correct mood refresh, safer first sign-up Performance and correctness fixes: diff --git a/CLAUDE.md b/CLAUDE.md index 65047b225..bb69ca078 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,7 +95,7 @@ The architecture map in `.planning/codebase/arch.md` walks each layer with file: - **`annotate()` on every interesting code path.** Wide-event observability. Action names follow `..` (`coach.budget.exceeded`, `measurement.batch.ingest`). Don't free-text in `meta` — pin the shape so dashboards stay stable. - **i18n keys exist for every `t()` call.** `i18n-call-site-coverage.test.ts` walks every `.ts(x)` under `src/`, extracts every `t("ns.key")` literal, and asserts each key resolves in `messages/en.json`. `i18n-locale-integrity.test.ts` propagates the EN guarantee across `de / en / es / fr / it / pl`. If a guard fails, fix the bundle, don't suppress the test. - **OpenAPI registry stays in sync.** Zod schemas carry `.meta()` annotations under `src/lib/openapi/`; `pnpm openapi:generate` emits `docs/api/openapi.yaml`. CI fails on drift. Re-run the generator after touching a request / response schema and commit the YAML alongside the Zod change. -- **File naming.** Components and lib files: kebab-case (`daily-briefing.tsx`, `secure-cookie.ts`). Hooks: `use-` prefix kebab-case (`use-coach-prefs.ts`). React component exports stay PascalCase regardless of filename. The 18 PascalCase outliers under `src/components/medications/` + `src/components/onboarding/` are pre-existing drift and worth cleaning up when the surrounding files come up for edit. +- **File naming.** Components and lib files: kebab-case (`daily-briefing.tsx`, `secure-cookie.ts`). Hooks: `use-` prefix kebab-case (`use-coach-prefs.ts`). React component exports stay PascalCase regardless of filename. ## Tests and commands diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 4368c1f8d..01dce3035 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.28.42 + version: 1.28.43 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/eslint-plugins/healthlog/__tests__/spacing-scale.test.mjs b/eslint-plugins/healthlog/__tests__/spacing-scale.test.mjs index 6bb22a55c..5c242ecbe 100644 --- a/eslint-plugins/healthlog/__tests__/spacing-scale.test.mjs +++ b/eslint-plugins/healthlog/__tests__/spacing-scale.test.mjs @@ -60,6 +60,32 @@ ruleTester.run("spacing-scale", rule, { code: '', filename: "/repo/src/components/__tests__/example.test.tsx", }, + // The card-shell `5` check is scoped to the bg-card+border pair, so the + // justified list-marker inset never trips it (no shell). + { + code: '
    ', + filename: APP_FILE, + }, + // A directional inset ON a shell is out of scope (pl-5/pr-5 not banned). + { + code: '
    ', + filename: APP_FILE, + }, + // Form-body rhythm without a shell surface is fine. + { + code: '
    ', + filename: APP_FILE, + }, + // A shell already on-scale is fine. + { + code: '
    ', + filename: APP_FILE, + }, + // bg-card without a border is not treated as a shell. + { + code: '
    ', + filename: APP_FILE, + }, ], invalid: [ { @@ -108,5 +134,34 @@ ruleTester.run("spacing-scale", rule, { filename: APP_FILE, errors: [{ messageId: "slotPadding" }], }, + // Card-shell `5` step: bg-card + border + p-5 on one element. + { + code: '
    ', + filename: APP_FILE, + errors: [{ messageId: "cardShellStep5" }], + }, + // …py-5, …space-y-5, …gap-5 on a shell all trip it. + { + code: '
    ', + filename: APP_FILE, + errors: [{ messageId: "cardShellStep5" }], + }, + { + code: '
    ', + filename: APP_FILE, + errors: [{ messageId: "cardShellStep5" }], + }, + // Split across cn() args — the join still sees the shell + the step-5. + { + code: '
    ', + filename: APP_FILE, + errors: [{ messageId: "cardShellStep5" }], + }, + // Modifier-prefixed step on a shell. + { + code: '
    ', + filename: APP_FILE, + errors: [{ messageId: "cardShellStep5" }], + }, ], }); diff --git a/eslint-plugins/healthlog/index.js b/eslint-plugins/healthlog/index.js index ca06edf90..d1529a8bc 100644 --- a/eslint-plugins/healthlog/index.js +++ b/eslint-plugins/healthlog/index.js @@ -6,7 +6,8 @@ * - `safe-fetch-required` — outbound fetch must route through safeFetch. * - `api-fetch-required` — client /api/ calls must route through apiFetch. * - `no-raw-palette-color` — ban raw Tailwind palette utilities in app UI. - * - `spacing-scale` — no pt-/pb- overrides on gap-based Card slots. + * - `spacing-scale` — no pt-/pb- overrides on gap-based Card slots, + * and no off-scale `5` step on a bg-card shell. */ "use strict"; diff --git a/eslint-plugins/healthlog/spacing-scale.js b/eslint-plugins/healthlog/spacing-scale.js index 0cf06c9f8..8fa8bcad4 100644 --- a/eslint-plugins/healthlog/spacing-scale.js +++ b/eslint-plugins/healthlog/spacing-scale.js @@ -1,34 +1,47 @@ /** - * @fileoverview ESLint rule — no `pt-*` / `pb-*` overrides on the - * gap-based Card slots. + * @fileoverview ESLint rule — spacing-scale discipline. Two checks: * - * The Card primitive is gap-based, not padding-based - * (`src/components/ui/card.tsx`): the header→body distance comes from - * the PARENT flex gap, never from slot padding. A `pb-*` on - * `` or a `pt-*` on `` therefore adds on top - * of the gap (or is a no-op) and inverts its own intent — the drift - * class the sl-001 sweep cleaned (~35 stale overrides). Density is - * expressed on the Card itself (``, - * the sanctioned compact tier), never on a slot. + * 1. `slotPadding` — no `pt-*` / `pb-*` overrides on the gap-based Card + * slots. The Card primitive is gap-based, not padding-based + * (`src/components/ui/card.tsx`): the header→body distance comes from + * the PARENT flex gap, never from slot padding. A `pb-*` on + * `` or a `pt-*` on `` therefore adds on top + * of the gap (or is a no-op) and inverts its own intent — the drift + * class the sl-001 sweep cleaned (~35 stale overrides). Density is + * expressed on the Card itself (``, + * the sanctioned compact tier), never on a slot. * - * Detection: JSX elements named `CardHeader` / `CardContent` (any - * member-expression tail, so `UI.CardHeader` matches too) whose - * `className` attribute carries a static `pt-` / `pb-` fragment — - * including modifier-prefixed (`md:pb-2`) and arbitrary-value - * (`pb-[10px]`) forms. Strings are collected from the whole attribute - * subtree, so `cn("pb-2", …)`, ternaries, and template literals are all - * seen. Non-numeric suffixes (`pb-safe`) never match. + * Detection: JSX elements named `CardHeader` / `CardContent` (any + * member-expression tail, so `UI.CardHeader` matches too) whose + * `className` carries a static `pt-` / `pb-` fragment — including + * modifier-prefixed (`md:pb-2`) and arbitrary-value (`pb-[10px]`) forms. + * Non-numeric suffixes (`pb-safe`) never match. + * + * 2. `cardShellStep5` — no off-scale `5` step (20 px, banned by §2) on a + * hand-rolled card SHELL. The shell heuristic (§7.2) keys off the + * `bg-card` + `border` pair on ONE element: a `p-5` / `px-5` / `py-5` / + * `space-y-5` / `gap-5` there sits denser than every sibling `` + * (which is `p-4 md:p-6`), and the 4 px step reads next to a conformant + * card on the same viewport. Deliberately NARROW: it fires only when the + * same element paints a card surface, so justified `pl-5`/`pl-7` + * list-marker insets (no `bg-card`) and form-body `space-y-5` rhythm (no + * shell) never trip it. Hand-rolled shells sweep to `p-4 md:p-6` + * (or `p-3` for a dense inner tile); new surfaces compose ``. + * + * Both checks collect strings from the whole `className` subtree, so + * `cn("…", cond && "…")`, ternaries, and template literals are all seen. * * Scope: `src/components/**` + `src/app/**`; test files exempt — same - * gating as the sibling rules. `src/components/ui/card.tsx` itself - * composes raw `
    `s and is untouched by construction. + * gating as the sibling rules. `src/components/ui/*` primitives compose the + * shells themselves and are untouched by construction (they carry no + * `bg-card` + `border` + step-5 combination). * * There is deliberately NO allowlist: no current site has a documented - * load-bearing reason for a slot padding override. If one ever appears, - * it carries an inline `eslint-disable-next-line` with the reason — - * visible in review — rather than a silent registry entry here. + * load-bearing reason. If one ever appears it carries an inline + * `eslint-disable-next-line` with the reason — visible in review — rather + * than a silent registry entry here. * - * @see .planning/audits/2026-07-05-fable/UI-STANDARDS.md §1 + §2. + * @see .planning/audits/2026-07-05-fable/UI-STANDARDS.md §1 + §2 + §7. */ "use strict"; @@ -44,6 +57,19 @@ const SLOT_PADDING_RE = const SLOT_NAMES = new Set(["CardHeader", "CardContent"]); +// Card-shell heuristic: the element paints a card surface (`bg-card`) AND a +// border. `\bborder\b` matches `border`, `border-border`, `border-b`, +// `border-dashed` — any border utility qualifies as a shell. +const SHELL_BG_RE = /\bbg-card\b/; +const SHELL_BORDER_RE = /\bborder(?:-|\b)/; + +// The banned `5` step (20 px) on a shell — the subset §7.2 scopes to an +// error-clean rule: p-5 / px-5 / py-5 / space-y-5 / gap-5, with optional +// modifier prefixes (`sm:p-5`). Directional insets (`pl-5`/`pr-5`) and +// half-steps are intentionally OUT of scope (see the header). +const SHELL_STEP5_RE = + /(?:^|\s)(?:[^\s"']*:)*((?:p|px|py|space-y|gap)-5)(?=\s|$)/; + function toPosix(filename) { return filename.replace(/\\/g, "/"); } @@ -122,12 +148,14 @@ const spacingScaleRule = { type: "problem", docs: { description: - "Ban pt-*/pb-* overrides on CardHeader/CardContent — the Card is gap-based; density is expressed via the Card's own gap/py, never via slot padding.", + "Spacing-scale discipline — no pt-*/pb-* overrides on gap-based Card slots, and no off-scale `5` step on a bg-card+border shell.", }, schema: [], messages: { slotPadding: '"{{match}}" on <{{element}}> fights the gap-based Card contract — the header→body distance comes from the parent flex gap, so slot padding stacks on top of it (or is a no-op). Tune density on the Card itself instead: .', + cardShellStep5: + 'Off-scale "{{match}}" (20 px) on a bg-card+border shell — the `5` step is banned (UI-STANDARDS §2) and reads denser than every sibling (p-4 md:p-6). Sweep the shell to `p-4 md:p-6` (or `p-3` for a dense inner tile); new surfaces should compose .', }, }, create(context) { @@ -138,9 +166,6 @@ const spacingScaleRule = { return { JSXOpeningElement(node) { - const name = elementName(node); - if (!name || !SLOT_NAMES.has(name)) return; - const classNameAttr = node.attributes.find( (attr) => attr.type === "JSXAttribute" && @@ -151,13 +176,33 @@ const spacingScaleRule = { const strings = []; collectStrings(classNameAttr.value, strings); - for (const { node: strNode, text } of strings) { - const m = SLOT_PADDING_RE.exec(text); + + // Check 1 — pt-/pb- slot padding on the gap-based Card slots. + const name = elementName(node); + if (name && SLOT_NAMES.has(name)) { + for (const { node: strNode, text } of strings) { + const m = SLOT_PADDING_RE.exec(text); + if (m) { + context.report({ + node: strNode, + messageId: "slotPadding", + data: { match: m[1], element: name }, + }); + } + } + } + + // Check 2 — off-scale `5` step on a card shell. Join every collected + // string so the `bg-card` + `border` pair and the step-5 utility are + // seen together even when split across cn() args, then report once. + const joined = strings.map((s) => s.text).join(" "); + if (SHELL_BG_RE.test(joined) && SHELL_BORDER_RE.test(joined)) { + const m = SHELL_STEP5_RE.exec(joined); if (m) { context.report({ - node: strNode, - messageId: "slotPadding", - data: { match: m[1], element: name }, + node: classNameAttr, + messageId: "cardShellStep5", + data: { match: m[1] }, }); } } diff --git a/eslint.config.mjs b/eslint.config.mjs index 992d0e885..0f6b64259 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -66,9 +66,14 @@ const eslintConfig = defineConfig([ // token references, not utilities, and stay legal — the light-theme // overrides in globals.css keep them AA on both themes. "healthlog/no-dracula-utility": ["error", { checks: ["dracula"] }], - // The Card primitive is gap-based; pt-/pb- on CardHeader/CardContent - // fights the gap and re-opens the sl-001 drift class. Density lives - // on the Card itself (`gap-2 py-3 md:py-4`). See the rule header. + // Spacing-scale discipline. Two error-clean checks: pt-/pb- on + // CardHeader/CardContent fights the gap-based Card contract (re-opens + // the sl-001 drift class), and an off-scale `5` step (p-5/py-5/space-y-5/ + // gap-5) on a bg-card+border shell reads denser than every sibling + // . The shell check is scoped to the bg-card+border pair so + // justified list-marker insets (pl-5/pl-7) and form-body rhythm never + // trip it. Density lives on the Card itself (`gap-2 py-3 md:py-4`) or on + // a swept `p-4 md:p-6` shell. See the rule header. "healthlog/spacing-scale": "error", }, }, diff --git a/package.json b/package.json index 01c82cfbc..8b1614fdc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.28.42", + "version": "1.28.43", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index d7253c1bb..b76eb843d 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.28.42"; + /* @sw-version-fallback */ "v1.28.43"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*` diff --git a/src/app/achievements/page.tsx b/src/app/achievements/page.tsx index 603726acd..656fd32e8 100644 --- a/src/app/achievements/page.tsx +++ b/src/app/achievements/page.tsx @@ -334,7 +334,7 @@ export default function AchievementsPage() { />
    -
    +

    {t("achievements.points")}

    @@ -354,7 +354,7 @@ export default function AchievementsPage() { aria-label={t("achievements.points")} />
    -
    +

    {t("achievements.unlocked")}

    @@ -368,7 +368,7 @@ export default function AchievementsPage() { {t("achievements.remainingUnlocks", { count: remainingUnlocks })}

    -
    +

    {t("achievements.nextGoal")}

    diff --git a/src/app/enroll-mfa/page.tsx b/src/app/enroll-mfa/page.tsx index 66c0ccb66..fdab86fb7 100644 --- a/src/app/enroll-mfa/page.tsx +++ b/src/app/enroll-mfa/page.tsx @@ -31,7 +31,10 @@ export default function EnrollMfaPage() {
    -

    diff --git a/src/components/charts/__tests__/medication-chart-polish.test.tsx b/src/components/charts/__tests__/medication-chart-polish.test.tsx index 3fdd48e49..e2b0d710c 100644 --- a/src/components/charts/__tests__/medication-chart-polish.test.tsx +++ b/src/components/charts/__tests__/medication-chart-polish.test.tsx @@ -49,4 +49,22 @@ describe(" v1.4.18 clean-line revert", () => { expect(html).not.toContain("chart-gradient-medication"); expect(html).not.toContain("linearGradient"); }); + + it("leads with the canonical TileHeader, not the old muted h-4 / text-sm row", async () => { + const { I18nProvider } = await import("@/lib/i18n/context"); + const { MedicationComplianceChart } = + await import("../medication-compliance-chart"); + + const html = renderToStaticMarkup( + + + , + ); + + // Adopts the shared tile-header primitive so the card matches its sibling + // HealthChart tiles (foreground h-5 glyph, text-base title). + expect(html).toContain('data-slot="tile-header"'); + // The old hand-rolled muted h-4 icon row is gone. + expect(html).not.toContain("text-muted-foreground h-4 w-4"); + }); }); diff --git a/src/components/charts/medication-compliance-chart.tsx b/src/components/charts/medication-compliance-chart.tsx index b3e5707e4..de819d182 100644 --- a/src/components/charts/medication-compliance-chart.tsx +++ b/src/components/charts/medication-compliance-chart.tsx @@ -47,6 +47,7 @@ import { } from "recharts"; import { ArrowDown, ArrowRight, ArrowUp, Pill } from "lucide-react"; import { ComplianceInfoTip } from "@/components/medications/card-parts/compliance-info-tip"; +import { TileHeader } from "@/components/insights/tile-header"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { useAuth } from "@/hooks/use-auth"; @@ -402,8 +403,10 @@ export function MedicationComplianceChart({ ≥sm: original side-by-side layout. */}

    - -

    {displayTitle}

    + {/* Canonical tile header (foreground h-5 icon, text-base title) so + this card reads in the same language as its sibling HealthChart + tiles instead of the old muted h-4 / text-sm row. */} + {/* `?` explainer for what the adherence percentage measures — shares the i18n body with the per-card bars. */} diff --git a/src/components/clinician/clinician-view.tsx b/src/components/clinician/clinician-view.tsx index 85b291cf7..cfc6fc467 100644 --- a/src/components/clinician/clinician-view.tsx +++ b/src/components/clinician/clinician-view.tsx @@ -106,7 +106,7 @@ function Section({ children: React.ReactNode; }) { return ( -
    +

    {title}

    {children}
    @@ -345,7 +345,7 @@ export function ClinicianView({ {/* ── Fenced wellness card (descriptive, NOT clinical) ────── */} {wellness.length > 0 ? ( -
    +

    {t("clinicianView.wellness.title")}

    diff --git a/src/components/gamification/__tests__/recent-achievements-card.test.tsx b/src/components/gamification/__tests__/recent-achievements-card.test.tsx index 6b5f98ec2..038f844c0 100644 --- a/src/components/gamification/__tests__/recent-achievements-card.test.tsx +++ b/src/components/gamification/__tests__/recent-achievements-card.test.tsx @@ -202,6 +202,18 @@ describe("", () => { expect(html).toContain("View all"); }); + it("routes the header through the canonical TileHeader (foreground icon, h2)", () => { + mockData = { summary: {}, achievements: [], metrics: {} }; + const html = render(); + // Adopts the shared tile-header primitive so it matches sibling tiles… + expect(html).toContain('data-slot="tile-header"'); + // …with the foreground (never muted) glyph and the h2 heading, not the + // old muted h-4 / text-sm row. + expect(html).toContain("Recent unlocks"); + expect(html).not.toMatch(/]*text-sm font-semibold/); + expect(html).not.toContain("text-muted-foreground h-4 w-4"); + }); + it("renders up to three most-recent unlocks with title + date", () => { mockData = { summary: {}, diff --git a/src/components/gamification/recent-achievements-card.tsx b/src/components/gamification/recent-achievements-card.tsx index 061af2df7..61321fcd3 100644 --- a/src/components/gamification/recent-achievements-card.tsx +++ b/src/components/gamification/recent-achievements-card.tsx @@ -25,6 +25,7 @@ import { formatDate } from "@/lib/format"; import { useAchievementsQuery } from "@/lib/queries/use-achievements-query"; import type { AchievementProgress } from "@/lib/gamification/achievements"; import { Skeleton } from "@/components/ui/skeleton"; +import { TileHeader } from "@/components/insights/tile-header"; const iconMap: Record = { Activity, @@ -113,23 +114,23 @@ export function RecentAchievementsCard() { data-slot="recent-achievements-card" className="bg-card border-border space-y-3 rounded-xl border p-4 md:p-6" > -
    -
    -
    - - {t("achievements.dashboardCard.viewAll")} - -
    + {/* Canonical tile header (foreground h-5 icon, text-base h2) so the + dashboard card matches its sibling tiles instead of the old muted + h-4 / text-sm row. The "view all" link rides the header's right slot + (§11 inline-action-row). */} + + {t("achievements.dashboardCard.viewAll")} + + } + /> {isPending ? (
      ", () => { expect(html).toContain("Good morning"); }); + it("renders the H1 at the canonical bold weight and on-scale sizes", () => { + const html = render(); + const greeting = html.match( + /data-slot="insights-hero-strip-greeting"[^>]*class="([^"]*)"/, + ); + expect(greeting).not.toBeNull(); + // App H1 weight is bold (UI-STANDARDS §5) — never font-semibold. + expect(greeting![1]).toContain("font-bold"); + expect(greeting![1]).not.toContain("font-semibold"); + // No off-scale arbitrary type sizes (§5) — round to the neighbouring step. + expect(greeting![1]).not.toContain("text-[28px]"); + expect(greeting![1]).toContain("sm:text-3xl"); + }); + + it("keeps the hero band and its columns on the spacing scale (no step-5)", () => { + const html = render(); + // The band's own padding + the flex column gaps stay on 4/6, never 5. + expect(html).not.toContain("py-5"); + expect(html).not.toContain("gap-5"); + }); + it("renders the morning greeting in German", () => { const html = render(, "de"); expect(html).toContain("Guten Morgen"); diff --git a/src/components/insights/hero-strip.tsx b/src/components/insights/hero-strip.tsx index f3f306693..2a75a8bb7 100644 --- a/src/components/insights/hero-strip.tsx +++ b/src/components/insights/hero-strip.tsx @@ -257,7 +257,7 @@ export function HeroStrip({ // it the shadow bled through the sticky section nav below // (the nav uses bg-background/80 + backdrop-blur, which the // shadow leaked through). - "relative isolate overflow-hidden rounded-xl px-4 py-5 sm:px-6 sm:py-6", + "relative isolate overflow-hidden rounded-xl px-4 py-4 sm:px-6 sm:py-6", )} > {/* @@ -290,12 +290,12 @@ export function HeroStrip({ from first paint. */}
      -
      +
      {/* Header glyph stays in the foreground tier — UI-STANDARDS §1 @@ -307,7 +307,7 @@ export function HeroStrip({ />

      {greeting}

      diff --git a/src/components/medications/research-mode-acknowledgment-dialog.tsx b/src/components/medications/research-mode-acknowledgment-dialog.tsx index 391a232f1..506cd8ca5 100644 --- a/src/components/medications/research-mode-acknowledgment-dialog.tsx +++ b/src/components/medications/research-mode-acknowledgment-dialog.tsx @@ -125,7 +125,7 @@ export function ResearchModeAcknowledgmentDialog({ onOpenChange={onOpenChange} title={ - + {t("medications.researchMode.dialog.title")} } diff --git a/src/components/medications/scheduling/natural-language-extractor.tsx b/src/components/medications/scheduling/natural-language-extractor.tsx index 29a55dd86..7f477e896 100644 --- a/src/components/medications/scheduling/natural-language-extractor.tsx +++ b/src/components/medications/scheduling/natural-language-extractor.tsx @@ -45,14 +45,7 @@ import { useCallback, useId, useRef, useState } from "react"; import { Loader2, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; +import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { useTranslations } from "@/lib/i18n/context"; @@ -208,92 +201,20 @@ export function NaturalLanguageExtractor({ const overLimit = charCount > MAX_TEXT_LENGTH; return ( - { if (!next) handleClose(); }} - > - - - - - - {t("medications.scheduling.naturalLanguage.description")} - - - -
      -
      - -