🔧 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:
Anirudh 2024-07-11 02:15:58 +05:30 committed by GitHub
parent b34a4ddac1
commit 03fe361917
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 573 additions and 328 deletions

View file

@ -31,7 +31,7 @@
"@ariakit/react": "^0.4.5",
"@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4",
"@headlessui/react": "^1.7.13",
"@headlessui/react": "^2.1.2",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.3",

View file

@ -122,7 +122,7 @@ export default function PopoverButtons({
type="button"
className={cn(
button.buttonClass,
'border-2 border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
'border border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
'ml-1 h-full bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white',
buttonClass ?? '',
)}
@ -141,7 +141,7 @@ export default function PopoverButtons({
type="button"
className={cn(
button.buttonClass,
'flex justify-center border-2 border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
'flex justify-center border border-gray-300/50 focus:ring-1 focus:ring-green-500/90 dark:border-gray-500/50 dark:focus:ring-green-500',
'h-full w-full bg-transparent px-2 py-1 text-xs font-medium font-normal text-black hover:bg-gray-100 hover:text-black dark:bg-transparent dark:text-white dark:hover:bg-gray-600 dark:hover:text-white',
buttonClass ?? '',
)}

View file

@ -178,7 +178,7 @@ const SetKeyDialog = ({
value={expiresAtLabel}
onChange={handleExpirationChange}
options={expirationOptions.map((option) => option.display)}
width={185}
sizeClasses="w-[185px]"
/>
<FormProvider {...methods}>
<EndpointComponent

View file

@ -1,4 +1,4 @@
import { Disclosure } from '@headlessui/react';
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
import { useCallback, memo, ReactNode } from 'react';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TResPlugin, TInput } from 'librechat-data-provider';
@ -98,12 +98,12 @@ const Plugin: React.FC<PluginProps> = ({ plugin }) => {
</div>
</div>
{plugin.loading && <Spinner className="ml-1 text-black" />}
<Disclosure.Button className="ml-12 flex items-center gap-2">
<DisclosureButton className="ml-12 flex items-center gap-2">
<ChevronDownIcon {...iconProps} />
</Disclosure.Button>
</DisclosureButton>
</div>
<Disclosure.Panel className="mt-3 flex max-w-full flex-col gap-3">
<DisclosurePanel className="mt-3 flex max-w-full flex-col gap-3">
<CodeBlock
lang={latestPlugin ? `REQUEST TO ${latestPlugin?.toUpperCase()}` : 'REQUEST'}
codeChildren={formatInputs(plugin.inputs ?? [])}
@ -120,7 +120,7 @@ const Plugin: React.FC<PluginProps> = ({ plugin }) => {
classProp="max-h-[450px]"
/>
)}
</Disclosure.Panel>
</DisclosurePanel>
</>
);
}}

View file

@ -1,7 +1,7 @@
import { FileText } from 'lucide-react';
import { useRecoilState } from 'recoil';
import { Fragment, useState, memo } from 'react';
import { Menu, Transition } from '@headlessui/react';
import { Menu, MenuItem, MenuButton, MenuItems, Transition } from '@headlessui/react';
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
import FilesView from '~/components/Chat/Input/Files/FilesView';
import { useAuthContext } from '~/hooks/AuthContext';
@ -32,7 +32,7 @@ function NavLinks() {
<Menu as="div" className="group relative">
{({ open }) => (
<>
<Menu.Button
<MenuButton
className={cn(
'group-ui-open:bg-gray-100 dark:group-ui-open:bg-gray-700 duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800',
open ? 'bg-gray-100 dark:bg-gray-800' : '',
@ -64,7 +64,7 @@ function NavLinks() {
>
{user?.name || user?.username || localize('com_nav_user')}
</div>
</Menu.Button>
</MenuButton>
<Transition
as={Fragment}
@ -75,7 +75,7 @@ function NavLinks() {
leaveFrom="translate-y-0 opacity-100"
leaveTo="translate-y-2 opacity-0"
>
<Menu.Items className="absolute bottom-full left-0 z-[100] mb-1 mt-1 w-full translate-y-0 overflow-hidden rounded-lg border border-gray-300 bg-white p-1.5 opacity-100 shadow-lg outline-none dark:border-gray-600 dark:bg-gray-700">
<MenuItems className="absolute bottom-full left-0 z-[100] mb-1 mt-1 w-full translate-y-0 overflow-hidden rounded-lg border border-gray-300 bg-white p-1.5 opacity-100 shadow-lg outline-none dark:border-gray-600 dark:bg-gray-700">
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="none">
{user?.email || localize('com_nav_user')}
</div>
@ -90,34 +90,34 @@ function NavLinks() {
<div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
</>
)}
<Menu.Item as="div">
<MenuItem as="div">
<NavLink
svg={() => <FileText className="icon-md" />}
text={localize('com_nav_my_files')}
clickHandler={() => setShowFiles(true)}
/>
</Menu.Item>
</MenuItem>
{startupConfig?.helpAndFaqURL !== '/' && (
<Menu.Item as="div">
<MenuItem as="div">
<NavLink
svg={() => <LinkIcon />}
text={localize('com_nav_help_faq')}
clickHandler={() => window.open(startupConfig?.helpAndFaqURL, '_blank')}
/>
</Menu.Item>
</MenuItem>
)}
<Menu.Item as="div">
<MenuItem as="div">
<NavLink
svg={() => <GearIcon className="icon-md" />}
text={localize('com_nav_settings')}
clickHandler={() => setShowSettings(true)}
/>
</Menu.Item>
</MenuItem>
<div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
<Menu.Item as="div">
<MenuItem as="div">
<Logout />
</Menu.Item>
</Menu.Items>
</MenuItem>
</MenuItems>
</Transition>
</>
)}

View file

@ -2,7 +2,14 @@ import * as Tabs from '@radix-ui/react-tabs';
import { MessageSquare } from 'lucide-react';
import { SettingsTabValues } from 'librechat-data-provider';
import type { TDialogProps } from '~/common';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui';
import {
Button,
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import { GearIcon, DataIcon, SpeechIcon, UserIcon, ExperimentIcon } from '~/components/svg';
import { General, Messages, Speech, Beta, Data, Account } from './SettingsTabs';
import { useMediaQuery, useLocalize } from '~/hooks';
@ -13,23 +20,72 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
const localize = useLocalize();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
disableScroll={isSmallScreen}
<Transition appear show={open}>
<Dialog as="div" className="relative z-50 focus:outline-none" onClose={onOpenChange}>
<TransitionChild
enter="ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/50 dark:bg-black/80" aria-hidden="true" />
</TransitionChild>
<TransitionChild
enter="ease-out duration-200"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-100"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div
className={cn(
'max-h-[90vh] overflow-auto shadow-2xl md:min-h-[500px] md:w-[680px]',
isSmallScreen ? 'min-h-[200px]' : '',
'fixed inset-0 flex w-screen items-center justify-center p-4',
isSmallScreen ? '' : '',
)}
>
<DialogHeader>
<DialogTitle className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
<DialogPanel
className={cn(
'overflow-hidden rounded-xl rounded-b-lg bg-white pb-6 shadow-2xl backdrop-blur-2xl animate-in dark:bg-gray-700 sm:rounded-lg md:min-h-[373px] md:w-[680px]',
)}
>
<DialogTitle
className="mb-3 flex items-center justify-between border-b border-black/10 p-6 pb-5 text-left dark:border-white/10"
as="div"
>
<h2 className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
{localize('com_nav_settings')}
</h2>
<button
type="button"
className="rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900 dark:data-[state=open]:bg-gray-800"
onClick={() => onOpenChange(false)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-5 w-5 text-black dark:text-white"
>
<line x1="18" x2="6" y1="6" y2="18"></line>
<line x1="6" x2="18" y1="6" y2="18"></line>
</svg>
<span className="sr-only">Close</span>
</button>
</DialogTitle>
</DialogHeader>
<div className="max-h-[373px] overflow-auto px-6 md:min-h-[373px] md:w-[680px]">
<Tabs.Root
defaultValue={SettingsTabValues.GENERAL}
className="flex flex-col gap-3 md:flex-row"
className="flex flex-col gap-10 md:flex-row"
orientation="horizontal"
>
<Tabs.List
@ -37,18 +93,17 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
role="tablist"
aria-orientation="horizontal"
className={cn(
isSmallScreen
? 'hide-scrollbar flex flex-row space-x-4 overflow-x-auto'
: 'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-col flex-wrap overflow-auto sm:max-w-none',
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
isSmallScreen ? 'flex-row rounded-lg bg-gray-200 p-1 dark:bg-gray-800' : '',
)}
style={{ outline: 'none' }}
>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.GENERAL}
@ -59,10 +114,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.MESSAGES}
@ -73,10 +128,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.BETA}
@ -87,10 +142,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.SPEECH}
@ -101,10 +156,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.DATA}
@ -115,10 +170,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</Tabs.Trigger>
<Tabs.Trigger
className={cn(
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-black radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600',
isSmallScreen
? 'flex-row items-center justify-center text-sm radix-state-active:bg-gray-100'
: 'bg-white radix-state-active:bg-gray-100',
? 'flex-1 items-center justify-center text-nowrap text-sm dark:text-gray-500 dark:radix-state-active:text-white'
: 'bg-white radix-state-active:bg-gray-200',
isSmallScreen ? '' : 'dark:bg-gray-700',
)}
value={SettingsTabValues.ACCOUNT}
@ -128,7 +183,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
{localize('com_nav_setting_account')}
</Tabs.Trigger>
</Tabs.List>
<div className="h-auto min-h-[280px] overflow-auto sm:w-full sm:max-w-none">
<div className="max-h-[373px] overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
<General />
<Messages />
<Beta />
@ -138,7 +193,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</div>
</Tabs.Root>
</div>
</DialogContent>
</DialogPanel>
</div>
</TransitionChild>
</Dialog>
</Transition>
);
}

View file

@ -34,9 +34,8 @@ export const ThemeSelector = ({
value={theme}
onChange={onChange}
options={themeOptions}
width={180}
position={'left'}
maxHeight="200px"
sizeClasses="w-[220px]"
anchor="bottom start"
testId="theme-selector"
/>
</div>
@ -111,8 +110,8 @@ export const LangSelector = ({
<Dropdown
value={langcode}
onChange={onChange}
position={'left'}
maxHeight="271px"
sizeClasses="[--anchor-max-height:256px]"
anchor="bottom start"
options={languageOptions}
/>
</div>

View file

@ -13,6 +13,11 @@ describe('LangSelector', () => {
});
it('renders correctly', () => {
global.ResizeObserver = class MockedResizeObserver {
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
};
const { getByText } = render(
<RecoilRoot>
<LangSelector langcode="en-US" onChange={mockOnChange} />
@ -24,6 +29,11 @@ describe('LangSelector', () => {
});
it('calls onChange when the select value changes', async () => {
global.ResizeObserver = class MockedResizeObserver {
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
};
const { getByText, getByTestId } = render(
<RecoilRoot>
<LangSelector langcode="en-US" onChange={mockOnChange} />

View file

@ -1,6 +1,8 @@
// ThemeSelector.spec.tsx
import 'test/matchMedia.mock';
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { ThemeSelector } from './General';
import { RecoilRoot } from 'recoil';
@ -13,6 +15,11 @@ describe('ThemeSelector', () => {
});
it('renders correctly', () => {
global.ResizeObserver = class MockedResizeObserver {
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
};
const { getByText } = render(
<RecoilRoot>
<ThemeSelector theme="system" onChange={mockOnChange} />
@ -24,6 +31,11 @@ describe('ThemeSelector', () => {
});
it('calls onChange when the select value changes', async () => {
global.ResizeObserver = class MockedResizeObserver {
observe = jest.fn();
unobserve = jest.fn();
disconnect = jest.fn();
};
const { getByText, getByTestId } = render(
<RecoilRoot>
<ThemeSelector theme="system" onChange={mockOnChange} />
@ -42,9 +54,9 @@ describe('ThemeSelector', () => {
const darkOption = getByText('Dark');
fireEvent.click(darkOption);
// Ensure that the onChange is called with the expected value after a short delay
await new Promise((resolve) => setTimeout(resolve, 0));
// Ensure that the onChange is called with the expected value
await waitFor(() => {
expect(mockOnChange).toHaveBeenCalledWith('dark');
});
});
});

View file

@ -29,8 +29,8 @@ export const ForkSettings = () => {
value={forkSetting}
onChange={setForkSetting}
options={forkOptions}
position={'left'}
maxHeight="199px"
sizeClasses="w-[200px]"
anchor="bottom start"
testId="fork-setting-dropdown"
/>
</div>

View file

@ -22,8 +22,8 @@ export default function EngineSTTDropdown() {
value={engineSTT}
onChange={handleSelect}
options={endpointOptions}
width={180}
position={'left'}
sizeClasses="w-[180px]"
anchor="bottom start"
testId="EngineSTTDropdown"
/>
</div>

View file

@ -98,8 +98,8 @@ export default function LanguageSTTDropdown() {
value={languageSTT}
onChange={handleSelect}
options={languageOptions}
width={220}
position={'left'}
sizeClasses="[--anchor-max-height:256px]"
anchor="bottom start"
testId="LanguageSTTDropdown"
/>
</div>

View file

@ -130,7 +130,7 @@ function Speech() {
<Tabs.Content
value={SettingsTabValues.SPEECH}
role="tabpanel"
className="w-full px-4 md:min-h-[300px]"
className="w-full md:min-h-[271px]"
ref={contentRef}
>
<Tabs.Root
@ -138,8 +138,8 @@ function Speech() {
orientation="horizontal"
value={advancedMode ? 'advanced' : 'simple'}
>
<div className="sticky top-0 z-50 bg-white dark:bg-gray-700">
<Tabs.List className="sticky top-0 mb-4 flex justify-center bg-white dark:bg-gray-700">
<div className="sticky -top-1 z-50 mb-4 bg-white dark:bg-gray-700">
<Tabs.List className="flex justify-center bg-white dark:bg-gray-700">
<Tabs.Trigger
onClick={() => setAdvancedMode(false)}
className={cn(

View file

@ -22,8 +22,8 @@ export default function EngineTTSDropdown() {
value={engineTTS}
onChange={handleSelect}
options={endpointOptions}
width={180}
position={'left'}
sizeClasses="w-[180px]"
anchor="bottom start"
testId="EngineTTSDropdown"
/>
</div>

View file

@ -67,6 +67,8 @@ export default function VoiceDropdown() {
value={voice}
onChange={setVoice}
options={voiceOptions}
sizeClasses="min-w-[200px] !max-w-[400px] [--anchor-max-width:400px]"
anchor="bottom start"
position="left"
testId="VoiceDropdown"
/>

View file

@ -1,5 +1,5 @@
import { Search, X } from 'lucide-react';
import { Dialog } from '@headlessui/react';
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react';
import { useState, useEffect, useCallback } from 'react';
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
import type { TError, TPlugin, TPluginAction } from 'librechat-data-provider';
@ -134,16 +134,16 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
<div className="fixed inset-0 bg-gray-600/65 transition-opacity dark:bg-black/80" />
{/* Full-screen container to center the panel */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel
<DialogPanel
className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-white text-left shadow-xl transition-all dark:bg-gray-700 max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
style={{ minHeight: '610px' }}
>
<div className="flex items-center justify-between border-b-[1px] border-black/10 p-6 pb-4 dark:border-white/10">
<div className="flex items-center">
<div className="text-center sm:text-left">
<Dialog.Title className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
<DialogTitle className="text-lg font-medium leading-6 text-gray-800 dark:text-gray-200">
{localize('com_nav_plugin_store')}
</Dialog.Title>
</DialogTitle>
</div>
</div>
<div>
@ -236,7 +236,7 @@ function PluginStoreDialog({ isOpen, setIsOpen }: TPluginStoreDialogProps) {
</div> */}
</div>
</div>
</Dialog.Panel>
</DialogPanel>
</div>
</Dialog>
);

View file

@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { Search, X } from 'lucide-react';
import { Dialog } from '@headlessui/react';
import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react';
import { useFormContext } from 'react-hook-form';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import type { AssistantsEndpoint, TError, TPluginAction } from 'librechat-data-provider';
@ -142,19 +142,19 @@ function ToolSelectDialog({
<div className="fixed inset-0 bg-gray-600/65 transition-opacity dark:bg-black/80" />
{/* Full-screen container to center the panel */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel
<DialogPanel
className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-white text-left shadow-xl transition-all dark:bg-gray-800 max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
style={{ minHeight: '610px' }}
>
<div className="flex items-center justify-between border-b-[1px] border-black/10 px-4 pb-4 pt-5 dark:border-white/10 sm:p-6">
<div className="flex items-center">
<div className="text-center sm:text-left">
<Dialog.Title className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
<DialogTitle className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-200">
{localize('com_nav_tool_dialog')}
</Dialog.Title>
<Dialog.Description className="text-sm text-gray-500 dark:text-gray-300">
</DialogTitle>
<Description className="text-sm text-gray-500 dark:text-gray-300">
{localize('com_nav_tool_dialog_description')}
</Dialog.Description>
</Description>
</div>
</div>
<div>
@ -232,7 +232,7 @@ function ToolSelectDialog({
)}
</div>
</div>
</Dialog.Panel>
</DialogPanel>
</div>
</Dialog>
);

View file

@ -1,5 +1,12 @@
import React, { FC, useState } 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,18 +31,12 @@ const Dropdown: FC<DropdownProps> = ({
onChange,
options,
className = '',
position = 'right',
width,
maxHeight = 'auto',
anchor,
sizeClasses,
testId = 'dropdown-menu',
}) => {
const [selectedValue, setSelectedValue] = useState(initialValue);
const positionClasses = {
right: 'origin-bottom-left left-0',
left: 'origin-bottom-right right-0',
};
return (
<div className={cn('relative', className)}>
<Listbox
@ -49,7 +47,7 @@ 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-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',
@ -76,21 +74,27 @@ const Dropdown: FC<DropdownProps> = ({
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</Listbox.Button>
<Listbox.Options
</ListboxButton>
<Transition
leave="transition ease-in duration-50"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<ListboxOptions
className={cn(
`absolute z-50 mt-1 flex max-h-[40vh] flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-100 bg-white p-1.5 text-black shadow-lg transition-opacity dark:border-gray-600 dark:bg-gray-700 dark:text-white ${positionClasses[position]}`,
'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,
)}
style={{ width: width ? `${width}px` : 'auto', maxHeight: maxHeight }}
anchor={anchor}
aria-label="List of options"
>
{options.map((item, index) => (
<Listbox.Option
<ListboxOption
key={index}
value={typeof item === 'string' ? item : item.value}
className={cn(
'duration-50 relative cursor-pointer select-none rounded border-gray-50 bg-white py-2.5 pl-3 pr-2 text-black transition-all ease-in-out hover:bg-gray-100 focus:bg-gray-200 dark:border-gray-50 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 dark:focus:bg-gray-500',
'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}
@ -119,9 +123,10 @@ const Dropdown: FC<DropdownProps> = ({
</span>
)}
</div>
</Listbox.Option>
</ListboxOption>
))}
</Listbox.Options>
</ListboxOptions>
</Transition>
</div>
</Listbox>
</div>

View file

@ -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
</ListboxButton>
<Transition
leave="transition ease-in duration-50"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<ListboxOptions
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]}`,
'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,
)}
style={{ width: width ? `${width}px` : 'auto', maxHeight: maxHeight }}
anchor={anchor}
aria-label="List of options"
>
{options.map((item, index) => (
<Listbox.Option
<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-6 text-gray-700 hover:bg-gray-100 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
'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>
</Listbox.Option>
</div>
</ListboxOption>
))}
</Listbox.Options>
</ListboxOptions>
</Transition>
</div>
</Listbox>
</div>

View file

@ -1,5 +1,12 @@
import React, { useState, useRef } from 'react';
import { Listbox, Transition } from '@headlessui/react';
import {
Listbox,
ListboxButton,
Label,
ListboxOptions,
ListboxOption,
Transition,
} from '@headlessui/react';
import { Wrench, ArrowRight } from 'lucide-react';
import { CheckMark } from '~/components/svg';
import useOnClickOutside from '~/hooks/useOnClickOutside';
@ -74,7 +81,7 @@ function MultiSelectDropDown({
<Listbox value={value} onChange={handleSelect} disabled={disabled}>
{() => (
<>
<Listbox.Button
<ListboxButton
className={cn(
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:outline-none focus:ring-0 focus:ring-offset-0 dark:border-gray-600 dark:border-white/20 dark:bg-gray-800 sm:text-sm',
className ?? '',
@ -85,13 +92,13 @@ function MultiSelectDropDown({
>
{' '}
{showLabel && (
<Listbox.Label
<Label
className={cn('block text-xs text-gray-700 dark:text-gray-500', labelClassName)}
id={excludeIds[1]}
data-headlessui-state=""
>
{title}
</Listbox.Label>
</Label>
)}
<span className="inline-flex w-full truncate" id={excludeIds[2]}>
<span
@ -144,7 +151,7 @@ function MultiSelectDropDown({
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</Listbox.Button>
</ListboxButton>
<Transition
show={isOpen}
as={React.Fragment}
@ -153,7 +160,7 @@ function MultiSelectDropDown({
leaveTo="opacity-0"
{...transitionProps}
>
<Listbox.Options
<ListboxOptions
ref={menuRef}
className={cn(
'absolute z-50 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%]',
@ -167,7 +174,7 @@ function MultiSelectDropDown({
}
const selected = isSelected(option[optionValueKey]);
return (
<Listbox.Option
<ListboxOption
key={i}
value={option[optionValueKey]}
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-800 last:border-0 hover:bg-gray-20 dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
@ -208,10 +215,10 @@ function MultiSelectDropDown({
</span>
)}
</span>
</Listbox.Option>
</ListboxOption>
);
})}
</Listbox.Options>
</ListboxOptions>
</Transition>
</>
)}

View file

@ -1,5 +1,12 @@
import React from 'react';
import { Listbox, Transition } from '@headlessui/react';
import {
Listbox,
ListboxButton,
Label,
ListboxOptions,
ListboxOption,
Transition,
} from '@headlessui/react';
import type { Option, OptionWithIcon } from '~/common';
import CheckMark from '../svg/CheckMark';
import { useLocalize } from '~/hooks';
@ -84,8 +91,7 @@ function SelectDropDown({
<Listbox value={value} onChange={setValue} disabled={disabled}>
{({ open }) => (
<>
<Listbox.Button
tabIndex={tabIndex}
<ListboxButton
data-testid="select-dropdown-button"
className={cn(
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left dark:border-gray-600 dark:bg-gray-700 sm:text-sm',
@ -94,13 +100,13 @@ function SelectDropDown({
>
{' '}
{showLabel && (
<Listbox.Label
<Label
className="block text-xs text-gray-700 dark:text-gray-500 "
id="headlessui-listbox-label-:r1:"
data-headlessui-state=""
>
{title}
</Listbox.Label>
</Label>
)}
<span className="inline-flex w-full truncate">
<span
@ -138,7 +144,7 @@ function SelectDropDown({
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</Listbox.Button>
</ListboxButton>
<Transition
show={open}
as={React.Fragment}
@ -147,14 +153,14 @@ function SelectDropDown({
leaveTo="opacity-0"
{...transitionProps}
>
<Listbox.Options
<ListboxOptions
className={cn(
'absolute z-10 mt-2 max-h-60 w-full overflow-auto rounded border bg-white text-xs ring-black/10 dark:border-gray-600 dark:bg-gray-700 dark:ring-white/20 md:w-[100%]',
optionsListClass ?? '',
)}
>
{renderOption && (
<Listbox.Option
<ListboxOption
key={'listbox-render-option'}
value={null}
className={cn(
@ -163,7 +169,7 @@ function SelectDropDown({
)}
>
{renderOption()}
</Listbox.Option>
</ListboxOption>
)}
{searchRender}
{options.map((option: string | Option, i: number) => {
@ -181,7 +187,7 @@ function SelectDropDown({
}
return (
<Listbox.Option
<ListboxOption
key={i}
value={currentValue}
className={({ active }) =>
@ -214,10 +220,10 @@ function SelectDropDown({
</span>
)}
</span>
</Listbox.Option>
</ListboxOption>
);
})}
</Listbox.Options>
</ListboxOptions>
</Transition>
</>
)}

View file

@ -16,3 +16,7 @@ import '@testing-library/jest-dom/extend-expect';
// Mock canvas when run unit test cases with jest.
// 'react-lottie' uses canvas
import 'jest-canvas-mock';
beforeEach(() => {
jest.clearAllMocks();
});

189
package-lock.json generated
View file

@ -1116,7 +1116,7 @@
"@ariakit/react": "^0.4.5",
"@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4",
"@headlessui/react": "^1.7.13",
"@headlessui/react": "^2.1.2",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-checkbox": "^1.0.3",
@ -5960,12 +5960,28 @@
"@floating-ui/utils": "^0.2.1"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz",
"integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==",
"node_modules/@floating-ui/react": {
"version": "0.26.19",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.19.tgz",
"integrity": "sha512-Jk6zITdjjIvjO/VdQFvpRaD3qPwOHH6AoDHxjhpy+oK4KFgaSP871HYWUAPdnLmx1gQ+w/pB312co3tVml+BXA==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.6.1"
"@floating-ui/react-dom": "^2.1.1",
"@floating-ui/utils": "^0.2.4",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz",
"integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
@ -5973,9 +5989,10 @@
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz",
"integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==",
"license": "MIT"
},
"node_modules/@google/generative-ai": {
"version": "0.1.3",
@ -6015,19 +6032,22 @@
}
},
"node_modules/@headlessui/react": {
"version": "1.7.18",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.18.tgz",
"integrity": "sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.1.2.tgz",
"integrity": "sha512-Kb3hgk9gRNRcTZktBrKdHhF3xFhYkca1Rk6e1/im2ENf83dgN54orMW0uSKTXFnUpZOUFZ+wcY05LlipwgZIFQ==",
"license": "MIT",
"dependencies": {
"@tanstack/react-virtual": "^3.0.0-beta.60",
"client-only": "^0.0.1"
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.17.1",
"@react-aria/interactions": "^3.21.3",
"@tanstack/react-virtual": "^3.8.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18",
"react-dom": "^16 || ^17 || ^18"
"react": "^18",
"react-dom": "^18"
}
},
"node_modules/@humanwhocodes/config-array": {
@ -8531,6 +8551,86 @@
"node": ">=8.x"
}
},
"node_modules/@react-aria/focus": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.17.1.tgz",
"integrity": "sha512-FLTySoSNqX++u0nWZJPPN5etXY0WBxaIe/YuL/GTEeuqUIuC/2bJSaw5hlsM6T2yjy6Y/VAxBcKSdAFUlU6njQ==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.21.3",
"@react-aria/utils": "^3.24.1",
"@react-types/shared": "^3.23.1",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/focus/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@react-aria/interactions": {
"version": "3.21.3",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.21.3.tgz",
"integrity": "sha512-BWIuf4qCs5FreDJ9AguawLVS0lV9UU+sK4CCnbCNNmYqOWY+1+gRXCsnOM32K+oMESBxilAjdHW5n1hsMqYMpA==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.4",
"@react-aria/utils": "^3.24.1",
"@react-types/shared": "^3.23.1",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.4",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.4.tgz",
"integrity": "sha512-4jmAigVq409qcJvQyuorsmBR4+9r3+JEC60wC+Y0MZV0HCtTmm8D9guYXlJMdx0SSkgj0hHAyFm/HvPNFofCoQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/utils": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.24.1.tgz",
"integrity": "sha512-O3s9qhPMd6n42x9sKeJ3lhu5V1Tlnzhu6Yk8QOvDuXf7UGuUjXf9mzfHJt1dYzID4l9Fwm8toczBzPM9t0jc8Q==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.4",
"@react-stately/utils": "^3.10.1",
"@react-types/shared": "^3.23.1",
"@swc/helpers": "^0.5.0",
"clsx": "^2.0.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-aria/utils/node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/@react-dnd/asap": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
@ -8546,6 +8646,27 @@
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
"integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
},
"node_modules/@react-stately/utils": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.1.tgz",
"integrity": "sha512-VS/EHRyicef25zDZcM/ClpzYMC5i2YGN6uegOeQawmgfGjb02yaCX0F0zR69Pod9m2Hr3wunTbtpgVXvYbZItg==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-types/shared": {
"version": "3.23.1",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.23.1.tgz",
"integrity": "sha512-5d+3HbFDxGZjhbMBeFHRQhexMFt4pUce3okyRtUVKbbedQFUrtXSBg9VszgF2RTeQDKDkMCIQDtz5ccP/Lk1gw==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@remix-run/router": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz",
@ -9542,6 +9663,15 @@
"sourcemap-codec": "^1.4.8"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.11",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz",
"integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tanstack/match-sorter-utils": {
"version": "8.11.8",
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.11.8.tgz",
@ -9633,11 +9763,12 @@
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.0.4.tgz",
"integrity": "sha512-tiqKW/e2MJVCr7/pRUXulpkyxllaOclkHNfhKTo4pmHjJIqnhMfwIjc1Q1R0Un3PI3kQywywu/791c8z9u0qeA==",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.8.2.tgz",
"integrity": "sha512-g78+DA29K0ByAfDkuibfLQqDshf8Aha/zcyEZ+huAX/yS/TWj/CUiEY4IJfDrFacdxIFmsLm0u4VtsLSKTngRw==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.0.0"
"@tanstack/virtual-core": "3.8.2"
},
"funding": {
"type": "github",
@ -9661,9 +9792,10 @@
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz",
"integrity": "sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg==",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.8.2.tgz",
"integrity": "sha512-ffpN6kTaPGwQPoWMcBAHbdv2ZCpj1SugldoYAcY0C4xH+Pej1KCOEUisNeEgbUnXOp8Y/4q6wGPu2tFHthOIQw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
@ -12296,11 +12428,6 @@
"node": ">= 12"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clipboardy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz",
@ -26431,6 +26558,12 @@
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"devOptional": true
},
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tailwind-merge": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz",