Files
portal/docs/ai/svelte-patterns.md
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

186 lines
2.6 KiB
Markdown

# 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:
<select bind:value={frequency}>
<option value="1">One</option>
</select>
This can fail to update correctly.
Use:
<select onchange={(e) => frequency = e.currentTarget.value}>
<option value="1" selected={frequency === '1'}>
One
</option>
</select>
Use selected on options.
---
# Input Elements
Text inputs are different.
This is acceptable:
<input bind:value={name} />
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:
<form oninput={markDirty}>
<button disabled={!!record?.id && !dirty}>
Save
</button>
</form>
---
# 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.