6 KiB
6 KiB
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 # TypeScript types mirroring backend models (manual sync)
├── 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.sveltefor 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:
export const load: PageServerLoad = async () => {
const data = await getProducts();
return { products: data };
};
Form actions parse FormData, call API, return result or fail():
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/GroupedSelectover bits-uiui/selectunless search/rich popup UX needed. - Use
form-classes.tstokens (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 hardcodedn/a.
API Client
src/lib/api.ts — typed fetch wrappers.
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<T>(), api.post<T>(), api.patch<T>(), 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
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/
Anti-Patterns
- No frontend tests exist. Only linting + type checking.
- ESLint
svelte/no-navigation-without-resolvehasignoreGoto: trueworkaround (upstream bug sveltejs/eslint-plugin-svelte#1327). src/paraglide/is a legacy output path — active i18n output is insrc/lib/paraglide/.