mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-24 11:24:10 +01:00
🪄 refactor: UI Polish and Admin Dialog Unification (#11108)
* refactor(OpenSidebar): removed useless classNames
* style(Header): update hover styles across various components for improved UI consistency
* style(Nav): update hover styles in AccountSettings and SearchBar for improved UI consistency
* style: update button classes for consistent hover effects and improved UI responsiveness
* style(Nav, OpenSidebar, Header, Convo): improve UI responsiveness and animation transitions
* style(PresetsMenu, NewChat): update icon sizes and improve component styling for better UI consistency
* style(Nav, Root): enhance sidebar mobile animations and responsiveness for better UI experience
* style(ExportAndShareMenu, BookmarkMenu): update icon sizes for improved UI consistency
* style: remove transition duration from button classes for improved UI responsiveness
* style(CustomMenu, ModelSelector): update background colors for improved UI consistency and responsiveness
* style(ExportAndShareMenu): update icon color for improved UI consistency
* style(TemporaryChat): refine button styles for improved UI consistency and responsiveness
* style(BookmarkNav): refactor to use DropdownPopup and remove BookmarkNavItems for improved UI consistency and functionality
* style(CustomMenu, EndpointItem): enhance UI elements for improved consistency and accessibility
* style(EndpointItem): adjust gap in icon container for improved layout consistency
* style(CustomMenu, EndpointItem): update focus ring color for improved UI consistency
* style(EndpointItem): update icon color for improved UI consistency in dark theme
* style: update focus styles for improved accessibility and consistency across components
* refactor(Nav): extract sidebar width to NAV_WIDTH constant
Centralize mobile (320px) and desktop (260px) sidebar widths in a single
exported constant to avoid magic numbers and ensure consistency.
* fix(BookmarkNav): memoize handlers used in useMemo
Wrap handleTagClick and handleClear in useCallback and add them to the
dropdownItems useMemo dependency array to prevent stale closures.
* feat: introduce FilterInput component and replace existing inputs with it across multiple components
* feat(DataTable): replace custom input with FilterInput component for improved filtering
* fix: Nested dialog overlay stacking issue
Fixes overlay appearing behind content when opening nested dialogs.
Introduced dynamic z-index calculation based on dialog depth using React context.
- First dialog: overlay z-50, content z-100
- Nested dialogs increment by 60: overlay z-110/content z-160, etc.
Preserves a11y escape key handling from #10975 and #10851.
Regression from #11008 (afb67fcf1) which increased content z-index
without adjusting overlay z-index for nested dialog scenarios.
* Refactor admin settings components to use a unified AdminSettingsDialog
- Removed redundant code from AdminSettings, MCPAdminSettings, and Memories AdminSettings components.
- Introduced AdminSettingsDialog component to handle permission management for different sections.
- Updated permission handling logic to use a consistent structure across components.
- Enhanced role selection and permission confirmation features in the new dialog.
- Improved UI consistency and maintainability by centralizing dialog functionality.
* refactor(Memory): memory management UI components and replace MemoryViewer with MemoryPanel
* refactor(Memory): enhance UI components for Memory dialogs and improve input styling
* refactor(Bookmarks): improve bookmark management UI with enhanced styling
* refactor(translations): remove redundant filter input and bookmark count entries
* refactor(Convo): integrate useShiftKey hook for enhanced keyboard interaction and improve UI responsiveness
This commit is contained in:
parent
c21733930c
commit
5181356bef
71 changed files with 2115 additions and 2191 deletions
|
|
@ -84,7 +84,7 @@ export default function ExportAndShareMenu({
|
|||
className="inline-flex size-10 flex-shrink-0 items-center justify-center rounded-xl border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
|
||||
>
|
||||
<Share2
|
||||
className="icon-md text-text-secondary"
|
||||
className="icon-lg text-text-primary"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ export default function Header() {
|
|||
{!navVisible && (
|
||||
<motion.div
|
||||
className="flex items-center gap-2"
|
||||
initial={{ width: 0, opacity: 0 }}
|
||||
animate={{ width: 'auto', opacity: 1 }}
|
||||
exit={{ width: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
key="header-buttons"
|
||||
>
|
||||
<OpenSidebar setNavVisible={setNavVisible} className="max-md:hidden" />
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const AttachFile = ({ disabled }: { disabled?: boolean | null }) => {
|
|||
aria-label={localize('com_sidepanel_attach_files')}
|
||||
disabled={isUploadDisabled}
|
||||
className={cn(
|
||||
'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50',
|
||||
'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-opacity-50',
|
||||
)}
|
||||
onKeyDownCapture={(e) => {
|
||||
if (!inputRef.current) {
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ const AttachFileMenu = ({
|
|||
id="attach-file-menu-button"
|
||||
aria-label="Attach File Options"
|
||||
className={cn(
|
||||
'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50',
|
||||
'flex size-9 items-center justify-center rounded-full p-1 hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-opacity-50',
|
||||
isPopoverActive && 'bg-surface-hover',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { Search } from 'lucide-react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
flexRender,
|
||||
|
|
@ -17,7 +16,6 @@ import type {
|
|||
} from '@tanstack/react-table';
|
||||
import { FileContext } from 'librechat-data-provider';
|
||||
import {
|
||||
Input,
|
||||
Table,
|
||||
Button,
|
||||
Spinner,
|
||||
|
|
@ -26,6 +24,7 @@ import {
|
|||
TableCell,
|
||||
TableHead,
|
||||
TrashIcon,
|
||||
FilterInput,
|
||||
TableHeader,
|
||||
useMediaQuery,
|
||||
} from '@librechat/client';
|
||||
|
|
@ -115,23 +114,13 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
|
|||
)}
|
||||
{!isSmallScreen && <span className="ml-2">{localize('com_ui_delete')}</span>}
|
||||
</Button>
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-text-secondary" />
|
||||
<Input
|
||||
id="files-filter"
|
||||
placeholder=" "
|
||||
value={(table.getColumn('filename')?.getFilterValue() as string | undefined) ?? ''}
|
||||
onChange={(event) => table.getColumn('filename')?.setFilterValue(event.target.value)}
|
||||
className="peer w-full pl-10 text-sm focus-visible:ring-2 focus-visible:ring-ring"
|
||||
aria-label={localize('com_files_filter_input')}
|
||||
/>
|
||||
<label
|
||||
htmlFor="files-filter"
|
||||
className="pointer-events-none absolute left-10 top-1/2 -translate-y-1/2 text-sm text-text-secondary transition-all duration-200 peer-focus:top-0 peer-focus:bg-background peer-focus:px-1 peer-focus:text-xs peer-[:not(:placeholder-shown)]:top-0 peer-[:not(:placeholder-shown)]:bg-background peer-[:not(:placeholder-shown)]:px-1 peer-[:not(:placeholder-shown)]:text-xs"
|
||||
>
|
||||
{localize('com_files_filter')}
|
||||
</label>
|
||||
</div>
|
||||
<FilterInput
|
||||
inputId="files-filter"
|
||||
label={localize('com_files_filter')}
|
||||
value={(table.getColumn('filename')?.getFilterValue() as string | undefined) ?? ''}
|
||||
onChange={(event) => table.getColumn('filename')?.setFilterValue(event.target.value)}
|
||||
containerClassName="flex-1"
|
||||
/>
|
||||
<div className="relative focus-within:z-[100]">
|
||||
<ColumnVisibilityDropdown
|
||||
table={table}
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => {
|
|||
id="tools-dropdown-button"
|
||||
aria-label="Tools Options"
|
||||
className={cn(
|
||||
'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50',
|
||||
'flex size-9 items-center justify-center rounded-full p-1 hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-opacity-50',
|
||||
isPopoverActive && 'bg-surface-hover',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -147,9 +147,9 @@ const BookmarkMenu: FC = () => {
|
|||
return <Spinner aria-label="Spinner" />;
|
||||
}
|
||||
if ((tags?.length ?? 0) > 0) {
|
||||
return <BookmarkFilledIcon className="icon-sm" aria-label="Filled Bookmark" />;
|
||||
return <BookmarkFilledIcon className="icon-lg" aria-label="Filled Bookmark" />;
|
||||
}
|
||||
return <BookmarkIcon className="icon-sm" aria-label="Bookmark" />;
|
||||
return <BookmarkIcon className="icon-lg" aria-label="Bookmark" />;
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ export const CustomMenu = React.forwardRef<HTMLDivElement, CustomMenuProps>(func
|
|||
!parent &&
|
||||
'flex h-10 w-full items-center justify-center gap-2 rounded-xl border border-border-light px-3 py-2 text-sm text-text-primary',
|
||||
menuStore.useState('open')
|
||||
? 'bg-surface-tertiary hover:bg-surface-tertiary'
|
||||
: 'bg-surface-secondary hover:bg-surface-tertiary',
|
||||
? 'bg-surface-active-alt hover:bg-surface-active-alt'
|
||||
: 'bg-presentation hover:bg-surface-active-alt',
|
||||
props.className,
|
||||
)}
|
||||
render={parent ? <CustomMenuItem render={trigger} /> : trigger}
|
||||
|
|
@ -66,7 +66,7 @@ export const CustomMenu = React.forwardRef<HTMLDivElement, CustomMenuProps>(func
|
|||
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`,
|
||||
'w-[var(--menu-width,auto)] min-w-[300px] flex-col overflow-auto rounded-xl border border-border-light',
|
||||
'bg-surface-secondary px-3 py-2 text-sm text-text-primary shadow-lg',
|
||||
'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]',
|
||||
searchable && 'p-0',
|
||||
)}
|
||||
|
|
@ -80,13 +80,13 @@ export const CustomMenu = React.forwardRef<HTMLDivElement, CustomMenuProps>(func
|
|||
autoSelect
|
||||
render={combobox}
|
||||
className={cn(
|
||||
'peer mt-1 h-10 w-full rounded-lg border-none bg-transparent px-2 text-base',
|
||||
'peer flex h-10 w-full items-center justify-center rounded-lg border-none bg-transparent px-2 text-base',
|
||||
'sm:h-8 sm:text-sm',
|
||||
'focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-white',
|
||||
'focus:outline-none focus:ring-0 focus-visible:ring-2 focus-visible:ring-primary',
|
||||
)}
|
||||
/>
|
||||
{comboboxLabel && (
|
||||
<label className="pointer-events-none absolute left-2.5 top-2.5 text-sm text-text-secondary transition-all duration-200 peer-[:not(:placeholder-shown)]:-top-1.5 peer-[:not(:placeholder-shown)]:left-1.5 peer-[:not(:placeholder-shown)]:bg-surface-secondary peer-[:not(:placeholder-shown)]:text-xs">
|
||||
<label className="pointer-events-none absolute left-2.5 top-2.5 text-sm text-text-secondary transition-all duration-200 peer-[:not(:placeholder-shown)]:-top-1.5 peer-[:not(:placeholder-shown)]:left-1.5 peer-[:not(:placeholder-shown)]:bg-presentation peer-[:not(:placeholder-shown)]:text-xs sm:top-1.5">
|
||||
{comboboxLabel}
|
||||
</label>
|
||||
)}
|
||||
|
|
@ -168,7 +168,7 @@ export const CustomMenuItem = React.forwardRef<HTMLDivElement, CustomMenuItemPro
|
|||
blurOnHoverEnd: false,
|
||||
...props,
|
||||
className: cn(
|
||||
'relative flex cursor-default items-center gap-2 rounded-lg p-2 outline-none! scroll-m-1 scroll-mt-[calc(var(--combobox-height,0px)+var(--label-height,4px))] aria-disabled:opacity-25 data-[active-item]:bg-black/[0.075] data-[active-item]:text-black dark:data-[active-item]:bg-white/10 dark:data-[active-item]:text-white sm:py-1 sm:text-sm min-w-0 w-full before:absolute before:left-0 before:top-1 before:bottom-1 before:w-0.5 before:bg-transparent before:rounded-full data-[active-item]:before:bg-black dark:data-[active-item]:before:bg-white',
|
||||
'relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1 outline-none! scroll-m-1 scroll-mt-[calc(var(--combobox-height,0px)+var(--label-height,4px))] aria-disabled:opacity-25 data-[active-item]:bg-black/[0.075] data-[active-item]:text-black dark:data-[active-item]:bg-white/10 dark:data-[active-item]:text-white sm:text-sm min-w-0 w-full before:absolute before:left-0 before:top-1 before:bottom-1 before:w-0.5 before:bg-transparent before:rounded-full data-[active-item]:before:bg-black dark:data-[active-item]:before:bg-white',
|
||||
props.className,
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ function ModelSelectorContent() {
|
|||
description={localize('com_ui_select_model')}
|
||||
render={
|
||||
<button
|
||||
className="my-1 flex h-10 w-full max-w-[70vw] items-center justify-center gap-2 rounded-xl border border-border-light bg-surface-secondary px-3 py-2 text-sm text-text-primary hover:bg-surface-tertiary"
|
||||
className="my-1 flex h-10 w-full max-w-[70vw] items-center justify-center gap-2 rounded-xl border border-border-light bg-presentation px-3 py-2 text-sm text-text-primary hover:bg-surface-active-alt"
|
||||
aria-label={localize('com_ui_select_model')}
|
||||
>
|
||||
{selectedIcon && React.isValidElement(selectedIcon) && (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useMemo } from 'react';
|
||||
import { SettingsIcon } from 'lucide-react';
|
||||
import { Spinner } from '@librechat/client';
|
||||
import { Spinner, TooltipAnchor } from '@librechat/client';
|
||||
import { CheckCircle2, MousePointerClick, SettingsIcon } from 'lucide-react';
|
||||
import { EModelEndpoint, isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||
import type { TModelSpec } from 'librechat-data-provider';
|
||||
import type { Endpoint } from '~/common';
|
||||
|
|
@ -28,29 +28,53 @@ const SettingsButton = ({
|
|||
}) => {
|
||||
const localize = useLocalize();
|
||||
const text = localize('com_endpoint_config_key');
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!endpoint.value) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
handleOpenKeyDialog(endpoint.value as EModelEndpoint, e);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (endpoint.value) {
|
||||
handleOpenKeyDialog(endpoint.value as EModelEndpoint, e as unknown as React.MouseEvent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
id={`endpoint-${endpoint.value}-settings`}
|
||||
onClick={(e) => {
|
||||
if (!endpoint.value) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
handleOpenKeyDialog(endpoint.value as EModelEndpoint, e);
|
||||
}}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn(
|
||||
'flex items-center overflow-visible text-text-primary transition-all duration-300 ease-in-out',
|
||||
'group/button rounded-md px-1 hover:bg-surface-secondary focus:bg-surface-secondary',
|
||||
'group/button flex items-center gap-1.5 rounded-md px-1.5',
|
||||
'text-text-secondary transition-colors duration-150',
|
||||
'hover:bg-surface-tertiary hover:text-text-primary',
|
||||
'focus-visible:bg-surface-tertiary focus-visible:text-text-primary',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
|
||||
className,
|
||||
)}
|
||||
aria-label={`${text} ${endpoint.label}`}
|
||||
>
|
||||
<div className="flex w-[28px] items-center gap-1 whitespace-nowrap transition-all duration-300 ease-in-out group-hover:w-auto group-focus/button:w-auto">
|
||||
<SettingsIcon className="h-4 w-4 flex-shrink-0" aria-hidden="true" />
|
||||
<span className="max-w-0 overflow-hidden whitespace-nowrap opacity-0 transition-all duration-300 ease-in-out group-hover:max-w-[100px] group-hover:opacity-100 group-focus/button:max-w-[100px] group-focus/button:opacity-100">
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
<SettingsIcon className="size-4 shrink-0" aria-hidden="true" />
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
'grid overflow-hidden transition-[grid-template-columns,opacity] duration-150 ease-out',
|
||||
'grid-cols-[0fr] opacity-0',
|
||||
'group-hover/button:grid-cols-[1fr] group-hover/button:opacity-100',
|
||||
'group-focus-visible/button:grid-cols-[1fr] group-focus-visible/button:opacity-100',
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 truncate pr-0.5">{text}</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
@ -88,21 +112,17 @@ export function EndpointItem({ endpoint, endpointIndex }: EndpointItemProps) {
|
|||
[endpointRequiresUserKey, endpoint.value],
|
||||
);
|
||||
|
||||
const isAssistantsNotLoaded =
|
||||
isAssistantsEndpoint(endpoint.value) && endpoint.models === undefined;
|
||||
|
||||
const renderIconLabel = () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{endpoint.icon && (
|
||||
<div className="flex flex-shrink-0 items-center justify-center overflow-hidden">
|
||||
<div className="flex shrink-0 items-center justify-center" aria-hidden="true">
|
||||
{endpoint.icon}
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
'truncate text-left',
|
||||
isUserProvided ? 'group-hover:w-24 group-focus:w-24' : '',
|
||||
)}
|
||||
>
|
||||
{endpoint.label}
|
||||
</span>
|
||||
<span className="truncate text-left">{endpoint.label}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
@ -124,17 +144,14 @@ export function EndpointItem({ endpoint, endpointIndex }: EndpointItemProps) {
|
|||
<Menu
|
||||
id={`endpoint-${endpoint.value}-menu`}
|
||||
key={`endpoint-${endpoint.value}-item`}
|
||||
className="transition-opacity duration-200 ease-in-out"
|
||||
defaultOpen={endpoint.value === selectedEndpoint}
|
||||
searchValue={searchValue}
|
||||
onSearch={(value) => setEndpointSearchValue(endpoint.value, value)}
|
||||
combobox={<input placeholder=" " />}
|
||||
comboboxLabel={placeholder}
|
||||
onClick={() => handleSelectEndpoint(endpoint)}
|
||||
label={
|
||||
<div
|
||||
onClick={() => handleSelectEndpoint(endpoint)}
|
||||
className="group flex w-full flex-shrink cursor-pointer items-center justify-between rounded-xl px-1 py-1 text-sm"
|
||||
>
|
||||
<div className="group flex w-full min-w-0 items-center justify-between gap-1.5 py-1 text-sm">
|
||||
{renderIconLabel()}
|
||||
{isUserProvided && (
|
||||
<SettingsButton endpoint={endpoint} handleOpenKeyDialog={handleOpenKeyDialog} />
|
||||
|
|
@ -143,8 +160,12 @@ export function EndpointItem({ endpoint, endpointIndex }: EndpointItemProps) {
|
|||
}
|
||||
>
|
||||
{isAssistantsEndpoint(endpoint.value) && endpoint.models === undefined ? (
|
||||
<div className="flex items-center justify-center p-2">
|
||||
<Spinner />
|
||||
<div
|
||||
className="flex items-center justify-center p-2"
|
||||
role="status"
|
||||
aria-label={localize('com_ui_loading')}
|
||||
>
|
||||
<Spinner aria-hidden="true" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -179,32 +200,27 @@ export function EndpointItem({ endpoint, endpointIndex }: EndpointItemProps) {
|
|||
id={`endpoint-${endpoint.value}-menu`}
|
||||
key={`endpoint-${endpoint.value}-item`}
|
||||
onClick={() => handleSelectEndpoint(endpoint)}
|
||||
className="flex h-8 w-full cursor-pointer items-center justify-between rounded-xl px-3 py-2 text-sm"
|
||||
className="group flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm"
|
||||
>
|
||||
<div className="group flex w-full min-w-0 items-center justify-between">
|
||||
{renderIconLabel()}
|
||||
<div className="flex items-center gap-2">
|
||||
{endpointRequiresUserKey(endpoint.value) && (
|
||||
<SettingsButton endpoint={endpoint} handleOpenKeyDialog={handleOpenKeyDialog} />
|
||||
)}
|
||||
{selectedEndpoint === endpoint.value && (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="block"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12ZM16.0755 7.93219C16.5272 8.25003 16.6356 8.87383 16.3178 9.32549L11.5678 16.0755C11.3931 16.3237 11.1152 16.4792 10.8123 16.4981C10.5093 16.517 10.2142 16.3973 10.0101 16.1727L7.51006 13.4227C7.13855 13.014 7.16867 12.3816 7.57733 12.0101C7.98598 11.6386 8.61843 11.6687 8.98994 12.0773L10.6504 13.9039L14.6822 8.17451C15 7.72284 15.6238 7.61436 16.0755 7.93219Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
{renderIconLabel()}
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{endpointRequiresUserKey(endpoint.value) && (
|
||||
<SettingsButton endpoint={endpoint} handleOpenKeyDialog={handleOpenKeyDialog} />
|
||||
)}
|
||||
{isAssistantsNotLoaded && (
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_click_to_view_var', { 0: endpoint.label })}
|
||||
side="top"
|
||||
render={
|
||||
<span className="flex items-center">
|
||||
<MousePointerClick className="size-4 text-text-secondary" aria-hidden="true" />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{selectedEndpoint === endpoint.value && !isAssistantsNotLoaded && (
|
||||
<CheckCircle2 className="size-4 shrink-0 text-text-primary" aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</MenuItem>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function HeaderNewChat() {
|
|||
variant="outline"
|
||||
data-testid="wide-header-new-chat-button"
|
||||
aria-label={localize('com_ui_new_chat')}
|
||||
className="rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover max-md:hidden"
|
||||
className="rounded-xl duration-0 hover:bg-surface-active-alt max-md:hidden"
|
||||
onClick={clickHandler}
|
||||
>
|
||||
<NewChatIcon />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { startTransition } from 'react';
|
||||
import { TooltipAnchor, Button, Sidebar } from '@librechat/client';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
|
@ -17,9 +18,13 @@ export default function OpenSidebar({
|
|||
const localize = useLocalize();
|
||||
|
||||
const handleClick = () => {
|
||||
setNavVisible((prev) => {
|
||||
localStorage.setItem('navVisible', JSON.stringify(!prev));
|
||||
return !prev;
|
||||
// Use startTransition to mark this as a non-urgent update
|
||||
// This prevents blocking the main thread during the cascade of re-renders
|
||||
startTransition(() => {
|
||||
setNavVisible((prev) => {
|
||||
localStorage.setItem('navVisible', JSON.stringify(!prev));
|
||||
return !prev;
|
||||
});
|
||||
});
|
||||
// Delay focus until after the sidebar animation completes (200ms)
|
||||
setTimeout(() => {
|
||||
|
|
@ -39,10 +44,7 @@ export default function OpenSidebar({
|
|||
aria-label={localize('com_nav_open_sidebar')}
|
||||
aria-expanded={false}
|
||||
aria-controls="chat-history-nav"
|
||||
className={cn(
|
||||
'rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover',
|
||||
className,
|
||||
)}
|
||||
className={cn('rounded-xl duration-0 hover:bg-surface-active-alt', className)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Sidebar aria-hidden="true" />
|
||||
|
|
|
|||
|
|
@ -49,16 +49,22 @@ const PresetsMenu: FC = () => {
|
|||
<Trigger asChild>
|
||||
<TooltipAnchor
|
||||
ref={presetsMenuTriggerRef}
|
||||
id="presets-button"
|
||||
aria-label={localize('com_endpoint_examples')}
|
||||
description={localize('com_endpoint_examples')}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
data-testid="presets-button"
|
||||
className="inline-flex size-10 flex-shrink-0 items-center justify-center rounded-xl border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
|
||||
>
|
||||
<BookCopy size={16} aria-hidden="true" />
|
||||
</TooltipAnchor>
|
||||
render={
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
tabIndex={0}
|
||||
id="presets-button"
|
||||
data-testid="presets-button"
|
||||
aria-label={localize('com_endpoint_examples')}
|
||||
className="rounded-xl p-2 duration-0 hover:bg-surface-active-alt max-md:hidden"
|
||||
// className="inline-flex size-10 flex-shrink-0 items-center justify-center rounded-xl border border-border-light bg-transparent text-text-primary transition-all ease-in-out hover:bg-surface-tertiary disabled:pointer-events-none disabled:opacity-50 radix-state-open:bg-surface-tertiary"
|
||||
>
|
||||
<BookCopy className="icon-lg" aria-hidden="true" />
|
||||
</Button>
|
||||
}
|
||||
></TooltipAnchor>
|
||||
</Trigger>
|
||||
<Portal>
|
||||
<div
|
||||
|
|
@ -74,7 +80,7 @@ const PresetsMenu: FC = () => {
|
|||
<Content
|
||||
side="bottom"
|
||||
align="center"
|
||||
className="mt-2 max-h-[495px] overflow-x-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-gray-700 dark:bg-gray-700 dark:text-white md:min-w-[400px]"
|
||||
className="mt-2 max-h-[495px] overflow-x-hidden rounded-lg border border-border-light bg-surface-secondary text-text-primary shadow-lg md:min-w-[400px]"
|
||||
>
|
||||
<PresetItems
|
||||
presets={presetsQuery.data}
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ function FeedbackButtons({
|
|||
|
||||
function buttonClasses(isActive: boolean, isLast: boolean) {
|
||||
return cn(
|
||||
'hover-button rounded-lg p-1.5 text-text-secondary-alt transition-colors duration-200',
|
||||
'hover-button rounded-lg p-1.5 text-text-secondary-alt',
|
||||
'hover:text-text-primary hover:bg-surface-hover',
|
||||
'md:group-hover:visible md:group-focus-within:visible md:group-[.final-completion]:visible',
|
||||
!isLast && 'md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100',
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ export default function Fork({
|
|||
});
|
||||
|
||||
const buttonStyle = cn(
|
||||
'hover-button rounded-lg p-1.5 text-text-secondary-alt transition-colors duration-200',
|
||||
'hover-button rounded-lg p-1.5 text-text-secondary-alt',
|
||||
'hover:text-text-primary hover:bg-surface-hover',
|
||||
'md:group-hover:visible md:group-focus-within:visible md:group-[.final-completion]:visible',
|
||||
!isLast && 'md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100',
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ const HoverButton = memo(
|
|||
className = '',
|
||||
}: HoverButtonProps) => {
|
||||
const buttonStyle = cn(
|
||||
'hover-button rounded-lg p-1.5 text-text-secondary-alt transition-colors duration-200',
|
||||
'hover-button rounded-lg p-1.5 text-text-secondary-alt',
|
||||
'hover:text-text-primary hover:bg-surface-hover',
|
||||
'md:group-hover:visible md:group-focus-within:visible md:group-[.final-completion]:visible',
|
||||
!isLast && 'md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100',
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export default function SiblingSwitch({
|
|||
};
|
||||
|
||||
const buttonStyle = cn(
|
||||
'hover-button rounded-lg p-1.5 text-text-secondary-alt transition-colors duration-200',
|
||||
'hover-button rounded-lg p-1.5 text-text-secondary-alt',
|
||||
'hover:text-text-primary hover:bg-surface-hover',
|
||||
'md:group-hover:visible md:group-focus-within:visible md:group-[.final-completion]:visible',
|
||||
'focus-visible:ring-2 focus-visible:ring-black dark:focus-visible:ring-white focus-visible:outline-none',
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ export function TemporaryChat() {
|
|||
|
||||
const temporaryBadge = {
|
||||
id: 'temporary',
|
||||
icon: MessageCircleDashed,
|
||||
label: 'com_ui_temporary' as const,
|
||||
atom: store.isTemporary,
|
||||
isAvailable: true,
|
||||
};
|
||||
|
|
@ -37,26 +35,20 @@ export function TemporaryChat() {
|
|||
return (
|
||||
<div className="relative flex flex-wrap items-center gap-2">
|
||||
<TooltipAnchor
|
||||
description={localize(temporaryBadge.label)}
|
||||
description={localize('com_ui_temporary')}
|
||||
render={
|
||||
<button
|
||||
onClick={handleBadgeToggle}
|
||||
aria-label={localize(temporaryBadge.label)}
|
||||
aria-label={localize('com_ui_temporary')}
|
||||
aria-pressed={isTemporary}
|
||||
className={cn(
|
||||
'inline-flex size-10 flex-shrink-0 items-center justify-center rounded-xl border border-border-light text-text-primary transition-all ease-in-out hover:bg-surface-tertiary',
|
||||
'inline-flex size-10 flex-shrink-0 items-center justify-center rounded-xl border border-border-light text-text-primary transition-all ease-in-out',
|
||||
isTemporary
|
||||
? 'bg-surface-active shadow-md'
|
||||
: 'bg-transparent shadow-sm hover:bg-surface-hover hover:shadow-md',
|
||||
'active:shadow-inner',
|
||||
? 'bg-surface-active'
|
||||
: 'bg-presentation shadow-sm hover:bg-surface-active-alt',
|
||||
)}
|
||||
>
|
||||
{temporaryBadge.icon && (
|
||||
<temporaryBadge.icon
|
||||
className={cn('relative h-5 w-5 md:h-4 md:w-4', !temporaryBadge.label && 'mx-auto')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<MessageCircleDashed className="icon-lg" aria-hidden="true" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue