213 lines
9.9 KiB
Markdown
213 lines
9.9 KiB
Markdown
# Frontend Design Cookbook
|
|
|
|
This cookbook defines the visual system for the frontend so every new change extends the existing style instead of inventing a new one.
|
|
|
|
## Agent workflow
|
|
|
|
- For any frontend edit, consult this cookbook before implementing changes.
|
|
- If a change introduces or alters reusable UI patterns, wrappers, component variants, tokens, motion rules, or shared classes, update this cookbook in the same change.
|
|
- Keep updates concise and actionable so future edits remain consistent.
|
|
|
|
## Design intent
|
|
|
|
- Core tone: light editorial, calm and information-first.
|
|
- Product feel: premium personal logbook, not generic SaaS dashboard.
|
|
- Contrast model: neutral paper and ink do most of the work; accents are restrained.
|
|
|
|
## Non-negotiables
|
|
|
|
- Keep layouts readable first. Aesthetic details support hierarchy, not the other way around.
|
|
- Use one visual language across the app shell, cards, forms, tables, and actions.
|
|
- Prefer subtle depth (borders, layered paper, soft shadows) over loud gradients.
|
|
- Keep motion purposeful and short; always preserve `prefers-reduced-motion` behavior.
|
|
|
|
## Typography
|
|
|
|
- Display/headings: `Cormorant Infant`.
|
|
- Body/UI text: `Manrope`.
|
|
- Use display typography for page titles and section heads only.
|
|
- Keep paragraph text in body font for legibility.
|
|
- Keep Google font loading aligned with current usage:
|
|
- `Cormorant Infant`: `600`, `700` (no italic)
|
|
- `Manrope`: `400`, `500`, `600`, `700`
|
|
|
|
## Color system
|
|
|
|
Global neutrals are defined in `frontend/src/app.css` using CSS variables.
|
|
|
|
- `--background`, `--card`, `--foreground`, `--muted-foreground`, `--border`
|
|
- `--page-accent` drives route-level emphasis.
|
|
- `--page-accent-soft` is the low-contrast companion tint.
|
|
|
|
### Domain accents (muted and professional)
|
|
|
|
- Dashboard: `--accent-dashboard`
|
|
- Products: `--accent-products`
|
|
- Routines: `--accent-routines`
|
|
- Skin: `--accent-skin`
|
|
- Health labs: `--accent-health-labs`
|
|
- Health medications: `--accent-health-meds`
|
|
|
|
The app shell assigns a domain class per route and maps it to `--page-accent`.
|
|
|
|
## Where to use accent color
|
|
|
|
Use accent for:
|
|
|
|
- active navigation state
|
|
- important buttons and key badges
|
|
- focus ring tint and hover border tint
|
|
- section separators and small status markers
|
|
|
|
Do not use accent for:
|
|
|
|
- full-page backgrounds
|
|
- body text
|
|
- large surfaces that reduce readability
|
|
|
|
Guideline: accent should occupy roughly 10-15% of visual area per screen.
|
|
|
|
## Layout rules
|
|
|
|
- Use the app shell spacing rhythm from `app.css` (`.app-main`, editorial cards).
|
|
- Keep max content width constrained for readability.
|
|
- Prefer asymmetry in hero/summary areas, but keep forms and dense data grids regular.
|
|
|
|
### Shared page wrappers
|
|
|
|
Use these wrappers before introducing route-specific structure:
|
|
|
|
- `editorial-page`: standard constrained content width for route pages.
|
|
- `editorial-hero`: top summary strip for title, subtitle, and primary actions.
|
|
- `editorial-panel`: primary surface for forms, tables, and ledgers.
|
|
- `editorial-toolbar`: compact action row under hero copy.
|
|
- `editorial-backlink`: standard top-left back navigation style.
|
|
- `editorial-alert`, `editorial-alert--error`, `editorial-alert--success`: feedback banners.
|
|
|
|
## Component rules
|
|
|
|
- Reuse UI primitives under `frontend/src/lib/components/ui/*`.
|
|
- Keep primitive APIs stable when changing visual treatment.
|
|
- Style via tokens and shared classes, not one-off hardcoded colors.
|
|
- New variants must be documented in this file.
|
|
|
|
### Existing shared utility patterns
|
|
|
|
These classes are already in use and should be reused:
|
|
|
|
- Lists and ledgers: `routine-ledger-row`, `products-mobile-card`, `health-entry-row`
|
|
- Group headers: `products-section-title`
|
|
- Table shell: `products-table-shell`
|
|
- Tabs shell: `products-tabs`, `editorial-tabs`
|
|
- Health semantic pills: `health-kind-pill*`, `health-flag-pill*`
|
|
- Lab results utilities:
|
|
- metadata chips: `lab-results-meta-strip`, `lab-results-meta-pill`
|
|
- filter surfaces: `lab-results-filter-panel`, `lab-results-filter-banner`, `lab-results-pager`
|
|
- row/link rhythm: `lab-results-row`, `lab-results-code-link`, `lab-results-value-cell`
|
|
- mobile density: `lab-results-mobile-grid`, `lab-results-mobile-card`, `lab-results-mobile-value`
|
|
|
|
## Forms and data views
|
|
|
|
- Inputs should remain high-contrast and calm.
|
|
- Validation/error states should be explicit and never color-only.
|
|
- Tables and dense lists should prioritize scanning: spacing, row separators, concise metadata.
|
|
- Filter toolbars for data-heavy routes should use `GET` forms with URL params so state is shareable and pagination links preserve active filters.
|
|
- For high-volume medical data lists, default the primary view to condensed/latest mode and offer full-history as an explicit secondary option.
|
|
- In condensed/latest mode, group rows by collection date using lightweight section headers (`products-section-title`) to preserve report context without introducing heavy card nesting.
|
|
- Change/highlight pills in dense tables should stay compact (`text-[10px]`), semantic (new/flag change/abnormal), and avoid overwhelming color blocks.
|
|
- For lab results, keep ordering fixed to newest collection date (`collected_at DESC`) and remove non-essential controls (no lab filter and no manual sort selector).
|
|
- For lab results, keep code links visibly interactive (`lab-results-code-link`) because they are a primary in-context drill-down interaction.
|
|
- For lab results, use compact metadata chips in hero sections (`lab-results-meta-pill`) for active view/filter context instead of introducing a second heavy summary card.
|
|
- In dense row-based lists, prefer `ghost` action controls; use icon-only buttons on desktop tables and short text+icon `ghost` actions on mobile cards to keep row actions subordinate to data.
|
|
- For editable data tables, open a dedicated inline edit panel above the list (instead of per-row expanded forms) and prefill it from row actions; keep users on the same filtered/paginated context after save.
|
|
- When a list is narrowed to a single entity key (for example `test_code`), display an explicit "filtered by" banner with a one-click clear action and avoid extra grouping wrappers that add no context.
|
|
|
|
### DRY form primitives
|
|
|
|
- Use shared form components for repeated native select markup:
|
|
- `frontend/src/lib/components/forms/SimpleSelect.svelte`
|
|
- `frontend/src/lib/components/forms/GroupedSelect.svelte`
|
|
- Use shared checkbox helper for repeated label+hint toggles:
|
|
- `frontend/src/lib/components/forms/HintCheckbox.svelte`
|
|
- Use shared input field helper for repeated label+input rows:
|
|
- `frontend/src/lib/components/forms/LabeledInputField.svelte`
|
|
- Use shared section card helper for repeated titled form panels:
|
|
- `frontend/src/lib/components/forms/FormSectionCard.svelte`
|
|
- Use shared class tokens from:
|
|
- `frontend/src/lib/components/forms/form-classes.ts`
|
|
- Prefer passing option labels from route files via `m.*` to keep i18n explicit.
|
|
|
|
### Select policy (performance + maintainability)
|
|
|
|
- Default to native `<select>` for enum-style fields and simple pickers.
|
|
- Use `SimpleSelect` / `GroupedSelect` for consistency and to reduce duplication.
|
|
- Avoid `ui/select` primitives unless search, custom keyboard behavior, or richer popup UX is truly needed.
|
|
- For grouped option sets (e.g. products by category), use `<optgroup>` via `GroupedSelect`.
|
|
|
|
### Chunk hygiene
|
|
|
|
- Large forms should be split into focused section components.
|
|
- Lazy-load heavyweight modal and optional sections where practical.
|
|
- After structural UI refactors, run `pnpm build` and inspect output for large chunks.
|
|
- Track and prevent regressions in known hotspots (form-heavy pages and shared interaction primitives).
|
|
|
|
## Motion
|
|
|
|
- Entry animations: short upward reveal with slight stagger.
|
|
- Hover interactions: subtle translation or tint only.
|
|
- Never stack multiple strong animations in the same viewport.
|
|
|
|
## Accessibility baseline
|
|
|
|
- Keyboard navigation for all interactive elements.
|
|
- Visible focus states on controls and links.
|
|
- Respect reduced motion.
|
|
- Maintain readable contrast for text, borders, and controls.
|
|
|
|
## i18n rules
|
|
|
|
- Do not add hardcoded UI labels in route components when a message key already exists.
|
|
- Prefer existing `m.*` keys from paraglide for headings, labels, empty states, and helper text.
|
|
- If a new label is required, add a new translation key instead of shipping English literals.
|
|
- Keep fallback display values localized (`m.common_unknown()` over hardcoded `n/a`).
|
|
- Avoid language-specific toggle text in reusable components unless fully localized.
|
|
|
|
## Implementation checklist for new UI work
|
|
|
|
1. Pick the route domain and rely on `--page-accent`.
|
|
2. Use existing typography pair and spacing rhythm.
|
|
3. Build with primitives first; add route-level wrappers only if needed.
|
|
4. Validate mobile and desktop layout.
|
|
5. Run:
|
|
- `pnpm check`
|
|
- `pnpm lint`
|
|
- `pnpm build` (for significant component architecture changes)
|
|
|
|
## File touchpoints
|
|
|
|
- Core tokens and global look: `frontend/src/app.css`
|
|
- App shell and route domain mapping: `frontend/src/routes/+layout.svelte`
|
|
- Route examples using the pattern:
|
|
- `frontend/src/routes/+page.svelte`
|
|
- `frontend/src/routes/products/+page.svelte`
|
|
- `frontend/src/routes/routines/+page.svelte`
|
|
- `frontend/src/routes/health/lab-results/+page.svelte`
|
|
- `frontend/src/routes/skin/+page.svelte`
|
|
- Primitive visuals:
|
|
- `frontend/src/lib/components/ui/button/button.svelte`
|
|
- `frontend/src/lib/components/ui/card/card.svelte`
|
|
- `frontend/src/lib/components/ui/input/input.svelte`
|
|
- `frontend/src/lib/components/ui/badge/badge.svelte`
|
|
|
|
- Shared form DRY helpers:
|
|
- `frontend/src/lib/components/forms/SimpleSelect.svelte`
|
|
- `frontend/src/lib/components/forms/GroupedSelect.svelte`
|
|
- `frontend/src/lib/components/forms/HintCheckbox.svelte`
|
|
- `frontend/src/lib/components/forms/LabeledInputField.svelte`
|
|
- `frontend/src/lib/components/forms/FormSectionCard.svelte`
|
|
- `frontend/src/lib/components/forms/form-classes.ts`
|
|
|
|
- i18n message source:
|
|
- `frontend/src/lib/paraglide/messages/*`
|
|
|
|
When introducing new visual patterns, update this cookbook in the same change.
|