diff --git a/client/package.json b/client/package.json index d54dfd285f..0ed17ee1a9 100644 --- a/client/package.json +++ b/client/package.json @@ -29,8 +29,8 @@ }, "homepage": "https://librechat.ai", "dependencies": { - "@ariakit/react": "^0.4.15", - "@ariakit/react-core": "^0.4.17", + "@ariakit/react": "^0.4.29", + "@ariakit/react-core": "^0.4.26", "@codesandbox/sandpack-react": "^2.19.10", "@dicebear/collection": "^9.4.1", "@dicebear/core": "^9.4.1", diff --git a/client/src/components/Chat/Input/ArtifactsSubMenu.tsx b/client/src/components/Chat/Input/ArtifactsSubMenu.tsx index b968c0ab55..2166cb669a 100644 --- a/client/src/components/Chat/Input/ArtifactsSubMenu.tsx +++ b/client/src/components/Chat/Input/ArtifactsSubMenu.tsx @@ -1,12 +1,12 @@ import React from 'react'; import * as Ariakit from '@ariakit/react'; import { PinIcon } from '@librechat/client'; -import { ChevronRight, WandSparkles } from 'lucide-react'; import { ArtifactModes } from 'librechat-data-provider'; +import { ChevronRight, WandSparkles } from 'lucide-react'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; -interface ArtifactsSubMenuProps { +interface ArtifactsSubMenuProps extends React.HTMLAttributes { isArtifactsPinned: boolean; setIsArtifactsPinned: (value: boolean) => void; artifactsMode: string; @@ -15,7 +15,7 @@ interface ArtifactsSubMenuProps { handleCustomToggle: () => void; } -const ArtifactsSubMenu = React.forwardRef( +const ArtifactsSubMenu = React.forwardRef( ( { isArtifactsPinned, @@ -24,6 +24,7 @@ const ArtifactsSubMenu = React.forwardRef handleArtifactsToggle, handleShadcnToggle, handleCustomToggle, + className, ...props }, ref, @@ -41,25 +42,24 @@ const ArtifactsSubMenu = React.forwardRef const isCustomEnabled = artifactsMode === ArtifactModes.CUSTOM; return ( -
+ <> - ) => { - e.stopPropagation(); - handleArtifactsToggle(); - }} - onMouseEnter={() => { - if (isEnabled) { - menuStore.show(); - } - }} - className="flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover" - /> - } + onClick={(e: React.MouseEvent) => { + e.stopPropagation(); + handleArtifactsToggle(); + }} + onMouseEnter={() => { + if (isEnabled) { + menuStore.show(); + } + }} + className={cn( + 'flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover', + className, + )} >
-
+ {isEnabled && ( )}
-
+ ); }, ); diff --git a/client/src/components/Chat/Input/MCPSubMenu.tsx b/client/src/components/Chat/Input/MCPSubMenu.tsx index a1ed542d97..c66a6d9df2 100644 --- a/client/src/components/Chat/Input/MCPSubMenu.tsx +++ b/client/src/components/Chat/Input/MCPSubMenu.tsx @@ -8,12 +8,12 @@ import { useBadgeRowContext } from '~/Providers'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; -interface MCPSubMenuProps { +interface MCPSubMenuProps extends React.HTMLAttributes { placeholder?: string; } -const MCPSubMenu = React.forwardRef( - ({ placeholder, ...props }, ref) => { +const MCPSubMenu = React.forwardRef( + ({ placeholder, className, ...props }, ref) => { const localize = useLocalize(); const context = useBadgeRowContext(); const { storageContextKey, mcpServerManager } = context ?? {}; @@ -48,20 +48,19 @@ const MCPSubMenu = React.forwardRef( const configDialogProps = getConfigDialogProps(); return ( -
+ <> - ) => { - e.stopPropagation(); - menuStore.toggle(); - }} - className="flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover" - /> - } + onClick={(e: React.MouseEvent) => { + e.stopPropagation(); + menuStore.toggle(); + }} + className={cn( + 'flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover', + className, + )} >
-
+ ( {configDialogProps && ( )} -
+ ); }, ); diff --git a/client/src/components/Chat/Input/TokenUsage/index.tsx b/client/src/components/Chat/Input/TokenUsage/index.tsx index cb9f4894a5..ff969148d5 100644 --- a/client/src/components/Chat/Input/TokenUsage/index.tsx +++ b/client/src/components/Chat/Input/TokenUsage/index.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, useRef } from 'react'; import * as Ariakit from '@ariakit/react'; import { TooltipAnchor } from '@librechat/client'; import type { TConversation } from 'librechat-data-provider'; @@ -29,6 +29,7 @@ function TokenUsageIndicator({ const localize = useLocalize(); const view = useTokenUsage({ index, conversation, isSubmitting }); const popover = Ariakit.usePopoverStore({ placement: 'top' }); + const disclosureRef = useRef(null); /** Hide until the branch has data — keeps a fresh, message-less chat clean and * lets the indicator animate into view once the first tokens land. */ @@ -64,6 +65,7 @@ function TokenUsageIndicator({ side="top" render={ } /> + {/* Focus the labelled dialog on open so screen readers enter and announce + the breakdown, and so focus stays contained instead of falling back to + the body (which the composer's global focus logic would steal). The + visible ring is suppressed via focus:outline-none, and finalFocus + returns focus to the gauge trigger on close. */} diff --git a/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx b/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx index 5413bcb68c..0eaaed2088 100644 --- a/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx @@ -332,6 +332,7 @@ export default function SharedLinks() { ; }) { const localize = useLocalize(); + const contentRef = useRef(null); + + /** The virtualized table has no stable focusable on mount, so Radix's default + * autofocus lands on a row that the virtualizer tears out, dropping focus to + * the page's top focus guard; anchor focus to the dialog content instead. */ + const handleOpenAutoFocus = (event: Event) => { + event.preventDefault(); + contentRef.current?.focus(); + }; return ( {localize('com_nav_archived_chats')} diff --git a/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx b/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx index 46752da5d1..15ac9dc456 100644 --- a/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx +++ b/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx @@ -109,7 +109,7 @@ export default function ArchivedChatsTable({ }, }); - const unarchiveMutation = useArchiveConvoMutation({ + const { mutate: unarchiveConversation, isLoading: isUnarchiving } = useArchiveConvoMutation({ onSuccess: async () => { await refetch(); }, @@ -253,20 +253,16 @@ export default function ArchivedChatsTable({ variant="ghost" className="h-8 w-8 p-0 hover:bg-surface-hover" onClick={() => - unarchiveMutation.mutate({ + unarchiveConversation({ conversationId: conversation.conversationId, isArchived: false, }) } title={localize('com_ui_unarchive_conversation')} aria-label={localize('com_ui_unarchive_conversation')} - disabled={unarchiveMutation.isLoading} + disabled={isUnarchiving} > - {unarchiveMutation.isLoading ? ( - - ) : ( - - )} + {isUnarchiving ? : } } /> @@ -296,24 +292,29 @@ export default function ArchivedChatsTable({ }, }, ], - [isSmallScreen, localize, unarchiveMutation], + [isSmallScreen, localize, unarchiveConversation, isUnarchiving], ); return ( <> - + {/* Fixed height keeps the loading (skeleton) and loaded states the same + size, so the virtualized table can't reflow the dialog on load. */} +
+ +
=8" } }, + "node_modules/@ariakit/components": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/components/-/components-0.1.2.tgz", + "integrity": "sha512-tvh2P0x1cJnoPXnmDEJwdRk3z7x6cTB8ArctcZdAUXlRg9tuwW/rJoBFJMzD5qMI9CDDlQ3Zctx58HvENw4BYw==", + "license": "MIT", + "dependencies": { + "@ariakit/store": "0.1.2", + "@ariakit/utils": "0.1.2" + } + }, "node_modules/@ariakit/core": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.15.tgz", - "integrity": "sha512-vvxmZvkNhiisKM+Y1TbGMUfVVchV/sWu9F0xw0RYADXcimWPK31dd9JnIZs/OQ5pwAryAHmERHwuGQVESkSjwQ==", + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.20.tgz", + "integrity": "sha512-DJbUnui0fM+2ZgiWLOMuFOmlWSJDNV3f6tqghIYRTWEm51TN/LoU6uM8og6/g7Nrwl4Uo5l8AoQT9Kkr/i/uRg==", + "deprecated": "This package has been split into smaller packages. Use @ariakit/components, @ariakit/store, or @ariakit/utils depending on the APIs you need.", "license": "MIT" }, "node_modules/@ariakit/react": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.17.tgz", - "integrity": "sha512-HQaIboE2axtlncJz1hRTaiQfJ1GGjhdtNcAnPwdjvl2RybfmlHowIB+HTVBp36LzroKPs/M4hPCxk7XTaqRZGg==", + "version": "0.4.29", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.29.tgz", + "integrity": "sha512-SLXlsddWHSwfUol4Yi0zULlalNWjzWjpS3zg7B7aaPd64saONQ5ktWf9KMxqBklcpjMLeF2dB9BAHAvpPVdCIQ==", "license": "MIT", "dependencies": { - "@ariakit/react-core": "0.4.17" + "@ariakit/react-components": "0.1.2" }, "funding": { "type": "opencollective", @@ -785,21 +796,83 @@ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@ariakit/react-core": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.17.tgz", - "integrity": "sha512-kFF6n+gC/5CRQIyaMTFoBPio2xUe0k9rZhMNdUobWRmc/twfeLVkODx+8UVYaNyKilTge8G0JFqwvFKku/jKEw==", + "node_modules/@ariakit/react-components": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/react-components/-/react-components-0.1.2.tgz", + "integrity": "sha512-SM+SPMAVlOZmGAfWNBza+0k9y4mkA5/dJhDoOyhE96cbNARy665uLdwowSJl1JGuFfcZzuzAwGon7f/rYeyfkQ==", "license": "MIT", "dependencies": { - "@ariakit/core": "0.4.15", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" + "@ariakit/components": "0.1.2", + "@ariakit/react-store": "0.1.2", + "@ariakit/react-utils": "0.1.2", + "@ariakit/store": "0.1.2", + "@ariakit/utils": "0.1.2", + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@ariakit/react-core": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.26.tgz", + "integrity": "sha512-/Peh1KiVpjj79nCJIa6lEdzSTT9P9FZoy+CxByIFKL3YKdlXmDIIhS1E/tAqKbDq4ODVdynnqmrIDxE5wCoZYw==", + "deprecated": "This package has been split into smaller packages. Use @ariakit/react-components or @ariakit/react-utils depending on the APIs you need.", + "license": "MIT", + "dependencies": { + "@ariakit/core": "0.4.20", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/react-store": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/react-store/-/react-store-0.1.2.tgz", + "integrity": "sha512-1r1Gn0tqhnOS0LFvHNGzn5/8C5aOANO5vb0Gxh94oR/be4zwCSE2zfQjOjRfpL+BBDhOcProME2+G6UslEJxbg==", + "license": "MIT", + "dependencies": { + "@ariakit/react-utils": "0.1.2", + "@ariakit/store": "0.1.2", + "@ariakit/utils": "0.1.2", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/react-utils": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/react-utils/-/react-utils-0.1.2.tgz", + "integrity": "sha512-Rnl6D1542Mqu80xK++oUv1JXS0PtNmKXd9nkdud5nyvySiBDTrmPqRW44/D+5GbuZrboreQuY3tPYwKL7a7onQ==", + "license": "MIT", + "dependencies": { + "@ariakit/store": "0.1.2", + "@ariakit/utils": "0.1.2" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/store": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/store/-/store-0.1.2.tgz", + "integrity": "sha512-SS7bV4+a+1q9M9i0WV6DD4P/ypRKlCvII8soo2UMe1yuaxZA/Fc0htHe+EZwjJ6TMLjHfHh2TDSnXyrjC7QImA==", + "license": "MIT", + "dependencies": { + "@ariakit/utils": "0.1.2" + } + }, + "node_modules/@ariakit/utils": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/utils/-/utils-0.1.2.tgz", + "integrity": "sha512-lBJhtBWpKjIck/9i7G8cahvaUgLsyGklI/Pjv+VtY9KTzyuzX5GpRbbLKMS/e1qLnFPS4C3CybYB70b1bVcAkw==", + "license": "MIT" + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -43076,8 +43149,8 @@ "typescript": "^5.9.3" }, "peerDependencies": { - "@ariakit/react": "^0.4.16", - "@ariakit/react-core": "^0.4.17", + "@ariakit/react": "^0.4.29", + "@ariakit/react-core": "^0.4.26", "@dicebear/collection": "^9.4.1", "@dicebear/core": "^9.4.1", "@headlessui/react": "^2.1.2", diff --git a/packages/client/package.json b/packages/client/package.json index 17674b9cc4..ad30791801 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -36,8 +36,8 @@ "dev": "tsdown --watch" }, "peerDependencies": { - "@ariakit/react": "^0.4.16", - "@ariakit/react-core": "^0.4.17", + "@ariakit/react": "^0.4.29", + "@ariakit/react-core": "^0.4.26", "@dicebear/collection": "^9.4.1", "@dicebear/core": "^9.4.1", "@headlessui/react": "^2.1.2", diff --git a/packages/client/src/components/DataTable.tsx b/packages/client/src/components/DataTable.tsx index 09136ac33a..9b3712aa81 100644 --- a/packages/client/src/components/DataTable.tsx +++ b/packages/client/src/components/DataTable.tsx @@ -435,7 +435,10 @@ export default function DataTable({ className, )} > - +
{table.getHeaderGroups().map((headerGroup) => (