KQ-1681 · LEAVE/GUARD

Leave Guard

An unsaved-changes guard that shows exactly what would be lost: it wraps the page and lists every edited field as a compact diff, old value struck and new value in the tone. Discard plays the edits backwards one row at a time inside the 600ms budget — each new value slides back onto the old one by its measured offset on springs.glide while the strike retracts on glide — before the dialog leaves on the exit ease; Save fills as fields report in and draws a check per row on springs.flick, and a failure stays inside the dialog. It is an alertdialog with a focus trap: focus starts on Keep editing, Escape keeps editing and focus goes back to the field that was being edited.

KQ-1681 · LEAVE/GUARD
Gaugeworks · Settings

Riverside PS4

Lift station on the Riverside rising main · 3 pumps

Night modeQuiet alarms 22:00–06:00

Pumps

  • P1Duty412 h since service
  • P2Standby398 h since service
  • P3Standby7 h · new impeller
4 unsaved changes

4 unsaved · all stations to leave

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/leave-guard

Props

changes
LeaveGuardChange[]
default defaultLeaveGuardChanges
Every edited field: { id, label, section?, from, to } with string, number, boolean or null values.
open / defaultOpen
boolean
default — / false
Whether the guard stands over the page, controlled or initial.
onOpenChange
(open) => void
default —
Fires when the guard closes after a save, a discard or Keep editing.
onSave
(changes, report) => void | Promise<void>
default —
Save was pressed. Call report(id) as each field lands to fill the progress; a promise holds the dialog, and a rejection (with optional fields messages) keeps it open with the reason inside.
onDiscard
(changes) => void
default —
The unwind finished: every change was let go.
onRevert
(change) => void
default —
One row let go during the unwind, so the page behind can revert in step.
onCancel
() => void
default —
Keep editing or Escape: the guard closed and focus went back to the page.
children
ReactNode
default —
The guarded page; it goes inert behind the scrim while the guard is open.
title
string | (count) => string
default "You changed four things"
The heading; the default spells the count in words.
description
string
default "Keep them or let go."
The line under the heading.
labels
{ save?, discard?, cancel? }
default Save changes · Discard · Keep editing
The three actions' words.
formatValue
(value, change) => string
default On/Off, "empty", String()
How a value is written in the diff.
diffStyle
"inline" | "stacked" | "split"
default "inline"
Old struck → new on one line, a − line and a + line, or Before and After columns.
density
"compact" | "cozy" | "roomy"
default "cozy"
Row padding and type size.
tone
"calm" | "caution" | "stern"
default "caution"
The badge, the new values' ink, the markers and Discard's weight: cobalt, amber or brick.
disabled
boolean
default false
Save and Discard are unavailable; Keep editing still works.
className
string
default —
Classes for the wrapper.