innercontext/frontend/AGENTS.md

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.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:

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

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