From dc4db405d2991d3ec6a114f3b08d3fccd057d3ee Mon Sep 17 00:00:00 2001
From: Wolfgang Müller
Date: Thu, 13 Feb 2025 17:52:16 +0100
Subject: frontend: Migrate to Svelte 5

---
 frontend/src/lib/toolbar/DeleteSelection.svelte    | 24 +++++----
 frontend/src/lib/toolbar/EditSelection.svelte      | 22 ++++----
 frontend/src/lib/toolbar/FilterBookmarked.svelte   | 15 +++---
 frontend/src/lib/toolbar/FilterFavourites.svelte   | 14 ++---
 frontend/src/lib/toolbar/FilterOrganized.svelte    | 18 +++----
 frontend/src/lib/toolbar/MarkBookmark.svelte       | 18 +++----
 frontend/src/lib/toolbar/MarkFavourite.svelte      | 18 +++----
 frontend/src/lib/toolbar/MarkOrganized.svelte      | 18 +++----
 frontend/src/lib/toolbar/MarkSelection.svelte      | 37 +++++++------
 frontend/src/lib/toolbar/Search.svelte             | 14 ++---
 frontend/src/lib/toolbar/SelectItems.svelte        | 21 ++++----
 frontend/src/lib/toolbar/SelectSort.svelte         | 58 +++++++++++---------
 frontend/src/lib/toolbar/SelectionControls.svelte  | 61 ++++++++++++----------
 .../src/lib/toolbar/ToggleAdvancedFilters.svelte   | 33 ++++++------
 frontend/src/lib/toolbar/Toolbar.svelte            | 38 +++++++-------
 15 files changed, 214 insertions(+), 195 deletions(-)

(limited to 'frontend/src/lib/toolbar')

diff --git a/frontend/src/lib/toolbar/DeleteSelection.svelte b/frontend/src/lib/toolbar/DeleteSelection.svelte
index 7459a87..7b37313 100644
--- a/frontend/src/lib/toolbar/DeleteSelection.svelte
+++ b/frontend/src/lib/toolbar/DeleteSelection.svelte
@@ -1,26 +1,28 @@
 <script lang="ts">
 	import type { DeleteMutation } from '$gql/Mutations';
-	import { getSelectionContext } from '$lib/Selection';
+	import DeleteButton from '$lib/components/DeleteButton.svelte';
+	import { getSelectionContext } from '$lib/selection/Selection.svelte';
 	import { toastFinally } from '$lib/Toasts';
 	import { confirmDeletion } from '$lib/Utils';
-	import DeleteButton from '$lib/components/DeleteButton.svelte';
 	import { getContextClient } from '@urql/svelte';
 
 	const client = getContextClient();
-	const selection = getSelectionContext();
 
-	export let mutation: DeleteMutation;
-	export let warning: string | undefined = undefined;
+	interface Props {
+		mutation: DeleteMutation;
+		warning?: string;
+	}
+
+	let { mutation, warning = undefined }: Props = $props();
+	let selection = getSelectionContext();
 
