Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,68 @@ Returns `undefined` when no instance with that key is currently mounted. The han
| `'manual'` | Fires `onConflict`, library does nothing automatic |
| `false` | Disables multi-tab (no BroadcastChannel overhead) |

## Conflict UI

When `multiTab: 'warn'` fires a conflict, `draft.onConflictData` holds the remote values and `draft.resolveConflict` accepts `'local' | 'remote' | merged`. Building the merge UI by hand is the same code in every app, so formdraft ships two helpers from the `formdraft/ui` subpath (separate chunk — main bundle is unaffected).

### Drop-in: `<ConflictDialog>`

Renders a modal dialog showing only the fields that differ, with per-field "Yours / Other tab" buttons and "Keep all mine / Take all theirs" shortcuts. Renders `null` when there is no conflict, so you can mount it unconditionally:

```tsx
import { ConflictDialog } from 'formdraft/ui';

function ProfileForm() {
const draft = useFormDraft({ /* ... */ });
return (
<>
{/* your form */}
<ConflictDialog draft={draft} />
</>
);
}
```

Optional props: `title`, `fieldLabels` (per-field display names), `formatValue` (custom value renderer).

### Headless: `<ConflictResolver>`

Same orchestration with a render-prop API, so you bring your own markup:

```tsx
import { ConflictResolver } from 'formdraft/ui';

{draft.onConflictData && (
<ConflictResolver
local={draft.values}
remote={draft.onConflictData}
onResolve={draft.resolveConflict}
renderField={({ name, localValue, remoteValue, pickLocal, pickRemote, picked }) => (
<div>
<strong>{name}</strong>
<button onClick={pickLocal} aria-pressed={picked === 'local'}>
Mine: {String(localValue)}
</button>
<button onClick={pickRemote} aria-pressed={picked === 'remote'}>
Theirs: {String(remoteValue)}
</button>
</div>
)}
>
{({ fields, apply, canApply, pendingCount }) => (
<div>
{fields}
<button onClick={apply} disabled={!canApply}>
{pendingCount === 0 ? 'Apply' : `Apply (${pendingCount} left)`}
</button>
</div>
)}
</ConflictResolver>
)}
```

Diffing is shallow object-equality (`Object.is` per key) — sufficient for v0.2. Deep diff and string char-level diff are tracked for later.

## Zero runtime dependencies

formdraft has **no** runtime dependencies. Only peer deps (which you'd install anyway): `react`, optionally `react-hook-form`, optionally `zod`.
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
"import": "./dist/tanstack-form/index.mjs",
"require": "./dist/tanstack-form/index.js"
},
"./ui": {
"types": "./dist/ui/index.d.ts",
"import": "./dist/ui/index.mjs",
"require": "./dist/ui/index.js"
},
"./storage/indexedDB": {
"types": "./dist/storage/indexedDB.d.ts",
"import": "./dist/storage/indexedDB.mjs",
Expand Down
Loading
Loading