From 359d4d19cc4219ce7f21d4d3024b1357bfd5e1bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 8 May 2026 12:05:15 +0200 Subject: [PATCH] v1.4 foundation: UI guidelines, primitives, AI test fix, autofill hardening (#122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(ui): add UI guidelines as single source of truth + shadcn MCP server - `docs/ui-guidelines.md` lays out Dracula tokens, spacing, typography, component picks (button/input/dialog/sheet decision tree), layout rules (settings split target, dashboard one-row), accessibility baseline (WCAG 2.1 AA + axe-core in CI), the autofill / honeypot pattern for measurement forms, and a reusable "Test button" UX for every integration. New components in v1.4 reference this doc. - `.mcp.json` wires the official `shadcn` MCP server so the design system can be browsed and components installed without leaving the editor. - Adds `@vitest/coverage-v8` to dev dependencies — `pnpm test --coverage` now reports real per-file coverage (the v1.4 cycle uses this to lift critical-path coverage). Co-Authored-By: Marc-André Bombeck * fix(settings/ai): connection-test honours the unsaved provider selection Changing the AI provider in /settings, then clicking "Verbindung testen" without saving first, used to silently run the test against the *saved* provider instead — so the result felt unrelated to what the user had on screen. This often surfaced as a confusing OK / failure depending on whichever provider had previously been persisted. The test endpoint now accepts an optional JSON body that mirrors the form fields (provider, model, baseUrl, anthropicKey, localKey). When present, the server resolves a transient provider from those values — falling back to the persisted, encrypted credentials per field, so a user with a stored Anthropic key can swap models in the dropdown without re-typing the key. Plaintext keys never persist; the existing SSRF guard for the Local provider's base URL is reused, the existing rate limit (5/min) and the V3 error-leak shielding stay in place. Empty body keeps the previous behaviour for any other caller. Tests cover the new override path (provider forwarded, unknown provider rejected with 422, empty body uses persisted config, config errors surface the resolver's status). Whitespace in `src/app/settings/page.tsx` is reformatted by Prettier as a side-effect; logic touches only the AI section. Co-Authored-By: Marc-André Bombeck * feat(ui): add Skeleton and EmptyState primitives Two pieces every page in v1.4 needs and the codebase didn't have: - `` — a presentation `
` with the standard pulse animation, `aria-hidden`, and `motion-reduce:animate-none`. Used to shape the page while data loads instead of dropping a spinner. The visible layout no longer jumps when the first measurement arrives. - `` — the shape every list, tile, and chart panel reaches for when it has no data: optional icon, title, description, primary action; `card` (default) and `plain` variants; `default`/`compact` density. Lives inside an `aria-live="polite"` region so screen readers announce the message and its action. Tests cover screen-reader semantics, prettier-merged class names, density/variant switches. These two land first because the v1.4 pages — the split settings sections, the new dashboard hero, the redesigned admin status grid — all assume the primitives exist. The patterns are documented in `docs/ui-guidelines.md` §4.1–4.2. Co-Authored-By: Marc-André Bombeck * fix(ui): block password-manager autofill on health-data inputs by default Browsers and password managers were happily autofilling the user's account credentials into measurement, medication, mood, and AI-token fields whenever those forms didn't explicitly set `autoComplete`. The codebase had zero `autoComplete` attributes across the data-entry surfaces, so this happened often enough to be both a UX papercut (account password landing in a "Notiz" field) and a real privacy issue (secrets persisting through CSV/JSON export). The base `` primitive now: - Defaults `autoComplete` to `"off"` when the caller doesn't supply a value. - Adds `data-lpignore="true"` and `data-1p-ignore="true"` whenever autoComplete resolves to `"off"`, so LastPass and 1Password skip the field too. - Keeps the password manager active for any form that already passes a semantic `autoComplete` (`"username"`, `"email"`, `"current-password"`, `"new-password"`) — the existing login, register, and AI-key inputs continue to work unchanged. Tests cover the three meaningful states (default off, explicit semantic value, login-flow `username`). Co-Authored-By: Marc-André Bombeck --------- Co-authored-by: Marc-André Bombeck --- .mcp.json | 8 + docs/ui-guidelines.md | 464 +++++++++++ package.json | 1 + pnpm-lock.yaml | 118 ++- src/app/api/ai/test/__tests__/route.test.ts | 127 ++- src/app/api/ai/test/route.ts | 51 +- src/app/settings/page.tsx | 784 ++++++++++-------- .../ui/__tests__/empty-state.test.tsx | 49 ++ src/components/ui/__tests__/input.test.tsx | 31 + src/components/ui/__tests__/skeleton.test.tsx | 24 + src/components/ui/empty-state.tsx | 118 +++ src/components/ui/input.tsx | 31 +- src/components/ui/skeleton.tsx | 32 + src/lib/ai/provider.ts | 143 +++- 14 files changed, 1589 insertions(+), 392 deletions(-) create mode 100644 .mcp.json create mode 100644 docs/ui-guidelines.md create mode 100644 src/components/ui/__tests__/empty-state.test.tsx create mode 100644 src/components/ui/__tests__/input.test.tsx create mode 100644 src/components/ui/__tests__/skeleton.test.tsx create mode 100644 src/components/ui/empty-state.tsx create mode 100644 src/components/ui/skeleton.tsx diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..29585572e --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "shadcn": { + "command": "npx", + "args": ["-y", "shadcn@latest", "mcp"] + } + } +} diff --git a/docs/ui-guidelines.md b/docs/ui-guidelines.md new file mode 100644 index 000000000..224a8c21d --- /dev/null +++ b/docs/ui-guidelines.md @@ -0,0 +1,464 @@ +# HealthLog UI Guidelines + +> Single source of truth for visual & interaction patterns in the HealthLog +> PWA. Consult this document before adding a new component, page, or +> setting. If a pattern below doesn't fit your case, propose an addition +> here in the same PR. + +Last updated: 2026-05-08 (v1.4 cycle) + +--- + +## 1. Foundations + +### 1.1 Theme + +HealthLog is a Dracula-themed app with a light counterpart (Alucard). Both +themes share the same six-color signal palette (purple, pink, green, cyan, +orange, red, yellow, lavender), exposed as CSS variables and as Tailwind +`dracula-*` color tokens. Use the semantic tokens (`--primary`, +`--destructive`, `--card`, …) for chrome and the `--dracula-*` tokens for +data viz only. + +| Surface | Token | +| ---------------------------- | ------------------ | +| App background | `--background` | +| Card / popover surface | `--card` | +| Primary action | `--primary` | +| Destructive action | `--destructive` | +| Secondary / muted background | `--secondary` | +| Border | `--border` | +| Focus ring | `--ring` | +| Chart series 1–5 | `--chart-1`…`-5` | +| Brand signal: purple | `--dracula-purple` | +| Brand signal: green (good) | `--dracula-green` | +| Brand signal: orange (warn) | `--dracula-orange` | +| Brand signal: red (alert) | `--dracula-red` | +| Brand signal: cyan (info) | `--dracula-cyan` | + +Dark mode is the default. The theme switcher in the sidebar three-dot menu +toggles `.dark` / `.light` on `:root`. Always test both modes before +shipping a component. + +### 1.2 Spacing + +Tailwind's default 4-pixel scale. Reach for these stops first: + +| Use | Token | Pixels | +| ------------------------------------ | ------------- | ------ | +| Inline gap between icon and label | `gap-1` | 4 | +| Tight stack inside a card | `gap-2` | 8 | +| Default form-field stack | `space-y-3` | 12 | +| Section block (heading + body) | `space-y-4` | 16 | +| Page section separator | `space-y-6` | 24 | +| Dashboard tile-row gap, hero spacing | `gap-4` | 16 | +| Page-edge padding (mobile / desktop) | `px-4 / px-6` | | + +Avoid arbitrary `space-y-5`, `gap-3.5`, etc. unless aligning to a fixed +external constraint. + +### 1.3 Typography + +The app uses the Tailwind defaults inherited via shadcn/ui. Reach for +these classes first: + +| Use | Class | +| ---------------------------------- | --------------------------------------- | +| Page title (h1) | `text-2xl font-semibold tracking-tight` | +| Section heading (h2) | `text-lg font-semibold` | +| Card title | `text-base font-medium` | +| Body text | `text-sm` | +| Muted helper / metadata | `text-sm text-muted-foreground` | +| Microcopy / footer | `text-xs text-muted-foreground` | +| Tabular data (latency, large nums) | `font-mono tabular-nums` | + +Never use `font-bold` for emphasis — bump to `font-semibold` instead and +let the design system breathe. + +### 1.4 Radius and elevation + +`--radius: 0.625rem` (10px) is the default. The Tailwind tokens +`rounded-md` (`--radius-md` ≈ 8px) and `rounded-lg` (`--radius` ≈ 10px) +cover 95% of cases. Use `rounded-2xl` only for hero cards or large +modals. + +Elevation is achieved via `shadow-xs`, `shadow-sm`, or +`shadow-[0_1px_0_rgba(255,255,255,0.04)]` for very subtle separators on +dark backgrounds. Avoid Tailwind's heavy shadows (`shadow-lg`+) in chrome. + +--- + +## 2. Components + +This section is **prescriptive, not exhaustive**. Each entry pairs a +Radix/shadcn primitive with the cases it's the right answer for and the +cases where another primitive should win. + +### 2.1 Buttons + +Every button comes from `@/components/ui/button`. Pick by purpose: + +| Purpose | Variant | Size | +| ----------------------------------------------------- | ------------- | ----------------- | +| Primary action on a page or dialog | `default` | `default` | +| Cancel, dismiss, secondary action next to primary | `outline` | `default` | +| Tertiary in-row action (edit, copy, snooze) | `ghost` | `sm` | +| Destructive (delete, revoke, disconnect) | `destructive` | `default` or `sm` | +| Icon-only action (toolbar, chart toggles, table rows) | `ghost` | `icon-sm` | +| Inline link to external help docs | `link` | `sm` | +| Toolbar / table row action when space is tight | `outline` | `xs` | + +**Position rules.** + +- Forms (full-width or modal): primary on the **right**, cancel/back on + the **left**. Match shadcn dialog defaults. +- Settings sections: a single sticky `Save` button at the **right end** + of the section header _or_ per-row inline `Save`. Never both in the + same section. +- Test buttons (Telegram test, AI ping, etc.): immediately to the right + of the related Save / Apply control. Never below — keeps the eye + motion horizontal. +- Destructive actions live in their own confirm dialog with a + type-to-confirm input when the action is irreversible (delete account, + revoke API token, drop user data). + +**Disabled state.** A disabled button must always come with a tooltip or +helper text explaining why. Silent disabled buttons frustrate users. + +### 2.2 Inputs and forms + +We use `react-hook-form` + Zod schemas from `src/lib/validations/`. +Wrap inputs in `