865c6c1d1e
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.
78 lines
2.7 KiB
TypeScript
78 lines
2.7 KiB
TypeScript
/**
|
|
* @file FileViewer.tsx
|
|
* @description Read-only file-content modal for the config explorer. Takes
|
|
* already-fetched data as props (path/data/error) rather than fetching
|
|
* itself, so it stays tool-agnostic — the orchestrator (Claude or Codex
|
|
* config page) owns the fetch.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { FileText, X, AlertCircle } from "lucide-react";
|
|
import type { CcFileResponse } from "../../lib/api";
|
|
import { CopyButton } from "./configPrimitives";
|
|
|
|
export function FileViewer({
|
|
state,
|
|
onClose,
|
|
}: {
|
|
state: { path: string; data: CcFileResponse | null; error: string | null };
|
|
onClose: () => void;
|
|
}) {
|
|
const { t } = useTranslation("ccConfig");
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") onClose();
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
}, [onClose]);
|
|
return (
|
|
<div
|
|
className="fixed inset-0 bg-black/60 z-40 flex items-center justify-center p-4"
|
|
onClick={onClose}
|
|
>
|
|
<div
|
|
className="relative w-full max-w-4xl max-h-[85vh] rounded-xl border border-border bg-surface-1 flex flex-col"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<div className="flex items-center gap-2 border-b border-border px-4 py-2.5">
|
|
<FileText className="w-4 h-4 text-fg-muted" />
|
|
<span className="font-mono text-[12px] text-fg-secondary truncate flex-1">
|
|
{state.path}
|
|
</span>
|
|
<CopyButton value={state.path} />
|
|
<button
|
|
onClick={onClose}
|
|
className="text-fg-secondary hover:text-fg-primary p-1 rounded-md hover:bg-surface-3"
|
|
aria-label={t("common.close")}
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
<div className="overflow-auto p-4">
|
|
{state.error ? (
|
|
<div className="text-sm text-status-danger inline-flex items-center gap-2">
|
|
<AlertCircle className="w-4 h-4" />
|
|
{state.error}
|
|
</div>
|
|
) : !state.data ? (
|
|
<div className="text-sm text-fg-muted">…</div>
|
|
) : (
|
|
<pre className="text-[11px] font-mono text-fg-secondary whitespace-pre-wrap break-words">
|
|
{state.data.text}
|
|
{state.data.truncated && (
|
|
<span className="text-fg-muted italic">
|
|
{"\n\n"}
|
|
{t("common.truncated")}
|
|
</span>
|
|
)}
|
|
</pre>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|