-	function remove() {
+	function onclick() {
 		const mutate = () => {
-			mutation(client, { ids: $selection.ids })
-				.then(() => ($selection = $selection.clear()))
-				.catch(toastFinally);
+			mutation(client, { ids: selection.ids }).then(selection.clear).catch(toastFinally);
 		};
 
-		confirmDeletion($selection.typename, $selection.names, mutate, warning);
+		confirmDeletion(selection.typename, selection.names, mutate, warning);
 	}
 </script>
 
-<DeleteButton on:click={remove} />
+<DeleteButton {onclick} />
diff --git a/frontend/src/lib/toolbar/EditSelection.svelte b/frontend/src/lib/toolbar/EditSelection.svelte
index 50e6656..1803ed4 100644
--- a/frontend/src/lib/toolbar/EditSelection.svelte
+++ b/frontend/src/lib/toolbar/EditSelection.svelte
@@ -1,20 +1,19 @@
 <script lang="ts">
-	import { getSelectionContext } from '$lib/Selection';
+	import { getSelectionContext } from '$lib/selection/Selection.svelte';
 	import { accelerator } from '$lib/Shortcuts';
-	import type { SvelteComponent } from 'svelte';
-	import { openModal } from 'svelte-modals';
+	import { toastFinally } from '$lib/Toasts';
+	import { modals, type ModalComponent, type ModalProps } from 'svelte-modals';
 
 	const selection = getSelectionContext();
 
-	export let dialog: typeof SvelteComponent<{
-		isOpen: boolean;
+	interface DialogProps extends ModalProps {
 		ids: number[];
-	}>;
+	}
+
+	let { dialog }: { dialog: ModalComponent<DialogProps> } = $props();
 
 	function edit() {
-		openModal(dialog, {
-			ids: $selection.ids
-		});
+		modals.open(dialog, { ids: selection.ids }).catch(toastFinally);
 	}
 </script>
 
@@ -22,8 +21,9 @@
 	type="button"
 	class="btn-slate hover:bg-blue-700"
 	title="Edit selection"
-	on:click={edit}
+	aria-label="Edit selection"
+	onclick={edit}
 	use:accelerator={'e'}
 >
-	<span class="icon-base icon-[material-symbols--edit]" />
+	<span class="icon-base icon-[material-symbols--edit]"></span>
 </button>
diff --git a/frontend/src/lib/toolbar/FilterBookmarked.svelte b/frontend/src/lib/toolbar/FilterBookmarked.svelte
index bcbe295..76403ec 100644
--- a/frontend/src/lib/toolbar/FilterBookmarked.svelte
+++ b/frontend/src/lib/toolbar/FilterBookmarked.svelte
@@ -1,15 +1,16 @@
 <script lang="ts">
-	import { page } from '$app/stores';
-	import { ComicFilterContext, cycleBooleanFilter, getFilterContext } from '$lib/Filter';
+	import { page } from '$app/state';
+	import { cycleBooleanFilter, type ComicFilterContext } from '$lib/Filter.svelte';
+
 	import { accelerator } from '$lib/Shortcuts';
 	import Bookmark from '$lib/icons/Bookmark.svelte';
 
-	const filter = getFilterContext<ComicFilterContext>();
-	$: bookmarked = $filter.include.controls.bookmarked.value;
+	let { filter }: { filter: ComicFilterContext } = $props();
+	let bookmarked = $derived(filter.include.bookmarked.value);
 
 	const toggle = () => {
-		$filter.include.controls.bookmarked.value = cycleBooleanFilter(bookmarked, false);
-		$filter.apply($page.url.searchParams);
+		filter.include.bookmarked.value = cycleBooleanFilter(bookmarked, false);
+		filter.apply(page.url.searchParams);
 	};
 </script>
 
@@ -17,7 +18,7 @@
 	class:toggled={bookmarked}
 	class="btn-slate"
 	title="Filter bookmarked"
-	on:click={toggle}
+	onclick={toggle}
 	use:accelerator={'b'}
 >
 	<Bookmark {bookmarked} />
diff --git a/frontend/src/lib/toolbar/FilterFavourites.svelte b/frontend/src/lib/toolbar/FilterFavourites.svelte
index 6591cef..5e9beb7 100644
--- a/frontend/src/lib/toolbar/FilterFavourites.svelte
+++ b/frontend/src/lib/toolbar/FilterFavourites.svelte
@@ -1,15 +1,15 @@
 <script lang="ts">
-	import { page } from '$app/stores';
-	import { ComicFilterContext, cycleBooleanFilter, getFilterContext } from '$lib/Filter';
+	import { page } from '$app/state';
+	import { ComicFilterContext, cycleBooleanFilter } from '$lib/Filter.svelte';
 	import { accelerator } from '$lib/Shortcuts';
 	import Star from '$lib/icons/Star.svelte';
 
-	const filter = getFilterContext<ComicFilterContext>();
-	$: favourite = $filter.include.controls.favourite.value;
+	let { filter }: { filter: ComicFilterContext } = $props();
+	let favourite = $derived(filter.include.favourite.value);
 
 	const toggle = () => {
-		$filter.include.controls.favourite.value = cycleBooleanFilter(favourite, false);
-		$filter.apply($page.url.searchParams);
+		filter.include.favourite.value = cycleBooleanFilter(favourite, false);
+		filter.apply(page.url.searchParams);
 	};
 </script>
 
@@ -17,7 +17,7 @@
 	class:toggled={favourite}
 	class="btn-slate"
 	title="Filter favourites"
-	on:click={toggle}
+	onclick={toggle}
 	use:accelerator={'f'}
 >
 	<Star {favourite} />
diff --git a/frontend/src/lib/toolbar/FilterOrganized.svelte b/frontend/src/lib/toolbar/FilterOrganized.svelte
index 754e663..0f95e5f 100644
--- a/frontend/src/lib/toolbar/FilterOrganized.svelte
+++ b/frontend/src/lib/toolbar/FilterOrganized.svelte
@@ -1,20 +1,20 @@
 <script lang="ts">
-	import { page } from '$app/stores';
+	import { page } from '$app/state';
 	import {
 		ArchiveFilterContext,
-		ComicFilterContext,
 		cycleBooleanFilter,
-		getFilterContext
-	} from '$lib/Filter';
+		type ComicFilterContext
+	} from '$lib/Filter.svelte';
+
 	import { accelerator } from '$lib/Shortcuts';
 	import Organized from '$lib/icons/Organized.svelte';
 
