refactor: Desktop Icon Strip DRY

This commit is contained in:
Aleksander Grygier 2026-04-03 10:02:06 +02:00
parent 6ec8aa9c6e
commit c12c0b5cfe
5 changed files with 212 additions and 165 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -18,7 +18,7 @@
<div style="display: contents">
<script>
{
__sveltekit_lswftp = {
__sveltekit_1irt4nm = {
base: new URL('.', location).pathname.slice(0, -1)
};

View File

@ -4,6 +4,8 @@
import { ActionIcon } from '$lib/components/app/actions';
import { McpLogo } from '$lib/components/app/mcp';
import { Database, Settings, Search, SquarePen } from '@lucide/svelte';
import { fade } from 'svelte/transition';
import { onMount, type Component } from 'svelte';
interface Props {
sidebarOpen: boolean;
@ -12,9 +14,62 @@
let { sidebarOpen, onSearchClick }: Props = $props();
const TRANSITION_DURATION = 200;
let isMcpActive = $derived(page.route.id === '/settings/mcp');
let isImportExportActive = $derived(page.route.id === '/settings/import-export');
let isSettingsActive = $derived(page.route.id === '/settings/chat');
interface IconItem {
icon: Component;
tooltip: string;
onclick: () => void;
activeClass?: string;
group: 'top' | 'bottom';
}
let icons = $derived<IconItem[]>([
{
icon: SquarePen,
tooltip: 'New Chat',
onclick: () => goto('?new_chat=true#/'),
group: 'top'
},
{
icon: Search,
tooltip: 'Search',
onclick: onSearchClick,
group: 'top'
},
{
icon: McpLogo,
tooltip: 'MCP Servers',
onclick: () => goto('#/settings/mcp'),
activeClass: isMcpActive ? 'bg-accent text-accent-foreground' : '',
group: 'bottom'
},
{
icon: Database,
tooltip: 'Import / Export',
onclick: () => goto('#/settings/import-export'),
activeClass: isImportExportActive ? 'bg-accent text-accent-foreground' : '',
group: 'bottom'
},
{
icon: Settings,
tooltip: 'Settings',
onclick: () => goto('#/settings/chat'),
activeClass: isSettingsActive ? 'bg-accent text-accent-foreground' : '',
group: 'bottom'
}
]);
let topIcons = $derived(icons.filter((i) => i.group === 'top'));
let bottomIcons = $derived(icons.filter((i) => i.group === 'bottom'));
let mounted = $state(false);
onMount(() => (mounted = true));
let showIcons = $derived(mounted && !sidebarOpen);
</script>
<!-- Spacer to reserve space for icon strip in the flex layout -->
@ -30,54 +85,35 @@
: 'opacity-100'}"
>
<div class="mt-12 flex flex-col items-center gap-1">
<ActionIcon
icon={SquarePen}
tooltip="New Chat"
size="lg"
iconSize="h-4 w-4"
class="h-9 w-9 rounded-full hover:bg-accent!"
onclick={() => goto('?new_chat=true#/')}
/>
<ActionIcon
icon={Search}
tooltip="Search"
size="lg"
iconSize="h-4 w-4"
class="h-9 w-9 rounded-full hover:bg-accent!"
onclick={onSearchClick}
/>
{#each topIcons as item (item.tooltip)}
{#if showIcons}
<div in:fade={{ duration: TRANSITION_DURATION, delay: TRANSITION_DURATION }}>
<ActionIcon
icon={item.icon}
tooltip={item.tooltip}
size="lg"
iconSize="h-4 w-4"
class="h-9 w-9 rounded-full hover:bg-accent! {item.activeClass ?? ''}"
onclick={item.onclick}
/>
</div>
{/if}
{/each}
</div>
<div class="flex flex-col items-center gap-1">
<ActionIcon
icon={McpLogo}
tooltip="MCP Servers"
size="lg"
iconSize="h-4 w-4"
class="h-9 w-9 rounded-full hover:bg-accent! {isMcpActive
? 'bg-accent text-accent-foreground'
: ''}"
onclick={() => goto('#/settings/mcp')}
/>
<ActionIcon
icon={Database}
tooltip="Import / Export"
size="lg"
iconSize="h-4 w-4"
class="h-9 w-9 rounded-full hover:bg-accent! {isImportExportActive
? 'bg-accent text-accent-foreground'
: ''}"
onclick={() => goto('#/settings/import-export')}
/>
<ActionIcon
icon={Settings}
tooltip="Settings"
size="lg"
iconSize="h-4 w-4"
class="h-9 w-9 rounded-full hover:bg-accent! {isSettingsActive
? 'bg-accent text-accent-foreground'
: ''}"
onclick={() => goto('#/settings/chat')}
/>
{#each bottomIcons as item (item.tooltip)}
{#if showIcons}
<div in:fade={{ duration: TRANSITION_DURATION, delay: TRANSITION_DURATION }}>
<ActionIcon
icon={item.icon}
tooltip={item.tooltip}
size="lg"
iconSize="h-4 w-4"
class="h-9 w-9 rounded-full hover:bg-accent! {item.activeClass ?? ''}"
onclick={item.onclick}
/>
</div>
{/if}
{/each}
</div>
</aside>

View File

@ -4,6 +4,8 @@
import { browser } from '$app/environment';
import { page } from '$app/state';
import { untrack } from 'svelte';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import {
ChatSidebar,
ChatSettings,
@ -32,6 +34,7 @@
let isMobile = new IsMobile();
let isDesktop = $derived(!isMobile.current);
let sidebarOpen = $state(false);
let mounted = $state(false);
let innerHeight = $state<number | undefined>();
let chatSidebar:
| { activateSearchMode?: () => void; editActiveConversation?: () => void }
@ -108,6 +111,10 @@
}
}
onMount(() => {
mounted = true;
});
$effect(() => {
if (alwaysShowSidebarOnDesktop && isDesktop) {
sidebarOpen = true;
@ -241,12 +248,16 @@
</Sidebar.Root>
{#if !(alwaysShowSidebarOnDesktop && isDesktop) && !(isSettingsRoute && !isDesktop)}
<Sidebar.Trigger
class="transition-left absolute left-0 z-[900] duration-200 ease-linear {sidebarOpen
? 'left-[calc(var(--sidebar-width)+0.75rem)] max-md:hidden'
: 'left-0!'}"
style="translate: 1rem 1rem;"
/>
{#if mounted}
<div in:fade={{ duration: 200 }}>
<Sidebar.Trigger
class="transition-left absolute left-0 z-[900] duration-200 ease-linear {sidebarOpen
? 'left-[calc(var(--sidebar-width)+0.75rem)] max-md:hidden'
: 'left-0!'}"
style="translate: 1rem 1rem;"
/>
</div>
{/if}
{/if}
{#if isDesktop && !alwaysShowSidebarOnDesktop}