# Frontend SvelteKit 2 + Svelte 5 (Runes) web UI. Adapter: `@sveltejs/adapter-node` (required for form actions). ## Structure ``` frontend/src/ ├── app.css # Tailwind v4 theme + editorial design system (1420 lines) ├── app.html # HTML shell (Cormorant Infant + Manrope fonts) ├── hooks.server.ts # Paraglide i18n middleware ├── routes/ # SvelteKit file-based routing │ ├── +layout.svelte # App shell, sidebar, mobile drawer, domain-based theming │ ├── +page.svelte # Dashboard (routines, snapshots, lab results) │ ├── products/ # List, [id] detail/edit, new, suggest (AI) │ ├── routines/ # List, [id] detail/edit, new, suggest (AI), grooming-schedule/ │ ├── health/ # medications/ (list, new), lab-results/ (list, new) │ ├── skin/ # Snapshots list, new (with photo analysis) │ └── profile/ # User profile └── lib/ ├── api.ts # Typed fetch wrappers (server: PUBLIC_API_BASE, browser: /api) ├── types.ts # Type bridge — re-exports from generated OpenAPI types with augmentations ├── api/generated/ # Auto-generated types from backend OpenAPI schema — DO NOT EDIT ├── utils.ts # cn() class merger, bits-ui types ├── utils/ # forms.ts (preventIfNotConfirmed), skin-display.ts (label helpers) ├── paraglide/ # Generated i18n runtime — DO NOT EDIT └── components/ ├── ui/ # bits-ui primitives: button, card, badge, input, label, select, tabs, table, separator ├── forms/ # DRY helpers: SimpleSelect, GroupedSelect, HintCheckbox, LabeledInputField, FormSectionCard, form-classes.ts ├── product-form/ # Sectioned form: Basic, Details, Classification, Assessment, Notes ├── PageHeader.svelte # Reusable page header (kicker, title, subtitle, backlink, actions via snippets) ├── ProductForm.svelte # Main product form (tabbed, 737 lines) ├── ProductFormAiModal.svelte # AI text-to-product parsing modal ├── FlashMessages.svelte # Error/success/warning/info alerts ├── StructuredErrorDisplay.svelte # Parses semicolon-separated backend errors into list ├── ValidationWarningsAlert.svelte # LLM validation warnings display ├── ReasoningChainViewer.svelte # AI reasoning chain viewer (collapsible) ├── MetadataDebugPanel.svelte # Token metrics, model info (collapsible) ├── AutoFixBadge.svelte # Auto-fix indicator └── LanguageSwitcher.svelte # i18n locale toggle ``` ## Design System **MUST READ**: `docs/frontend-design-cookbook.md` — update when introducing new UI patterns. - **Typography**: `Cormorant Infant` (display/headings), `Manrope` (body/UI). - **Colors**: CSS variables in `app.css`. Domain accents per route: products (green), routines (cyan), skin (orange), profile (blue), health-labs (purple), health-meds (teal). - **Layout wrappers**: `.editorial-page`, `.editorial-hero`, `.editorial-panel`, `.editorial-toolbar`, `.editorial-backlink`, `.editorial-alert`. - **Page header**: Use `PageHeader.svelte` for consistent title hierarchy, backlinks, and actions. - **Accent rule**: ~10-15% of visual area. Never full backgrounds or body text. - **Motion**: Short purposeful reveals. Always respect `prefers-reduced-motion`. ## Route Patterns Every page: `+page.svelte` (UI) + `+page.server.ts` (load + actions). Load functions fetch from API, return data: ```typescript export const load: PageServerLoad = async () => { const data = await getProducts(); return { products: data }; }; ``` Form actions parse FormData, call API, return result or `fail()`: ```typescript export const actions = { default: async ({ request }) => { const form = await request.formData(); try { const result = await createProduct(payload); return { success: true, product: result }; } catch (e) { return fail(500, { error: (e as Error).message }); } } }; ``` ## Component Conventions - Prefer `SimpleSelect` / `GroupedSelect` over bits-ui `ui/select` unless search/rich popup UX needed. - Use `form-classes.ts` tokens (`baseSelectClass`, `baseTextareaClass`) for consistent form styling. - Svelte 5 runes: `$props()`, `$state()`, `$derived()`, `$effect()`, `$bindable()`. - Snippet-based composition in `PageHeader` (actions, meta, children snippets). - Compound components: Card → CardHeader, CardContent, CardFooter, etc. ## i18n - Source messages: `frontend/messages/{en,pl}.json`. - Generated runtime: `src/lib/paraglide/` (via Vite plugin). - Import: `import * as m from '$lib/paraglide/messages.js'`. - **No hardcoded English labels.** Use `m.*` keys. Add new keys to message files if needed. - Fallback display: use `m.common_unknown()` not hardcoded `n/a`. ## API Client `src/lib/api.ts` — typed fetch wrappers. ```typescript const base = browser ? "/api" : PUBLIC_API_BASE; // Browser: /api (nginx proxies, strips prefix to backend) // Server-side (SSR): PUBLIC_API_BASE (http://localhost:8000) ``` Methods: `api.get()`, `api.post()`, `api.patch()`, `api.del()`. File upload: `analyzeSkinPhotos()` uses FormData (not JSON). Error handling: throws `Error` with `.detail` from backend response. ## Environment | Variable | Default | Set at | |----------|---------|--------| | `PUBLIC_API_BASE` | `http://localhost:8000` | Build time | Production: `PUBLIC_API_BASE=http://innercontext.lan/api pnpm build`. ## Commands ```bash pnpm dev # Dev server (API proxied to :8000) pnpm check # Type check + Svelte validation pnpm lint # ESLint pnpm format # Prettier pnpm build # Production build → build/ pnpm generate:api # Regenerate TypeScript types from backend OpenAPI schema ``` ## Anti-Patterns - No frontend tests exist. Only linting + type checking. - ESLint `svelte/no-navigation-without-resolve` has `ignoreGoto: true` workaround (upstream bug sveltejs/eslint-plugin-svelte#1327). - `src/paraglide/` is a legacy output path — active i18n output is in `src/lib/paraglide/`. ## Type Generation TypeScript types are auto-generated from the FastAPI backend's OpenAPI schema using `@hey-api/openapi-ts`. ### Workflow 1. Generate `openapi.json` from backend: `cd backend && uv run python -c "import json; from main import app; print(json.dumps(app.openapi(), indent=2))" > ../frontend/openapi.json` 2. Generate types: `cd frontend && pnpm generate:api` 3. Output lands in `src/lib/api/generated/types.gen.ts` — **never edit this file directly**. ### Architecture - **Generated types**: `src/lib/api/generated/types.gen.ts` — raw OpenAPI types, auto-generated. - **Bridge file**: `src/lib/types.ts` — re-exports from generated types with: - **Renames**: `ProductWithInventory` → `Product`, `ProductListItem` → `ProductSummary`, `UserProfilePublic` → `UserProfile`, `SkinConditionSnapshotPublic` → `SkinConditionSnapshot`. - **`Require` augmentations**: Fields with `default_factory` in SQLModel are optional in OpenAPI but always present in API responses (e.g. `id`, `created_at`, `updated_at`, `targets`, `inventory`). - **Relationship fields**: SQLModel `Relationship()` fields are excluded from OpenAPI schema. Added manually: `MedicationEntry.usage_history`, `Routine.steps`, `RoutineStep.product`, `ProductInventory.product`, `Product.inventory` (with augmented `ProductInventory`). - **Manual types**: `PriceTierSource`, `ShoppingPriority` — inline literals in backend, not named in OpenAPI. - **Canonical import**: Always `import type { ... } from '$lib/types'` — never import from `$lib/api/generated` directly. ### When to regenerate - After adding/modifying backend models or response schemas. - After adding/modifying API endpoints that change the OpenAPI spec. - After updating the bridge file, run `pnpm check` to verify type compatibility.