-	const filter = getFilterContext<ArchiveFilterContext | ComicFilterContext>();
-	$: organized = $filter.include.controls.organized.value;
+	let { filter }: { filter: ComicFilterContext | ArchiveFilterContext } = $props();
+	let organized = $derived(filter.include.organized.value);
 
 	const toggle = () => {
-		$filter.include.controls.organized.value = cycleBooleanFilter(organized);
-		$filter.apply($page.url.searchParams);
+		filter.include.organized.value = cycleBooleanFilter(organized);
+		filter.apply(page.url.searchParams);
 	};
 </script>
 
@@ -23,7 +23,7 @@
 	class:toggled={organized !== undefined}
 	class="btn-slate"
 	title="Filter organized"
-	on:click={toggle}
+	onclick={toggle}
 	use:accelerator={'o'}
 >
 	<Organized tristate {organized} />
diff --git a/frontend/src/lib/toolbar/MarkBookmark.svelte b/frontend/src/lib/toolbar/MarkBookmark.svelte
index 792b84f..776ddd8 100644
--- a/frontend/src/lib/toolbar/MarkBookmark.svelte
+++ b/frontend/src/lib/toolbar/MarkBookmark.svelte
@@ -1,27 +1,25 @@
 <script lang="ts">
-	import { getSelectionContext } from '$lib/Selection';
-	import { toastFinally } from '$lib/Toasts';
+	import type { MutationWith } from '$gql/Utils';
 	import Bookmark from '$lib/icons/Bookmark.svelte';
-	import { Client, getContextClient } from '@urql/svelte';
+	import { getSelectionContext } from '$lib/selection/Selection.svelte';
+	import { toastFinally } from '$lib/Toasts';
+	import { getContextClient } from '@urql/svelte';
 
 	const client = getContextClient();
 	const selection = getSelectionContext();
 
-	export let mutation: (
-		client: Client,
-		args: { ids: number[]; input: { bookmarked: boolean } }
-	) => Promise<unknown>;
+	let { mutation }: { mutation: MutationWith<{ bookmarked: boolean }> } = $props();
 
 	function mutate(bookmarked: boolean) {
-		mutation(client, { ids: $selection.ids, input: { bookmarked } }).catch(toastFinally);
+		mutation(client, { ids: selection.ids, input: { bookmarked } }).catch(toastFinally);
 	}
 </script>
 
-<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(true)}>
+<button type="button" class="btn-slate flex justify-start gap-1" onclick={() => mutate(true)}>
 	<Bookmark bookmarked={true} />
 	<span>Bookmark</span>
 </button>
-<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(false)}>
+<button type="button" class="btn-slate flex justify-start gap-1" onclick={() => mutate(false)}>
 	<Bookmark bookmarked={false} />
 	<span>Unbookmark</span>
 </button>
diff --git a/frontend/src/lib/toolbar/MarkFavourite.svelte b/frontend/src/lib/toolbar/MarkFavourite.svelte
index 42eaa39..1af5d60 100644
--- a/frontend/src/lib/toolbar/MarkFavourite.svelte
+++ b/frontend/src/lib/toolbar/MarkFavourite.svelte
@@ -1,27 +1,25 @@
 <script lang="ts">
