refactor: unify modal overlay system with shadcn/ui Dialog
Fix inconsistent modal overlays by migrating all custom implementations to the unified shadcn/ui Dialog component with proper z-index layering. Changes: - Update Dialog component to support three z-index levels: - base (z-40): First-level dialogs - nested (z-50): Nested dialogs - alert (z-[60]): Alert/confirmation dialogs (using Tailwind arbitrary value) - Refactor all custom modal implementations to use Dialog: - EndpointSpeedTest: API endpoint speed testing panel - ClaudeConfigEditor: Claude common config editor - CodexQuickWizardModal: Codex quick setup wizard - CodexCommonConfigModal: Codex common config editor - SettingsDialog: Restart confirmation prompt - Remove custom backdrop implementations and manual z-index - Leverage Radix UI Portal for automatic DOM order management - Ensure consistent overlay behavior and keyboard interactions This eliminates the "background residue" issue where overlays from different layers would conflict, providing a unified and professional user experience across all modal interactions.
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import { X, Save } from "lucide-react";
|
||||
import { Save } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isLinux } from "@/lib/platform";
|
||||
import {
|
||||
generateThirdPartyAuth,
|
||||
generateThirdPartyConfig,
|
||||
} from "@/config/codexProviderPresets";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
interface CodexQuickWizardModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -101,225 +109,187 @@ export const CodexQuickWizardModal: React.FC<CodexQuickWizardModalProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
||||
onMouseDown={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
||||
isLinux() ? "" : " backdrop-blur-sm"
|
||||
}`}
|
||||
/>
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
|
||||
<DialogContent zIndex="nested" className="max-w-2xl max-h-[90vh] flex flex-col p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-0">
|
||||
<DialogTitle>{t("codexConfig.quickWizard")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<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">
|
||||
<div className="flex h-full min-h-0 flex-col" role="form">
|
||||
{/* 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("codexConfig.quickWizard")}
|
||||
</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 className="flex-1 min-h-0 space-y-4 overflow-auto px-6 py-4">
|
||||
<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">
|
||||
{t("codexConfig.wizardHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-h-0 space-y-4 overflow-auto p-6">
|
||||
<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">
|
||||
{t("codexConfig.wizardHint")}
|
||||
<div className="space-y-4">
|
||||
{/* API Key */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.apiKeyLabel")}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={templateApiKey}
|
||||
ref={apiKeyInputRef}
|
||||
onChange={(e) => setTemplateApiKey(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
pattern=".*\S.*"
|
||||
title={t("common.enterValidValue")}
|
||||
placeholder={t("codexConfig.apiKeyPlaceholder")}
|
||||
required
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Display Name */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.supplierNameLabel")}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={templateDisplayName}
|
||||
ref={displayNameInputRef}
|
||||
onChange={(e) => setTemplateDisplayName(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder={t("codexConfig.supplierNamePlaceholder")}
|
||||
required
|
||||
pattern=".*\S.*"
|
||||
title={t("common.enterValidValue")}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{t("codexConfig.supplierNameHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* API Key */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.apiKeyLabel")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateApiKey}
|
||||
ref={apiKeyInputRef}
|
||||
onChange={(e) => setTemplateApiKey(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
pattern=".*\S.*"
|
||||
title={t("common.enterValidValue")}
|
||||
placeholder={t("codexConfig.apiKeyPlaceholder")}
|
||||
required
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Display Name */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.supplierNameLabel")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateDisplayName}
|
||||
ref={displayNameInputRef}
|
||||
onChange={(e) => setTemplateDisplayName(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder={t("codexConfig.supplierNamePlaceholder")}
|
||||
required
|
||||
pattern=".*\S.*"
|
||||
title={t("common.enterValidValue")}
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{t("codexConfig.supplierNameHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Provider Name */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.supplierCodeLabel")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateProviderName}
|
||||
onChange={(e) => setTemplateProviderName(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder={t("codexConfig.supplierCodePlaceholder")}
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{t("codexConfig.supplierCodeHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Base URL */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.apiUrlLabel")}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={templateBaseUrl}
|
||||
ref={baseUrlInputRef}
|
||||
onChange={(e) => setTemplateBaseUrl(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder={t("codexConfig.apiUrlPlaceholder")}
|
||||
required
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Website URL */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.websiteLabel")}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={templateWebsiteUrl}
|
||||
onChange={(e) => setTemplateWebsiteUrl(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder={t("codexConfig.websitePlaceholder")}
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{t("codexConfig.websiteHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Model Name */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.modelNameLabel")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={templateModelName}
|
||||
ref={modelNameInputRef}
|
||||
onChange={(e) => setTemplateModelName(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
pattern=".*\S.*"
|
||||
title={t("common.enterValidValue")}
|
||||
placeholder={t("codexConfig.modelNamePlaceholder")}
|
||||
required
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
{/* Provider Name */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.supplierCodeLabel")}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={templateProviderName}
|
||||
onChange={(e) => setTemplateProviderName(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder={t("codexConfig.supplierCodePlaceholder")}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{t("codexConfig.supplierCodeHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
{(templateApiKey || templateProviderName || templateBaseUrl) && (
|
||||
<div className="space-y-2 border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.configPreview")}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
auth.json
|
||||
</label>
|
||||
<pre className="overflow-x-auto rounded-lg bg-gray-50 p-3 text-xs font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
{JSON.stringify(
|
||||
generateThirdPartyAuth(templateApiKey),
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
config.toml
|
||||
</label>
|
||||
<pre className="whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-xs font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
{templateProviderName && templateBaseUrl
|
||||
? generateThirdPartyConfig(
|
||||
templateProviderName,
|
||||
templateBaseUrl,
|
||||
templateModelName,
|
||||
)
|
||||
: ""}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Base URL */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.apiUrlLabel")}
|
||||
</label>
|
||||
<Input
|
||||
type="url"
|
||||
value={templateBaseUrl}
|
||||
ref={baseUrlInputRef}
|
||||
onChange={(e) => setTemplateBaseUrl(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder={t("codexConfig.apiUrlPlaceholder")}
|
||||
required
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Website URL */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.websiteLabel")}
|
||||
</label>
|
||||
<Input
|
||||
type="url"
|
||||
value={templateWebsiteUrl}
|
||||
onChange={(e) => setTemplateWebsiteUrl(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder={t("codexConfig.websitePlaceholder")}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{t("codexConfig.websiteHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Model Name */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.modelNameLabel")}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={templateModelName}
|
||||
ref={modelNameInputRef}
|
||||
onChange={(e) => setTemplateModelName(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
pattern=".*\S.*"
|
||||
title={t("common.enterValidValue")}
|
||||
placeholder={t("codexConfig.modelNamePlaceholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<button
|
||||
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")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
applyTemplate();
|
||||
}}
|
||||
className="flex items-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
{t("codexConfig.applyConfig")}
|
||||
</button>
|
||||
</div>
|
||||
{/* Preview */}
|
||||
{(templateApiKey || templateProviderName || templateBaseUrl) && (
|
||||
<div className="space-y-2 border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{t("codexConfig.configPreview")}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
auth.json
|
||||
</label>
|
||||
<pre className="overflow-x-auto rounded-lg bg-gray-50 p-3 text-xs font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
{JSON.stringify(
|
||||
generateThirdPartyAuth(templateApiKey),
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
config.toml
|
||||
</label>
|
||||
<pre className="whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-xs font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||||
{templateProviderName && templateBaseUrl
|
||||
? generateThirdPartyConfig(
|
||||
templateProviderName,
|
||||
templateBaseUrl,
|
||||
templateModelName,
|
||||
)
|
||||
: ""}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="px-6 pb-6 pt-4 border-t border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-800 m-0">
|
||||
<Button type="button" variant="outline" onClick={handleClose}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
applyTemplate();
|
||||
}}
|
||||
className="gap-2"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
{t("codexConfig.applyConfig")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user