NIMA MZ

Import Patchbay

Forms 121

About

CSV import mapper built like a patchbay: each file column is a jack that patches a cable into one of your schema fields. It reads the sample values, so a column named col_7 full of dates still finds Signed up; matches it is unsure of hang dashed. Zero deps, drag or keyboard.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Import Patchbay to your project and fits it to your theme.

Code

How Import Patchbay is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client"

import * as React from "react"
// Relative import on purpose: the 21st CLI rewrites it to the published path (`@/components/ui/<slug>`)
// and to a temporary path during `21st render`. An absolute "@/…" import breaks the render build.
import {
  ImportPatchbay,
  type PatchColumn,
  type PatchField,
  type PatchMapping,
} from "@/components/ui/import-patchbay"

// A real CRM export, messy the way real exports are: a renamed column (`col_7`), a synonym
// (`mobile`), an abbreviation (`MRR (USD)`), and a second date column that makes the signup date
// genuinely ambiguous — which is why the matcher patches it dashed instead of pretending to be sure.
// The order is deliberate too: file order is never schema order, which is the whole reason this
// screen exists, so the cables cross instead of running in flat parallel lines.
const COLUMNS: PatchColumn[] = [
  { id: "c-first", name: "first_name", samples: ["Priya", "Jonas"] },
  { id: "c-last", name: "last_name", samples: ["Raman", "Weber"] },
  { id: "c-company", name: "Company / Org", samples: ["Northwind", "Lumen Labs"] },
  { id: "c-7", name: "col_7", samples: ["2026-03-14", "2026-01-09"] },
  {
    id: "c-email",
    name: "Email Address",
    samples: ["priya@northwind.io", "j.weber@lumen.dev"],
  },
  { id: "c-mobile", name: "mobile", samples: ["+1 415 555 0132", "+49 30 5550 118"] },
  { id: "c-mrr", name: "MRR (USD)", samples: ["$1,240", "$380"] },
  { id: "c-touch", name: "last_touch", samples: ["2026-08-30", "2026-08-27"] },
]

const FIELDS: PatchField[] = [
  { id: "email", label: "Email", required: true, kind: "email" },
  { id: "firstName", label: "First name", required: true, kind: "text" },
  { id: "lastName", label: "Last name", kind: "text" },
  { id: "company", label: "Company", kind: "text", synonyms: ["organisation", "account"] },
  { id: "phone", label: "Phone", kind: "phone" },
  {
    id: "signedUp",
    label: "Signed up",
    required: true,
    kind: "date",
    hint: "when they registered",
  },
  { id: "monthlyValue", label: "Monthly value", kind: "currency" },
]

// What someone would have patched by hand before giving up: the four columns whose names say it all.
const OBVIOUS: PatchMapping = {
  email: "c-email",
  firstName: "c-first",
  lastName: "c-last",
  company: "c-company",
}

// Module-level `settings` + a default export that accepts them as props gives this demo a live
// controls panel on 21st.dev. Signature knobs first, then look, then copy.
const settings = {
  // Off starts from a bare board, so the button wires all seven at once.
  startPatched: true,
  // Score a pair needs before a cable is proposed at all.
  threshold: 0.4,
  // Score a pair needs to be drawn solid rather than dashed and counted for review.
  sureThreshold: 0.68,
  // How far a cable droops, as a fraction of the gutter width.
  sag: 0.18,
  gutterWidth: 96,
  rowHeight: 46,
  showSamples: true,
  showKinds: true,
  fileName: "contacts-q3.csv",
  // One line at every width the cover is captured at: the card is the photograph, the headline
  // only has to name the screen.
  headline: "The import step, already wired.",
  subline:
    "Auto-patch reads the values, not just the header names, so col_7 full of dates finds Signed up.",
}

export default function Demo(props: Partial<typeof settings>) {
  const s = { ...settings, ...props }
  const [mapping, setMapping] = React.useState<PatchMapping>(s.startPatched ? OBVIOUS : {})
  const [imported, setImported] = React.useState<string[] | null>(null)

  React.useEffect(() => {
    setMapping(s.startPatched ? OBVIOUS : {})
    setImported(null)
  }, [s.startPatched])

  return (
    <div className="bg-background flex min-h-[max(560px,100svh)] w-full flex-col items-center justify-center px-5 py-10 sm:px-8">
      <div className="flex w-full max-w-3xl flex-col items-center">
        <h1 className="text-foreground text-center text-3xl font-semibold tracking-tight text-balance sm:text-5xl">
          {s.headline}
        </h1>
        <p className="text-muted-foreground mt-3 max-w-3xl text-center text-sm text-pretty sm:text-base">
          {s.subline}
        </p>

        <div className="mt-8 w-full">
          <ImportPatchbay
            columns={COLUMNS}
            fields={FIELDS}
            value={mapping}
            onValueChange={setMapping}
            onConfirm={(m) =>
              setImported(
                COLUMNS.filter((c) => !Object.values(m).includes(c.id)).map((c) => c.name)
              )
            }
            fileName={s.fileName}
            rowCount={1284}
            threshold={s.threshold}
            sureThreshold={s.sureThreshold}
            sag={s.sag}
            gutterWidth={s.gutterWidth}
            rowHeight={s.rowHeight}
            showSamples={s.showSamples}
            showKinds={s.showKinds}
            confirmLabel="Import 1,284 contacts"
          />
        </div>

        {/* Rendered only once it has something to say, so it costs the cover no height. */}
        {imported ? (
          <p aria-live="polite" className="text-muted-foreground mt-3 text-center text-xs">
            {`Imported 1,284 contacts. Skipped ${imported.length} ` +
              `column${imported.length === 1 ? "" : "s"}: ${imported.join(", ")}.`}
          </p>
        ) : null}
      </div>
    </div>
  )
}

More by NIMA MZ

View all
  • Agent Trace

    Agent Trace

    684
  • Sonar Grid

    Sonar Grid

    578
  • Receipt Pricing

    Receipt Pricing

    147
  • Lookout

    Lookout

    82
  • Moiré Field

    Moiré Field

    31
  • Meridian Rail

    Meridian Rail

    2
  • Split Flap Button

    Split Flap Button

    1
  • Ink Marbling

    Ink Marbling

More in Forms

View all
  • Premium Auth

    Premium Auth

    494
  • Login 1

    Login 1

    52
  • Authentication Card

    Authentication Card

    44
  • Login 2

    Login 2

    7
  • Form

    Form

    31
  • Login Card

    Login Card

    32
  • Card Studio

    Card Studio

    41
  • Form

    Form

    136