refactor(client): migrate low-risk primitives to @librechat/client

Swap raw <label>, <textarea>, and native title= tooltips for the
@librechat/client Label, Textarea, and TooltipAnchor components across
Agents, Endpoints settings, Export modal, Prompts, Sharing, and Memory
dialogs. Add localization keys (scroll, sibling navigation, none
selected, select var) for the remaining swap waves.
This commit is contained in:
Marco Beretta 2026-06-24 17:58:23 +02:00
parent f658ffdb3c
commit a1b22b745d
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
10 changed files with 120 additions and 78 deletions

View file

@ -1,7 +1,13 @@
import React, { useRef } from 'react';
import { Link, Pin, PinOff } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { OGDialog, OGDialogContent, Button, useToastContext } from '@librechat/client';
import {
OGDialog,
OGDialogContent,
Button,
TooltipAnchor,
useToastContext,
} from '@librechat/client';
import {
QueryKeys,
Constants,
@ -165,24 +171,32 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
{/* Action button */}
<div className="mb-4 mt-6 flex justify-center gap-2">
<Button
variant="outline"
size="icon"
onClick={handleFavoriteClick}
title={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
>
{isFavorite ? <PinOff className="h-4 w-4" /> : <Pin className="h-4 w-4" />}
</Button>
<Button
variant="outline"
size="icon"
onClick={handleCopyLink}
title={localize('com_agents_copy_link')}
aria-label={localize('com_agents_copy_link')}
>
<Link className="h-4 w-4" aria-hidden="true" />
</Button>
<TooltipAnchor
description={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
render={
<Button
variant="outline"
size="icon"
onClick={handleFavoriteClick}
aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
>
{isFavorite ? <PinOff className="h-4 w-4" /> : <Pin className="h-4 w-4" />}
</Button>
}
/>
<TooltipAnchor
description={localize('com_agents_copy_link')}
render={
<Button
variant="outline"
size="icon"
onClick={handleCopyLink}
aria-label={localize('com_agents_copy_link')}
>
<Link className="h-4 w-4" aria-hidden="true" />
</Button>
}
/>
<Button
variant="submit"
className="w-full max-w-xs"

View file

@ -1,7 +1,7 @@
import React from 'react';
import { Link, Pin, PinOff } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { OGDialogContent, Button, useToastContext } from '@librechat/client';
import { OGDialogContent, Button, TooltipAnchor, useToastContext } from '@librechat/client';
import {
QueryKeys,
Constants,
@ -119,24 +119,32 @@ const AgentDetailContent: React.FC<AgentDetailContentProps> = ({ agent }) => {
{/* Action button */}
<div className="mb-4 mt-6 flex justify-center gap-2">
<Button
variant="outline"
size="icon"
onClick={handleFavoriteClick}
title={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
>
{isFavorite ? <PinOff className="h-4 w-4" /> : <Pin className="h-4 w-4" />}
</Button>
<Button
variant="outline"
size="icon"
onClick={handleCopyLink}
title={localize('com_agents_copy_link')}
aria-label={localize('com_agents_copy_link')}
>
<Link className="h-4 w-4" aria-hidden="true" />
</Button>
<TooltipAnchor
description={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
render={
<Button
variant="outline"
size="icon"
onClick={handleFavoriteClick}
aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
>
{isFavorite ? <PinOff className="h-4 w-4" /> : <Pin className="h-4 w-4" />}
</Button>
}
/>
<TooltipAnchor
description={localize('com_agents_copy_link')}
render={
<Button
variant="outline"
size="icon"
onClick={handleCopyLink}
aria-label={localize('com_agents_copy_link')}
>
<Link className="h-4 w-4" aria-hidden="true" />
</Button>
}
/>
<Button
variant="submit"
className="w-full max-w-xs"

View file

@ -32,6 +32,7 @@ jest.mock(
Button: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button {...props}>{children}</button>
),
TooltipAnchor: ({ render }: { render: React.ReactNode }) => render,
useToastContext: () => ({
showToast: jest.fn(),
}),

View file

@ -283,18 +283,18 @@ export default function Settings({
</HoverCard>
<div className="w-full">
<div className="mb-2 flex w-full justify-between gap-2">
<label
<Label
htmlFor="resend-files"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<small>{localize('com_endpoint_plug_resend_files')}</small>
</label>
<label
</Label>
<Label
htmlFor="image-detail-value"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<small>{localize('com_endpoint_plug_image_detail')}</small>
</label>
</Label>
<Input
id="image-detail-value"
disabled={true}

View file

@ -1,5 +1,5 @@
import filenamify from 'filenamify';
import { useEffect, useState, useMemo, useCallback } from 'react';
import filenamify from 'filenamify';
import {
Input,
Label,
@ -126,7 +126,7 @@ export default function ExportModal({
onCheckedChange={setIncludeOptions}
aria-labelledby="includeOptions-label"
/>
<label
<Label
id="includeOptions-label"
htmlFor="includeOptions"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
@ -134,7 +134,7 @@ export default function ExportModal({
{exportOptionsSupport
? localize('com_nav_export_include_endpoint_options')
: localize('com_nav_not_supported')}
</label>
</Label>
</div>
</div>
</div>
@ -150,7 +150,7 @@ export default function ExportModal({
onCheckedChange={setExportBranches}
aria-labelledby="exportBranches-label"
/>
<label
<Label
id="exportBranches-label"
htmlFor="exportBranches"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
@ -158,7 +158,7 @@ export default function ExportModal({
{exportBranchesSupport
? localize('com_nav_export_all_message_branches')
: localize('com_nav_not_supported')}
</label>
</Label>
</div>
</div>
{type === 'json' ? (
@ -173,13 +173,13 @@ export default function ExportModal({
onCheckedChange={setRecursive}
aria-labelledby="recursive-label"
/>
<label
<Label
id="recursive-label"
htmlFor="recursive"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{localize('com_nav_export_recursive')}
</label>
</Label>
</div>
</div>
) : null}

View file

@ -1,9 +1,8 @@
import { useState, memo, useRef, useCallback, useId, useMemo } from 'react';
import * as Ariakit from '@ariakit/react';
import { useNavigate, useParams } from 'react-router-dom';
import { Ellipsis, Eye, SquarePen, Trash, EarthIcon, User } from 'lucide-react';
import { PermissionBits, ResourceType } from 'librechat-data-provider';
import type { TPromptGroup } from 'librechat-data-provider';
import { Ellipsis, Eye, SquarePen, Trash, EarthIcon, User } from 'lucide-react';
import {
Label,
Button,
@ -14,12 +13,13 @@ import {
OGDialogTemplate,
useToastContext,
} from '@librechat/client';
import type { TPromptGroup } from 'librechat-data-provider';
import { useLocalize, useAuthContext, useSubmitMessage, useResourcePermissions } from '~/hooks';
import { useRecordPromptUsage, useDeletePromptGroup } from '~/data-provider';
import { useLiveAnnouncer } from '~/Providers';
import VariableDialog from '../dialogs/VariableDialog';
import PreviewPrompt from '../dialogs/PreviewPrompt';
import CategoryIcon from '../utils/CategoryIcon';
import { useLiveAnnouncer } from '~/Providers';
import { detectVariables, cn } from '~/utils';
const PROMPT_PATH = '/prompts';
@ -157,9 +157,15 @@ function ChatGroupItem({
/>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<span className="truncate text-sm font-semibold text-text-primary" title={group.name}>
{group.name}
</span>
<TooltipAnchor
description={group.name}
side="top"
render={
<span className="truncate text-sm font-semibold text-text-primary">
{group.name}
</span>
}
/>
{isSharedPrompt && (
<TooltipAnchor
description={localize('com_ui_by_author', { 0: group.authorName })}

View file

@ -380,31 +380,35 @@ export default function GenericGrantAccessDialog({
<div className="flex justify-between pt-4">
<div className="flex gap-2">
{resourceId && resourceUrl && (
<Button
variant="outline"
onClick={() => {
if (isCopying) return;
copyResourceUrl(setIsCopying);
showToast({
message: localize('com_ui_agent_url_copied'),
status: 'success',
});
}}
disabled={isCopying}
className={cn('shrink-0', isCopying ? 'cursor-default' : '')}
aria-label={localize('com_ui_copy_url_to_clipboard')}
title={
<TooltipAnchor
description={
isCopying
? config?.getCopyUrlMessage()
: localize('com_ui_copy_url_to_clipboard')
}
>
{isCopying ? (
<CopyCheck className="h-4 w-4" aria-hidden="true" />
) : (
<Link className="h-4 w-4" aria-hidden="true" />
)}
</Button>
render={
<Button
variant="outline"
onClick={() => {
if (isCopying) return;
copyResourceUrl(setIsCopying);
showToast({
message: localize('com_ui_agent_url_copied'),
status: 'success',
});
}}
disabled={isCopying}
className={cn('shrink-0', isCopying ? 'cursor-default' : '')}
aria-label={localize('com_ui_copy_url_to_clipboard')}
>
{isCopying ? (
<CopyCheck className="h-4 w-4" aria-hidden="true" />
) : (
<Link className="h-4 w-4" aria-hidden="true" />
)}
</Button>
}
/>
)}
</div>
<div className="flex gap-2">

View file

@ -7,6 +7,7 @@ import {
Label,
Input,
Spinner,
Textarea,
useToastContext,
} from '@librechat/client';
import { useCreateMemoryMutation } from '~/data-provider';
@ -129,7 +130,7 @@ export default function MemoryCreateDialog({
<Label htmlFor="memory-value" className="text-sm font-medium text-text-primary">
{localize('com_ui_value')}
</Label>
<textarea
<Textarea
id="memory-value"
value={value}
onChange={(e) => setValue(e.target.value)}

View file

@ -7,6 +7,7 @@ import {
Label,
Input,
Spinner,
Textarea,
useToastContext,
} from '@librechat/client';
import type { TUserMemory } from 'librechat-data-provider';
@ -200,7 +201,7 @@ export default function MemoryEditDialog({
<Label htmlFor="memory-value" className="text-sm font-medium text-text-primary">
{localize('com_ui_value')}
</Label>
<textarea
<Textarea
id="memory-value"
value={value}
onChange={(e) => hasUpdateAccess && setValue(e.target.value)}

View file

@ -1464,6 +1464,7 @@
"com_ui_new_project": "New project",
"com_ui_next": "Next",
"com_ui_next_result": "Next result",
"com_ui_next_sibling": "Next sibling message",
"com_ui_no": "No",
"com_ui_no_auth": "None (Auto-detect)",
"com_ui_no_bookmarks": "it seems like you have no bookmarks yet. Click on a chat and add a new one",
@ -1487,6 +1488,7 @@
"com_ui_no_skills_found": "No skills found",
"com_ui_no_terms_content": "No terms and conditions content to display",
"com_ui_none": "None",
"com_ui_none_selected": "None selected",
"com_ui_not_used": "Not Used",
"com_ui_nothing_found": "Nothing found",
"com_ui_oauth": "OAuth",
@ -1541,6 +1543,7 @@
"com_ui_preview_failed": "Preview unavailable",
"com_ui_preview_preparing": "Preparing preview…",
"com_ui_preview_unavailable": "Preview not available for this file type",
"com_ui_previous_sibling": "Previous sibling message",
"com_ui_privacy_policy": "Privacy policy",
"com_ui_privacy_policy_url": "Privacy Policy URL",
"com_ui_pro": "Pro",
@ -1686,6 +1689,8 @@
"com_ui_saving": "Saving...",
"com_ui_schema": "Schema",
"com_ui_scope": "Scope",
"com_ui_scroll_left": "Scroll left",
"com_ui_scroll_right": "Scroll right",
"com_ui_scroll_to_bottom": "Scroll to bottom",
"com_ui_search": "Search",
"com_ui_search_above_to_add": "Search above to add users or groups",
@ -1717,6 +1722,7 @@
"com_ui_select_search_model": "Search model by name",
"com_ui_select_search_provider": "Search provider by name",
"com_ui_select_search_region": "Search region by name",
"com_ui_select_var": "Select {{0}}",
"com_ui_send_now": "Send now",
"com_ui_set": "Set",
"com_ui_settings_label_2fa": "Two-factor authentication",
@ -1819,6 +1825,7 @@
"com_ui_show_more": "Show more",
"com_ui_show_n_files": "Show {{0}} files",
"com_ui_show_qr": "Show QR Code",
"com_ui_sibling_navigation": "Sibling message navigation",
"com_ui_sign_in_to_domain": "Sign-in to {{0}}",
"com_ui_size": "Size",
"com_ui_size_sort": "Sort by Size",