Skip to content
Product block

Archive and Restore

The bin with the clock on it: days until purge per row, who archived it, and a restore disabled with a reason when the parent has already gone.

277 lineslucide-reactAdded 13 Sept 2026
  • crud
  • archive
  • restore
  • soft delete
  • retention

What's included

  • components/crud-archive-restore.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add crud-archive-restore

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Archive

Archived records are kept for 90 days and then permanently removed. Sorted by whichever goes first.

4 archived records.

Archived records, soonest to be purged first
RecordArchivedPurged inActions
Warehouse relabellingProject9 Sep 2026by Sam Okafor3 days
Fabrikam Parts GmbHSupplier2 Sep 2026by Rhea Patel11 days
Q3 carrier tenderDocument28 Aug 2026by Jordan Lee16 daysCannot restore — its workspace was purged on 1 September
Returns triage rewriteProject21 Aug 2026by Rhea Patel44 days

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/crud-archive-restore.tsx
'use client'

/**
 * <CrudArchiveRestore> — the bin, and the clock running on it.
 *
 * Soft delete is the setting almost every internal tool turns on and almost
 * none of them builds a screen for. The row disappears from the list, a
 * `deleted_at` column fills in, and the only way to get it back is a support
 * request that ends in someone running SQL.
 *
 * WHAT AN ARCHIVE SCREEN OWES ITS USER
 *
 *  - **The deadline, per record, in days.** "Archived" is not a state, it is
 *    a countdown. A row with four days left and a row with ninety are not the
 *    same row, and sorting by soonest-purge is the default here for that
 *    reason.
 *  - **Who archived it, and when.** Restoring someone else's deletion without
 *    knowing it was deliberate is how the same record gets deleted twice.
 *  - **A real difference between restore and purge.** Restore is one click
 *    and reversible. Purge is guarded and final, and they must not sit next
 *    to each other looking alike — so purge is destructive-styled, out of the
 *    row, and only reachable through selection.
 *
 * THE RESTORE CAN FAIL, AND THE REASON IS INTERESTING
 *
 * A record whose parent has been purged cannot come back to anything. Rather
 * than a generic error on click, those rows are marked up front and their
 * Restore is disabled with the reason — the workspace they belonged to is
 * gone, so restoring would produce an orphan. This is the counterpart of the
 * "orphaned" fate in `crud-delete-cascade`, seen from the other end.
 *
 * ACCESSIBILITY: a real `<table>` with scoped column headers, because this is
 * tabular data and a list of divs is not. Selection is a checkbox per row
 * labelled with the record's name, and the header checkbox reflects the
 * indeterminate state through the DOM property — `indeterminate` is not an
 * attribute and cannot be set in JSX. The purge bar is a live region.
 */

import * as React from 'react'
import { ArchiveRestore, Ban, Clock, Trash2 } from 'lucide-react'

export interface ArchivedRecord {
  id: string
  name: string
  kind: string
  archivedBy: string
  archivedOn: string
  /** Days until it is purged. Small numbers are styled as urgent. */
  daysLeft: number
  /** Set when the record cannot be restored, with the reason. */
  blocked?: string
}

export interface CrudArchiveRestoreProps {
  records?: ArchivedRecord[]
  retentionDays?: number
  className?: string
}

const DEFAULT_RECORDS: ArchivedRecord[] = [
  {
    id: 'prj-91',
    name: 'Warehouse relabelling',
    kind: 'Project',
    archivedBy: 'Sam Okafor',
    archivedOn: '9 Sep 2026',
    daysLeft: 3,
  },
  {
    id: 'sup-208',
    name: 'Fabrikam Parts GmbH',
    kind: 'Supplier',
    archivedBy: 'Rhea Patel',
    archivedOn: '2 Sep 2026',
    daysLeft: 11,
  },
  {
    id: 'doc-4412',
    name: 'Q3 carrier tender',
    kind: 'Document',
    archivedBy: 'Jordan Lee',
    archivedOn: '28 Aug 2026',
    daysLeft: 16,
    blocked: 'its workspace was purged on 1 September',
  },
  {
    id: 'prj-88',
    name: 'Returns triage rewrite',
    kind: 'Project',
    archivedBy: 'Rhea Patel',
    archivedOn: '21 Aug 2026',
    daysLeft: 44,
  },
]

