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.
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
/**
|
|
* @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`;
|
|
}
|