🎨 refactor: Enhance UI Consistency, Accessibility & Localization (#7788)

This commit is contained in:
Marco Beretta 2025-06-08 20:00:57 +02:00 committed by GitHub
parent 9bb9aba8ec
commit b0054c775a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 104 additions and 127 deletions

View file

@ -1,11 +1,10 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import type { FC } from 'react'; import type { FC } from 'react';
import { Label, OGDialog, OGDialogTrigger, TooltipAnchor } from '~/components/ui'; import { Button, TrashIcon, Label, OGDialog, OGDialogTrigger, TooltipAnchor } from '~/components';
import { useDeleteConversationTagMutation } from '~/data-provider'; import { useDeleteConversationTagMutation } from '~/data-provider';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { NotificationSeverity } from '~/common'; import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers'; import { useToastContext } from '~/Providers';
import { TrashIcon } from '~/components/svg';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
const DeleteBookmarkButton: FC<{ const DeleteBookmarkButton: FC<{
@ -36,31 +35,26 @@ const DeleteBookmarkButton: FC<{
await deleteBookmarkMutation.mutateAsync(bookmark); await deleteBookmarkMutation.mutateAsync(bookmark);
}, [bookmark, deleteBookmarkMutation]); }, [bookmark, deleteBookmarkMutation]);
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
setOpen(!open);
}
};
return ( return (
<> <>
<OGDialog open={open} onOpenChange={setOpen}> <OGDialog open={open} onOpenChange={setOpen}>
<OGDialogTrigger asChild> <OGDialogTrigger asChild>
<TooltipAnchor <TooltipAnchor
role="button"
aria-label={localize('com_ui_bookmarks_delete')}
description={localize('com_ui_delete')} description={localize('com_ui_delete')}
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" render={
tabIndex={tabIndex} <Button
onFocus={onFocus} variant="ghost"
onBlur={onBlur} aria-label={localize('com_ui_bookmarks_delete')}
onClick={() => setOpen(!open)} tabIndex={tabIndex}
onKeyDown={handleKeyDown} onFocus={onFocus}
> onBlur={onBlur}
<TrashIcon className="size-4" /> onClick={() => setOpen(!open)}
</TooltipAnchor> className="h-8 w-8 p-0"
>
<TrashIcon />
</Button>
}
/>
</OGDialogTrigger> </OGDialogTrigger>
<OGDialogTemplate <OGDialogTemplate
showCloseButton={false} showCloseButton={false}

View file

@ -1,9 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import type { FC } from 'react'; import type { FC } from 'react';
import type { TConversationTag } from 'librechat-data-provider'; import type { TConversationTag } from 'librechat-data-provider';
import { TooltipAnchor, OGDialogTrigger } from '~/components/ui'; import { TooltipAnchor, OGDialogTrigger, EditIcon, Button } from '~/components';
import BookmarkEditDialog from './BookmarkEditDialog'; import BookmarkEditDialog from './BookmarkEditDialog';
import { EditIcon } from '~/components/svg';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
const EditBookmarkButton: FC<{ const EditBookmarkButton: FC<{
@ -15,12 +14,6 @@ const EditBookmarkButton: FC<{
const localize = useLocalize(); const localize = useLocalize();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
setOpen(!open);
}
};
return ( return (
<BookmarkEditDialog <BookmarkEditDialog
context="EditBookmarkButton" context="EditBookmarkButton"
@ -30,18 +23,21 @@ const EditBookmarkButton: FC<{
> >
<OGDialogTrigger asChild> <OGDialogTrigger asChild>
<TooltipAnchor <TooltipAnchor
role="button"
aria-label={localize('com_ui_bookmarks_edit')}
description={localize('com_ui_edit')} description={localize('com_ui_edit')}
tabIndex={tabIndex} render={
onFocus={onFocus} <Button
onBlur={onBlur} variant="ghost"
onClick={() => setOpen(!open)} aria-label={localize('com_ui_bookmarks_edit')}
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" tabIndex={tabIndex}
onKeyDown={handleKeyDown} onFocus={onFocus}
> onBlur={onBlur}
<EditIcon /> onClick={() => setOpen(!open)}
</TooltipAnchor> className="h-8 w-8 p-0"
>
<EditIcon />
</Button>
}
/>
</OGDialogTrigger> </OGDialogTrigger>
</BookmarkEditDialog> </BookmarkEditDialog>
); );

View file

@ -1,3 +1,4 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';
import type { TMessageProps } from '~/common'; import type { TMessageProps } from '~/common';
import { cn } from '~/utils'; import { cn } from '~/utils';
@ -22,57 +23,46 @@ export default function SiblingSwitch({
setSiblingIdx && setSiblingIdx(siblingIdx + 1); setSiblingIdx && setSiblingIdx(siblingIdx + 1);
}; };
const buttonStyle = cn(
'hover-button rounded-lg p-1.5 text-text-secondary-alt transition-colors duration-200',
'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',
);
return siblingCount > 1 ? ( return siblingCount > 1 ? (
<div className="visible flex items-center justify-center gap-1 self-center pt-0 text-xs"> <nav
className="visible flex items-center justify-center gap-2 self-center pt-0 text-xs"
aria-label="Sibling message navigation"
>
<button <button
className={cn( className={buttonStyle}
'hover-button rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
)}
type="button" type="button"
onClick={previous} onClick={previous}
disabled={siblingIdx == 0} disabled={siblingIdx == 0}
aria-label="Previous sibling message"
aria-disabled={siblingIdx == 0}
> >
<svg <ChevronLeft size="19" aria-hidden="true" />
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button> </button>
<span className="flex-shrink-0 flex-grow tabular-nums"> <span
className="flex-shrink-0 flex-grow tabular-nums"
aria-live="polite"
aria-atomic="true"
role="status"
>
{siblingIdx + 1} / {siblingCount} {siblingIdx + 1} / {siblingCount}
</span> </span>
<button <button
className={cn( className={buttonStyle}
'hover-button rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-400/70 dark:hover:bg-gray-700 dark:hover:text-gray-200 disabled:dark:hover:text-gray-400 md:group-hover:visible md:group-[.final-completion]:visible',
)}
type="button" type="button"
onClick={next} onClick={next}
disabled={siblingIdx == siblingCount - 1} disabled={siblingIdx == siblingCount - 1}
aria-label="Next sibling message"
aria-disabled={siblingIdx == siblingCount - 1}
> >
<svg <ChevronRight size="19" aria-hidden="true" />
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="9 18 15 12 9 6" />
</svg>
</button> </button>
</div> </nav>
) : null; ) : null;
} }

View file

@ -30,6 +30,12 @@ const BookmarkTableRow: React.FC<BookmarkTableRowProps> = ({ row, moveRow, posit
mutation.mutate( mutation.mutate(
{ ...row, position: item.index }, { ...row, position: item.index },
{ {
onSuccess: () => {
showToast({
message: localize('com_ui_bookmarks_update_success'),
severity: NotificationSeverity.SUCCESS,
});
},
onError: () => { onError: () => {
showToast({ showToast({
message: localize('com_ui_bookmarks_update_error'), message: localize('com_ui_bookmarks_update_error'),
@ -44,7 +50,9 @@ const BookmarkTableRow: React.FC<BookmarkTableRowProps> = ({ row, moveRow, posit
accept: 'bookmark', accept: 'bookmark',
drop: handleDrop, drop: handleDrop,
hover(item: DragItem) { hover(item: DragItem) {
if (!ref.current || item.index === position) {return;} if (!ref.current || item.index === position) {
return;
}
moveRow(item.index, position); moveRow(item.index, position);
item.index = position; item.index = position;
}, },

View file

@ -117,7 +117,7 @@ export default function MemoryEditDialog({
<div> <div>
{memory.tokenCount.toLocaleString()} {memory.tokenCount.toLocaleString()}
{memData?.tokenLimit && ` / ${memData.tokenLimit.toLocaleString()}`}{' '} {memData?.tokenLimit && ` / ${memData.tokenLimit.toLocaleString()}`}{' '}
{localize('com_ui_tokens')} {localize(memory.tokenCount === 1 ? 'com_ui_token' : 'com_ui_tokens')}
</div> </div>
)} )}
</div> </div>

View file

@ -5,6 +5,9 @@ import { matchSorter } from 'match-sorter';
import { SystemRoles, PermissionTypes, Permissions } from 'librechat-data-provider'; import { SystemRoles, PermissionTypes, Permissions } from 'librechat-data-provider';
import type { TUserMemory } from 'librechat-data-provider'; import type { TUserMemory } from 'librechat-data-provider';
import { import {
Spinner,
EditIcon,
TrashIcon,
Table, Table,
Input, Input,
Label, Label,
@ -18,7 +21,7 @@ import {
TableHeader, TableHeader,
TooltipAnchor, TooltipAnchor,
OGDialogTrigger, OGDialogTrigger,
} from '~/components/ui'; } from '~/components';
import { import {
useGetUserQuery, useGetUserQuery,
useMemoriesQuery, useMemoriesQuery,
@ -27,10 +30,8 @@ import {
} from '~/data-provider'; } from '~/data-provider';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks'; import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { EditIcon, TrashIcon } from '~/components/svg';
import MemoryCreateDialog from './MemoryCreateDialog'; import MemoryCreateDialog from './MemoryCreateDialog';
import MemoryEditDialog from './MemoryEditDialog'; import MemoryEditDialog from './MemoryEditDialog';
import Spinner from '~/components/svg/Spinner';
import { useToastContext } from '~/Providers'; import { useToastContext } from '~/Providers';
import AdminSettings from './AdminSettings'; import AdminSettings from './AdminSettings';
@ -121,13 +122,6 @@ export default function MemoryViewer() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLDivElement>(null); const triggerRef = useRef<HTMLDivElement>(null);
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
setOpen(!open);
}
};
// Only show edit button if user has UPDATE permission // Only show edit button if user has UPDATE permission
if (!hasUpdateAccess) { if (!hasUpdateAccess) {
return null; return null;
@ -142,17 +136,18 @@ export default function MemoryViewer() {
> >
<OGDialogTrigger asChild> <OGDialogTrigger asChild>
<TooltipAnchor <TooltipAnchor
ref={triggerRef} description={localize('com_ui_edit_memory')}
role="button" render={
aria-label={localize('com_ui_edit')} <Button
description={localize('com_ui_edit')} variant="ghost"
tabIndex={0} aria-label={localize('com_ui_bookmarks_edit')}
onClick={() => setOpen(!open)} onClick={() => setOpen(!open)}
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" className="h-8 w-8 p-0"
onKeyDown={handleKeyDown} >
> <EditIcon />
<EditIcon /> </Button>
</TooltipAnchor> }
/>
</OGDialogTrigger> </OGDialogTrigger>
</MemoryEditDialog> </MemoryEditDialog>
); );
@ -161,14 +156,6 @@ export default function MemoryViewer() {
const DeleteMemoryButton = ({ memory }: { memory: TUserMemory }) => { const DeleteMemoryButton = ({ memory }: { memory: TUserMemory }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
setOpen(!open);
}
};
if (!hasUpdateAccess) { if (!hasUpdateAccess) {
return null; return null;
} }
@ -196,20 +183,22 @@ export default function MemoryViewer() {
<OGDialog open={open} onOpenChange={setOpen}> <OGDialog open={open} onOpenChange={setOpen}>
<OGDialogTrigger asChild> <OGDialogTrigger asChild>
<TooltipAnchor <TooltipAnchor
role="button" description={localize('com_ui_delete_memory')}
aria-label={localize('com_ui_delete')} render={
description={localize('com_ui_delete')} <Button
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" variant="ghost"
tabIndex={0} aria-label={localize('com_ui_delete')}
onClick={() => setOpen(!open)} onClick={() => setOpen(!open)}
onKeyDown={handleKeyDown} className="h-8 w-8 p-0"
> >
{deletingKey === memory.key ? ( {deletingKey === memory.key ? (
<Spinner className="size-4 animate-spin" /> <Spinner className="size-4 animate-spin" />
) : ( ) : (
<TrashIcon className="size-4" /> <TrashIcon className="size-4" />
)} )}
</TooltipAnchor> </Button>
}
/>
</OGDialogTrigger> </OGDialogTrigger>
<OGDialogTemplate <OGDialogTemplate
showCloseButton={false} showCloseButton={false}

View file

@ -8,6 +8,7 @@ import {
OGDialogDescription, OGDialogDescription,
} from './OriginalDialog'; } from './OriginalDialog';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
import { Button } from './Button';
import { Spinner } from '../svg'; import { Spinner } from '../svg';
import { cn } from '~/utils/'; import { cn } from '~/utils/';
@ -53,7 +54,6 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
showCancelButton = true, showCancelButton = true,
} = props; } = props;
const { selectHandler, selectClasses, selectText, isLoading } = selection || {}; const { selectHandler, selectClasses, selectText, isLoading } = selection || {};
const Cancel = localize('com_ui_cancel');
const defaultSelect = const defaultSelect =
'bg-gray-800 text-white transition-colors hover:bg-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-gray-200'; 'bg-gray-800 text-white transition-colors hover:bg-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-gray-200';
@ -83,12 +83,12 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
) : null} ) : null}
</div> </div>
<div className="flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:flex-row"> <div className="flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:flex-row">
{buttons != null ? buttons : null}
{showCancelButton && ( {showCancelButton && (
<OGDialogClose className="btn btn-neutral border-token-border-light relative justify-center rounded-lg text-sm ring-offset-2 focus:ring-2 focus:ring-black dark:ring-offset-0 max-sm:order-last max-sm:w-full sm:order-first"> <OGDialogClose asChild>
{Cancel} <Button variant="outline">{localize('com_ui_cancel')}</Button>
</OGDialogClose> </OGDialogClose>
)} )}
{buttons != null ? buttons : null}
{selection ? ( {selection ? (
<OGDialogClose <OGDialogClose
onClick={selectHandler} onClick={selectHandler}

View file

@ -798,6 +798,7 @@
"com_ui_usage": "Usage", "com_ui_usage": "Usage",
"com_ui_current": "Current", "com_ui_current": "Current",
"com_ui_tokens": "tokens", "com_ui_tokens": "tokens",
"com_ui_token": "token",
"com_ui_mention": "Mention an endpoint, assistant, or preset to quickly switch to it", "com_ui_mention": "Mention an endpoint, assistant, or preset to quickly switch to it",
"com_ui_min_tags": "Cannot remove more values, a minimum of {{0}} are required.", "com_ui_min_tags": "Cannot remove more values, a minimum of {{0}} are required.",
"com_ui_misc": "Misc.", "com_ui_misc": "Misc.",
@ -1006,4 +1007,3 @@
"com_ui_memory_created": "Memory created successfully", "com_ui_memory_created": "Memory created successfully",
"com_ui_memory_key_exists": "A memory with this key already exists. Please use a different key." "com_ui_memory_key_exists": "A memory with this key already exists. Please use a different key."
} }