refactor: migrate all MCP dialogs to shadcn/ui Dialog component

Convert all MCP-related modal windows to use the unified shadcn/ui Dialog
component for consistency with the rest of the application.

Changes:
- McpPanel: Replace custom modal with Dialog component
  - Update props from onClose to open/onOpenChange pattern
  - Use DialogContent, DialogHeader, DialogTitle components
  - Remove custom backdrop and close button (handled by Dialog)

- McpFormModal: Migrate form modal to Dialog
  - Wrap entire form in Dialog component structure
  - Use DialogFooter for action buttons
  - Apply variant="mcp" to maintain green button styling
  - Remove unused X icon import

- McpWizardModal: Convert wizard to Dialog
  - Replace custom modal structure with Dialog components
  - Use Button component with variant="mcp" for consistency
  - Remove unused isLinux and X icon imports

- App.tsx: Update McpPanel usage
  - Remove conditional rendering wrapper
  - Pass open and onOpenChange props directly

- dialog.tsx: Fix dialog overlay and content styling
  - Change overlay from bg-background/80 to bg-black/50 for consistency
  - Change content from bg-background to explicit bg-white dark:bg-gray-900
  - Ensures opaque backgrounds matching MCP panel style

Benefits:
- Unified dialog behavior across the application
- Consistent styling and animations
- Better accessibility with Radix UI primitives
- Reduced code duplication
- Maintains MCP-specific green color scheme

All dialogs now share the same base styling while preserving their unique
content and functionality.
This commit is contained in:
Jason
2025-10-16 16:20:45 +08:00
parent 5f2bede5c4
commit 92528e6a9f
5 changed files with 372 additions and 403 deletions

View File

@@ -333,13 +333,12 @@ function App() {
onImportSuccess={handleImportSuccess} onImportSuccess={handleImportSuccess}
/> />
{isMcpOpen && ( <McpPanel
<McpPanel open={isMcpOpen}
appType={activeApp} onOpenChange={setIsMcpOpen}
onClose={() => setIsMcpOpen(false)} appType={activeApp}
onNotify={handleNotify} onNotify={handleNotify}
/> />
)}
</div> </div>
); );
} }

View File

@@ -1,7 +1,6 @@
import React, { useMemo, useState, useEffect } from "react"; import React, { useMemo, useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
X,
Save, Save,
AlertCircle, AlertCircle,
ChevronDown, ChevronDown,
@@ -9,6 +8,13 @@ import {
AlertTriangle, AlertTriangle,
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { mcpApi, type AppType } from "@/lib/api"; import { mcpApi, type AppType } from "@/lib/api";
@@ -495,267 +501,255 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
}; };
return ( return (
<div className="fixed inset-0 z-[60] flex items-center justify-center"> <>
{/* Backdrop */} <Dialog open={true} onOpenChange={(open) => !open && onClose()}>
<div <DialogContent className="max-w-3xl max-h-[90vh] flex flex-col">
className="absolute inset-0 bg-black/50 backdrop-blur-sm" <DialogHeader>
onClick={onClose} <DialogTitle>{getFormTitle()}</DialogTitle>
/> </DialogHeader>
{/* Modal */} {/* Content - Scrollable */}
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg max-w-3xl w-full mx-4 max-h-[90vh] overflow-hidden flex flex-col"> <div className="flex-1 overflow-y-auto -mx-6 px-6 space-y-4">
{/* Header */} {/* 预设选择(仅新增时展示) */}
<div className="flex-shrink-0 flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800"> {!isEditing && (
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{getFormTitle()}
</h3>
<button
onClick={onClose}
className="p-1 text-gray-500 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
>
<X size={18} />
</button>
</div>
{/* Content - Scrollable */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{/* 预设选择(仅新增时展示) */}
{!isEditing && (
<div>
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
{t("mcp.presets.title")}
</label>
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={applyCustom}
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedPreset === -1
? "bg-emerald-500 text-white dark:bg-emerald-600"
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
}`}
>
{t("presetSelector.custom")}
</button>
{mcpPresets.map((preset, idx) => {
const descriptionKey = `mcp.presets.${preset.id}.description`;
return (
<button
key={preset.id}
type="button"
onClick={() => applyPreset(idx)}
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedPreset === idx
? "bg-emerald-500 text-white dark:bg-emerald-600"
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
}`}
title={t(descriptionKey)}
>
{preset.id}
</button>
);
})}
</div>
</div>
)}
{/* ID (标题) */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{t("mcp.form.title")} <span className="text-red-500">*</span>
</label>
{!isEditing && idError && (
<span className="text-xs text-red-500 dark:text-red-400">
{idError}
</span>
)}
</div>
<Input
type="text"
placeholder={t("mcp.form.titlePlaceholder")}
value={formId}
onChange={(e) => handleIdChange(e.target.value)}
disabled={isEditing}
/>
</div>
{/* Name */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("mcp.form.name")}
</label>
<Input
type="text"
placeholder={t("mcp.form.namePlaceholder")}
value={formName}
onChange={(e) => setFormName(e.target.value)}
/>
</div>
{/* 可折叠的附加信息按钮 */}
<div>
<button
type="button"
onClick={() => setShowMetadata(!showMetadata)}
className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
>
{showMetadata ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
{t("mcp.form.additionalInfo")}
</button>
</div>
{/* 附加信息区域(可折叠) */}
{showMetadata && (
<>
{/* Description (描述) */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">
{t("mcp.form.description")} {t("mcp.presets.title")}
</label> </label>
<Input <div className="flex flex-wrap gap-2">
type="text" <button
placeholder={t("mcp.form.descriptionPlaceholder")} type="button"
value={formDescription} onClick={applyCustom}
onChange={(e) => setFormDescription(e.target.value)} className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
/> selectedPreset === -1
</div> ? "bg-emerald-500 text-white dark:bg-emerald-600"
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
{/* Tags */} }`}
<div> >
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> {t("presetSelector.custom")}
{t("mcp.form.tags")} </button>
</label> {mcpPresets.map((preset, idx) => {
<Input const descriptionKey = `mcp.presets.${preset.id}.description`;
type="text" return (
placeholder={t("mcp.form.tagsPlaceholder")} <button
value={formTags} key={preset.id}
onChange={(e) => setFormTags(e.target.value)} type="button"
/> onClick={() => applyPreset(idx)}
</div> className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedPreset === idx
{/* Homepage */} ? "bg-emerald-500 text-white dark:bg-emerald-600"
<div> : "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> }`}
{t("mcp.form.homepage")} title={t(descriptionKey)}
</label> >
<Input {preset.id}
type="text" </button>
placeholder={t("mcp.form.homepagePlaceholder")} );
value={formHomepage} })}
onChange={(e) => setFormHomepage(e.target.value)} </div>
/>
</div>
{/* Docs */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("mcp.form.docs")}
</label>
<Input
type="text"
placeholder={t("mcp.form.docsPlaceholder")}
value={formDocs}
onChange={(e) => setFormDocs(e.target.value)}
/>
</div>
</>
)}
{/* 配置输入框(根据格式显示 JSON 或 TOML */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{useToml ? t("mcp.form.tomlConfig") : t("mcp.form.jsonConfig")}
</label>
{(isEditing || selectedPreset === -1) && (
<button
type="button"
onClick={() => setIsWizardOpen(true)}
className="text-sm text-blue-500 dark:text-blue-400 hover:text-blue-600 dark:hover:text-blue-300 transition-colors"
>
{t("mcp.form.useWizard")}
</button>
)}
</div>
<Textarea
className="h-48 resize-none font-mono text-xs"
placeholder={
useToml
? t("mcp.form.tomlPlaceholder")
: t("mcp.form.jsonPlaceholder")
}
value={formConfig}
onChange={(e) => handleConfigChange(e.target.value)}
/>
{configError && (
<div className="flex items-center gap-2 mt-2 text-red-500 dark:text-red-400 text-sm">
<AlertCircle size={16} />
<span>{configError}</span>
</div> </div>
)} )}
</div> {/* ID (标题) */}
</div> <div>
<div className="flex items-center justify-between mb-2">
{/* Footer */} <label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
<div className="flex-shrink-0 flex items-center justify-between p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800"> {t("mcp.form.title")} <span className="text-red-500">*</span>
{/* 双端同步选项 */} </label>
<div className="flex items-center gap-3"> {!isEditing && idError && (
<div className="flex items-center gap-2"> <span className="text-xs text-red-500 dark:text-red-400">
<input {idError}
id={syncCheckboxId} </span>
type="checkbox" )}
className="h-4 w-4 rounded border-gray-300 text-emerald-600 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-800" </div>
checked={syncOtherSide} <Input
onChange={(event) => setSyncOtherSide(event.target.checked)} type="text"
placeholder={t("mcp.form.titlePlaceholder")}
value={formId}
onChange={(e) => handleIdChange(e.target.value)}
disabled={isEditing}
/> />
<label
htmlFor={syncCheckboxId}
className="text-sm text-gray-700 dark:text-gray-300 cursor-pointer select-none"
title={t("mcp.form.syncOtherSideHint", {
target: syncTargetLabel,
})}
>
{t("mcp.form.syncOtherSide", { target: syncTargetLabel })}
</label>
</div> </div>
{syncOtherSide && otherSideHasConflict && (
<div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400"> {/* Name */}
<AlertTriangle size={14} /> <div>
<span className="text-xs font-medium"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("mcp.form.willOverwriteWarning", { {t("mcp.form.name")}
</label>
<Input
type="text"
placeholder={t("mcp.form.namePlaceholder")}
value={formName}
onChange={(e) => setFormName(e.target.value)}
/>
</div>
{/* 可折叠的附加信息按钮 */}
<div>
<button
type="button"
onClick={() => setShowMetadata(!showMetadata)}
className="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
>
{showMetadata ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} />
)}
{t("mcp.form.additionalInfo")}
</button>
</div>
{/* 附加信息区域(可折叠) */}
{showMetadata && (
<>
{/* Description (描述) */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("mcp.form.description")}
</label>
<Input
type="text"
placeholder={t("mcp.form.descriptionPlaceholder")}
value={formDescription}
onChange={(e) => setFormDescription(e.target.value)}
/>
</div>
{/* Tags */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("mcp.form.tags")}
</label>
<Input
type="text"
placeholder={t("mcp.form.tagsPlaceholder")}
value={formTags}
onChange={(e) => setFormTags(e.target.value)}
/>
</div>
{/* Homepage */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("mcp.form.homepage")}
</label>
<Input
type="text"
placeholder={t("mcp.form.homepagePlaceholder")}
value={formHomepage}
onChange={(e) => setFormHomepage(e.target.value)}
/>
</div>
{/* Docs */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("mcp.form.docs")}
</label>
<Input
type="text"
placeholder={t("mcp.form.docsPlaceholder")}
value={formDocs}
onChange={(e) => setFormDocs(e.target.value)}
/>
</div>
</>
)}
{/* 配置输入框(根据格式显示 JSON 或 TOML */}
<div>
<div className="flex items-center justify-between mb-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{useToml
? t("mcp.form.tomlConfig")
: t("mcp.form.jsonConfig")}
</label>
{(isEditing || selectedPreset === -1) && (
<button
type="button"
onClick={() => setIsWizardOpen(true)}
className="text-sm text-blue-500 dark:text-blue-400 hover:text-blue-600 dark:hover:text-blue-300 transition-colors"
>
{t("mcp.form.useWizard")}
</button>
)}
</div>
<Textarea
className="h-48 resize-none font-mono text-xs"
placeholder={
useToml
? t("mcp.form.tomlPlaceholder")
: t("mcp.form.jsonPlaceholder")
}
value={formConfig}
onChange={(e) => handleConfigChange(e.target.value)}
/>
{configError && (
<div className="flex items-center gap-2 mt-2 text-red-500 dark:text-red-400 text-sm">
<AlertCircle size={16} />
<span>{configError}</span>
</div>
)}
</div>
</div>
{/* Footer */}
<DialogFooter className="flex-col sm:flex-row sm:justify-between gap-3 pt-4">
{/* 双端同步选项 */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<input
id={syncCheckboxId}
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-emerald-600 focus:ring-emerald-500 dark:border-gray-600 dark:bg-gray-800"
checked={syncOtherSide}
onChange={(event) => setSyncOtherSide(event.target.checked)}
/>
<label
htmlFor={syncCheckboxId}
className="text-sm text-gray-700 dark:text-gray-300 cursor-pointer select-none"
title={t("mcp.form.syncOtherSideHint", {
target: syncTargetLabel, target: syncTargetLabel,
})} })}
</span> >
{t("mcp.form.syncOtherSide", { target: syncTargetLabel })}
</label>
</div> </div>
)} {syncOtherSide && otherSideHasConflict && (
</div> <div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400">
<AlertTriangle size={14} />
<span className="text-xs font-medium">
{t("mcp.form.willOverwriteWarning", {
target: syncTargetLabel,
})}
</span>
</div>
)}
</div>
{/* 操作按钮 */} {/* 操作按钮 */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button type="button" variant="ghost" size="sm" onClick={onClose}> <Button type="button" variant="ghost" size="sm" onClick={onClose}>
{t("common.cancel")} {t("common.cancel")}
</Button> </Button>
<Button <Button
type="button" type="button"
size="sm" size="sm"
onClick={handleSubmit} onClick={handleSubmit}
disabled={saving || (!isEditing && !!idError)} disabled={saving || (!isEditing && !!idError)}
className="bg-emerald-500 text-white hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700" variant="mcp"
> >
<Save size={16} /> <Save size={16} />
{saving {saving
? t("common.saving") ? t("common.saving")
: isEditing : isEditing
? t("common.save") ? t("common.save")
: t("common.add")} : t("common.add")}
</Button> </Button>
</div> </div>
</div> </DialogFooter>
</div> </DialogContent>
</Dialog>
{/* Wizard Modal */} {/* Wizard Modal */}
<McpWizardModal <McpWizardModal
@@ -766,7 +760,7 @@ const McpFormModal: React.FC<McpFormModalProps> = ({
initialTitle={formId} initialTitle={formId}
initialServer={wizardInitialSpec} initialServer={wizardInitialSpec}
/> />
</div> </>
); );
}; };

