style: enhance UI components and improve layout consistency across various dialogs

This commit is contained in:
Marco Beretta 2026-01-07 23:45:10 +01:00
parent e5e5663ca9
commit 0fe503e12a
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
16 changed files with 180 additions and 186 deletions

View file

@ -11,6 +11,7 @@ import {
Spinner,
OGDialog,
Dropdown,
OGDialogClose,
OGDialogTitle,
OGDialogHeader,
OGDialogFooter,
@ -104,43 +105,41 @@ const RevokeKeysButton = ({
const isLoading = revokeKeyMutation.isLoading || revokeKeysMutation.isLoading;
return (
<div className="flex items-center justify-between">
<OGDialog open={open} onOpenChange={setOpen}>
<OGDialogTrigger asChild>
<OGDialog open={open} onOpenChange={setOpen}>
<OGDialogTrigger asChild>
<Button
variant="destructive"
className="flex w-full items-center justify-center rounded-lg transition-colors duration-200"
onClick={() => setOpen(true)}
disabled={disabled}
>
{localize('com_ui_revoke')}
</Button>
</OGDialogTrigger>
<OGDialogContent className="max-w-[450px]">
<OGDialogHeader>
<OGDialogTitle>{localize('com_ui_revoke_key_endpoint', { 0: endpoint })}</OGDialogTitle>
</OGDialogHeader>
<div className="py-4">
<Label className="text-left text-sm font-medium">
{localize('com_ui_revoke_key_confirm')}
</Label>
</div>
<OGDialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
{localize('com_ui_cancel')}
</Button>
<Button
variant="destructive"
className="flex items-center justify-center rounded-lg transition-colors duration-200"
onClick={() => setOpen(true)}
disabled={disabled}
onClick={onClick}
disabled={isLoading}
className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80"
>
{localize('com_ui_revoke')}
{isLoading ? <Spinner /> : localize('com_ui_revoke')}
</Button>
</OGDialogTrigger>
<OGDialogContent className="max-w-[450px]">
<OGDialogHeader>
<OGDialogTitle>{localize('com_ui_revoke_key_endpoint', { 0: endpoint })}</OGDialogTitle>
</OGDialogHeader>
<div className="py-4">
<Label className="text-left text-sm font-medium">
{localize('com_ui_revoke_key_confirm')}
</Label>
</div>
<OGDialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
{localize('com_ui_cancel')}
</Button>
<Button
variant="destructive"
onClick={onClick}
disabled={isLoading}
className="bg-destructive text-white transition-all duration-200 hover:bg-destructive/80"
>
{isLoading ? <Spinner /> : localize('com_ui_revoke')}
</Button>
</OGDialogFooter>
</OGDialogContent>
</OGDialog>
</div>
</OGDialogFooter>
</OGDialogContent>
</OGDialog>
);
};
@ -276,7 +275,7 @@ const SetKeyDialog = ({
return (
<OGDialog open={open} onOpenChange={onOpenChange}>
<OGDialogContent className="w-11/12 max-w-2xl">
<OGDialogContent className="w-11/12 max-w-2xl" showCloseButton={false}>
<OGDialogHeader>
<OGDialogTitle>
{`${localize('com_endpoint_config_key_for')} ${alternateName[endpoint] ?? endpoint}`}
@ -310,11 +309,16 @@ const SetKeyDialog = ({
<HelpText endpoint={endpoint} />
</div>
<OGDialogFooter>
<RevokeKeysButton
endpoint={endpoint}
disabled={!(expiryTime ?? '')}
setDialogOpen={onOpenChange}
/>
<OGDialogClose asChild>
<Button variant="outline">{localize('com_ui_cancel')}</Button>
</OGDialogClose>
{expiryTime && (
<RevokeKeysButton
endpoint={endpoint}
disabled={!expiryTime}
setDialogOpen={onOpenChange}
/>
)}
<Button variant="submit" onClick={submit}>
{localize('com_ui_submit')}
</Button>

View file

@ -4,7 +4,6 @@ import { Button, Spinner } from '@librechat/client';
import { useLocalize, useMCPServerManager, useMCPConnectionStatus } from '~/hooks';
interface ServerInitializationSectionProps {
sidePanel?: boolean;
serverName: string;
requiresOAuth: boolean;
hasCustomUserVars?: boolean;
@ -15,7 +14,6 @@ export default function ServerInitializationSection({
serverName,
requiresOAuth,
conversationId,
sidePanel = false,
hasCustomUserVars = false,
}: ServerInitializationSectionProps) {
const localize = useLocalize();
@ -106,10 +104,10 @@ export default function ServerInitializationSection({
</Button>
)}
<Button
size="sm"
variant={buttonVariant}
onClick={() => initializeServer(serverName, false)}
disabled={isServerInitializing}
size={sidePanel ? 'sm' : 'default'}
className="flex-1"
>
{icon}

View file

@ -60,7 +60,7 @@ const AdminSettings = () => {
}}
>
<OGDialogTemplate
showCloseButton={true}
showCloseButton={false}
title={localize('com_ui_confirm_change')}
className="w-11/12 max-w-lg"
main={<p className="mb-4">{localize('com_ui_confirm_admin_use_change')}</p>}

View file

@ -9,6 +9,7 @@ import {
OGDialog,
OGDialogTitle,
OGDialogClose,
OGDialogFooter,
OGDialogContent,
OGDialogTrigger,
useToastContext,
@ -277,7 +278,7 @@ export default function GenericGrantAccessDialog({
</div>
</OGDialogTitle>
<div className="space-y-6 p-2">
<div className="space-y-6 py-2">
{/* Unified Search and Management Section */}
<div className="space-y-4">
{/* Search Bar with Default Permission Setting */}
@ -359,68 +360,53 @@ export default function GenericGrantAccessDialog({
)}
{/* Footer Actions */}
<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={
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>
)}
</div>
<div className="flex gap-2">
<PeoplePickerAdminSettings />
<OGDialogClose asChild>
<Button
variant="outline"
onClick={handleCancel}
aria-label={localize('com_ui_cancel')}
>
{localize('com_ui_cancel')}
</Button>
</OGDialogClose>
<OGDialogFooter className="pt-4">
{resourceId && resourceUrl && (
<Button
onClick={handleSave}
disabled={
updatePermissionsMutation.isLoading ||
!submitButtonActive ||
(hasChanges && !hasAtLeastOneOwner)
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={
isCopying ? config?.getCopyUrlMessage() : localize('com_ui_copy_url_to_clipboard')
}
className="min-w-[120px]"
aria-label={localize('com_ui_save_changes')}
>
{updatePermissionsMutation.isLoading ? (
<div className="flex items-center gap-2">
<Spinner className="h-4 w-4" />
{localize('com_ui_saving')}
</div>
{isCopying ? (
<CopyCheck className="h-4 w-4" aria-hidden="true" />
) : (
localize('com_ui_save_changes')
<Link className="h-4 w-4" aria-hidden="true" />
)}
</Button>
</div>
</div>
)}
<PeoplePickerAdminSettings />
<Button
onClick={handleSave}
disabled={
updatePermissionsMutation.isLoading ||
!submitButtonActive ||
(hasChanges && !hasAtLeastOneOwner)
}
className="min-w-[120px]"
aria-label={localize('com_ui_save_changes')}
>
{updatePermissionsMutation.isLoading ? (
<div className="flex items-center gap-2">
<Spinner className="h-4 w-4" />
{localize('com_ui_saving')}
</div>
) : (
localize('com_ui_save_changes')
)}
</Button>
</OGDialogFooter>
</div>
</OGDialogContent>
</OGDialog>

View file

@ -180,9 +180,11 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
)}
>
<div className="flex flex-col items-center gap-2">
<Search className="h-8 w-8 text-text-tertiary opacity-50" />
<div className="font-medium">{localize('com_ui_no_results_found')}</div>
<div className="text-xs text-text-tertiary">
<Search className="h-8 w-8 text-text-primary opacity-50" />
<div className="font-medium text-text-primary">
{localize('com_ui_no_results_found')}
</div>
<div className="text-xs text-text-secondary">
{localize('com_ui_try_adjusting_search')}
</div>
</div>

View file

@ -42,12 +42,12 @@ export default function MCPIcon({ icon, onIconChange }: MCPIconProps) {
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 cursor-pointer items-center justify-center rounded-full border-2 border-dashed border-border-heavy focus:outline-none focus-visible:ring-2 focus-visible:ring-border-heavy"
>
{previewUrl ? (
<img
src={previewUrl}
className="h-full w-full rounded-xl object-cover"
className="h-full w-full rounded-full object-cover"
alt="MCP Icon"
width="64"
height="64"

View file

@ -1,13 +1,17 @@
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,
OGDialogContent,
OGDialogTemplate,
} from '@librechat/client';
import {
SystemRoles,
@ -16,10 +20,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 +43,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,6 +105,8 @@ export default function MCPServerDialog({
? `${window.location.origin}/api/mcp/${createdServerId}/oauth/callback`
: '';
const copyLink = useCopyToClipboard({ text: redirectUri });
return (
<>
{/* Delete confirmation dialog */}
@ -127,48 +135,50 @@ 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">
{localize('com_ui_redirect_uri')}
</label>
<div className="space-y-4">
<Label className="text-sm">{localize('com_ui_redirect_uri_instructions')}</Label>
<div className="space-y-2">
<Label className="text-sm font-medium">{localize('com_ui_redirect_uri')}</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
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">
<OGDialogHeader>
<Button
variant="default"
onClick={() => {
setShowRedirectUriDialog(false);
onOpenChange(false);
setCreatedServerId(null);
}}
variant="submit"
className="text-white"
>
{localize('com_ui_done')}
</Button>
</div>
</OGDialogHeader>
</div>
</OGDialogContent>
</OGDialog>
@ -187,6 +197,7 @@ export default function MCPServerDialog({
})
: undefined
}
showCloseButton={false}
className="w-11/12 md:max-w-3xl"
main={<MCPServerForm formHook={formHook} />}
footerClassName="sm:justify-between"
@ -218,10 +229,9 @@ export default function MCPServerDialog({
buttons={
<Button
type="button"
variant="submit"
variant={isEditMode ? 'default' : 'submit'}
onClick={onSubmit}
disabled={isSubmitting}
className="text-white"
>
{isSubmitting ? (
<Spinner className="size-4" />

View file

@ -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;

View file

@ -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();
@ -50,26 +50,23 @@ 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-text-destructive')}
/>
{errors.title && <p className="text-xs text-red-500">{errors.title.message}</p>}
{errors.title && <p 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">
{localize('com_ui_description')}{' '}
<span className="text-xs text-text-secondary">{localize('com_ui_optional')}</span>
</Label>
<TextareaAutosize
<Textarea
id="description"
aria-label={localize('com_ui_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>

View file

@ -918,7 +918,7 @@
"com_ui_edit": "Edit",
"com_ui_edit_editing_image": "Editing image",
"com_ui_edit_mcp_server": "Edit MCP Server",
"com_ui_edit_mcp_server_dialog_description": "Unique Server Identifier: {{serverName}}",
"com_ui_edit_mcp_server_dialog_description": "Server ID: {{serverName}}",
"com_ui_edit_memory": "Edit Memory",
"com_ui_edit_preset_title": "Edit Preset - {{title}}",
"com_ui_edit_prompt_page": "Edit Prompt Page",

View file

@ -70,6 +70,7 @@ html {
--text-secondary-alt: var(--gray-500);
--text-tertiary: var(--gray-500);
--text-warning: var(--amber-500);
--text-destructive: var(--red-600);
--ring-primary: var(--gray-500);
--header-primary: var(--white);
--header-hover: var(--gray-50);
@ -131,6 +132,7 @@ html {
--text-secondary-alt: var(--gray-400);
--text-tertiary: var(--gray-500);
--text-warning: var(--amber-500);
--text-destructive: var(--red-600);
--header-primary: var(--gray-700);
--header-hover: var(--gray-600);
--header-button-hover: var(--gray-700);

View file

@ -92,6 +92,7 @@ module.exports = {
'text-secondary-alt': 'var(--text-secondary-alt)',
'text-tertiary': 'var(--text-tertiary)',
'text-warning': 'var(--text-warning)',
'text-destructive': 'var(--text-destructive)',
'ring-primary': 'var(--ring-primary)',
'header-primary': 'var(--header-primary)',
'header-hover': 'var(--header-hover)',

View file

@ -13,7 +13,7 @@ const Label = React.forwardRef<
{...props}
{...{
className: cn(
'block w-full break-all text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 dark:text-gray-200',
'block w-full text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-text-primary',
className,
),
}}

View file

@ -75,38 +75,32 @@ const OGDialogTemplate = forwardRef((props: DialogTemplateProps, ref: Ref<HTMLDi
</OGDialogHeader>
<div className={cn('px-0 py-2', mainClassName)}>{main != null ? main : null}</div>
<OGDialogFooter className={footerClassName}>
<div>
{leftButtons != null ? (
<div className="mt-3 flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:mt-0 sm:flex-row">
{leftButtons}
</div>
) : null}
</div>
<div className="flex h-auto gap-3 max-sm:w-full max-sm:flex-col sm:flex-row">
{showCancelButton && (
<OGDialogClose asChild>
<Button variant="outline" aria-label={localize('com_ui_cancel')}>
{localize('com_ui_cancel')}
</Button>
</OGDialogClose>
)}
{buttons != null ? buttons : null}
{selection ? (
<OGDialogClose
onClick={selectHandler}
disabled={isLoading}
className={`${
selectClasses ?? defaultSelect
} flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm disabled:opacity-80 max-sm:order-first max-sm:w-full sm:order-none`}
>
{isLoading === true ? (
<Spinner className="size-4 text-white" />
) : (
(selectText as React.JSX.Element)
)}
</OGDialogClose>
) : null}
</div>
{leftButtons != null ? (
<div className="mr-auto flex flex-row gap-2">{leftButtons}</div>
) : null}
{showCancelButton && (
<OGDialogClose asChild>
<Button variant="outline" aria-label={localize('com_ui_cancel')}>
{localize('com_ui_cancel')}
</Button>
</OGDialogClose>
)}
{buttons != null ? buttons : null}
{selection ? (
<OGDialogClose
onClick={selectHandler}
disabled={isLoading}
className={`${
selectClasses ?? defaultSelect
} flex h-10 items-center justify-center rounded-lg border-none px-4 py-2 text-sm disabled:opacity-80`}
>
{isLoading === true ? (
<Spinner className="size-4 text-white" />
) : (
(selectText as React.JSX.Element)
)}
</OGDialogClose>
) : null}
</OGDialogFooter>
</OGDialogContent>
);

View file

@ -168,7 +168,7 @@ DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex w-full flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
className={cn('flex w-full gap-2', 'flex-col sm:flex-row', 'sm:justify-end', className)}
{...props}
/>
);

View file

@ -10,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
'flex h-20 w-full resize-none rounded-md border border-gray-300 bg-transparent px-3 py-2 text-sm placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-700 dark:text-gray-50 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900',
'flex h-20 w-full resize-none rounded-md border border-border-light bg-transparent px-3 py-2 text-sm placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-50 dark:focus-visible:ring-gray-400 dark:focus-visible:ring-offset-gray-900',
className,
)}
ref={ref}