-	import { getSelectionContext } from '$lib/Selection';
-	import { toastFinally } from '$lib/Toasts';
+	import type { MutationWith } from '$gql/Utils';
 	import Star from '$lib/icons/Star.svelte';
-	import { Client, getContextClient } from '@urql/svelte';
+	import { getSelectionContext } from '$lib/selection/Selection.svelte';
+	import { toastFinally } from '$lib/Toasts';
+	import { getContextClient } from '@urql/svelte';
 
 	const client = getContextClient();
 	const selection = getSelectionContext();
 
-	export let mutation: (
-		client: Client,
-		args: { ids: number[]; input: { favourite: boolean } }
-	) => Promise<unknown>;
+	let { mutation }: { mutation: MutationWith<{ favourite: boolean }> } = $props();
 
 	function mutate(favourite: boolean) {
-		mutation(client, { ids: $selection.ids, input: { favourite } }).catch(toastFinally);
+		mutation(client, { ids: selection.ids, input: { favourite } }).catch(toastFinally);
 	}
 </script>
 
-<button type="button" class="btn-slate justify-start gap-1" on:click={() => mutate(true)}>
+<button type="button" class="btn-slate justify-start gap-1" onclick={() => mutate(true)}>
 	<Star favourite={true} />
 	<span>Favourite</span>
 </button>
-<button type="button" class="btn-slate justify-start gap-1" on:click={() => mutate(false)}>
+<button type="button" class="btn-slate justify-start gap-1" onclick={() => mutate(false)}>
 	<Star favourite={false} />
 	<span>Unfavourite</span>
 </button>
diff --git a/frontend/src/lib/toolbar/MarkOrganized.svelte b/frontend/src/lib/toolbar/MarkOrganized.svelte
index 4dc3a83..63c8622 100644
--- a/frontend/src/lib/toolbar/MarkOrganized.svelte
+++ b/frontend/src/lib/toolbar/MarkOrganized.svelte
@@ -1,27 +1,25 @@
 <script lang="ts">
-	import { getSelectionContext } from '$lib/Selection';
-	import { toastFinally } from '$lib/Toasts';
+	import type { MutationWith } from '$gql/Utils';
 	import Organized from '$lib/icons/Organized.svelte';
-	import { Client, getContextClient } from '@urql/svelte';
+	import { getSelectionContext } from '$lib/selection/Selection.svelte';
+	import { toastFinally } from '$lib/Toasts';
+	import { getContextClient } from '@urql/svelte';
 
 	const client = getContextClient();
 	const selection = getSelectionContext();
 
-	export let mutation: (
-		client: Client,
-		args: { ids: number[]; input: { organized: boolean } }
-	) => Promise<unknown>;
+	let { mutation }: { mutation: MutationWith<{ organized: boolean }> } = $props();
 
 	function mutate(organized: boolean) {
-		mutation(client, { ids: $selection.ids, input: { organized } }).catch(toastFinally);
+		mutation(client, { ids: selection.ids, input: { organized } }).catch(toastFinally);
 	}
 </script>
 
-<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(true)}>
+<button type="button" class="btn-slate flex justify-start gap-1" onclick={() => mutate(true)}>
 	<Organized tristate organized={true} />
 	<span>Organized</span>
 </button>
-<button type="button" class="btn-slate flex justify-start gap-1" on:click={() => mutate(false)}>
+<button type="button" class="btn-slate flex justify-start gap-1" onclick={() => mutate(false)}>
 	<Organized dim tristate organized={false} />
 	<span>Unorganized</span>
 </button>
diff --git a/frontend/src/lib/toolbar/MarkSelection.svelte b/frontend/src/lib/toolbar/MarkSelection.svelte
index 27eb2c7..1af36ca 100644
--- a/frontend/src/lib/toolbar/MarkSelection.svelte
+++ b/frontend/src/lib/toolbar/MarkSelection.svelte
@@ -1,24 +1,23 @@
 <script lang="ts">
 	import Dropdown from '$lib/components/Dropdown.svelte';
+	import type { Snippet } from 'svelte';
 
-	let visible = false;
-	let button: HTMLElement;
+	let { children }: { children: Snippet } = $props();
 </script>
 
