refactor: migrate all Svelte components from Svelte 4 to Svelte 5 runes

- Replace `export let` with `$props()` and `$bindable()` across all components
- Replace `$:` reactive statements with `$derived()` and `$effect()`
- Replace `createEventDispatcher` with callback props (e.g. `onchange`)
- Replace `on:event` directives with inline event handlers (`onclick`, `oninput`, etc.)
- Replace `<slot />` with `{@render children()}` in layout
- Use `untrack()` for SSR-safe $state initialization from reactive props
- Replace `blur` + `setTimeout` anti-pattern in TagInput with `clickOutside` action
- Fix `page` store usage in layout to use `$app/state` directly
- 0 errors, 0 warnings after svelte-check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-17 11:43:26 +01:00
parent 25e095ea47
commit 4417fc9828
14 changed files with 388 additions and 441 deletions

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import { goto } from '$app/navigation';
export let data;
let searchTimeout: any;
let { data } = $props();
let searchTimeout: ReturnType<typeof setTimeout>;
// Live-Suche (Debounce)
function handleSearch(e: Event) {
const value = (e.target as HTMLInputElement).value;
clearTimeout(searchTimeout);
@@ -49,7 +49,7 @@ function handleSearch(e: Event) {
type="text"
placeholder="Namen suchen..."
value={data.q || ''}
on:input={handleSearch}
oninput={handleSearch}
class="block w-full rounded-sm border border-gray-300 bg-white py-2.5 pr-10 pl-4 font-sans text-sm text-brand-navy placeholder-gray-400 shadow-sm focus:border-brand-navy focus:ring-1 focus:ring-brand-navy focus:outline-none"
/>
<div

View File

@@ -2,21 +2,25 @@
import { enhance } from '$app/forms';
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
export let data;
export let form;
let { data, form } = $props();
$: ({ person, documents } = data);
const person = $derived(data.person);
const documents = $derived(data.documents);
let editMode = false;
let mergeTargetId = '';
let showMergeConfirm = false;
let editMode = $state(false);
let mergeTargetId = $state('');
let showMergeConfirm = $state(false);
function enterEdit() { editMode = true; }
function cancelEdit() { editMode = false; }
$effect(() => {
if (form?.updated) editMode = false;
});
$: if (form?.updated) { editMode = false; }
$: person.id, (() => { mergeTargetId = ''; showMergeConfirm = false; })();
$effect(() => {
// Reset merge state whenever person changes
person.id;
mergeTargetId = '';
showMergeConfirm = false;
});
</script>
<div class="max-w-4xl mx-auto py-10 px-4">
@@ -83,7 +87,7 @@
<button type="submit" class="px-5 py-2 bg-brand-navy text-white text-sm font-bold uppercase tracking-widest rounded hover:bg-brand-navy/80 transition-colors">
Speichern
</button>
<button type="button" on:click={cancelEdit} class="px-5 py-2 border border-gray-300 text-gray-600 text-sm font-bold uppercase tracking-widest rounded hover:bg-gray-50 transition-colors">
<button type="button" onclick={() => (editMode = false)} class="px-5 py-2 border border-gray-300 text-gray-600 text-sm font-bold uppercase tracking-widest rounded hover:bg-gray-50 transition-colors">
Abbrechen
</button>
</div>
@@ -103,7 +107,7 @@
<h1 class="text-4xl font-serif text-brand-navy">
{person.firstName} {person.lastName}
</h1>
<button on:click={enterEdit} class="ml-4 flex-shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold uppercase tracking-widest border border-gray-300 text-gray-500 rounded hover:border-brand-navy hover:text-brand-navy transition-colors">
<button onclick={() => (editMode = true)} class="ml-4 flex-shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold uppercase tracking-widest border border-gray-300 text-gray-500 rounded hover:border-brand-navy hover:text-brand-navy transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Bearbeiten
</button>
@@ -150,7 +154,7 @@
name="_targetPersonDisplay"
label="Zusammenführen mit"
value={mergeTargetId}
on:change={(e) => { mergeTargetId = e.detail.value; showMergeConfirm = false; }}
onchange={(value) => { mergeTargetId = value; showMergeConfirm = false; }}
/>
</div>
@@ -158,7 +162,7 @@
<button
type="button"
disabled={!mergeTargetId}
on:click={() => showMergeConfirm = true}
onclick={() => (showMergeConfirm = true)}
class="px-4 py-2 text-sm font-bold uppercase tracking-widest border border-red-300 text-red-600 rounded hover:bg-red-50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
>
Zusammenführen
@@ -173,7 +177,7 @@
</button>
<button
type="button"
on:click={() => showMergeConfirm = false}
onclick={() => (showMergeConfirm = false)}
class="px-4 py-2 text-sm font-bold uppercase tracking-widest border border-gray-300 text-gray-500 rounded hover:bg-gray-50 transition-colors"
>
Abbrechen

View File

@@ -1,5 +1,5 @@
<script lang="ts">
export let form;
let { form } = $props();
</script>
<div class="mx-auto max-w-2xl px-4 py-8">