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.
186 lines
2.6 KiB
Markdown
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.
|