mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-16 15:38:10 +01:00
♿ style(MCP): Enhance dialog accessibility and styling consistency (#11585)
* style: update input IDs in BasicInfoSection for consistency and improve accessibility * style: add border-destructive variable for improved design consistency * style: update error border color for title input in BasicInfoSection * style: update delete confirmation dialog title and description for MCP Server * style: add text-destructive variable for improved design consistency * style: update error message and border color for URL and trust fields for consistency * style: reorder imports and update error message styling for consistency across sections * style: enhance MCPServerDialog with copy link functionality and UI improvements * style: enhance MCPServerDialog with improved accessibility and loading indicators * style: bump @librechat/client to 0.4.51 and enhance OGDialogTemplate for improved selection handling * a11y: enhance accessibility and error handling in MCPServerDialog sections * style: enhance MCPServerDialog accessibility and improve resource name handling * style: improve accessibility in MCPServerDialog and AuthSection, update translation for delete confirmation * style: update aria-invalid attributes to use string values for improved accessibility in form sections * style: enhance accessibility in AuthSection by updating aria attributes and adding error messages * style: remove unnecessary aria-hidden attributes from Spinner components in MCPServerDialog * style: simplify legacy selection check in OGDialogTemplate
This commit is contained in:
parent
299efc2ccb
commit
d6b6f191f7
13 changed files with 295 additions and 141 deletions
|
|
@ -1,10 +1,10 @@
|
|||
import { FormProvider } from 'react-hook-form';
|
||||
import type { useMCPServerForm } from './hooks/useMCPServerForm';
|
||||
import ConnectionSection from './sections/ConnectionSection';
|
||||
import BasicInfoSection from './sections/BasicInfoSection';
|
||||
import TransportSection from './sections/TransportSection';
|
||||
import AuthSection from './sections/AuthSection';
|
||||
import TrustSection from './sections/TrustSection';
|
||||
import type { useMCPServerForm } from './hooks/useMCPServerForm';
|
||||
import AuthSection from './sections/AuthSection';
|
||||
|
||||
interface MCPServerFormProps {
|
||||
formHook: ReturnType<typeof useMCPServerForm>;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Copy, CopyCheck } from 'lucide-react';
|
||||
import {
|
||||
OGDialog,
|
||||
OGDialogTemplate,
|
||||
OGDialogContent,
|
||||
OGDialogHeader,
|
||||
OGDialogTitle,
|
||||
Label,
|
||||
Input,
|
||||
Button,
|
||||
TrashIcon,
|
||||
Spinner,
|
||||
TrashIcon,
|
||||
useToastContext,
|
||||
OGDialog,
|
||||
OGDialogTitle,
|
||||
OGDialogHeader,
|
||||
OGDialogFooter,
|
||||
OGDialogContent,
|
||||
OGDialogTemplate,
|
||||
} from '@librechat/client';
|
||||
import {
|
||||
SystemRoles,
|
||||
|
|
@ -16,10 +21,10 @@ import {
|
|||
PermissionBits,
|
||||
PermissionTypes,
|
||||
} from 'librechat-data-provider';
|
||||
import { GenericGrantAccessDialog } from '~/components/Sharing';
|
||||
import { useAuthContext, useHasAccess, useResourcePermissions, MCPServerDefinition } from '~/hooks';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { GenericGrantAccessDialog } from '~/components/Sharing';
|
||||
import { useMCPServerForm } from './hooks/useMCPServerForm';
|
||||
import { useLocalize, useCopyToClipboard } from '~/hooks';
|
||||
import MCPServerForm from './MCPServerForm';
|
||||
|
||||
interface MCPServerDialogProps {
|
||||
|
|
@ -39,8 +44,10 @@ export default function MCPServerDialog({
|
|||
}: MCPServerDialogProps) {
|
||||
const localize = useLocalize();
|
||||
const { user } = useAuthContext();
|
||||
const { showToast } = useToastContext();
|
||||
|
||||
// State for dialogs
|
||||
const [isCopying, setIsCopying] = useState(false);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [showRedirectUriDialog, setShowRedirectUriDialog] = useState(false);
|
||||
const [createdServerId, setCreatedServerId] = useState<string | null>(null);
|
||||
|
|
@ -99,20 +106,26 @@ export default function MCPServerDialog({
|
|||
? `${window.location.origin}/api/mcp/${createdServerId}/oauth/callback`
|
||||
: '';
|
||||
|
||||
const copyLink = useCopyToClipboard({ text: redirectUri });
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Delete confirmation dialog */}
|
||||
<OGDialog open={showDeleteConfirm} onOpenChange={(isOpen) => setShowDeleteConfirm(isOpen)}>
|
||||
<OGDialogTemplate
|
||||
title={localize('com_ui_delete')}
|
||||
className="max-w-[450px]"
|
||||
main={<p className="text-left text-sm">{localize('com_ui_mcp_server_delete_confirm')}</p>}
|
||||
selection={{
|
||||
selectHandler: handleDelete,
|
||||
selectClasses:
|
||||
'bg-destructive text-white transition-all duration-200 hover:bg-destructive/80',
|
||||
selectText: isDeleting ? <Spinner /> : localize('com_ui_delete'),
|
||||
}}
|
||||
title={localize('com_ui_delete_mcp_server')}
|
||||
className="w-11/12 max-w-md"
|
||||
description={localize('com_ui_mcp_server_delete_confirm', { 0: server?.serverName })}
|
||||
selection={
|
||||
<Button
|
||||
onClick={handleDelete}
|
||||
variant="destructive"
|
||||
aria-live="polite"
|
||||
aria-label={isDeleting ? localize('com_ui_deleting') : localize('com_ui_delete')}
|
||||
>
|
||||
{isDeleting ? <Spinner /> : localize('com_ui_delete')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</OGDialog>
|
||||
|
||||
|
|
@ -127,48 +140,53 @@ export default function MCPServerDialog({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<OGDialogContent className="w-full max-w-lg border-none bg-surface-primary text-text-primary">
|
||||
<OGDialogHeader className="border-b border-border-light px-4 py-3">
|
||||
<OGDialogContent showCloseButton={false} className="w-11/12 max-w-lg">
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle>{localize('com_ui_mcp_server_created')}</OGDialogTitle>
|
||||
</OGDialogHeader>
|
||||
<div className="space-y-4 p-4">
|
||||
<p className="text-sm text-text-secondary">
|
||||
{localize('com_ui_redirect_uri_instructions')}
|
||||
</p>
|
||||
<div className="rounded-lg border border-border-medium bg-surface-secondary p-3">
|
||||
<label className="mb-2 block text-xs font-medium text-text-secondary">
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm">{localize('com_ui_redirect_uri_instructions')}</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="redirect-uri-input" className="text-sm font-medium">
|
||||
{localize('com_ui_redirect_uri')}
|
||||
</label>
|
||||
</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
className="flex-1 rounded border border-border-medium bg-surface-primary px-3 py-2 text-sm"
|
||||
value={redirectUri}
|
||||
<Input
|
||||
id="redirect-uri-input"
|
||||
type="text"
|
||||
readOnly
|
||||
value={redirectUri}
|
||||
className="flex-1 text-text-secondary"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(redirectUri);
|
||||
}}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="whitespace-nowrap"
|
||||
onClick={() => {
|
||||
if (isCopying) return;
|
||||
showToast({ message: localize('com_ui_copied_to_clipboard') });
|
||||
copyLink(setIsCopying);
|
||||
}}
|
||||
disabled={isCopying}
|
||||
className="p-0"
|
||||
aria-label={localize('com_ui_copy_link')}
|
||||
>
|
||||
{localize('com_ui_copy_link')}
|
||||
{isCopying ? <CopyCheck className="size-4" /> : <Copy className="size-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<OGDialogFooter>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setShowRedirectUriDialog(false);
|
||||
onOpenChange(false);
|
||||
setCreatedServerId(null);
|
||||
}}
|
||||
variant="submit"
|
||||
className="text-white"
|
||||
>
|
||||
{localize('com_ui_done')}
|
||||
</Button>
|
||||
</div>
|
||||
</OGDialogFooter>
|
||||
</div>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
|
|
@ -187,6 +205,7 @@ export default function MCPServerDialog({
|
|||
})
|
||||
: undefined
|
||||
}
|
||||
showCloseButton={false}
|
||||
className="w-11/12 md:max-w-3xl"
|
||||
main={<MCPServerForm formHook={formHook} />}
|
||||
footerClassName="sm:justify-between"
|
||||
|
|
@ -194,16 +213,15 @@ export default function MCPServerDialog({
|
|||
isEditMode ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
aria-label={localize('com_ui_delete')}
|
||||
aria-label={localize('com_ui_delete_mcp_server_name', {
|
||||
0: server?.config?.title || server?.serverName || '',
|
||||
})}
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
disabled={isSubmitting || isDeleting}
|
||||
>
|
||||
<div className="flex w-full items-center justify-center gap-2 text-red-500">
|
||||
<TrashIcon />
|
||||
</div>
|
||||
<TrashIcon aria-hidden="true" />
|
||||
</Button>
|
||||
{shouldShowShareButton && server && (
|
||||
<GenericGrantAccessDialog
|
||||
|
|
@ -218,10 +236,15 @@ export default function MCPServerDialog({
|
|||
buttons={
|
||||
<Button
|
||||
type="button"
|
||||
variant="submit"
|
||||
variant={isEditMode ? 'default' : 'submit'}
|
||||
onClick={onSubmit}
|
||||
disabled={isSubmitting}
|
||||
className="text-white"
|
||||
aria-live="polite"
|
||||
aria-label={
|
||||
isSubmitting
|
||||
? localize(isEditMode ? 'com_ui_updating' : 'com_ui_creating')
|
||||
: localize(isEditMode ? 'com_ui_update_mcp_server' : 'com_ui_create_mcp_server')
|
||||
}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Spinner className="size-4" />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { Copy, CopyCheck } from 'lucide-react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { Label, Input, Checkbox, SecretInput, Radio, useToastContext } from '@librechat/client';
|
||||
import { Copy, CopyCheck } from 'lucide-react';
|
||||
import { useLocalize, useCopyToClipboard } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import { AuthTypeEnum, AuthorizationTypeEnum } from '../hooks/useMCPServerForm';
|
||||
import type { MCPServerFormData } from '../hooks/useMCPServerForm';
|
||||
import { useLocalize, useCopyToClipboard } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface AuthSectionProps {
|
||||
isEditMode: boolean;
|
||||
|
|
@ -62,15 +62,20 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
|||
return (
|
||||
<div className="space-y-3">
|
||||
{/* Auth Type Radio */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm font-medium">{localize('com_ui_authentication')}</Label>
|
||||
<fieldset className="space-y-1.5">
|
||||
<legend>
|
||||
<Label id="auth-type-label" className="text-sm font-medium">
|
||||
{localize('com_ui_authentication')}
|
||||
</Label>
|
||||
</legend>
|
||||
<Radio
|
||||
options={authTypeOptions}
|
||||
value={authType || AuthTypeEnum.None}
|
||||
onChange={(val) => setValue('auth.auth_type', val as AuthTypeEnum)}
|
||||
fullWidth
|
||||
aria-labelledby="auth-type-label"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{/* API Key Fields */}
|
||||
{authType === AuthTypeEnum.ServiceHttp && (
|
||||
|
|
@ -83,9 +88,13 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
|||
onCheckedChange={(checked) =>
|
||||
setValue('auth.api_key_source', checked ? 'user' : 'admin')
|
||||
}
|
||||
aria-label={localize('com_ui_user_provides_key')}
|
||||
aria-labelledby="user_provides_key_label"
|
||||
/>
|
||||
<label htmlFor="user_provides_key" className="cursor-pointer text-sm">
|
||||
<label
|
||||
id="user_provides_key_label"
|
||||
htmlFor="user_provides_key"
|
||||
className="cursor-pointer text-sm"
|
||||
>
|
||||
{localize('com_ui_user_provides_key')}
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -101,8 +110,12 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
|||
)}
|
||||
|
||||
{/* Header Format Radio */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm font-medium">{localize('com_ui_header_format')}</Label>
|
||||
<fieldset className="space-y-1.5">
|
||||
<legend>
|
||||
<Label id="header-format-label" className="text-sm font-medium">
|
||||
{localize('com_ui_header_format')}
|
||||
</Label>
|
||||
</legend>
|
||||
<Radio
|
||||
options={headerFormatOptions}
|
||||
value={authorizationType || AuthorizationTypeEnum.Bearer}
|
||||
|
|
@ -110,8 +123,9 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
|||
setValue('auth.api_key_authorization_type', val as AuthorizationTypeEnum)
|
||||
}
|
||||
fullWidth
|
||||
aria-labelledby="header-format-label"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{/* Custom header name */}
|
||||
{authorizationType === AuthorizationTypeEnum.Custom && (
|
||||
|
|
@ -137,27 +151,67 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
|||
<div className="space-y-1.5">
|
||||
<Label htmlFor="oauth_client_id" className="text-sm font-medium">
|
||||
{localize('com_ui_client_id')}{' '}
|
||||
{!isEditMode && <span className="text-text-secondary">*</span>}
|
||||
{!isEditMode && (
|
||||
<>
|
||||
<span aria-hidden="true" className="text-text-secondary">
|
||||
*
|
||||
</span>
|
||||
<span className="sr-only">{localize('com_ui_field_required')}</span>
|
||||
</>
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
id="oauth_client_id"
|
||||
autoComplete="off"
|
||||
placeholder={isEditMode ? localize('com_ui_leave_blank_to_keep') : ''}
|
||||
aria-invalid={errors.auth?.oauth_client_id ? 'true' : 'false'}
|
||||
aria-describedby={
|
||||
errors.auth?.oauth_client_id ? 'oauth-client-id-error' : undefined
|
||||
}
|
||||
{...register('auth.oauth_client_id', { required: !isEditMode })}
|
||||
className={cn(errors.auth?.oauth_client_id && 'border-red-500')}
|
||||
className={cn(errors.auth?.oauth_client_id && 'border-border-destructive')}
|
||||
/>
|
||||
{errors.auth?.oauth_client_id && (
|
||||
<p
|
||||
id="oauth-client-id-error"
|
||||
role="alert"
|
||||
className="text-xs text-text-destructive"
|
||||
>
|
||||
{localize('com_ui_field_required')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="oauth_client_secret" className="text-sm font-medium">
|
||||
{localize('com_ui_client_secret')}{' '}
|
||||
{!isEditMode && <span className="text-text-secondary">*</span>}
|
||||
{!isEditMode && (
|
||||
<>
|
||||
<span aria-hidden="true" className="text-text-secondary">
|
||||
*
|
||||
</span>
|
||||
<span className="sr-only">{localize('com_ui_field_required')}</span>
|
||||
</>
|
||||
)}
|
||||
</Label>
|
||||
<SecretInput
|
||||
id="oauth_client_secret"
|
||||
placeholder={isEditMode ? localize('com_ui_leave_blank_to_keep') : ''}
|
||||
aria-invalid={errors.auth?.oauth_client_secret ? 'true' : 'false'}
|
||||
aria-describedby={
|
||||
errors.auth?.oauth_client_secret ? 'oauth-client-secret-error' : undefined
|
||||
}
|
||||
{...register('auth.oauth_client_secret', { required: !isEditMode })}
|
||||
className={cn(errors.auth?.oauth_client_secret && 'border-red-500')}
|
||||
className={cn(errors.auth?.oauth_client_secret && 'border-border-destructive')}
|
||||
/>
|
||||
{errors.auth?.oauth_client_secret && (
|
||||
<p
|
||||
id="oauth-client-secret-error"
|
||||
role="alert"
|
||||
className="text-xs text-text-destructive"
|
||||
>
|
||||
{localize('com_ui_field_required')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -196,9 +250,12 @@ export default function AuthSection({ isEditMode, serverName }: AuthSectionProps
|
|||
{/* Redirect URI */}
|
||||
{isEditMode && redirectUri && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm font-medium">{localize('com_ui_redirect_uri')}</Label>
|
||||
<Label htmlFor="auth-redirect-uri" className="text-sm font-medium">
|
||||
{localize('com_ui_redirect_uri')}
|
||||
</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="auth-redirect-uri"
|
||||
type="text"
|
||||
readOnly
|
||||
value={redirectUri}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useFormContext } from 'react-hook-form';
|
||||
import { Input, Label, TextareaAutosize } from '@librechat/client';
|
||||
import { Input, Label, Textarea } from '@librechat/client';
|
||||
import type { MCPServerFormData } from '../hooks/useMCPServerForm';
|
||||
import MCPIcon from '~/components/SidePanel/Agents/MCPIcon';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import MCPIcon from '~/components/SidePanel/Agents/MCPIcon';
|
||||
import type { MCPServerFormData } from '../hooks/useMCPServerForm';
|
||||
|
||||
export default function BasicInfoSection() {
|
||||
const localize = useLocalize();
|
||||
|
|
@ -36,13 +36,19 @@ export default function BasicInfoSection() {
|
|||
<MCPIcon icon={iconValue} onIconChange={handleIconChange} />
|
||||
</div>
|
||||
<div className="w-full space-y-1.5 sm:flex-1">
|
||||
<Label htmlFor="title" className="text-sm font-medium">
|
||||
{localize('com_ui_name')} <span className="text-text-secondary">*</span>
|
||||
<Label htmlFor="mcp-title" className="text-sm font-medium">
|
||||
{localize('com_ui_name')}{' '}
|
||||
<span aria-hidden="true" className="text-text-secondary">
|
||||
*
|
||||
</span>
|
||||
<span className="sr-only">{localize('com_ui_field_required')}</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="title"
|
||||
id="mcp-title"
|
||||
autoComplete="off"
|
||||
placeholder={localize('com_agents_mcp_name_placeholder')}
|
||||
aria-invalid={errors.title ? 'true' : 'false'}
|
||||
aria-describedby={errors.title ? 'mcp-title-error' : undefined}
|
||||
{...register('title', {
|
||||
required: localize('com_ui_field_required'),
|
||||
pattern: {
|
||||
|
|
@ -50,26 +56,26 @@ export default function BasicInfoSection() {
|
|||
message: localize('com_ui_mcp_title_invalid'),
|
||||
},
|
||||
})}
|
||||
className={cn(errors.title && 'border-red-500 focus:border-red-500')}
|
||||
className={cn(errors.title && 'border-border-destructive')}
|
||||
/>
|
||||
{errors.title && <p className="text-xs text-red-500">{errors.title.message}</p>}
|
||||
{errors.title && (
|
||||
<p id="mcp-title-error" role="alert" className="text-xs text-text-destructive">
|
||||
{errors.title.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description - always visible, full width */}
|
||||
{/* Description */}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="description" className="text-sm font-medium">
|
||||
<Label htmlFor="mcp-description" className="text-sm font-medium">
|
||||
{localize('com_ui_description')}{' '}
|
||||
<span className="text-xs text-text-secondary">{localize('com_ui_optional')}</span>
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="description"
|
||||
aria-label={localize('com_ui_description')}
|
||||
<Textarea
|
||||
id="mcp-description"
|
||||
placeholder={localize('com_agents_mcp_description_placeholder')}
|
||||
{...register('description')}
|
||||
minRows={2}
|
||||
maxRows={4}
|
||||
className="w-full resize-none rounded-lg border border-input bg-transparent px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,13 +15,19 @@ export default function ConnectionSection() {
|
|||
return (
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="url" className="text-sm font-medium">
|
||||
{localize('com_ui_mcp_url')} <span className="text-text-secondary">*</span>
|
||||
{localize('com_ui_mcp_url')}{' '}
|
||||
<span aria-hidden="true" className="text-text-secondary">
|
||||
*
|
||||
</span>
|
||||
<span className="sr-only">{localize('com_ui_field_required')}</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="url"
|
||||
type="url"
|
||||
autoComplete="off"
|
||||
placeholder={localize('com_ui_mcp_server_url_placeholder')}
|
||||
aria-invalid={errors.url ? 'true' : 'false'}
|
||||
aria-describedby={errors.url ? 'url-error' : undefined}
|
||||
{...register('url', {
|
||||
required: localize('com_ui_field_required'),
|
||||
validate: (value) => {
|
||||
|
|
@ -29,9 +35,13 @@ export default function ConnectionSection() {
|
|||
return isValidUrl(normalized) || localize('com_ui_mcp_invalid_url');
|
||||
},
|
||||
})}
|
||||
className={cn(errors.url && 'border-red-500 focus:border-red-500')}
|
||||
className={cn(errors.url && 'border-border-destructive')}
|
||||
/>
|
||||
{errors.url && <p className="text-xs text-red-500">{errors.url.message}</p>}
|
||||
{errors.url && (
|
||||
<p id="url-error" role="alert" className="text-xs text-text-destructive">
|
||||
{errors.url.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,19 @@ export default function TransportSection() {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">{localize('com_ui_mcp_transport')}</Label>
|
||||
<fieldset className="space-y-2">
|
||||
<legend>
|
||||
<Label id="transport-label" className="text-sm font-medium">
|
||||
{localize('com_ui_mcp_transport')}
|
||||
</Label>
|
||||
</legend>
|
||||
<Radio
|
||||
options={transportOptions}
|
||||
value={transportType}
|
||||
onChange={handleTransportChange}
|
||||
fullWidth
|
||||
aria-labelledby="transport-label"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,17 +26,17 @@ export default function TrustSection() {
|
|||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
aria-labelledby="trust-label"
|
||||
aria-describedby="trust-description"
|
||||
aria-describedby={
|
||||
errors.trust ? 'trust-description trust-error' : 'trust-description'
|
||||
}
|
||||
aria-invalid={errors.trust ? 'true' : 'false'}
|
||||
aria-required="true"
|
||||
className="mt-0.5"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Label
|
||||
id="trust-label"
|
||||
htmlFor="trust"
|
||||
className="flex cursor-pointer flex-col gap-0.5 text-sm"
|
||||
>
|
||||
<span className="font-medium text-text-primary">
|
||||
<Label htmlFor="trust" className="flex cursor-pointer flex-col gap-0.5 text-sm">
|
||||
<span id="trust-label" className="font-medium text-text-primary">
|
||||
{startupConfig?.interface?.mcpServers?.trustCheckbox?.label ? (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
|
|
@ -49,7 +49,9 @@ export default function TrustSection() {
|
|||
) : (
|
||||
localize('com_ui_trust_app')
|
||||
)}{' '}
|
||||
<span className="text-text-secondary">*</span>
|
||||
<span aria-hidden="true" className="text-text-secondary">
|
||||
*
|
||||
</span>
|
||||
</span>
|
||||
<span id="trust-description" className="text-xs font-normal text-text-secondary">
|
||||
{startupConfig?.interface?.mcpServers?.trustCheckbox?.subLabel ? (
|
||||
|
|
@ -68,7 +70,9 @@ export default function TrustSection() {
|
|||
</Label>
|
||||
</div>
|
||||
{errors.trust && (
|
||||
<p className="mt-2 text-xs text-red-500">{localize('com_ui_field_required')}</p>
|
||||
<p id="trust-error" role="alert" className="mt-2 text-xs text-text-destructive">
|
||||
{localize('com_ui_field_required')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue