- glossary: 10 punctuation rules (full-width in Chinese, CJK/Latin spacing, full-width parens around Chinese, single ellipsis, 「」 quotes, period rules) - scripts/check-copy-punctuation.mjs: checks string literals, templates and JSX text (not comments, types, keys, match/compare strings); --fix, --strict, --by-file; wired into deno task ci as check:copy in warn mode - --fix applied to web, server and contracts; files other lanes were editing (subscriptions, sites, browser, agent, auth/account, inventory) left for them - tests updated for the changed messages Co-Authored-By: Claude Opus 5.5 <[email protected]>
102 lines
3.9 KiB
TypeScript
102 lines
3.9 KiB
TypeScript
// Adapted from agentui (https://github.com/ashish200729/agentui), components/agents/file-diff.tsx
|
||
// Copyright (c) 2026 Saurabh Chauhan. MIT License, see ./LICENSE-agentui.md
|
||
// 改造:新增/删除行底色 emerald/rose → success/destructive 洗层(并保留 +/− 符号,不只靠颜色);不做语法高亮(去 shiki);
|
||
// 折叠用 Disclosure;maxHeight 改 Tailwind 类;用于展示路径类变更(如整理记录的入库目标)。
|
||
import { ChevronDown, FileCode2 } from "lucide-react";
|
||
import { type ReactNode, useId, useState } from "react";
|
||
import { cn } from "~/lib/utils.ts";
|
||
import { CHEVRON_CLASS, Disclosure } from "./disclosure.tsx";
|
||
|
||
export type FileDiffLineType = "added" | "removed" | "context";
|
||
|
||
export interface FileDiffLine {
|
||
id: string;
|
||
type?: FileDiffLineType;
|
||
content: ReactNode;
|
||
/** 行尾附注(如状态) */
|
||
note?: ReactNode;
|
||
}
|
||
|
||
function Count({ value, type }: { value: number; type: "added" | "removed" }) {
|
||
if (!value) return null;
|
||
return (
|
||
<span className={cn("font-mono text-meta tabular-nums", type === "added" ? "text-success" : "text-destructive")}>
|
||
{type === "added" ? "+" : "−"}
|
||
{value}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
export function FileDiff({
|
||
file,
|
||
lines,
|
||
defaultOpen = true,
|
||
maxHeightClassName = "max-h-56",
|
||
className,
|
||
}: {
|
||
file: ReactNode;
|
||
lines: FileDiffLine[];
|
||
defaultOpen?: boolean;
|
||
maxHeightClassName?: string;
|
||
className?: string;
|
||
}) {
|
||
const id = useId();
|
||
const [open, setOpen] = useState(defaultOpen);
|
||
const added = lines.filter((l) => l.type === "added").length;
|
||
const removed = lines.filter((l) => l.type === "removed").length;
|
||
return (
|
||
<div className={cn("w-full min-w-0 overflow-hidden rounded-lg border border-border/60 text-sm", className)}>
|
||
<button
|
||
type="button"
|
||
aria-expanded={open}
|
||
aria-controls={id}
|
||
onClick={() => setOpen(!open)}
|
||
className="flex h-8 w-full cursor-pointer items-center gap-2 px-2.5 text-left outline-none transition-colors duration-micro hover:bg-foreground/[0.03] focus-visible:ring-2 focus-visible:ring-ring"
|
||
>
|
||
<FileCode2 aria-hidden className="size-3.5 shrink-0 text-muted-foreground/70" />
|
||
<span className="min-w-0 flex-1 truncate text-meta text-foreground/85">{file}</span>
|
||
<Count value={added} type="added" />
|
||
<Count value={removed} type="removed" />
|
||
<ChevronDown aria-hidden className={cn("size-3.5", CHEVRON_CLASS, open && "rotate-180")} />
|
||
</button>
|
||
<Disclosure id={id} open={open}>
|
||
<div className={cn("overflow-auto border-t border-border/50 py-1", maxHeightClassName)}>
|
||
<ul className="font-mono text-meta leading-5">
|
||
{lines.map((l) => (
|
||
<li
|
||
key={l.id}
|
||
className={cn(
|
||
"grid grid-cols-[1.25rem_minmax(0,1fr)_auto] gap-1 pr-2.5",
|
||
l.type === "added" && "bg-success/[0.08]",
|
||
l.type === "removed" && "bg-destructive/[0.08]",
|
||
)}
|
||
>
|
||
<span
|
||
aria-hidden
|
||
className={cn(
|
||
"text-center select-none",
|
||
l.type === "added"
|
||
? "text-success"
|
||
: l.type === "removed"
|
||
? "text-destructive"
|
||
: "text-muted-foreground/50",
|
||
)}
|
||
>
|
||
{l.type === "added" ? "+" : l.type === "removed" ? "−" : " "}
|
||
</span>
|
||
<span className="min-w-0 break-all text-foreground/85">
|
||
<span className="sr-only">
|
||
{l.type === "added" ? "新增:" : l.type === "removed" ? "移除:" : ""}
|
||
</span>
|
||
{l.content}
|
||
</span>
|
||
{l.note ? <span className="shrink-0 font-sans text-muted-foreground">{l.note}</span> : null}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
</Disclosure>
|
||
</div>
|
||
);
|
||
}
|