f-ui
Components

Field Types

The typed f.* catalog that authors table, list, and editable-table schemas — one field kind per data shape.

Plus Registry

This component ships from registry.plus.json, not the public registry.json. Set up @f-ui-plus and a token on the Installation page, then install @f-ui-plus/field-types.

The f.* catalog is the typed authoring surface for Table, QueryList, and Editable Table schemas. Each factory returns a field config whose kind drives one registered Field Type — a single definition of how a value reads, filters, and edits across every surface.

When To Use

  • Always author defineDataListSchema / defineEditableColumns fields with f.* — you get compile-time narrowing per kind (currency carries currency/locale, enum carries variants).
  • Use f.enum({ render: "status" }) for lifecycle/severity values (good/bad/urgent) and f.enum({ render: "label" }) for neutral categories/roles.
  • Prefer a built-in kind over f.custom; reach for f.custom only when no kind matches.
  • Wrap formatted cells with renderCell (badge, extra copy, CellCriticalityValue) — do not replace kind with f.custom for that job.

Features

KindReads asFilters as
f.textplain textstring
f.number / f.currencyformatted number / moneynumber
f.date / f.datetime / f.timeformatted temporaldate
f.enum({ render })Status Tag or Label Tag variantenum
f.booleantrue/false labelboolean
f.email / f.phone / f.urlmailto / tel / external linkstring
f.passwordmasked dots
f.linkanchor via href(row)
f.fileFile Upload list (done items)— (filter: false by default)
f.richTextRich Text (HTML or JSON)— (filter: false by default)
f.customyour render(value, row), then optional renderCell wrap

Installing

pnpm dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/field-types.json
npx shadcn@latest add https://ui.isaacfei.com/api/plus/r/field-types.json
yarn dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/field-types.json
bun x shadcn@latest add https://ui.isaacfei.com/api/plus/r/field-types.json

With a namespace: npx shadcn@latest add @f-ui-plus/field-types.

Usage

import { defineDataListSchema } from '@/components/f-ui/data-list-internals/schema/define-data-list-schema';
import { f } from '@/components/f-ui/field-types/catalog';

const schema = defineDataListSchema<Order>({
  customer: f.text({ label: 'Customer', sortable: true }),
  status: f.enum({
    label: 'Status',
    render: 'status',
    variants: { paid: { label: 'Paid', tone: 'success' } },
  }),
  amount: f.currency({ label: 'Amount', currency: 'USD', measure: 'none' }),
});

Examples

Authoring a Table Schema

The catalog in one schema — text, enum in both status and label modes, currency, and date. Sort a column and note the tag tones.

AdaPaidPro4,200.002026-01-04
LinPendingStandard3,100.002026-02-11
MeiRefundedStandard760.002026-03-02
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";
import { useDataList } from "@/components/f-ui/data-list-internals/use-data-list";
import { f } from "@/components/f-ui/field-types/catalog";
import { Table } from "@/components/f-ui/table/table";

interface Order {
  id: string;
  customer: string;
  status: "paid" | "pending" | "refunded";
  tier: "std" | "pro";
  amount: number;
  createdAt: string;
}

const ORDERS: Order[] = [
  { id: "1", customer: "Ada", status: "paid", tier: "pro", amount: 4200, createdAt: "2026-01-04" },
  { id: "2", customer: "Lin", status: "pending", tier: "std", amount: 3100, createdAt: "2026-02-11" },
  { id: "3", customer: "Mei", status: "refunded", tier: "std", amount: 760, createdAt: "2026-03-02" },
];

const orderSchema = defineDataListSchema<Order>({
  customer: f.text({ label: "Customer", sortable: true }),
  status: f.enum({
    label: "Status",
    render: "status",
    variants: {
      paid: { label: "Paid", tone: "success" },
      pending: { label: "Pending", tone: "warning" },
      refunded: { label: "Refunded", tone: "destructive" },
    },
  }),
  tier: f.enum({
    label: "Tier",
    render: "label",
    variants: { std: { label: "Standard" }, pro: { label: "Pro", accent: "auto" } },
  }),
  amount: f.currency({ label: "Amount", currency: "USD", sortable: true }),
  createdAt: f.date({ label: "Created", sortable: true }),
});

