ui: New Logo + Navigation cleanup & Mobile UI/UX improvements (#24897)

* chore: `npm audit fix --force`

* feat: Update sidebar toggle to use Logo

* refactor: Clean up favicon SVG

* feat: Refactor logo component and implement theme-aware favicon generation

* feat: Add configurable padding to generated PWA assets

* test: Add unit tests for writeThemeFavicons

* refactor: Componentization

* feat: WIP

* feat: WIP

* feat: WIP

* feat: Mobile UI

* feat: add SEARCH route constant

* feat: create SidebarNavigationSearchResults component

* refactor: use SidebarNavigationSearchResults in conversation list

* feat: enable mobile search navigation in sidebar actions

* feat: add mobile search route and page

* fix: prevent sidebar overflow on mobile viewports

* fix: Mobile sidebar

* feat: Mobile Search WIP

* feat: Mobile WIP

* feat: Add PWA standalone detection and refine mobile UI

* feat: Improve mobile layout, sidebar handling, and chat scrolling

* feat: Improve mobile sidebar visibility and iOS Safari chat spacing

* fix: Disable auto-scroll on mobile

* chore: Linting

* fix: Wrong condition

* feat: Mobile chat scroll

* refactor: WIP

* fix: Desktop initial scroll always working again

* fix: Partial fix for mobile auto-scroll / initial scroll

* fix: Desktop auto-scroll on initial load and during streaming

* fix: Mobile scrolling logic

* refactor: Clean up

* feat: Improve start UI

* feat: Add `delay` to `fadeInView`

* feat: Auto-scroll button

* refactor: Cleanup

* refactor: Extract chat dialogs and alerts into dedicated component

* refactor: Reorganize ChatScreen component structure and initialization

* feat: Improve auto-scroll after sending message

* feat: UI improvements

* fix: Settings link

* feat: UI improvements

* fix: better UI spacing

* fix: Remove unneeded logic

* fix: Chat Processing Info UI rendering

* feat: Improve mobile UI

* feat: UI improvement

* fix: Conditional transition delay for Chat Messages based on route from

* fix: Delay mobile sidebar collapse for smoother transitions

* fix: Mobile scroll down button + sidebar pointer events

* fix: Mobile UI

* fix: Auto scrolling

* fix: Implement dynamic height calculations for chat auto-scroll positioning and UI elements

* fix: Retrieve `autofocus` for Chat Form textarea

* fix: Use proper class

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* refactor: extract scroll-to-bottom logic and fix message send flow

* fix: update viewport store usage and remove conflicting autofocus

* feat: add accessibility labels to scroll down button

* fix: correct HTML structure in sidebar empty states

* fix: dynamically toggle processing info visibility

* chore: remove commented exports and fix formatting

* fix

* fix: Mobile Chat Form Add Action Sheet interactions

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Aleksander Grygier
2026-06-24 10:21:33 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 88636e178f
commit ef9c13d4c2
88 changed files with 2121 additions and 2146 deletions
+30 -64
View File
@@ -6,18 +6,12 @@
import { page } from '$app/state';
import { untrack } from 'svelte';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import {
DesktopIconStrip,
DialogConversationTitleUpdate,
SidebarNavigation
} from '$lib/components/app';
import { SidebarNavigation, DialogConversationTitleUpdate } from '$lib/components/app';
import { PwaMetaTags, PwaRefreshAlert } from '$lib/components/pwa';
import { pwaAssetsHead } from 'virtual:pwa-assets/head';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as Tooltip from '$lib/components/ui/tooltip';
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
import { config, settingsStore } from '$lib/stores/settings.svelte';
@@ -31,7 +25,6 @@
import { FAVICON_PATHS, FAVICON_SELECTORS } from '$lib/constants/pwa';
import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte';
import { usePwa } from '$lib/hooks/use-pwa.svelte';
import { useSettingsNavigation } from '$lib/hooks/use-settings-navigation.svelte';
import { conversations } from '$lib/stores/conversations.svelte';
import { isMobile } from '$lib/stores/viewport.svelte';
import { theme } from '$lib/stores/theme.svelte';
@@ -42,8 +35,6 @@
let { children } = $props();
let alwaysShowSidebarOnDesktop = $derived(config().alwaysShowSidebarOnDesktop);
let isDesktop = $derived(!isMobile.current);
let sidebarOpen = $state(false);
let mounted = $state(false);
let innerHeight = $state<number | undefined>();
let innerWidth = $state(browser ? window.innerWidth : 0);
@@ -61,7 +52,6 @@
let titleUpdateNewTitle = $state('');
let titleUpdateResolve: ((value: boolean) => void) | null = null;
const panelNav = useSettingsNavigation();
// Keep the hook object intact: destructuring needRefreshByStorage reads the getter once and freezes it
const pwa = usePwa();
const { needRefresh, updateServiceWorker } = pwa;
@@ -166,7 +156,6 @@
onMount(() => {
updateFavicon();
mounted = true;
});
$effect(() => {
@@ -177,8 +166,6 @@
$effect(() => {
if (alwaysShowSidebarOnDesktop && isDesktop) {
sidebarOpen = true;
return;
}
});
@@ -300,19 +287,25 @@
<PwaMetaTags />
</svelte:head>
<!-- PWA update prompt + version -->
<div class="fixed right-4 bottom-4 z-[9999] flex flex-col items-end gap-1">
{#if showBuildVersion && buildInfoStore.value}
<span class="text-[10px] tabular-nums text-muted-foreground">{buildInfoStore.value}</span>
{/if}
<PwaRefreshAlert
needRefresh={$needRefresh || pwa.needRefreshByStorage}
forceReload={pwa.needRefreshByStorage}
{updateServiceWorker}
/>
</div>
<svelte:window onkeydown={handleKeydown} bind:innerHeight bind:innerWidth />
<Tooltip.Provider delayDuration={TOOLTIP_DELAY_DURATION}>
<div class="flex flex-col md:flex-row">
<SidebarNavigation
onSearchClick={() => {
if (isMobile.current) {
goto(ROUTES.SEARCH);
} else if (chatSidebar?.activateSearchMode) {
chatSidebar.activateSearchMode();
}
}}
/>
<div class="flex-1">
{@render children?.()}
</div>
</div>
<ModeWatcher />
<Toaster richColors />
@@ -324,44 +317,17 @@
onConfirm={handleTitleUpdateConfirm}
onCancel={handleTitleUpdateCancel}
/>
<Sidebar.Provider bind:open={sidebarOpen}>
<div class="flex h-full w-full grow">
<Sidebar.Root variant="floating" class="h-full"
><SidebarNavigation bind:this={chatSidebar} /></Sidebar.Root
>
{#if !(alwaysShowSidebarOnDesktop && isDesktop) && !(panelNav.isSettingsRoute && !isDesktop)}
{#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}
<DesktopIconStrip
{sidebarOpen}
onSearchClick={() => {
if (chatSidebar?.activateSearchMode) {
chatSidebar.activateSearchMode();
}
sidebarOpen = true;
}}
/>
{/if}
<Sidebar.Inset class="flex flex-1 flex-col overflow-hidden">
{@render children?.()}
</Sidebar.Inset>
</div>
</Sidebar.Provider>
</Tooltip.Provider>
<svelte:window onkeydown={handleKeydown} bind:innerHeight bind:innerWidth />
<!-- PWA update prompt + version -->
<div class="fixed right-4 bottom-4 z-9999 flex flex-col items-end gap-1">
{#if showBuildVersion && buildInfoStore.value}
<span class="text-[10px] tabular-nums text-muted-foreground">{buildInfoStore.value}</span>
{/if}
<PwaRefreshAlert
needRefresh={$needRefresh || pwa.needRefreshByStorage}
forceReload={pwa.needRefreshByStorage}
{updateServiceWorker}
/>
</div>
+95
View File
@@ -0,0 +1,95 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { browser } from '$app/environment';
import { SearchInput, SidebarNavigationSearchResults } from '$lib/components/app';
import { ROUTES } from '$lib/constants/routes';
import { RouterService } from '$lib/services/router.service';
import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
import { isMobile } from '$lib/stores/viewport.svelte';
let searchQuery = $state('');
let searchInputRef = $state<HTMLInputElement | null>(null);
let currentChatId = $derived(page.params.id);
let filteredConversations = $derived.by(() => {
const query = searchQuery.trim().toLowerCase();
if (query.length === 0) return [];
return conversations().filter((c) => c.name.toLowerCase().includes(query));
});
// Search page is intended for mobile; on desktop the sidebar already exposes
// in-place search, so bounce back to a chat.
$effect(() => {
if (browser && !isMobile.current) {
goto(ROUTES.NEW_CHAT, { replaceState: true });
}
});
async function selectConversation(id: string) {
await goto(RouterService.chat(id));
}
async function handleEditConversation(id: string) {
const conversation = conversations().find((c) => c.id === id);
if (!conversation) return;
const newName = window.prompt('Rename conversation', conversation.name);
if (newName && newName.trim()) {
await conversationsStore.updateConversationName(id, newName.trim());
}
}
async function handleDeleteConversation(id: string) {
const conversation = conversations().find((c) => c.id === id);
if (!conversation) return;
const confirmed = window.confirm(
`Delete "${conversation.name}"? This action cannot be undone.`
);
if (!confirmed) return;
await conversationsStore.deleteConversation(id, { deleteWithForks: false });
}
function handleStopGeneration(id: string) {
chatStore.stopGenerationForChat(id);
}
function handleBack() {
if (history.length > 1) {
history.back();
} else {
goto(ROUTES.NEW_CHAT);
}
}
</script>
<svelte:head>
<title>Search · llama.cpp</title>
</svelte:head>
<div class="fixed top-0 z-10 left-0 right-0 p-2">
<SearchInput
autofocus
bind:value={searchQuery}
bind:ref={searchInputRef}
onClose={handleBack}
placeholder="Search conversations..."
/>
</div>
<div class="p-2 pt-16">
<SidebarNavigationSearchResults
{searchQuery}
{filteredConversations}
{currentChatId}
onSelect={selectConversation}
onEdit={handleEditConversation}
onDelete={handleDeleteConversation}
onStop={handleStopGeneration}
/>
</div>
+5 -7
View File
@@ -27,12 +27,10 @@
}
</script>
<div class="relative h-full">
<div class="fixed top-4.5 right-4 z-50 md:hidden">
<ActionIcon icon={X} tooltip="Close" onclick={handleClose} />
</div>
<div class="fixed top-4.5 right-4 z-50 md:hidden">
<ActionIcon icon={X} tooltip="Close" onclick={handleClose} />
</div>
<div class="min-h-full">
{@render children?.()}
</div>
<div class="min-h-full">
{@render children?.()}
</div>
@@ -1,12 +1,11 @@
<script lang="ts">
import { SettingsChat } from '$lib/components/app/settings';
import { page } from '$app/state';
import { replaceState } from '$app/navigation';
import { afterNavigate, replaceState } from '$app/navigation';
import { RouterService } from '$lib/services';
import { SETTINGS_SECTION_SLUGS } from '$lib/constants';
import { onMount } from 'svelte';
onMount(() => {
afterNavigate(() => {
if (!page.params.section) {
replaceState(RouterService.settings(SETTINGS_SECTION_SLUGS.GENERAL), {});
}