mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-20 09:38:43 +02:00
chat : turn tab bar into a horizontally scrollable carousel
Make the tab bar a horizontally scrollable carousel with edge scroll buttons and active-tab centering, and align its styling with the sidebar. Assisted-by: pi
This commit is contained in:
committed by
GitHub
parent
1144652072
commit
8bcd025902
@@ -1,9 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Loader2, Plus, SquarePen, X } from '@lucide/svelte';
|
||||
import { ChevronLeft, ChevronRight, Loader2, Plus, SquarePen, X } from '@lucide/svelte';
|
||||
import { page } from '$app/state';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import { chatStore, conversationsStore, NEW_CHAT_TAB_ID, tabsStore } from '$lib/stores';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import { useScrollCarousel } from '$lib/hooks/use-scroll-carousel.svelte';
|
||||
import { chatStore, conversationsStore, NEW_CHAT_TAB_ID, tabsStore, uiStore } from '$lib/stores';
|
||||
|
||||
const carousel = useScrollCarousel();
|
||||
|
||||
let activeId = $derived(page.params.id ?? NEW_CHAT_TAB_ID);
|
||||
|
||||
@@ -34,82 +38,122 @@
|
||||
handleClose(id);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
// keep the active tab in view whenever it changes (activeId drives re-run)
|
||||
const el = carousel.scrollContainer?.querySelector<HTMLElement>('[data-active-tab]');
|
||||
|
||||
if (el && activeId) {
|
||||
carousel.scrollToCenter(el);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav
|
||||
class="sticky pl-1 top-0 pt-3.5 z-10 hidden md:block transition-colors ease-in-out"
|
||||
class="sticky pl-1 top-0 z-10 hidden md:block transition-[padding] duration-200 ease-in-out pt-3.25 {uiStore.isSidebarExpanded
|
||||
? 'pt-1.5 max-w-[calc(100vw-19.5rem)]'
|
||||
: 'max-w-[calc(100vw-4.5rem)]'}"
|
||||
aria-label="Open conversations"
|
||||
>
|
||||
<div class="flex h-10 items-center gap-1.25 overflow-x-auto px-2">
|
||||
{#each tabs as tab (tab.id)}
|
||||
{@const isActive = tab.id === activeId}
|
||||
{@const isLoading = loadingIds.has(tab.id)}
|
||||
<div class="relative flex h-10 items-center" style="scroll-padding: 1rem;">
|
||||
<button
|
||||
class="absolute left-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-muted shadow-md backdrop-blur-sm transition-opacity hover:bg-accent {carousel.canScrollLeft
|
||||
? 'opacity-100'
|
||||
: 'pointer-events-none opacity-0'}"
|
||||
onclick={carousel.scrollLeft}
|
||||
aria-label="Scroll left"
|
||||
>
|
||||
<ChevronLeft class={ICON_CLASS_DEFAULT} />
|
||||
</button>
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
'group flex h-8 max-w-52 min-w-0 shrink-0 items-center gap-1 rounded-lg pr-1 pl-3 text-sm transition-colors backdrop-blur-xl',
|
||||
isActive
|
||||
? 'bg-foreground/8 text-foreground'
|
||||
: 'text-muted-foreground hover:bg-foreground/5 hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
class="flex min-w-0 flex-1 cursor-pointer items-center gap-2"
|
||||
onclick={() => tabsStore.activate(tab.id)}
|
||||
onauxclick={(e) => handleAuxClick(tab.id, e)}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
>
|
||||
{#if isLoading}
|
||||
<Loader2 class="h-3.5 w-3.5 shrink-0 animate-spin" />
|
||||
{:else if tab.isNewChat}
|
||||
<SquarePen class="h-3.5 w-3.5 shrink-0" />
|
||||
{/if}
|
||||
<div
|
||||
class="scrollbar-hide flex h-10 min-w-0 items-center overflow-x-auto"
|
||||
bind:this={carousel.scrollContainer}
|
||||
onscroll={carousel.updateScrollButtons}
|
||||
>
|
||||
<div class="flex min-w-max items-center gap-1.25">
|
||||
{#each tabs as tab (tab.id)}
|
||||
{@const isActive = tab.id === activeId}
|
||||
{@const isLoading = loadingIds.has(tab.id)}
|
||||
|
||||
<span class="truncate">{tab.name}</span>
|
||||
</button>
|
||||
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<button
|
||||
{...props}
|
||||
class={cn(
|
||||
'flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-opacity hover:bg-foreground/10 hover:text-foreground'
|
||||
)}
|
||||
onclick={() => handleClose(tab.id)}
|
||||
aria-label="Close tab"
|
||||
>
|
||||
<X class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Close tab</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if showNewChatButton}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<div
|
||||
data-active-tab={isActive ? 'true' : undefined}
|
||||
class={cn(
|
||||
'group flex h-8 max-w-52 min-w-0 shrink-0 items-center gap-1 rounded-lg pr-1 pl-3 text-sm whitespace-nowrap transition-colors hover:bg-foreground/10 border backdrop-blur-xl first:ml-2',
|
||||
isActive
|
||||
? 'bg-muted/60 border-border/10 shadow-sm text-accent-foreground hover:bg-primary/15'
|
||||
: 'text-muted-foreground hover:text-foreground border-transparent hover:bg-primary/10 hover:border-border/10 hover:shadow-sm'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
{...props}
|
||||
class="backdrop-blur-lg flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-foreground/5 hover:text-foreground"
|
||||
onclick={() => conversationsStore.openNewChat()}
|
||||
aria-label="New chat"
|
||||
class="flex min-w-0 flex-1 cursor-pointer items-center gap-2"
|
||||
onclick={() => tabsStore.activate(tab.id)}
|
||||
onauxclick={(e) => handleAuxClick(tab.id, e)}
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
>
|
||||
<Plus class="h-4 w-4" />
|
||||
</button>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
{#if isLoading}
|
||||
<Loader2 class="h-3.5 w-3.5 shrink-0 animate-spin" />
|
||||
{:else if tab.isNewChat}
|
||||
<SquarePen class="h-3.5 w-3.5 shrink-0" />
|
||||
{/if}
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>New chat</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
<span class="truncate">{tab.name}</span>
|
||||
</button>
|
||||
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<button
|
||||
{...props}
|
||||
class={cn(
|
||||
'flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-opacity hover:bg-foreground/10 hover:text-foreground'
|
||||
)}
|
||||
onclick={() => handleClose(tab.id)}
|
||||
aria-label="Close tab"
|
||||
>
|
||||
<X class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>Close tab</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if showNewChatButton}
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<button
|
||||
{...props}
|
||||
class="backdrop-blur-lg flex h-8 w-8 mr-4 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-foreground/5 hover:text-foreground"
|
||||
onclick={() => conversationsStore.openNewChat()}
|
||||
aria-label="New chat"
|
||||
>
|
||||
<Plus class="h-4 w-4" />
|
||||
</button>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<p>New chat</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="absolute right-2 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-muted shadow-md backdrop-blur-sm transition-opacity hover:bg-accent {carousel.canScrollRight
|
||||
? 'opacity-100'
|
||||
: 'pointer-events-none opacity-0'}"
|
||||
onclick={carousel.scrollRight}
|
||||
aria-label="Scroll right"
|
||||
>
|
||||
<ChevronRight class={ICON_CLASS_DEFAULT} />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user