Files
portal/docs/ai/svelte-patterns.md
T
NeilClack 91d162c746 Rebuild admin pages around DataTable/AdminModal; restructure categories/groups and v2 groups API
Remove the generic $lib/svelte-admin CRUD layer and AdminTable.svelte; each
admin route now owns its own form/modal logic. Add DataTable.svelte,
superform-errors.ts, and profile-specific modal/field components.

Flatten proclamation categories/groups and drop subscription levels; add the
v2 groups API; update v1/v2 endpoints, serializers, auth, and regenerated
Prisma client with the new migrations.

Document the overhaul plan in docs/ADMIN_OVERHAUL.md.
2026-09-10 09:38:01 -05:00

2.6 KiB

Svelte 5 Patterns and Rules

This document contains Svelte-specific implementation rules.

Only read this file when modifying Svelte components, routes, or Svelte-specific logic.


Svelte Version

This project uses:

  • SvelteKit 2
  • Svelte 5
  • runes mode

Always use:

  • $state()
  • $derived()
  • $effect()
  • $props()

Do not use legacy stores:

  • writable()
  • readable()
  • derived()

Component Props

Always destructure $props() with defaults.

Preferred pattern:

let { record = {} as Record<string, unknown>, fieldErrors = {} as Record<string, string>, isSuper = false }: { record?: Record<string, unknown>; fieldErrors?: Record<string, string>; isSuper?: boolean; } = $props();

Do not assume props exist.


State Synchronization

IMPORTANT:

Do not initialize $state from props when the prop can change.

Incorrect:

let name = $state(record.name);

This only captures the initial value.

Correct:

let name = $state('');

$effect(() => { name = String(record.name ?? ''); });

Use $effect whenever component state needs to follow changing props.


Effect Ordering

$effect blocks execute in declaration order.

When multiple effects modify related state:

  1. Reset state first.
  2. Populate state second.
  3. Perform dependent calculations afterward.

Example:

let dirty = $state(false);

$effect(() => { dirty = false; });

$effect(() => { name = String(record.name ?? ''); });

This ensures a new record starts clean.


Select Inputs

IMPORTANT:

Do not use bind:value on select elements whose values are synchronized through $effect.

Incorrect:

One

This can fail to update correctly.

Use:

<select onchange={(e) => frequency = e.currentTarget.value}>

One

Use selected on options.


Input Elements

Text inputs are different.

This is acceptable:

bind:value works correctly with normal input elements.


Forms

Admin forms use:

  • $state for field values
  • $effect for prop synchronization
  • oninput for dirty tracking
  • use:enhance for submission

Example:

Save

Avoid Unnecessary State

Prefer:

$derived()

for values computed from other state.

Use:

$state()

only for values that change independently.


Component Changes

Before modifying a component:

  1. Read the entire component.
  2. Find similar components.
  3. Follow existing patterns.
  4. Avoid introducing new abstractions unless necessary.

Small, consistent changes are preferred.