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} + + - {#if mode.current === 'light'} -

Dark Mode

- {:else} -

Light Mode

- {/if} +

Dark 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}

+ +
+ + +