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/defineEditableColumnsfields withf.*— you get compile-time narrowing per kind (currency carriescurrency/locale,enumcarriesvariants). - Use
f.enum({ render: "status" })for lifecycle/severity values (good/bad/urgent) andf.enum({ render: "label" })for neutral categories/roles. - Prefer a built-in kind over
f.custom; reach forf.customonly when no kind matches. - Wrap formatted cells with
renderCell(badge, extra copy,CellCriticalityValue) — do not replacekindwithf.customfor that job.
Features
| Kind | Reads as | Filters as |
|---|---|---|
f.text | plain text | string |
f.number / f.currency | formatted number / money | number |
f.date / f.datetime / f.time | formatted temporal | date |
f.enum({ render }) | Status Tag or Label Tag variant | enum |
f.boolean | true/false label | boolean |
f.email / f.phone / f.url | mailto / tel / external link | string |
f.password | masked dots | — |
f.link | anchor via href(row) | — |
f.file | File Upload list (done items) | — (filter: false by default) |
f.richText | Rich Text (HTML or JSON) | — (filter: false by default) |
f.custom | your render(value, row), then optional renderCell wrap | — |
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/field-types.jsonnpx shadcn@latest add https://ui.isaacfei.com/api/plus/r/field-types.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/field-types.jsonbun x shadcn@latest add https://ui.isaacfei.com/api/plus/r/field-types.jsonWith 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.
| Ada | Paid | Pro | 4,200.00 | 2026-01-04 |
| Lin | Pending | Standard | 3,100.00 | 2026-02-11 |
| Mei | Refunded | Standard | 760.00 | 2026-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-affordanceHover for copy
wf_7c2a9…7e0fAlways 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"—statusrenders a Status Tag (semantic tone);labelrenders a Label Tag (neutral category).variants: map of value →{ label, tone?, icon? }(status) or{ label, accent? }(label).computeDisplay: optional(row) => stringto 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 tofalse(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
| Value | Behavior |
|---|---|
true | Wrap the rendered cell with CopyAffordance; clipboard = String(value) for string/number/boolean |
{ text } | Override clipboard payload (string or (value) => string) |
| omitted / falsy | No 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 |
| Surfaces | Table, QueryList, Editable Table display (browse / readPretty / locked column text), Descriptions |
| Skip | Access masked / pending, or schema mask — hosts cannot decorate hidden values |
| Clipboard | copyable 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.