Resolve field-level differences between local and incoming changes, inspect the merged result, and save with retry.
Resolve changes
2 unresolved of 2 changed fields.
Choose each changed field before saving.
pnpm dlx shadcn@latest add @latheui/conflict-resolverPart of Lathe Pro. Set up your project once with your license key: npx latheui init --key <key>. See Lathe Pro.
Conflict Resolver is part of Lathe Pro
The preview is live and free to try. The source, the install and every assembly come with Pro: $79 once, with lifetime updates.
Depends on
| Prop | Type | Default | Description |
|---|---|---|---|
| fields* (required) | readonly ConflictField[] | — | Unique key, label and local/incoming strings for each field. Equal values need no decision. |
| onResolve* (required) | (merged: Record<string, string>, signal: AbortSignal) => void | Promise<void> | — | Persist the merged record. Reject to keep selections and allow retry; honor the signal to cancel. |
| title | string | "Resolve changes" | Visible heading and accessible section name. |
| disabled | boolean | false | Disable choices and saving. |
| className | string | — | Classes for the section. |
Replace the demo data and callbacks with your application. Honor AbortSignal in asynchronous callbacks so cancellation also stops transport work.
"use client"
import * as React from "react"
import { Checkbox } from "@/components/ui/checkbox"
import { ConflictResolver } from "@/components/conflict-resolver"
const fields = [
{
key: "name",
label: "Project name",
local: "Field notes",
incoming: "Field journal",
},
{
key: "description",
label: "Description",
local: "A collection of things worth keeping.",
incoming: "A quieter place for your best ideas.",
},
{ key: "owner", label: "Owner", local: "Maya Chen", incoming: "Maya Chen" },
]
export default function ConflictDemo() {
const [fail, setFail] = React.useState(false)
return (
<div className="w-full max-w-2xl space-y-3">
<ConflictResolver
fields={fields}
onResolve={async (_merged, signal) => {
await new Promise((resolve) => setTimeout(resolve, 450))
if (signal.aborted) return
if (fail) {
setFail(false)
throw new Error("Save interrupted. Your selections are preserved.")
}
}}
/>
<Checkbox
label="Fail next save"
checked={fail}
onCheckedChange={(checked) => setFail(checked === true)}
className="text-xs text-muted-foreground"
/>
</div>
)
}