feat: put the library view mode in the url via shallow routing

This commit is contained in:
2026-08-11 20:08:53 -04:00
parent 0139f6f5eb
commit c09b8365fa
3 changed files with 142 additions and 36 deletions
@@ -12,7 +12,9 @@
import FilterButton from './filter-button.svelte';
import SortButton from './sort-button.svelte';
import ViewToggle from './view-toggle.svelte';
import PresetChips from './preset-chips.svelte';
import BookTable from './book-table.svelte';
import BookRows from './book-rows.svelte';
import BatchOperationsToolbar from './batch-operations-toolbar.svelte';
import { getBookCollectionState } from '$lib/state/bookCollection.svelte';
import { getBookSelectionState } from '$lib/state/bookSelection.svelte';
@@ -22,7 +24,6 @@
const bookCollection = getBookCollectionState();
const bookOps = getBookOperationsState();
let view = $state('grid');
let sentinel = $state<HTMLElement>();
let scrollContainer = $state<HTMLElement | null>(null);
@@ -66,7 +67,14 @@
<div class="top-0 z-1 mb-4 flex h-12 w-full rounded-lg border bg-sidebar px-5">
<div class="flex w-full items-center py-2">
{#if !selectionState.selectionModeActive}
<ViewToggle bind:view />
<ViewToggle
value={bookCollection.view}
onValueChange={(next) => bookCollection.setView(next)}
/>
<div class="mx-4 min-w-0 flex-1">
<PresetChips />
</div>
<div class="ml-auto">
<SortButton />
@@ -92,8 +100,10 @@
</div>
{:else if bookCollection.books.length > 0}
<ScrollArea bind:viewportRef={scrollContainer} orientation="both" class="h-[calc(100vh-11rem)] w-full px-5 pb-5">
{#if view === 'grid'}
{#if bookCollection.view === 'grid'}
<BookGrid books={bookCollection.books} />
{:else if bookCollection.view === 'list'}
<BookRows books={bookCollection.books} />
{:else}
<BookTable books={bookCollection.books} />
{/if}
@@ -1,40 +1,54 @@
<script>
<script lang="ts">
import * as Tooltip from '$lib/components/ui/tooltip/index';
import * as ToggleGroup from '$lib/components/ui/toggle-group/index';
import { BOOK_VIEWS, type BookView } from '$lib/state/bookCollection.svelte';
import { List, LayoutGrid } from '@lucide/svelte';
import { List, LayoutGrid, Table } from '@lucide/svelte';
let { view = $bindable(), class: className = '' } = $props();
/**
* Controlled rather than bound: the parent writes the URL when the view
* changes, and a callback keeps that write next to the intent instead of
* needing an effect that also fires on the initial assignment.
*/
let {
value,
onValueChange,
class: className = ''
}: {
value: BookView;
onValueChange: (view: BookView) => void;
class?: string;
} = $props();
const ICONS = { grid: LayoutGrid, list: List, table: Table };
const LABELS = { grid: 'Grid view', list: 'List view', table: 'Table view' };
</script>
<ToggleGroup.Root type="single" bind:value={view} class={className}>
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
<ToggleGroup.Item value="grid" aria-label="Toggle grid view" {...props}>
<LayoutGrid class="size-4" />
</ToggleGroup.Item>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content>
<p>Grid view</p>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>
{#snippet child({ props })}
<ToggleGroup.Item value="list" aria-label="Toggle list view" {...props}>
<List class="size-4" />
</ToggleGroup.Item>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content>
<p>List view</p>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
<ToggleGroup.Root
type="single"
{value}
onValueChange={(next) => {
// The group emits '' when the active item is pressed again; keep the
// current view rather than leaving the browser with nothing to render.
if (next) onValueChange(next as BookView);
}}
class={className}
>
{#each BOOK_VIEWS as view (view)}
{@const Icon = ICONS[view]}
<Tooltip.Provider>
<Tooltip.Root ignoreNonKeyboardFocus>
<Tooltip.Trigger>
{#snippet child({ props })}
<ToggleGroup.Item value={view} aria-label={LABELS[view]} {...props}>
<Icon class="size-4" />
</ToggleGroup.Item>
{/snippet}
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<p>{LABELS[view]}</p>
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
{/each}
</ToggleGroup.Root>