const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });

function FieldTypesDemoInner() {
  const handle = useDataList({
    schema: orderSchema,
    listCode: "field-types-demo",
    data: ORDERS,
    getRowId: (r) => r.id,
    defaultSort: [{ field: "customer", order: "asc" }],
  });
  return (
    <div className="rounded-xl border bg-card p-4 shadow-sm">
      <Table dataList={handle} columns={{ amount: { align: "end" } }} />
    </div>
  );
}

/** The `f.*` catalog authoring a table schema — text, enum (status + label), currency, date. */
export function FieldTypesDemo() {
  return (
    <QueryClientProvider client={queryClient}>
      <FieldTypesDemoInner />
    </QueryClientProvider>
  );
}

Copyable Fields

Set copyable: true (or a CopyableConfig) on any field. Table and Descriptions both attach a lightweight hover Copy Affordance through renderCellByKind — same mount as ProComponents’ genCopyable, not a separate product chip.

const schema = defineDataListSchema<Order>({
  orderId: f.text({ label: "Order ID", copyable: true }),
  customer: f.text({ label: "Customer" }),
});

Hover the Order ID value and click copy.

Order ID
ORD-2026-004821
Customer
Acme Corp
Amount
USD 1,280.00
"use client";

import { Descriptions } from "@/components/f-ui/descriptions/descriptions";
import { descriptionsFieldsFromSchema } from "@/components/f-ui/descriptions/lib/descriptions-field";
import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";
import { f } from "@/components/f-ui/field-types/catalog";

type Row = {
  orderId: string;
  customer: string;
  amount: number;
};

const schema = defineDataListSchema<Row>({
  orderId: f.text({ label: "Order ID", copyable: true }),
  customer: f.text({ label: "Customer" }),
  amount: f.currency({ label: "Amount", currency: "USD" }),
});

const record: Row = {
  orderId: "ORD-2026-004821",
  customer: "Acme Corp",
  amount: 1280,
};

const fields = descriptionsFieldsFromSchema(schema);

/**
 * Schema-first copyable: set `copyable: true` on FieldConfig.
 * Descriptions (and Table) attach CopyAffordance via renderCellByKind.
 */
export function FieldCopyableDemo() {
  return (
    <Descriptions
      record={record}
      fields={fields}
      column={1}
      size="small"
      className="max-w-md"
    />
  );
}

Access masked / pending fields never get a copy control — see Access. Prefer not enabling copyable on secrets; the gate is a UX guard, not a security boundary.

Wrap Formatted Cells

Set renderCell on any f.* field to wrap the already-formatted node. Qty stays a number; the demo adds Bulk. The same schema field paints in Table, QueryList, Editable Table display cells, and Descriptions — the kernel applies renderCell once.

Order ID
ORD-2026-004821
Qty
12Bulk
Amount
USD 1,280.00
"use client";

import { Descriptions } from "@/components/f-ui/descriptions/descriptions";
import { descriptionsFieldsFromSchema } from "@/components/f-ui/descriptions/lib/descriptions-field";
import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";
import { f } from "@/components/f-ui/field-types/catalog";

type Row = {
  orderId: string;
  qty: number;
  amount: number;
};

const schema = defineDataListSchema<Row>({
  orderId: f.text({ label: "Order ID" }),
  qty: f.number({
    label: "Qty",
    renderCell: (formatted, row) =>
      row.qty >= 10 ? (
        <span className="inline-flex items-center gap-1">
          {formatted}
          <span className="text-muted-foreground text-xs">Bulk</span>
        </span>
      ) : (
        formatted
      ),
  }),
  amount: f.currency({ label: "Amount", currency: "USD" }),
});

const record: Row = {
  orderId: "ORD-2026-004821",
  qty: 12,
  amount: 1280,
};

const fields = descriptionsFieldsFromSchema(schema);

