Update AGENTS.md
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user