19 lines
12 KiB
JSON
19 lines
12 KiB
JSON
{
|
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
"name": "diff-table",
|
|
"type": "registry:component",
|
|
"title": "Diff Table",
|
|
"description": "AI-proposed edits sweeping through tabular data.",
|
|
"registryDependencies": [
|
|
"https://www.beautifului.dev/r/foundation.json",
|
|
"https://www.beautifului.dev/r/button.json"
|
|
],
|
|
"files": [
|
|
{
|
|
"path": "components/primitives/DiffTable.tsx",
|
|
"content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Button } from \"@/components/atoms/Button\";\n\n/* ─────────────────────────────────────────────────────────\n * DIFF TABLE\n * The proposed edit plays once and rests on the completed\n * diff. Each changed row is the control: click it to include\n * or exclude that specific addition/removal before applying.\n * ───────────────────────────────────────────────────────── */\n\nfunction useStage(steps: number[]) {\n const [stage, setStage] = useState(0);\n useEffect(() => {\n if (stage >= steps.length) return;\n const t = setTimeout(() => setStage((s) => s + 1), steps[stage]);\n return () => clearTimeout(t);\n }, [stage, steps]);\n return stage;\n}\n\nconst STAGE_DELAYS = [180, 260];\n\nexport type DiffRow = {\n key: string;\n id: string;\n dept: string;\n email: string;\n removed: boolean;\n};\n\nconst ROWS: DiffRow[] = [\n { key: \"rocky\", id: \"Rocky Road\", dept: \"Classic\", email: \"aurora-scoops\", removed: true },\n { key: \"bubblegum\", id: \"Bubblegum\", dept: \"Retro\", email: \"kumo-creamery\", removed: true },\n { key: \"mint\", id: \"Mint Chip\", dept: \"Classic\", email: \"maple-orbit\", removed: false },\n];\n\nconst DOT: Record<string, string> = {\n Classic: \"bg-accent\",\n Retro: \"bg-ink-3\",\n Seasonal: \"bg-orange\",\n};\n\nfunction IncludedMark({ included, tone }: { included: boolean; tone: \"red\" | \"green\" }) {\n return (\n <span\n aria-hidden\n className={`flex size-4.5 shrink-0 items-center justify-center rounded-[5px] transition-[background-color,color,transform] duration-150 ${\n included\n ? tone === \"red\" ? \"bg-red text-white\" : \"bg-green text-white\"\n : \"bg-inset text-ink-3 shadow-hairline\"\n }`}\n style={{ transform: included ? \"scale(1)\" : \"scale(0.92)\" }}\n >\n {included ? (\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></svg>\n ) : null}\n </span>\n );\n}\n\nexport default function DiffTable({\n rows = ROWS,\n}: {\n rows?: DiffRow[];\n variant?: string;\n} = {}) {\n const stage = useStage(STAGE_DELAYS);\n // 0 plain · 1 removals · 2 completed diff\n const tinted = stage >= 1;\n const settled = stage >= 2;\n const [accepted, setAccepted] = useState(false);\n const [edits, setEdits] = useState<Record<string, boolean>>({ rocky: true, bubblegum: true, pistachio: true });\n\n const removals = [\"rocky\", \"bubblegum\"].filter((key) => edits[key]).length;\n const additions = edits.pistachio ? 1 : 0;\n const showAdded = settled;\n\n const toggleEdit = (key: string) => setEdits((current) => ({ ...current, [key]: !current[key] }));\n\n return (\n <div className=\"w-full max-w-95\">\n <div className=\"relative overflow-hidden rounded-card bg-surface shadow-card\">\n <div className=\"primitive-card-bar flex items-center justify-between border-b border-line\">\n <span className=\"text-[12.5px] font-medium text-ink\">Proposed menu cleanup</span>\n {settled && !accepted && <span className=\"text-[11px] text-ink-3\">Click changed rows to toggle</span>}\n </div>\n\n <table className=\"w-full table-fixed border-collapse text-left\">\n <colgroup>\n <col className=\"w-[34%]\" />\n <col className=\"w-[30%]\" />\n <col className=\"w-[36%]\" />\n </colgroup>\n <thead>\n <tr className=\"border-b border-line\">\n {[\"Flavor\", \"Category\", \"Supplier\"].map((h) => (\n <th key={h} className=\"primitive-table-cell text-[12px] font-medium text-ink-3\">\n {h}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row) => {\n const out = row.removed && tinted && edits[row.key];\n const interactive = row.removed && settled && !accepted;\n return (\n <tr\n key={row.key}\n tabIndex={interactive ? 0 : undefined}\n aria-selected={row.removed ? edits[row.key] : undefined}\n onClick={interactive ? () => toggleEdit(row.key) : undefined}\n onKeyDown={interactive ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n toggleEdit(row.key);\n }\n } : undefined}\n className={`border-b border-line transition-[background-color,filter,opacity] duration-150 last:border-0 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-accent ${\n interactive ? \"cursor-pointer hover:brightness-[0.985]\" : \"\"\n }`}\n style={{ background: out ? \"var(--red-tint)\" : undefined }}\n >\n <td\n className=\"primitive-table-cell text-[13px] font-medium tabular-nums transition-colors duration-200\"\n style={{ color: out ? \"var(--red)\" : \"var(--ink)\" }}\n >\n {row.id}\n </td>\n <td className=\"primitive-table-cell\">\n <span\n className=\"inline-flex h-5.5 items-center gap-1.5 rounded-full bg-inset px-2 text-[11.5px] font-medium shadow-hairline transition-opacity duration-200\"\n style={{ opacity: out ? 0.55 : 1 }}\n >\n <span className={`size-1.5 rounded-full ${DOT[row.dept]}`} />\n <span className=\"text-ink-2\">{row.dept}</span>\n </span>\n </td>\n <td\n className=\"primitive-table-cell text-[12.5px] whitespace-nowrap transition-colors duration-200\"\n style={{\n color: out ? \"var(--red)\" : \"var(--ink-2)\",\n textDecorationLine: out ? \"line-through\" : \"none\",\n textDecorationColor: \"color-mix(in srgb, var(--red) 50%, transparent)\",\n }}\n >\n <span className=\"flex items-center justify-between gap-2\">\n <span className=\"min-w-0 truncate\">{row.email}</span>\n {row.removed && settled && <IncludedMark included={edits[row.key]} tone=\"red\" />}\n </span>\n </td>\n </tr>\n );\n })}\n {/* added row */}\n <tr>\n <td colSpan={3} className=\"p-0\">\n <div\n className=\"grid transition-[grid-template-rows,opacity] duration-200\"\n style={{\n gridTemplateRows: showAdded ? \"1fr\" : \"0fr\",\n opacity: showAdded ? 1 : 0,\n transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n }}\n >\n <div className=\"overflow-hidden\">\n <div\n role=\"checkbox\"\n tabIndex={accepted ? -1 : 0}\n aria-checked={edits.pistachio}\n aria-label=\"Include adding Pistachio\"\n onClick={accepted ? undefined : () => toggleEdit(\"pistachio\")}\n onKeyDown={accepted ? undefined : (event) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n toggleEdit(\"pistachio\");\n }\n }}\n className={`grid grid-cols-[34%_30%_36%] items-center border-t border-line transition-[background-color,filter,opacity] duration-150 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-accent ${\n accepted ? \"\" : \"cursor-pointer hover:brightness-[0.985]\"\n }`}\n style={{ background: edits.pistachio ? \"var(--green-tint)\" : undefined }}\n >\n <span className=\"primitive-table-cell text-[13px] font-medium tabular-nums transition-colors duration-200\" style={{ color: edits.pistachio ? \"var(--green)\" : \"var(--ink-3)\" }}>\n Pistachio\n </span>\n <span className=\"primitive-table-cell\">\n <span className=\"inline-flex h-5.5 items-center gap-1.5 rounded-full bg-surface px-2 text-[11.5px] font-medium shadow-hairline\">\n <span className=\"size-1.5 rounded-full bg-green\" />\n <span className=\"text-ink-2\">Seasonal</span>\n </span>\n </span>\n <span className=\"primitive-table-cell text-[13px] transition-colors duration-200\" style={{ color: edits.pistachio ? \"var(--green)\" : \"var(--ink-3)\" }}>\n <span className=\"flex items-center justify-between gap-2\">\n <span className=\"min-w-0 truncate\">maple-orbit</span>\n <IncludedMark included={edits.pistachio} tone=\"green\" />\n </span>\n </span>\n </div>\n </div>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n\n {/* footer — the summary follows the row-level selection */}\n {settled && (\n <div\n className=\"primitive-card-footer flex min-h-11 items-center justify-between border-t border-line\"\n style={{ animation: \"fade-up 180ms cubic-bezier(0.23,1,0.32,1) both\" }}\n >\n {accepted ? (\n <span\n className=\"inline-flex items-center gap-1.5 rounded-full bg-green-tint py-1 pr-2.5 pl-1 text-[12.5px] font-medium text-green\"\n style={{ animation: \"pop-in 180ms cubic-bezier(0.23,1,0.32,1) both\" }}\n >\n <span className=\"flex size-4.5 items-center justify-center rounded-full bg-green text-white\">\n <svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\"><path d=\"M20 6L9 17l-5-5\" /></svg>\n </span>\n {removals + additions} {removals + additions === 1 ? \"edit\" : \"edits\"} applied\n </span>\n ) : (\n <>\n <span className=\"text-[11.5px] tabular-nums text-ink-3\">\n {removals} {removals === 1 ? \"removal\" : \"removals\"} · {additions} {additions === 1 ? \"addition\" : \"additions\"}\n </span>\n <span className=\"flex items-center gap-1.5\">\n <Button\n variant=\"accent\"\n size=\"sm\"\n disabled={removals + additions === 0}\n onClick={() => {\n setAccepted(true);\n }}\n className=\"text-[12px]\"\n >\n Apply {removals + additions} {removals + additions === 1 ? \"change\" : \"changes\"}\n </Button>\n </span>\n </>\n )}\n </div>\n )}\n </div>\n </div>\n );\n}\n",
|
|
"type": "registry:component",
|
|
"target": "components/primitives/DiffTable.tsx"
|
|
}
|
|
]
|
|
} |