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:
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @file configPrimitives.tsx
|
||||
* @description Small tool-agnostic atoms shared by the config-explorer UI
|
||||
* (Claude Code today, Codex next): an empty-state card, a skeleton-row list
|
||||
* placeholder, a copy-to-clipboard button, and a byte-size formatter. None
|
||||
* of these know about any particular tool's API.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Copy, Check } from "lucide-react";
|
||||
|
||||
export function CopyButton({ value }: { value: string }) {
|
||||
const { t } = useTranslation("ccConfig");
|
||||
const [copied, setCopied] = useState(false);
|
||||
return (
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
} catch {
|
||||
/* clipboard unavailable */
|
||||
}
|
||||
}}
|
||||
className="text-[10px] font-medium px-1.5 py-1 rounded border border-border bg-surface-1 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary inline-flex items-center gap-1 flex-shrink-0"
|
||||
title={t("common.copyPath")}
|
||||
>
|
||||
{copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function Empty() {
|
||||
const { t } = useTranslation("ccConfig");
|
||||
return (
|
||||
<div className="rounded-lg border border-dashed border-border bg-surface-2 px-4 py-8 text-center text-sm text-fg-muted">
|
||||
{t("common.empty")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SkeletonRows({ n }: { n: number }) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: n }).map((_, i) => (
|
||||
<div key={i} className="h-16 rounded-lg border border-border bg-surface-2 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
Reference in New Issue
Block a user