refactor(client): migrate buttons, inputs and labels to @librechat/client

Swap raw <button>, <input> and <label> elements for the @librechat/client
Button, Input and Label components across Auth, Chat, Conversations,
Endpoints, Nav, Prompts, Skills, Tools and Web. Preserve bespoke geometry
and behavior via cn className merging, keep data-testid/aria wiring, and
localize previously hardcoded aria-labels. Skip swaps that would break
floating-label animations, tiny bespoke controls or inline-text links.
Add com_ui_reload_page key.
This commit is contained in:
Marco Beretta 2026-06-24 18:11:10 +02:00
parent a1b22b745d
commit 91c40d1907
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
38 changed files with 324 additions and 230 deletions

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Search, X } from 'lucide-react';
import { Input } from '@librechat/client';
import { Button, Input } from '@librechat/client';
import { useDebounce, useLocalize } from '~/hooks';
/**
@ -89,7 +89,9 @@ const SearchBar: React.FC<SearchBarProps> = ({ value, onSearch, className = '' }
</div>
{/* Show clear button only when search has value - Google style */}
{searchTerm && (
<button
<Button
variant="ghost"
size="icon"
type="button"
onClick={handleClear}
className="group absolute right-4 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-text-primary focus:ring-offset-2"
@ -101,7 +103,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ value, onSearch, className = '' }
strokeWidth={2.5}
aria-hidden="true"
/>
</button>
</Button>
)}
</div>
);

View file

@ -1,7 +1,7 @@
import React, { useState, useEffect, useContext } from 'react';
import { useForm } from 'react-hook-form';
import { Turnstile } from '@marsidev/react-turnstile';
import { ThemeContext, SecretInput, Spinner, Button, isDark } from '@librechat/client';
import { ThemeContext, SecretInput, Spinner, Button, Input, isDark } from '@librechat/client';
import type { TLoginUser, TStartupConfig } from 'librechat-data-provider';
import type { TAuthContext } from '~/common';
import { useResendVerificationEmail, useGetStartupConfig } from '~/data-provider';
@ -96,7 +96,7 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
>
<div className="mb-4">
<div className="relative">
<input
<Input
type="text"
id="email"
autoComplete={useUsernameLogin ? 'username' : 'email'}

View file

