feat: UI improvements
This commit is contained in:
parent
1dafe26599
commit
c374e3e286
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -18,7 +18,7 @@
|
|||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_dduflh = {
|
||||
__sveltekit_e2p3su = {
|
||||
base: new URL('.', location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
disabled?: boolean;
|
||||
onclick: (e?: MouseEvent) => void;
|
||||
'aria-label'?: string;
|
||||
tooltipSide?: 'top' | 'right' | 'bottom' | 'left';
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -23,6 +24,7 @@
|
|||
class: className = '',
|
||||
disabled = false,
|
||||
iconSize = 'h-3 w-3',
|
||||
tooltipSide = 'top',
|
||||
onclick,
|
||||
'aria-label': ariaLabel
|
||||
}: Props = $props();
|
||||
|
|
@ -44,7 +46,7 @@
|
|||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
|
||||
<Tooltip.Content>
|
||||
<Tooltip.Content side={tooltipSide}>
|
||||
<p>{tooltip}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<header
|
||||
class="pointer-events-none fixed top-0 right-0 left-0 z-50 flex items-center justify-end p-2 duration-200 ease-linear md:p-4 {sidebar.open
|
||||
? 'md:left-[var(--sidebar-width)]'
|
||||
: ''}"
|
||||
: ''} {sidebar.isResizing ? '!duration-0' : ''}"
|
||||
>
|
||||
<div class="pointer-events-auto flex items-center space-x-2">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
import { getPreviewText } from '$lib/utils';
|
||||
import ChatSidebarActions from './ChatSidebarActions.svelte';
|
||||
import ChatSidebarFooter from './ChatSidebarFooter.svelte';
|
||||
import { APP_NAME } from '$lib/constants';
|
||||
|
||||
const sidebar = Sidebar.useSidebar();
|
||||
|
||||
|
|
@ -157,7 +158,7 @@
|
|||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="#/" onclick={handleMobileSidebarItemClick}>
|
||||
<h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">llama.cpp</h1>
|
||||
<h1 class="inline-flex items-center gap-1 px-2 text-xl font-semibold">{APP_NAME}</h1>
|
||||
</a>
|
||||
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import { McpLogo } from '$lib/components/app/mcp';
|
||||
import { Database, Settings, Search, SquarePen } from '@lucide/svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { circIn } from 'svelte/easing';
|
||||
import { onMount, type Component } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -14,7 +15,9 @@
|
|||
|
||||
let { sidebarOpen, onSearchClick }: Props = $props();
|
||||
|
||||
const TRANSITION_DURATION = 200;
|
||||
const TRANSITION_DURATION = 250;
|
||||
const TRANSITION_DELAY_MULTIPLIER = 150;
|
||||
const TRANSITION_EASING = circIn;
|
||||
|
||||
let isMcpActive = $derived(page.route.id === '/settings/mcp');
|
||||
let isImportExportActive = $derived(page.route.id === '/settings/import-export');
|
||||
|
|
@ -72,25 +75,31 @@
|
|||
let showIcons = $derived(mounted && !sidebarOpen);
|
||||
</script>
|
||||
|
||||
<!-- Spacer to reserve space for icon strip in the flex layout -->
|
||||
<div
|
||||
class="hidden shrink-0 transition-[width] duration-200 ease-linear md:block {sidebarOpen
|
||||
? 'w-0'
|
||||
: 'w-[calc(var(--sidebar-width-icon)+1.5rem)]'}"
|
||||
></div>
|
||||
<!-- Desktop: icon strip, fixed position so it stays stationary and only fades -->
|
||||
|
||||
<aside
|
||||
class="fixed top-0 bottom-0 left-0 z-10 hidden w-[calc(var(--sidebar-width-icon)+1.5rem)] flex-col items-center justify-between py-3 transition-opacity duration-200 ease-linear md:flex {sidebarOpen
|
||||
? 'pointer-events-none opacity-0'
|
||||
: 'opacity-100'}"
|
||||
>
|
||||
<div class="mt-12 flex flex-col items-center gap-1">
|
||||
{#each topIcons as item (item.tooltip)}
|
||||
{#each topIcons as item, i (item.tooltip)}
|
||||
{#if showIcons}
|
||||
<div in:fade={{ duration: TRANSITION_DURATION, delay: TRANSITION_DURATION }}>
|
||||
<div
|
||||
in:fade={{
|
||||
duration: TRANSITION_DURATION,
|
||||
delay: TRANSITION_DELAY_MULTIPLIER + i * TRANSITION_DELAY_MULTIPLIER,
|
||||
easing: TRANSITION_EASING
|
||||
}}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={item.icon}
|
||||
tooltip={item.tooltip}
|
||||
tooltipSide="right"
|
||||
size="lg"
|
||||
iconSize="h-4 w-4"
|
||||
class="h-9 w-9 rounded-full hover:bg-accent! {item.activeClass ?? ''}"
|
||||
|
|
@ -100,10 +109,18 @@
|
|||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
{#each bottomIcons as item (item.tooltip)}
|
||||
{#each bottomIcons as item, i (item.tooltip)}
|
||||
{#if showIcons}
|
||||
<div in:fade={{ duration: TRANSITION_DURATION, delay: TRANSITION_DURATION }}>
|
||||
<div
|
||||
in:fade={{
|
||||
duration: TRANSITION_DURATION,
|
||||
delay:
|
||||
TRANSITION_DELAY_MULTIPLIER + (topIcons.length + i) * TRANSITION_DELAY_MULTIPLIER,
|
||||
easing: TRANSITION_EASING
|
||||
}}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={item.icon}
|
||||
tooltip={item.tooltip}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export const SIDEBAR_COOKIE_NAME = 'sidebar:state';
|
||||
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
export const SIDEBAR_WIDTH = '18rem';
|
||||
export const SIDEBAR_MIN_WIDTH = '18rem';
|
||||
export const SIDEBAR_MAX_WIDTH = '32rem';
|
||||
export const SIDEBAR_WIDTH_MOBILE = '18rem';
|
||||
export const SIDEBAR_WIDTH_ICON = '3rem';
|
||||
export const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { IsMobile } from '$lib/hooks/is-mobile.svelte.js';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { SIDEBAR_KEYBOARD_SHORTCUT } from './constants.js';
|
||||
import { SIDEBAR_KEYBOARD_SHORTCUT, SIDEBAR_MIN_WIDTH } from './constants.js';
|
||||
|
||||
type Getter<T> = () => T;
|
||||
|
||||
|
|
@ -24,6 +24,8 @@ class SidebarState {
|
|||
readonly props: SidebarStateProps;
|
||||
open = $derived.by(() => this.props.open());
|
||||
openMobile = $state(false);
|
||||
sidebarWidth = $state(SIDEBAR_MIN_WIDTH);
|
||||
isResizing = $state(false);
|
||||
setOpen: SidebarStateProps['setOpen'];
|
||||
#isMobile: IsMobile;
|
||||
state = $derived.by(() => (this.open ? 'expanded' : 'collapsed'));
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
import {
|
||||
SIDEBAR_COOKIE_MAX_AGE,
|
||||
SIDEBAR_COOKIE_NAME,
|
||||
SIDEBAR_WIDTH,
|
||||
SIDEBAR_MIN_WIDTH,
|
||||
SIDEBAR_MAX_WIDTH,
|
||||
SIDEBAR_WIDTH_ICON
|
||||
} from './constants.js';
|
||||
import { setSidebar } from './context.svelte.js';
|
||||
|
|
@ -38,7 +39,7 @@
|
|||
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style="--sidebar-width: {SIDEBAR_WIDTH}; --sidebar-width-icon: {SIDEBAR_WIDTH_ICON}; {style}"
|
||||
style="--sidebar-width: {sidebar.sidebarWidth}; --sidebar-min-width: {SIDEBAR_MIN_WIDTH}; --sidebar-max-width: {SIDEBAR_MAX_WIDTH}; --sidebar-width-icon: {SIDEBAR_WIDTH_ICON}; {style}"
|
||||
class={cn(
|
||||
'group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar',
|
||||
className
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@
|
|||
size="icon-lg"
|
||||
class="rounded-full backdrop-blur-lg {className} {sidebar.open
|
||||
? 'top-1.5'
|
||||
: 'top-0'} md:left-[14.5rem]"
|
||||
: 'top-0'} md:left-[calc(var(--sidebar-width)-3.25rem)] {sidebar.isResizing
|
||||
? '!duration-0'
|
||||
: ''}"
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
onclick?.(e);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/components/ui/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { SIDEBAR_MIN_WIDTH, SIDEBAR_MAX_WIDTH } from './constants.js';
|
||||
import { useSidebar } from './context.svelte.js';
|
||||
|
||||
let {
|
||||
|
|
@ -18,6 +19,40 @@
|
|||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
function remToPx(rem: string): number {
|
||||
const val = parseFloat(rem);
|
||||
const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
return val * fontSize;
|
||||
}
|
||||
|
||||
function handleResizePointerDown(e: PointerEvent) {
|
||||
if (sidebar.isMobile) return;
|
||||
e.preventDefault();
|
||||
|
||||
const target = e.currentTarget as HTMLElement;
|
||||
target.setPointerCapture(e.pointerId);
|
||||
|
||||
const minPx = remToPx(SIDEBAR_MIN_WIDTH);
|
||||
const maxPx = remToPx(SIDEBAR_MAX_WIDTH);
|
||||
|
||||
sidebar.isResizing = true;
|
||||
|
||||
function onPointerMove(ev: PointerEvent) {
|
||||
const newWidth = side === 'left' ? ev.clientX : window.innerWidth - ev.clientX;
|
||||
const clamped = Math.min(maxPx, Math.max(minPx, newWidth));
|
||||
sidebar.sidebarWidth = `${clamped}px`;
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
sidebar.isResizing = false;
|
||||
target.removeEventListener('pointermove', onPointerMove);
|
||||
target.removeEventListener('pointerup', onPointerUp);
|
||||
}
|
||||
|
||||
target.addEventListener('pointermove', onPointerMove);
|
||||
target.addEventListener('pointerup', onPointerUp);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if collapsible === 'none'}
|
||||
|
|
@ -46,6 +81,7 @@
|
|||
data-slot="sidebar-gap"
|
||||
class={cn(
|
||||
'relative bg-transparent transition-[width] duration-200 ease-linear',
|
||||
sidebar.isResizing && '!duration-0',
|
||||
'w-0',
|
||||
variant === 'floating'
|
||||
? 'md:w-[calc(var(--sidebar-width)+0.75rem)]'
|
||||
|
|
@ -62,6 +98,7 @@
|
|||
data-slot="sidebar-container"
|
||||
class={cn(
|
||||
'fixed inset-y-0 z-[900] flex w-[calc(100dvw-1.5rem)] duration-200 ease-linear md:z-0 md:w-(--sidebar-width)',
|
||||
sidebar.isResizing && '!duration-0',
|
||||
variant === 'floating'
|
||||
? [
|
||||
'transition-[left,right,width,opacity]',
|
||||
|
|
@ -94,6 +131,24 @@
|
|||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<!-- Resize handle -->
|
||||
{#if side === 'left'}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
data-slot="sidebar-resize-handle"
|
||||
class="absolute inset-y-0 right-0 z-50 hidden w-1.5 cursor-ew-resize touch-none select-none hover:bg-sidebar-border/50 active:bg-sidebar-border md:block"
|
||||
class:bg-sidebar-border={sidebar.isResizing}
|
||||
onpointerdown={handleResizePointerDown}
|
||||
></div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
data-slot="sidebar-resize-handle"
|
||||
class="absolute inset-y-0 left-0 z-50 hidden w-1.5 cursor-ew-resize touch-none select-none hover:bg-sidebar-border/50 active:bg-sidebar-border md:block"
|
||||
class:bg-sidebar-border={sidebar.isResizing}
|
||||
onpointerdown={handleResizePointerDown}
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export const FORK_TREE_DEPTH_PADDING = 8;
|
||||
export const SYSTEM_MESSAGE_PLACEHOLDER = 'System message';
|
||||
export const APP_NAME = 'llama.cpp';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { replaceState } from '$app/navigation';
|
||||
import { APP_NAME } from '$lib/constants';
|
||||
|
||||
let qParam = $derived(page.url.searchParams.get('q'));
|
||||
let modelParam = $derived(page.url.searchParams.get('model'));
|
||||
|
|
@ -87,7 +88,7 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>llama.cpp</title>
|
||||
<title>{APP_NAME}</title>
|
||||
</svelte:head>
|
||||
|
||||
<DialogModelNotAvailable
|
||||
|
|
|
|||
Loading…
Reference in New Issue