ui : track sidebar expanded state in a shared ui store

Move the desktop sidebar expanded/collapsed state out of deviceStore into a
dedicated uiStore so the chat tab bar can react to it.

Assisted-by: pi
This commit is contained in:
Aleksander Grygier
2026-08-20 18:13:03 +02:00
committed by GitHub
parent 1ae4576fe3
commit b59d8bfe9d
3 changed files with 43 additions and 25 deletions
@@ -14,7 +14,7 @@
import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte'; import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte';
import { useMarqueeSelection } from '$lib/hooks/use-marquee-selection.svelte'; import { useMarqueeSelection } from '$lib/hooks/use-marquee-selection.svelte';
import { RouterService } from '$lib/services/router.service'; import { RouterService } from '$lib/services/router.service';
import { chatStore, conversationsStore, deviceStore, settingsStore } from '$lib/stores'; import { chatStore, conversationsStore, deviceStore, settingsStore, uiStore } from '$lib/stores';
import { buildConversationTree } from '$lib/utils'; import { buildConversationTree } from '$lib/utils';
import { circIn } from 'svelte/easing'; import { circIn } from 'svelte/easing';
import { SvelteSet } from 'svelte/reactivity'; import { SvelteSet } from 'svelte/reactivity';
@@ -31,30 +31,29 @@
toggleSidebar: () => toggleExpandedMode() toggleSidebar: () => toggleExpandedMode()
}); });
let isExpandedMode = $state(false);
let hoveredTooltip = $state<string | null>(null); let hoveredTooltip = $state<string | null>(null);
let logoHovered = $state(false); let logoHovered = $state(false);
const isStripExpanded = $derived(isExpandedMode || hoveredTooltip !== null); const isStripExpanded = $derived(uiStore.isSidebarExpanded || hoveredTooltip !== null);
const isOnMobile = $derived(deviceStore.isMobile); const isOnMobile = $derived(deviceStore.isMobile);
const alwaysShowOnDesktop = $derived(settingsStore.config.alwaysShowSidebarOnDesktop as boolean); const alwaysShowOnDesktop = $derived(settingsStore.config.alwaysShowSidebarOnDesktop as boolean);
$effect(() => { $effect(() => {
if (alwaysShowOnDesktop && !isOnMobile) { if (alwaysShowOnDesktop && !isOnMobile) {
isExpandedMode = true; uiStore.isSidebarExpanded = true;
} }
}); });
function toggleExpandedMode() { function toggleExpandedMode() {
isExpandedMode = !isExpandedMode; uiStore.isSidebarExpanded = !uiStore.isSidebarExpanded;
if (!isExpandedMode) { if (!uiStore.isSidebarExpanded) {
hoveredTooltip = null; hoveredTooltip = null;
} }
} }
$effect(() => { $effect(() => {
if (!isExpandedMode) { if (!uiStore.isSidebarExpanded) {
isSearchModeActive = false; isSearchModeActive = false;
searchQuery = ''; searchQuery = '';
@@ -66,7 +65,7 @@
$effect(() => { $effect(() => {
if (deviceStore.isMobile && page.url.hash.includes(ROUTES.SEARCH)) { if (deviceStore.isMobile && page.url.hash.includes(ROUTES.SEARCH)) {
isExpandedMode = false; uiStore.isSidebarExpanded = false;
} }
}); });
@@ -294,7 +293,7 @@
} }
pendingCollapse = setTimeout(() => { pendingCollapse = setTimeout(() => {
isExpandedMode = false; uiStore.isSidebarExpanded = false;
pendingCollapse = null; pendingCollapse = null;
}, 100); }, 100);
} }
@@ -314,7 +313,7 @@
class={[ class={[
'fixed md:sticky top-2 left-2 md:left-0 md:ml-2 md:mt-2 pt-2 z-10 w-[calc(100dvw-1rem)]', 'fixed md:sticky top-2 left-2 md:left-0 md:ml-2 md:mt-2 pt-2 z-10 w-[calc(100dvw-1rem)]',
'md:h-[calc(100dvh-1.125rem)]', 'md:h-[calc(100dvh-1.125rem)]',
isExpandedMode && uiStore.isSidebarExpanded &&
(deviceStore.isStandalone (deviceStore.isStandalone
? 'h-[calc(100dvh-2rem)]' ? 'h-[calc(100dvh-2rem)]'
: deviceStore.isIOSDevice : deviceStore.isIOSDevice
@@ -323,9 +322,9 @@
'rounded-3xl md:rounded-2xl', 'rounded-3xl md:rounded-2xl',
'flex flex-col justify-between', 'flex flex-col justify-between',
'md:transition-[width,padding] duration-200 ease-out', 'md:transition-[width,padding] duration-200 ease-out',
isStripExpanded && 'md:w-72 md:bg-muted/60 md:backdrop-blur-xl border-border shadow-md', isStripExpanded && 'md:w-72 md:bg-muted/60 md:backdrop-blur-xl shadow-md',
!isStripExpanded && 'md:w-12', !isStripExpanded && 'md:w-12',
isExpandedMode && 'is-expanded' uiStore.isSidebarExpanded && 'is-expanded'
]} ]}
> >
<div class="px-2 flex items-center justify-between"> <div class="px-2 flex items-center justify-between">
@@ -337,24 +336,26 @@
onmouseleave={() => (logoHovered = false)} onmouseleave={() => (logoHovered = false)}
> >
<ActionIcon <ActionIcon
icon={!isExpandedMode && logoHovered && innerWidth > 768 ? PanelLeftOpen : Logo} icon={!uiStore.isSidebarExpanded && logoHovered && innerWidth > 768
? PanelLeftOpen
: Logo}
size="lg" size="lg"
iconSize="h-4.5 w-4.5 md:h-4 md:w-4" iconSize="h-4.5 w-4.5 md:h-4 md:w-4"
class="{isExpandedMode class="{uiStore.isSidebarExpanded
? 'bg-muted! md:bg-foreground/5!' ? 'bg-muted! md:bg-foreground/5!'
: 'bg-transparent!'} md:h-9 md:w-9 h-10 w-10 rounded-full md:hover:bg-foreground/10! pointer-events-auto" : 'bg-transparent'} md:h-9 md:w-9 h-10 w-10 rounded-full md:hover:bg-foreground/10! pointer-events-auto"
href={isExpandedMode ? ROUTES.START : undefined} href={uiStore.isSidebarExpanded ? ROUTES.START : undefined}
onclick={isExpandedMode ? undefined : toggleExpandedMode} onclick={uiStore.isSidebarExpanded ? undefined : toggleExpandedMode}
tooltip={isExpandedMode ? undefined : 'Open Sidebar'} tooltip={uiStore.isSidebarExpanded ? undefined : 'Open Sidebar'}
tooltipSide={TooltipSide.RIGHT} tooltipSide={TooltipSide.RIGHT}
ariaLabel={isExpandedMode ? 'Go to start' : 'Expand navigation'} ariaLabel={uiStore.isSidebarExpanded ? 'Go to start' : 'Expand navigation'}
/> />
</div> </div>
{#if isOnMobile || (isExpandedMode && !alwaysShowOnDesktop)} {#if isOnMobile || (uiStore.isSidebarExpanded && !alwaysShowOnDesktop)}
<div <div
class="flex items-center transition-all duration-150 ease-out {deviceStore.isMobile && class="flex items-center transition-all duration-150 ease-out {deviceStore.isMobile &&
!isExpandedMode !uiStore.isSidebarExpanded
? 'opacity-0 h-0!' ? 'opacity-0 h-0!'
: ''}" : ''}"
in:fade={{ delay: 50, duration: 150, easing: circIn }} in:fade={{ delay: 50, duration: 150, easing: circIn }}
@@ -377,12 +378,12 @@
<div <div
class="mt-2 flex min-h-0 flex-1 flex-col gap-4 md:gap-1 {deviceStore.isMobile class="mt-2 flex min-h-0 flex-1 flex-col gap-4 md:gap-1 {deviceStore.isMobile
? 'transition-[opacity,height] duration-200 ease-out' ? 'transition-[opacity,height] duration-200 ease-out'
: ''} {deviceStore.isMobile && !isExpandedMode ? 'opacity-0 !h-0' : ''}" : ''} {deviceStore.isMobile && !uiStore.isSidebarExpanded ? 'opacity-0 !h-0' : ''}"
in:fade={{ duration: 200 }} in:fade={{ duration: 200 }}
out:fade={{ duration: 200 }} out:fade={{ duration: 200 }}
> >
<SidebarNavigationActions <SidebarNavigationActions
isExpandedMode={innerWidth > 768 ? isExpandedMode : true} isExpandedMode={innerWidth > 768 ? uiStore.isSidebarExpanded : true}
class="px-2" class="px-2"
bind:isSearchModeActive bind:isSearchModeActive
bind:searchQuery bind:searchQuery
@@ -391,7 +392,7 @@
searchQuery = ''; searchQuery = '';
}} }}
onSearchClick={() => { onSearchClick={() => {
isExpandedMode = true; uiStore.isSidebarExpanded = true;
isSearchModeActive = true; isSearchModeActive = true;
}} }}
onNewChat={() => { onNewChat={() => {
@@ -401,7 +402,7 @@
}} }}
/> />
{#if isExpandedMode || isOnMobile} {#if uiStore.isSidebarExpanded || isOnMobile}
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto"> <div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
<SidebarNavigationConversationList <SidebarNavigationConversationList
class="px-2" class="px-2"
+3
View File
@@ -43,6 +43,9 @@ export { modelsStore } from './models/index.svelte';
// SERVER // SERVER
export { serverStore } from './server.svelte'; export { serverStore } from './server.svelte';
// UI / LAYOUT
export { uiStore } from './ui.svelte';
// SETTINGS / UI PREFERENCES // SETTINGS / UI PREFERENCES
export { settingsStore } from './settings/index.svelte'; export { settingsStore } from './settings/index.svelte';
+14
View File
@@ -0,0 +1,14 @@
/**
* uiStore - Shared UI/layout state
*
* Holds cross-component UI state that does not belong to a single component
* (e.g. the desktop sidebar's expanded/collapsed state, which the sidebar
* controls and the chat tab bar reacts to).
*/
class UiStore {
/** Whether the desktop sidebar is expanded (open). */
isSidebarExpanded = $state(false);
}
export const uiStore = new UiStore();