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

@@ -3,21 +3,19 @@ import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
import { goto } from '$app/navigation';
import TagInput from '$lib/components/TagInput.svelte';
import { slide } from 'svelte/transition';
import { untrack } from 'svelte';
export let data;
let { data } = $props();
// Local state variables
let q = data.filters?.q || '';
let from = data.filters?.from || '';
let to = data.filters?.to || '';
let senderId = data.filters?.senderId || '';
let receiverId = data.filters?.receiverId || '';
let tagNames = data.filters?.tags || [];
let q = $state(untrack(() => data.filters?.q || ''));
let from = $state(untrack(() => data.filters?.from || ''));
let to = $state(untrack(() => data.filters?.to || ''));
let senderId = $state(untrack(() => data.filters?.senderId || ''));
let receiverId = $state(untrack(() => data.filters?.receiverId || ''));
let tagNames = $state<string[]>(untrack(() => data.filters?.tags || []));
// Debounce Timer
let searchTimer: any;
let showAdvanced = false;
let searchTimer: ReturnType<typeof setTimeout>;
let showAdvanced = $state(false);
function triggerSearch() {
const params = new URLSearchParams();
@@ -42,27 +40,24 @@ function handleTextSearch() {
}, 500);
}
let previousTags = tagNames.join(',');
$: {
const currentTags = tagNames.join(',');
if (currentTags !== previousTags) {
previousTags = currentTags;
// Trigger search when tags change
let prevTagStr = untrack(() => tagNames.join(','));
$effect(() => {
const cur = tagNames.join(',');
if (cur !== prevTagStr) {
prevTagStr = cur;
triggerSearch();
}
}
});
function toggleAdvanced() {
showAdvanced = !showAdvanced;
}
// Sync with server data (e.g. after reset)
$: {
// Sync local state with server data after navigation
$effect(() => {
q = data.filters?.q || '';
from = data.filters?.from || '';
to = data.filters?.to || '';
senderId = data.filters?.senderId || '';
receiverId = data.filters?.receiverId || '';
}
});
</script>
<!-- Outer Container: Matches the 'Sand' background of the layout -->
@@ -76,7 +71,7 @@ $: {
<input
type="text"
bind:value={q}
on:input={handleTextSearch}
oninput={handleTextSearch}
placeholder="Suche in Titel, Inhalt, Ort..."
class="block w-full border-gray-300 py-2.5 pr-10 pl-3 placeholder-gray-400 shadow-sm focus:border-brand-navy focus:ring-brand-navy"
/>
@@ -94,7 +89,7 @@ $: {
<!-- Toggle Advanced Button -->
<button
on:click={toggleAdvanced}
onclick={() => (showAdvanced = !showAdvanced)}
class="flex items-center gap-2 border border-gray-300 bg-gray-50 px-4 py-2.5 text-sm font-bold tracking-wide text-gray-600 uppercase transition hover:bg-gray-100 hover:text-brand-navy"
>
<svg
@@ -143,7 +138,7 @@ $: {
<p class="mb-2 block text-xs font-bold tracking-widest text-gray-500 uppercase">
Schlagworte
</p>
<TagInput bind:tags={tagNames} allowCreation={false} on:change={triggerSearch} />
<TagInput bind:tags={tagNames} allowCreation={false} />
</div>
<!-- Sender -->
@@ -156,7 +151,7 @@ $: {
label="Absender"
bind:value={senderId}
initialName={data.initialValues?.senderName}
on:change={triggerSearch}
onchange={triggerSearch}
/>
</div>
</div>
@@ -171,7 +166,7 @@ $: {
label="Empfänger"
bind:value={receiverId}
initialName={data.initialValues?.receiverName}
on:change={triggerSearch}
onchange={triggerSearch}
/>
</div>
</div>
@@ -188,7 +183,7 @@ $: {
type="date"
id="from"
bind:value={from}
on:change={triggerSearch}
onchange={triggerSearch}
class="block w-full border-gray-300 py-2.5 text-sm shadow-sm"
/>
</div>
@@ -202,7 +197,7 @@ $: {
type="date"
id="to"
bind:value={to}
on:change={triggerSearch}
onchange={triggerSearch}
class="block w-full border-gray-300 py-2.5 text-sm shadow-sm"
/>
</div>
@@ -329,15 +324,14 @@ $: {
</div>
</div>
<!-- NEW: Tags Display -->
<!-- Tags Display -->
{#if doc.tags && doc.tags.length > 0}
<div class="mt-4 flex flex-wrap gap-2 pt-3">
{#each doc.tags as tag}
<button
type="button"
class="relative z-10 inline-flex items-center rounded bg-brand-sand/30 px-2 py-1 text-[10px] font-bold tracking-widest text-brand-navy uppercase transition-colors hover:bg-brand-navy hover:text-white"
on:click|preventDefault|stopPropagation={() =>
goto(`/?tag=${encodeURIComponent(tag.name)}`)}
onclick={(e) => { e.preventDefault(); e.stopPropagation(); goto(`/?tag=${encodeURIComponent(tag.name)}`); }}
>
{tag.name}
</button>
@@ -384,7 +378,7 @@ $: {
Versuchen Sie, die Filter anzupassen oder den Suchbegriff zu ändern.
</p>
<button
on:click={() => goto('/')}
onclick={() => goto('/')}
class="mt-6 text-sm font-bold tracking-wide text-brand-mint uppercase transition hover:text-brand-navy"
>
Alle Filter löschen