refactor(client): migrate Agent Builder and Tool Library to @librechat/client

Swap raw buttons, inputs, labels, textareas and native title tooltips for
the @librechat/client Button/Input/Label/Textarea/TooltipAnchor components
across the Agent Builder panel (SidePanel/Agents) and the Tools
marketplace. Remove heavy input focus rings in favor of subtle borders,
soften dropdown trigger borders, and convert stray shadcn/raw colors in
touched lines to semantic tokens. Localize the tool delete aria-label and
toast messages. Update co-located test mocks to provide the newly-used
Button component.
This commit is contained in:
Marco Beretta 2026-07-06 17:39:26 +02:00
parent 8a6047ac34
commit 9a68fe6da0
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
38 changed files with 283 additions and 226 deletions

View file

@ -8,10 +8,12 @@ import {
AuthTypeEnum,
} from 'librechat-data-provider';
import {
Label,
Button,
Spinner,
Textarea,
OGDialog,
TooltipAnchor,
OGDialogTitle,
OGDialogHeader,
OGDialogContent,
@ -243,15 +245,20 @@ export default function ActionsInput({
>
{localize('com_ui_schema')}
</label>
<button
type="button"
onClick={() => setIsSchemaDialogOpen(true)}
aria-label={localize('com_ui_expand_editor')}
title={localize('com_ui_expand_editor')}
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-text-secondary transition-colors hover:bg-surface-secondary hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
>
<Maximize2 className="h-4 w-4" strokeWidth={1.75} aria-hidden={true} />
</button>
<TooltipAnchor
side="top"
description={localize('com_ui_expand_editor')}
render={
<button
type="button"
onClick={() => setIsSchemaDialogOpen(true)}
aria-label={localize('com_ui_expand_editor')}
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-text-secondary transition-colors hover:bg-surface-secondary hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary"
>
<Maximize2 className="h-4 w-4" strokeWidth={1.75} aria-hidden={true} />
</button>
}
/>
</div>
<Textarea
id="schemaInput"
@ -271,9 +278,9 @@ export default function ActionsInput({
</div>
{(data || showSkeleton) && (
<div className="mt-4 flex min-h-0 flex-1 flex-col">
<label className="mb-1 block shrink-0 text-sm font-medium text-text-primary">
<Label className="mb-1 shrink-0 font-medium">
{localize('com_assistants_available_actions')}
</label>
</Label>
<div className="min-h-0 flex-1 overflow-y-auto">
{data ? <ActionsTable columns={columns} data={data} /> : <ActionsTableSkeleton />}
</div>

View file

@ -21,7 +21,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="sticky top-0 z-10 border-b border-border-light bg-background py-2 pr-3 text-left text-[11px] font-medium uppercase tracking-wide text-text-secondary"
className="sticky top-0 z-10 border-b border-border-light bg-surface-primary py-2 pr-3 text-left text-[11px] font-medium uppercase tracking-wide text-text-secondary"
>
{header.isPlaceholder
? null

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Settings2 } from 'lucide-react';
import { Button } from '@librechat/client';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
@ -11,15 +12,15 @@ const AdvancedButton: React.FC<AdvancedButtonProps> = ({ setActivePanel }) => {
const localize = useLocalize();
return (
<button
type="button"
<Button
variant="subtle"
onClick={() => setActivePanel(Panel.advanced)}
aria-label={localize('com_ui_advanced')}
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-xl border border-border-light bg-transparent px-3 text-sm font-medium text-text-primary transition-colors hover:bg-surface-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
className="h-9 w-full px-3"
>
<Settings2 className="h-4 w-4" strokeWidth={1.75} aria-hidden="true" />
{localize('com_ui_advanced')}
</button>
</Button>
);
};

View file

@ -1,7 +1,7 @@
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useToastContext } from '@librechat/client';
import { ChevronLeft, Check, Copy } from 'lucide-react';
import { Button, TooltipAnchor, useToastContext } from '@librechat/client';
import type { AgentForm } from '~/common';
import { sectionLabelClass, groupHeadingClass } from './ui';
import { useAgentPanelContext } from '~/Providers';
@ -34,14 +34,15 @@ export default function AdvancedPanel() {
return (
<div className="mb-1 flex w-full flex-col gap-4 text-sm">
<header className="grid grid-cols-[auto_1fr_auto] items-center gap-2 pt-1">
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={() => setActivePanel(Panel.builder)}
aria-label={localize('com_ui_back_to_builder')}
className="inline-flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl border border-border-light text-text-secondary transition-colors hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
className="h-10 w-10 flex-shrink-0 rounded-xl border border-border-light text-text-secondary hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-text-primary"
>
<ChevronLeft className="h-5 w-5" strokeWidth={1.75} aria-hidden="true" />
</button>
</Button>
<h2 className="text-center text-base font-semibold text-text-primary">
{localize('com_ui_advanced_settings')}
</h2>
@ -59,23 +60,27 @@ export default function AdvancedPanel() {
{currentAgentId && (
<div className="flex items-center justify-between gap-2 border-t border-border-light pt-3">
<span className={sectionLabelClass}>{localize('com_ui_agent_id')}</span>
<button
type="button"
onClick={handleCopyAgentId}
title={currentAgentId}
aria-label={localize('com_ui_agent_id_copy')}
className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-text-secondary transition-colors hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
>
<code className="max-w-[150px] truncate font-mono text-xs">{currentAgentId}</code>
<span className="t-icon-swap" data-state={copied ? 'b' : 'a'} aria-hidden="true">
<span className="t-icon" data-icon="a">
<Copy className="h-3.5 w-3.5" aria-hidden="true" />
</span>
<span className="t-icon" data-icon="b">
<Check className="h-3.5 w-3.5 text-green-500" aria-hidden="true" />
</span>
</span>
</button>
<TooltipAnchor
description={currentAgentId}
render={
<Button
variant="ghost"
onClick={handleCopyAgentId}
aria-label={localize('com_ui_agent_id_copy')}
className="h-auto gap-1.5 rounded-lg px-2 py-1 text-text-secondary hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-text-primary"
>
<code className="max-w-[150px] truncate font-mono text-xs">{currentAgentId}</code>
<span className="t-icon-swap" data-state={copied ? 'b' : 'a'} aria-hidden="true">
<span className="t-icon" data-icon="a">
<Copy className="h-3.5 w-3.5" aria-hidden="true" />
</span>
<span className="t-icon" data-icon="b">
<Check className="h-3.5 w-3.5 text-green-500" aria-hidden="true" />
</span>
</span>
</Button>
}
/>
</div>
)}
</div>

View file

@ -1,6 +1,6 @@
import React, { useState, useMemo, useEffect } from 'react';
import { Waypoints, ChevronDown } from 'lucide-react';
import { Label, Input, Textarea } from '@librechat/client';
import { Button, Label, Input, Textarea } from '@librechat/client';
import type { ControllerRenderProps } from 'react-hook-form';
import type { GraphEdge } from 'librechat-data-provider';
import type { AgentForm } from '~/common';
@ -118,9 +118,10 @@ const AgentHandoffs: React.FC<AgentHandoffsProps> = ({ field, currentAgentId })
icon={agentIcon(getAgent(targetAgentId))}
ariaLabel={localize('com_ui_agent_var', { 0: localize('com_ui_select') })}
/>
<button
type="button"
className="flex-shrink-0 rounded-lg p-1 text-text-secondary transition hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
<Button
variant="ghost"
size="icon"
className="size-auto flex-shrink-0 rounded-lg p-1 text-text-secondary hover:bg-surface-secondary hover:text-text-primary"
onClick={() => toggleExpanded(idx)}
aria-expanded={isExpanded}
aria-label={localize(isExpanded ? 'com_ui_collapse' : 'com_ui_expand')}
@ -130,7 +131,7 @@ const AgentHandoffs: React.FC<AgentHandoffsProps> = ({ field, currentAgentId })
className={`transition-transform ${isExpanded ? 'rotate-180' : ''}`}
aria-hidden="true"
/>
</button>
</Button>
</AgentRow>
{isExpanded && (

View file

@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { X, PlusCircle } from 'lucide-react';
import { ControlCombobox } from '@librechat/client';
import { EModelEndpoint } from 'librechat-data-provider';
import { Button, ControlCombobox } from '@librechat/client';
import type { Agent, TMessage } from 'librechat-data-provider';
import type { ReactNode } from 'react';
import type { OptionWithIcon } from '~/common';
@ -83,7 +83,7 @@ export function AddAgentSelect({ options, onSelect, placeholder, ariaLabel }: Ad
selectPlaceholder={placeholder}
searchPlaceholder={localize('com_ui_agent_var', { 0: localize('com_ui_search') })}
items={options}
className="h-9 w-full border-dashed border-border-heavy text-center text-text-secondary hover:text-text-primary"
className="h-9 w-full border-dashed border-border-light text-center text-text-secondary hover:text-text-primary"
containerClassName="px-0"
SelectIcon={<PlusCircle size={16} className="text-text-secondary" />}
/>
@ -134,14 +134,15 @@ interface RemoveButtonProps {
/** Ghost remove (×) button shared by agent rows. */
export function RemoveButton({ onClick, label }: RemoveButtonProps) {
return (
<button
type="button"
className="flex-shrink-0 rounded-lg p-1 text-text-secondary transition hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
<Button
variant="ghost"
size="icon"
className="size-auto flex-shrink-0 p-1 text-text-secondary hover:bg-surface-secondary hover:text-text-primary"
onClick={onClick}
aria-label={label}
>
<X size={16} aria-hidden="true" />
</button>
</Button>
);
}

View file

@ -1,5 +1,5 @@
import { useFormContext, Controller } from 'react-hook-form';
import { Input, HoverCard, HoverCardPortal, HoverCardContent } from '@librechat/client';
import { Input, Label, HoverCard, HoverCardPortal, HoverCardContent } from '@librechat/client';
import type { AgentForm } from '~/common';
import { useLocalize } from '~/hooks';
import { InfoTrigger } from './ui';
@ -13,9 +13,9 @@ export default function MaxAgentSteps() {
<HoverCard openDelay={50}>
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-1.5">
<label htmlFor="recursion_limit" className="text-[13px] font-medium text-text-primary">
<Label htmlFor="recursion_limit" className="w-auto text-[13px] font-medium">
{localize('com_ui_agent_recursion_limit')}
</label>
</Label>
<InfoTrigger />
</div>
<Controller

View file

@ -1,4 +1,5 @@
import {
Button,
Switch,
HoverCard,
CircleHelpIcon,
@ -43,13 +44,13 @@ export function InfoTrigger() {
const localize = useLocalize();
return (
<HoverCardTrigger asChild>
<button
type="button"
<Button
variant="ghost"
aria-label={localize('com_ui_more_info')}
className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded text-text-tertiary transition-colors hover:text-text-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
className="flex h-4 w-4 flex-shrink-0 items-center justify-center rounded p-0 text-text-tertiary transition-colors hover:bg-transparent hover:text-text-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-text-primary"
>
<CircleHelpIcon className="h-3.5 w-3.5" aria-hidden={true} />
</button>
</Button>
</HoverCardTrigger>
);
}

View file

@ -82,7 +82,7 @@ function Avatar({ avatar }: { avatar: AgentAvatar | null }) {
trigger={
<button
type="button"
className="h-14 w-14 overflow-hidden rounded-full outline-none ring-offset-0 transition-shadow focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
className="h-14 w-14 overflow-hidden rounded-full outline-none ring-offset-0 transition-shadow focus:outline-none focus-visible:ring-2 focus-visible:ring-text-primary"
aria-label={localize('com_ui_upload_agent_avatar_label')}
>
<span

View file

@ -1,4 +1,4 @@
import { Input } from '@librechat/client';
import { Input, Label } from '@librechat/client';
import { Controller, useWatch, useFormContext } from 'react-hook-form';
import { EModelEndpoint, getEndpointField } from 'librechat-data-provider';
import type { AgentForm, IconComponentTypes } from '~/common';
@ -106,12 +106,12 @@ export default function AgentConfig() {
{/* MODEL + CATEGORY — balanced 2-column grid */}
<div className="mb-3 grid grid-cols-2 gap-2">
<div className="flex min-w-0 flex-col">
<label
<Label
className="mb-1 block text-[11px] font-medium uppercase tracking-wide text-text-secondary"
htmlFor="provider"
>
{localize('com_ui_model')} <span className="text-red-500">*</span>
</label>
</Label>
<button
type="button"
onClick={() => setActivePanel(Panel.model)}
@ -139,12 +139,12 @@ export default function AgentConfig() {
</button>
</div>
<div className="flex flex-col">
<label
<Label
className="mb-1 block text-[11px] font-medium uppercase tracking-wide text-text-secondary"
htmlFor="category-selector"
>
{localize('com_ui_category')} <span className="text-red-500">*</span>
</label>
</Label>
<AgentCategorySelector className="w-full rounded-lg" />
</div>
</div>
@ -164,9 +164,9 @@ export default function AgentConfig() {
{/* SUPPORT CONTACT */}
<div className="mb-3 flex flex-col">
<label className="mb-1 block text-[11px] font-medium uppercase tracking-wide text-text-secondary">
<Label className="mb-1 block text-[11px] font-medium uppercase tracking-wide text-text-secondary">
{localize('com_ui_support_contact')}
</label>
</Label>
<div className="space-y-2">
<Controller
name="support_contact.name"

View file

@ -1,5 +1,5 @@
import { Globe } from 'lucide-react';
import { Spinner } from '@librechat/client';
import { Button, Spinner } from '@librechat/client';
import { useWatch, useFormContext } from 'react-hook-form';
import {
SystemRoles,
@ -114,26 +114,29 @@ export default function AgentFooter({
resourceName={agent?.name ?? ''}
resourceType={ResourceType.REMOTE_AGENT}
>
<button
<Button
type="button"
className="btn btn-neutral border-token-border-light h-9 px-3"
variant="outline"
aria-label={localize('com_ui_remote_access')}
title={localize('com_ui_remote_access')}
className="h-9 w-auto px-3"
>
<Globe className="h-4 w-4" aria-hidden="true" />
</button>
</Button>
</GenericGrantAccessDialog>
)}
{(agent?.author === user?.id || user?.role === SystemRoles.ADMIN || canEditThisAgent) &&
!permissionsLoading && <DuplicateAgent agent_id={agent_id} />}
{/* Submit Button */}
<button
className="btn btn-primary focus:shadow-outline flex h-9 w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
<Button
variant="submit"
className="h-9 w-full px-4 py-2 font-semibold"
type="submit"
disabled={isSaving}
aria-busy={isSaving}
>
{renderSaveButton()}
</button>
</Button>
</div>
</div>
);

View file

@ -3,6 +3,7 @@ import { Wrench } from 'lucide-react';
import { useFormContext } from 'react-hook-form';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import {
Button,
OGDialog,
TrashIcon,
CircleHelpIcon,
@ -47,12 +48,15 @@ export default function AgentTool({
{ pluginKey: toolId, action: 'uninstall', auth: {}, isEntityTool: true },
{
onError: (error: unknown) => {
showToast({ message: `Error while deleting the tool: ${error}`, status: 'error' });
showToast({
message: localize('com_ui_delete_tool_error_var', { 0: String(error) }),
status: 'error',
});
},
onSuccess: () => {
const remainingToolIds = getValues('tools')?.filter((id: string) => id !== toolId);
setValue('tools', remainingToolIds);
showToast({ message: 'Tool deleted successfully', status: 'success' });
showToast({ message: localize('com_ui_delete_tool_success'), status: 'success' });
},
},
);
@ -98,21 +102,20 @@ export default function AgentTool({
</div>
<OGDialogTrigger asChild>
<button
type="button"
<Button
variant="ghost"
size="icon"
className={cn(
'flex h-7 w-7 items-center justify-center rounded transition-all duration-200',
'hover:bg-gray-200 dark:hover:bg-gray-700',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1',
'focus:opacity-100',
'h-7 w-7 rounded transition-all duration-200 hover:bg-surface-hover',
'focus:opacity-100 focus-visible:opacity-100',
isHovering || isFocused ? 'opacity-100' : 'pointer-events-none opacity-0',
)}
aria-label={`Delete ${currentTool.name}`}
aria-label={localize('com_ui_delete_var', { 0: currentTool.name })}
tabIndex={0}
onFocus={() => setIsFocused(true)}
>
<TrashIcon className="h-4 w-4" />
</button>
</Button>
</OGDialogTrigger>
</div>
<OGDialogTemplate
@ -137,8 +140,7 @@ export default function AgentTool({
}
selection={{
selectHandler: () => removeTool(tool),
selectClasses:
'bg-red-700 hover:bg-red-800 dark:bg-red-600 dark:hover:bg-red-800 text-white',
selectClasses: 'bg-surface-destructive hover:bg-surface-destructive-hover text-white',
selectText: localize('com_ui_delete'),
}}
/>

View file

@ -94,7 +94,7 @@ function DeleteButton({
title={localize('com_ui_delete_agent')}
type="button"
>
<div className="flex w-full items-center justify-center gap-2 text-red-500">
<div className="flex w-full items-center justify-center gap-2 text-text-destructive">
<TrashIcon />
</div>
</Button>
@ -115,7 +115,7 @@ function DeleteButton({
}
selection={{
selectHandler: () => deleteAgent.mutate({ agent_id }),
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectClasses: 'bg-surface-destructive hover:bg-surface-destructive-hover text-white',
selectText: localize('com_ui_delete'),
}}
/>

View file

@ -1,5 +1,5 @@
import { CopyPlus } from 'lucide-react';
import { useToastContext, Button } from '@librechat/client';
import { useToastContext, TooltipAnchor, Button } from '@librechat/client';
import { useDuplicateAgentMutation } from '~/data-provider';
import { isEphemeralAgent } from '~/common';
import { useLocalize } from '~/hooks';
@ -33,17 +33,22 @@ export default function DuplicateAgent({ agent_id }: { agent_id: string }) {
};
return (
<Button
size="sm"
variant="outline"
aria-label={localize('com_ui_duplicate_agent')}
title={localize('com_ui_duplicate_agent')}
type="button"
onClick={handleDuplicate}
>
<div className="flex w-full items-center justify-center gap-2 text-primary">
<CopyPlus className="size-4" />
</div>
</Button>
<TooltipAnchor
description={localize('com_ui_duplicate_agent')}
side="top"
render={
<Button
size="sm"
variant="outline"
aria-label={localize('com_ui_duplicate_agent')}
type="button"
onClick={handleDuplicate}
>
<div className="flex w-full items-center justify-center gap-2 text-text-primary">
<CopyPlus className="size-4" />
</div>
</Button>
}
/>
);
}

View file

@ -2,7 +2,7 @@ import { memo, useMemo, useRef, useState } from 'react';
import * as Ariakit from '@ariakit/react';
import { Folder, Plus, Info } from 'lucide-react';
import { EModelEndpoint, EToolResources } from 'librechat-data-provider';
import { DropdownPopup, SharePointIcon, TooltipAnchor } from '@librechat/client';
import { Button, DropdownPopup, SharePointIcon, TooltipAnchor } from '@librechat/client';
import type { ExtendedFile } from '~/common';
import { useSharePointFileHandlingNoChatContext } from '~/hooks/Files/useSharePointFileHandling';
import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling';
@ -125,8 +125,8 @@ function FileContext({
unmountOnHide={true}
/>
) : (
<button
type="button"
<Button
variant="ghost"
disabled={disabledUploadButton}
aria-label={addLabel}
className={addButtonClassName}
@ -134,7 +134,7 @@ function FileContext({
>
<Plus className="h-3.5 w-3.5" strokeWidth={1.75} aria-hidden="true" />
{localize('com_ui_add')}
</button>
</Button>
);
return (

View file

@ -1,4 +1,4 @@
import { Checkbox } from '@librechat/client';
import { Checkbox, Label } from '@librechat/client';
import { Capabilities } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import type { AssistantForm } from '~/common';
@ -25,9 +25,9 @@ export default function ImageVision() {
/>
)}
/>
<label
<Label
id="image-vision-label"
className="form-check-label text-token-text-primary w-full cursor-pointer"
className="cursor-pointer"
htmlFor={Capabilities.image_vision}
onClick={() =>
setValue(Capabilities.image_vision, !getValues(Capabilities.image_vision), {
@ -36,7 +36,7 @@ export default function ImageVision() {
}
>
<div className="flex items-center">{localize('com_assistants_image_vision')}</div>
</label>
</Label>
</div>
);
}

View file

@ -4,21 +4,20 @@ import { PlusCircle, Maximize2 } from 'lucide-react';
import { specialVariables } from 'librechat-data-provider';
import { Controller, useFormContext } from 'react-hook-form';
import {
Label,
Button,
DropdownPopup,
OGDialog,
Textarea,
DropdownPopup,
OGDialogClose,
OGDialogContent,
OGDialogHeader,
TooltipAnchor,
OGDialogTitle,
OGDialogHeader,
OGDialogContent,
} from '@librechat/client';
import type { TSpecialVarLabel } from 'librechat-data-provider';
import type { AgentForm } from '~/common';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
const textareaClass =
'lc-field flex w-full rounded-lg border border-border-light bg-surface-secondary px-3 py-2 text-text-primary placeholder:text-text-secondary focus-visible:outline-none focus-visible:border-border-medium focus-visible:ring-2 focus-visible:ring-ring-primary disabled:cursor-not-allowed disabled:opacity-50';
interface VariableOption {
label: TSpecialVarLabel;
@ -58,12 +57,12 @@ export default function Instructions() {
return (
<div className="mb-3 flex flex-col">
<div className="mb-1 flex items-center justify-between">
<label
<Label
className="block text-[11px] font-medium uppercase tracking-wide text-text-secondary"
htmlFor="instructions"
>
{localize('com_ui_instructions')}
</label>
</Label>
<div className="flex items-center gap-0.5">
<DropdownPopup
portal={true}
@ -86,15 +85,19 @@ export default function Instructions() {
menuId={menuId}
className="pointer-events-auto z-30"
/>
<button
type="button"
onClick={() => setIsDialogOpen(true)}
aria-label={localize('com_ui_expand_editor')}
title={localize('com_ui_expand_editor')}
className="inline-flex h-7 w-7 items-center justify-center rounded-lg text-text-secondary transition-colors hover:bg-surface-secondary hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
>
<Maximize2 className="h-4 w-4" strokeWidth={1.75} aria-hidden={true} />
</button>
<TooltipAnchor
description={localize('com_ui_expand_editor')}
render={
<Button
variant="ghost"
onClick={() => setIsDialogOpen(true)}
aria-label={localize('com_ui_expand_editor')}
className="h-7 w-7 p-0 text-text-secondary hover:bg-surface-secondary hover:text-text-primary"
>
<Maximize2 className="h-4 w-4" strokeWidth={1.75} aria-hidden={true} />
</Button>
}
/>
</div>
</div>
<Controller
@ -102,10 +105,10 @@ export default function Instructions() {
control={control}
render={({ field, fieldState: { error } }) => (
<>
<textarea
<Textarea
{...field}
value={field.value ?? ''}
className={cn(textareaClass, 'min-h-[88px] resize-y text-sm')}
className="min-h-[88px] resize-y"
id="instructions"
placeholder={localize('com_agents_instructions_placeholder')}
rows={3}
@ -115,7 +118,7 @@ export default function Instructions() {
/>
{error && (
<span
className="mt-1 text-xs text-red-500 transition duration-300 ease-in-out"
className="mt-1 text-xs text-text-destructive transition duration-300 ease-in-out"
role="alert"
>
{localize('com_ui_field_required')}
@ -139,13 +142,10 @@ export default function Instructions() {
name="instructions"
control={control}
render={({ field }) => (
<textarea
<Textarea
{...field}
value={field.value ?? ''}
className={cn(
textareaClass,
'min-h-0 flex-1 resize-none text-base leading-relaxed',
)}
className="min-h-0 flex-1 resize-none text-base leading-relaxed"
placeholder={localize('com_agents_instructions_placeholder')}
aria-label={localize('com_ui_instructions')}
/>

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from 'react';
import { SquirclePlusIcon } from '@librechat/client';
import { Button, SquirclePlusIcon } from '@librechat/client';
import { useLocalize } from '~/hooks';
interface MCPIconProps {
@ -27,22 +27,13 @@ export default function MCPIcon({ icon, onIconChange }: MCPIconProps) {
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
};
return (
<div className="flex items-center gap-4">
<div
role="button"
tabIndex={0}
<Button
variant="ghost"
onClick={handleClick}
onKeyDown={handleKeyDown}
aria-label={localize('com_ui_upload_icon')}
className="bg-token-surface-secondary dark:bg-token-surface-tertiary border-token-border-medium flex h-16 w-16 shrink-0 cursor-pointer items-center justify-center rounded-xl border-2 border-dashed focus:outline-none focus-visible:ring-2 focus-visible:ring-border-heavy"
className="flex h-16 w-16 shrink-0 items-center justify-center rounded-xl border-2 border-dashed border-border-medium bg-surface-secondary p-0 hover:bg-surface-hover"
>
{previewUrl ? (
<img
@ -55,7 +46,7 @@ export default function MCPIcon({ icon, onIconChange }: MCPIconProps) {
) : (
<SquirclePlusIcon />
)}
</div>
</Button>
<div className="flex flex-col gap-1">
<span className="token-text-secondary text-sm">
{localize('com_ui_icon')} {localize('com_ui_optional')}

View file

@ -1,6 +1,6 @@
import { useState } from 'react';
import { TooltipAnchor } from '@librechat/client';
import { Check, Clock, Code2, Info } from 'lucide-react';
import { Button, TooltipAnchor } from '@librechat/client';
import type { AgentToolType } from 'librechat-data-provider';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -17,8 +17,7 @@ interface MCPToolItemProps {
onToggleProgrammatic: () => void;
}
const iconButton =
'flex size-6 items-center justify-center rounded-md transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary';
const iconButton = 'size-6 rounded-md';
export default function MCPToolItem({
tool,
@ -69,8 +68,8 @@ export default function MCPToolItem({
description={localize('com_ui_mcp_click_to_defer')}
side="top"
render={
<button
type="button"
<Button
variant="ghost"
onClick={onToggleDefer}
aria-pressed={isDeferred}
aria-label={localize('com_ui_mcp_defer_loading')}
@ -80,7 +79,7 @@ export default function MCPToolItem({
)}
>
<Clock className="size-4" aria-hidden="true" />
</button>
</Button>
}
/>
)}
@ -89,8 +88,8 @@ export default function MCPToolItem({
description={localize('com_ui_mcp_click_to_programmatic')}
side="top"
render={
<button
type="button"
<Button
variant="ghost"
onClick={onToggleProgrammatic}
aria-pressed={isProgrammatic}
aria-label={localize('com_ui_mcp_programmatic')}
@ -102,12 +101,12 @@ export default function MCPToolItem({
)}
>
<Code2 className="size-4" aria-hidden="true" />
</button>
</Button>
}
/>
)}
<button
type="button"
<Button
variant="ghost"
onClick={() => setExpanded((value) => !value)}
aria-expanded={expanded}
aria-controls={detailsId}
@ -118,7 +117,7 @@ export default function MCPToolItem({
)}
>
<Info className="size-4" aria-hidden="true" />
</button>
</Button>
</div>
</div>
{/* Auto-height reveal via grid-template-rows 0fr -> 1fr so the panel and

View file

@ -2,6 +2,7 @@ import { memo } from 'react';
import { AgentCapabilities } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form';
import {
Button,
Checkbox,
HoverCard,
HoverCardContent,
@ -44,13 +45,13 @@ function Memory() {
{localize('com_agents_enable_memory')}
</label>
<HoverCardTrigger asChild className="ml-2">
<button
type="button"
className="inline-flex items-center"
<Button
variant="ghost"
aria-label={localize('com_agents_memory_info')}
className="inline-flex h-4 w-4 items-center justify-center rounded p-0 text-text-tertiary hover:bg-transparent"
>
<CircleHelpIcon className="h-4 w-4 text-text-tertiary" />
</button>
<CircleHelpIcon className="h-4 w-4" aria-hidden={true} />
</Button>
</HoverCardTrigger>
<HoverCardPortal>
<HoverCardContent side={ESide.Top} className="w-80">

View file

@ -1,7 +1,7 @@
import React, { useMemo, useEffect } from 'react';
import keyBy from 'lodash/keyBy';
import { ControlCombobox } from '@librechat/client';
import { ChevronLeft, RotateCcw } from 'lucide-react';
import { Button, ControlCombobox } from '@librechat/client';
import { useFormContext, useWatch, Controller } from 'react-hook-form';
import {
alternateName,
@ -105,14 +105,15 @@ export default function ModelPanel({
return (
<div className="mb-1 flex w-full flex-col gap-3 text-sm">
<header className="grid grid-cols-[auto_1fr_auto] items-center gap-2 pt-1">
<button
type="button"
<Button
variant="outline"
size="icon"
onClick={() => setActivePanel(Panel.builder)}
aria-label={localize('com_ui_back_to_builder')}
className="inline-flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl border border-border-light text-text-secondary transition-colors hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
className="h-10 w-10 flex-shrink-0 rounded-xl text-text-secondary hover:bg-surface-secondary hover:text-text-primary"
>
<ChevronLeft className="h-5 w-5" strokeWidth={1.75} aria-hidden="true" />
</button>
</Button>
<h2 className="text-center text-base font-semibold text-text-primary">
{localize('com_ui_model_parameters')}
</h2>
@ -250,14 +251,14 @@ export default function ModelPanel({
</div>
)}
{/* Reset Parameters Button */}
<button
type="button"
<Button
variant="outline"
onClick={handleResetParameters}
className="mt-2 inline-flex h-9 w-full items-center justify-center gap-2 rounded-xl border border-border-light bg-transparent px-4 text-sm font-medium text-text-secondary transition-colors hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
className="mt-2 h-9 w-full rounded-xl px-4 font-medium text-text-secondary hover:bg-surface-secondary hover:text-text-primary"
>
<RotateCcw className="h-4 w-4" strokeWidth={1.75} aria-hidden="true" />
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</Button>
</div>
);
}

View file

@ -7,8 +7,8 @@ import {
ScraperProviders,
SearchCategories,
} from 'librechat-data-provider';
import type { SearchApiKeyFormData } from '~/hooks/Plugins/useAuthSearchTool';
import type { UseFormRegister, UseFormHandleSubmit } from 'react-hook-form';
import type { SearchApiKeyFormData } from '~/hooks/Plugins/useAuthSearchTool';
import InputSection, { type DropdownOption } from './InputSection';
import { useGetStartupConfig } from '~/data-provider';
import { useLocalize } from '~/hooks';
@ -273,16 +273,12 @@ export default function ApiKeyDialog({
}
selection={{
selectHandler: handleSubmit(onSubmit),
selectClasses: 'bg-green-500 hover:bg-green-600 text-white',
selectClasses: 'bg-surface-submit hover:bg-surface-submit-hover text-white',
selectText: localize('com_ui_save'),
}}
buttons={
isToolAuthenticated && (
<Button
onClick={onRevoke}
className="bg-red-500 text-white hover:bg-red-600"
aria-label={localize('com_ui_revoke')}
>
<Button variant="destructive" onClick={onRevoke} aria-label={localize('com_ui_revoke')}>
{localize('com_ui_revoke')}
</Button>
)

View file

@ -132,7 +132,7 @@ export default function ActionEditor({
});
},
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
'bg-surface-destructive text-white transition-colors duration-200 hover:bg-surface-destructive-hover',
selectText: localize('com_ui_delete'),
}}
/>

View file

@ -251,8 +251,9 @@ export default function McpSection({ item }: Props) {
}
side="top"
render={
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={() => toggleDeferAll(tools)}
aria-pressed={allDeferred}
aria-label={
@ -261,15 +262,14 @@ export default function McpSection({ item }: Props) {
: localize('com_ui_mcp_defer_all')
}
className={cn(
'flex size-7 items-center justify-center rounded-md transition-colors',
'hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary',
'size-7 rounded-md',
allDeferred
? 'text-amber-600 dark:text-amber-500'
: 'text-text-secondary hover:text-text-primary',
)}
>
<Clock className="size-4" aria-hidden="true" />
</button>
</Button>
}
/>
)}
@ -282,8 +282,9 @@ export default function McpSection({ item }: Props) {
}
side="top"
render={
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={() => toggleProgrammaticAll(tools)}
aria-pressed={allProgrammatic}
aria-label={
@ -292,15 +293,14 @@ export default function McpSection({ item }: Props) {
: localize('com_ui_mcp_programmatic_all')
}
className={cn(
'flex size-7 items-center justify-center rounded-md transition-colors',
'hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary',
'size-7 rounded-md',
allProgrammatic
? 'text-violet-600 dark:text-violet-500'
: 'text-text-secondary hover:text-text-primary',
)}
>
<Code2 className="size-4" aria-hidden="true" />
</button>
</Button>
}
/>
)}

View file

@ -1,7 +1,7 @@
import { useState } from 'react';
import { CheckCircle2 } from 'lucide-react';
import { useFormContext } from 'react-hook-form';
import { useToastContext } from '@librechat/client';
import { Button, useToastContext } from '@librechat/client';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import type { TError, TPluginAction } from 'librechat-data-provider';
import type { ToolItem } from '../../items/types';
@ -74,13 +74,13 @@ export default function ToolSection({ item }: Props) {
<CheckCircle2 className="size-4 text-emerald-500" aria-hidden="true" />
{localize('com_ui_tools_info_configured')}
</span>
<button
type="button"
<Button
variant="ghost"
onClick={() => setEditing(true)}
className="rounded-md px-2 py-1 text-xs font-medium text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
className="h-auto rounded-md px-2 py-1 text-xs font-medium text-text-secondary"
>
{localize('com_ui_edit')}
</button>
</Button>
</div>
)}
{showForm && (

View file

@ -5,6 +5,7 @@ import { PermissionTypes, Permissions } from 'librechat-data-provider';
import {
Radio,
Input,
Label,
Button,
OGDialog,
OGDialogTitle,
@ -193,9 +194,9 @@ export default function SkillsDialog({ open, onOpenChange, agentId }: SkillsDial
/>
</div>
<CategoryFilter options={categoryOptions} value={category} onChange={setCategory} />
<label id="skills-view-label" className="sr-only">
<Label id="skills-view-label" className="sr-only">
{localize('com_ui_skills_filter')}
</label>
</Label>
<Radio
options={viewOptions}
value={view}

View file

@ -1,4 +1,5 @@
import { memo, useState } from 'react';
import { Button } from '@librechat/client';
import { Info, Settings, X } from 'lucide-react';
import type { TranslationKeys } from '~/hooks/useLocalize';
import type { AgentItem } from './items/types';
@ -78,32 +79,30 @@ function ToolRowImpl({ item, onInfo, onRemove }: Props) {
'group-focus-within:opacity-100 group-hover:opacity-100',
)}
>
<button
type="button"
<Button
variant="ghost"
onClick={() => onInfo(item)}
aria-label={
configurable ? localize('com_ui_tools_configure') : localize('com_ui_tools_info')
}
className={cn(
'flex size-6 items-center justify-center rounded-md text-text-secondary',
'hover:bg-surface-hover hover:text-text-primary',
'focus:outline-none focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring-primary',
'size-6 rounded-md p-0 text-text-secondary',
'hover:text-text-primary focus-visible:opacity-100',
)}
>
<DetailIcon className="size-3.5" aria-hidden="true" />
</button>
<button
type="button"
</Button>
<Button
variant="ghost"
onClick={() => onRemove(item)}
aria-label={localize('com_ui_tools_remove')}
className={cn(
'flex size-6 items-center justify-center rounded-md text-text-secondary',
'hover:bg-surface-hover hover:text-text-primary',
'focus:outline-none focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring-primary',
'size-6 rounded-md p-0 text-text-secondary',
'hover:text-text-primary focus-visible:opacity-100',
)}
>
<X className="size-3.5" aria-hidden="true" />
</button>
</Button>
</div>
</div>
);

View file

@ -340,7 +340,7 @@ export default function ToolsSection({ agentId }: Props) {
selection={{
selectHandler: confirmActionRemoval,
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
'bg-surface-destructive hover:bg-surface-destructive-hover transition-colors duration-200 text-white',
selectText: localize('com_ui_delete'),
}}
/>
@ -365,7 +365,7 @@ export default function ToolsSection({ agentId }: Props) {
selection={{
selectHandler: confirmMcpRemoval,
selectClasses:
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 transition-color duration-200 text-white',
'bg-surface-destructive hover:bg-surface-destructive-hover transition-colors duration-200 text-white',
selectText: localize('com_ui_delete'),
}}
/>

View file

@ -70,6 +70,16 @@ jest.mock('../ItemDialog/ItemDialog', () => ({
jest.mock('@librechat/client', () => ({
useToastContext: () => ({ showToast: jest.fn() }),
Button: ({
children,
variant: _variant,
size: _size,
...rest
}: React.ComponentProps<'button'> & { variant?: string; size?: string }) => (
<button type="button" {...rest}>
{children}
</button>
),
Label: ({ children }: { children?: React.ReactNode }) => <span>{children}</span>,
OGDialog: ({ open, children }: { open?: boolean; children?: React.ReactNode }) =>
open ? <div>{children}</div> : null,

View file

@ -1,4 +1,5 @@
import { History } from 'lucide-react';
import { Button } from '@librechat/client';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';
@ -10,15 +11,15 @@ const VersionButton = ({ setActivePanel }: VersionButtonProps) => {
const localize = useLocalize();
return (
<button
type="button"
<Button
variant="subtle"
onClick={() => setActivePanel(Panel.version)}
aria-label={localize('com_ui_agent_version')}
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-xl border border-border-light bg-transparent px-3 text-sm font-medium text-text-primary transition-colors hover:bg-surface-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
className="h-9 w-full px-3"
>
<History className="h-4 w-4" strokeWidth={1.75} aria-hidden="true" />
{localize('com_ui_agent_version')}
</button>
</Button>
);
};

View file

@ -24,9 +24,9 @@ function EmptyState({
const iconWrapBase = 'mb-3 flex h-12 w-12 items-center justify-center rounded-full border';
const iconWrapTone =
tone === 'danger'
? 'border-red-200 bg-red-50 text-red-500 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-400'
? 'border-status-error-border bg-status-error-subtle text-text-destructive'
: 'border-border-light bg-surface-secondary text-text-secondary';
const textTone = tone === 'danger' ? 'text-red-600 dark:text-red-400' : 'text-text-secondary';
const textTone = tone === 'danger' ? 'text-text-destructive' : 'text-text-secondary';
return (
<div className="flex flex-col items-center justify-center px-4 py-12 text-center">

View file

@ -3,6 +3,7 @@ import { formatDistanceToNow } from 'date-fns';
import { Check, RotateCcw, Circle } from 'lucide-react';
import {
Label,
Button,
OGDialog,
OGDialogTrigger,
OGDialogTemplate,
@ -159,13 +160,14 @@ export default function VersionItem({
description={localize('com_ui_agent_version_restore')}
side="left"
render={
<button
type="button"
<Button
variant="ghost"
size="icon"
aria-label={localize('com_ui_agent_version_restore')}
className="inline-flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-lg border border-border-light text-text-secondary opacity-0 transition-all hover:border-border-medium hover:bg-surface-hover hover:text-text-primary focus:outline-none focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring-primary group-hover:opacity-100"
className="size-7 flex-shrink-0 rounded-lg border border-border-light text-text-secondary opacity-0 transition-all hover:border-border-medium focus:outline-none focus-visible:opacity-100 group-hover:opacity-100"
>
<RotateCcw className="h-3.5 w-3.5" strokeWidth={1.75} aria-hidden="true" />
</button>
</Button>
}
/>
</OGDialogTrigger>

View file

@ -1,6 +1,6 @@
import { useCallback, useMemo } from 'react';
import { ChevronLeft } from 'lucide-react';
import { useToastContext } from '@librechat/client';
import { Button, useToastContext } from '@librechat/client';
import type { AgentWithVersions, VersionContext, VersionRecord } from './types';
import {
useGetAgentVersionsQuery,
@ -141,14 +141,15 @@ export default function VersionPanel() {
return (
<div className="scrollbar-gutter-stable h-full min-h-[40vh] overflow-auto pb-12 text-sm">
<header className="grid grid-cols-[auto_1fr_auto] items-center gap-2 px-2 pb-2 pt-1">
<button
type="button"
<Button
variant="subtle"
size="icon"
onClick={() => setActivePanel(Panel.builder)}
aria-label={localize('com_ui_back_to_builder')}
className="inline-flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl border border-border-light text-text-secondary transition-colors hover:bg-surface-secondary hover:text-text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
className="flex-shrink-0 text-text-secondary hover:text-text-primary"
>
<ChevronLeft className="h-5 w-5" strokeWidth={1.75} aria-hidden="true" />
</button>
</Button>
<div className="flex flex-col items-center">
<h2 className="text-base font-semibold text-text-primary">
{localize('com_ui_agent_version_history')}

View file

@ -27,6 +27,8 @@ jest.mock('@librechat/client', () => {
const React = jest.requireActual('react');
return {
Label: ({ children, ...rest }: any) => React.createElement('label', rest, children),
Button: ({ children, variant: _variant, size: _size, ...rest }: any) =>
React.createElement('button', rest, children),
TooltipAnchor: ({ render }: { render: React.ReactNode }) => render,
OGDialog: ({ children }: any) => React.createElement(React.Fragment, null, children),
OGDialogTrigger: ({ children }: any) => children,

View file

@ -157,6 +157,14 @@ jest.mock('../DuplicateAgent', () => ({
jest.mock('@librechat/client', () => ({
Spinner: () => <div data-testid="spinner" />,
Button: ({
children,
variant: _variant,
size: _size,
...props
}: React.ComponentProps<'button'> & { variant?: string; size?: string }) => (
<button {...props}>{children}</button>
),
}));
jest.mock('~/components/Sharing', () => ({

View file

@ -56,6 +56,14 @@ jest.mock('@ariakit/react', () => ({
}));
jest.mock('@librechat/client', () => ({
Button: ({
children,
variant: _variant,
size: _size,
...props
}: React.ComponentProps<'button'> & { variant?: string; size?: string }) => (
<button {...props}>{children}</button>
),
SharePointIcon: () => <span />,
DropdownPopup: () => null,
TooltipAnchor: ({ render }: { render: React.ReactElement }) => render,

View file

@ -9,6 +9,14 @@ jest.mock('~/hooks', () => ({
jest.mock('@librechat/client', () => ({
TooltipAnchor: ({ render }: { render: React.ReactElement }) => render,
Button: ({
children,
variant: _variant,
size: _size,
...props
}: React.ComponentProps<'button'> & { variant?: string; size?: string }) => (
<button {...props}>{children}</button>
),
}));
const tool = {

View file

@ -1091,7 +1091,10 @@
"com_ui_delete_tool": "Delete Tool",
"com_ui_delete_tool_confirm": "Are you sure you want to delete this tool?",
"com_ui_delete_tool_error": "Error removing the tool's saved credentials.",
"com_ui_delete_tool_error_var": "Error while deleting the tool: {{0}}",
"com_ui_delete_tool_save_reminder": "Tool removed. Save the agent to apply changes.",
"com_ui_delete_tool_success": "Tool deleted successfully",
"com_ui_delete_var": "Delete {{0}}",
"com_ui_deleted": "Deleted",
"com_ui_deleting": "Deleting...",
"com_ui_deleting_file": "Deleting file...",