From 2d62eca612115a6176c35f992593bcbca9cff76a Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sun, 22 Sep 2024 04:45:50 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=90=20style:=20Improve=20a11y/theming?= =?UTF-8?q?=20for=20Settings=20Dialog,=20Dropdown=20Menus;=20fix:=20Search?= =?UTF-8?q?Bar=20focus=20issues=20(#4091)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: cursor pointer not applying correct in the root component * fix: add cursor-not-allowed to disabled state in SendButton component * feat: update Dropdown to ariakit and changed LLM error's style * feat: switched to ariakit's Dropdown and style improvements * feat: archive updates * refactor: delete conversations in archive * refactor: settings * add cool settings animation * a11y: settings update * style: update settings * style: settings account settings menu; a11y(AccountSettings): switched to AriaKit * a11y: account settings update * style: update my files dialog * fix: tests * chore: remove console.log() --------- Co-authored-by: Danny Avila --- .../Bookmarks/DeleteBookmarkButton.tsx | 2 +- .../Bookmarks/EditBookmarkButton.tsx | 2 +- client/src/components/Chat/Input/ChatForm.tsx | 12 +- .../components/Chat/Input/Files/FilesView.tsx | 27 +- .../src/components/Chat/Input/SendButton.tsx | 2 +- .../Chat/Messages/Content/MessageContent.tsx | 2 +- .../ConvoOptions/DeleteButton.tsx | 50 +-- .../Conversations/ConvoOptions/index.ts | 2 +- client/src/components/Nav/AccountSettings.tsx | 205 ++++++------ client/src/components/Nav/Logout.tsx | 26 -- client/src/components/Nav/NavLink.tsx | 2 +- client/src/components/Nav/NavToggle.tsx | 2 +- client/src/components/Nav/NewChat.tsx | 11 +- client/src/components/Nav/SearchBar.tsx | 6 + client/src/components/Nav/Settings.tsx | 41 ++- .../components/Nav/SettingsTabs/Beta/Beta.tsx | 2 +- .../components/Nav/SettingsTabs/Chat/Chat.tsx | 14 +- .../Nav/SettingsTabs/Chat/ChatDirection.tsx | 10 +- .../Nav/SettingsTabs/Chat/ForkSettings.tsx | 7 +- .../Nav/SettingsTabs/Commands/Commands.tsx | 6 +- .../Nav/SettingsTabs/Data/SharedLinkTable.tsx | 216 +++++++------ .../SettingsTabs/General/ArchivedChats.tsx | 6 +- .../General/ArchivedChatsTable.tsx | 303 +++++++++++++----- .../SettingsTabs/General/AutoScrollSwitch.tsx | 1 + .../Nav/SettingsTabs/General/General.tsx | 18 +- .../General/HideSidePanelSwitch.tsx | 1 + .../General/LangSelector.spec.tsx | 23 +- .../General/ThemeSelector.spec.tsx | 9 +- .../Speech/STT/EngineSTTDropdown.tsx | 1 - .../Nav/SettingsTabs/Speech/Speech.tsx | 94 ++---- client/src/components/Nav/index.ts | 1 - .../Prompts/Groups/FilterPrompts.tsx | 2 +- client/src/components/Prompts/Groups/List.tsx | 2 +- .../src/components/Prompts/ManagePrompts.tsx | 11 +- client/src/components/Prompts/PromptsView.tsx | 2 +- client/src/components/SidePanel/Nav.tsx | 58 ++-- client/src/components/SidePanel/SidePanel.tsx | 4 +- client/src/components/ui/Button.tsx | 108 ++----- client/src/components/ui/Dropdown.tsx | 166 ++++------ client/src/components/ui/DropdownMenu.tsx | 2 +- client/src/components/ui/DropdownNoState.tsx | 4 + client/src/components/ui/Input.tsx | 2 +- client/src/components/ui/OGDialogTemplate.tsx | 5 +- client/src/components/ui/OriginalDialog.tsx | 8 +- client/src/components/ui/Pagination.tsx | 105 ++++++ client/src/components/ui/Separator.tsx | 2 +- client/src/components/ui/Slider.tsx | 49 +-- client/src/components/ui/Switch.tsx | 10 +- client/src/components/ui/Table.tsx | 29 +- client/src/components/ui/Tabs.tsx | 13 +- client/src/components/ui/Tooltip.tsx | 12 +- client/src/components/ui/index.ts | 1 + client/src/data-provider/queries.ts | 4 +- client/src/localization/languages/Eng.ts | 1 + client/src/store/search.ts | 6 + client/src/style.css | 117 ++++++- client/tailwind.config.cjs | 36 +++ package-lock.json | 15 +- 58 files changed, 1054 insertions(+), 824 deletions(-) delete mode 100644 client/src/components/Nav/Logout.tsx create mode 100644 client/src/components/ui/Pagination.tsx diff --git a/client/src/components/Bookmarks/DeleteBookmarkButton.tsx b/client/src/components/Bookmarks/DeleteBookmarkButton.tsx index d33d368c2b..af12fcc2d3 100644 --- a/client/src/components/Bookmarks/DeleteBookmarkButton.tsx +++ b/client/src/components/Bookmarks/DeleteBookmarkButton.tsx @@ -50,7 +50,7 @@ const DeleteBookmarkButton: FC<{ setOpen(!open)} - className="flex size-7 cursor-pointer items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" + className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" onKeyDown={handleKeyDown} > diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index 719d9681f3..412983c010 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -1,4 +1,4 @@ -import { memo, useRef, useMemo } from 'react'; +import { memo, useRef, useMemo, useEffect } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { supportsFiles, @@ -44,6 +44,7 @@ const ChatForm = ({ index = 0 }) => { const TextToSpeech = useRecoilValue(store.textToSpeech); const automaticPlayback = useRecoilValue(store.automaticPlayback); + const isSearching = useRecoilValue(store.isSearching); const [showStopButton, setShowStopButton] = useRecoilState(store.showStopButtonByIndex(index)); const [showPlusPopover, setShowPlusPopover] = useRecoilState(store.showPlusPopoverFamily(index)); const [showMentionPopover, setShowMentionPopover] = useRecoilState( @@ -123,6 +124,12 @@ const ChatForm = ({ index = 0 }) => { }, }); + useEffect(() => { + if (!isSearching && textAreaRef.current && !disableInputs) { + textAreaRef.current.focus(); + } + }, [isSearching, disableInputs]); + return (
submitMessage(data))} @@ -164,9 +171,6 @@ const ChatForm = ({ index = 0 }) => { {endpoint && ( { ref(e); textAreaRef.current = e; diff --git a/client/src/components/Chat/Input/Files/FilesView.tsx b/client/src/components/Chat/Input/Files/FilesView.tsx index 8791e6c915..fbe6ea862c 100644 --- a/client/src/components/Chat/Input/Files/FilesView.tsx +++ b/client/src/components/Chat/Input/Files/FilesView.tsx @@ -1,10 +1,9 @@ import { FileSources, FileContext } from 'librechat-data-provider'; import type { TFile } from 'librechat-data-provider'; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui'; +import { OGDialog, OGDialogContent, OGDialogHeader, OGDialogTitle } from '~/components'; import { useGetFiles } from '~/data-provider'; import { DataTable, columns } from './Table'; import { useLocalize } from '~/hooks'; -import { cn } from '~/utils/'; export default function Files({ open, onOpenChange }) { const localize = useLocalize(); @@ -19,20 +18,16 @@ export default function Files({ open, onOpenChange }) { }); return ( - - + - - - {localize('com_nav_my_files')} - - -
- -
-
- -
+ + {localize('com_nav_my_files')} + + + + ); } diff --git a/client/src/components/Chat/Input/SendButton.tsx b/client/src/components/Chat/Input/SendButton.tsx index 917c312c58..4b80f367ee 100644 --- a/client/src/components/Chat/Input/SendButton.tsx +++ b/client/src/components/Chat/Input/SendButton.tsx @@ -26,7 +26,7 @@ const SubmitButton = React.memo( id="send-button" disabled={props.disabled} className={cn( - 'absolute rounded-lg border border-black p-0.5 text-white outline-offset-4 transition-colors enabled:bg-black disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white', + 'absolute rounded-lg border border-black p-0.5 text-white outline-offset-4 transition-colors enabled:bg-black disabled:cursor-not-allowed disabled:bg-black disabled:text-gray-400 disabled:opacity-10 dark:border-white dark:bg-white dark:disabled:bg-white', props.isRTL ? 'bottom-1.5 left-2 md:bottom-3 md:left-3' : 'bottom-1.5 right-2 md:bottom-3 md:right-3', diff --git a/client/src/components/Chat/Messages/Content/MessageContent.tsx b/client/src/components/Chat/Messages/Content/MessageContent.tsx index 06d3715fa8..d391e4d08d 100644 --- a/client/src/components/Chat/Messages/Content/MessageContent.tsx +++ b/client/src/components/Chat/Messages/Content/MessageContent.tsx @@ -57,7 +57,7 @@ export const ErrorMessage = ({
diff --git a/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx b/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx index 51de1aa1dc..589bd4ff30 100644 --- a/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx +++ b/client/src/components/Conversations/ConvoOptions/DeleteButton.tsx @@ -1,13 +1,12 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback } from 'react'; import { QueryKeys } from 'librechat-data-provider'; import { useQueryClient } from '@tanstack/react-query'; import { useParams, useNavigate } from 'react-router-dom'; import type { TMessage } from 'librechat-data-provider'; import { useDeleteConversationMutation } from '~/data-provider'; -import { OGDialog, OGDialogTrigger, Label, TooltipAnchor } from '~/components/ui'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; -import { TrashIcon } from '~/components/svg'; import { useLocalize, useNewConvo } from '~/hooks'; +import { OGDialog, Label } from '~/components'; type DeleteButtonProps = { conversationId: string; @@ -17,19 +16,21 @@ type DeleteButtonProps = { setShowDeleteDialog?: (value: boolean) => void; }; -export default function DeleteButton({ +export function DeleteConversationDialog({ conversationId, retainView, title, - showDeleteDialog, - setShowDeleteDialog, -}: DeleteButtonProps) { +}: { + conversationId: string; + retainView: () => void; + title: string; +}) { const localize = useLocalize(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { newConversation } = useNewConvo(); const { conversationId: currentConvoId } = useParams(); - const [open, setOpen] = useState(false); + const deleteConvoMutation = useDeleteConversationMutation({ onSuccess: () => { if (currentConvoId === conversationId || currentConvoId === 'new') { @@ -47,7 +48,7 @@ export default function DeleteButton({ deleteConvoMutation.mutate({ conversationId, thread_id, source: 'button' }); }, [conversationId, deleteConvoMutation, queryClient]); - const dialogContent = ( + return ( ); +} - if (showDeleteDialog !== undefined && setShowDeleteDialog !== undefined) { - return ( - - {dialogContent} - - ); +export default function DeleteButton({ + conversationId, + retainView, + title, + showDeleteDialog, + setShowDeleteDialog, +}: DeleteButtonProps) { + if (showDeleteDialog === undefined && setShowDeleteDialog === undefined) { + return null; } return ( - - - - - - - {dialogContent} + + ); } diff --git a/client/src/components/Conversations/ConvoOptions/index.ts b/client/src/components/Conversations/ConvoOptions/index.ts index 60ceeb2675..c294bb45ce 100644 --- a/client/src/components/Conversations/ConvoOptions/index.ts +++ b/client/src/components/Conversations/ConvoOptions/index.ts @@ -1,4 +1,4 @@ -export { default as DeleteButton } from './DeleteButton'; +export * from './DeleteButton'; export { default as ShareButton } from './ShareButton'; export { default as SharedLinkButton } from './SharedLinkButton'; export { default as ConvoOptions } from './ConvoOptions'; diff --git a/client/src/components/Nav/AccountSettings.tsx b/client/src/components/Nav/AccountSettings.tsx index dfed968e72..1b227d3663 100644 --- a/client/src/components/Nav/AccountSettings.tsx +++ b/client/src/components/Nav/AccountSettings.tsx @@ -1,23 +1,20 @@ -import { FileText } from 'lucide-react'; import { useRecoilState } from 'recoil'; +import * as Select from '@ariakit/react/select'; import { Fragment, useState, memo } from 'react'; -import { Menu, MenuItem, MenuButton, MenuItems, Transition } from '@headlessui/react'; +import { FileText, LogOut } from 'lucide-react'; import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query'; +import { LinkIcon, GearIcon, DropdownMenuSeparator } from '~/components'; import FilesView from '~/components/Chat/Input/Files/FilesView'; import { useAuthContext } from '~/hooks/AuthContext'; import useAvatar from '~/hooks/Messages/useAvatar'; -import { LinkIcon, GearIcon } from '~/components'; import { UserIcon } from '~/components/svg'; import { useLocalize } from '~/hooks'; import Settings from './Settings'; -import NavLink from './NavLink'; -import Logout from './Logout'; -import { cn } from '~/utils/'; import store from '~/store'; function AccountSettings() { const localize = useLocalize(); - const { user, isAuthenticated } = useAuthContext(); + const { user, isAuthenticated, logout } = useAuthContext(); const { data: startupConfig } = useGetStartupConfig(); const balanceQuery = useGetUserBalance({ enabled: !!isAuthenticated && startupConfig?.checkBalance, @@ -29,115 +26,105 @@ function AccountSettings() { const name = user?.avatar ?? user?.username ?? ''; return ( - <> - - {({ open }) => ( - <> - -
-
- {name.length === 0 ? ( -
- -
- ) : ( - avatar - )} -
-
+ + +
+
+ {name.length === 0 ? ( - - - - -
- {user?.email ?? localize('com_nav_user')} -
-
- {startupConfig?.checkBalance === true && - balanceQuery.data != null && - !isNaN(parseFloat(balanceQuery.data)) && ( - <> -
- {`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`} -
-
- - )} - - {({ focus }) => ( - } - text={localize('com_nav_my_files')} - clickHandler={() => setShowFiles(true)} - /> - )} - - {startupConfig?.helpAndFaqURL !== '/' && ( - - {({ focus }) => ( - } - text={localize('com_nav_help_faq')} - clickHandler={() => window.open(startupConfig?.helpAndFaqURL, '_blank')} - /> - )} - - )} - - {({ focus }) => ( - } - text={localize('com_nav_settings')} - clickHandler={() => { - setTimeout(() => setShowSettings(true), 50); - }} - /> - )} - -
- - {({ focus }) => } - - - + ) : ( + {`${name}'s + )} +
+
+
+ {user?.name ?? user?.username ?? localize('com_nav_user')} +
+ + +
+ {user?.email ?? localize('com_nav_user')} +
+ + {startupConfig?.checkBalance === true && + balanceQuery.data != null && + !isNaN(parseFloat(balanceQuery.data)) && ( + <> +
+ {`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`} +
+ )} -
+ setShowFiles(true)} + className="select-item text-sm" + > + + {startupConfig?.helpAndFaqURL !== '/' && ( + window.open(startupConfig?.helpAndFaqURL, '_blank')} + className="select-item text-sm" + > + + )} + setShowSettings(true)} + className="select-item text-sm" + > + + + logout()} + value="logout" + className="select-item text-sm" + > + + {localize('com_nav_log_out')} + + {showFiles && } {showSettings && } - + ); } diff --git a/client/src/components/Nav/Logout.tsx b/client/src/components/Nav/Logout.tsx deleted file mode 100644 index 6b7d476d47..0000000000 --- a/client/src/components/Nav/Logout.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { forwardRef } from 'react'; -import { useAuthContext } from '~/hooks/AuthContext'; -import { LogOutIcon } from '~/components/svg'; -import { useLocalize } from '~/hooks'; -import { cn } from '~/utils'; - -const Logout = forwardRef((props, ref) => { - const { logout } = useAuthContext(); - const localize = useLocalize(); - - return ( - - ); -}); - -export default Logout; diff --git a/client/src/components/Nav/NavLink.tsx b/client/src/components/Nav/NavLink.tsx index be77ed2c4a..e30cb69903 100644 --- a/client/src/components/Nav/NavLink.tsx +++ b/client/src/components/Nav/NavLink.tsx @@ -16,7 +16,7 @@ const NavLink: FC = forwardRef((props, ref) => onClick?: React.MouseEventHandler; } = { className: cn( - 'w-full flex gap-2 rounded p-2.5 text-sm cursor-pointer group items-center transition-colors duration-200 text-text-primary hover:bg-surface-hover', + 'w-full flex gap-2 rounded p-2.5 text-sm cursor-pointer group items-center transition-colors duration-200 text-text-primary', className, { 'opacity-50 pointer-events-none': disabled, diff --git a/client/src/components/Nav/NavToggle.tsx b/client/src/components/Nav/NavToggle.tsx index a8ce43197f..1c4333b090 100644 --- a/client/src/components/Nav/NavToggle.tsx +++ b/client/src/components/Nav/NavToggle.tsx @@ -41,7 +41,7 @@ export default function NavToggle({ description={ navVisible ? localize('com_nav_close_sidebar') : localize('com_nav_open_sidebar') } - className="flex cursor-pointer items-center justify-center" + className="flex items-center justify-center" tabIndex={0} > diff --git a/client/src/components/Nav/NewChat.tsx b/client/src/components/Nav/NewChat.tsx index 22aa888eb0..35b8bb6050 100644 --- a/client/src/components/Nav/NewChat.tsx +++ b/client/src/components/Nav/NewChat.tsx @@ -7,7 +7,6 @@ import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils'; import { icons } from '~/components/Chat/Menus/Endpoints/Icons'; import ConvoIconURL from '~/components/Endpoints/ConvoIconURL'; import { useLocalize, useNewConvo } from '~/hooks'; -import { TooltipAnchor } from '~/components/ui'; import { NewChatIcon } from '~/components/svg'; import store from '~/store'; @@ -96,15 +95,7 @@ export default function NewChat({
- - - +
diff --git a/client/src/components/Nav/SearchBar.tsx b/client/src/components/Nav/SearchBar.tsx index 90d0bb981f..1eef13722c 100644 --- a/client/src/components/Nav/SearchBar.tsx +++ b/client/src/components/Nav/SearchBar.tsx @@ -20,6 +20,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) = const setSearchQuery = useSetRecoilState(store.searchQuery); const [showClearIcon, setShowClearIcon] = useState(false); const [text, setText] = useState(''); + const setIsSearching = useSetRecoilState(store.isSearching); const localize = useLocalize(); const clearText = useCallback(() => { @@ -47,6 +48,8 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) = }, [queryClient, clearConvoState, setSearchQuery], ); + + // TODO: make the debounce time configurable via yaml const debouncedSendRequest = useMemo(() => debounce(sendRequest, 350), [sendRequest]); const onChange = (e: React.FormEvent) => { @@ -54,6 +57,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) = setShowClearIcon(value.length > 0); setText(value); debouncedSendRequest(value); + setIsSearching(true); }; return ( @@ -78,6 +82,8 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) = aria-label={localize('com_nav_search_placeholder')} placeholder={localize('com_nav_search_placeholder')} onKeyUp={handleKeyUp} + onFocus={() => setIsSearching(true)} + onBlur={() => setIsSearching(true)} autoComplete="off" dir="auto" /> diff --git a/client/src/components/Nav/Settings.tsx b/client/src/components/Nav/Settings.tsx index 09e473d3cb..4adfc8be5a 100644 --- a/client/src/components/Nav/Settings.tsx +++ b/client/src/components/Nav/Settings.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import { useState, useRef } from 'react'; import * as Tabs from '@radix-ui/react-tabs'; import { MessageSquare, Command } from 'lucide-react'; import { SettingsTabValues } from 'librechat-data-provider'; @@ -12,7 +12,8 @@ import { cn } from '~/utils'; export default function Settings({ open, onOpenChange }: TDialogProps) { const isSmallScreen = useMediaQuery('(max-width: 767px)'); const localize = useLocalize(); - const [activeTab, setActiveTab] = React.useState(SettingsTabValues.GENERAL); + const [activeTab, setActiveTab] = useState(SettingsTabValues.GENERAL); + const tabRefs = useRef({}); const handleKeyDown = (event: React.KeyboardEvent) => { const tabs = [ @@ -28,12 +29,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) { switch (event.key) { case 'ArrowDown': - case 'ArrowRight': event.preventDefault(); setActiveTab(tabs[(currentIndex + 1) % tabs.length]); break; case 'ArrowUp': - case 'ArrowLeft': event.preventDefault(); setActiveTab(tabs[(currentIndex - 1 + tabs.length) % tabs.length]); break; @@ -48,6 +47,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) { } }; + const handleTabChange = (value: string) => { + setActiveTab(value as SettingsTabValues); + }; + return ( @@ -55,7 +58,7 @@ export default function Settings({ open, onOpenChange }: TDialogProps) { enter="ease-out duration-200" enterFrom="opacity-0" enterTo="opacity-100" - leave="ease-in duration-100" + leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0" > @@ -70,15 +73,10 @@ export default function Settings({ open, onOpenChange }: TDialogProps) { leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > -
+
Close -
+
setActiveTab(value as SettingsTabValues)} + onValueChange={handleTabChange} className="flex flex-col gap-10 md:flex-row" - orientation="horizontal" + orientation="vertical" > @@ -166,19 +164,20 @@ export default function Settings({ open, onOpenChange }: TDialogProps) { (tabRefs.current[value] = el)} > {icon} {localize(label)} ))} -
+
diff --git a/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx b/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx index c71c11d4ad..d687efbbd3 100644 --- a/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx +++ b/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx @@ -4,7 +4,7 @@ import CodeArtifacts from './CodeArtifacts'; function Beta() { return (
-
+
diff --git a/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx b/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx index 94d9eb6499..71369ce59b 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx @@ -11,26 +11,26 @@ import SaveDraft from './SaveDraft'; function Chat() { return (
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx b/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx index 6048cb0507..e4bb42a627 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useRecoilState } from 'recoil'; import { useLocalize } from '~/hooks'; +import { Button } from '~/components'; import store from '~/store'; const ChatDirection = () => { @@ -16,12 +17,11 @@ const ChatDirection = () => {
{localize('com_nav_chat_direction')}
- +
); }; diff --git a/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx b/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx index 71c0a26e69..84ecc9bb50 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/ForkSettings.tsx @@ -19,7 +19,7 @@ export const ForkSettings = () => { return ( <> -
+
{localize('com_ui_fork_change_default')}
@@ -30,12 +30,11 @@ export const ForkSettings = () => { onChange={setForkSetting} options={forkOptions} sizeClasses="w-[200px]" - anchor="bottom start" testId="fork-setting-dropdown" />
-
+
{localize('com_ui_fork_default')}
{ />
-
+
{localize('com_ui_fork_split_target_setting')}
diff --git a/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx b/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx index d9c33034c2..72894d6bc0 100644 --- a/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx +++ b/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx @@ -28,16 +28,16 @@ function Commands() {
-
+
{hasAccessToMultiConvo === true && ( -
+
)} {hasAccessToPrompts === true && ( -
+
)} diff --git a/client/src/components/Nav/SettingsTabs/Data/SharedLinkTable.tsx b/client/src/components/Nav/SettingsTabs/Data/SharedLinkTable.tsx index 13084667a3..aa48f1d2f9 100644 --- a/client/src/components/Nav/SettingsTabs/Data/SharedLinkTable.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/SharedLinkTable.tsx @@ -1,36 +1,45 @@ -import { useMemo, useState } from 'react'; +import { useState, useMemo } from 'react'; import { Link } from 'react-router-dom'; -import { Link as LinkIcon } from 'lucide-react'; +import { Link as LinkIcon, TrashIcon } from 'lucide-react'; import type { SharedLinksResponse, TSharedLink } from 'librechat-data-provider'; import { useDeleteSharedLinkMutation, useSharedLinksInfiniteQuery } from '~/data-provider'; import { useAuthContext, useLocalize, useNavScrolling } from '~/hooks'; -import { Spinner, TooltipAnchor, TrashIcon } from '~/components'; +import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; import { NotificationSeverity } from '~/common'; import { useToastContext } from '~/Providers'; import { cn } from '~/utils'; +import { + Button, + Label, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + TooltipAnchor, + Skeleton, + Spinner, + OGDialog, + OGDialogTrigger, +} from '~/components'; -function SharedLinkDeleteButton({ - shareId, - setIsDeleting, -}: { - shareId: string; - setIsDeleting: (isDeleting: boolean) => void; -}) { +function ShareLinkRow({ sharedLink }: { sharedLink: TSharedLink }) { + const [isDeleting, setIsDeleting] = useState(false); const localize = useLocalize(); + const { showToast } = useToastContext(); const mutation = useDeleteSharedLinkMutation({ onError: () => { showToast({ message: localize('com_ui_share_delete_error'), severity: NotificationSeverity.ERROR, - showIcon: true, }); setIsDeleting(false); }, }); - const handleDelete = async (e: React.MouseEvent) => { - e.preventDefault(); + const confirmDelete = async (shareId: TSharedLink['shareId']) => { if (mutation.isLoading) { return; } @@ -38,67 +47,78 @@ function SharedLinkDeleteButton({ await mutation.mutateAsync({ shareId }); setIsDeleting(false); }; - return ( - - - - ); -} - -function ShareLinkRow({ sharedLink }: { sharedLink: TSharedLink }) { - const [isDeleting, setIsDeleting] = useState(false); return ( - - - - + + + + {sharedLink.title} - - -
-
- {new Date(sharedLink.createdAt).toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric', - })} -
-
- {sharedLink.conversationId && ( -
- -
- )} -
-
- - +
+ + {new Date(sharedLink.createdAt).toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + })} + + + {sharedLink.conversationId && ( + + + + + + } + > + + +
+
+ +
+
+ + } + selection={{ + selectHandler: () => confirmDelete(sharedLink.shareId), + selectClasses: + 'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white', + selectText: localize('com_ui_delete'), + }} + /> +
+ )} +
+
); } -export default function ShareLinkTable({ className }: { className?: string }) { + +export default function ShareLinkTable({ className }) { const localize = useLocalize(); const { isAuthenticated } = useAuthContext(); const [showLoading, setShowLoading] = useState(false); @@ -114,15 +134,28 @@ export default function ShareLinkTable({ className }: { className?: string }) { }); const sharedLinks = useMemo(() => data?.pages.flatMap((page) => page.sharedLinks) || [], [data]); - const classProp: { className?: string } = { - className: 'p-1 hover:text-black dark:hover:text-white', - }; - if (className) { - classProp.className = className; - } + + const getRandomWidth = () => Math.floor(Math.random() * (400 - 170 + 1)) + 170; + + const skeletons = Array.from({ length: 11 }, (_, index) => { + const randomWidth = getRandomWidth(); + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); + }); if (isLoading) { - return ; + return
{skeletons}
; } if (isError) { @@ -132,35 +165,34 @@ export default function ShareLinkTable({ className }: { className?: string }) {
); } - if (!sharedLinks || sharedLinks.length === 0) { + + if (sharedLinks.length === 0) { return
{localize('com_nav_shared_links_empty')}
; } return (
- - - - - - - - +
{localize('com_nav_shared_links_name')}{localize('com_nav_shared_links_date_shared')}
+ + + {localize('com_nav_shared_links_name')} + {localize('com_nav_shared_links_date_shared')} + {localize('com_assistants_actions')} + + + {sharedLinks.map((sharedLink) => ( ))} - -
- {(isFetchingNextPage || showLoading) && ( - - )} + + + {(isFetchingNextPage || showLoading) && }
); } diff --git a/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx b/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx index 0c257a26b3..793954013a 100644 --- a/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx +++ b/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx @@ -1,6 +1,6 @@ import { useLocalize } from '~/hooks'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; -import { OGDialog, OGDialogTrigger } from '~/components/ui'; +import { OGDialog, OGDialogTrigger, Button } from '~/components'; import ArchivedChatsTable from './ArchivedChatsTable'; @@ -12,9 +12,9 @@ export default function ArchivedChats() {
{localize('com_nav_archived_chats')}
- + (null); + const [currentPage, setCurrentPage] = useState(1); + const [searchQuery, setSearchQuery] = useState(''); + const [totalPages, setTotalPages] = useState(1); + const [isOpened, setIsOpened] = useState(false); - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useConversationsInfiniteQuery( - { pageNumber: '1', isArchived: true }, - { enabled: isAuthenticated }, + const { data, isLoading, refetch } = useConversationsInfiniteQuery( + { pageNumber: currentPage.toString(), limit: 10, isArchived: true }, + { enabled: isAuthenticated && isOpened }, ); - const { containerRef, moveToTop } = useNavScrolling({ - setShowLoading, - hasNextPage: hasNextPage, - fetchNextPage: fetchNextPage, - isFetchingNextPage: isFetchingNextPage, + useEffect(() => { + if (data) { + setTotalPages(Math.ceil(Number(data.pages))); + } + }, [data]); + + const archiveHandler = useArchiveHandler(conversationId ?? '', false, () => { + refetch(); }); - const conversations = useMemo( - () => data?.pages.flatMap((page) => page.conversations) || [], - [data], - ); + const handleChatClick = useCallback((conversationId) => { + window.open(`/c/${conversationId}`, '_blank'); + }, []); - const archiveHandler = useArchiveHandler(conversationId ?? '', false, moveToTop); + const handlePageChange = useCallback((newPage) => { + setCurrentPage(newPage); + }, []); - if (!data || conversations.length === 0) { + const handleSearch = useCallback((query) => { + setSearchQuery(query); + setCurrentPage(1); + }, []); + + const getRandomWidth = () => Math.floor(Math.random() * (400 - 170 + 1)) + 170; + + const skeletons = Array.from({ length: 11 }, (_, index) => { + const randomWidth = getRandomWidth(); + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); + }); + + if (isLoading) { + return
{skeletons}
; + } + + if (!data || data.pages.length === 0 || data.pages[0].conversations.length === 0) { return
{localize('com_nav_archived_chats_empty')}
; } + const conversations = data.pages.flatMap((page) => page.conversations); + return (
setIsOpened(true)} > - - - - - - - - - {conversations.map((conversation) => { - if (!conversation.conversationId) { - return null; - } - return ( - - -
{localize('com_nav_archive_name')}{localize('com_nav_archive_created_at')}
- - {conversation.title} - -
-
- {new Date(conversation.createdAt).toLocaleDateString('en-US', { - month: 'long', - day: 'numeric', - year: 'numeric', - })} -
-
- { - setConversationId(conversation.conversationId); - archiveHandler(); - }} - className="cursor-pointer hover:text-black dark:hover:text-white" - > - - -
- +
+ + handleSearch(e.target.value)} + className="w-full border-none" + /> +
+ + {conversations.length === 0 ? ( +
{localize('com_nav_no_search_results')}
+ ) : ( + <> + + + + {localize('com_nav_archive_name')} + + {localize('com_nav_archive_created_at')} + + + {localize('com_assistants_actions')} + + + + + {conversations.map((conversation: TConversation) => ( + + + + + +
+
+ {new Date(conversation.createdAt).toLocaleDateString('en-US', { + month: 'long', + day: 'numeric', + year: 'numeric', + })}
- - - - ); - })} - -
- {(isFetchingNextPage || showLoading) && ( - + + + { + setConversationId(conversation.conversationId); + archiveHandler(); + }} + > + + + } + > + + + + + + + } + > + + {DeleteConversationDialog({ + conversationId: conversation.conversationId ?? '', + retainView: refetch, + title: conversation.title ?? '', + })} + + + + ))} + +
+ +
+
+ Page {currentPage} of {totalPages} +
+
+ + + + +
+
+ )}
); diff --git a/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx b/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx index e811e3e7fe..294d7ac043 100644 --- a/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx @@ -24,6 +24,7 @@ export default function AutoScrollSwitch({
@@ -112,7 +111,6 @@ export const LangSelector = ({ value={langcode} onChange={onChange} sizeClasses="[--anchor-max-height:256px]" - anchor="bottom start" options={languageOptions} />
@@ -149,26 +147,24 @@ function General() { return (
-
+
-
+
-
+
-
+
-
+
-
+
- {/*
-
*/}
); } diff --git a/client/src/components/Nav/SettingsTabs/General/HideSidePanelSwitch.tsx b/client/src/components/Nav/SettingsTabs/General/HideSidePanelSwitch.tsx index b8cf3ec372..d5854a50ce 100644 --- a/client/src/components/Nav/SettingsTabs/General/HideSidePanelSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/General/HideSidePanelSwitch.tsx @@ -25,6 +25,7 @@ export default function HideSidePanelSwitch({ { unobserve = jest.fn(); disconnect = jest.fn(); }; - const { getByText } = render( + const { getByText, getByRole } = render( , ); expect(getByText('Language')).toBeInTheDocument(); - expect(getByText('English')).toBeInTheDocument(); + const dropdownButton = getByRole('combobox'); + expect(dropdownButton).toHaveTextContent('English'); }); it('calls onChange when the select value changes', async () => { @@ -34,25 +35,23 @@ describe('LangSelector', () => { unobserve = jest.fn(); disconnect = jest.fn(); }; - const { getByText, getByTestId } = render( + const { getByRole, getByTestId } = render( , ); - expect(getByText('English')).toBeInTheDocument(); + expect(getByRole('combobox')).toHaveTextContent('English'); - // Find the dropdown button by data-testid const dropdownButton = getByTestId('dropdown-menu'); - // Open the dropdown fireEvent.click(dropdownButton); - // Find the option by text and click it - const darkOption = getByText('Italiano'); - fireEvent.click(darkOption); + const italianOption = getByRole('option', { name: 'Italiano' }); + fireEvent.click(italianOption); - // Ensure that the onChange is called with the expected value after a short delay - await new Promise((resolve) => setTimeout(resolve, 0)); + await waitFor(() => { + expect(mockOnChange).toHaveBeenCalledWith('it-IT'); + }); }); }); diff --git a/client/src/components/Nav/SettingsTabs/General/ThemeSelector.spec.tsx b/client/src/components/Nav/SettingsTabs/General/ThemeSelector.spec.tsx index e8d28c8a39..d3c55f1062 100644 --- a/client/src/components/Nav/SettingsTabs/General/ThemeSelector.spec.tsx +++ b/client/src/components/Nav/SettingsTabs/General/ThemeSelector.spec.tsx @@ -20,14 +20,15 @@ describe('ThemeSelector', () => { unobserve = jest.fn(); disconnect = jest.fn(); }; - const { getByText } = render( + const { getByText, getByRole } = render( , ); expect(getByText('Theme')).toBeInTheDocument(); - expect(getByText('System')).toBeInTheDocument(); + const dropdownButton = getByRole('combobox'); + expect(dropdownButton).toHaveTextContent('System'); }); it('calls onChange when the select value changes', async () => { @@ -44,17 +45,13 @@ describe('ThemeSelector', () => { expect(getByText('Theme')).toBeInTheDocument(); - // Find the dropdown button by data-testid const dropdownButton = getByTestId('theme-selector'); - // Open the dropdown fireEvent.click(dropdownButton); - // Find the option by text and click it const darkOption = getByText('Dark'); fireEvent.click(darkOption); - // Ensure that the onChange is called with the expected value await waitFor(() => { expect(mockOnChange).toHaveBeenCalledWith('dark'); }); diff --git a/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx b/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx index 9ccba661cc..0f59c85d49 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/STT/EngineSTTDropdown.tsx @@ -31,7 +31,6 @@ const EngineSTTDropdown: React.FC = ({ external }) => { onChange={handleSelect} options={endpointOptions} sizeClasses="w-[180px]" - anchor="bottom start" testId="EngineSTTDropdown" />
diff --git a/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx b/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx index d1420b89f4..137c449383 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx @@ -146,14 +146,12 @@ function Speech() { value={advancedMode ? 'advanced' : 'simple'} >
- + setAdvancedMode(false)} className={cn( - 'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 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', - isSmallScreen - ? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white' - : 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700', + 'group m-1 flex items-center justify-center gap-2 bg-transparent px-4 py-2 text-sm text-text-secondary transition-all duration-200 ease-in-out radix-state-active:bg-secondary radix-state-active:text-foreground radix-state-active:shadow-lg', + isSmallScreen ? 'flex-row rounded-lg' : 'rounded-xl', 'w-full', )} value="simple" @@ -165,10 +163,8 @@ function Speech() { setAdvancedMode(true)} className={cn( - 'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 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', - isSmallScreen - ? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white' - : 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700', + 'group m-1 flex items-center justify-center gap-2 bg-transparent px-4 py-2 text-sm text-text-secondary transition-all duration-200 ease-in-out radix-state-active:bg-secondary radix-state-active:text-foreground radix-state-active:shadow-lg', + isSmallScreen ? 'flex-row rounded-lg' : 'rounded-xl', 'w-full', )} value="advanced" @@ -181,79 +177,53 @@ function Speech() {
-
-
- -
-
- -
-
- -
-
-
- -
-
- -
-
- -
+
+ + + +
+ + +
-
-
- -
-
-
- -
-
- -
-
- -
-
+
+ +
+ + + + + +
{autoTranscribeAudio && ( -
+
)} -
+
-
-
+
+
-
- -
-
- -
-
- -
+ + + {engineTTS === 'browser' && ( -
+
)} -
+
-
- -
+
diff --git a/client/src/components/Nav/index.ts b/client/src/components/Nav/index.ts index 895dc12b76..5770793712 100644 --- a/client/src/components/Nav/index.ts +++ b/client/src/components/Nav/index.ts @@ -1,7 +1,6 @@ export * from './ExportConversation'; export * from './SettingsTabs/'; export { default as ClearConvos } from './ClearConvos'; -export { default as Logout } from './Logout'; export { default as MobileNav } from './MobileNav'; export { default as Nav } from './Nav'; export { default as NavLink } from './NavLink'; diff --git a/client/src/components/Prompts/Groups/FilterPrompts.tsx b/client/src/components/Prompts/Groups/FilterPrompts.tsx index 3230a8e314..1981faf5b1 100644 --- a/client/src/components/Prompts/Groups/FilterPrompts.tsx +++ b/client/src/components/Prompts/Groups/FilterPrompts.tsx @@ -31,7 +31,7 @@ export function FilterItem({ return ( {icon} {label} diff --git a/client/src/components/Prompts/Groups/List.tsx b/client/src/components/Prompts/Groups/List.tsx index f8d0d82fd0..d04b3358f8 100644 --- a/client/src/components/Prompts/Groups/List.tsx +++ b/client/src/components/Prompts/Groups/List.tsx @@ -31,7 +31,7 @@ export default function List({
); } diff --git a/client/src/components/Prompts/PromptsView.tsx b/client/src/components/Prompts/PromptsView.tsx index 089f2dbb4a..fcc23bfb7c 100644 --- a/client/src/components/Prompts/PromptsView.tsx +++ b/client/src/components/Prompts/PromptsView.tsx @@ -41,7 +41,7 @@ export default function PromptsView() {
- +
{ - if (link.onClick) { - link.onClick(e); - setActive(''); - return; - } - setActive(link.id); - resize && resize(25); - }} description={localize(link.title)} side="left" - > - - {link.title} - + render={ + + } + > ) : ( - + diff --git a/client/src/components/SidePanel/SidePanel.tsx b/client/src/components/SidePanel/SidePanel.tsx index de38ea4e52..b150e2c2dc 100644 --- a/client/src/components/SidePanel/SidePanel.tsx +++ b/client/src/components/SidePanel/SidePanel.tsx @@ -254,7 +254,7 @@ const SidePanel = ({ localStorage.setItem('react-resizable-panels:collapsed', 'true'); }} className={cn( - 'sidenav hide-scrollbar border-l border-border-light bg-surface-primary-alt transition-opacity', + 'sidenav hide-scrollbar border-l border-border-light bg-background transition-opacity', isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]', (isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse ? 'hidden min-w-0' @@ -264,7 +264,7 @@ const SidePanel = ({ {interfaceConfig.modelSelect && (
diff --git a/client/src/components/ui/Button.tsx b/client/src/components/ui/Button.tsx index a48e650292..64989f4d1a 100644 --- a/client/src/components/ui/Button.tsx +++ b/client/src/components/ui/Button.tsx @@ -1,52 +1,28 @@ import * as React from 'react'; -import { VariantProps, cva } from 'class-variance-authority'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '~/utils'; const buttonVariants = cva( - 'rounded-md inline-flex items-center justify-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none', + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { - default: - 'bg-gray-600 text-white hover:bg-gray-800 dark:bg-gray-200 dark:text-gray-900 dark:hover:bg-gray-300', - destructive: 'bg-red-600 text-white hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700', + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: - 'bg-transparent border border-gray-200 text-gray-700 hover:bg-gray-200 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-700', - subtle: - 'bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600', - ghost: - 'bg-transparent text-gray-900 hover:bg-gray-100 dark:text-gray-100 dark:hover:bg-gray-800 data-[state=open]:bg-transparent', - link: 'bg-transparent underline-offset-4 hover:underline text-gray-600 dark:text-gray-400 hover:bg-transparent dark:hover:bg-transparent', - success: - 'bg-green-500 text-white hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-700', - warning: - 'bg-yellow-500 text-white hover:bg-yellow-600 dark:bg-yellow-600 dark:hover:bg-yellow-700', - info: 'bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700', + 'text-text-primary border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', }, size: { - default: 'h-10 py-2 px-4', - sm: 'h-8 px-3 rounded', - lg: 'h-12 px-6 rounded-md', - xl: 'h-14 px-8 rounded-lg text-base', - icon: 'h-10 w-10', - }, - fullWidth: { - true: 'w-full', - }, - loading: { - true: 'opacity-80 pointer-events-none', + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'size-10', }, }, - compoundVariants: [ - { - variant: ['default', 'destructive', 'success', 'warning', 'info'], - className: 'focus-visible:ring-white focus-visible:ring-offset-2', - }, - { - variant: 'outline', - className: 'focus-visible:ring-gray-400 dark:focus-visible:ring-gray-500', - }, - ], defaultVariants: { variant: 'default', size: 'default', @@ -57,62 +33,14 @@ const buttonVariants = cva( export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - loading?: boolean; - leftIcon?: React.ReactNode; - rightIcon?: React.ReactNode; + asChild?: boolean; } -const Button = React.forwardRef( - ( - { - className, - variant, - size, - fullWidth, - loading, - leftIcon, - rightIcon, - children, - customId, - ...props - }, - ref, - ) => { +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; return ( - + ); }, ); diff --git a/client/src/components/ui/Dropdown.tsx b/client/src/components/ui/Dropdown.tsx index bd96780831..73aef17afc 100644 --- a/client/src/components/ui/Dropdown.tsx +++ b/client/src/components/ui/Dropdown.tsx @@ -1,14 +1,7 @@ -import React, { FC, useState } from 'react'; -import { - Listbox, - ListboxButton, - ListboxOption, - ListboxOptions, - Transition, -} from '@headlessui/react'; -import { AnchorPropsWithSelection } from '@headlessui/react/dist/internal/floating'; -import type { Option } from '~/common'; +import React, { useState } from 'react'; +import * as Select from '@ariakit/react/select'; import { cn } from '~/utils/'; +import type { Option } from '~/common'; interface DropdownProps { value: string; @@ -16,112 +9,89 @@ interface DropdownProps { onChange: (value: string) => void; options: string[] | Option[]; className?: string; - anchor?: AnchorPropsWithSelection; sizeClasses?: string; testId?: string; } -const Dropdown: FC = ({ +const Dropdown: React.FC = ({ value: initialValue, label = '', onChange, options, className = '', - anchor, sizeClasses, testId = 'dropdown-menu', }) => { const [selectedValue, setSelectedValue] = useState(initialValue); + const handleChange = (value: string) => { + setSelectedValue(value); + onChange(value); + }; + + const selectProps = Select.useSelectStore({ + value: selectedValue, + setValue: handleChange, + }); + return (
- { - setSelectedValue(newValue); - onChange(newValue); - }} + -
- - - {label} - {options - .map((o) => (typeof o === 'string' ? { value: o, label: o } : o)) - .find((o) => o.value === selectedValue)?.label ?? selectedValue} - - - - - - - - - - {options.map((item, index) => ( - -
- - {typeof item === 'string' ? item : (item as Option).label} - - {selectedValue === (typeof item === 'string' ? item : item.value) && ( - - - - - - )} -
-
- ))} -
-
+
+ + {label} + {options + .map((o) => (typeof o === 'string' ? { value: o, label: o } : o)) + .find((o) => o.value === selectedValue)?.label ?? selectedValue} + +
- + + + {options.map((item, index) => ( + +
+ + {typeof item === 'string' ? item : (item as Option).label} + + {selectedValue === (typeof item === 'string' ? item : item.value) && ( + + + + + + )} +
+
+ ))} +
); }; diff --git a/client/src/components/ui/DropdownMenu.tsx b/client/src/components/ui/DropdownMenu.tsx index a06f079400..b317806826 100644 --- a/client/src/components/ui/DropdownMenu.tsx +++ b/client/src/components/ui/DropdownMenu.tsx @@ -156,7 +156,7 @@ const DropdownMenuSeparator = React.forwardRef< >(({ className = '', ...props }, ref) => ( )); diff --git a/client/src/components/ui/DropdownNoState.tsx b/client/src/components/ui/DropdownNoState.tsx index 2d6b7a22f6..03ef5f0770 100644 --- a/client/src/components/ui/DropdownNoState.tsx +++ b/client/src/components/ui/DropdownNoState.tsx @@ -21,6 +21,10 @@ interface DropdownProps { testId?: string; } +/* + * Mainly used for the Speech Voice Selection Dropdown + */ + const Dropdown: FC = ({ value, label = '', diff --git a/client/src/components/ui/Input.tsx b/client/src/components/ui/Input.tsx index 20b2f59dd5..042f6a5690 100644 --- a/client/src/components/ui/Input.tsx +++ b/client/src/components/ui/Input.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef(({ className, ...pr return ( e.stopPropagation()} > diff --git a/client/src/components/ui/OriginalDialog.tsx b/client/src/components/ui/OriginalDialog.tsx index c09ac786f8..f68746b9ff 100644 --- a/client/src/components/ui/OriginalDialog.tsx +++ b/client/src/components/ui/OriginalDialog.tsx @@ -18,7 +18,7 @@ const DialogOverlay = React.forwardRef< {children} {showCloseButton && ( - + Close @@ -89,7 +89,7 @@ const DialogDescription = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/client/src/components/ui/Pagination.tsx b/client/src/components/ui/Pagination.tsx new file mode 100644 index 0000000000..84779ee487 --- /dev/null +++ b/client/src/components/ui/Pagination.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'; +import { ButtonProps, buttonVariants } from './Button'; +import { cn } from '~/utils'; + +const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => ( +