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

@@ -2,15 +2,17 @@
import { enhance } from '$app/forms';
import { slide } from 'svelte/transition';
export let data;
export let form;
let { data, form } = $props();
let activeTab = 'users';
let editingTagId: string | null = null;
let editingTagName = '';
let editingUserId: string | null = null;
let activeTab = $state('users');
let editingTagId: string | null = $state(null);
let editingTagName = $state('');
let editingUserId: string | null = $state(null);
let editingGroupId: string | null = $state(null);
function startEditTag(tag: any) {
const availablePermissions = ['WRITE_ALL', 'ADMIN', 'ADMIN_USER', 'ADMIN_TAG', 'ADMIN_PERMISSION'];
function startEditTag(tag: { id: string; name: string }) {
editingTagId = tag.id;
editingTagName = tag.name;
}
@@ -20,7 +22,7 @@
editingTagName = '';
}
function startEditUser(id: string) {
function startEditUser(id: string) {
editingUserId = id;
}
@@ -28,9 +30,6 @@
editingUserId = null;
}
let editingGroupId: string | null = null;
const availablePermissions = ['WRITE_ALL', 'ADMIN', 'ADMIN_USER', 'ADMIN_TAG', 'ADMIN_PERMISSION'];
function startEditGroup(id: string) {
editingGroupId = id;
}
@@ -51,21 +50,21 @@
'users'
? 'bg-brand-navy text-white'
: 'text-gray-500 hover:text-brand-navy'}"
on:click={() => (activeTab = 'users')}>Benutzer</button
onclick={() => (activeTab = 'users')}>Benutzer</button
>
<button
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
'groups'
? 'bg-brand-navy text-white'
: 'text-gray-500 hover:text-brand-navy'}"
on:click={() => (activeTab = 'groups')}>Gruppen</button
onclick={() => (activeTab = 'groups')}>Gruppen</button
>
<button
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
'tags'
? 'bg-brand-navy text-white'
: 'text-gray-500 hover:text-brand-navy'}"
on:click={() => (activeTab = 'tags')}>Schlagworte</button
onclick={() => (activeTab = 'tags')}>Schlagworte</button
>
</div>
</div>
@@ -106,7 +105,6 @@
<!-- === EDIT MODE === -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{user.username}
<!-- Hidden ID Input for the form -->
<input
type="hidden"
name="username"
@@ -116,7 +114,6 @@
</td>
<td class="px-6 py-4 text-sm">
<!-- Groups Select -->
<select
name="groupIds"
multiple
@@ -126,7 +123,7 @@
{#each data.groups as group}
<option
value={group.id}
selected={user.groups.some((g) => g.id === group.id)}
selected={user.groups.some((g: { id: string }) => g.id === group.id)}
>
{group.name}
</option>
@@ -136,7 +133,6 @@
</td>
<td class="px-6 py-4 whitespace-nowrap text-right align-top">
<!-- Password & Buttons -->
<form
id="edit-form-{user.id}"
method="POST"
@@ -164,7 +160,7 @@
</button>
<button
type="button"
on:click={cancelEditUser}
onclick={cancelEditUser}
class="bg-gray-200 text-gray-600 px-2 py-1 rounded text-xs font-bold uppercase hover:bg-gray-300"
>
Abbrechen
@@ -194,15 +190,13 @@
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
<div class="flex items-center justify-end gap-4">
<!-- Edit Button -->
<button
on:click={() => startEditUser(user.id)}
onclick={() => startEditUser(user.id)}
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
>
Bearbeiten
</button>
<!-- Delete Button -->
<form
method="POST"
action="?/deleteUser"
@@ -265,7 +259,6 @@
class="rounded border-gray-300 text-sm w-full"
/>
<!-- Multi-Select for Groups -->
<div class="md:col-span-3">
<select
name="groupIds"
@@ -290,7 +283,6 @@
</div>
</div>
{:else if activeTab === 'tags'}
<!-- TAGS SECTION (unchanged logic, just ensuring style consistency) -->
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
<div class="p-6 border-b border-gray-100 bg-yellow-50/50">
<h2 class="text-lg font-bold text-gray-700">Schlagworte</h2>
@@ -332,9 +324,9 @@
>
<button
type="button"
on:click={cancelEditTag}
onclick={cancelEditTag}
aria-label="Abbrechen"
class="text-gray-400 hover:text-gray-600"
class="text-gray-400 hover:text-gray-600"
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
@@ -353,7 +345,7 @@
class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"
>
<button
on:click={() => startEditTag(tag)}
onclick={() => startEditTag(tag)}
aria-label="Schlagwort bearbeiten"
class="p-1 text-gray-400 hover:text-brand-navy"
>
@@ -370,16 +362,13 @@
method="POST"
action="?/deleteTag"
use:enhance={({ cancel }) => {
// This runs BEFORE the request is sent
if (
!confirm(
'Wirklich löschen? Das Schlagwort wird aus allen Dokumenten entfernt.'
)
) {
cancel(); // Stop the request
cancel();
}
// This runs AFTER the server responds
return async ({ update }) => {
await update();
};
@@ -443,7 +432,6 @@
>
<input type="hidden" name="id" value={group.id} />
<!-- Name Input -->
<div class="w-full sm:w-1/3">
<input
type="text"
@@ -454,7 +442,6 @@
/>
</div>
<!-- Permissions Checkboxes -->
<div class="flex-1 flex flex-wrap gap-4 items-center h-full pt-2">
{#each availablePermissions as perm}
<label
@@ -472,7 +459,6 @@
{/each}
</div>
<!-- Actions -->
<div class="flex gap-2 self-start sm:self-center">
<button type="submit" aria-label="Speichern" class="text-green-600 hover:text-green-800 p-1">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
@@ -486,9 +472,9 @@
</button>
<button
type="button"
on:click={cancelEditGroup}
onclick={cancelEditGroup}
aria-label="Abbrechen"
class="text-gray-400 hover:text-red-500 p-1"
class="text-gray-400 hover:text-red-500 p-1"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
@@ -524,7 +510,7 @@
<td class="px-6 py-4 whitespace-nowrap text-right">
<div class="flex items-center justify-end gap-3">
<button
on:click={() => startEditGroup(group.id)}
onclick={() => startEditGroup(group.id)}
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
>
Bearbeiten
@@ -537,7 +523,6 @@
if (!confirm('Gruppe wirklich löschen?')) {
cancel();
}
return async ({ update }) => {
await update();
};