mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-10 04:28:50 +01:00
🔧 fix+chore: Resolve Overflow in Settings Modal & Upgrade to Headless UI 2.0 (#2661)
* fix: dropdown overflow * fix: make dropdown work on mobile * feat: update headlessui to 2.0 and use portal * feat: rewrite modal using headlessui * fix: applying of maxHeight * fix: optimize backdrop for dark mode * fix: rendering dropdown width * feat: match small screen layout to radix-ui dialog * revert: mobile modifications * fix: modal animations * fix: z-index * chore: Migrate from HeadlessUI 1.0 to 2.0 * fix: h2 nesting * fix: use lighter border for PopoverButtons * feat: Move modal to the top if using a small screen * fix: mobile position * fix: frontend tests * feat: use row layout in mobile instead of col * fix: remove config path from tsconfig * fix: fix dropdown tests (gpt4o ftw!) * feat: Upgrade to latest headlessui version * fix:test1 * fix: ThemeSelector test * fix: re-add speech tab * style: use pl and pr-3 * fix: speech tab dropdowns * style: use maxHeight for language * feat: convert DropdownNoState to v2.0 * fix: use v2 params for voiceDropdown * style: reduce maxHeight for VoiceDropdown and set fixed width * chore: rebuild package-lock * style(fix): copy over the same styles for the settingsTab * style(fix): use -top-1 for speech tabs * style(fix): use max-w-[400px] --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
b34a4ddac1
commit
03fe361917
23 changed files with 573 additions and 328 deletions
|
|
@ -1,5 +1,12 @@
|
|||
import React, { FC } from 'react';
|
||||
import { Listbox } from '@headlessui/react';
|
||||
import React, { FC, useContext, useState } from 'react';
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
Transition,
|
||||
} from '@headlessui/react';
|
||||
import { AnchorPropsWithSelection } from '@headlessui/react/dist/internal/floating';
|
||||
import { cn } from '~/utils/';
|
||||
|
||||
type OptionType = {
|
||||
|
|
@ -7,17 +14,14 @@ type OptionType = {
|
|||
display?: string;
|
||||
};
|
||||
|
||||
type DropdownPosition = 'left' | 'right';
|
||||
|
||||
interface DropdownProps {
|
||||
value: string;
|
||||
label?: string;
|
||||
onChange: (value: string) => void;
|
||||
options: (string | OptionType)[];
|
||||
className?: string;
|
||||
position?: DropdownPosition;
|
||||
width?: number;
|
||||
maxHeight?: string;
|
||||
anchor?: AnchorPropsWithSelection;
|
||||
sizeClasses?: string;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
|
|
@ -27,16 +31,10 @@ const Dropdown: FC<DropdownProps> = ({
|
|||
onChange,
|
||||
options,
|
||||
className = '',
|
||||
position = 'right',
|
||||
width,
|
||||
maxHeight = 'auto',
|
||||
anchor,
|
||||
sizeClasses,
|
||||
testId = 'dropdown-menu',
|
||||
}) => {
|
||||
const positionClasses = {
|
||||
right: 'origin-bottom-left left-0',
|
||||
left: 'origin-bottom-right right-0',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<Listbox
|
||||
|
|
@ -46,13 +44,14 @@ const Dropdown: FC<DropdownProps> = ({
|
|||
}}
|
||||
>
|
||||
<div className={cn('relative', className)}>
|
||||
<Listbox.Button
|
||||
<ListboxButton
|
||||
data-testid={testId}
|
||||
className={cn(
|
||||
'relative inline-flex items-center justify-between rounded-md border-gray-300 bg-white py-2 pl-3 pr-8 text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
|
||||
'relative inline-flex items-center justify-between rounded-md border-gray-50 bg-white py-2 pl-3 pr-8 text-black transition-all duration-100 ease-in-out hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 dark:focus:ring-white dark:focus:ring-offset-gray-700',
|
||||
'w-auto',
|
||||
className,
|
||||
)}
|
||||
aria-label="Select an option"
|
||||
>
|
||||
<span className="block truncate">
|
||||
{label}
|
||||
|
|
@ -67,35 +66,45 @@ const Dropdown: FC<DropdownProps> = ({
|
|||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
className="h-4 w-5 rotate-0 transform text-gray-400 transition-transform duration-300 ease-in-out"
|
||||
className="h-4 w-5 rotate-0 transform text-black transition-transform duration-300 ease-in-out dark:text-gray-50"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
<Listbox.Options
|
||||
className={cn(
|
||||
`absolute z-50 mt-1 flex max-h-[40vh] flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-300 bg-white p-1.5 text-gray-700 shadow-lg transition-opacity focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white ${positionClasses[position]}`,
|
||||
className,
|
||||
)}
|
||||
style={{ width: width ? `${width}px` : 'auto', maxHeight: maxHeight }}
|
||||
</ListboxButton>
|
||||
<Transition
|
||||
leave="transition ease-in duration-50"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
{options.map((item, index) => (
|
||||
<Listbox.Option
|
||||
key={index}
|
||||
value={typeof item === 'string' ? item : item.value}
|
||||
className={cn(
|
||||
'relative cursor-pointer select-none rounded border-gray-300 bg-white py-2.5 pl-3 pr-6 text-gray-700 hover:bg-gray-100 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
|
||||
)}
|
||||
style={{ width: '100%' }}
|
||||
data-theme={typeof item === 'string' ? item : (item as OptionType).value}
|
||||
>
|
||||
<span className="block truncate">
|
||||
{typeof item === 'string' ? item : (item as OptionType).display}
|
||||
</span>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
<ListboxOptions
|
||||
className={cn(
|
||||
'absolute z-50 mt-1 flex flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-300 bg-white bg-white p-1.5 text-gray-700 shadow-lg transition-opacity focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||
sizeClasses,
|
||||
className,
|
||||
)}
|
||||
anchor={anchor}
|
||||
aria-label="List of options"
|
||||
>
|
||||
{options.map((item, index) => (
|
||||
<ListboxOption
|
||||
key={index}
|
||||
value={typeof item === 'string' ? item : item.value}
|
||||
className={cn(
|
||||
'relative cursor-pointer select-none rounded border-gray-300 bg-white py-2.5 pl-3 pr-3 text-sm text-gray-700 hover:bg-gray-100 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
|
||||
)}
|
||||
style={{ width: '100%' }}
|
||||
data-theme={typeof item === 'string' ? item : (item as OptionType).value}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="block truncate">
|
||||
{typeof item === 'string' ? item : (item as OptionType).display}
|
||||
</span>
|
||||
</div>
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue