Data List View
Shared typed-cell, row-action, density, and overlay primitives used by Table, List View, and Descriptions.
Plus Registry
Data List View ships on the authenticated @f-ui-plus registry. Configure FUI_PLUS_REGISTRY_TOKEN as described in Installation — Plus Registry.
Data List View is the shared paint layer for list and detail read cells: renderCellByKind, empty placeholders, row-action overflow, density, and the body spin overlay. You rarely mount this package alone — Table, List View, and Descriptions already call it. Install it when you compose a custom body or put DataListRowActionsCell on presentational List.
Field kinds and copyable / renderCell live on Field Types. Row-action layout rules live on Row Actions Column.
When To Use
- Compose
DataListRowActionsCellinto Listactionswhen a related-list row has more than two ops. - Call
renderCellByKindin a custom cell / detail slot so currency, status, and copy match Table. - Use
EmptyCellPlaceholderfor pretty / display empty — not inside a locked fill-in control. See Empty Value Placeholder. - Prefer Query List / Table when you want the full list recipe. This package is not a list page.
- Do not invent a second formatter beside
kind— wrap already-formatted nodes with fieldrenderCell.
Features
| Area | Behavior |
|---|---|
| Cells | renderCellByKind dispatches kind → registered read slot (variant="table" truncates; "detail" wraps) |
| Empty | EmptyCellPlaceholder — muted em dash for display cells |
| Overlay | DataListBodySpinOverlay — nested loading veil on the scroll viewport, not the CRUD page default |
| Density | useDataListDensity — controlled or uncontrolled "compact" | "normal" | "comfortable" |
| Row ops | DataListRowActionsCell — inline cluster + overflow + optional trailing destructive |
| i18n | useDataListViewI18n — chrome strings (actions menu, overlay). Host I18nProvider applies |
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/data-list-view.jsonnpx shadcn@latest add https://ui.isaacfei.com/api/plus/r/data-list-view.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/data-list-view.jsonbunx shadcn@latest add https://ui.isaacfei.com/api/plus/r/data-list-view.jsonWith the @f-ui-plus namespace configured in components.json, shadcn add @f-ui-plus/data-list-view also works.
Runtime npm deps: date-fns, lucide-react. registryDependencies: @f-ui-plus/data-list-internals, @f-ui-plus/field-types, open f-ui (i18n, Currency Format, Empty Value Placeholder, Label Tag, Status Tag, Access, Copy Affordance, Confirm, Link), shadcn button, dropdown-menu, popover, tooltip.
Usage
import { renderCellByKind } from "@/components/f-ui/data-list-view/render-cell-by-kind";
renderCellByKind(value, row, field, { variant: "detail", align: "start" });Examples
Typed Cells
One schema, four kinds. Notes is empty so the display dash shows. Order # is copyable. Amount and status use the same formatters as Table.
- Order #
- ORD-1001
- Status
- Confirmed
- Amount
- USD 1,280.00
- Notes
- —
- Standalone empty
- —
"use client";
import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";
import { renderCellByKind } from "@/components/f-ui/data-list-view/render-cell-by-kind";
import { EmptyCellPlaceholder } from "@/components/f-ui/data-list-view/empty-cell-placeholder";
import { f } from "@/components/f-ui/field-types/catalog";
type Row = {
orderNumber: string;
status: "confirmed" | "shipped";
amount: number;
notes: string;
};
const schema = defineDataListSchema<Row>({
orderNumber: f.text({ label: "Order #", copyable: true }),
status: f.enum({
render: "status",
label: "Status",
variants: {
confirmed: { label: "Confirmed", tone: "success" },
shipped: { label: "Shipped", tone: "info" },
},
}),
amount: f.currency({ label: "Amount", currency: "USD" }),
notes: f.text({ label: "Notes" }),
});
const row: Row = {
orderNumber: "ORD-1001",
status: "confirmed",
amount: 1280,
notes: "",
};
const CELLS = [
{ key: "orderNumber" as const, label: "Order #" },
{ key: "status" as const, label: "Status" },
{ key: "amount" as const, label: "Amount" },
{ key: "notes" as const, label: "Notes" },
];
/** Shared `renderCellByKind` — same formatters Table, List View, and Descriptions use. */
export function DataListViewDemo() {
return (
<dl className="grid max-w-md gap-3">
{CELLS.map(({ key, label }) => {
const field = schema.definition[key];
const value = row[key];
return (
<div key={key} className="flex items-baseline justify-between gap-4">
<dt className="text-xs font-medium text-muted-foreground">{label}</dt>
<dd className="min-w-0 text-sm">
{renderCellByKind(value, row, field, { variant: "detail", align: "start" })}
</dd>
</div>
);
})}
<div className="flex items-baseline justify-between gap-4">
<dt className="text-xs font-medium text-muted-foreground">Standalone empty</dt>
<dd className="text-sm">
<EmptyCellPlaceholder />
</dd>
</div>
</dl>
);
}Row Actions Cell
Inline Edit + overflow, trailing Remove. Compose this cell into List actions — do not ship an icon wall. Full rules: Row Actions Column and List — Many Row Actions.
- Ada Lovelaceada@example.comOwner
"use client";
import { PencilIcon, Share2Icon, Trash2Icon } from "lucide-react";
import type { RowAction } from "@/components/f-ui/data-list-internals/row-actions/row-action-types";
import { useDataListViewI18n } from "@/components/f-ui/data-list-view/hooks/use-data-list-view-i18n";
import { DataListRowActionsCell } from "@/components/f-ui/data-list-view/row-actions-cell";
import { List } from "@/components/f-ui/list/list";
import { TooltipProvider } from "@/components/ui/tooltip";
type Member = {
id: string;
name: string;
email: string;
};
const MEMBER: Member = {
id: "ada",
name: "Ada Lovelace",
email: "ada@example.com",
};
const ACTIONS: RowAction<Member>[] = [
{
id: "edit",
label: "Edit",
icon: <PencilIcon className="size-3.5" />,
onClick: () => {},
},
{
id: "share",
label: "Share",
icon: <Share2Icon className="size-3.5" />,
onClick: () => {},
},
{
id: "remove",
label: "Remove",
icon: <Trash2Icon className="size-3.5" />,
variant: "destructive",
placement: "trailing",
confirm: false,
onClick: () => {},
},
];
/** Compose `DataListRowActionsCell` into presentational List `actions` — same cell Table uses. */
export function DataListViewRowActionsDemo() {
const { t } = useDataListViewI18n();
return (
<TooltipProvider>
<List>
<List.Item
extra="Owner"
actions={[
<DataListRowActionsCell
key="ops"
row={MEMBER}
actions={ACTIONS}
display="inline"
inlinePresentation="icon"
maxInline={2}
t={t}
/>,
]}
>
<List.Meta title={MEMBER.name} description={MEMBER.email} />
</List.Item>
</List>
</TooltipProvider>
);
}Composition
Table / ListView / Descriptions
└─ renderCellByKind(kind)
├─ EmptyCellPlaceholder (pretty empty)
├─ CopyAffordance (copyable)
└─ StatusTag / LabelTag / currency / …
DataListRowActionsCell (List.Item actions or Table Actions column)
DataListBodySpinOverlay (refetch overlay on the scroll shell)API Reference
renderCellByKind(value, row, field, opts?)
| Option | Type | Default | Description |
|---|---|---|---|
variant | "table" | "detail" | "table" | Table truncates one line; detail wraps (Descriptions). |
align | "start" | "end" | "end" | Number / currency alignment. Detail grids pass "start". |
masked | boolean | — | Access mask — never the raw value; skips host renderCell. |
pending | boolean | — | Skeleton; never the raw value. |
linkComponent | LinkComponent | "a" | — | f.link renderer. |
t | DataListViewTranslateFn | — | Overlay / copy chrome. |
applyRenderCell runs field renderCell(formatted, row) unless pending, masked, or config.mask is set.
DataListRowActionsCell Props
| Prop | Type | Description |
|---|---|---|
row | TData | Current row. |
actions | RowAction<TData>[] | Definitions from data-list-internals. |
display | "inline" | "menu" | Inline cluster + overflow, or overflow-only. |
inlinePresentation | "icon" | "text" | Icon+tooltip vs text link. Default max inline is 2 (icon) or 3 (text). |
maxInline | number | Override the inline cap. |
t | DataListViewTranslateFn | Required — pass useDataListViewI18n().t. |
linkComponent | LinkComponent | "a" | Navigate actions. |
At most one placement: "trailing" action. Destructive ops order last. Confirm uses Confirm.
useDataListDensity
const { density, setDensity } = useDataListDensity({
density, // controlled
defaultDensity, // uncontrolled initial, default "normal"
onDensityChange,
});List View useList is a thin wrapper around this hook. Data, selection, sort, and filters stay on the DataListHandle.