]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
Improve Mobile UI for dialogs and action dropdowns (#16222)
authorAleksander Grygier <redacted>
Mon, 29 Sep 2025 08:37:20 +0000 (10:37 +0200)
committerGitHub <redacted>
Mon, 29 Sep 2025 08:37:20 +0000 (10:37 +0200)
* fix: Always show conversation item actions

* feat: Improve Alert Dialog and Dialog mobile UI

* feat: Add settings reset to default confirmation

* fix: Close Edit dialog on save

* chore: update webui build output

* webui: implement proper z-index system and scroll management

- Add CSS variable for centralized z-index control
- Fix dropdown positioning with Settings dialog conflicts
- Prevent external scroll interference with proper event handling
- Clean up hardcoded z-index values for maintainable architecture

* webui: ensured the settings dialog enforces dynamic viewport height on mobile while retaining existing desktop sizing overrides

* feat: Use `dvh` instead of computed px height for dialogs max height on mobile

* chore: update webui build output

* feat: Improve Settings fields UI

* chore: update webui build output

* chore: update webui build output

---------

Co-authored-by: Pascal <redacted>
12 files changed:
tools/server/public/index.html.gz
tools/server/webui/src/app.css
tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsDialog.svelte
tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte
tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFooter.svelte
tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte
tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebarConversationItem.svelte
tools/server/webui/src/lib/components/app/misc/ActionDropdown.svelte
tools/server/webui/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte
tools/server/webui/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte
tools/server/webui/src/lib/components/ui/dialog/dialog-content.svelte
tools/server/webui/src/lib/components/ui/select/select-content.svelte

index c02b55f5f27a52d7cd4aee8ca5349dd8a345f66a..c1e6841d38d2b0e15e5817443dfb195bebbbb0a0 100644 (file)
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
index a9ac80ab9cf002166942dfe014d33eb2aa315824..c74319936149d94cd5803a8de8fe5c65edbf91ee 100644 (file)
@@ -39,6 +39,7 @@
        --sidebar-ring: oklch(0.708 0 0);
        --code-background: oklch(0.225 0 0);
        --code-foreground: oklch(0.875 0 0);
+       --layer-popover: 1000000;
 }
 
 .dark {
index ebe86c9ccad1b5460e3d17916f56cac355914a15..2099536d743012816f92f7ac180ce2214702ace6 100644 (file)
 
 <Dialog.Root {open} onOpenChange={handleClose}>
        <Dialog.Content
-               class="z-999999 flex h-[100vh] flex-col gap-0 rounded-none p-0 md:h-[64vh] md:rounded-lg"
+               class="z-999999 flex h-[100dvh] max-h-[100dvh] min-h-[100dvh] flex-col gap-0 rounded-none p-0
+                       md:h-[64vh] md:max-h-[64vh] md:min-h-0 md:rounded-lg"
                style="max-width: 48rem;"
        >
                <div class="flex flex-1 flex-col overflow-hidden md:flex-row">
                                </div>
                        </div>
 
-                       <ScrollArea class="max-h-[calc(100vh-13.5rem)] flex-1">
+                       <ScrollArea class="max-h-[calc(100dvh-13.5rem)] flex-1 md:max-h-[calc(100vh-13.5rem)]">
                                <div class="space-y-6 p-4 md:p-6">
                                        <div>
                                                <div class="mb-6 flex hidden items-center gap-2 border-b border-border/30 pb-6 md:flex">
index 7e11b2a92fe88a9930ff6d7c4e43ea2bb9c4ec37..e06399e0bc16361af986ecb22a14749b398845cc 100644 (file)
@@ -5,7 +5,6 @@
        import * as Select from '$lib/components/ui/select';
        import { Textarea } from '$lib/components/ui/textarea';
        import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
-       import { IsMobile } from '$lib/hooks/is-mobile.svelte';
        import { supportsVision } from '$lib/stores/server.svelte';
        import type { Component } from 'svelte';
 
@@ -17,8 +16,6 @@
        }
 
        let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props();
