mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
* 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * feat(ui): add Skeleton and EmptyState primitives Two pieces every page in v1.4 needs and the codebase didn't have: - `<Skeleton>` — a presentation `<div>` 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. - `<EmptyState>` — 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 <noreply@anthropic.com> * 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 `<Input>` 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 <noreply@anthropic.com> --------- Co-authored-by: Marc-André Bombeck <noreply@anthropic.com>
51 lines
2.1 KiB
TypeScript
51 lines
2.1 KiB
TypeScript
import * as React from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
/**
|
|
* Base input primitive.
|
|
*
|
|
* v1.4 hardening: defaults `autoComplete` to `"off"` and tells
|
|
* LastPass / 1Password to skip the field when the caller does not
|
|
* opt in to a semantic autocomplete value. HealthLog is a health-data
|
|
* app — letting browsers / password managers autofill measurement,
|
|
* medication, mood, and AI-token inputs with the user's last-saved
|
|
* email or account password is at best confusing and at worst leaks
|
|
* credentials into a free-text field that gets persisted server-side.
|
|
*
|
|
* Auth and profile forms (login, register, change-email) keep working
|
|
* because they already pass an explicit `autoComplete` value —
|
|
* `"username"`, `"email"`, `"current-password"`, `"new-password"`. When
|
|
* `autoComplete` is anything other than `"off"`, the LastPass /
|
|
* 1Password ignore attrs are dropped so password managers fill the
|
|
* field normally.
|
|
*/
|
|
function Input({
|
|
className,
|
|
type,
|
|
autoComplete,
|
|
...props
|
|
}: React.ComponentProps<"input">) {
|
|
const resolvedAutoComplete = autoComplete ?? "off";
|
|
const skipAutofill = resolvedAutoComplete === "off";
|
|
|
|
return (
|
|
<input
|
|
type={type}
|
|
data-slot="input"
|
|
autoComplete={resolvedAutoComplete}
|
|
data-lpignore={skipAutofill ? "true" : undefined}
|
|
data-1p-ignore={skipAutofill ? "true" : undefined}
|
|
className={cn(
|
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export { Input };
|