@ -1,10 +1,10 @@
import { useForm } from 'react-hook-form';
import React, { useContext, useState } from 'react';
import { useForm } from 'react-hook-form';
import { loginPage } from 'librechat-data-provider';
import { Turnstile } from '@marsidev/react-turnstile';
import { ThemeContext, SecretInput, Spinner, Button, isDark } from '@librechat/client';
import { useNavigate, useOutletContext, useLocation } from 'react-router-dom';
import { useRegisterUserMutation } from 'librechat-data-provider/react-query';
import { loginPage } from 'librechat-data-provider';
import { ThemeContext, SecretInput, Spinner, Button, Input, isDark } from '@librechat/client';
import type { TRegisterUser, TError } from 'librechat-data-provider';
import type { TLoginLayoutContext } from '~/common';
import { useLocalize, TranslationKeys } from '~/hooks';
@ -98,7 +98,7 @@ const Registration: React.FC = () => {
/>
) : (
<>
<input
<Input
id={id}
type={type}
autoComplete={id}

View file

@ -1,12 +1,12 @@
import { useForm } from 'react-hook-form';
import { useState, ReactNode } from 'react';
import { Spinner, Button, Alert } from '@librechat/client';
import { useForm } from 'react-hook-form';
import { useOutletContext } from 'react-router-dom';
import { useRequestPasswordResetMutation } from 'librechat-data-provider/react-query';
import { loginPage } from 'librechat-data-provider';
import { Spinner, Button, Alert, Input } from '@librechat/client';
import { useRequestPasswordResetMutation } from 'librechat-data-provider/react-query';
import type { TRequestPasswordReset, TRequestPasswordResetResponse } from 'librechat-data-provider';
import type { TLoginLayoutContext } from '~/common';
import type { FC } from 'react';
import type { TLoginLayoutContext } from '~/common';
import { useLocalize } from '~/hooks';
const BodyTextWrapper: FC<{ children: ReactNode }> = ({ children }) => {
@ -84,7 +84,7 @@ function RequestPasswordReset() {
>
<div className="space-y-2">
<div className="relative">
<input
<Input
type="email"
id="email"
autoComplete="off"

View file

@ -3,7 +3,14 @@ import { useSearchParams } from 'react-router-dom';
import { useToastContext } from '@librechat/client';
import { useForm, Controller } from 'react-hook-form';
import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp';
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Label } from '@librechat/client';
import {
Label,
Button,
InputOTP,
InputOTPSlot,
InputOTPGroup,
InputOTPSeparator,
} from '@librechat/client';
import { useVerifyTwoFactorTempMutation } from '~/data-provider';
import { useLocalize } from '~/hooks';
@ -141,33 +148,36 @@ const TwoFactorScreen: React.FC = React.memo(() => {
</div>
)}
<div className="flex items-center justify-between">
<button
<Button
type="submit"
variant="submit"
aria-label={localize('com_auth_continue')}
data-testid="login-button"
disabled={isLoading}
className="w-full rounded-2xl bg-surface-submit px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-surface-submit-hover disabled:opacity-80"
className="w-full rounded-2xl px-4 py-3 text-sm font-medium disabled:opacity-80"
>
{isLoading ? localize('com_auth_email_verifying_ellipsis') : localize('com_ui_verify')}
</button>
</Button>
</div>
<div className="mt-4 flex justify-center">
{!useBackup ? (
<button
<Button
type="button"
variant="link"
onClick={toggleBackupOn}
className="inline-flex p-1 text-sm font-medium text-accent-primary transition-colors hover:text-accent-primary-hover"
>
{localize('com_ui_use_backup_code')}
</button>
</Button>
) : (
<button
<Button
type="button"
variant="link"
onClick={toggleBackupOff}
className="inline-flex p-1 text-sm font-medium text-accent-primary transition-colors hover:text-accent-primary-hover"
>
{localize('com_ui_use_2fa_code')}
</button>
</Button>
)}
</div>
</form>

View file

@ -1,6 +1,6 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { Spinner, ThemeSelector } from '@librechat/client';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { Button, Spinner, ThemeSelector } from '@librechat/client';
import { useVerifyEmailMutation, useResendVerificationEmail } from '~/data-provider';
import { useLocalize } from '~/hooks';
@ -87,13 +87,15 @@ function RequestPasswordReset() {
{showResendLink && countdown === 0 && (
<p className="text-center text-lg text-text-secondary">
{localize('com_auth_email_verification_resend_prompt')}
<button
className="ml-2 text-link hover:underline"
<Button
type="button"
variant="link"
className="ml-2 inline h-auto p-0 text-link"
onClick={handleResendEmail}
disabled={resendEmailMutation.isLoading}
>
{localize('com_auth_email_resend_link')}
</button>
</Button>
</p>
)}
</div>

View file

@ -1,7 +1,8 @@
import React, { useState } from 'react';
import { FileUp } from 'lucide-react';
import { cn } from '~/utils/';
import { Button } from '@librechat/client';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils/';
type FileUploadProps = {
onFileSelected: (jsonData: Record<string, unknown>) => void;
@ -71,8 +72,10 @@ const FileUpload: React.FC<FileUploadProps> = ({
return (
<>
<button
<Button
type="button"
variant="ghost"
size="sm"
onClick={handleClick}
className={cn(
'mr-1 flex h-auto cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-normal text-text-secondary transition-colors hover:bg-surface-hover hover:text-status-success focus:ring-text-primary',
@ -83,7 +86,7 @@ const FileUpload: React.FC<FileUploadProps> = ({
>
<FileUp className="mr-1 flex w-[22px] items-center stroke-1" aria-hidden="true" />
<span className="flex text-xs">{statusText}</span>
</button>
</Button>
<input
id={`file-upload-${id}`}
value=""

View file

@ -5,6 +5,7 @@ import { FileSources } from 'librechat-data-provider';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import ProgressCircle from './ProgressCircle';
import SourceIcon from './SourceIcon';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
type styleProps = {
@ -29,6 +30,7 @@ const ImagePreview = ({
source?: FileSources;
alt?: string;
}) => {
const localize = useLocalize();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
@ -179,8 +181,9 @@ const ImagePreview = ({
ref={closeButtonRef}
onClick={() => handleOpenChange(false)}
variant="ghost"
className="absolute right-4 top-4 z-20 h-10 w-10 p-0 text-white hover:bg-white/10"
aria-label="Close"
size="icon"
className="absolute right-4 top-4 z-20 text-white hover:bg-white/10"
aria-label={localize('com_ui_close')}
>
<X className="size-5" aria-hidden="true" />
</Button>

View file

@ -133,11 +133,7 @@ export default function MCPConfigDialog({
}}
buttons={
onRevoke && (
<Button
onClick={handleRevoke}
className="bg-surface-destructive text-white hover:bg-surface-destructive-hover"
disabled={isSubmitting}
>
<Button variant="destructive" onClick={handleRevoke} disabled={isSubmitting}>
{localize('com_ui_revoke')}
</Button>
)

View file

@ -1,7 +1,7 @@
import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react';
import { AutoSizer, List } from 'react-virtualized';
import { Spinner, useCombobox } from '@librechat/client';
import { useSetRecoilState, useRecoilValue } from 'recoil';
import { Input, Spinner, useCombobox } from '@librechat/client';
import type { TPromptGroup } from 'librechat-data-provider';
import type { PromptOption } from '~/common';
import useInitPopoverInput from '~/hooks/Input/useInitPopoverInput';
@ -206,10 +206,10 @@ function PromptsCommand({
>
<div className="absolute bottom-28 z-10 w-full space-y-2">
<div className="popover border-token-border-light rounded-2xl border bg-surface-tertiary-alt p-2 shadow-lg">
<input
<Input
ref={initInputRef}
placeholder={localize('com_ui_command_usage_placeholder')}
className="mb-1 w-full border-0 bg-surface-tertiary-alt p-2 text-sm text-text-primary focus:outline-none"
className="mb-1 h-auto w-full rounded-none border-0 bg-surface-tertiary-alt p-2 text-sm text-text-primary focus:outline-none"
autoComplete="off"
value={searchValue}
onKeyDown={(e) => {

View file

@ -1,8 +1,8 @@
import { memo, useState, useRef, useEffect, useMemo, useCallback } from 'react';
import { ScrollText } from 'lucide-react';
import { AutoSizer, List } from 'react-virtualized';
import { Spinner, useCombobox } from '@librechat/client';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Input, Spinner, useCombobox } from '@librechat/client';
import type { TSkillSummary } from 'librechat-data-provider';
import type { MentionOption } from '~/common';
import useInitPopoverInput from '~/hooks/Input/useInitPopoverInput';
@ -281,10 +281,10 @@ function SkillsCommandContent({
return (
<div className="absolute bottom-28 z-10 w-full space-y-2">
<div className="popover border-token-border-light rounded-2xl border bg-surface-tertiary-alt p-2 shadow-lg">
<input
<Input
ref={initInputRef}
placeholder={localize('com_ui_skills_command_placeholder')}
className="mb-1 w-full border-0 bg-surface-tertiary-alt p-2 text-sm text-text-primary focus:outline-none"
className="mb-1 h-auto w-full rounded-none border-0 bg-surface-tertiary-alt p-2 text-sm text-text-primary focus:outline-none"
autoComplete="off"
value={searchValue}
onKeyDown={(e) => {

View file

@ -1,10 +1,11 @@
import { useRecoilState } from 'recoil';
import { useCallback, useEffect, useMemo } from 'react';
import { useRecoilState } from 'recoil';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, isAgentsEndpoint } from 'librechat-data-provider';
import {
Input,
Label,
Button,
OGDialog,
OGDialogTitle,
SelectDropDown,
@ -197,18 +198,12 @@ const EditPresetDialog = ({
{/* Action buttons */}
<div className="flex justify-end gap-2 border-t border-border-medium pt-2 md:pt-4">
<button
onClick={exportPreset}
className="rounded-md border border-border-medium bg-surface-secondary px-3 py-2 text-sm font-medium text-text-primary hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-ring-primary focus:ring-offset-2 md:px-4"
>
<Button variant="outline" onClick={exportPreset}>
{localize('com_endpoint_export')}
</button>
<button
onClick={submitPreset}
className="rounded-md bg-surface-submit px-3 py-2 text-sm font-medium text-white hover:bg-surface-submit-hover focus:outline-none focus:ring-2 focus:ring-ring-primary focus:ring-offset-2 md:px-4"
>
</Button>
<Button variant="submit" onClick={submitPreset}>
{localize('com_ui_save')}
</button>
</Button>
</div>
</div>
</OGDialogContent>

View file

@ -1,5 +1,6 @@
import { useState, useMemo, useCallback } from 'react';
import copy from 'copy-to-clipboard';
import { Button } from '@librechat/client';
import CopyButton from '~/components/Messages/Content/CopyButton';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -141,22 +142,24 @@ export default function OutputRenderer({ text }: OutputRendererProps) {
/>
</div>
{needsTruncation && (
<button
type="button"
className="mt-1 text-xs text-text-secondary underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-heavy"
<Button
variant="link"
size="sm"
className="mt-1 h-auto p-0 text-xs text-text-secondary underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-heavy"
onClick={() => setIsExpanded((prev) => !prev)}
>
{isExpanded ? localize('com_ui_show_less') : localize('com_ui_show_more')}
</button>
</Button>
)}
{error && rawError && rawError !== displayText && (
<button
type="button"
className="mt-1 block text-xs text-text-secondary underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-heavy"
<Button
variant="link"
size="sm"
className="mt-1 block h-auto p-0 text-xs text-text-secondary underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-heavy"
onClick={() => setShowErrorDetails((prev) => !prev)}
>
{localize('com_ui_details')}
</button>
</Button>
)}
{showErrorDetails && rawError && (
<pre className="mt-2 max-h-[200px] overflow-auto whitespace-pre-wrap break-words font-mono text-xs text-status-error">

View file

@ -1,6 +1,14 @@
import React, { useState, useMemo, memo } from 'react';
import { useRecoilState } from 'recoil';
import { EditIcon, Clipboard, CheckMark, ContinueIcon, RegenerateIcon } from '@librechat/client';
import {
Button,
EditIcon,
CheckMark,
Clipboard,
ContinueIcon,
TooltipAnchor,
RegenerateIcon,
} from '@librechat/client';
import type { TConversation, TMessage, TFeedback } from 'librechat-data-provider';
import { useGenerationsByLatest, useLocalize } from '~/hooks';
import { Fork } from '~/components/Conversations';
@ -84,7 +92,7 @@ const HoverButton = memo(
dataTestId,
}: HoverButtonProps) => {
const buttonStyle = cn(
'hover-button rounded-lg p-1.5 text-text-secondary-alt',
'hover-button size-auto rounded-lg p-1.5 text-text-secondary-alt',
'hover:text-text-primary hover:bg-surface-hover',
'group-hover:visible group-focus-within:visible group-[.final-completion]:visible',
!isLast &&
@ -96,17 +104,23 @@ const HoverButton = memo(
);
return (
<button
id={id}
data-testid={dataTestId}
className={buttonStyle}
onClick={onClick}
type="button"
title={title}
disabled={isDisabled}
>
{icon}
</button>
<TooltipAnchor
description={title}
render={
<Button
variant="ghost"
size="icon"
id={id}
data-testid={dataTestId}
aria-label={title}
className={buttonStyle}
onClick={onClick}
disabled={isDisabled}
>
{icon}
</Button>
}
/>
);
},
);

View file

@ -1,5 +1,5 @@
import { useState } from 'react';
import { Clipboard, CheckMark } from '@librechat/client';
import { Button, Clipboard, CheckMark, TooltipAnchor } from '@librechat/client';
import type { TMessage, SearchResultData } from 'librechat-data-provider';
import { useLocalize, useCopyToClipboard } from '~/hooks';
@ -19,20 +19,30 @@ export default function MinimalHoverButtons({ message, searchResults }: THoverBu
return (
<div className="visible mt-1 flex justify-center gap-1 self-end text-text-tertiary lg:justify-start">
<button
className="ml-0 flex items-center gap-1.5 rounded-lg p-1.5 text-xs text-text-secondary-alt transition-colors duration-200 hover:bg-surface-hover hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary group-focus-within:opacity-100 group-hover:opacity-100 [@media(hover:hover)]:opacity-0"
onClick={() => copyToClipboard(setIsCopied)}
type="button"
title={
<TooltipAnchor
description={
isCopied ? localize('com_ui_copied_to_clipboard') : localize('com_ui_copy_to_clipboard')
}
>
{isCopied ? (
<CheckMark className="h-[19px] w-[19px]" />
) : (
<Clipboard className="h-[19px] w-[19px]" />
)}
</button>
render={
<Button
variant="ghost"
size="icon"
aria-label={
isCopied
? localize('com_ui_copied_to_clipboard')
: localize('com_ui_copy_to_clipboard')
}
className="ml-0 flex size-auto items-center gap-1.5 rounded-lg p-1.5 text-xs text-text-secondary-alt transition-colors duration-200 hover:bg-surface-hover hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary group-focus-within:opacity-100 group-hover:opacity-100 [@media(hover:hover)]:opacity-0"
onClick={() => copyToClipboard(setIsCopied)}
>
{isCopied ? (
<CheckMark className="h-[19px] w-[19px]" />
) : (
<Clipboard className="h-[19px] w-[19px]" />
)}
</Button>
}
/>
</div>
);
}

View file

@ -1,5 +1,7 @@
import { Button } from '@librechat/client';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import type { TMessageProps } from '~/common';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
type TSiblingSwitchProps = Pick<TMessageProps, 'siblingIdx' | 'siblingCount' | 'setSiblingIdx'>;
@ -9,6 +11,8 @@ export default function SiblingSwitch({
siblingCount,
setSiblingIdx,
}: TSiblingSwitchProps) {
const localize = useLocalize();
if (siblingIdx === undefined) {
return null;
} else if (siblingCount === undefined) {
@ -24,7 +28,7 @@ export default function SiblingSwitch({
};
const buttonStyle = cn(
'hover-button rounded-lg p-1.5 text-text-secondary-alt',
'hover-button h-auto rounded-lg p-1.5 text-text-secondary-alt',
'hover:text-text-primary hover:bg-surface-hover',
'group-hover:visible group-focus-within:visible group-[.final-completion]:visible',
'focus-visible:ring-2 focus-visible:ring-text-primary focus-visible:outline-none',
@ -33,18 +37,18 @@ export default function SiblingSwitch({
return siblingCount > 1 ? (
<nav
className="visible flex items-center justify-center gap-2 self-center pt-0 text-xs"
aria-label="Sibling message navigation"
aria-label={localize('com_ui_sibling_navigation')}
>
<button
<Button
variant="ghost"
className={buttonStyle}
type="button"
onClick={previous}
disabled={siblingIdx == 0}
aria-label="Previous sibling message"
aria-label={localize('com_ui_previous_sibling')}
aria-disabled={siblingIdx == 0}
>
<ChevronLeft size="19" aria-hidden="true" />
</button>
</Button>
<span
className="flex-shrink-0 flex-grow tabular-nums"
aria-live="polite"
@ -53,16 +57,16 @@ export default function SiblingSwitch({
>
{siblingIdx + 1} / {siblingCount}
</span>
<button
<Button
variant="ghost"
className={buttonStyle}
type="button"
onClick={next}
disabled={siblingIdx == siblingCount - 1}
aria-label="Next sibling message"
aria-label={localize('com_ui_next_sibling')}
aria-disabled={siblingIdx == siblingCount - 1}
>
<ChevronRight size="19" aria-hidden="true" />
</button>
</Button>
</nav>
) : null;
}

View file

@ -3,7 +3,7 @@ import { Pin } from 'lucide-react';
import { useRecoilValue } from 'recoil';
import { useParams } from 'react-router-dom';
import { Constants } from 'librechat-data-provider';
import { useToastContext, useMediaQuery } from '@librechat/client';
import { Spinner, useToastContext, useMediaQuery } from '@librechat/client';
import type { TConversation } from 'librechat-data-provider';
import { useNavigateToConvo, useLocalize, useShiftKey } from '~/hooks';
import ConversationEndpointIcon from './ConversationEndpointIcon';
@ -179,19 +179,9 @@ function Conversation({
};
const generatingSpinner = (
<svg
className="h-5 w-5 flex-shrink-0 animate-spin text-text-primary"
viewBox="0 0 24 24"
fill="none"
aria-label={localize('com_ui_generating')}
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<span role="img" aria-label={localize('com_ui_generating')}>
<Spinner className="h-5 w-5 flex-shrink-0 text-text-primary" />
</span>
);
let actionVisibilityClassName =

View file

@ -1,4 +1,6 @@
import React from 'react';
import { Button } from '@librechat/client';
import { useLocalize } from '~/hooks';
type TPagesProps = {
pages: number;
@ -15,6 +17,7 @@ export default function Pages({
previousPage,
setPageNumber,
}: TPagesProps) {
const localize = useLocalize();
const clickHandler =
(func: () => Promise<void>) => async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
@ -27,29 +30,35 @@ export default function Pages({
return pageNumber == 1 && pages == 1 ? null : (
<div className="m-auto mb-2 mt-4 flex items-center justify-center gap-2">
<button
<Button
variant="ghost"
size="sm"
aria-label={localize('com_ui_prev')}
onClick={clickHandler(previousPage)}
className={
'btn btn-small bg-transition m-auto flex gap-2 text-text-primary transition hover:bg-surface-hover disabled:text-text-tertiary' +
'm-auto flex gap-2 text-text-primary disabled:text-text-tertiary' +
(pageNumber <= 1 ? ' hidden-visibility' : '')
}
disabled={pageNumber <= 1}
>
&lt;
</button>
</Button>
<span className="flex-none text-text-tertiary">
{pageNumber} / {pages}
</span>
<button
<Button
variant="ghost"
size="sm"
aria-label={localize('com_ui_next')}
onClick={clickHandler(nextPage)}
className={
'btn btn-small bg-transition m-auto flex gap-2 text-text-primary transition hover:bg-surface-hover disabled:text-text-tertiary' +
'm-auto flex gap-2 text-text-primary disabled:text-text-tertiary' +
(pageNumber >= pages ? ' hidden-visibility' : '')
}
disabled={pageNumber >= pages}
>
&gt;
</button>
</Button>
</div>
);
}

View file

@ -247,13 +247,15 @@ const ProjectChatsInline = memo(function ProjectChatsInline({
/>
))}
{hasMore && (
<button
<Button
type="button"
variant="ghost"
size="sm"
onClick={onShowAll}
className="ml-1 mt-0.5 rounded-md px-2 py-1 text-xs font-medium text-text-secondary outline-none transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-text-primary"
className="ml-1 mt-0.5 h-auto rounded-md px-2 py-1 text-xs font-medium text-text-secondary transition-colors hover:bg-transparent hover:text-text-primary"
>
{localize('com_ui_show_all')}
</button>
</Button>
)}
</div>
);
@ -453,14 +455,15 @@ const ProjectsSection = ({ toggleNav, isAuthenticated }: ProjectsSectionProps) =
if (projects.length === 0) {
return (
<button
<Button
type="button"
variant="ghost"
onClick={() => setIsCreateOpen(true)}
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-sm text-text-secondary outline-none transition-colors hover:bg-surface-hover hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-text-primary"
className="flex h-auto w-full justify-start gap-2 rounded-lg px-2 py-1.5 text-sm font-normal text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-primary"
>
<FolderPlus className="h-4 w-4 shrink-0" aria-hidden="true" />
<span className="truncate">{localize('com_ui_new_project')}</span>
</button>
</Button>
);
}
@ -476,13 +479,14 @@ const ProjectsSection = ({ toggleNav, isAuthenticated }: ProjectsSectionProps) =
))}
{hasMore && (
<li className="list-none">
<button
<Button
type="button"
variant="ghost"
onClick={openProjects}
className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-xs font-medium text-text-secondary outline-none transition-colors hover:bg-surface-hover hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-text-primary"
className="flex h-auto w-full justify-start gap-2 rounded-lg px-2 py-1.5 text-xs font-medium text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-primary"
>
{localize('com_ui_all_projects')}
</button>
</Button>
</li>
)}
</ul>

View file

@ -1,6 +1,13 @@
import React, { useEffect, useState } from 'react';
import { useCreatePresetMutation } from 'librechat-data-provider/react-query';
import { OGDialogTemplate, OGDialog, Input, Label, useToastContext } from '@librechat/client';
import {
OGDialogTemplate,
OGDialog,
Button,
Input,
Label,
useToastContext,
} from '@librechat/client';
import type { TEditPresetProps } from '~/common';
import { cn, removeFocusOutlines, cleanupPreset, defaultTextProps } from '~/utils';
import { NotificationSeverity } from '~/common';
@ -81,11 +88,11 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }: TEditPresetProps) =>
</div>
</div>
}
selection={{
selectHandler: submitPreset,
selectClasses: 'bg-surface-submit hover:bg-surface-submit-hover text-white',
selectText: localize('com_ui_save'),
}}
buttons={
<Button variant="submit" onClick={submitPreset}>
{localize('com_ui_save')}
</Button>
}
/>
</OGDialog>
);

View file

@ -3,6 +3,7 @@ import { useMultiSearch } from '@librechat/client';
import { Root, Trigger, Content, Portal } from '@radix-ui/react-popover';
import type { TPlugin } from 'librechat-data-provider';
import MenuItem from '~/components/Chat/Menus/UI/MenuItem';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils/';
type SelectDropDownProps = {
@ -32,6 +33,7 @@ function MultiSelectPop({
optionValueKey = 'value',
searchPlaceholder,
}: SelectDropDownProps) {
const localize = useLocalize();
const title = _title;
const excludeIds = ['select-plugin', 'plugins-label', 'selected-plugins'];
@ -71,7 +73,7 @@ function MultiSelectPop({
)} */}
<span className="flex items-center gap-1">
<div className="flex gap-1">
{value.length === 0 && 'None selected'}
{value.length === 0 && localize('com_ui_none_selected')}
{value.map((v, i) => (
<div key={i} className="relative">
{v.icon ? (

View file

@ -72,7 +72,7 @@ function SelectDropDownPop({
'hover:bg-surface-hover radix-state-open:bg-surface-hover',
'min-w-[200px] max-w-[215px] sm:min-w-full sm:max-w-full',
)}
aria-label={`Select ${title}`}
aria-label={localize('com_ui_select_var', { 0: title })}
aria-haspopup="false"
>
{' '}

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState, useRef, useCallback, memo } from 'react';
import { RefreshCw } from 'lucide-react';
import { Spinner } from '@librechat/client';
import { Button, Spinner } from '@librechat/client';
import useSvgProcessing from './useSvgProcessing';
import useMermaidZoom from './useMermaidZoom';
import MermaidDialog from './MermaidDialog';
@ -178,14 +178,15 @@ const Mermaid: React.FC<MermaidProps> = memo(({ children, id, theme }) => {
<span className="font-semibold text-text-destructive">
{localize('com_ui_mermaid_failed')}
</span>
<button
type="button"
<Button
variant="ghost"
size="sm"
onClick={handleRetry}
className="flex items-center gap-1 rounded px-2 py-1 text-xs text-text-secondary hover:bg-surface-hover"
className="h-auto gap-1 rounded px-2 py-1 text-xs text-text-secondary hover:bg-surface-hover"
>
<RefreshCw className="h-3 w-3" />
{localize('com_ui_retry')}
</button>
</Button>
</div>
<pre className="overflow-auto text-xs text-text-destructive">{error.message}</pre>
{showCode && (

View file

@ -1,7 +1,7 @@
import { useState } from 'react';
import * as Tabs from '@radix-ui/react-tabs';
import { X, ChevronLeft } from 'lucide-react';
import { useMediaQuery } from '@librechat/client';
import { Button, useMediaQuery } from '@librechat/client';
import { SettingsTabValues } from 'librechat-data-provider';
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
import type { TDialogProps } from '~/common';
@ -88,14 +88,15 @@ export default function SettingsDialog({ open, onOpenChange }: TDialogProps) {
{localize('com_nav_settings')}
</h2>
)}
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={() => onOpenChange(false)}
className="rounded-lg p-1 text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-border-xheavy"
aria-label={localize('com_ui_close_settings')}
className="h-auto w-auto rounded-lg p-1 text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-border-xheavy"
>
<X className="h-5 w-5" aria-hidden="true" />
<span className="sr-only">{localize('com_ui_close_settings')}</span>
</button>
</Button>
</DialogTitle>
<Tabs.Root
value={effectiveTab}

View file

@ -3,12 +3,6 @@ import { RefreshCcw } from 'lucide-react';
import { useSetRecoilState } from 'recoil';
import { motion, AnimatePresence } from 'framer-motion';
import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp';
import type {
TRegenerateBackupCodesResponse,
TRegenerateBackupCodesRequest,
TBackupCode,
TUser,
} from 'librechat-data-provider';
import {
InputOTPSeparator,
InputOTPGroup,
@ -24,6 +18,12 @@ import {
TooltipAnchor,
useToastContext,
} from '@librechat/client';
import type {
TRegenerateBackupCodesResponse,
TRegenerateBackupCodesRequest,
TBackupCode,
TUser,
} from 'librechat-data-provider';
import { useRegenerateBackupCodesMutation } from '~/data-provider';
import { useAuthContext, useLocalize } from '~/hooks';
import store from '~/store';
@ -250,16 +250,17 @@ const BackupCodesItem: React.FC = () => {
)}
</InputOTP>
</div>
<button
<Button
type="button"
variant="link"
onClick={() => {
setUseBackup(!useBackup);
setOtpToken('');
}}
className="text-sm text-text-primary hover:underline"
className="h-auto p-0 text-sm text-text-primary hover:underline"
>
{useBackup ? localize('com_ui_use_2fa_code') : localize('com_ui_use_backup_code')}
</button>
</Button>
</div>
)}
</motion.div>

View file

@ -77,12 +77,14 @@ export const DisablePhase: React.FC<DisablePhaseProps> = ({ onDisable, isDisabli
{isDisabling && <Spinner className="mr-2" />}
{isDisabling ? localize('com_ui_disabling') : localize('com_ui_2fa_disable')}
</Button>
<button
<Button
type="button"
variant="link"
onClick={() => setUseBackup(!useBackup)}
className="text-sm text-text-primary hover:underline"
className="h-auto p-0 text-sm text-text-primary hover:underline"
>
{useBackup ? localize('com_ui_use_2fa_code') : localize('com_ui_use_backup_code')}
</button>
</Button>
</motion.div>
);
};

View file

@ -258,7 +258,6 @@ export default function ArchivedChatsTable({
isArchived: false,
})
}
title={localize('com_ui_unarchive_conversation')}
aria-label={localize('com_ui_unarchive_conversation')}
disabled={isUnarchiving}
>
@ -276,7 +275,6 @@ export default function ArchivedChatsTable({
setDeleteConversation(row.original);
setIsDeleteOpen(true);
}}
title={localize('com_ui_delete_conversation_tooltip')}
aria-label={localize('com_ui_delete_conversation_tooltip')}
>
<TrashIcon className="size-4" />
@ -333,7 +331,11 @@ export default function ArchivedChatsTable({
</OGDialogTitle>
</OGDialogHeader>
<div className="flex justify-end gap-4 pt-4">
<Button aria-label="cancel" variant="outline" onClick={() => setIsDeleteOpen(false)}>
<Button
aria-label={localize('com_ui_cancel')}
variant="outline"
onClick={() => setIsDeleteOpen(false)}
>
{localize('com_ui_cancel')}
</Button>
<Button

View file

@ -1,4 +1,5 @@
import React from 'react';
import { Button } from '@librechat/client';
import { useSearchParams } from 'react-router-dom';
import { useLocalize } from '~/hooks';
@ -59,13 +60,13 @@ export default function OAuthError() {
{localize('com_ui_oauth_error_title') || 'Authentication Failed'}
</h1>
<p className="mb-6 text-sm text-text-secondary">{getErrorMessage(error)}</p>
<button
<Button
variant="default"
onClick={() => window.close()}
className="rounded-md bg-surface-inverted px-4 py-2 text-sm font-medium text-text-inverted hover:bg-surface-inverted-hover focus:outline-none focus:ring-2 focus:ring-ring-primary focus:ring-offset-2"
aria-label={localize('com_ui_close_window') || 'Close Window'}
aria-label={localize('com_ui_close_window')}
>
{localize('com_ui_close_window') || 'Close Window'}
</button>
{localize('com_ui_close_window')}
</Button>
</div>
</div>
);

View file

@ -1,5 +1,6 @@
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { Pencil, Check, Loader2, X } from 'lucide-react';
import { Input, TooltipAnchor } from '@librechat/client';
import { useLocalize } from '~/hooks';
type Props = {
@ -98,10 +99,31 @@ const PromptName: React.FC<Props> = ({ name, isLoading = false, isError = false,
};
}, []);
const displayButton = (
<button
type="button"
onClick={() => {
if (!isLoading && saveStatus !== 'saving') {
setIsEditing(true);
}
}}
className="h-8 min-w-0 flex-1 cursor-text truncate pl-2 text-left text-base font-semibold text-text-primary transition-colors hover:text-text-secondary focus:outline-none"
aria-label={localize('com_ui_edit') + ': ' + (newName ?? '')}
>
{newName}
</button>
);
const readView = newName ? (
<TooltipAnchor description={newName} render={displayButton} />
) : (
displayButton
);
return (
<div className="group/title relative mr-2 flex h-8 min-w-0 flex-1 items-center">
{isEditing ? (
<input
<Input
ref={inputRef}
type="text"
value={newName ?? ''}
@ -113,19 +135,7 @@ const PromptName: React.FC<Props> = ({ name, isLoading = false, isError = false,
aria-label={localize('com_ui_name')}
/>
) : (
<button
type="button"
onClick={() => {
if (!isLoading && saveStatus !== 'saving') {
setIsEditing(true);
}
}}
className="h-8 min-w-0 flex-1 cursor-text truncate pl-2 text-left text-base font-semibold text-text-primary transition-colors hover:text-text-secondary focus:outline-none"
title={newName}
aria-label={localize('com_ui_edit') + ': ' + (newName ?? '')}
>
{newName}
</button>
readView
)}
<div className="ml-1.5 flex shrink-0 items-center justify-center">
{saveStatus === 'saving' && (

View file

@ -214,6 +214,7 @@ const CreatePromptForm = ({
<Command onValueChange={(value) => methods.setValue('command', value)} tabIndex={0} />
<div className="mt-4 flex justify-end">
<Button
variant="submit"
aria-label={localize('com_ui_create_prompt')}
className={cn(
'w-full sm:w-auto',

View file

@ -1,6 +1,7 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import keyBy from 'lodash/keyBy';
import { RotateCcw } from 'lucide-react';
import { Button } from '@librechat/client';
import {
excludedKeys,
paramSettings,
@ -181,23 +182,25 @@ export default function Parameters() {
})}
</div>
<div className="mt-4 flex justify-center">
<button
<Button
variant="outline"
type="button"
onClick={resetParameters}
className="btn btn-neutral flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
className="flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</Button>
</div>
<div className="mt-2 flex justify-center">
<button
<Button
variant="submit"
onClick={openDialog}
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-surface-submit-hover focus:border-surface-submit"
className="flex w-full items-center justify-center px-4 py-2 font-semibold"
type="button"
>
{localize('com_endpoint_save_as_preset')}
</button>
</Button>
</div>
{preset && (
<SaveAsPresetDialog open={isDialogOpen} onOpenChange={setIsDialogOpen} preset={preset} />

View file

@ -1,17 +1,19 @@
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import {
SKILL_NAME_PATTERN,
SKILL_NAME_MAX_LENGTH,
SKILL_DESCRIPTION_MAX_LENGTH,
} from 'librechat-data-provider';
import {
Input,
Label,
Button,
OGDialog,
OGDialogContent,
TextareaAutosize,
useToastContext,
} from '@librechat/client';
import {
SKILL_NAME_PATTERN,
SKILL_NAME_MAX_LENGTH,
SKILL_DESCRIPTION_MAX_LENGTH,
} from 'librechat-data-provider';
import type { TSkill } from 'librechat-data-provider';
import type { FormEvent } from 'react';
import { useCreateSkillMutation } from '~/data-provider';
@ -124,10 +126,10 @@ export default function CreateSkillDialog({
{/* Skill name */}
<div className="flex flex-col gap-1.5">
<label htmlFor="create-skill-name" className="text-sm font-medium text-text-secondary">
<Label htmlFor="create-skill-name" className="text-sm font-medium text-text-secondary">
{localize('com_ui_name')}
</label>
<input
</Label>
<Input
id="create-skill-name"
placeholder={localize('com_ui_skill_name_placeholder')}
aria-invalid={errors.name ? 'true' : 'false'}

View file

@ -1,6 +1,6 @@
import React, { useState, useMemo } from 'react';
import { format } from 'date-fns';
import { TooltipAnchor } from '@librechat/client';
import { Button, TooltipAnchor } from '@librechat/client';
import { Eye, Code, User, Calendar, EarthIcon, ScrollText } from 'lucide-react';
import type { TSkill } from 'librechat-data-provider';
import { useLocalize, useAuthContext, useSkillPermissions, useSkillActiveState } from '~/hooks';
@ -139,13 +139,14 @@ export default function SkillDetail({ skill, onEdit, onDelete }: SkillDetailProp
/>
<ShareSkill skill={skill} />
{permissions.canEdit && onEdit && (
<button
type="button"
<Button
variant="outline"
size="sm"
onClick={onEdit}
className="inline-flex h-9 items-center justify-center rounded-md border border-border-medium px-3 text-xs font-semibold text-text-primary transition-colors hover:bg-surface-hover"
className="h-9 rounded-md border-border-medium px-3 text-xs font-semibold"
>
{localize('com_ui_edit')}
</button>
</Button>
)}
{permissions.canDelete && onDelete && (
<DeleteSkill skillId={skill._id} skillName={skill.name} onDelete={onDelete} />

View file

@ -1,3 +1,4 @@
import { Button } from '@librechat/client';
import { XCircle, PlusCircleIcon, Wrench } from 'lucide-react';
import type { TPlugin, AgentToolType } from 'librechat-data-provider';
import { useLocalize } from '~/hooks';
@ -51,8 +52,9 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled = false }: ToolIt
{name}
</div>
{!isInstalled ? (
<button
className="btn btn-primary relative"
<Button
variant="submit"
className="relative"
aria-label={`${localize('com_ui_add')} ${name}`}
onClick={handleClick}
>
@ -60,10 +62,11 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled = false }: ToolIt
{localize('com_ui_add')}
<PlusCircleIcon className="flex h-4 w-4 items-center stroke-2" aria-hidden="true" />
</div>
</button>
</Button>
) : (
<button
className="btn relative bg-surface-tertiary hover:bg-surface-hover"
<Button
variant="outline"
className="relative"
onClick={handleClick}
aria-label={`${localize('com_nav_tool_remove')} ${name}`}
>
@ -71,7 +74,7 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled = false }: ToolIt
{localize('com_nav_tool_remove')}
<XCircle className="flex h-4 w-4 items-center stroke-2" aria-hidden="true" />
</div>
</button>
</Button>
)}
</div>
</div>

View file

@ -1,7 +1,8 @@
import React, { ReactNode } from 'react';
import * as Ariakit from '@ariakit/react';
import { ChevronDown, FileText } from 'lucide-react';
import { Button } from '@librechat/client';
import { VisuallyHidden } from '@ariakit/react';
import { ChevronDown, FileText } from 'lucide-react';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -70,12 +71,13 @@ function FileHovercardContent({
<>
<div className="flex items-center gap-2">
<FileText className="size-4 shrink-0 text-text-secondary" aria-hidden="true" />
<button
<Button
variant="link"
onClick={onClick}
className="min-w-0 truncate text-sm font-medium text-text-primary hover:underline"
className="h-auto min-w-0 justify-start truncate p-0 text-sm font-medium text-text-primary hover:underline"
>
{fileName}
</button>
</Button>
</div>
{(fileRelevance != null || (filePages && filePages.length > 0)) && (
<div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1">

View file

@ -5,6 +5,7 @@ import { VisuallyHidden } from '@ariakit/react';
import { Tools } from 'librechat-data-provider';
import { X, Globe, Newspaper, Image, ChevronDown, File, Download } from 'lucide-react';
import {
Button,
OGDialog,
AnimatedTabs,
OGDialogClose,
@ -752,13 +753,15 @@ export default function Sources(props: SourcesProps) {
<div className="mb-2 text-sm text-text-secondary">
{localize('com_sources_error_fallback')}
</div>
<button
<Button
variant="outline"
size="sm"
onClick={() => window.location.reload()}
className="hover:bg-surface-primary-hover rounded-md bg-surface-primary px-3 py-1 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-text-primary"
className="hover:bg-surface-primary-hover rounded-md bg-surface-primary px-3 py-1 text-sm text-text-primary"
aria-label={localize('com_sources_reload_page')}
>
{localize('com_ui_refresh')}
</button>
</Button>
</div>
);

View file

@ -1,4 +1,6 @@
import React, { Component, ReactNode } from 'react';
import { Button } from '@librechat/client';
import { useLocalize } from '~/hooks';
interface Props {
children: ReactNode;
@ -11,6 +13,28 @@ interface State {
hasError: boolean;
}
function DefaultFallback() {
const localize = useLocalize();
return (
<div
className="flex flex-col items-center justify-center rounded-lg border border-border-medium bg-surface-secondary p-4 text-center"
role="alert"
aria-live="polite"
>
{/* eslint-disable-next-line i18next/no-literal-string */}
<div className="mb-2 text-sm text-text-secondary">Sources temporarily unavailable</div>
<Button
variant="outline"
size="sm"
onClick={() => window.location.reload()}
aria-label={localize('com_ui_reload_page')}
>
{localize('com_ui_refresh')}
</Button>
</div>
);
}
class SourcesErrorBoundary extends Component<Props, State> {
state = { hasError: false };
@ -30,25 +54,7 @@ class SourcesErrorBoundary extends Component<Props, State> {
return this.props.fallback;
}
// Default simple error UI (using localized strings from Sources.tsx fallback)
/* eslint-disable i18next/no-literal-string */
return (
<div
className="flex flex-col items-center justify-center rounded-lg border border-border-medium bg-surface-secondary p-4 text-center"
role="alert"
aria-live="polite"
>
<div className="mb-2 text-sm text-text-secondary">Sources temporarily unavailable</div>
<button
onClick={() => window.location.reload()}
className="hover:bg-surface-primary-hover rounded-md bg-surface-primary px-3 py-1 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-text-primary"
aria-label="Reload the page"
>
Refresh
</button>
</div>
);
/* eslint-enable i18next/no-literal-string */
return <DefaultFallback />;
}
return this.props.children;

View file

@ -1616,6 +1616,7 @@
"com_ui_reject": "Reject",
"com_ui_reject_reason_placeholder": "Optional: reason for rejecting (shared with the agent)",
"com_ui_relevance": "Relevance",
"com_ui_reload_page": "Reload the page",
"com_ui_remote_access": "Remote Access",
"com_ui_remote_agent_role_editor": "Editor",
"com_ui_remote_agent_role_editor_desc": "Can view and modify the agent via API",