-
-       let isMobile = $state(new IsMobile());
 </script>
 
 {#each fields as field (field.key)}
@@ -33,7 +30,7 @@
                                value={String(localConfig[field.key] ?? '')}
                                onchange={(e) => onConfigChange(field.key, e.currentTarget.value)}
                                placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`}
-                               class={isMobile ? 'w-full' : 'max-w-md'}
+                               class="w-full md:max-w-md"
                        />
                        {#if field.help || SETTING_CONFIG_INFO[field.key]}
                                <p class="mt-1 text-xs text-muted-foreground">
@@ -50,7 +47,7 @@
                                value={String(localConfig[field.key] ?? '')}
                                onchange={(e) => onConfigChange(field.key, e.currentTarget.value)}
                                placeholder={`Default: ${SETTING_CONFIG_DEFAULT[field.key] ?? 'none'}`}
-                               class={isMobile ? 'min-h-[100px] w-full' : 'min-h-[100px] max-w-2xl'}
+                               class="min-h-[100px] w-full md:max-w-2xl"
                        />
                        {#if field.help || SETTING_CONFIG_INFO[field.key]}
                                <p class="mt-1 text-xs text-muted-foreground">
@@ -78,7 +75,7 @@
                                        }
                                }}
                        >
-                               <Select.Trigger class={isMobile ? 'w-full' : 'max-w-md'}>
+                               <Select.Trigger class="w-full md:w-auto md:max-w-md">
                                        <div class="flex items-center gap-2">
                                                {#if selectedOption?.icon}
                                                        {@const IconComponent = selectedOption.icon}
index e862cdb2bc68a051087ba0a1f30d097b23ac02bd..3408fe3ce4257f89a28b1ba3bd0605043fb58b73 100644 (file)
@@ -1,5 +1,6 @@
 <script lang="ts">
        import { Button } from '$lib/components/ui/button';
+       import * as AlertDialog from '$lib/components/ui/alert-dialog';
 
        interface Props {
                onReset?: () => void;
@@ -8,8 +9,15 @@
 
        let { onReset, onSave }: Props = $props();
 
-       function handleReset() {
+       let showResetDialog = $state(false);
+
+       function handleResetClick() {
+               showResetDialog = true;
+       }
+
+       function handleConfirmReset() {
                onReset?.();
+               showResetDialog = false;
        }
 
        function handleSave() {
 </script>
 
 <div class="flex justify-between border-t border-border/30 p-6">
-       <Button variant="outline" onclick={handleReset}>Reset to default</Button>
+       <Button variant="outline" onclick={handleResetClick}>Reset to default</Button>
 
        <Button onclick={handleSave}>Save settings</Button>
 </div>
+
+<AlertDialog.Root bind:open={showResetDialog}>
+       <AlertDialog.Content>
+               <AlertDialog.Header>
+                       <AlertDialog.Title>Reset Settings to Default</AlertDialog.Title>
+                       <AlertDialog.Description>
+                               Are you sure you want to reset all settings to their default values? This action cannot be
+                               undone and will permanently remove all your custom configurations.
+                       </AlertDialog.Description>
+               </AlertDialog.Header>
+               <AlertDialog.Footer>
+                       <AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
+                       <AlertDialog.Action onclick={handleConfirmReset}>Reset to Default</AlertDialog.Action>
+               </AlertDialog.Footer>
+       </AlertDialog.Content>
+</AlertDialog.Root>
index 6af348e696da72370cc3d889b05e920ff13f7744..8dd4b20dcbb97a9ebe96a845333c492825e7ebf5 100644 (file)
@@ -87,7 +87,7 @@
                <Sidebar.GroupContent>
                        <Sidebar.Menu>
                                {#each filteredConversations as conversation (conversation.id)}
-                                       <Sidebar.MenuItem class="mb-1" onclick={handleMobileSidebarItemClick}>
+                                       <Sidebar.MenuItem class="mb-1">
                                                <ChatSidebarConversationItem
                                                        conversation={{
                                                                id: conversation.id,
@@ -95,6 +95,7 @@
                                                                lastModified: conversation.lastModified,
                                                                currNode: conversation.currNode
                                                        }}
+                                                       {handleMobileSidebarItemClick}
                                                        isActive={currentChatId === conversation.id}
                                                        onSelect={selectConversation}
                                                        onEdit={editConversation}
index 71b4d1d5bdb41aff4904ffbeb92be3ed17c3d6de..6c3fb5764eb95f6e7908af6f30bd92948c592435 100644 (file)
@@ -8,6 +8,7 @@
        interface Props {
                isActive?: boolean;
                conversation: DatabaseConversation;
+               handleMobileSidebarItemClick?: () => void;
                onDelete?: (id: string) => void;
                onEdit?: (id: string, name: string) => void;
                onSelect?: (id: string) => void;
@@ -16,6 +17,7 @@
 
        let {
                conversation,
+               handleMobileSidebarItemClick,
                onDelete,
                onEdit,
                onSelect,
@@ -47,6 +49,7 @@
 
        function handleConfirmEdit() {
                if (!editedName.trim()) return;
+               showEditDialog = false;
                onEdit?.(conversation.id, editedName);
        }
 
                : ''}"
        onclick={handleSelect}
 >
-       <div class="text flex min-w-0 flex-1 items-center space-x-3">
+       <!-- svelte-ignore a11y_click_events_have_key_events -->
+       <!-- svelte-ignore a11y_no_static_element_interactions -->
+       <div
+               class="text flex min-w-0 flex-1 items-center space-x-3"
+               onclick={handleMobileSidebarItemClick}
+       >
                <div class="min-w-0 flex-1">
                        <p class="truncate text-sm font-medium">{conversation.name}</p>
 
                &:is(:hover) :global([data-slot='dropdown-menu-trigger']) {
                        opacity: 1;
                }
+               @media (max-width: 768px) {
+                       :global([data-slot='dropdown-menu-trigger']) {
+                               opacity: 1 !important;
+                       }
+               }
        }
 </style>
index e06bbc5086721a76d944b404c56ac434a53c4194..da29e2584f9674cd59d43fb94f06ad0cdba0a24e 100644 (file)
@@ -37,6 +37,7 @@
 <DropdownMenu.Root bind:open>
        <DropdownMenu.Trigger
                class="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md p-0 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground {triggerClass}"
+               onclick={(e) => e.stopPropagation()}
        >
                {#if triggerTooltip}
                        <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
@@ -53,7 +54,7 @@
                {/if}
        </DropdownMenu.Trigger>
 
-       <DropdownMenu.Content {align} class="z-999 w-48">
+       <DropdownMenu.Content {align} class="z-[999999] w-48">
                {#each actions as action, index (action.label)}
                        {#if action.separator && index > 0}
                                <DropdownMenu.Separator />
index 2d4a8fd62d4a355b2cc0461474c7ccd465475f1f..2398daee70b59dcc09da9219eedc411076a49116 100644 (file)
                bind:ref
                data-slot="alert-dialog-content"
                class={cn(
-                       'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',
+                       'fixed z-[999999] grid w-full gap-4 border bg-background p-6 shadow-lg duration-200',
+                       // Mobile: Bottom sheet behavior
+                       'right-0 bottom-0 left-0 max-h-[100dvh] translate-x-0 translate-y-0 overflow-y-auto rounded-t-lg',
+                       'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-bottom-full',
+                       'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-bottom-full',
+                       // Desktop: Centered dialog behavior
+                       'sm:top-[50%] sm:right-auto sm:bottom-auto sm:left-[50%] sm:max-h-[100vh] sm:max-w-lg sm:translate-x-[-50%] sm:translate-y-[-50%] sm:rounded-lg',
+                       'sm:data-[state=closed]:slide-out-to-bottom-0 sm:data-[state=closed]:zoom-out-95',
+                       'sm:data-[state=open]:slide-in-from-bottom-0 sm:data-[state=open]:zoom-in-95',
                        className
                )}
                {...restProps}
index 4b55eb65d155ac277ad466d217499dbe96a38485..da0f7be74b65a78018d4944120c5452556b5bbab 100644 (file)
 <div
        bind:this={ref}
        data-slot="alert-dialog-footer"
-       class={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
+       class={cn(
+               'mt-6 flex flex-row gap-2 sm:mt-0 sm:justify-end [&>*]:flex-1 sm:[&>*]:flex-none',
+               className
+       )}
        {...restProps}
 >
        {@render children?.()}
index 93d97bc4a6687e3a352fac5cb9cc228f6bb6e34f..74df0eacb57081b313f6c45846720f1f438d6824 100644 (file)
@@ -25,7 +25,7 @@
                bind:ref
                data-slot="dialog-content"
                class={cn(
-                       'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-border/30 bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',
+                       `fixed top-[50%] left-[50%] z-50 grid max-h-[100dvh] w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 overflow-y-auto rounded-lg border border-border/30 bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg md:max-h-[100vh]`,
                        className
                )}
                {...restProps}
index a5199584b64f2620687502de39cc1d30e5458d31..4050628cc8769c870c7d98db4d9bab118b368780 100644 (file)
@@ -1,4 +1,5 @@
 <script lang="ts">
+       import { onDestroy, onMount } from 'svelte';
        import { Select as SelectPrimitive } from 'bits-ui';
        import SelectScrollUpButton from './select-scroll-up-button.svelte';
        import SelectScrollDownButton from './select-scroll-down-button.svelte';
        }: WithoutChild<SelectPrimitive.ContentProps> & {
                portalProps?: SelectPrimitive.PortalProps;
        } = $props();
+
+       let cleanupInternalListeners: (() => void) | undefined;
+
+       onMount(() => {
+               const listenerOptions: AddEventListenerOptions = { passive: false };
+
+               const blockOutsideWheel = (event: WheelEvent) => {
+                       if (!ref) {
+                               return;
+                       }
+
+                       const target = event.target as Node | null;
+
+                       if (!target || !ref.contains(target)) {
+                               event.preventDefault();
+                               event.stopPropagation();
+                       }
+               };
+
+               const blockOutsideTouchMove = (event: TouchEvent) => {
+                       if (!ref) {
+                               return;
+                       }
+
+                       const target = event.target as Node | null;
+
+                       if (!target || !ref.contains(target)) {
+                               event.preventDefault();
+                               event.stopPropagation();
+                       }
+               };
+
+               document.addEventListener('wheel', blockOutsideWheel, listenerOptions);
+               document.addEventListener('touchmove', blockOutsideTouchMove, listenerOptions);
+
+               return () => {
+                       document.removeEventListener('wheel', blockOutsideWheel, listenerOptions);
+                       document.removeEventListener('touchmove', blockOutsideTouchMove, listenerOptions);
+               };
+       });
+
+       $effect(() => {
+               const element = ref;
+
+               cleanupInternalListeners?.();
+
+               if (!element) {
+                       return;
+               }
+
+               const stopWheelPropagation = (event: WheelEvent) => {
+                       event.stopPropagation();
+               };
+
+               const stopTouchPropagation = (event: TouchEvent) => {
+                       event.stopPropagation();
+               };
+
+               element.addEventListener('wheel', stopWheelPropagation);
+               element.addEventListener('touchmove', stopTouchPropagation);
+
+               cleanupInternalListeners = () => {
+                       element.removeEventListener('wheel', stopWheelPropagation);
+                       element.removeEventListener('touchmove', stopTouchPropagation);
+               };
+       });
+
+       onDestroy(() => {
+               cleanupInternalListeners?.();
+       });
 </script>
 
 <SelectPrimitive.Portal {...portalProps}>
@@ -22,7 +93,7 @@
                {sideOffset}
                data-slot="select-content"
                class={cn(
-                       'relative z-50 max-h-(--bits-select-content-available-height) min-w-[8rem] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:translate-y-1 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:-translate-x-1 data-[side=left]:slide-in-from-right-2 data-[side=right]:translate-x-1 data-[side=right]:slide-in-from-left-2 data-[side=top]:-translate-y-1 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
+                       'relative z-[var(--layer-popover,1000000)] max-h-(--bits-select-content-available-height) min-w-[8rem] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:translate-y-1 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:-translate-x-1 data-[side=left]:slide-in-from-right-2 data-[side=right]:translate-x-1 data-[side=right]:slide-in-from-left-2 data-[side=top]:-translate-y-1 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
                        className
                )}
                {...restProps}