12 KiB
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-motionbehavior.
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-accentdrives route-level emphasis.--page-accent-softis the low-contrast companion tint.
Domain accents (muted and professional)
- Dashboard:
--accent-dashboard - Products:
--accent-products - Routines:
--accent-routines - Skin:
--accent-skin - Profile:
--accent-profile - 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.PageHeader.svelte: preferred reusable wrapper for page-level hero sections; use it to keep title hierarchy, backlinks, meta rows, and action placement consistent.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,editorial-alert--warning,editorial-alert--info: feedback banners.page-header-meta,page-header-foot,hero-strip: shared secondary rows inside page headers for compact metadata and summary stats.
Collapsible panels
For secondary information (debug data, reasoning chains, metadata), use this pattern:
<div class="border border-muted rounded-lg overflow-hidden">
<button class="w-full flex items-center gap-2 px-4 py-3 bg-muted/30 hover:bg-muted/50 transition-colors">
<Icon class="size-4 text-muted-foreground" />
<span class="text-sm font-medium text-foreground">Panel Title</span>
<ChevronIcon class="ml-auto size-4 text-muted-foreground" />
</button>
{#if expanded}
<div class="p-4 bg-card border-t border-muted">
<!-- Content -->
</div>
{/if}
</div>
This matches the warm editorial aesthetic and maintains visual consistency with Card components.
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,editorial-mobile-card,health-entry-row - Group headers:
editorial-section-title - Table shell:
editorial-table-shell - Compact metadata rows:
editorial-meta-strip - Tabs shell:
products-tabs,editorial-tabs - App shell/navigation:
app-mobile-header,app-drawer,app-nav-list,app-nav-link,app-sidebar-footer - Reusable locale control:
LanguageSwitcher.sveltewithlanguage-switcher*classes - Dashboard summary patterns:
dashboard-stat-strip,dashboard-stat-card,dashboard-attention-list,dashboard-attention-item - Health semantic pills:
health-kind-pill*,health-flag-pill* - Lab results utilities:
- metadata chips:
lab-results-meta-strip,lab-results-meta-pill - filter/paging surfaces:
editorial-filter-row,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
- metadata chips:
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
GETforms with URL params so state is shareable and pagination links preserve active filters. - Use the products filter pattern as the shared baseline: compact search input, chip-style toggle rows (
editorial-filter-row+ smallButtonvariants), and apply/reset actions aligned at the end of the toolbar. - For high-volume medical data lists, default the primary view to condensed/latest mode and offer full-history as an explicit secondary option.
- For profile/settings forms, reuse shared primitives (
FormSectionCard,LabeledInputField,SimpleSelect) before creating route-specific field wrappers. - 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; keep this strip terse (one context chip + one stats chip, with optional alert chip). - In dense row-based lists, prefer
ghostaction controls; use icon-only buttons on desktop tables and short text+iconghostactions 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. - For dashboard-style summaries, prefer compact stat strips and attention rows over large decorative cards; each item should pair one strong value with one short explanatory line.
DRY form primitives
- Use shared form components for repeated native select markup:
frontend/src/lib/components/forms/SimpleSelect.sveltefrontend/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/GroupedSelectfor consistency and to reduce duplication. - Avoid
ui/selectprimitives unless search, custom keyboard behavior, or richer popup UX is truly needed. - For grouped option sets (e.g. products by category), use
<optgroup>viaGroupedSelect.
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 buildand 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 hardcodedn/a). - Avoid language-specific toggle text in reusable components unless fully localized.
Implementation checklist for new UI work
- Pick the route domain and rely on
--page-accent. - Use existing typography pair and spacing rhythm.
- Build with primitives first; add route-level wrappers only if needed.
- Validate mobile and desktop layout.
- Run:
pnpm checkpnpm lintpnpm 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 -
Shared page header:
frontend/src/lib/components/PageHeader.svelte -
Route examples using the pattern:
frontend/src/routes/+page.sveltefrontend/src/routes/products/+page.sveltefrontend/src/routes/routines/+page.sveltefrontend/src/routes/profile/+page.sveltefrontend/src/routes/health/lab-results/+page.sveltefrontend/src/routes/skin/+page.svelte
-
Primitive visuals:
frontend/src/lib/components/ui/button/button.sveltefrontend/src/lib/components/ui/card/card.sveltefrontend/src/lib/components/ui/input/input.sveltefrontend/src/lib/components/ui/badge/badge.svelte
-
Shared form DRY helpers:
frontend/src/lib/components/forms/SimpleSelect.sveltefrontend/src/lib/components/forms/GroupedSelect.sveltefrontend/src/lib/components/forms/HintCheckbox.sveltefrontend/src/lib/components/forms/LabeledInputField.sveltefrontend/src/lib/components/forms/FormSectionCard.sveltefrontend/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.