export function CrudArchiveRestore({
  records = DEFAULT_RECORDS,
  retentionDays = 90,
  className = '',
}: CrudArchiveRestoreProps) {
  const uid = React.useId()
  const [rows, setRows] = React.useState<ArchivedRecord[]>(
    // Soonest purge first: a countdown list sorted by name is a list that
    // buries the row about to disappear.
    [...records].sort((a, b) => a.daysLeft - b.daysLeft),
  )
  const [selected, setSelected] = React.useState<string[]>([])
  const [restored, setRestored] = React.useState<string | null>(null)
  const headerBox = React.useRef<HTMLInputElement>(null)

  const selectable = rows.filter((row) => !row.blocked)
  const allSelected = selectable.length > 0 && selected.length === selectable.length

  React.useEffect(() => {
    // `indeterminate` is a DOM property, not an attribute — it cannot be set
    // from JSX and has to be written after render.
    if (headerBox.current) {
      headerBox.current.indeterminate = selected.length > 0 && !allSelected
    }
  }, [selected.length, allSelected])

  function restore(row: ArchivedRecord) {
    setRows((r) => r.filter((item) => item.id !== row.id))
    setSelected((s) => s.filter((id) => id !== row.id))
    setRestored(row.name)
  }

  return (
    <section className={`bg-background px-4 py-10 sm:px-6 ${className}`}>
      <div className="mx-auto max-w-4xl">
        <header className="flex flex-wrap items-end justify-between gap-4">
          <div>
            <h2 className="text-xl font-bold tracking-tight">Archive</h2>
            <p className="mt-1 text-sm text-muted-foreground">
              Archived records are kept for {retentionDays} days and then
              permanently removed. Sorted by whichever goes first.
            </p>
          </div>
          <p aria-live="polite" className="text-sm text-muted-foreground">
            {restored ? `${restored} restored.` : `${rows.length} archived records.`}
          </p>
        </header>

        {selected.length > 0 ? (
          <div
            role="status"
            className="mt-5 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-destructive/40 bg-destructive/5 p-3"
          >
            <p className="text-sm">
              <span className="font-semibold">{selected.length} selected.</span>{' '}
              <span className="text-muted-foreground">
                Purging removes them now, before the retention clock runs out.
              </span>
            </p>
            <button
              type="button"
              className="inline-flex items-center gap-1.5 rounded-lg bg-destructive px-3 py-1.5 text-sm font-semibold text-destructive-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
            >
              <Trash2 aria-hidden className="h-4 w-4" />
              Purge {selected.length} permanently
            </button>
          </div>
        ) : null}

        {/* `relative` on the scroller: sr-only is position:absolute, and a
            static overflow-x container does not clip it — the label escapes
            and scrolls the whole page sideways on a phone. */}
        <div className="relative mt-5 overflow-x-auto rounded-xl border border-border">
          <table className="w-full min-w-[42rem] border-collapse text-sm">
            <caption className="sr-only">
              Archived records, soonest to be purged first
            </caption>
            <thead>
              <tr className="border-b border-border bg-muted/50 text-start">
                <th scope="col" className="w-10 px-3 py-2.5">
                  <input
                    ref={headerBox}
                    type="checkbox"
                    checked={allSelected}
                    onChange={(e) =>
                      setSelected(e.target.checked ? selectable.map((r) => r.id) : [])
                    }
                    aria-label="Select every restorable record"
                    className="h-4 w-4 rounded border-border accent-primary"
                  />
                </th>
                <th scope="col" className="px-3 py-2.5 text-start font-semibold">
                  Record
                </th>
                <th scope="col" className="px-3 py-2.5 text-start font-semibold">
                  Archived
                </th>
                <th scope="col" className="px-3 py-2.5 text-start font-semibold">
                  Purged in
                </th>
                <th scope="col" className="px-3 py-2.5 text-end font-semibold">
                  <span className="sr-only">Actions</span>
                </th>
              </tr>
            </thead>
            <tbody>
              {rows.map((row) => {
                const urgent = row.daysLeft <= 7
                return (
                  <tr key={row.id} className="border-b border-border/70 last:border-0">
                    <td className="px-3 py-3">
                      <input
                        type="checkbox"
                        id={`${uid}-${row.id}`}
                        checked={selected.includes(row.id)}
                        disabled={Boolean(row.blocked)}
                        onChange={(e) =>
                          setSelected((s) =>
                            e.target.checked ? [...s, row.id] : s.filter((id) => id !== row.id),
                          )
                        }
                        className="h-4 w-4 rounded border-border accent-primary disabled:opacity-40"
                      />
                      <label htmlFor={`${uid}-${row.id}`} className="sr-only">
                        Select {row.name}
                      </label>
                    </td>
                    <td className="px-3 py-3">
                      <span className="block font-medium">{row.name}</span>
                      <span className="block text-xs text-muted-foreground">{row.kind}</span>
                    </td>
                    <td className="px-3 py-3 text-muted-foreground">
                      <span className="block">{row.archivedOn}</span>
                      <span className="block text-xs">by {row.archivedBy}</span>
                    </td>
                    <td className="px-3 py-3">
                      <span
                        className={`inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold ${
                          urgent
                            ? 'bg-destructive/10 text-destructive'
                            : 'bg-muted text-muted-foreground'
                        }`}
                      >
                        <Clock aria-hidden className="h-3 w-3" />
                        {row.daysLeft} days
                      </span>
                    </td>
                    <td className="px-3 py-3 text-end">
                      {row.blocked ? (
                        <span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
                          <Ban aria-hidden className="h-3.5 w-3.5" />
                          Cannot restore — {row.blocked}
                        </span>
                      ) : (
                        <button
                          type="button"
                          onClick={() => restore(row)}
                          className="inline-flex items-center gap-1.5 rounded-lg border border-border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                        >
                          <ArchiveRestore aria-hidden className="h-3.5 w-3.5" />
                          Restore
                          <span className="sr-only"> {row.name}</span>
                        </button>
                      )}
                    </td>
                  </tr>
                )
              })}
            </tbody>
          </table>
        </div>

        {rows.length === 0 ? (
          <p className="mt-5 rounded-xl border border-dashed border-border p-6 text-center text-sm text-muted-foreground">
            The archive is empty. Records deleted from a list land here first.
          </p>
        ) : null}
      </div>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/crud-archive-restore.tsx and import it where you need the section:

import { CrudArchiveRestore } from '@/components/crud-archive-restore'

Customize

1 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
recordsArchivedRecord[]DEFAULT_RECORDS
retentionDaysnumber90
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

crud-archive-restore.html
<!--
  Archive and Restore — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section class="bg-background px-4 py-10 sm:px-6 ">
  <div class="mx-auto max-w-4xl">
    <header class="flex flex-wrap items-end justify-between gap-4">
      <div>
        <h2 class="text-xl font-bold tracking-tight">Archive</h2>
        <p class="mt-1 text-sm text-muted-foreground">Archived records are kept for 90 days and then permanently removed. Sorted by whichever goes first.</p>
      </div>
      <p aria-live="polite" class="text-sm text-muted-foreground">4 archived records.</p>
    </header>
    <div class="relative mt-5 overflow-x-auto rounded-xl border border-border">
      <table class="w-full min-w-[42rem] border-collapse text-sm">
        <caption class="sr-only">Archived records, soonest to be purged first</caption>
        <thead>
          <tr class="border-b border-border bg-muted/50 text-start">
            <th scope="col" class="w-10 px-3 py-2.5">
              <input type="checkbox" aria-label="Select every restorable record" class="h-4 w-4 rounded border-border accent-primary" />
            </th>
            <th scope="col" class="px-3 py-2.5 text-start font-semibold">Record</th>
            <th scope="col" class="px-3 py-2.5 text-start font-semibold">Archived</th>
            <th scope="col" class="px-3 py-2.5 text-start font-semibold">Purged in</th>
            <th scope="col" class="px-3 py-2.5 text-end font-semibold">
              <span class="sr-only">Actions</span>
            </th>
          </tr>
        </thead>
        <tbody>
          <tr class="border-b border-border/70 last:border-0">
            <td class="px-3 py-3">
              <input type="checkbox" id="_R_0_-prj-91" class="h-4 w-4 rounded border-border accent-primary disabled:opacity-40" />
              <label for="_R_0_-prj-91" class="sr-only">Select Warehouse relabelling</label>
            </td>
            <td class="px-3 py-3">
              <span class="block font-medium">Warehouse relabelling</span>
              <span class="block text-xs text-muted-foreground">Project</span>
            </td>
            <td class="px-3 py-3 text-muted-foreground">
              <span class="block">9 Sep 2026</span>
              <span class="block text-xs">by Sam Okafor</span>
            </td>
            <td class="px-3 py-3">
              <span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold bg-destructive/10 text-destructive"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3 w-3" aria-hidden="true"><path d="M12 6v6l4 2"></path><circle cx="12" cy="12" r="10"></circle></svg>3 days</span>
            </td>
            <td class="px-3 py-3 text-end">
              <button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-archive-restore h-3.5 w-3.5" aria-hidden="true"><rect width="20" height="5" x="2" y="3" rx="1"></rect><path d="M4 8v11a2 2 0 0 0 2 2h2"></path><path d="M20 8v11a2 2 0 0 1-2 2h-2"></path><path d="m9 15 3-3 3 3"></path><path d="M12 12v9"></path></svg>Restore<span class="sr-only"> Warehouse relabelling</span></button>
            </td>
          </tr>
          <tr class="border-b border-border/70 last:border-0">
            <td class="px-3 py-3">
              <input type="checkbox" id="_R_0_-sup-208" class="h-4 w-4 rounded border-border accent-primary disabled:opacity-40" />
              <label for="_R_0_-sup-208" class="sr-only">Select Fabrikam Parts GmbH</label>
            </td>
            <td class="px-3 py-3">
              <span class="block font-medium">Fabrikam Parts GmbH</span>
              <span class="block text-xs text-muted-foreground">Supplier</span>
            </td>
            <td class="px-3 py-3 text-muted-foreground">
              <span class="block">2 Sep 2026</span>
              <span class="block text-xs">by Rhea Patel</span>
            </td>
            <td class="px-3 py-3">
              <span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold bg-muted text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3 w-3" aria-hidden="true"><path d="M12 6v6l4 2"></path><circle cx="12" cy="12" r="10"></circle></svg>11 days</span>
            </td>
            <td class="px-3 py-3 text-end">
              <button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-archive-restore h-3.5 w-3.5" aria-hidden="true"><rect width="20" height="5" x="2" y="3" rx="1"></rect><path d="M4 8v11a2 2 0 0 0 2 2h2"></path><path d="M20 8v11a2 2 0 0 1-2 2h-2"></path><path d="m9 15 3-3 3 3"></path><path d="M12 12v9"></path></svg>Restore<span class="sr-only"> Fabrikam Parts GmbH</span></button>
            </td>
          </tr>
          <tr class="border-b border-border/70 last:border-0">
            <td class="px-3 py-3">
              <input type="checkbox" id="_R_0_-doc-4412" disabled="" class="h-4 w-4 rounded border-border accent-primary disabled:opacity-40" />
              <label for="_R_0_-doc-4412" class="sr-only">Select Q3 carrier tender</label>
            </td>
            <td class="px-3 py-3">
              <span class="block font-medium">Q3 carrier tender</span>
              <span class="block text-xs text-muted-foreground">Document</span>
            </td>
            <td class="px-3 py-3 text-muted-foreground">
              <span class="block">28 Aug 2026</span>
              <span class="block text-xs">by Jordan Lee</span>
            </td>
            <td class="px-3 py-3">
              <span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold bg-muted text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3 w-3" aria-hidden="true"><path d="M12 6v6l4 2"></path><circle cx="12" cy="12" r="10"></circle></svg>16 days</span>
            </td>
            <td class="px-3 py-3 text-end">
              <span class="inline-flex items-center gap-1.5 text-xs text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ban h-3.5 w-3.5" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="m4.9 4.9 14.2 14.2"></path></svg>Cannot restore — its workspace was purged on 1 September</span>
            </td>
          </tr>
          <tr class="border-b border-border/70 last:border-0">
            <td class="px-3 py-3">
              <input type="checkbox" id="_R_0_-prj-88" class="h-4 w-4 rounded border-border accent-primary disabled:opacity-40" />
              <label for="_R_0_-prj-88" class="sr-only">Select Returns triage rewrite</label>
            </td>
            <td class="px-3 py-3">
              <span class="block font-medium">Returns triage rewrite</span>
              <span class="block text-xs text-muted-foreground">Project</span>
            </td>
            <td class="px-3 py-3 text-muted-foreground">
              <span class="block">21 Aug 2026</span>
              <span class="block text-xs">by Rhea Patel</span>
            </td>
            <td class="px-3 py-3">
              <span class="inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold bg-muted text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-3 w-3" aria-hidden="true"><path d="M12 6v6l4 2"></path><circle cx="12" cy="12" r="10"></circle></svg>44 days</span>
            </td>
            <td class="px-3 py-3 text-end">
              <button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-archive-restore h-3.5 w-3.5" aria-hidden="true"><rect width="20" height="5" x="2" y="3" rx="1"></rect><path d="M4 8v11a2 2 0 0 0 2 2h2"></path><path d="M20 8v11a2 2 0 0 1-2 2h-2"></path><path d="m9 15 3-3 3 3"></path><path d="M12 12v9"></path></svg>Restore<span class="sr-only"> Returns triage rewrite</span></button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

11 more blocks in CRUD

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse CRUD

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More CRUD blocks

View category
Open the full page for this block

Create Record Form

A full-page create form with a guidance rail and a Save-and-create-another that keeps the shared fields - the shape a record with more than six fields actually needs.

CRUD408 lines1 dep
Open the full page for this block

Create Record Modal

Three fields over the list they add to, with the new row appearing behind the dialog rather than after it closes.

CRUD262 lines1 dep
Open the full page for this block

Create Record Drawer

A long create form in a side drawer: the fields scroll, the header and the action bar do not, and the list stays legible beside it.

CRUD336 lines1 dep
Open the full page for this block

Record Detail Page

The read view arranged by what a reader came for - a verdict band of four facts, then details, then the related records a field dump can never show.

CRUD314 lines1 dep
Open the full page for this block

Record Quick Look

The verification modal from a table row: copyable identifiers, no editing, nothing scrolling, and the row still marked behind it.

CRUD244 lines1 dep
Open the full page for this block

Edit Record Form

An edit page that shows the diff - per-field change markers carrying the previous value, a per-field revert, and a Save button that counts what it will write.

CRUD259 lines1 dep
Open the full page for this block

Edit Record Modal

The edit modal including the parts nobody draws: the in-flight state, the rejected save that keeps your typing, and the conflict when someone else got there first.

CRUD248 lines1 dep
Open the full page for this block

Field-Level Edit Drawer

The save-on-blur model, with per-field confirmation, per-field failure and a written-in-this-session log - the only honest answer to what did I just change.

CRUD327 lines1 dep
Open the full page for this block

Cascade Delete Dialog

Deleting a record other records depend on: dependents counted and grouped by fate - deleted, orphaned, retained - with a guard that scales to the blast radius.

CRUD262 lines1 dep
Open the full page for this block

Record Created State

The screen after the write: the reference copyable, next actions ranked rather than listed, and an undo window that counts down honestly and then says why it is gone.

CRUD260 lines1 dep
Open the full page for this block

Bulk Edit Drawer

One change applied to many records that disagree - mixed values shown as mixed, untouched fields never written, and skipped rows counted before you commit.

CRUD229 lines1 dep