innercontext/docs/frontend-design-cookbook.md

10 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-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/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

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.
  • Use the products filter pattern as the shared baseline: compact search input, chip-style toggle rows (editorial-filter-row + small Button variants), 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.
  • 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 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.