diff --git a/frontend/src/lib/components/layout/nav-main.svelte b/frontend/src/lib/components/layout/nav-main.svelte
index 9f28114..71e101e 100644
--- a/frontend/src/lib/components/layout/nav-main.svelte
+++ b/frontend/src/lib/components/layout/nav-main.svelte
@@ -3,58 +3,74 @@
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { Badge } from "$lib/components/ui/badge/index.js";
+ import { useSidebar } from '$lib/components/ui/sidebar/index.js';
import { getBookshelfState } from '$lib/state/bookshelf.svelte';
import { getLibraryState } from '$lib/state/library.svelte';
import { House, LibraryBig, Rows3, ChevronRightIcon } from '@lucide/svelte';
const libraryState = getLibraryState();
const bookshelfState = getBookshelfState();
+ const sidebar = useSidebar();
- let items = $derived(
- [
- {
- title: 'Home',
- url: `/library/${libraryState.activeLibrary?.id}`,
- icon: House
- },
- {
- title: 'Library',
- url: `/library/${libraryState.activeLibrary?.id}/view`,
- icon: LibraryBig
- },
- {
- title: 'Shelves',
- url: '#',
- icon: Rows3,
- shelves: []
- }
- ].map((item) => ({
- ...item,
- isActive: page.url.pathname === item.url
- }))
- );
+ // Owned here so the collapsed rail can force it open when it expands the
+ // sidebar. Previously open={isActive}, which was always false — the Shelves
+ // item has no route of its own, so pathname never matched.
+ let shelvesOpen = $state(false);
+
+ // Deliberately a plain const, not $derived. These objects hold component
+ // references, and `` is a dynamic component — if the array were
+ // rebuilt on every navigation the icons would remount, flashing and shifting
+ // layout. Only the url and active state need to be reactive, so they are
+ // computed per-item in the markup instead.
+ const items = [
+ { title: 'Home', icon: House, path: (id?: number) => `/library/${id}` },
+ { title: 'Library', icon: LibraryBig, path: (id?: number) => `/library/${id}/view` },
+ { title: 'Shelves', icon: Rows3, path: () => '#', shelves: [] }
+ ];
+{#snippet shelvesTrigger(item: { title: string; icon: typeof House })}
+
+ {item.title}
+
+{/snippet}
+
{#each items as item (item.title)}
+ {@const url = item.path(libraryState.activeLibrary?.id)}
+ {@const isActive = page.url.pathname === url}
{#if 'shelves' in item}
-
+
{#snippet child({ props })}
-
- {#snippet child({ props })}
-
- {#if item.icon}
-
- {/if}
- {item.title}
-
-
- {/snippet}
-
+ {#if sidebar.state === 'collapsed'}
+
+ {
+ sidebar.setOpen(true);
+ shelvesOpen = true;
+ }}
+ >
+ {@render shelvesTrigger(item)}
+
+ {:else}
+
+ {#snippet child({ props })}
+
+ {@render shelvesTrigger(item)}
+
+ {/snippet}
+
+ {/if}
{#each bookshelfState.getBookshelves(libraryState.activeLibrary!.id) ?? [] as shelf (shelf.id)}
@@ -85,9 +101,9 @@
{:else}
-
+
{#snippet child({ props })}
-
+
{#if item.icon}
{/if}
diff --git a/frontend/src/lib/components/layout/theme-toggle.svelte b/frontend/src/lib/components/layout/theme-toggle.svelte
index 66c3a62..75c339e 100644
--- a/frontend/src/lib/components/layout/theme-toggle.svelte
+++ b/frontend/src/lib/components/layout/theme-toggle.svelte
@@ -1,5 +1,5 @@
+
- {#if mode.current === 'light'}
-
- {:else}
-
- {/if}
+
+
+ Toggle light and dark mode
- {#if mode.current === 'light'}
- Dark Mode
- {:else}
- Light Mode
- {/if}
+ Dark Mode
+ Light Mode
diff --git a/frontend/src/lib/components/view/book-list.svelte b/frontend/src/lib/components/view/book-list.svelte
index 022f55f..cffcffc 100644
--- a/frontend/src/lib/components/view/book-list.svelte
+++ b/frontend/src/lib/components/view/book-list.svelte
@@ -23,8 +23,10 @@
// Start observing
resizeObserver.observe(scrollContainer);
- // Initial check with delay
- setTimeout(updateScrollState, 0);
+ // Measure synchronously. Reading scrollWidth/clientWidth forces layout, so
+ // the numbers are already accurate here — deferring to a macrotask just
+ // guaranteed one painted frame with the arrows in the wrong state.
+ updateScrollState();
// Cleanup
return () => {
@@ -33,12 +35,14 @@
});
const scrollLeft = () => {
+ if (!scrollContainer) return;
scrollContainer.scrollBy({ left: -scrollContainer.clientWidth, behavior: 'smooth' });
// Update state after scroll
setTimeout(updateScrollState, 100);
};
const scrollRight = () => {
+ if (!scrollContainer) return;
scrollContainer.scrollBy({ left: scrollContainer.clientWidth, behavior: 'smooth' });
// Update state after scroll
setTimeout(updateScrollState, 100);
@@ -61,29 +65,32 @@
{#if books.length > 0}
-
{title}
- {#if needsScroll}
-
-
-
-
- {/if}
+
{title}
+
+
+
+
+