Object Messaging And Table Chrome
Reusable decisions for Message Popover, validation surfaces, cell value state, and region table toolbars — distilled from enterprise patterns.
Patterns below are product design decisions for integrators. Prefer kit chrome (Message Popover, DataListToolbar, Editable Table) over inventing a second language.
Examples
Footer Message Popover
Object readiness lives in a sticky footer: Message Popover on the left, Save / Submit on the right. Click Validate (or a blocked Submit) to open — draft edits do not auto-open the shell.
The miniature page body uses the same px-6 inset as FooterToolbar so title, Validate, and footer actions share one column.
Purchase request · PR-1042
Draft edits stay quiet. Click Validate to open the object message list.
"use client";
import { useState } from "react";
import {
MessagePopover,
type MessageItem,
} from "@/components/f-ui/message-popover/message-popover";
import { FooterToolbar } from "@/components/f-ui/page/footer-toolbar";
import { Button } from "@/components/ui/button";
const MESSAGES: MessageItem[] = [
{
id: "e1",
severity: "error",
title: "Missing unit price",
subtitle: "Line 2 · Unit price",
group: "Line items",
description: "Enter a price greater than 0.",
},
{
id: "w1",
severity: "warning",
title: "Qty looks high",
subtitle: "Line 1 · Qty",
group: "Line items",
description: "Confirm quantity on line 1 before submit.",
},
];
/**
* Footer left = Message Popover; finalizing actions on the right.
* Open after Validate / blocked Submit — not on every draft keystroke.
*/
export function ObjectMessagingFooterPopoverDemo() {
const [open, setOpen] = useState(false);
const [items] = useState(MESSAGES);
return (
<div className="overflow-hidden rounded-xl border bg-card">
<div className="space-y-2 border-b px-6 py-3">
<p className="text-sm font-medium">Purchase request · PR-1042</p>
<p className="text-xs text-muted-foreground">
Draft edits stay quiet. Click Validate to open the object message list.
</p>
<Button
type="button"
variant="outline"
onClick={() => setOpen(true)}
>
Validate
</Button>
</div>
<FooterToolbar
className="border-t-0"
extra={
<MessagePopover
items={items}
open={open}
onOpenChange={setOpen}
/>
}
>
<Button type="button" variant="outline">
Save draft
</Button>
<Button
type="button"
onClick={() => setOpen(true)}
>
Submit
</Button>
</FooterToolbar>
</div>
);
}Validation Surface Matrix
Do not stack Form Error Summary, Editable Error Rollup, and Message Popover essays for the same cells. For a single Editable Table, prefer cells + region rollup; use Message Popover when the page has multiple regions or an object-readiness footer. Required-empty cells stay blank inside that chrome — do not paint EmptyValuePlaceholder (—) as fake invalid data (that glyph is read-only empty only).
Wrong
- SKU required
- Qty must be > 0
- SKU required
- Line 1 · SKU — Required
- Line 2 · Qty — must be > 0
Three Banner lists for the same cell issues — wrong is dual ownership, not pink Alert chrome
Right
| 12 | |
| SKU-104 |
Cells + region rollup; Message Popover for multi-region readiness
"use client";
/**
* Validation matrix: cell chrome + one Message Popover — not stacked essays.
*
* Empty invalid cells = blank kit field box (Fiori / Ant / Editable Table).
* Never put EmptyValuePlaceholder (—) inside error chrome — that glyph is
* read-only empty only, not “required failed.”
*
* @see docs/superpowers/research/2026-08-10-editable-cell-error-chrome-single-border-industry.md
*/
import { useMemo, type ReactNode } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AlertTriangle, CircleAlert } from "lucide-react";
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 {
editableTableCellControlClass,
editableTableEditableFieldErrorClass,
} from "@/components/f-ui/editable-table/lib/table-cell-chrome";
import { f } from "@/components/f-ui/field-types/catalog";
import {
formErrorBannerBodyInnerClassName,
formErrorBannerHeadingClassName,
formErrorBannerLinkClassName,
formErrorBannerRootClassName,
} from "@/components/f-ui/formily/internals/form-error-banner-chrome";
import {
MessagePopover,
type MessageItem,
} from "@/components/f-ui/message-popover/message-popover";
import { FooterToolbar } from "@/components/f-ui/page/footer-toolbar";
import { Table } from "@/components/f-ui/table/table";
import { Button } from "@/components/ui/button";
import { DesignCompare } from "@/demos/_design/design-compare";
import { cn } from "@/lib/utils";
const ITEMS: MessageItem[] = [
{
id: "e1",
severity: "error",
title: "SKU required",
description: "Line 1 · SKU",
},
{
id: "e2",
severity: "error",
title: "Qty must be > 0",
description: "Line 2 · Qty",
},
];
type LineRow = {
id: string;
sku: string;
qty: number;
invalid?: "sku" | "qty";
};
/** Empty string = missing required SKU — never fake with "—". */
const ROWS: LineRow[] = [
{ id: "1", sku: "", qty: 12, invalid: "sku" },
{ id: "2", sku: "SKU-104", qty: 0, invalid: "qty" },
];
function CellValue({
invalid,
empty,
children,
}: {
invalid: boolean;
empty?: boolean;
children?: ReactNode;
}) {
if (!invalid) {
return (
<span className="inline-flex min-h-8 items-center px-2 text-sm">
{children}
</span>
);
}
return (
<div
data-cell-error
className={cn(
editableTableEditableFieldErrorClass,
"inline-flex min-h-8 min-w-[4.5rem] items-center",
)}
>
{empty ? (
<span
aria-hidden
className={cn(editableTableCellControlClass, "block min-h-8 w-full")}
/>
) : (
<span
className={cn(
editableTableCellControlClass,
"inline-flex min-h-8 items-center text-sm tabular-nums",
)}
>
{children}
</span>
)}
</div>
);
}
const schema = defineDataListSchema<LineRow>({
sku: f.custom({
label: "SKU",
render: (_value, row) => (
<CellValue invalid={row.invalid === "sku"} empty={!row.sku.trim()}>
{row.sku || undefined}
</CellValue>
),
}),
qty: f.custom({
label: "Qty",
render: (_value, row) => (
<CellValue invalid={row.invalid === "qty"}>
<span className="tabular-nums">{row.qty}</span>
</CellValue>
),
}),
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
function LoudEssay({
icon,
title,
lines,
}: {
icon: ReactNode;
title: string;
lines: string[];
}) {
return (
<div className={formErrorBannerRootClassName}>
<div className={formErrorBannerHeadingClassName}>
{icon}
{title}
</div>
<ul className={cn(formErrorBannerBodyInnerClassName, "space-y-0.5")}>
{lines.map((line) => (
<li key={line} className={`${formErrorBannerLinkClassName} text-xs`}>
{line}
</li>
))}
</ul>
</div>
);
}
function RightMatrix() {
const handle = useDataList({
schema,
listCode: "demo-msg-matrix",
data: ROWS,
getRowId: (r) => r.id,
defaultPageSize: "all",
});
return (
<div className="space-y-3">
<Table dataList={handle} />
<FooterToolbar
className="rounded-lg border"
extra={<MessagePopover items={ITEMS} />}
>
<Button type="button">Submit</Button>
</FooterToolbar>
<p className="text-xs text-muted-foreground">
Cells + region rollup; Message Popover for multi-region readiness
</p>
</div>
);
}
/**
* Do not stack FormErrorSummary + EditableErrorRollup + Message Popover essays
* for the same cells. Prefer cell chrome + object Message Popover.
*/
export function ObjectMessagingValidationMatrixDemo() {
const client = useMemo(() => queryClient, []);
return (
<DesignCompare
wrong={
<div className="space-y-2">
<LoudEssay
icon={<CircleAlert className="size-3.5" />}
title="Form errors"
lines={["SKU required", "Qty must be > 0", "SKU required"]}
/>
<LoudEssay
icon={<AlertTriangle className="size-3.5" />}
title="Table rollup"
lines={["Line 1 · SKU — Required", "Line 2 · Qty — must be > 0"]}
/>
<div className="rounded-lg border p-2">
<MessagePopover items={ITEMS} />
</div>
<p className="text-xs text-muted-foreground">
Three Banner lists for the same cell issues — wrong is dual ownership, not pink Alert chrome
</p>
</div>
}
right={
<QueryClientProvider client={client}>
<RightMatrix />
</QueryClientProvider>
}
/>
);
}Cell Focus Value State
Table cells use one severity border on the field (SAP Fiori value state / Ant Form.Item — never a second nested Input border). Show the message on focus (value-state popper), not as a hover Tooltip. Blurred invalid cells keep that single chrome; the full list stays in Message Popover / rollup.
Precedent: Fiori Input — Value State; NN/G — Don’t use tooltips to report errors; kit research cell-tooltip-vs-value-state + editable-cell-error-chrome-single-border.
Wrong
Qty
Same single chrome — but recovery text only on hover Tooltip (banned as the primary channel)
Right
Qty
Focus → value-state popper (Fiori / Editable Table). One border only.
"use client";
/**
* Industry chrome: ONE severity border on the field box (SAP Fiori value state,
* Ant Form.Item / EditableProTable, Salesforce cell highlight) — never Input’s
* own border + a wrapper border.
*
* @see docs/superpowers/research/2026-08-10-editable-cell-error-chrome-single-border-industry.md
* @see docs/superpowers/research/2026-08-05-cell-tooltip-vs-value-state.md
*/
import { useState } from "react";
import {
editableTableCellControlClass,
editableTableEditableFieldErrorClass,
} from "@/components/f-ui/editable-table/lib/table-cell-chrome";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverAnchor,
PopoverContent,
} from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { DesignCompare } from "@/demos/_design/design-compare";
import { cn } from "@/lib/utils";
const ERROR = "Qty must be greater than 0";
/** Fiori / kit: single field box; control is borderless. */
function ErrorFieldBox({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div
data-cell-error
className={cn(
editableTableEditableFieldErrorClass,
"w-full max-w-[12rem]",
className,
)}
>
{children}
</div>
);
}
function BorderlessInvalidInput(
props: React.ComponentProps<typeof Input>,
) {
return (
<Input
{...props}
aria-invalid
className={cn(editableTableCellControlClass, props.className)}
/>
);
}
function HoverTooltipCell() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<ErrorFieldBox>
<BorderlessInvalidInput defaultValue="0" />
</ErrorFieldBox>
</TooltipTrigger>
<TooltipContent side="bottom">{ERROR}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
function FocusValueStateCell() {
const [focused, setFocused] = useState(false);
return (
<Popover open={focused} modal={false}>
<PopoverAnchor asChild>
<ErrorFieldBox>
<BorderlessInvalidInput
defaultValue="0"
aria-description={ERROR}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
/>
</ErrorFieldBox>
</PopoverAnchor>
<PopoverContent
side="bottom"
align="start"
sideOffset={4}
className="w-auto max-w-xs p-2 text-xs text-destructive shadow-md"
onOpenAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => e.preventDefault()}
>
<p role="alert">{ERROR}</p>
</PopoverContent>
</Popover>
);
}
/**
* Wrong = hover Tooltip as primary error channel (NN/G anti-pattern).
* Right = Fiori value-state: one field chrome + message on focus (kit ArrayCellDecorator).
*/
export function ObjectMessagingCellFocusDemo() {
return (
<DesignCompare
wrong={
<div className="space-y-3">
<p className="text-sm font-medium text-muted-foreground">Qty</p>
<HoverTooltipCell />
<p className="text-xs text-muted-foreground">
Same single chrome — but recovery text only on hover Tooltip (banned
as the primary channel)
</p>
</div>
}
right={
<div className="space-y-3">
<p className="text-sm font-medium text-muted-foreground">Qty</p>
<FocusValueStateCell />
<p className="text-xs text-muted-foreground">
Focus → value-state popper (Fiori / Editable Table). One border only.
</p>
</div>
}
/>
);
}Region Table Toolbar
A section that owns a table keeps title and tools on one horizontal row. Left side shows the floating vertical tool stack anti-pattern; right side uses DataListToolbar rhythm.
Wrong
Documents
| API overview | Ready |
| Onboarding guide | Pending |
Vertical tool stack + solid Refresh + mixed control heights
Right
| API overview | Ready |
| Onboarding guide | Pending |
One row — title · ghost Refresh · outline Columns · one primary Add
"use client";
import { useMemo } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Columns3Icon, PlusIcon, RotateCwIcon } from "lucide-react";
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 { DataListProvider } from "@/components/f-ui/data-list-chrome/layout/data-list-provider";
import { DataListToolbar } from "@/components/f-ui/data-list-chrome/data-list-toolbar";
import { f } from "@/components/f-ui/field-types/catalog";
import { TableView } from "@/components/f-ui/table/table-parts/table-view";
import { useTable } from "@/components/f-ui/table/use-table";
import { Button } from "@/components/ui/button";
import { DesignCompare } from "@/demos/_design/design-compare";
type DocRow = {
id: string;
title: string;
status: "ready" | "pending";
};
const ROWS: DocRow[] = [
{ id: "1", title: "API overview", status: "ready" },
{ id: "2", title: "Onboarding guide", status: "pending" },
];
const schema = defineDataListSchema<DocRow>({
title: f.text({ label: "Title" }),
status: f.enum({
render: "status",
label: "Status",
variants: {
ready: { label: "Ready", tone: "success" },
pending: { label: "Pending", tone: "warning" },
},
}),
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
function MiniTable() {
const handle = useDataList({
schema,
listCode: "demo-msg-region",
data: ROWS,
getRowId: (r) => r.id,
defaultPageSize: "all",
});
const view = useTable(handle);
return (
<DataListProvider dataList={handle}>
<TableView
table={view.table}
density={view.density}
sort={handle.sort}
onSortColumn={handle.onSortColumn}
selection={handle.selection}
getRowId={handle.getRowId}
dataList={handle}
/>
</DataListProvider>
);
}
/**
* Region table chrome: one horizontal title + tools row — never a vertical
* floating tool stack in the corner.
*/
export function ObjectMessagingRegionToolbarDemo() {
const client = useMemo(() => queryClient, []);
return (
<QueryClientProvider client={client}>
<DesignCompare
wrong={
<div className="space-y-3 rounded-xl border bg-card p-4">
<div className="flex items-start justify-between gap-4">
<h3 className="text-sm font-medium text-muted-foreground">
Documents
</h3>
<div className="flex flex-col items-stretch gap-2">
<Button type="button">
<RotateCwIcon className="size-4" />
Refresh
</Button>
<Button type="button" variant="outline">
<Columns3Icon className="size-4" />
Columns
</Button>
<Button type="button">
<PlusIcon className="size-4" />
Add
</Button>
</div>
</div>
<MiniTable />
<p className="text-xs text-muted-foreground">
Vertical tool stack + solid Refresh + mixed control heights
</p>
</div>
}
right={
<div className="space-y-3 rounded-xl border bg-card p-4">
<DataListToolbar
title={
<span className="text-sm font-medium text-muted-foreground">
Documents
</span>
}
actions={
<>
<Button type="button" variant="ghost">
<RotateCwIcon className="size-4" />
Refresh
</Button>
<Button type="button" variant="outline">
<Columns3Icon className="size-4" />
Columns
</Button>
<Button type="button">
<PlusIcon className="size-4" />
Add
</Button>
</>
}
/>
<MiniTable />
<p className="text-xs text-muted-foreground">
One row — title · ghost Refresh · outline Columns · one primary Add
</p>
</div>
}
/>
</QueryClientProvider>
);
}Segmented Same-Table Views
Mutually exclusive filters on one table use a segmented control (≤3) — not a pile of independent outline/default Buttons pretending to be tabs.
Wrong
Button pile pretending to be tabs on one table
Right
Showing · All
Segmented tabs (≤3) for mutually exclusive same-table filters
"use client";
import { useState } from "react";
import { DataListToolbar } from "@/components/f-ui/data-list-chrome/data-list-toolbar";
import { DataListToolbarTabs } from "@/components/f-ui/data-list-chrome/data-list-toolbar-tabs";
import { Button } from "@/components/ui/button";
import { DesignCompare } from "@/demos/_design/design-compare";
import { cn } from "@/lib/utils";
const VIEWS = [
{ id: "all", label: "All" },
{ id: "open", label: "Open" },
{ id: "closed", label: "Closed" },
] as const;
/**
* Same-table mutually exclusive view filters → segmented control (≤3), not a
* pile of independent outline/default Buttons pretending to be tabs.
*/
export function ObjectMessagingSegmentedViewDemo() {
const [wrongView, setWrongView] = useState<(typeof VIEWS)[number]["id"]>("all");
const [rightView, setRightView] = useState<(typeof VIEWS)[number]["id"]>("all");
return (
<DesignCompare
wrong={
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-2">
{VIEWS.map((view) => (
<Button
key={view.id}
type="button"
variant={wrongView === view.id ? "default" : "outline"}
onClick={() => setWrongView(view.id)}
>
{view.label}
</Button>
))}
</div>
<p className="text-xs text-muted-foreground">
Button pile pretending to be tabs on one table
</p>
</div>
}
right={
<div className="space-y-3">
<DataListToolbar
title={
<span className="text-sm font-medium text-muted-foreground">
Line items
</span>
}
tabs={
<DataListToolbarTabs
tabs={[...VIEWS]}
value={rightView}
onChange={(id) =>
setRightView(id as (typeof VIEWS)[number]["id"])
}
/>
}
/>
<p
className={cn(
"rounded-lg border bg-muted/30 px-3 py-6 text-center text-xs text-muted-foreground",
)}
>
Showing · {VIEWS.find((v) => v.id === rightView)?.label}
</p>
<p className="text-xs text-muted-foreground">
Segmented tabs (≤3) for mutually exclusive same-table filters
</p>
</div>
}
/>
);
}1. Object Message Aggregation
Industry twin: SAP Fiori Message Popover.
| Decision | Lock |
|---|---|
| Role | Aggregate object / multi-region readiness messages — not cell chrome, not toast, not page Result |
| Not a bell | Items die with the object. Cross-app activity that outlives this page belongs to Notification Center — see Messaging Surfaces |
| Placement | Sticky footer toolbar, left; finalizing actions (Save / Submit) on the right |
| Trigger | Ghost control with worst-type icon + count of that type only (2 errors); hide when no messages. Hosts may replace the trigger. |
| Open timing | Do not auto-open on draft edits; do open after Validate / Run check / blocked Submit (finalizing) |
| Shell | Desktop Popover · mobile Sheet |
| Filters | Stable All · Errors · Warnings (with counts). Info / Success tabs only when those items exist. Do not collapse empty Errors/Warnings tabs; empty filter → region Empty + Show all |
| Acknowledge | Optional host policy only (soft warnings / compliance). Not part of the Message Popover twin — forced acknowledgment belongs on Message Box / submit confirm. Kit shows ack chrome only when host passes callbacks |
| Navigate | Title is the jump (link emphasis). Optional subtitle for location. Optional group section headers. Do not add a row Go to field button |
Component: Message Popover.
2. Validation Surface Matrix
Do not stack three loud essays for the same cell paths.
| Layer | Surface | Owns |
|---|---|---|
| Cell | Value state (table) / Form Item (panel) | Field-shaped issues on that control |
| Row (optional later) | Contains-errors scan marker | Which rows need attention — not the full message |
| Region | Editable Error Rollup | That table’s cell list + jump links; defer with showErrorRollup={false} while drafting |
| Form-root | Form Error Summary | Root / header / array-root issues (e.g. “at least one line”). Never dual-list Editable Table cell paths |
| Object | Message Popover | Multi-region readiness after Validate / blocked Submit — not a draft auto-open |
Defaults
- Single Editable Table on a form: prefer cells + region rollup. Use Message Popover when the page has multiple regions or an object-readiness footer.
- Table-heavy page with header or array-root rules: keep FormPage’s default
FormErrorSummary(scope="all") — kit strips owned cell paths and still surfaces header / array-root. - Region rollup (
EditableErrorRollup) uses the same Banner card as Form Error Summary (not a pink Alert). Same chrome, disjoint paths. - Pure table cell errors only (no header / array-root / system):
errorSummary={false}so the rollup owns the aggregate, orscope="form"for system/root (path == null) only — that scope does not include header or array-root paths. - Kit drops owned cell paths from
scope="all"automatically; array-root path issues still belong in Form Error Summary underscope="all".
Forbidden: listing the same cell issue in Form Error Summary and Editable Error Rollup; rollup-only with no cell chrome after reveal; GOV.UK-style flattening of every table cell into the page summary when a region rollup already owns those cells.
3. Cell Error And Warning Messages
| Decision | Lock |
|---|---|
| Always | Value-state chrome (ring / field tint) only when field.pattern === "editable" |
| Table cells | Show message on focus (value-state popper under the cell) — not hover Tooltip as the primary channel |
readOnly / readPretty / display | No ring. Host issues still appear in EditableErrorRollup. Display attention uses getRowHighlight + CellCriticalityValue (text + icon) |
| Panel fields | Inline message under the control (Form Item rhythm) |
| Blurred invalid cell | Chrome + accessible description; full list via Message Popover / rollup |
| ⓘ on column / label | Only for supplemental definition help — never for required format rules or validation text. Default: omit until a clear need |
Industry backing: NN/G — Don’t use tooltips to report errors; Fiori value state + value state message; Carbon helper text vs tooltip.
Shipped in Editable Table ArrayCellDecorator.
Display emphasis (not a ring)
| Job | Language |
|---|---|
| Which rows | getRowHighlight leading-edge bar (error / warning / success / new) |
| Which cell | renderCell wrapping CellCriticalityValue — colour on the value plus a mandatory icon |
| Why | Host reason slot (inline, on a real link/button, or in the rollup). Kit never ships "was" / "changed from" |
| Jump | Rollup item → scroll + focus, or locate pulse when there is no control |
| Wrap / extra copy | Schema renderCell(formatted, row) — does not replace kind |
| Cell wash (not a ring) | cellClassName on the <td> / cell — never rebuild value-state chrome |
Wrong: a display table whose only signal is a red input ring. Right: row bar + coloured value with icon + rollup.
Layered cell customization (wrap copy, td wash, status + host badge) lives on Table — Cell Customization and Editable Table. CellCriticalityValue is a helper inside renderCell, not a fifth ring.
4. Region Table Toolbar
Authority rhythm: Ant Design Pro ListToolBar + CRUD Page Patterns — Region Table Toolbar.
[ Section title ] [ view filter ] [ region actions ]
[ Columns ] [ Expand ] ← same control height
[ table … ]| Decision | Lock |
|---|---|
| One horizontal row with the section title — no vertical “tool stack” in a corner | |
| Control height | Button default (h-8). Do not mix size="sm" beside default Columns / tools |
| Emphasis | View tools (Columns, Expand, Refresh) = ghost / outline; at most one solid primary in the row |
| Same-table view filters | Mutually exclusive filters on one table → segmented control (≤3) or Select (≥4). Not a pile of independent outline/default Buttons pretending to be tabs |
| True Tabs | Only when switching different content areas or separate table instances — not for filtering one Editable Table |
| Separate roles | View filter ≠ region action (Change selection / Mass edit) ≠ table personalization (Columns) |
Industry: Fiori Table Toolbar (segmented ≤3 / select ≥4); NN/G Progressive Disclosure.
- Prefer stock DataListToolbar /
DataListToolbarTabs/ Editable Table toolbar over hand-rolled chip rows.
Also see CRUD Page Patterns — Region Table Toolbar.
5. Copy And Hierarchy
- In-app section titles, labels, and buttons: sentence case — never CSS
uppercase/ ALL CAPS section chips. Normative page: Sentence Case. - Docs / registry display titles: Title Case (documentation convention only)
See Also
- Form Validation — panel-form recipes (reveal, summary, async, server map); this page owns table / object surfaces
- CRUD Page Patterns — list / detail / form / region toolbar
- Page And Region Status — Loading / Empty / Error / Populated
- Message Popover · Editable Table · Empty · Result