-<div class="relative">
-	<button
-		type="button"
-		class="btn-slate rounded-inherit relative hover:bg-blue-700 [&:not(:only-child)]:bg-blue-700"
-		title="Set flag..."
-		bind:this={button}
-		on:click={() => (visible = !visible)}
-	>
-		<span class="icon-base icon-[material-symbols--flag] pointer-events-none" />
-	</button>
-
-	<Dropdown parent={button} bind:visible>
-		<div class="grid grid-cols-[min-content_min-content] gap-1">
-			<slot />
-		</div>
-	</Dropdown>
-</div>
+<Dropdown>
+	{#snippet button(onclick)}
+		<button
+			type="button"
+			class="btn-slate rounded-inherit relative hover:bg-blue-700 [&:not(:only-child)]:bg-blue-700"
+			title="Set flag..."
+			aria-label="Set flag..."
+			{onclick}
+		>
+			<span class="icon-base icon-[material-symbols--flag] pointer-events-none"></span>
+		</button>
+	{/snippet}
+	<div class="grid grid-cols-[min-content_min-content] gap-1">
+		{@render children?.()}
+	</div>
+</Dropdown>
diff --git a/frontend/src/lib/toolbar/Search.svelte b/frontend/src/lib/toolbar/Search.svelte
index f033258..4806971 100644
--- a/frontend/src/lib/toolbar/Search.svelte
+++ b/frontend/src/lib/toolbar/Search.svelte
@@ -1,13 +1,15 @@
 <script lang="ts">
-	import { page } from '$app/stores';
+	import { page } from '$app/state';
 	import { debounce } from '$lib/Actions';
-	import { BasicFilterContext, getFilterContext } from '$lib/Filter';
 	import { accelerator } from '$lib/Shortcuts';
 
-	const filter = getFilterContext<BasicFilterContext>();
+	interface Props {
+		name: string;
+		field: string;
+		filter: { apply: (params: URLSearchParams) => void };
+	}
 
-	export let name: string;
-	export let field: string;
+	let { name, field = $bindable(), filter }: Props = $props();
 </script>
 
 <input
@@ -16,6 +18,6 @@
 	class="btn-slate w-min"
 	placeholder="Search {name}..."
 	bind:value={field}
-	use:debounce={{ callback: () => $filter.apply($page.url.searchParams) }}
+	use:debounce={{ callback: () => filter.apply(page.url.searchParams) }}
 	use:accelerator={'F'}
 />
diff --git a/frontend/src/lib/toolbar/SelectItems.svelte b/frontend/src/lib/toolbar/SelectItems.svelte
index 7ff339e..68a0652 100644
--- a/frontend/src/lib/toolbar/SelectItems.svelte
+++ b/frontend/src/lib/toolbar/SelectItems.svelte
@@ -1,18 +1,19 @@
 <script lang="ts">
-	import { page } from '$app/stores';
-	import { getPaginationContext } from '$lib/Pagination';
+	import { page } from '$app/state';
+	import { navigate, type PaginationData } from '$lib/Navigation';
 
-	const pagination = getPaginationContext();
+	let { pagination }: { pagination: PaginationData } = $props();
 
-	$: values = new Set([24, 48, 72, 90, 120, 150, 180, $pagination.items].sort((a, b) => a - b));
+	let values = $derived(
+		new Set([24, 48, 72, 90, 120, 150, 180, pagination.items].sort((a, b) => a - b))
+	);
+
+	function onchange(e: Event & { currentTarget: EventTarget & HTMLSelectElement }) {
+		navigate({ pagination: { items: +e.currentTarget.value } }, page.url.searchParams);
+	}
 </script>
 
-<select
-	class="btn-slate"
-	bind:value={$pagination.items}
-	on:change={() => $pagination.apply($page.url.searchParams)}
-	title="Limit displayed items to..."
->
+<select class="btn-slate" value={pagination.items} {onchange} title="Limit displayed items to...">
 	{#each values as value}
 		<option {value}>{value}</option>
 	{/each}
diff --git a/frontend/src/lib/toolbar/SelectSort.svelte b/frontend/src/lib/toolbar/SelectSort.svelte
index fdcb057..0e59df6 100644
--- a/frontend/src/lib/toolbar/SelectSort.svelte
+++ b/frontend/src/lib/toolbar/SelectSort.svelte
@@ -1,60 +1,68 @@
 <script lang="ts">
-	import { page } from '$app/stores';
+	import { page } from '$app/state';
 	import { SortDirection } from '$gql/graphql';
-
-	import { getSortContext } from '$lib/Sort';
+	import { navigate, type SortData } from '$lib/Navigation';
 	import { slideXFast } from '$lib/Transitions';
 	import { getRandomInt } from '$lib/Utils';
 	import { slide } from 'svelte/transition';
 
-	const sort = getSortContext();
+	let { sort, labels }: { sort: SortData<string>; labels: Record<string, string> } = $props();
+
+	function apply(sort: SortData<string>) {
+		navigate({ sort }, page.url.searchParams);
+	}
 
 	function toggle() {
-		if ($sort.direction === SortDirection.Ascending) {
-			$sort.direction = SortDirection.Descending;
+		if (sort.direction === SortDirection.Ascending) {
+			apply({ ...sort, direction: SortDirection.Descending });
 		} else {
-			$sort.direction = SortDirection.Ascending;
+			apply({ ...sort, direction: SortDirection.Ascending });
 		}
+	}
 
-		apply();
+	function newSeed() {
+		return getRandomInt(0, 1000000000);
 	}
 
-	function apply() {
-		if ($sort.on === 'RANDOM' && $sort.seed === undefined) {
-			$sort.seed = getRandomInt(0, 1000000000);
-		}
-		$sort.apply($page.url.searchParams);
+	function shuffle() {
+		apply({ ...sort, seed: newSeed() });
 	}
 
-	function reshuffle() {
-		$sort.seed = undefined;
-		apply();
+	function onchange(e: Event & { currentTarget: EventTarget & HTMLSelectElement }) {
+		let seed: number | undefined = undefined;
+
+		if (e.currentTarget.value === 'RANDOM') {
+			seed = newSeed();
+		}
+
+		apply({ ...sort, on: e.currentTarget.value, seed });
 	}
 </script>
 
 <div class="rounded-group flex flex-row">
-	<select class="btn-slate" bind:value={$sort.on} on:change={apply} title="Sort on...">
-		{#each Object.entries($sort.labels) as [value, label]}
+	<select class="btn-slate" value={sort.on} {onchange} title="Sort on...">
+		{#each Object.entries(labels) as [value, label]}
 			<option {value}>{label}</option>
 		{/each}
 	</select>
-	<button type="button" class="btn-slate" title="Toggle sort direction" on:click={toggle}>
-		{#if $sort.direction === SortDirection.Ascending}
-			<span class="icon-base icon-[material-symbols--sort] -scale-y-100" />
+	<button type="button" class="btn-slate" title="Toggle sort direction" onclick={toggle}>
+		{#if sort.direction === SortDirection.Ascending}
+			<span class="icon-base icon-[material-symbols--sort] -scale-y-100"></span>
 		{:else}
-			<span class="icon-base icon-[material-symbols--sort]" />
+			<span class="icon-base icon-[material-symbols--sort]"></span>
 		{/if}
 	</button>
-	{#if $sort.on === 'RANDOM'}
+	{#if sort.on === 'RANDOM'}
 		<button
 			type="button"
 			class="btn-slate"
 			title="Reshuffle"
-			on:click={reshuffle}
+			aria-label="Reshuffle"
+			onclick={shuffle}
 			transition:slide={slideXFast}
 		>
 			<div class="flex">
-				<span class="icon-base icon-[material-symbols--shuffle]" />
+				<span class="icon-base icon-[material-symbols--shuffle]"></span>
 			</div>
 		</button>
 	{/if}
diff --git a/frontend/src/lib/toolbar/SelectionControls.svelte b/frontend/src/lib/toolbar/SelectionControls.svelte
index 4d309df..f0026c8 100644
--- a/frontend/src/lib/toolbar/SelectionControls.svelte
+++ b/frontend/src/lib/toolbar/SelectionControls.svelte
@@ -1,57 +1,64 @@
 <script lang="ts">
-	import { getSelectionContext } from '$lib/Selection';
+	import Badge from '$lib/components/Badge.svelte';
+	import { getSelectionContext } from '$lib/selection/Selection.svelte';
 	import { accelerator } from '$lib/Shortcuts';
 	import { fadeDefault, slideXFast } from '$lib/Transitions';
-	import Badge from '$lib/components/Badge.svelte';
-	import { onDestroy } from 'svelte';
+	import { onDestroy, type Snippet } from 'svelte';
 	import { fade, slide } from 'svelte/transition';
 
-	const selection = getSelectionContext();
-
-	export let page = false;
-
-	const toggle = () => ($selection = $selection.toggle());
-	const all = () => ($selection = $selection.all());
-	const none = () => ($selection = $selection.none());
+	let { page = false, children }: { page?: boolean; children?: Snippet } = $props();
+	let selection = getSelectionContext();
 
-	onDestroy(() => ($selection = $selection.clear()));
+	onDestroy(selection.clear);
 </script>
 
 <div class="rounded-group flex">
 	<button
 		type="button"
 		class="btn-slate relative"
-		class:toggled={$selection.active}
-		title={`${$selection.active ? 'Exit' : 'Enter'} ${page ? 'page ' : ' '}selection mode`}
-		on:click={toggle}
+		class:toggled={selection.active}
+		title={`${selection.active ? 'Exit' : 'Enter'} ${page ? 'page ' : ' '}selection mode`}
+		onclick={selection.toggle}
 		use:accelerator={'s'}
 	>
-		{#if $selection.active}
+		{#if selection.active}
 			{#if page}
-				<span class="icon-base icon-[material-symbols--edit-document]" />
+				<span class="icon-base icon-[material-symbols--edit-document]"></span>
 			{:else}
-				<span class="icon-base icon-[material-symbols--remove-selection]" />
+				<span class="icon-base icon-[material-symbols--remove-selection]"></span>
 			{/if}
 		{:else if page}
-			<span class="icon-base icon-[material-symbols--edit-document-outline]" />
+			<span class="icon-base icon-[material-symbols--edit-document-outline]"></span>
 		{:else}
-			<span class="icon-base icon-[material-symbols--select]" />
+			<span class="icon-base icon-[material-symbols--select]"></span>
 		{/if}
-		<Badge number={$selection.size} />
+		<Badge number={selection.size} />
 	</button>
-	{#if $selection.active}
+	{#if selection.active}
 		<div class="rounded-group-end flex" transition:slide={slideXFast}>
-			<button type="button" class="btn-slate" title="Select all" on:click={all}>
-				<span class="icon-base icon-[material-symbols--select-all]" />
+			<button
+				type="button"
+				class="btn-slate"
+				title="Select all"
+				aria-label="Select all"
+				onclick={selection.all}
+			>
+				<span class="icon-base icon-[material-symbols--select-all]"></span>
 			</button>
-			<button type="button" class="btn-slate" title="Select none" on:click={none}>
-				<span class="icon-base icon-[material-symbols--deselect]" />
+			<button
+				type="button"
+				class="btn-slate"
+				title="Select none"
+				aria-label="Select all"
+				onclick={selection.none}
+			>
+				<span class="icon-base icon-[material-symbols--deselect]"></span>
 			</button>
 		</div>
 	{/if}
 </div>
-{#if $selection.size > 0}
+{#if selection.size > 0}
 	<div class="rounded-group flex" transition:fade={fadeDefault}>
-		<slot />
+		{@render children?.()}
 	</div>
 {/if}
diff --git a/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte b/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte
index 2e7869f..ee07902 100644
--- a/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte
+++ b/frontend/src/lib/toolbar/ToggleAdvancedFilters.svelte
@@ -1,39 +1,42 @@
 <script lang="ts">
-	import { page } from '$app/stores';
-	import { getFilterContext } from '$lib/Filter';
+	import { page } from '$app/state';
 	import { navigate } from '$lib/Navigation';
 	import { slideXFast } from '$lib/Transitions';
 	import Badge from '$lib/components/Badge.svelte';
 	import { slide } from 'svelte/transition';
-	import { getToolbarContext } from './Toolbar.svelte';
+	import type { ToolbarState } from './Toolbar.svelte';
 
-	const toolbar = getToolbarContext();
-	const filter = getFilterContext();
+	interface Props extends ToolbarState {
+		filterSize: number;
+	}
+
+	let { expanded, toggle, filterSize }: Props = $props();
 </script>
 
 <div class="rounded-group flex">
 	<button
-		class:toggled={$toolbar.expand}
+		class:toggled={expanded}
 		class="btn-slate relative"
-		title={`${$toolbar.expand ? 'Hide' : 'Show'} filters`}
-		on:click={() => ($toolbar.expand = !$toolbar.expand)}
+		title={`${expanded ? 'Hide' : 'Show'} filters`}
+		onclick={toggle}
 	>
-		{#if $toolbar.expand}
-			<span class="icon-base icon-[material-symbols--filter-alt]" />
+		{#if expanded}
+			<span class="icon-base icon-[material-symbols--filter-alt]"></span>
 		{:else}
-			<span class="icon-base icon-[material-symbols--filter-alt-outline]" />
+			<span class="icon-base icon-[material-symbols--filter-alt-outline]"></span>
 		{/if}
-		<Badge number={$filter.include.size + $filter.exclude.size} />
+		<Badge number={filterSize} />
 	</button>
-	{#if $filter.include.size + $filter.exclude.size > 0}
+	{#if filterSize > 0}
 		<button
 			class="btn-slate relative hover:bg-rose-700"
-			on:click={() => navigate({ filter: {} }, $page.url.searchParams)}
+			onclick={() => navigate({ filter: {} }, page.url.searchParams)}
 			transition:slide={slideXFast}
 			title="Reset filters"
+			aria-label="Reset filters"
 		>
 			<div class="flex">
-				<span class="icon-base icon-[material-symbols--filter-alt-off]" />
+				<span class="icon-base icon-[material-symbols--filter-alt-off]"></span>
 			</div>
 		</button>
 	{/if}
diff --git a/frontend/src/lib/toolbar/Toolbar.svelte b/frontend/src/lib/toolbar/Toolbar.svelte
index e87d731..03cd892 100644
--- a/frontend/src/lib/toolbar/Toolbar.svelte
+++ b/frontend/src/lib/toolbar/Toolbar.svelte
@@ -1,23 +1,25 @@
-<script lang="ts" context="module">
-	import { writable, type Writable } from 'svelte/store';
+<script lang="ts">
+	import { type Snippet } from 'svelte';
 
-	interface ToolbarContext {
-		expand: boolean;
+	export interface ToolbarState {
+		expanded: boolean;
+		toggle: () => void;
 	}
 
-	function initToolbarContext() {
-		return setContext<Writable<ToolbarContext>>('toolbar', writable({ expand: false }));
+	interface Props {
+		start?: Snippet<[ToolbarState]>;
+		center?: Snippet<[ToolbarState]>;
+		end?: Snippet<[ToolbarState]>;
+		expansion?: Snippet;
 	}
 
-	export function getToolbarContext() {
-		return getContext<Writable<ToolbarContext>>('toolbar');
-	}
-</script>
+	let { start, center, end, expansion }: Props = $props();
 
-<script lang="ts">
-	import { getContext, setContext } from 'svelte';
+	let expanded = $state(false);
 
-	const toolbar = initToolbarContext();
+	function toggle() {
+		expanded = !expanded;
+	}
 </script>
 
 <div class="flex flex-col">
@@ -25,18 +27,18 @@
 		class="flex flex-row flex-wrap gap-4 text-sm xl:grid xl:grid-flow-col xl:grid-cols-[1fr_2fr_1fr]"
 	>
 		<div class="flex flex-row justify-start gap-2">
-			<slot name="start" />
+			{@render start?.({ expanded, toggle })}
 		</div>
 		<div class="flex flex-row flex-wrap justify-start gap-2 xl:flex-nowrap xl:justify-center">
-			<slot name="center" />
+			{@render center?.({ expanded, toggle })}
 		</div>
 		<div class="flex flex-row justify-end gap-2">
-			<slot name="end" />
+			{@render end?.({ expanded, toggle })}
 		</div>
 	</div>
-	{#if $toolbar.expand}
+	{#if expanded}
 		<div class="mt-4">
-			<slot />
+			{@render expansion?.()}
 		</div>
 	{/if}
 </div>
-- 
cgit v1.2.3-2-gb3c3