KQ-1672 · DRAFT/KEEPER
Draft Keeper
A long form that keeps itself: edits queue and are sent as one save a moment after your last keystroke, with an indicator that counts the time since; offline edits pile up with a count that rolls on springs.snap, a failed save keeps the queue and offers Retry, and Ctrl or Cmd+S saves at once. When someone else edits a field you have also changed, their avatar drops onto it on springs.recoil and the field splits along its width on springs.glide into yours and theirs, each with the words the other lacks marked; keeping one slides it into the whole field as the other folds away on the exit ease. Tab reaches the choice and the arrows move between its two buttons.
Waylight 4.2 launch brief
Shared with Mira and Jonah
All changes saved.
summary · yours or mira's · autosave on
Install
One command — the source lands in your repo. Or copy it from the Code tab.
$ pnpm dlx shadcn@latest add @kinetiq/draft-keeper
Props
- values / defaultValues
- Record<string, string>
- default — / defaultDraftValues
- The document, controlled or initial.
- onValuesChange
- (values) => void
- default —
- Fires from your edits, kept versions and clean remote edits, with the whole document.
- fields
- DraftField[]
- default defaultDraftFields
- The form: { id, label, kind: "text" | "textarea" | "select" | "date", options?, placeholder?, hint?, wide? }.
- collaborators
- DraftCollaborator[]
- default defaultDraftCollaborators
- Who else is in the document: { id, name, hue }.
- incoming
- DraftRemoteEdit[]
- default []
- Remote edits pushed by the host, { id, field, value, by, at, base? }; each new id is taken once. Yours differing from `base`, or a field you touched, makes a conflict.
- me
- string
- default "You"
- Your name, for your avatar in the header.
- online
- boolean
- default true
- Offline, edits queue with a count; coming back sends the queue.
- onSave
- (values, changed: string[]) => void | Promise<void>
- default —
- Saves the queued changes. A promise shows Saving; a rejection keeps the queue and offers Retry.
- onConflict
- (field, yours, theirs, by) => void
- default —
- Someone else changed a field you had also changed; it has split into two versions.
- onResolve
- (field, kept: "yours" | "theirs", value) => void
- default —
- A version was kept.
- autosaveMs
- number
- default 1200
- Quiet time after your last keystroke before an autosave, in ms.
- lastSaved
- number
- default now − 12 s
- When the document was last saved, ms on the `now` clock.
- now
- number
- default defaultDraftNow
- The moment to count from, in ms; never read from the clock during render.
- indicatorStyle
- "pill" | "dot" | "line"
- default "pill"
- The save indicator: a bordered pill, a quiet dot, or a line along the panel's top edge that fills while saving. Below 30rem each shrinks to its icon.
- density
- "compact" | "cozy" | "roomy"
- default "cozy"
- Field heights and the room between them.
- tone
- "neutral" | "paper" | "slate"
- default "neutral"
- The surface temperature in both themes: plain card, warmed paper or cooled slate, for the panel, fields and version panes.
- title
- string
- default "Waylight 4.2 launch brief"
- The form's heading and accessible name.
- height
- number
- default 520
- Panel height in px; the form scrolls inside it.
- disabled
- boolean
- default false
- Nothing can be edited or saved.
- className
- string
- default —
- Classes for the outer container.
| Prop | Type | Default | Description |
|---|---|---|---|
| values / defaultValues | Record<string, string> | — / defaultDraftValues | The document, controlled or initial. |
| onValuesChange | (values) => void | — | Fires from your edits, kept versions and clean remote edits, with the whole document. |
| fields | DraftField[] | defaultDraftFields | The form: { id, label, kind: "text" | "textarea" | "select" | "date", options?, placeholder?, hint?, wide? }. |
| collaborators | DraftCollaborator[] | defaultDraftCollaborators | Who else is in the document: { id, name, hue }. |
| incoming | DraftRemoteEdit[] | [] | Remote edits pushed by the host, { id, field, value, by, at, base? }; each new id is taken once. Yours differing from `base`, or a field you touched, makes a conflict. |
| me | string | "You" | Your name, for your avatar in the header. |
| online | boolean | true | Offline, edits queue with a count; coming back sends the queue. |
| onSave | (values, changed: string[]) => void | Promise<void> | — | Saves the queued changes. A promise shows Saving; a rejection keeps the queue and offers Retry. |
| onConflict | (field, yours, theirs, by) => void | — | Someone else changed a field you had also changed; it has split into two versions. |
| onResolve | (field, kept: "yours" | "theirs", value) => void | — | A version was kept. |
| autosaveMs | number | 1200 | Quiet time after your last keystroke before an autosave, in ms. |
| lastSaved | number | now − 12 s | When the document was last saved, ms on the `now` clock. |
| now | number | defaultDraftNow | The moment to count from, in ms; never read from the clock during render. |
| indicatorStyle | "pill" | "dot" | "line" | "pill" | The save indicator: a bordered pill, a quiet dot, or a line along the panel's top edge that fills while saving. Below 30rem each shrinks to its icon. |
| density | "compact" | "cozy" | "roomy" | "cozy" | Field heights and the room between them. |
| tone | "neutral" | "paper" | "slate" | "neutral" | The surface temperature in both themes: plain card, warmed paper or cooled slate, for the panel, fields and version panes. |
| title | string | "Waylight 4.2 launch brief" | The form's heading and accessible name. |
| height | number | 520 | Panel height in px; the form scrolls inside it. |
| disabled | boolean | false | Nothing can be edited or saved. |
| className | string | — | Classes for the outer container. |