# HealthLog UI Guidelines > Visual & interaction patterns in the HealthLog PWA. The binding rules > for card anatomy, spacing, text colour, tokens and primitives live in > [`contributing/ui-standards.md`](./contributing/ui-standards.md); where > this document disagrees with them, the standards win. If a pattern > below doesn't fit your case, propose an addition 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 | | Label-to-field stack (form fields) | `space-y-2` | 8 | | 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 / section card | `text-lg font-semibold` | | Tile / inner card title | `text-base font-semibold` | | 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` | Card titles come in exactly two sizes: `text-lg font-semibold` for a section-level card (one per page region) and `text-base font-semibold` for tiles and inner cards. Don't introduce a third level — a smaller heading is a sign the content wants a list row, not a card. 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 Forms keep a controlled draft in component state and validate it with the Zod schemas from `src/lib/validations/`; the server's schema is the authority. Wrap inputs in `