🪜 fix: Layering Conflicts and UX Polish (#11177)

* 🔧 refactor: Update z-index values for popover components

- Reduced z-index from 50 to 40 across various popover components including Artifacts, ArtifactsSubMenu, MCPSubMenu, CustomMenu, and others to ensure consistent layering and improve UI behavior.
- Adjusted related CSS styles in Dropdown.css and DropdownMenu.tsx to align with the new z-index values, enhancing overall component visibility and interaction.

* chore: remove string template for className concatenation in CustomMenu component

- Improved the readability of the className prop in the CustomMenu component by restructuring the concatenation of class names. This change enhances maintainability and clarity in the styling logic.

* refactor: Simplify button visibility logic in SiblingHeader component

- Updated the button rendering logic in the SiblingHeader component to improve clarity and maintainability. The button is now always rendered, with its visibility controlled by the disabled state based on messageId, agentId, and submission status, enhancing user experience during interactions.

* refactor: Update shift key handling in Conversation and ConvoOptions components

- Modified the handling of the `isShiftHeld` state in both the Conversation and ConvoOptions components to improve clarity and functionality. The logic now ensures that the shift key state is accurately reflected based on the active conversation status, enhancing user interaction during conversations.
- Cleaned up imports in ConvoOptions by removing the unused `useShiftKey` hook, streamlining the component's dependencies.

* refactor: Improve Escape key handling in OriginalDialog component

- Updated the Escape key handling logic to prevent closing the dialog when a tooltip or dropdown menu has focus. This change enhances accessibility by ensuring compliance with WCAG standards for dismissable tooltips.
- Simplified the focus checking mechanism by directly assessing the active element within dropdown menus and tooltips, improving code clarity and maintainability.

* chore: imports

* refactor: Enhance Escape key handling in OriginalDialog component

- Updated the Escape key handling logic to prevent closing the dialog when a trigger with an open popover is focused. This change improves accessibility and user experience by ensuring that the dialog remains open during interactions with popovers, dropdowns, and listboxes.
- Simplified the focus checking mechanism to include additional roles, enhancing the clarity and maintainability of the code.

* refactor: Add dropdownClassName prop to FilterPrompts component

- Enhanced the FilterPrompts component by introducing a new dropdownClassName prop, allowing for customizable styling of the dropdown element.
- Updated the PromptsView component to utilize the new prop, improving the flexibility of the FilterPrompts integration within the UI.

* refactor: Clean up imports and remove unused code in DashBreadcrumb component

- Streamlined the DashBreadcrumb component by removing commented-out imports and unused code, enhancing clarity and maintainability.
- Adjusted the import order for better organization and readability.

* refactor: Update z-index handling in Dropdown component

- Removed the z-index property from Dropdown.css to streamline styling.
- Adjusted the className in Dropdown.tsx to include a new z-40 class for consistent z-index management, enhancing UI layering and interaction.

* refactor: Enhance file type acceptance in AttachFileMenu and useDragHelpers

- Updated the AttachFileMenu component to accept additional image formats (.heif, .heic) alongside existing types, improving file upload flexibility.
- Modified the useDragHelpers hook to utilize inferMimeType for better file type detection, ensuring accurate handling of dragged files.

* refactor: Enhance FloatingThinkingBar with copy functionality

- Added a copy button to the FloatingThinkingBar component, allowing users to copy thoughts to the clipboard.
- Updated the tooltip descriptions for the expand/collapse and copy actions to improve user experience.
- Cleaned up imports and adjusted prop types for better clarity and maintainability.

* refactor: Enhance RunCode component with icon-only mode

- Updated the RunCode component to accept an `iconOnly` prop, allowing for a simplified button display that shows only the icon when desired.
- Adjusted the button rendering logic to improve user experience and maintainability.
- Cleaned up imports and ensured consistent styling in the FloatingCodeBar component.
This commit is contained in:
Danny Avila 2026-01-02 11:43:03 -05:00 committed by GitHub
parent 1e74dc231f
commit b1a2b96276
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 233 additions and 164 deletions

View file

@ -107,7 +107,7 @@ function Artifacts() {
<Ariakit.Menu
gutter={4}
className={cn(
'animate-popover-top-left z-50 flex min-w-[250px] flex-col rounded-xl',
'animate-popover-top-left z-40 flex min-w-[250px] flex-col rounded-xl',
'border border-border-light bg-surface-secondary shadow-lg',
)}
portal={true}

View file

@ -90,7 +90,7 @@ const ArtifactsSubMenu = React.forwardRef<HTMLDivElement, ArtifactsSubMenuProps>
portal={true}
unmountOnHide={true}
className={cn(
'animate-popover-left z-50 ml-3 mt-6 flex min-w-[250px] flex-col rounded-xl',
'animate-popover-left z-40 ml-3 mt-6 flex min-w-[250px] flex-col rounded-xl',
'border border-border-light bg-surface-secondary shadow-lg',
)}
>

View file

@ -94,13 +94,13 @@ const AttachFileMenu = ({
}
inputRef.current.value = '';
if (fileType === 'image') {
inputRef.current.accept = 'image/*';
inputRef.current.accept = 'image/*,.heif,.heic';
} else if (fileType === 'document') {
inputRef.current.accept = '.pdf,application/pdf';
} else if (fileType === 'image_document') {
inputRef.current.accept = 'image/*,.pdf,application/pdf';
inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf';
} else if (fileType === 'image_document_video_audio') {
inputRef.current.accept = 'image/*,.pdf,application/pdf,video/*,audio/*';
inputRef.current.accept = 'image/*,.heif,.heic,.pdf,application/pdf,video/*,audio/*';
} else {
inputRef.current.accept = '';
}

View file

@ -87,7 +87,7 @@ const MCPSubMenu = React.forwardRef<HTMLDivElement, MCPSubMenuProps>(
unmountOnHide={true}
aria-label={localize('com_ui_mcp_servers')}
className={cn(
'animate-popover-left z-50 ml-3 flex min-w-[260px] max-w-[320px] flex-col rounded-xl',
'animate-popover-left z-40 ml-3 flex min-w-[260px] max-w-[320px] flex-col rounded-xl',
'border border-border-light bg-presentation p-1.5 shadow-lg',
)}
>

View file

@ -64,7 +64,8 @@ export const CustomMenu = React.forwardRef<HTMLDivElement, CustomMenuProps>(func
unmountOnHide
gutter={parent ? -4 : 4}
className={cn(
`${parent ? 'animate-popover-left ml-3' : 'animate-popover'} outline-none! z-50 flex max-h-[min(450px,var(--popover-available-height))] w-full`,
parent ? 'animate-popover-left ml-3' : 'animate-popover',
'outline-none! z-40 flex max-h-[min(450px,var(--popover-available-height))] w-full',
'w-[var(--menu-width,auto)] min-w-[300px] flex-col overflow-auto rounded-xl border border-border-light',
'bg-presentation px-3 py-2 text-sm text-text-primary shadow-lg',
'max-w-[calc(100vw-4rem)] sm:max-h-[calc(65vh)] sm:max-w-[400px]',

View file

@ -121,6 +121,7 @@ const Reasoning = memo(({ reasoning, isLast }: ReasoningProps) => {
isVisible={isBarVisible && isExpanded}
isExpanded={isExpanded}
onClick={handleClick}
content={reasoningText}
/>
</div>
</div>

View file

@ -1,8 +1,8 @@
import { useState, useMemo, memo, useCallback, useRef } from 'react';
import { useState, useMemo, memo, useCallback, useRef, type MouseEvent } from 'react';
import { useAtomValue } from 'jotai';
import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client';
import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react';
import type { MouseEvent, FocusEvent, FC } from 'react';
import type { FocusEvent, FC } from 'react';
import { showThinkingAtom } from '~/store/showThinking';
import { fontSizeAtom } from '~/store/fontSize';
import { useLocalize } from '~/hooks';
@ -122,7 +122,7 @@ export const ThinkingButton = memo(
);
/**
* FloatingThinkingBar - Floating bar with expand/collapse button
* FloatingThinkingBar - Floating bar with expand/collapse and copy buttons
* Shows on hover/focus, positioned at bottom right of thinking content
* Inspired by CodeBlock's FloatingCodeBar pattern
*/
@ -131,16 +131,36 @@ export const FloatingThinkingBar = memo(
isVisible,
isExpanded,
onClick,
content,
}: {
isVisible: boolean;
isExpanded: boolean;
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
content?: string;
}) => {
const localize = useLocalize();
const tooltipText = isExpanded
const [isCopied, setIsCopied] = useState(false);
const handleCopy = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
if (content) {
navigator.clipboard.writeText(content);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
}
},
[content],
);
const collapseTooltip = isExpanded
? localize('com_ui_collapse_thoughts')
: localize('com_ui_expand_thoughts');
const copyTooltip = isCopied
? localize('com_ui_copied_to_clipboard')
: localize('com_ui_copy_thoughts_to_clipboard');
return (
<div
className={cn(
@ -149,13 +169,13 @@ export const FloatingThinkingBar = memo(
)}
>
<TooltipAnchor
description={tooltipText}
description={collapseTooltip}
render={
<button
type="button"
tabIndex={isVisible ? 0 : -1}
onClick={onClick}
aria-label={tooltipText}
aria-label={collapseTooltip}
className={cn(
'flex items-center justify-center rounded-lg bg-surface-secondary p-1.5 text-text-secondary-alt shadow-sm',
'hover:bg-surface-hover hover:text-text-primary',
@ -170,6 +190,30 @@ export const FloatingThinkingBar = memo(
</button>
}
/>
{content && (
<TooltipAnchor
description={copyTooltip}
render={
<button
type="button"
tabIndex={isVisible ? 0 : -1}
onClick={handleCopy}
aria-label={copyTooltip}
className={cn(
'flex items-center justify-center rounded-lg bg-surface-secondary p-1.5 text-text-secondary-alt shadow-sm',
'hover:bg-surface-hover hover:text-text-primary',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-heavy',
)}
>
{isCopied ? (
<CheckMark className="h-[18px] w-[18px]" aria-hidden="true" />
) : (
<Clipboard size="18" aria-hidden="true" />
)}
</button>
}
/>
)}
</div>
);
},
@ -265,6 +309,7 @@ const Thinking: React.ElementType = memo(({ children }: { children: React.ReactN
isVisible={isBarVisible && isExpanded}
isExpanded={isExpanded}
onClick={handleClick}
content={textContent}
/>
</div>
</div>

View file

@ -118,23 +118,22 @@ export default function SiblingHeader({
</div>
<span className="truncate text-sm font-medium text-text-primary">{displayName}</span>
</div>
{messageId && agentId && !isSubmitting && (
<button
type="button"
onClick={handleBranch}
disabled={isSubmitting || branchMessage.isLoading}
className={cn(
'flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-md',
'text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-primary',
'focus:outline-none focus:ring-2 focus:ring-border-medium focus:ring-offset-1',
'disabled:cursor-not-allowed disabled:opacity-50',
)}
aria-label={localize('com_ui_branch_message')}
title={localize('com_ui_branch_message')}
>
<GitBranchPlus className="h-4 w-4" aria-hidden="true" />
</button>
)}
<button
type="button"
onClick={handleBranch}
disabled={!messageId || !agentId || isSubmitting || branchMessage.isLoading}
className={cn(
'flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-md',
'text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-primary',
'focus:outline-none focus:ring-2 focus:ring-border-medium focus:ring-offset-1',
'disabled:cursor-not-allowed disabled:opacity-50',
(!messageId || !agentId || isSubmitting) && 'invisible',
)}
aria-label={localize('com_ui_branch_message')}
title={localize('com_ui_branch_message')}
>
<GitBranchPlus className="h-4 w-4" aria-hidden="true" />
</button>
</div>
);
}

View file

@ -142,6 +142,7 @@ export default function Conversation({
conversationId,
isPopoverActive,
setIsPopoverActive,
isShiftHeld: isActiveConvo ? isShiftHeld : false,
};
return (
@ -236,7 +237,7 @@ export default function Conversation({
isPopoverActive || isActiveConvo
? 'pointer-events-auto scale-x-100 opacity-100'
: 'pointer-events-none max-w-0 scale-x-0 opacity-0 group-focus-within:pointer-events-auto group-focus-within:max-w-[60px] group-focus-within:scale-x-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:max-w-[60px] group-hover:scale-x-100 group-hover:opacity-100',
(isPopoverActive || isActiveConvo) && (isShiftHeld ? 'max-w-[60px]' : 'max-w-[28px]'),
!isPopoverActive && isActiveConvo && isShiftHeld ? 'max-w-[60px]' : 'max-w-[28px]',
)}
// Removing aria-hidden to fix accessibility issue: ARIA hidden element must not be focusable or contain focusable elements
// but not sure what its original purpose was, so leaving the property commented out until it can be cleared safe to delete.

View file

@ -13,7 +13,7 @@ import {
useGetStartupConfig,
useArchiveConvoMutation,
} from '~/data-provider';
import { useLocalize, useNavigateToConvo, useNewConvo, useShiftKey } from '~/hooks';
import { useLocalize, useNavigateToConvo, useNewConvo } from '~/hooks';
import { NotificationSeverity } from '~/common';
import { useChatContext } from '~/Providers';
import DeleteButton from './DeleteButton';
@ -28,6 +28,7 @@ function ConvoOptions({
isPopoverActive,
setIsPopoverActive,
isActiveConvo,
isShiftHeld = false,
}: {
conversationId: string | null;
title: string | null;
@ -36,10 +37,10 @@ function ConvoOptions({
isPopoverActive: boolean;
setIsPopoverActive: React.Dispatch<React.SetStateAction<boolean>>;
isActiveConvo: boolean;
isShiftHeld?: boolean;
}) {
const localize = useLocalize();
const queryClient = useQueryClient();
const isShiftHeld = useShiftKey();
const { index } = useChatContext();
const { data: startupConfig } = useGetStartupConfig();
const { navigateToConvo } = useNavigateToConvo(index);
@ -253,7 +254,7 @@ function ConvoOptions({
: 'opacity-0 focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[open]:opacity-100',
);
if (isShiftHeld) {
if (isShiftHeld && isActiveConvo && !isPopoverActive && !showShareDialog && !showDeleteDialog) {
return (
<div className="flex items-center gap-0.5">
<button
@ -350,6 +351,7 @@ export default memo(ConvoOptions, (prevProps, nextProps) => {
prevProps.conversationId === nextProps.conversationId &&
prevProps.title === nextProps.title &&
prevProps.isPopoverActive === nextProps.isPopoverActive &&
prevProps.isActiveConvo === nextProps.isActiveConvo
prevProps.isActiveConvo === nextProps.isActiveConvo &&
prevProps.isShiftHeld === nextProps.isShiftHeld
);
});

View file

@ -112,7 +112,7 @@ const FloatingCodeBar: React.FC<FloatingCodeBarProps> = React.memo(
) : (
<>
{allowExecution === true && (
<RunCode lang={lang} codeRef={codeRef} blockIndex={blockIndex} />
<RunCode lang={lang} codeRef={codeRef} blockIndex={blockIndex} iconOnly />
)}
<TooltipAnchor
description={isCopied ? localize('com_ui_copied') : localize('com_ui_copy_code')}

View file

@ -2,7 +2,7 @@ import React, { useMemo, useCallback, useEffect } from 'react';
import debounce from 'lodash/debounce';
import { TerminalSquareIcon } from 'lucide-react';
import { Tools, AuthType } from 'librechat-data-provider';
import { Spinner, useToastContext } from '@librechat/client';
import { Spinner, TooltipAnchor, useToastContext } from '@librechat/client';
import type { CodeBarProps } from '~/common';
import { useVerifyAgentToolAuth, useToolCallMutation } from '~/data-provider';
import ApiKeyDialog from '~/components/SidePanel/Agents/Code/ApiKeyDialog';
@ -10,107 +10,125 @@ import { useLocalize, useCodeApiKeyForm } from '~/hooks';
import { useMessageContext } from '~/Providers';
import { cn, normalizeLanguage } from '~/utils';
const RunCode: React.FC<CodeBarProps> = React.memo(({ lang, codeRef, blockIndex }) => {
const localize = useLocalize();
const { showToast } = useToastContext();
const execute = useToolCallMutation(Tools.execute_code, {
onError: () => {
showToast({ message: localize('com_ui_run_code_error'), status: 'error' });
},
});
const RunCode: React.FC<CodeBarProps & { iconOnly?: boolean }> = React.memo(
({ lang, codeRef, blockIndex, iconOnly = false }) => {
const localize = useLocalize();
const { showToast } = useToastContext();
const execute = useToolCallMutation(Tools.execute_code, {
onError: () => {
showToast({ message: localize('com_ui_run_code_error'), status: 'error' });
},
});
const { messageId, conversationId, partIndex } = useMessageContext();
const normalizedLang = useMemo(() => normalizeLanguage(lang), [lang]);
const { data } = useVerifyAgentToolAuth(
{ toolId: Tools.execute_code },
{
retry: 1,
},
);
const authType = useMemo(() => data?.message ?? false, [data?.message]);
const isAuthenticated = useMemo(() => data?.authenticated ?? false, [data?.authenticated]);
const { methods, onSubmit, isDialogOpen, setIsDialogOpen, handleRevokeApiKey } =
useCodeApiKeyForm({});
const { messageId, conversationId, partIndex } = useMessageContext();
const normalizedLang = useMemo(() => normalizeLanguage(lang), [lang]);
const { data } = useVerifyAgentToolAuth(
{ toolId: Tools.execute_code },
{
retry: 1,
},
);
const authType = useMemo(() => data?.message ?? false, [data?.message]);
const isAuthenticated = useMemo(() => data?.authenticated ?? false, [data?.authenticated]);
const { methods, onSubmit, isDialogOpen, setIsDialogOpen, handleRevokeApiKey } =
useCodeApiKeyForm({});
const handleExecute = useCallback(async () => {
if (!isAuthenticated) {
setIsDialogOpen(true);
return;
}
const codeString: string = codeRef.current?.textContent ?? '';
if (
typeof codeString !== 'string' ||
codeString.length === 0 ||
typeof normalizedLang !== 'string' ||
normalizedLang.length === 0
) {
return;
}
const handleExecute = useCallback(async () => {
if (!isAuthenticated) {
setIsDialogOpen(true);
return;
}
const codeString: string = codeRef.current?.textContent ?? '';
if (
typeof codeString !== 'string' ||
codeString.length === 0 ||
typeof normalizedLang !== 'string' ||
normalizedLang.length === 0
) {
return;
}
execute.mutate({
execute.mutate({
partIndex,
messageId,
blockIndex,
conversationId: conversationId ?? '',
lang: normalizedLang,
code: codeString,
});
}, [
codeRef,
execute,
partIndex,
messageId,
blockIndex,
conversationId: conversationId ?? '',
lang: normalizedLang,
code: codeString,
});
}, [
codeRef,
execute,
partIndex,
messageId,
blockIndex,
conversationId,
normalizedLang,
setIsDialogOpen,
isAuthenticated,
]);
conversationId,
normalizedLang,
setIsDialogOpen,
isAuthenticated,
]);
const debouncedExecute = useMemo(
() => debounce(handleExecute, 1000, { leading: true }),
[handleExecute],
);
const debouncedExecute = useMemo(
() => debounce(handleExecute, 1000, { leading: true }),
[handleExecute],
);
useEffect(() => {
return () => {
debouncedExecute.cancel();
};
}, [debouncedExecute]);
useEffect(() => {
return () => {
debouncedExecute.cancel();
};
}, [debouncedExecute]);
if (typeof normalizedLang !== 'string' || normalizedLang.length === 0) {
return null;
}
if (typeof normalizedLang !== 'string' || normalizedLang.length === 0) {
return null;
}
return (
<>
<button
type="button"
className={cn(
'ml-auto flex gap-2 rounded-sm px-2 py-1 hover:bg-gray-700 focus:bg-gray-700 focus:outline focus:outline-white',
)}
onClick={debouncedExecute}
disabled={execute.isLoading}
>
const buttonContent = (
<>
{execute.isLoading ? (
<Spinner className="animate-spin" size={18} />
) : (
<TerminalSquareIcon size={18} aria-hidden="true" />
)}
{localize('com_ui_run_code')}
{!iconOnly && localize('com_ui_run_code')}
</>
);
const button = (
<button
type="button"
className={cn(
'flex items-center justify-center rounded-sm hover:bg-gray-700 focus:bg-gray-700 focus:outline focus:outline-white',
iconOnly ? 'p-1.5' : 'ml-auto gap-2 px-2 py-1',
)}
onClick={debouncedExecute}
disabled={execute.isLoading}
aria-label={localize('com_ui_run_code')}
>
{buttonContent}
</button>
<ApiKeyDialog
onSubmit={onSubmit}
isOpen={isDialogOpen}
register={methods.register}
onRevoke={handleRevokeApiKey}
onOpenChange={setIsDialogOpen}
handleSubmit={methods.handleSubmit}
isToolAuthenticated={isAuthenticated}
isUserProvided={authType === AuthType.USER_PROVIDED}
/>
</>
);
});
);
return (
<>
{iconOnly ? (
<TooltipAnchor description={localize('com_ui_run_code')} render={button} />
) : (
button
)}
<ApiKeyDialog
onSubmit={onSubmit}
isOpen={isDialogOpen}
register={methods.register}
onRevoke={handleRevokeApiKey}
onOpenChange={setIsDialogOpen}
handleSubmit={methods.handleSubmit}
isToolAuthenticated={isAuthenticated}
isUserProvided={authType === AuthType.USER_PROVIDED}
/>
</>
);
},
);
export default RunCode;

View file

@ -1,13 +1,13 @@
import filenamify from 'filenamify';
import { useEffect, useState, useMemo, useCallback } from 'react';
import {
OGDialogTemplate,
OGDialog,
Button,
Input,
Label,
Button,
OGDialog,
Checkbox,
Dropdown,
OGDialogTemplate,
} from '@librechat/client';
import type { TConversation } from 'librechat-data-provider';
import { useLocalize, useExportConversation } from '~/hooks';

View file

@ -9,7 +9,13 @@ import { usePromptGroupsContext } from '~/Providers';
import { cn } from '~/utils';
import store from '~/store';
export default function FilterPrompts({ className = '' }: { className?: string }) {
export default function FilterPrompts({
className = '',
dropdownClassName = '',
}: {
className?: string;
dropdownClassName?: string;
}) {
const localize = useLocalize();
const { name, setName, hasAccess, promptGroups } = usePromptGroupsContext();
const { categories } = useCategories({ className: 'h-4 w-4', hasAccess });
@ -94,7 +100,7 @@ export default function FilterPrompts({ className = '' }: { className?: string }
value={categoryFilter || SystemCategories.ALL}
onChange={onSelect}
options={filterOptions}
className="rounded-lg bg-transparent"
className={cn('rounded-lg bg-transparent', dropdownClassName)}
icon={<ListFilter className="h-4 w-4" />}
label="Filter: "
ariaLabel={localize('com_ui_filter_prompts')}

View file

@ -93,7 +93,7 @@ export default function PromptsView() {
onClose={isSmallerScreen && isDetailView ? togglePanel : undefined}
>
<div className="mt-1 flex flex-row items-center justify-between px-2 md:px-2">
<FilterPrompts />
<FilterPrompts dropdownClassName="z-[100]" />
</div>
</GroupSidePanel>
</div>

View file

@ -8,6 +8,7 @@ import {
Tools,
QueryKeys,
Constants,
inferMimeType,
EToolResources,
EModelEndpoint,
mergeFileConfig,
@ -118,7 +119,9 @@ export default function useDragHelpers() {
}
/** Determine if dragged files are all images (enables the base image option) */
const allImages = item.files.every((f) => f.type?.startsWith('image/'));
const allImages = item.files.every((f) =>
inferMimeType(f.name, f.type)?.startsWith('image/'),
);
const shouldShowModal =
allImages ||

View file

@ -1,27 +1,20 @@
import { useMemo, useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
import { Sidebar } from '@librechat/client';
import { useLocation } from 'react-router-dom';
import { SystemRoles } from 'librechat-data-provider';
import { ArrowLeft, MessageSquareQuote } from 'lucide-react';
import { Sidebar } from '@librechat/client';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
// BreadcrumbEllipsis,
// DropdownMenu,
// DropdownMenuItem,
// DropdownMenuContent,
// DropdownMenuTrigger,
} from '@librechat/client';
import { useLocalize, useCustomLink, useAuthContext } from '~/hooks';
import AdvancedSwitch from '~/components/Prompts/AdvancedSwitch';
// import { RightPanel } from '../../components/Prompts/RightPanel';
import AdminSettings from '~/components/Prompts/AdminSettings';
import { useDashboardContext } from '~/Providers';
// import { PromptsEditorMode } from '~/common';
import store from '~/store';
const promptsPathPattern = /prompts\/(?!new(?:\/|$)).*$/;

View file

@ -104,7 +104,7 @@ export default function ComboboxComponent({
aria-label={ariaLabel + 's'}
position="popper"
className={cn(
'bg-popover text-popover-foreground relative z-50 max-h-[52vh] min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600',
'bg-popover text-popover-foreground relative z-40 max-h-[52vh] min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600',
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
'bg-white dark:bg-gray-700',
)}

View file

@ -134,7 +134,7 @@ function ControlCombobox({
gutter={4}
portal
className={cn(
'animate-popover z-50 overflow-hidden rounded-xl border border-border-light bg-surface-secondary shadow-lg',
'animate-popover z-40 overflow-hidden rounded-xl border border-border-light bg-surface-secondary shadow-lg',
)}
style={{ width: isCollapsed ? '300px' : (buttonWidth ?? '300px') }}
>

View file

@ -23,7 +23,6 @@
translate: 0 -0.5rem;
margin-top: 4px;
margin-right: -2px;
z-index: 100;
}
.popover-animate {

View file

@ -102,7 +102,7 @@ const Dropdown: React.FC<DropdownProps> = ({
portal={portal}
store={selectProps}
className={cn(
'popover-ui',
'popover-ui z-40',
sizeClasses,
className,
'max-h-[80vh] overflow-y-auto',

View file

@ -30,7 +30,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
'text-popover-foreground max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border border-border-light bg-surface-secondary p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'text-popover-foreground max-h-(--radix-dropdown-menu-content-available-height) origin-(--radix-dropdown-menu-content-transform-origin) z-40 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border border-border-light bg-surface-secondary p-1 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
@ -198,7 +198,7 @@ function DropdownMenuSubContent({
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
'text-popover-foreground origin-(--radix-dropdown-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border border-border-medium bg-surface-secondary p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
'text-popover-foreground origin-(--radix-dropdown-menu-content-transform-origin) z-40 min-w-[8rem] overflow-hidden rounded-md border border-border-medium bg-surface-secondary p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}

View file

@ -85,7 +85,7 @@ const Menu: React.FC<MenuProps> = ({
finalFocus={finalFocus}
unmountOnHide={unmountOnHide}
preserveTabOrder={preserveTabOrder}
className={cn('popover-ui z-50', className)}
className={cn('popover-ui z-40', className)}
{...props}
>
{items

View file

@ -137,7 +137,7 @@ export default function MultiSelect<T extends string>({
unmountOnHide
finalFocus={selectRef}
className={cn(
'animate-popover z-50 flex max-h-[300px]',
'animate-popover z-40 flex max-h-[300px]',
'flex-col overflow-auto overscroll-contain rounded-xl',
'bg-surface-secondary px-1.5 py-1 text-text-primary shadow-lg',
'border border-border-light',

View file

@ -96,33 +96,34 @@ const DialogContent = React.forwardRef<
const depth = React.useContext(DialogDepthContext);
const contentZIndex = 100 + (depth - 1) * 60;
/* Handle Escape key to prevent closing dialog if a tooltip or dropdown is open
/* Handle Escape key to prevent closing dialog if a tooltip or dropdown has focus
(this is a workaround in order to achieve WCAG compliance which requires
that our tooltips be dismissable with Escape key) */
const handleEscapeKeyDown = React.useCallback(
(event: KeyboardEvent) => {
const tooltips = document.querySelectorAll('.tooltip');
const dropdownMenus = document.querySelectorAll('[role="menu"]');
const activeElement = document.activeElement;
for (const tooltip of tooltips) {
const computedStyle = window.getComputedStyle(tooltip);
if (
computedStyle.display !== 'none' &&
computedStyle.visibility !== 'hidden' &&
parseFloat(computedStyle.opacity) > 0
) {
// Check if active element is a trigger with an open popover (aria-expanded="true")
if (activeElement?.getAttribute('aria-expanded') === 'true') {
event.preventDefault();
return;
}
// Check if a dropdown menu, listbox, or combobox has focus (focus is within it)
const popoverElements = document.querySelectorAll(
'[role="menu"], [role="listbox"], [role="combobox"]',
);
for (const popover of popoverElements) {
if (popover.contains(activeElement)) {
event.preventDefault();
return;
}
}
for (const dropdownMenu of dropdownMenus) {
const computedStyle = window.getComputedStyle(dropdownMenu);
if (
computedStyle.display !== 'none' &&
computedStyle.visibility !== 'hidden' &&
parseFloat(computedStyle.opacity) > 0
) {
// Check if a tooltip has focus (focus is within it)
const tooltips = document.querySelectorAll('.tooltip');
for (const tooltip of tooltips) {
if (tooltip.contains(activeElement)) {
event.preventDefault();
return;
}

View file

@ -75,7 +75,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
'bg-popover text-popover-foreground relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600',
'bg-popover text-popover-foreground relative z-40 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-gray-600',
position === 'popper'
? 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1'
: '',