--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 {
<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">
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';
}
let { fields, localConfig, onConfigChange, onThemeChange }: Props = $props();
-
- let isMobile = $state(new IsMobile());
</script>
{#each fields as field (field.key)}
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">
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">
}
}}
>
- <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}
<script lang="ts">
import { Button } from '$lib/components/ui/button';
+ import * as AlertDialog from '$lib/components/ui/alert-dialog';
interface Props {
onReset?: () => void;
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>
<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,
lastModified: conversation.lastModified,
currNode: conversation.currNode
}}
+ {handleMobileSidebarItemClick}
isActive={currentChatId === conversation.id}
onSelect={selectConversation}
onEdit={editConversation}
interface Props {
isActive?: boolean;
conversation: DatabaseConversation;
+ handleMobileSidebarItemClick?: () => void;
onDelete?: (id: string) => void;
onEdit?: (id: string, name: string) => void;
onSelect?: (id: string) => void;
let {
conversation,
+ handleMobileSidebarItemClick,
onDelete,
onEdit,
onSelect,
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>
<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}>
{/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 />
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}
<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?.()}
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}
<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}>
{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}