Files
UHD_Media_Tool/apps/web/app/components/agentui/file-diff.tsx
T
rootandClaude Opus 5.5 52402864d6 style(copy): punctuation rules and site-wide fix with a CI check (QA U11)
- 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]>
2026-09-28 20:09:29 +08:00

102 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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>
);
}