/** Same `renderCell` wrap on Descriptions — kernel is shared with Table. */
export function FieldTypesRenderCellDemo() {
  return (
    <Descriptions
      record={record}
      fields={fields}
      column={1}
      size="small"
      className="max-w-md"
    />
  );
}

Do not use f.custom for a suffix or badge. Table compositions (wash, criticality, status + badge): Table — Cell Customization.

Ad-Hoc Copy Affordance

For layouts outside a schema (install commands, one-off IDs), compose the Open CopyAffordance lib with your own display. Truncation is a pure helper — not baked into the affordance.

npx shadcn@latest add @f-ui/copy-affordance

Hover for copy

wf_7c2a9…7e0f

Always visible

wf_7c2a9f1…4c7e0f
"use client";

import { CopyAffordance } from "@/components/f-ui/copy-affordance/copy-affordance";
import { formatTruncatedText } from "@/components/f-ui/copy-affordance/format-truncated-text";

const WORKFLOW_ID = "wf_7c2a9f1e4b8d3a0c5e6f9a2b1d4c7e0f";

/**
 * Ad-hoc ID layout: compose CopyAffordance with your own display
 * (truncate helper + optional mono). Not a product chip component.
 */
export function CopyAffordanceDemo() {
  return (
    <div className="flex flex-col gap-4 text-sm">
      <div className="space-y-1">
        <p className="text-muted-foreground text-xs font-medium">Hover for copy</p>
        <CopyAffordance text={WORKFLOW_ID}>
          <code className="bg-muted/60 rounded px-1.5 py-0.5 font-mono text-xs">
            {formatTruncatedText(WORKFLOW_ID, {
              mode: "middle",
              prefix: 8,
              suffix: 4,
            })}
          </code>
        </CopyAffordance>
      </div>
      <div className="space-y-1">
        <p className="text-muted-foreground text-xs font-medium">Always visible</p>
        <CopyAffordance text={WORKFLOW_ID} visibility="always">
          <code className="bg-muted/60 rounded px-1.5 py-0.5 font-mono text-xs">
            {formatTruncatedText(WORKFLOW_ID, { mode: "middle", prefix: 10, suffix: 6 })}
          </code>
        </CopyAffordance>
      </div>
    </div>
  );
}

API Reference

f.enum

  • render: "status" | "label"status renders a Status Tag (semantic tone); label renders a Label Tag (neutral category).
  • variants: map of value → { label, tone?, icon? } (status) or { label, accent? } (label).
  • computeDisplay: optional (row) => string to derive the variant key from the row.

f.richText

  • format: "html" (default) or "json" — reads through Rich Text; Formily edits resolve to Rich Text Editor.
  • filter: defaults to false (rich documents are not query-filterable).
  • Table cells should render read-only; do not rely on automatic inline rich editing.

Other Factories

Each f.<kind> accepts that kind's config minus kind — see the Features table for the read/filter behavior. All factories accept the shared label, accessor, sortable, filter, hidden, access, copyable, and renderCell fields.

copyable

ValueBehavior
trueWrap the rendered cell with CopyAffordance; clipboard = String(value) for string/number/boolean
{ text }Override clipboard payload (string or (value) => string)
omitted / falsyNo copy control

Masked / pending Access decisions skip the wrap. Install the Open lib for ad-hoc use: @f-ui/copy-affordance.

renderCell

Wrap the already-formatted cell (Ant Design Pro render(dom, entity)). Does not replace kind. Coexists with enum render: "status" | "label". Read/display surfaces only — never wraps an editing control.

Signature(formatted: ReactNode, row) => ReactNode
SurfacesTable, QueryList, Editable Table display (browse / readPretty / locked column text), Descriptions
SkipAccess masked / pending, or schema mask — hosts cannot decorate hidden values
Clipboardcopyable copies layer 2 (kind / copyable.text), not the wrapped DOM

Live wrap: Wrap Formatted Cells. Table layers: Cell Customization. For cell attention, wrap with CellCriticalityValue inside renderCell. Paint the <td> with Table columns.cellClassName.

On this page