refactor(client): migrate dialogs, toggles and remaining controls to @librechat/client

Swap behavioral controls for @librechat/client equivalents: HeadlessUI
and legacy dialogs to OGDialog, native checkbox/switch to Checkbox/Switch
(onCheckedChange), and remaining buttons/inputs across Chat, Skills,
Tools, Sharing, Memories and Settings. Convert applicable native title=
tooltips to TooltipAnchor and localize close/scroll aria-labels. Skip
swaps that would break floating-label animations or bespoke select
behavior. Update co-located test mocks to provide the newly-used Button
and cn dependencies.
This commit is contained in:
Marco Beretta 2026-06-24 18:28:16 +02:00
parent 9317bf6785
commit b235ad2cbf
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
30 changed files with 356 additions and 320 deletions

View file

@ -1,6 +1,6 @@
import { memo, useCallback, useRef } from 'react';
import { MicOff } from 'lucide-react';
import { useToastContext, TooltipAnchor, ListeningIcon, Spinner } from '@librechat/client';
import { Button, useToastContext, TooltipAnchor, ListeningIcon, Spinner } from '@librechat/client';
import { useLocalize, useSpeechToText, useGetAudioSettings } from '~/hooks';
import { globalAudioId, type TAskFunction } from '~/common';
import { useChatFormContext } from '~/Providers';
@ -107,20 +107,19 @@ export default memo(function AudioRecorder({
<TooltipAnchor
description={localize('com_ui_use_micrphone')}
render={
<button
<Button
id="audio-recorder"
type="button"
variant="ghost"
size="icon"
aria-label={localize('com_ui_use_micrphone')}
onClick={isListening === true ? handleStopRecording : handleStartRecording}
disabled={disabled}
className={cn(
'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover',
)}
title={localize('com_ui_use_micrphone')}
className={cn('size-9 rounded-full p-1')}
aria-pressed={isListening}
>
{renderIcon()}
</button>
</Button>
}
/>
);

View file

@ -1,8 +1,8 @@
import { memo, useState, useRef, useEffect } from 'react';
import { AutoSizer, List } from 'react-virtualized';
import { Spinner, useCombobox } from '@librechat/client';
import { EModelEndpoint } from 'librechat-data-provider';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Input, Spinner, useCombobox } from '@librechat/client';
import type { RecoilState } from 'recoil';
import type { MentionOption, ConvoGenerator } from '~/common';
import { useGetConversation, useLocalize, TranslationKeys } from '~/hooks';
@ -182,10 +182,10 @@ function MentionContent({
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-secondary p-2 shadow-lg">
<input
<Input
ref={initInputRef}
placeholder={localize(placeholder)}
className="mb-1 w-full border-0 bg-surface-secondary p-2 text-sm text-text-primary focus:outline-none"
className="mb-1 h-auto w-full rounded-none border-0 bg-surface-secondary p-2 text-sm text-text-primary focus:outline-none"
autoComplete="off"
value={searchValue}
onKeyDown={(e) => {

View file

@ -5,6 +5,7 @@ import { getEndpointField } from 'librechat-data-provider';
import {
Dialog,
Label,
Button,
PinIcon,
EditIcon,
TrashIcon,
@ -51,17 +52,18 @@ const PresetItems: FC<{
tabIndex={-1}
>
<div className="flex h-full grow items-center justify-end gap-2">
<label
<Label
htmlFor="default-preset"
className="w-40 truncate rounded bg-transparent py-1 text-xs font-medium text-text-secondary transition-colors sm:w-72"
>
{defaultPreset
? `${localize('com_endpoint_preset_default_item')} ${defaultPreset.title}`
: localize('com_endpoint_preset_default_none')}
</label>
</Label>
<Dialog>
<DialogTrigger asChild>
<button
<Button
variant="ghost"
type="button"
className="mr-1 flex h-[32px] cursor-pointer items-center rounded bg-transparent px-2 py-1 text-xs font-medium text-text-secondary transition-colors hover:bg-surface-hover hover:text-text-destructive focus:ring-text-primary"
aria-label={localize('com_ui_clear_all')}
@ -78,7 +80,7 @@ const PresetItems: FC<{
<path d="M9.293 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.707 4L10 .293A1 1 0 0 0 9.293 0M9.5 3.5v-2l3 3h-2a1 1 0 0 1-1-1M6.854 7.146 8 8.293l1.146-1.147a.5.5 0 1 1 .708.708L8.707 9l1.147 1.146a.5.5 0 0 1-.708.708L8 9.707l-1.146 1.147a.5.5 0 0 1-.708-.708L7.293 9 6.146 7.854a.5.5 0 1 1 .708-.708"></path>
</svg>
{localize('com_ui_clear_all')}
</button>
</Button>
</DialogTrigger>
<DialogTemplate
showCloseButton={false}
@ -173,7 +175,8 @@ const PresetItems: FC<{
: localize('com_ui_pin')
}
render={
<button
<Button
variant="ghost"
className={cn(
'm-0 h-full rounded-md bg-transparent p-2 text-text-tertiary hover:text-text-primary focus:text-text-primary',
defaultPreset?.presetId === presetId
@ -194,14 +197,15 @@ const PresetItems: FC<{
}}
>
<PinIcon unpin={defaultPreset?.presetId === presetId} />
</button>
</Button>
}
/>
<TooltipAnchor
description={localize('com_ui_edit')}
aria-label={localize('com_ui_edit')}
render={
<button
<Button
variant="ghost"
className="m-0 h-full rounded-md p-2 text-text-tertiary hover:text-text-primary focus:text-text-primary sm:invisible sm:group-focus-within:visible sm:group-hover:visible"
onClick={(e) => {
e.preventDefault();
@ -217,14 +221,15 @@ const PresetItems: FC<{
}}
>
<EditIcon />
</button>
</Button>
}
/>
<TooltipAnchor
description={localize('com_ui_delete')}
aria-label={localize('com_ui_delete')}
render={
<button
<Button
variant="ghost"
className="m-0 h-full rounded-md p-2 text-text-tertiary hover:text-text-primary focus:text-text-primary sm:invisible sm:group-focus-within:visible sm:group-hover:visible"
onClick={(e) => {
e.preventDefault();
@ -240,7 +245,7 @@ const PresetItems: FC<{
}}
>
<TrashIcon />
</button>
</Button>
}
/>
</div>

View file

@ -2,7 +2,13 @@ import { useCallback, useEffect, useId, useMemo, useReducer, useRef, useState }
import { useRecoilValue } from 'recoil';
import { ContentTypes, EModelEndpoint } from 'librechat-data-provider';
import { ArrowDown, ChevronRight, Maximize2, Minimize2, Users } from 'lucide-react';
import { OGDialog, OGDialogContent, OGDialogTitle, OGDialogDescription } from '@librechat/client';
import {
Button,
OGDialog,
OGDialogTitle,
OGDialogContent,
OGDialogDescription,
} from '@librechat/client';
import type { Agents, TAttachment, TMessage, TMessageContentParts } from 'librechat-data-provider';
import type { PartWithIndex } from '~/components/Chat/Messages/Content/ParallelContent';
import type { SubagentTickerLine } from '~/utils/subagentContent';
@ -539,14 +545,15 @@ export default function SubagentCall({
<div className="relative min-h-0 flex-1 border-t border-border-light bg-surface-primary">
{!isAtBottom && (
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={scrollDialogToBottom}
aria-label={localize('com_ui_subagent_scroll_to_bottom')}
className="absolute bottom-3 right-4 z-10 flex h-8 w-8 items-center justify-center rounded-full border border-border-light bg-surface-secondary text-text-secondary shadow-md transition hover:bg-surface-tertiary hover:text-text-primary"
className="absolute bottom-3 right-4 z-10 h-8 w-8 rounded-full border border-border-light bg-surface-secondary text-text-secondary shadow-md transition hover:bg-surface-tertiary hover:text-text-primary"
>
<ArrowDown size={16} aria-hidden="true" />
</button>
</Button>
)}
<div
ref={scrollRef}
@ -633,14 +640,15 @@ function SubagentPrompt({
<h3 id={headingId} className="text-sm font-medium text-text-primary">
{localize('com_ui_prompt')}
</h3>
<button
type="button"
<Button
variant="ghost"
size="sm"
onClick={onToggle}
aria-controls={contentId}
aria-expanded={expanded}
aria-label={toggleLabel}
title={toggleLabel}
className="inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-xs font-medium text-text-secondary transition hover:bg-surface-tertiary hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-text-primary"
className="h-8 gap-1.5 rounded-md px-2 text-xs font-medium text-text-secondary transition hover:bg-surface-tertiary hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-text-primary"
>
{expanded ? (
<Minimize2 size={14} aria-hidden="true" />
@ -648,7 +656,7 @@ function SubagentPrompt({
<Maximize2 size={14} aria-hidden="true" />
)}
<span className="hidden sm:inline">{toggleLabel}</span>
</button>
</Button>
</div>
<div
id={contentId}

View file

@ -1,9 +1,9 @@
import { memo, useMemo, useState, useCallback, useRef, useId, useEffect } from 'react';
import { useAtomValue } from 'jotai';
import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client';
import { ScrollText, ChevronDown, ChevronUp } from 'lucide-react';
import type { MouseEvent, FocusEvent } from 'react';
import { Button, Clipboard, CheckMark, TooltipAnchor } from '@librechat/client';
import type { SummaryContentPart } from 'librechat-data-provider';
import type { MouseEvent, FocusEvent } from 'react';
import { fontSizeAtom } from '~/store/fontSize';
import { useMessageContext } from '~/Providers';
import { useLocalize } from '~/hooks';
@ -75,13 +75,13 @@ const SummaryButton = memo(
return (
<div className="group/summary flex w-full items-center justify-between gap-2">
<button
type="button"
<Button
variant="ghost"
onClick={onClick}
aria-expanded={isExpanded}
aria-controls={contentId}
className={cn(
'group/button flex flex-1 items-center justify-start rounded-lg leading-[18px]',
'group/button h-auto flex-1 justify-start gap-0 rounded-lg p-0 font-normal leading-[18px] hover:bg-transparent',
fontSize,
)}
>
@ -99,16 +99,17 @@ const SummaryButton = memo(
/>
</span>
<span>{label}</span>
</button>
</Button>
{content && showCopyButton && (
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={onCopy}
aria-label={
isCopied ? localize('com_ui_copied_to_clipboard') : localize('com_ui_copy_summary')
}
className={cn(
'rounded-lg p-1.5 text-text-secondary-alt',
'size-auto rounded-lg p-1.5 text-text-secondary-alt',
isExpanded
? 'opacity-0 group-focus-within/summary-container:opacity-100 group-hover/summary-container:opacity-100'
: 'opacity-0',
@ -124,7 +125,7 @@ const SummaryButton = memo(
) : (
<Clipboard size="19" aria-hidden="true" />
)}
</button>
</Button>
)}
</div>
);

View file

@ -1,7 +1,7 @@
import { useState, useMemo, memo, useCallback, useRef, useId, type MouseEvent } from 'react';
import { useAtomValue } from 'jotai';
import { Lightbulb, ChevronDown, ChevronUp } from 'lucide-react';
import { Clipboard, CheckMark, TooltipAnchor } from '@librechat/client';
import { Button, Clipboard, CheckMark, TooltipAnchor } from '@librechat/client';
import type { FocusEvent, FC } from 'react';
import { useLocalize, useExpandCollapse } from '~/hooks';
import { showThinkingAtom } from '~/store/showThinking';
@ -90,8 +90,9 @@ export const ThinkingButton = memo(
{label}
</button>
{content && showCopyButton && (
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={handleCopy}
aria-label={
isCopied
@ -99,7 +100,7 @@ export const ThinkingButton = memo(
: localize('com_ui_copy_thoughts_to_clipboard')
}
className={cn(
'rounded-lg p-1.5 text-text-secondary-alt',
'size-auto gap-0 rounded-lg p-1.5 text-text-secondary-alt',
isExpanded
? 'opacity-0 group-focus-within/thinking-container:opacity-100 group-hover/thinking-container:opacity-100'
: 'opacity-0',
@ -117,7 +118,7 @@ export const ThinkingButton = memo(
) : (
<Clipboard size="19" aria-hidden="true" />
)}
</button>
</Button>
)}
</div>
);

View file

@ -84,6 +84,14 @@ jest.mock('../Attachment', () => ({
jest.mock('@librechat/client', () => ({
__esModule: true,
Button: ({
children,
variant: _variant,
size: _size,
...props
}: React.ComponentProps<'button'> & { variant?: string; size?: string }) => (
<button {...props}>{children}</button>
),
OGDialog: ({ children }: { children: React.ReactNode }) => <>{children}</>,
OGDialogContent: ({ children }: { children: React.ReactNode }) => (
<div data-testid="dialog-content">{children}</div>

View file

@ -1,4 +1,5 @@
import { ChevronDown } from 'lucide-react';
import { Button } from '@librechat/client';
import * as Popover from '@radix-ui/react-popover';
import CancelledIcon from './CancelledIcon';
import { cn } from '~/utils';
@ -84,12 +85,13 @@ export default function ProgressText({
return (
<Wrapper popover={popover}>
<button
<Button
type="button"
variant="ghost"
className={cn(
'inline-flex w-full items-center gap-2',
'h-auto w-full justify-start gap-2 rounded-none p-0 hover:bg-transparent hover:text-inherit disabled:opacity-100',
hasInput
? 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-heavy'
? 'focus-visible:ring-border-heavy focus-visible:ring-offset-0'
: 'pointer-events-none',
)}
disabled={!hasInput}
@ -110,7 +112,7 @@ export default function ProgressText({
aria-hidden="true"
/>
)}
</button>
</Button>
</Wrapper>
);
}

View file

@ -1,5 +1,6 @@
import { useState, useRef, useMemo, useEffect, useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { Button } from '@librechat/client';
import { ChevronDown, Users } from 'lucide-react';
import { Tools, Constants, ContentTypes, ToolCallTypes } from 'librechat-data-provider';
import type {
@ -10,12 +11,12 @@ import type {
} from 'librechat-data-provider';
import type { PartWithIndex } from './ParallelContent';
import { useLocalize, useExpandCollapse, scheduleMessageContentLayoutReconcile } from '~/hooks';
import { isBashProgrammaticToolCall } from './routing';
import { cn, getToolDisplayLabel } from '~/utils';
import { StackedToolIcons } from './ToolOutput';
import { useMCPIconMap } from '~/hooks/MCP';
import { AttachmentGroup } from './Parts';
import store from '~/store';
import { isBashProgrammaticToolCall } from './routing';
interface ToolMeta {
name: string;
@ -239,9 +240,10 @@ export default function ToolCallGroup({
return (
<div className="mb-2 mt-1" ref={rootRef}>
<button
<Button
variant="ghost"
type="button"
className="inline-flex w-full items-center gap-2 py-1 text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-heavy"
className="inline-flex h-auto w-full items-center justify-start gap-2 rounded-none bg-transparent p-0 py-1 text-text-secondary hover:bg-transparent hover:text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-heavy focus-visible:ring-offset-0"
onClick={handleToggle}
aria-expanded={isExpanded}
aria-label={groupLabel}
@ -282,7 +284,7 @@ export default function ToolCallGroup({
)}
aria-hidden="true"
/>
</button>
</Button>
<div style={expandStyle} onTransitionEnd={handleTransitionEnd} aria-hidden={!isExpanded}>
{shouldRenderBody && (
<div className="overflow-hidden" ref={expandRef}>

View file

@ -1,14 +1,18 @@
import React, { useState } from 'react';
import { Button } from '@librechat/client';
import { UIResourceRenderer } from '@mcp-ui/client';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import type { UIResource } from 'librechat-data-provider';
import { useOptionalMessagesOperations } from '~/Providers';
import { handleUIAction } from '~/utils';
import { useLocalize } from '~/hooks';
interface UIResourceCarouselProps {
uiResources: UIResource[];
}
const UIResourceCarousel: React.FC<UIResourceCarouselProps> = React.memo(({ uiResources }) => {
const localize = useLocalize();
const [showLeftArrow, setShowLeftArrow] = useState(false);
const [showRightArrow, setShowRightArrow] = useState(true);
const [isContainerHovered, setIsContainerHovered] = useState(false);
@ -70,23 +74,17 @@ const UIResourceCarousel: React.FC<UIResourceCarouselProps> = React.memo(({ uiRe
/>
{showLeftArrow && (
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={() => scroll('left')}
className={`absolute left-2 top-1/2 z-20 -translate-y-1/2 rounded-xl bg-surface-fixed p-2 text-text-fixed shadow-lg transition-all duration-200 hover:scale-110 hover:bg-surface-fixed-hover hover:shadow-xl active:scale-95 ${
className={`absolute left-2 top-1/2 z-20 h-auto w-auto -translate-y-1/2 rounded-xl bg-surface-fixed p-2 text-text-fixed shadow-lg transition-all duration-200 hover:scale-110 hover:bg-surface-fixed-hover hover:text-text-fixed hover:shadow-xl active:scale-95 ${
isContainerHovered ? 'opacity-100' : 'pointer-events-none opacity-0'
}`}
aria-label="Scroll left"
aria-label={localize('com_ui_scroll_left')}
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
<ChevronLeft className="h-5 w-5" />
</Button>
)}
<div
@ -122,18 +120,17 @@ const UIResourceCarousel: React.FC<UIResourceCarouselProps> = React.memo(({ uiRe
</div>
{showRightArrow && (
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={() => scroll('right')}
className={`absolute right-2 top-1/2 z-20 -translate-y-1/2 rounded-xl bg-surface-fixed p-2 text-text-fixed shadow-lg transition-all duration-200 hover:scale-110 hover:bg-surface-fixed-hover hover:shadow-xl active:scale-95 ${
className={`absolute right-2 top-1/2 z-20 h-auto w-auto -translate-y-1/2 rounded-xl bg-surface-fixed p-2 text-text-fixed shadow-lg transition-all duration-200 hover:scale-110 hover:bg-surface-fixed-hover hover:text-text-fixed hover:shadow-xl active:scale-95 ${
isContainerHovered ? 'opacity-100' : 'pointer-events-none opacity-0'
}`}
aria-label="Scroll right"
aria-label={localize('com_ui_scroll_right')}
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
<ChevronRight className="h-5 w-5" />
</Button>
)}
</div>
);

View file

@ -1,8 +1,8 @@
import React from 'react';
import { RecoilRoot } from 'recoil';
import { ContentTypes } from 'librechat-data-provider';
import type { TAttachment, TMessageContentParts } from 'librechat-data-provider';
import { fireEvent, render, screen } from '@testing-library/react';
import type { TAttachment, TMessageContentParts } from 'librechat-data-provider';
import ContentParts from '../ContentParts';
jest.mock('~/hooks', () => ({
@ -53,7 +53,14 @@ jest.mock('lucide-react', () => ({
}));
jest.mock('@librechat/client', () => ({
Button: ({ children }: { children?: React.ReactNode }) => <button>{children}</button>,
Button: ({
children,
variant: _variant,
size: _size,
...props
}: React.ComponentProps<'button'> & { variant?: string; size?: string }) => (
<button {...props}>{children}</button>
),
}));
jest.mock('../Parts', () => ({

View file

@ -24,6 +24,7 @@ jest.mock('~/Providers', () => ({
// Mock handleUIAction utility
jest.mock('~/utils', () => ({
handleUIAction: jest.fn(),
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
}));
// Mock scrollTo

View file

@ -14,8 +14,10 @@ import {
import {
Button,
OGDialog,
Textarea,
OGDialogContent,
OGDialogTitle,
TooltipAnchor,
ThumbUpIcon,
ThumbDownIcon,
} from '@librechat/client';
@ -55,19 +57,19 @@ function FeedbackOptionButton({
const label = localize(tag.label as Parameters<typeof localize>[0]);
return (
<button
<Button
variant="ghost"
className={cn(
'flex w-full items-center gap-3 rounded-xl p-2 text-text-secondary transition-colors duration-200 hover:bg-surface-hover hover:text-text-primary',
'h-auto w-full justify-start gap-3 rounded-xl p-2 text-text-secondary transition-colors duration-200 hover:bg-surface-hover hover:text-text-primary',
active && 'bg-surface-hover font-semibold text-text-primary',
)}
onClick={onClick}
type="button"
aria-label={label}
aria-pressed={active}
>
<Icon size="19" bold={active} aria-hidden="true" />
<span>{label}</span>
</button>
</Button>
);
}
@ -146,16 +148,17 @@ function FeedbackButtons({
<Ariakit.PopoverAnchor
store={upStore}
render={
<button
<Button
variant="ghost"
size="icon"
className={buttonClasses(feedback?.rating === 'thumbsUp', isLast)}
onClick={handleThumbsUpClick}
type="button"
title={localize('com_ui_feedback_positive')}
aria-pressed={feedback?.rating === 'thumbsUp'}
aria-haspopup="menu"
>
<ThumbUpIcon size="19" bold={feedback?.rating === 'thumbsUp'} />
</button>
</Button>
}
/>
<Ariakit.Popover
@ -180,16 +183,17 @@ function FeedbackButtons({
<Ariakit.PopoverAnchor
store={downStore}
render={
<button
<Button
variant="ghost"
size="icon"
className={buttonClasses(feedback?.rating === 'thumbsDown', isLast)}
onClick={handleThumbsDownClick}
type="button"
title={localize('com_ui_feedback_negative')}
aria-pressed={feedback?.rating === 'thumbsDown'}
aria-haspopup="menu"
>
<ThumbDownIcon size="19" bold={feedback?.rating === 'thumbsDown'} />
</button>
</Button>
}
/>
<Ariakit.Popover
@ -216,7 +220,7 @@ function FeedbackButtons({
function buttonClasses(isActive: boolean, isLast: boolean) {
return 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 &&
@ -284,21 +288,27 @@ export default function Feedback({
? localize('com_ui_feedback_positive')
: localize('com_ui_feedback_negative');
return (
<button
className={buttonClasses(true, isLast)}
onClick={() => {
if (isThumbsUp) {
handleButtonFeedback(undefined);
} else {
setOpenDialog(true);
}
}}
type="button"
title={label}
aria-pressed="true"
>
<Icon size="19" bold />
</button>
<TooltipAnchor
description={label}
render={
<Button
variant="ghost"
size="icon"
className={buttonClasses(true, isLast)}
onClick={() => {
if (isThumbsUp) {
handleButtonFeedback(undefined);
} else {
setOpenDialog(true);
}
}}
aria-label={label}
aria-pressed="true"
>
<Icon size="19" bold />
</Button>
}
/>
);
};
@ -319,8 +329,8 @@ export default function Feedback({
<OGDialogTitle className="text-token-text-primary text-lg font-semibold leading-6">
{localize('com_ui_feedback_more_information')}
</OGDialogTitle>
<textarea
className="w-full rounded-xl border border-border-light bg-transparent p-2 text-text-primary"
<Textarea
className="h-auto w-full rounded-xl border-border-light p-2"
value={feedback?.text || ''}
onChange={handleTextChange}
rows={4}

View file

@ -3,9 +3,9 @@ import { useRecoilState } from 'recoil';
import * as Ariakit from '@ariakit/react';
import { VisuallyHidden } from '@ariakit/react';
import { GitFork, InfoIcon } from 'lucide-react';
import { useToastContext } from '@librechat/client';
import { ForkOptions } from 'librechat-data-provider';
import { GitCommit, GitBranchPlus, ListTree } from 'lucide-react';
import { Button, Label, Checkbox, useToastContext } from '@librechat/client';
import { TranslationKeys, useLocalize, useNavigateToConvo } from '~/hooks';
import { useForkConvoMutation } from '~/data-provider';
import { cn } from '~/utils';
@ -153,28 +153,28 @@ const CheckboxOption: React.FC<CheckboxOptionProps> = ({
<Ariakit.HovercardAnchor
render={
<div className="flex items-center">
<Ariakit.Checkbox
<Checkbox
id={id}
checked={checked}
onChange={(e) => {
const value = e.target.checked;
if (value && showToastOnCheck) {
onCheckedChange={(value) => {
const isChecked = value === true;
if (isChecked && showToastOnCheck) {
showToast({
message: localize('com_ui_fork_remember_checked'),
status: 'info',
});
}
onToggle(value);
onToggle(isChecked);
}}
className="h-4 w-4 rounded-sm border border-border-xheavy ring-offset-surface-primary transition duration-300 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-surface-inverted data-[state=checked]:text-text-inverted"
className="transition duration-300 ease-in-out"
aria-label={localize(labelKey)}
/>
<label
<Label
htmlFor={id}
className="ml-2 cursor-pointer select-none text-sm text-text-secondary hover:text-text-primary"
className="ml-2 w-auto cursor-pointer select-none break-normal text-sm text-text-secondary hover:text-text-primary"
>
{localize(labelKey)}
</label>
</Label>
</div>
}
/>
@ -227,7 +227,7 @@ export default function Fork({
});
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 &&
@ -330,7 +330,9 @@ export default function Fork({
<Ariakit.PopoverAnchor
store={popoverStore}
render={
<button
<Button
variant="ghost"
size="icon"
className={buttonStyle}
onClick={(e) => {
if (rememberGlobal) {
@ -351,7 +353,7 @@ export default function Fork({
aria-label={localize('com_ui_fork_open_menu')}
>
<GitFork size="19" aria-hidden="true" />
</button>
</Button>
}
/>
<Ariakit.Popover

View file

@ -5,7 +5,7 @@ import { ChevronDown } from 'lucide-react';
import { QueryKeys } from 'librechat-data-provider';
import { useQueryClient } from '@tanstack/react-query';
import { List, CellMeasurer, CellMeasurerCache } from 'react-virtualized';
import { Spinner, TooltipAnchor, NewChatIcon, useMediaQuery } from '@librechat/client';
import { Button, Spinner, TooltipAnchor, NewChatIcon, useMediaQuery } from '@librechat/client';
import type { TConversation } from 'librechat-data-provider';
import {
useLocalize,
@ -127,14 +127,15 @@ const ChatsHeader: FC<ChatsHeaderProps> = memo(({ isExpanded, onToggle }) => {
<TooltipAnchor
description={localize('com_ui_new_chat')}
render={
<button
type="button"
<Button
variant="ghost"
size="icon"
aria-label={localize('com_ui_new_chat')}
className={headerIconButtonClassName}
onClick={handleNewChat}
>
<NewChatIcon className="h-4 w-4" />
</button>
</Button>
}
/>
</div>

View file

@ -1,6 +1,8 @@
import React, { useEffect, useRef } from 'react';
import { Check, X } from 'lucide-react';
import { Button, Input } from '@librechat/client';
import type { KeyboardEvent } from 'react';
import { cn } from '~/utils';
interface RenameFormProps {
titleInput: string;
@ -45,10 +47,12 @@ const RenameForm: React.FC<RenameFormProps> = ({
role="form"
aria-label={localize('com_ui_rename_conversation')}
>
<input
<Input
ref={inputRef}
type="text"
className="w-full rounded bg-transparent p-0.5 text-sm leading-tight focus-visible:outline-none"
className={cn(
'h-auto w-full rounded border-0 bg-transparent p-0.5 text-sm leading-tight focus-visible:outline-none',
)}
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
onKeyDown={handleKeyDown}
@ -56,22 +60,30 @@ const RenameForm: React.FC<RenameFormProps> = ({
aria-label={localize('com_ui_new_conversation_title')}
/>
<div className="flex gap-1" role="toolbar">
<button
<Button
variant="ghost"
size="icon"
onClick={() => onCancel()}
className="rounded-md p-1 hover:opacity-70 focus:outline-none focus:ring-2 focus:ring-text-primary"
className={cn(
'size-auto rounded-md p-1 hover:opacity-70 focus:outline-none focus:ring-2 focus:ring-text-primary',
)}
aria-label={localize('com_ui_cancel')}
type="button"
>
<X className="h-4 w-4" aria-hidden="true" />
</button>
<button
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => onSubmit(titleInput)}
className="rounded-md p-1 hover:opacity-70 focus:outline-none focus:ring-2 focus:ring-text-primary"
className={cn(
'size-auto rounded-md p-1 hover:opacity-70 focus:outline-none focus:ring-2 focus:ring-text-primary',
)}
aria-label={localize('com_ui_save')}
type="button"
>
<Check className="h-4 w-4" aria-hidden="true" />
</button>
</Button>
</div>
</div>
);

View file

@ -86,6 +86,14 @@ jest.mock('@librechat/client', () => ({
useMediaQuery: () => false,
TooltipAnchor: ({ render }: { render: React.ReactNode }) => render,
NewChatIcon: () => <svg data-testid="new-chat-icon" />,
Button: ({
children,
variant: _variant,
size: _size,
...props
}: React.ComponentProps<'button'> & { variant?: string; size?: string }) => (
<button {...props}>{children}</button>
),
}));
jest.mock('~/data-provider', () => ({

View file

@ -10,6 +10,7 @@ import {
BedrockIcon,
AssistantIcon,
AnthropicIcon,
TooltipAnchor,
AzureMinimalIcon,
CustomMinimalIcon,
} from '@librechat/client';
@ -62,8 +63,8 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
const assistantsIcon = {
icon: iconURL ? (
<div className="relative flex h-6 w-6 items-center justify-center">
<div
title={assistantName}
<TooltipAnchor
description={assistantName ?? ''}
style={{
width: size,
height: size,
@ -76,7 +77,7 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
alt={assistantName}
style={{ height: '80', width: '80' }}
/>
</div>
</TooltipAnchor>
</div>
) : (
<div className="h-6 w-6">
@ -91,8 +92,8 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
const agentsIcon = {
icon: iconURL ? (
<div className="relative flex h-6 w-6 items-center justify-center">
<div
title={agentName}
<TooltipAnchor
description={agentName ?? ''}
style={{
width: size,
height: size,
@ -105,7 +106,7 @@ const MessageEndpointIcon: React.FC<IconProps> = (props) => {
alt={agentName}
style={{ height: '80', width: '80' }}
/>
</div>
</TooltipAnchor>
</div>
) : (
<div className="h-6 w-6">

View file

@ -1,6 +1,6 @@
import React from 'react';
import { Spinner } from '@librechat/client';
import { PlugZap, SlidersHorizontal, X } from 'lucide-react';
import { Button, Spinner, TooltipAnchor } from '@librechat/client';
import type { MCPServerStatus } from 'librechat-data-provider';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -144,18 +144,23 @@ function CompactStatusDot({ serverStatus, isInitializing }: CompactStatusDotProp
function LoadingStatusIcon({ serverName, onCancel, canCancel }: InitializingStatusProps) {
if (canCancel) {
return (
<button
type="button"
onClick={onCancel}
className="group flex size-6 items-center justify-center rounded p-1 hover:bg-status-error-subtle"
aria-label={localize('com_ui_cancel')}
title={localize('com_ui_cancel')}
>
<div className="relative size-4">
<Spinner className="size-4 text-text-primary group-hover:opacity-0" />
<X className="absolute inset-0 size-4 text-text-destructive opacity-0 group-hover:opacity-100" />
</div>
</button>
<TooltipAnchor
description={localize('com_ui_cancel')}
render={
<Button
variant="ghost"
size="icon"
onClick={onCancel}
className="group size-6 rounded p-1 hover:bg-status-error-subtle"
aria-label={localize('com_ui_cancel')}
>
<div className="relative size-4">
<Spinner className="size-4 text-text-primary group-hover:opacity-0" />
<X className="absolute inset-0 size-4 text-text-destructive opacity-0 group-hover:opacity-100" />
</div>
</Button>
}
/>
);
}
@ -183,27 +188,29 @@ function ConnectingSpinner({ serverName }: { serverName: string }) {
/** Connect button - shown for disconnected/error states. Uses PlugZap icon. */
function ConnectButton({ serverName, onConfigClick }: StatusIconProps) {
return (
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={onConfigClick}
className="flex size-6 items-center justify-center rounded p-1 hover:bg-surface-secondary"
className="size-6 rounded p-1 hover:bg-surface-secondary"
aria-label={localize('com_nav_mcp_connect_server', { 0: serverName })}
>
<PlugZap className="size-4 text-text-secondary" aria-hidden="true" />
</button>
</Button>
);
}
/** Configure button - shown for connected servers with custom vars. Uses SlidersHorizontal icon. */
function ConfigureButton({ serverName, onConfigClick }: StatusIconProps) {
return (
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={onConfigClick}
className="flex size-6 items-center justify-center rounded p-1 hover:bg-surface-secondary"
className="size-6 rounded p-1 hover:bg-surface-secondary"
aria-label={localize('com_nav_mcp_configure_server', { 0: serverName })}
>
<SlidersHorizontal className="size-4 text-text-secondary" aria-hidden="true" />
</button>
</Button>
);
}

View file

@ -139,16 +139,17 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
)}
</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 font-normal text-text-primary hover:underline"
>
{useBackup ? localize('com_ui_use_2fa_code') : localize('com_ui_use_backup_code')}
</button>
</Button>
</div>
)}
{renderDeleteButton(handleDeleteUser, isDeleting, isLocked || !otpReady, localize)}
@ -166,9 +167,9 @@ const renderInput = (
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
) => (
<div className="mb-4">
<label className="mb-1 block text-sm font-medium text-text-primary" htmlFor={id}>
<Label className="mb-1 text-sm font-medium" htmlFor={id}>
{label}
</label>
</Label>
<Input id={id} onChange={onChange} placeholder={value} />
</div>
);
@ -179,10 +180,13 @@ const renderDeleteButton = (
isLocked: boolean,
localize: LocalizeFunction,
) => (
<button
<Button
variant="destructive"
className={cn(
'mt-4 flex w-full items-center justify-center rounded-lg bg-surface-tertiary px-4 py-2 transition-all duration-200',
isLocked ? 'cursor-not-allowed opacity-30' : 'bg-surface-destructive text-white',
'mt-4 w-full gap-0 bg-surface-tertiary text-text-primary transition-all duration-200 hover:bg-surface-tertiary disabled:opacity-100',
isLocked
? 'cursor-not-allowed opacity-30'
: 'bg-surface-destructive text-white hover:bg-surface-destructive-hover',
)}
onClick={handleDeleteUser}
disabled={isDeleting || isLocked}
@ -206,7 +210,7 @@ const renderDeleteButton = (
)}
</>
)}
</button>
</Button>
);
export default DeleteAccount;

View file

@ -193,21 +193,23 @@ export default function SharedLinks() {
},
cell: ({ row }) => {
const { title, shareId } = row.original;
const link = (
<Link
to={`/share/${shareId}`}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center gap-1 truncate rounded-sm text-link underline decoration-1 underline-offset-2 hover:decoration-2 focus:outline-none focus:ring-2 focus:ring-text-primary"
>
<span className="truncate">{title}</span>
<ExternalLink
className="size-3 flex-shrink-0 opacity-70 group-hover:opacity-100"
aria-hidden="true"
/>
</Link>
);
return (
<div className="flex items-center gap-2">
<Link
to={`/share/${shareId}`}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center gap-1 truncate rounded-sm text-link underline decoration-1 underline-offset-2 hover:decoration-2 focus:outline-none focus:ring-2 focus:ring-text-primary"
title={title}
>
<span className="truncate">{title}</span>
<ExternalLink
className="size-3 flex-shrink-0 opacity-70 group-hover:opacity-100"
aria-hidden="true"
/>
</Link>
{title ? <TooltipAnchor description={title} render={link} /> : link}
</div>
);
},
@ -271,17 +273,18 @@ export default function SharedLinks() {
<TooltipAnchor
description={localize('com_ui_open_source_chat_new_tab')}
render={
<a
href={`/c/${row.original.conversationId}`}
target="_blank"
rel="noopener noreferrer"
className="flex h-8 w-8 items-center justify-center rounded-md p-0 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-text-primary"
aria-label={localize('com_ui_open_source_chat_new_tab_title', {
title: row.original.title || localize('com_ui_untitled'),
})}
>
<MessageSquare className="size-4" aria-hidden="true" />
</a>
<Button asChild variant="ghost" className="h-8 w-8 p-0 hover:bg-surface-hover">
<a
href={`/c/${row.original.conversationId}`}
target="_blank"
rel="noopener noreferrer"
aria-label={localize('com_ui_open_source_chat_new_tab_title', {
title: row.original.title || localize('com_ui_untitled'),
})}
>
<MessageSquare className="size-4" aria-hidden="true" />
</a>
</Button>
}
/>
<TooltipAnchor

View file

@ -4,6 +4,7 @@ import { ShieldEllipsis } from 'lucide-react';
import { useForm, Controller } from 'react-hook-form';
import { Permissions, SystemRoles, PermissionTypes } from 'librechat-data-provider';
import {
Label,
Button,
Switch,
OGDialog,
@ -13,7 +14,7 @@ import {
OGDialogTrigger,
useToastContext,
} from '@librechat/client';
import type { Control, UseFormSetValue, UseFormGetValues } from 'react-hook-form';
import type { Control } from 'react-hook-form';
import { useUpdatePeoplePickerPermissionsMutation } from '~/data-provider';
import { useLocalize, useAuthContext, useRoleSelector } from '~/hooks';
@ -27,36 +28,20 @@ type LabelControllerProps = {
label: string;
peoplePickerPerm: Permissions.VIEW_USERS | Permissions.VIEW_GROUPS | Permissions.VIEW_ROLES;
control: Control<FormValues, unknown, FormValues>;
setValue: UseFormSetValue<FormValues>;
getValues: UseFormGetValues<FormValues>;
};
const LabelController: React.FC<LabelControllerProps> = ({
control,
peoplePickerPerm,
label,
getValues,
setValue,
}) => (
const LabelController: React.FC<LabelControllerProps> = ({ control, peoplePickerPerm, label }) => (
<div className="mb-4 flex items-center justify-between gap-2">
<button
className="cursor-pointer select-none"
type="button"
onClick={() =>
setValue(peoplePickerPerm, !getValues(peoplePickerPerm), {
shouldDirty: true,
})
}
tabIndex={0}
>
<Label htmlFor={peoplePickerPerm} className="w-auto cursor-pointer select-none break-normal">
{label}
</button>
</Label>
<Controller
name={peoplePickerPerm}
control={control}
render={({ field }) => (
<Switch
{...field}
id={peoplePickerPerm}
checked={field.value ?? false}
onCheckedChange={field.onChange}
value={(field.value ?? false).toString()}
@ -93,8 +78,6 @@ const PeoplePickerAdminSettings = () => {
const {
reset,
control,
setValue,
getValues,
handleSubmit,
formState: { isSubmitting },
} = useForm<FormValues>({
@ -140,7 +123,7 @@ const PeoplePickerAdminSettings = () => {
<OGDialogTrigger asChild>
<Button
variant={'outline'}
className="btn btn-neutral border-token-border-light gap-1 rounded-lg font-medium"
className="gap-1 rounded-lg font-medium"
aria-label={localize('com_ui_admin_settings')}
>
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
@ -179,14 +162,13 @@ const PeoplePickerAdminSettings = () => {
control={control}
peoplePickerPerm={peoplePickerPerm}
label={label}
getValues={getValues}
setValue={setValue}
/>
</div>
))}
</div>
<div className="flex justify-end">
<button
<Button
variant="submit"
type="button"
onClick={handleSubmit(onSubmit)}
disabled={
@ -194,10 +176,10 @@ const PeoplePickerAdminSettings = () => {
isLoading ||
(isSelectedCustomRole && (isCustomRoleLoading || isCustomRoleError))
}
className="btn rounded bg-surface-submit font-bold text-white transition-all hover:bg-surface-submit-hover"
className="font-bold"
>
{localize('com_ui_save')}
</button>
</Button>
</div>
</form>
</div>

View file

@ -3,6 +3,7 @@ import { Pencil } from 'lucide-react';
import { Trans } from 'react-i18next';
import {
Label,
Button,
Spinner,
OGDialog,
TrashIcon,
@ -64,14 +65,16 @@ export default function MemoryCardActions({ memory }: MemoryCardActionsProps) {
description={localize('com_ui_edit_memory')}
side="top"
render={
<button
<Button
ref={triggerRef}
variant="ghost"
size="icon"
className={buttonBaseClass}
aria-label={localize('com_ui_edit')}
onClick={() => setEditOpen(true)}
>
<Pencil className="size-3.5" aria-hidden="true" />
</button>
</Button>
}
/>
</OGDialogTrigger>
@ -84,7 +87,9 @@ export default function MemoryCardActions({ memory }: MemoryCardActionsProps) {
description={localize('com_ui_delete_memory')}
side="top"
render={
<button
<Button
variant="ghost"
size="icon"
className={buttonBaseClass}
aria-label={localize('com_ui_delete')}
onClick={() => setDeleteOpen(true)}
@ -94,7 +99,7 @@ export default function MemoryCardActions({ memory }: MemoryCardActionsProps) {
) : (
<TrashIcon className="size-3.5" aria-hidden="true" />
)}
</button>
</Button>
}
/>
</OGDialogTrigger>

View file

@ -1,5 +1,5 @@
import { memo } from 'react';
import { cn } from '~/utils';
import { Switch } from '@librechat/client';
interface SkillToggleProps {
enabled: boolean;
@ -9,31 +9,12 @@ interface SkillToggleProps {
function SkillToggle({ enabled, onChange, ariaLabel }: SkillToggleProps) {
return (
<button
type="button"
role="switch"
aria-checked={enabled}
aria-label={ariaLabel}
onClick={(e) => {
e.stopPropagation();
onChange();
}}
<span
onClick={(e) => e.stopPropagation()}
className="inline-flex h-9 items-center justify-center rounded-md px-1 transition-colors hover:bg-surface-hover"
>
<span
className={cn(
'relative inline-flex h-5 w-9 shrink-0 rounded-full transition-colors duration-200',
enabled ? 'bg-surface-submit' : 'bg-border-medium',
)}
>
<span
className={cn(
'pointer-events-none mt-0.5 inline-block size-4 rounded-full bg-surface-fixed shadow-sm transition-transform duration-200',
enabled ? 'translate-x-[1.125rem]' : 'translate-x-0.5',
)}
/>
</span>
</button>
<Switch checked={enabled} onCheckedChange={() => onChange()} aria-label={ariaLabel} />
</span>
);
}

View file

@ -2,7 +2,7 @@ import { useState } from 'react';
import { AlertTriangle } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useForm, Controller, FormProvider } from 'react-hook-form';
import { Input, Button, TextareaAutosize, useToastContext } from '@librechat/client';
import { Input, Label, Button, TextareaAutosize, useToastContext } from '@librechat/client';
import {
InvocationMode,
SKILL_NAME_PATTERN,
@ -11,9 +11,9 @@ import {
} from 'librechat-data-provider';
import type { TSkill, TCreateSkill, TSkillWarning } from 'librechat-data-provider';
import { useCreateSkillMutation } from '~/data-provider';
import { useLocalize } from '~/hooks';
import SkillContentEditor from './SkillContentEditor';
import CategorySelector from './CategorySelector';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
const DEFAULT_BODY = `# Overview
@ -177,12 +177,12 @@ export default function CreateSkillForm({
aria-invalid={errors.name ? 'true' : 'false'}
aria-describedby={errors.name ? 'skill-name-error' : undefined}
/>
<label
<Label
htmlFor="skill-name"
className="pointer-events-none absolute -top-1 left-3 origin-[0] translate-y-3 scale-100 rounded bg-presentation px-1 text-base text-text-secondary transition-transform duration-200 peer-placeholder-shown:translate-y-3 peer-placeholder-shown:scale-100 peer-focus:-translate-y-2 peer-focus:scale-75 peer-focus:text-text-primary peer-[:not(:placeholder-shown)]:-translate-y-2 peer-[:not(:placeholder-shown)]:scale-75"
>
{localize('com_ui_name')}*
</label>
</Label>
<div
id="skill-name-error"
className={cn(
@ -217,13 +217,13 @@ export default function CreateSkillForm({
}}
render={({ field }) => (
<div className="flex flex-col">
<label
<Label
htmlFor="skill-description"
className="mb-1 text-sm font-medium text-text-secondary"
>
{localize('com_ui_description')}
<span className="ml-0.5 text-text-destructive">*</span>
</label>
</Label>
<TextareaAutosize
{...field}
id="skill-description"

View file

@ -1,8 +1,15 @@
import { useEffect, useMemo, useState } from 'react';
import { Info, AlertTriangle } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { Info, AlertTriangle } from 'lucide-react';
import { useForm, Controller, FormProvider } from 'react-hook-form';
import { Input, Button, Skeleton, TextareaAutosize, useToastContext } from '@librechat/client';
import {
Label,
Input,
Button,
Skeleton,
TextareaAutosize,
useToastContext,
} from '@librechat/client';
import {
InvocationMode,
SKILL_NAME_PATTERN,
@ -278,13 +285,13 @@ export default function SkillForm({ skillId }: SkillFormProps) {
}}
render={({ field }) => (
<div className="flex flex-col">
<label
<Label
htmlFor="skill-description"
className="mb-1 text-sm font-medium text-text-secondary"
>
{localize('com_ui_description')}
<span className="ml-0.5 text-text-destructive">*</span>
</label>
</Label>
<TextareaAutosize
{...field}
id="skill-description"

View file

@ -1,6 +1,6 @@
import { useState, useCallback, useRef } from 'react';
import { Button, Spinner } from '@librechat/client';
import { Save, FileText, Circle } from 'lucide-react';
import { Button, Spinner, Textarea } from '@librechat/client';
import { useGetSkillNodeContentQuery, useUpdateSkillNodeContentMutation } from '~/data-provider';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
@ -101,13 +101,14 @@ export default function SkillFileEditor({ skillId, nodeId, fileName }: SkillFile
</Button>
</div>
<div className="relative flex-1 overflow-hidden">
<textarea
<Textarea
ref={textareaRef}
value={displayContent}
onChange={handleChange}
spellCheck={false}
className={cn(
'size-full resize-none bg-transparent px-4 py-3 font-mono text-[13px] leading-6 text-text-primary outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring-primary',
'size-full resize-none rounded-none border-0 bg-transparent px-4 py-3 font-mono text-[13px] leading-6 text-text-primary outline-none',
'focus:ring-0 focus:ring-offset-0 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring-primary',
'selection:bg-blue-500/20',
)}
aria-label={`${localize('com_ui_edit')} ${fileName}`}

View file

@ -1,10 +1,16 @@
import { useEffect } from 'react';
import { Search, X } from 'lucide-react';
import { Alert } from '@librechat/client';
import { Dialog, DialogPanel, DialogTitle, Description } from '@headlessui/react';
import { Search } from 'lucide-react';
import { useFormContext } from 'react-hook-form';
import { isAgentsEndpoint } from 'librechat-data-provider';
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
import {
Alert,
Input,
OGDialog,
OGDialogTitle,
OGDialogContent,
OGDialogDescription,
} from '@librechat/client';
import type {
AssistantsEndpoint,
EModelEndpoint,
@ -140,49 +146,32 @@ function AssistantToolsDialog({
]);
return (
<Dialog
<OGDialog
open={isOpen}
onClose={() => {
setIsOpen(false);
setCurrentPage(1);
setSearchValue('');
onOpenChange={(open) => {
if (!open) {
setIsOpen(false);
setCurrentPage(1);
setSearchValue('');
}
}}
className="relative z-[102]"
>
{/* The backdrop, rendered as a fixed sibling to the panel container */}
<div className="fixed inset-0 bg-surface-primary opacity-60 transition-opacity dark:opacity-80" />
{/* Full-screen container to center the panel */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel
className="relative w-full transform overflow-hidden overflow-y-auto rounded-lg bg-surface-secondary text-left shadow-xl transition-all max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
style={{ minHeight: '610px' }}
>
<OGDialogContent
className="overflow-hidden overflow-y-auto bg-surface-secondary text-left max-sm:h-full sm:mx-7 sm:my-8 sm:max-w-2xl lg:max-w-5xl xl:max-w-7xl"
style={{ minHeight: '610px' }}
>
<div>
<div className="flex items-center justify-between border-b-[1px] border-border-medium px-4 pb-4 pt-5 sm:p-6">
<div className="flex items-center">
<div className="text-center sm:text-left">
<DialogTitle className="text-lg font-medium leading-6 text-text-primary">
<OGDialogTitle className="text-lg font-medium leading-6 text-text-primary">
{isAgentTools
? localize('com_nav_tool_dialog_agents')
: localize('com_nav_tool_dialog')}
</DialogTitle>
<Description className="text-sm text-text-secondary">
</OGDialogTitle>
<OGDialogDescription className="text-sm text-text-secondary">
{localize('com_nav_tool_dialog_description')}
</Description>
</div>
</div>
<div>
<div className="sm:mt-0">
<button
onClick={() => {
setIsOpen(false);
setCurrentPage(1);
}}
className="inline-block rounded-full text-text-secondary transition-colors hover:text-text-primary"
aria-label="Close dialog"
type="button"
>
<X aria-hidden="true" />
</button>
</OGDialogDescription>
</div>
</div>
</div>
@ -203,12 +192,13 @@ function AssistantToolsDialog({
<div className="p-4 sm:p-6 sm:pt-4">
<div className="mt-4 flex flex-col gap-4">
<div className="flex items-center justify-center space-x-4">
<Search className="h-6 w-6 text-text-tertiary" />
<input
<Search className="h-6 w-6 text-text-tertiary" aria-hidden="true" />
<Input
type="text"
value={searchValue}
onChange={handleSearch}
placeholder={localize('com_nav_tool_search')}
aria-label={localize('com_nav_tool_search')}
className="w-64 rounded border border-border-medium bg-transparent px-2 py-1 text-text-primary focus:outline-none"
/>
</div>
@ -243,9 +233,9 @@ function AssistantToolsDialog({
)}
</div>
</div>
</DialogPanel>
</div>
</Dialog>
</div>
</OGDialogContent>
</OGDialog>
);
}

View file

@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { OGDialog, DialogTemplate, useToastContext } from '@librechat/client';
import { OGDialog, Button, OGDialogTemplate, useToastContext } from '@librechat/client';
import type { TTermsOfService } from 'librechat-data-provider';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import { useAcceptTermsMutation } from '~/data-provider';
@ -63,7 +63,7 @@ const TermsAndConditionsModal = ({
return (
<OGDialog open={open} onOpenChange={handleOpenChange}>
<DialogTemplate
<OGDialogTemplate
title={title ?? localize('com_ui_terms_and_conditions')}
className="w-11/12 max-w-3xl sm:w-3/4 md:w-1/2 lg:w-2/5"
showCloseButton={false}
@ -87,18 +87,12 @@ const TermsAndConditionsModal = ({
}
buttons={
<>
<button
onClick={handleDecline}
className="inline-flex h-10 items-center justify-center rounded-lg border border-border-heavy bg-surface-secondary px-4 py-2 text-sm text-text-primary hover:bg-surface-active"
>
<Button variant="outline" onClick={handleDecline}>
{localize('com_ui_decline')}
</button>
<button
onClick={handleAccept}
className="inline-flex h-10 items-center justify-center rounded-lg border border-border-heavy bg-surface-secondary px-4 py-2 text-sm text-text-primary hover:bg-surface-submit hover:text-white focus:bg-surface-submit focus:text-white"
>
</Button>
<Button variant="submit" onClick={handleAccept}>
{localize('com_ui_accept')}
</button>
</Button>
</>
}
/>

View file

@ -3,6 +3,7 @@ import { JSX } from 'react/jsx-runtime';
import { Sun, Moon, Monitor } from 'lucide-react';
import { ThemeContext, isDark } from '../theme';
import { useLocalize } from '../hooks';
import { Button } from './Button';
declare global {
interface Window {
@ -35,23 +36,19 @@ const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) =
}, [nextTheme, onChange]);
return (
<button
className="flex items-center gap-2 rounded-lg p-2 text-text-primary transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary focus-visible:ring-offset-2"
<Button
variant="ghost"
size="icon"
className="h-auto w-auto p-2 text-text-primary"
aria-label={localize('com_ui_toggle_theme')}
aria-keyshortcuts="Ctrl+Shift+T"
onClick={(e) => {
e.preventDefault();
onChange(nextTheme);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onChange(nextTheme);
}
}}
>
{themeIcons[theme as ThemeType]}
</button>
</Button>
);
};