refactor(config): lift the tool-agnostic config modals out of CcConfig

Moves FileViewer, EditorModal, ConfirmDeleteModal, ToastNotice, MdItemList,
ScopeBadge, and the shared atoms (Empty/SkeletonRows/CopyButton/formatBytes)
into client/src/components/config/ so the upcoming Codex config panel can
reuse them. EditorModal now takes a fetchContent callback instead of calling
api.ccConfig.file itself; CcConfig.tsx supplies the Claude-specific fetcher.
The moved EditorState/ConfirmDeleteState type field widens from
CcArtifactType to string so non-Claude callers can use them too, with a cast
at the two remaining Claude-specific call sites in CcConfig.tsx.
This commit is contained in:
2026-09-08 20:40:47 +07:00
parent e38dc5555c
commit 865c6c1d1e
9 changed files with 759 additions and 614 deletions
@@ -0,0 +1,89 @@
/**
* @file ConfirmDeleteModal.tsx
* @description Confirmation modal for deleting a config artifact. Tool-
* agnostic — the caller supplies the async delete action; the modal only
* tracks the busy state and Escape-to-cancel behavior.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ShieldAlert, Trash2 } from "lucide-react";
export type ConfirmDeleteState = {
type: string;
scope: "user" | "project" | "auto-memory";
name?: string;
path: string;
project?: string; // set for type === "auto-memory"
} | null;
interface ConfirmDeleteModalProps {
state: NonNullable<ConfirmDeleteState>;
onCancel: () => void;
onConfirm: () => Promise<void>;
}
export function ConfirmDeleteModal({ state, onCancel, onConfirm }: ConfirmDeleteModalProps) {
const { t } = useTranslation("ccConfig");
const [busy, setBusy] = useState(false);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onCancel();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onCancel]);
const handleConfirm = useCallback(async () => {
setBusy(true);
try {
await onConfirm();
} finally {
setBusy(false);
}
}, [onConfirm]);
return (
<div
className="fixed inset-0 bg-black/60 z-40 flex items-center justify-center p-4"
onClick={onCancel}
>
<div
className="relative w-full max-w-lg rounded-xl border border-status-danger/40 bg-surface-1"
onClick={(e) => e.stopPropagation()}
>
<div className="border-b border-border px-4 py-2.5 flex items-center gap-2">
<ShieldAlert className="w-4 h-4 text-status-danger" />
<span className="text-sm font-medium text-fg-primary flex-1">
{t("edit.confirmDelete")}
</span>
</div>
<div className="p-4 space-y-3">
<p className="text-xs text-fg-secondary leading-relaxed">{t("edit.confirmDeleteBody")}</p>
<div className="rounded-md border border-border bg-surface-2 px-3 py-2 font-mono text-[11px] text-fg-secondary break-all">
{t("edit.confirmDeletePath", { path: state.path })}
</div>
</div>
<div className="border-t border-border px-4 py-3 flex items-center justify-end gap-2">
<button
onClick={onCancel}
disabled={busy}
className="text-[12px] font-medium px-3 py-1.5 rounded-md border border-border bg-surface-2 hover:bg-surface-3 text-fg-secondary disabled:opacity-60"
>
{t("edit.cancel")}
</button>
<button
onClick={handleConfirm}
disabled={busy}
className="text-[12px] font-medium px-3 py-1.5 rounded-md border border-status-danger/50 bg-status-danger/15 hover:bg-status-danger/25 text-status-danger inline-flex items-center gap-1.5 disabled:opacity-60"
>
<Trash2 className="w-3.5 h-3.5" />
{busy ? t("edit.deleting") : t("edit.confirmDeleteAction")}
</button>
</div>
</div>
</div>
);
}