View File

@@ -1,7 +1,13 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { X, Plus, Server, Check } from "lucide-react"; import { Plus, Server, Check } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { mcpApi, type AppType } from "@/lib/api"; import { mcpApi, type AppType } from "@/lib/api";
import { McpServer } from "../../types"; import { McpServer } from "../../types";
import McpListItem from "./McpListItem"; import McpListItem from "./McpListItem";
@@ -13,7 +19,8 @@ import {
} from "../../utils/errorUtils"; } from "../../utils/errorUtils";
interface McpPanelProps { interface McpPanelProps {
onClose: () => void; open: boolean;
onOpenChange: (open: boolean) => void;
onNotify?: ( onNotify?: (
message: string, message: string,
type: "success" | "error", type: "success" | "error",
@@ -26,7 +33,12 @@ interface McpPanelProps {
* MCP 管理面板 * MCP 管理面板
* 采用与主界面一致的设计风格,右上角添加按钮,每个 MCP 占一行 * 采用与主界面一致的设计风格,右上角添加按钮,每个 MCP 占一行
*/ */
const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => { const McpPanel: React.FC<McpPanelProps> = ({
open,
onOpenChange,
onNotify,
appType,
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [servers, setServers] = useState<Record<string, McpServer>>({}); const [servers, setServers] = useState<Record<string, McpServer>>({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -180,97 +192,90 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => {
appType === "claude" ? t("mcp.claudeTitle") : t("mcp.codexTitle"); appType === "claude" ? t("mcp.claudeTitle") : t("mcp.codexTitle");
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center"> <>
{/* Backdrop */} <Dialog open={open} onOpenChange={onOpenChange}>
<div <DialogContent className="max-w-3xl max-h-[85vh] min-h-[600px] flex flex-col">
className="absolute inset-0 bg-black/50 backdrop-blur-sm" <DialogHeader>
onClick={onClose} <div className="flex items-center justify-between pr-8">
/> <DialogTitle>{panelTitle}</DialogTitle>
<Button type="button" variant="mcp" size="sm" onClick={handleAdd}>
{/* Panel */} <Plus size={16} />
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-lg max-w-3xl w-full mx-4 overflow-hidden flex flex-col max-h-[85vh] min-h-[600px]"> {t("mcp.add")}
{/* Header */} </Button>
<div className="flex-shrink-0 flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
{panelTitle}
</h3>
<div className="flex items-center gap-3">
<Button type="button" variant="mcp" size="sm" onClick={handleAdd}>
<Plus size={16} />
{t("mcp.add")}
</Button>
<Button type="button" variant="ghost" size="icon" onClick={onClose}>
<X size={18} />
</Button>
</div>
</div>
{/* Info Section */}
<div className="flex-shrink-0 px-6 pt-4 pb-2">
<div className="text-sm text-gray-500 dark:text-gray-400">
{t("mcp.serverCount", { count: Object.keys(servers).length })} ·{" "}
{t("mcp.enabledCount", { count: enabledCount })}
</div>
</div>
{/* Content - Scrollable */}
<div className="flex-1 overflow-y-auto px-6 py-4">
{loading ? (
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
{t("mcp.loading")}
</div> </div>
) : ( </DialogHeader>
(() => {
const hasAny = serverEntries.length > 0; {/* Info Section */}
if (!hasAny) { <div className="flex-shrink-0 -mt-2">
return ( <div className="text-sm text-gray-500 dark:text-gray-400">
<div className="text-center py-12"> {t("mcp.serverCount", { count: Object.keys(servers).length })} ·{" "}
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center"> {t("mcp.enabledCount", { count: enabledCount })}
<Server </div>
size={24} </div>
className="text-gray-400 dark:text-gray-500"
/> {/* Content - Scrollable */}
<div className="flex-1 overflow-y-auto -mx-6 px-6">
{loading ? (
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
{t("mcp.loading")}
</div>
) : (
(() => {
const hasAny = serverEntries.length > 0;
if (!hasAny) {
return (
<div className="text-center py-12">
<div className="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
<Server
size={24}
className="text-gray-400 dark:text-gray-500"
/>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
{t("mcp.empty")}
</h3>
<p className="text-gray-500 dark:text-gray-400 text-sm">
{t("mcp.emptyDescription")}
</p>
</div> </div>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2"> );
{t("mcp.empty")} }
</h3>
<p className="text-gray-500 dark:text-gray-400 text-sm"> return (
{t("mcp.emptyDescription")} <div className="space-y-3">
</p> {/* 已安装 */}
{serverEntries.map(([id, server]) => (
<McpListItem
key={`installed-${id}`}
id={id}
server={server}
onToggle={handleToggle}
onEdit={handleEdit}
onDelete={handleDelete}
/>
))}
{/* 预设已移至"新增 MCP"面板中展示与套用 */}
</div> </div>
); );
} })()
)}
</div>
return ( {/* Footer */}
<div className="space-y-3"> <div className="flex-shrink-0 flex items-center justify-end pt-4 border-t border-gray-200 dark:border-gray-800">
{/* 已安装 */} <Button
{serverEntries.map(([id, server]) => ( type="button"
<McpListItem variant="mcp"
key={`installed-${id}`} size="sm"
id={id} onClick={() => onOpenChange(false)}
server={server} >
onToggle={handleToggle} <Check size={16} />
onEdit={handleEdit} {t("common.done")}
onDelete={handleDelete} </Button>
/> </div>
))} </DialogContent>
</Dialog>
{/* 预设已移至"新增 MCP"面板中展示与套用 */}
</div>
);
})()
)}
</div>
{/* Footer */}
<div className="flex-shrink-0 flex items-center justify-end p-6 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800">
<Button type="button" variant="mcp" size="sm" onClick={onClose}>
<Check size={16} />
{t("common.done")}
</Button>
</div>
</div>
{/* Form Modal */} {/* Form Modal */}
{isFormOpen && ( {isFormOpen && (
@@ -295,7 +300,7 @@ const McpPanel: React.FC<McpPanelProps> = ({ onClose, onNotify, appType }) => {
onCancel={() => setConfirmDialog(null)} onCancel={() => setConfirmDialog(null)}
/> />
)} )}
</div> </>
); );
}; };

View File

@@ -1,8 +1,15 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { X, Save } from "lucide-react"; import { Save } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { McpServerSpec } from "../../types"; import { McpServerSpec } from "../../types";
import { isLinux } from "../../lib/platform";
interface McpWizardModalProps { interface McpWizardModalProps {
isOpen: boolean; isOpen: boolean;
@@ -216,45 +223,17 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
setWizardHeaders(""); setWizardHeaders("");
}, [isOpen]); }, [isOpen]);
if (!isOpen) return null;
const preview = generatePreview(); const preview = generatePreview();
return ( return (
<div <Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
className="fixed inset-0 z-[70] flex items-center justify-center" <DialogContent className="max-w-2xl max-h-[90vh] flex flex-col">
onMouseDown={(e) => { <DialogHeader>
if (e.target === e.currentTarget) { <DialogTitle>{t("mcp.wizard.title")}</DialogTitle>
handleClose(); </DialogHeader>
}
}}
>
{/* Backdrop */}
<div
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
isLinux() ? "" : " backdrop-blur-sm"
}`}
/>
{/* Modal */}
<div className="relative mx-4 flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl bg-white shadow-lg dark:bg-gray-900">
{/* Header */}
<div className="flex items-center justify-between border-b border-gray-200 p-6 dark:border-gray-800">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
{t("mcp.wizard.title")}
</h2>
<button
type="button"
onClick={handleClose}
className="rounded-md p-1 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100"
aria-label={t("common.close")}
>
<X size={18} />
</button>
</div>
{/* Content */} {/* Content */}
<div className="flex-1 min-h-0 space-y-4 overflow-auto p-6"> <div className="flex-1 overflow-y-auto -mx-6 px-6 space-y-4">
{/* Hint */} {/* Hint */}
<div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20"> <div className="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
<p className="text-sm text-blue-800 dark:text-blue-200"> <p className="text-sm text-blue-800 dark:text-blue-200">
@@ -419,25 +398,17 @@ const McpWizardModal: React.FC<McpWizardModalProps> = ({
</div> </div>
{/* Footer */} {/* Footer */}
<div className="flex items-center justify-end gap-3 border-t border-gray-200 bg-gray-100 p-6 dark:border-gray-800 dark:bg-gray-800"> <DialogFooter className="gap-3 pt-4">
<button <Button type="button" variant="ghost" onClick={handleClose}>
type="button"
onClick={handleClose}
className="rounded-lg px-4 py-2 text-sm font-medium text-gray-500 transition-colors hover:bg-white hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-100"
>
{t("common.cancel")} {t("common.cancel")}
</button> </Button>
<button <Button type="button" variant="mcp" onClick={handleApply}>
type="button"
onClick={handleApply}
className="flex items-center gap-2 rounded-lg bg-emerald-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-700"
>
<Save className="h-4 w-4" /> <Save className="h-4 w-4" />
{t("mcp.wizard.apply")} {t("mcp.wizard.apply")}
</button> </Button>
</div> </DialogFooter>
</div> </DialogContent>
</div> </Dialog>
); );
}; };

View File

@@ -18,7 +18,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className, className,
)} )}
{...props} {...props}
@@ -35,7 +35,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white dark:bg-gray-900 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className, className,
)} )}
{...props} {...props}