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

View file

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

View file

@ -1,3 +1,4 @@
import { ChevronLeft, ChevronRight } from 'lucide-react';
import type { TMessageProps } from '~/common';
import { cn } from '~/utils';
@ -22,57 +23,46 @@ export default function SiblingSwitch({
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 ? (
<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
className={cn(
'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',
)}
className={buttonStyle}
type="button"
onClick={previous}
disabled={siblingIdx == 0}
aria-label="Previous sibling message"
aria-disabled={siblingIdx == 0}
>
<svg
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>
<ChevronLeft size="19" aria-hidden="true" />
</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}
</span>
<button
className={cn(
'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',
)}
className={buttonStyle}
type="button"
onClick={next}
disabled={siblingIdx == siblingCount - 1}
aria-label="Next sibling message"
aria-disabled={siblingIdx == siblingCount - 1}
>
<svg
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>
<ChevronRight size="19" aria-hidden="true" />
</button>
</div>
</nav>
) : null;
}

View file

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

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import {
OGDialogDescription,
} from './OriginalDialog';
import { useLocalize } from '~/hooks';
import { Button } from './Button';
import { Spinner } from '../svg';
import { cn } from '~/utils/';
@ -53,7 +54,6 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
showCancelButton = true,
} = props;
const { selectHandler, selectClasses, selectText, isLoading } = selection || {};
const Cancel = localize('com_ui_cancel');
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';
@ -83,12 +83,12 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
) : null}
</div>
<div className="flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:flex-row">
{buttons != null ? buttons : null}
{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">
{Cancel}
<OGDialogClose asChild>
<Button variant="outline">{localize('com_ui_cancel')}</Button>
</OGDialogClose>
)}
{buttons != null ? buttons : null}
{selection ? (
<OGDialogClose
onClick={selectHandler}

View file

@ -798,6 +798,7 @@
"com_ui_usage": "Usage",
"com_ui_current": "Current",
"com_ui_tokens": "tokens",
"com_ui_token": "token",
"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_misc": "Misc.",
@ -1006,4 +1007,3 @@
"com_ui_memory_created": "Memory created successfully",
"com_ui_memory_key_exists": "A memory with this key already exists. Please use a different key."
}