@@ -117,6 +179,13 @@ export const Anatomy: Story = {
data-tone exposes whether the
surface stays neutral, subtle, or accent-led.
+
+ layout,{" "}
+ align,{" "}
+ size, and{" "}
+ actions layout let the same
+ composition flex between dense cards and broader setup moments.
+
diff --git a/apps/docs/src/components/popover.stories.tsx b/apps/docs/src/components/popover.stories.tsx
index f370d5a..6106499 100644
--- a/apps/docs/src/components/popover.stories.tsx
+++ b/apps/docs/src/components/popover.stories.tsx
@@ -1,5 +1,6 @@
import {
Button,
+ Input,
Popover,
PopoverArrow,
PopoverClose,
@@ -61,6 +62,111 @@ function SummaryPopover({
);
}
+function InspectorPopover() {
+ return (
+
@@ -132,11 +259,12 @@ export const Anatomy: Story = {
Popover anatomy
-
+
data-slot="content" wraps
- the floating surface and exposes data-size.
+ the floating surface and exposes data-size and{" "}
+ data-padding.
data-slot="arrow" visually
@@ -170,3 +298,41 @@ export const Motion: Story = {
)
};
+
+export const ContextualWorkflows: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "Popover works best for anchored context, lightweight review panels, and compact forms that should stay attached to a trigger instead of escalating to dialog."
+ }
+ }
+ },
+ render: () => (
+
+
+
+ Anchored release context
+
+
+ Use a richer popover when the user needs more detail than a tooltip, but not a
+ full modal interruption.
+
+
+
+
+
+
+
+ Quick assignment flow
+
+
+ Single-purpose forms can stay lightweight and local to the trigger.
+
+
+
+
+
+
+ )
+};
diff --git a/packages/ui/src/components/combobox.test.tsx b/packages/ui/src/components/combobox.test.tsx
index 16099d7..4c1e21b 100644
--- a/packages/ui/src/components/combobox.test.tsx
+++ b/packages/ui/src/components/combobox.test.tsx
@@ -42,7 +42,7 @@ describe("Combobox", () => {
it("renders a selected value, filters options, and updates uncontrolled state", async () => {
const user = userEvent.setup();
- render(
+ const loadingView = render(
{
expect(trigger).toHaveAttribute("aria-describedby", expect.stringContaining(message.id));
expect(trigger.closest("[data-slot='root']")).toHaveAttribute("data-invalid", "");
});
+
+ it("supports loading, custom empty state, footer actions, and practical keyboard navigation", async () => {
+ const user = userEvent.setup();
+
+ const loadingView = render(
+ `Create “${query}” as a new lane`}
+ footer={
+
+ Manage routing lanes
+
+ }
+ items={reviewLaneItems}
+ loading
+ loadingMessage="Searching review lanes…"
+ />
+ );
+
+ const trigger = screen.getByRole("combobox", { name: "Async review lane" });
+
+ await user.click(trigger);
+ expect(screen.getByText("Searching review lanes…")).toBeInTheDocument();
+ expect(screen.getByText("Manage routing lanes")).toBeInTheDocument();
+
+ loadingView.unmount();
+
+ render(
+ `Create “${query}” as a new lane`}
+ footer={Manage routing lanes }
+ items={reviewLaneItems}
+ />
+ );
+
+ const keyboardTrigger = screen.getByRole("combobox", { name: "Keyboard review lane" });
+ await user.click(keyboardTrigger);
+
+ const searchbox = screen.getByRole("searchbox", { name: "Search options" });
+ await user.type(searchbox, "design");
+ await user.keyboard("{Escape}");
+
+ expect(searchbox).toHaveValue("");
+
+ await user.click(keyboardTrigger);
+
+ await user.keyboard("{Home}");
+ const firstOption = screen.getByRole("option", { name: /Editorial review/i });
+ expect(firstOption).toHaveAttribute("data-active", "");
+
+ await user.keyboard("{End}");
+ const lastOption = screen.getByRole("option", { name: /Legal review/i });
+ expect(lastOption).toHaveAttribute("data-active", "");
+
+ const refreshedSearchbox = screen.getByRole("searchbox", { name: "Search options" });
+ await user.clear(refreshedSearchbox);
+ await user.type(refreshedSearchbox, "security");
+ expect(screen.getByText("Create “security” as a new lane")).toBeInTheDocument();
+
+ await user.keyboard("{Tab}");
+ await waitFor(() => {
+ expect(screen.queryByRole("listbox")).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/packages/ui/src/components/combobox.tsx b/packages/ui/src/components/combobox.tsx
index 0016c07..563eb85 100644
--- a/packages/ui/src/components/combobox.tsx
+++ b/packages/ui/src/components/combobox.tsx
@@ -7,7 +7,8 @@ import {
useRef,
useState,
type ComponentPropsWithoutRef,
- type KeyboardEvent
+ type KeyboardEvent,
+ type ReactNode
} from "react";
import {
@@ -18,11 +19,12 @@ import {
comboboxLabelVariants,
comboboxListVariants,
comboboxSearchVariants,
- comboboxTriggerVariants
+ comboboxTriggerVariants,
+ comboboxFooterVariants
} from "./combobox.variants";
import { cn } from "../lib/cn";
import { createDataAttributes, createSlot } from "../lib/contracts";
-import { CheckIcon, ChevronDownIcon } from "../lib/icons";
+import { CheckIcon, ChevronDownIcon, SpinnerIcon } from "../lib/icons";
import { useFieldContext } from "./field";
function mergeIds(...ids: Array) {
@@ -68,9 +70,13 @@ export type ComboboxProps = Omit<
defaultOpen?: boolean;
defaultSearchValue?: string;
defaultValue?: string;
- emptyMessage?: string;
+ emptyMessage?: ReactNode | ((query: string) => ReactNode);
+ filter?: (item: ComboboxItem, query: string) => boolean;
+ footer?: ReactNode;
invalid?: boolean;
items: ComboboxItem[];
+ loading?: boolean;
+ loadingMessage?: ReactNode;
onOpenChange?: (open: boolean) => void;
onSearchValueChange?: (value: string) => void;
onValueChange?: (value: string) => void;
@@ -89,9 +95,13 @@ export const Combobox = forwardRef(function Co
defaultValue,
disabled,
emptyMessage = "No matching results.",
+ filter,
+ footer,
id,
invalid,
items,
+ loading = false,
+ loadingMessage = "Searching…",
onOpenChange,
onSearchValueChange,
onValueChange,
@@ -137,6 +147,10 @@ export const Combobox = forwardRef(function Co
}
return items.filter((item) => {
+ if (filter) {
+ return filter(item, query);
+ }
+
const haystack = [item.label, item.value, ...(item.keywords ?? [])]
.join(" ")
.toLowerCase();
@@ -282,10 +296,45 @@ export const Combobox = forwardRef(function Co
if (event.key === "Escape") {
event.preventDefault();
+
+ if (resolvedSearchValue.length > 0) {
+ setSearchState("");
+ return;
+ }
+
+ setOpenState(false);
+ return;
+ }
+
+ if (event.key === "Home") {
+ event.preventDefault();
+ setActiveIndex(filteredItems.findIndex((item) => !item.disabled));
+ return;
+ }
+
+ if (event.key === "End") {
+ event.preventDefault();
+ const lastEnabledIndex = [...filteredItems]
+ .reverse()
+ .findIndex((item) => !item.disabled);
+
+ if (lastEnabledIndex >= 0) {
+ setActiveIndex(filteredItems.length - 1 - lastEnabledIndex);
+ }
+
+ return;
+ }
+
+ if (event.key === "Tab") {
setOpenState(false);
}
};
+ const renderedEmptyMessage =
+ typeof emptyMessage === "function"
+ ? emptyMessage(resolvedSearchValue.trim())
+ : emptyMessage;
+
return (
(function Co
value={resolvedSearchValue}
/>
- {filteredItems.length === 0 ? (
+ {loading ? (
- {emptyMessage}
+
+
+ {loadingMessage}
+
+
+ ) : filteredItems.length === 0 ? (
+
+ {renderedEmptyMessage}
) : (
(function Co
))}
)}
+ {footer ? (
+
+ {footer}
+
+ ) : null}
diff --git a/packages/ui/src/components/combobox.variants.ts b/packages/ui/src/components/combobox.variants.ts
index b3a2d6d..fc06521 100644
--- a/packages/ui/src/components/combobox.variants.ts
+++ b/packages/ui/src/components/combobox.variants.ts
@@ -48,3 +48,7 @@ export const comboboxItemVariants = cva([
export const comboboxEmptyVariants = cva([
"px-3 py-6 text-center text-sm text-[var(--color-muted-foreground)]"
]);
+
+export const comboboxFooterVariants = cva([
+ "border-t border-[var(--ui-panel-border)] bg-[color-mix(in_oklch,var(--ui-panel-bg)_92%,var(--color-background))] px-2 py-2"
+]);
diff --git a/packages/ui/src/components/command.test.tsx b/packages/ui/src/components/command.test.tsx
index 083b98e..2bc1c13 100644
--- a/packages/ui/src/components/command.test.tsx
+++ b/packages/ui/src/components/command.test.tsx
@@ -4,6 +4,7 @@ import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
+import { Button } from "./button";
import {
Command,
CommandDialog,
@@ -96,4 +97,60 @@ describe("Command", () => {
expect(screen.queryByPlaceholderText("Search across workspace")).not.toBeInTheDocument();
});
});
+
+ it("supports loading and footer content on the root command surface", async () => {
+ render(
+ Manage commands}
+ label="Workspace palette"
+ loading
+ loadingMessage="Searching workspace…"
+ >
+
+
+ No matching items.
+
+
+ );
+
+ expect(screen.getByText("Searching workspace…")).toHaveAttribute("data-slot", "loading");
+ expect(screen.getByText("Manage commands")).toBeInTheDocument();
+ });
+
+ it("supports dialog title, description, and footer actions for a practical palette shell", async () => {
+ const user = userEvent.setup();
+
+ function CommandDialogExample() {
+ const [open, setOpen] = useState(true);
+
+ return (
+ Manage shortcuts}
+ onOpenChange={setOpen}
+ open={open}
+ title="Workspace command palette"
+ >
+
+
+
+ Recent release brief
+
+
+
+ );
+ }
+
+ render( );
+
+ expect(screen.getByText("Workspace command palette")).toBeInTheDocument();
+ expect(screen.getByText("Jump to docs, recent launches, and operational shortcuts.")).toBeInTheDocument();
+ expect(screen.getByText("Manage shortcuts")).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "Close dialog" }));
+
+ await waitFor(() => {
+ expect(screen.queryByText("Workspace command palette")).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/packages/ui/src/components/command.tsx b/packages/ui/src/components/command.tsx
index 0f2379c..1950786 100644
--- a/packages/ui/src/components/command.tsx
+++ b/packages/ui/src/components/command.tsx
@@ -10,16 +10,24 @@ import {
import {
commandDialogContentVariants,
commandEmptyVariants,
+ commandFooterVariants,
commandGroupVariants,
commandInputVariants,
commandInputWrapperVariants,
commandItemVariants,
commandListVariants,
+ commandLoadingVariants,
commandSeparatorVariants,
commandShortcutVariants,
commandVariants
} from "./command.variants";
-import { DialogContent } from "./dialog";
+import {
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle
+} from "./dialog";
import { cn } from "../lib/cn";
import { createDataAttributes, createSlot } from "../lib/contracts";
@@ -38,17 +46,49 @@ function SearchIcon() {
);
}
-export type CommandProps = ComponentPropsWithoutRef;
+export type CommandProps = ComponentPropsWithoutRef & {
+ footer?: ReactNode;
+ loading?: boolean;
+ loadingMessage?: ReactNode;
+};
export const Command = forwardRef, CommandProps>(
- function Command({ className, ...props }, ref) {
+ function Command(
+ {
+ children,
+ className,
+ footer,
+ loading = false,
+ loadingMessage = "Loading results…",
+ ...props
+ },
+ ref
+ ) {
return (
+ >
+ {loading ? (
+
+ {loadingMessage}
+
+ ) : null}
+ {children}
+ {footer ? (
+
+ {footer}
+
+ ) : null}
+
);
}
);
@@ -57,18 +97,45 @@ export type CommandDialogProps = ComponentPropsWithoutRef
- {children}
+ {title || description ? (
+
+ {title ? {title} : null}
+ {description ? {description} : null}
+
+ ) : null}
+ {footer}
+ ) : undefined
+ }
+ loading={loading}
+ loadingMessage={loadingMessage}
+ >
+ {children}
+
);
diff --git a/packages/ui/src/components/command.variants.ts b/packages/ui/src/components/command.variants.ts
index 7a0a07f..544a00b 100644
--- a/packages/ui/src/components/command.variants.ts
+++ b/packages/ui/src/components/command.variants.ts
@@ -25,6 +25,10 @@ export const commandListVariants = cva([
"max-h-[22rem] overflow-y-auto overflow-x-hidden p-2"
]);
+export const commandLoadingVariants = cva([
+ "px-4 py-8 text-sm text-[var(--color-muted-foreground)]"
+]);
+
export const commandEmptyVariants = cva([
"py-10 text-center text-sm text-[var(--color-muted-foreground)]"
]);
@@ -51,3 +55,7 @@ export const commandSeparatorVariants = cva([
export const commandShortcutVariants = cva([
"ml-auto text-[0.7rem] uppercase tracking-[var(--tracking-caps)] text-[var(--color-muted-foreground)]"
]);
+
+export const commandFooterVariants = cva([
+ "border-t border-[var(--ui-panel-border)] bg-[color-mix(in_oklch,var(--ui-panel-bg)_94%,var(--color-background))] px-4 py-3"
+]);
diff --git a/packages/ui/src/components/data-table.test.tsx b/packages/ui/src/components/data-table.test.tsx
index 5494803..fc888cc 100644
--- a/packages/ui/src/components/data-table.test.tsx
+++ b/packages/ui/src/components/data-table.test.tsx
@@ -239,4 +239,81 @@ describe("DataTable", () => {
expect(onSortingChange).toHaveBeenCalledWith([{ desc: false, id: "lane" }]);
expect(onSelectionChange).toHaveBeenCalledWith({ support: true });
});
+
+ it("toggles hideable columns from the built-in view menu", async () => {
+ const user = userEvent.setup();
+
+ render(
+
+ );
+
+ expect(screen.getByRole("columnheader", { name: /owner/i })).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "View" }));
+ await user.click(screen.getByRole("menuitemcheckbox", { name: "Owner" }));
+
+ expect(screen.queryByRole("columnheader", { name: /owner/i })).not.toBeInTheDocument();
+ expect(screen.queryByText("Ava")).not.toBeInTheDocument();
+ });
+
+ it("switches density from the built-in view menu", async () => {
+ const user = userEvent.setup();
+
+ render( );
+
+ const root = screen.getByRole("table").closest('[data-slot="root"]');
+ expect(root).toHaveAttribute("data-density", "comfortable");
+
+ await user.click(screen.getByRole("button", { name: "View" }));
+ await user.click(screen.getByRole("menuitemradio", { name: "Compact" }));
+
+ expect(root).toHaveAttribute("data-density", "compact");
+ expect(screen.getByRole("columnheader", { name: /lane/i })).toHaveAttribute(
+ "data-density",
+ "compact"
+ );
+ });
+
+ it("opens row details inside a sheet", async () => {
+ const user = userEvent.setup();
+
+ render(
+ {row.note}
}
+ rowDetailsDescription={(row) => `${row.lane} handoff`}
+ rowDetailsTitle={(row) => `${row.lane} detail`}
+ rows={rows.slice(0, 2)}
+ />
+ );
+
+ await user.click(screen.getAllByRole("button", { name: "Open details" })[0]);
+
+ const detailHeading = await screen.findByText("Legal detail");
+ const sheet = detailHeading.closest('[data-slot="content"]');
+
+ expect(detailHeading).toBeInTheDocument();
+ expect(screen.getByText("Legal handoff")).toBeInTheDocument();
+ expect(
+ within(sheet as HTMLElement).getByText(
+ "Footnote needs one more pass before the customer note goes out."
+ )
+ ).toBeInTheDocument();
+ });
});
diff --git a/packages/ui/src/components/data-table.tsx b/packages/ui/src/components/data-table.tsx
index 1ede175..cbc9e10 100644
--- a/packages/ui/src/components/data-table.tsx
+++ b/packages/ui/src/components/data-table.tsx
@@ -26,6 +26,16 @@ import {
import { Button } from "./button";
import { Checkbox } from "./checkbox";
+import {
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from "./dropdown-menu";
import {
EmptyState,
EmptyStateActions,
@@ -34,6 +44,13 @@ import {
EmptyStateTitle
} from "./empty-state";
import { Input, type InputProps } from "./input";
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle
+} from "./sheet";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
import { Skeleton } from "./skeleton";
import { Spinner } from "./spinner";
@@ -62,6 +79,7 @@ import {
} from "./data-table.variants";
export type DataTableAlignment = "start" | "center" | "end";
+export type DataTableDensity = "comfortable" | "compact";
export type DataTableSort = {
desc?: boolean;
@@ -78,6 +96,7 @@ export type DataTableColumn = {
align?: DataTableAlignment;
cell?: (row: TData) => ReactNode;
header: ReactNode | ((column: DataTableColumnContext) => ReactNode);
+ hideable?: boolean;
id: string;
searchValue?: (row: TData) => string;
searchable?: boolean;
@@ -87,25 +106,34 @@ export type DataTableColumn = {
export type DataTableProps = Omit, "children"> & {
columns: DataTableColumn[];
+ defaultDensity?: DataTableDensity;
defaultPageIndex?: number;
defaultPageSize?: number;
defaultSearchValue?: string;
defaultSelection?: Record;
defaultSorting?: DataTableSort[];
+ defaultVisibleColumns?: Record;
+ density?: DataTableDensity;
empty?: ReactNode;
enableSelection?: boolean;
getRowId?: (row: TData, index: number) => string;
loading?: boolean;
loadingRowCount?: number;
+ onDensityChange?: (density: DataTableDensity) => void;
onPageIndexChange?: (pageIndex: number) => void;
onPageSizeChange?: (pageSize: number) => void;
onSearchValueChange?: (searchValue: string) => void;
onSelectionChange?: (selection: Record) => void;
onSortingChange?: (sorting: DataTableSort[]) => void;
+ onVisibleColumnsChange?: (visibility: Record) => void;
pageIndex?: number;
pageSize?: number;
pageSizeOptions?: number[];
+ renderRowDetails?: (row: TData) => ReactNode;
renderRowActions?: (row: TData) => ReactNode;
+ rowDetailsDescription?: ReactNode | ((row: TData) => ReactNode);
+ rowDetailsLabel?: string;
+ rowDetailsTitle?: ReactNode | ((row: TData) => ReactNode);
rows: TData[];
searchLabel?: string;
searchPlaceholder?: string;
@@ -116,6 +144,7 @@ export type DataTableProps = Omit, "child
sorting?: DataTableSort[];
tableLabel?: string;
toolbarActions?: ReactNode;
+ visibleColumns?: Record;
};
type InternalColumnMeta = {
@@ -212,25 +241,34 @@ function DataTableInner(
{
className,
columns,
+ defaultDensity = "comfortable",
defaultPageIndex = 0,
defaultPageSize = 5,
defaultSearchValue = "",
defaultSelection = {},
defaultSorting = [],
+ defaultVisibleColumns = {},
+ density,
empty,
enableSelection = false,
getRowId,
loading = false,
loadingRowCount = 5,
+ onDensityChange,
onPageIndexChange,
onPageSizeChange,
onSearchValueChange,
onSelectionChange,
onSortingChange,
+ onVisibleColumnsChange,
pageIndex,
pageSize,
pageSizeOptions = [5, 10, 20],
+ renderRowDetails,
renderRowActions,
+ rowDetailsDescription,
+ rowDetailsLabel = "Open details",
+ rowDetailsTitle,
rows,
searchLabel = "Search rows",
searchPlaceholder = "Search rows",
@@ -241,6 +279,7 @@ function DataTableInner(
sorting,
tableLabel = "Data table",
toolbarActions,
+ visibleColumns,
...props
}: DataTableProps,
ref: ForwardedRef
@@ -275,6 +314,19 @@ function DataTableInner(
defaultValue: defaultPageSize,
onChange: onPageSizeChange
});
+ const [currentDensity, setCurrentDensity] = useControllableState({
+ controlledValue: density,
+ defaultValue: defaultDensity,
+ onChange: onDensityChange
+ });
+ const [currentVisibleColumns, setCurrentVisibleColumns] = useControllableState<
+ Record
+ >({
+ controlledValue: visibleColumns,
+ defaultValue: defaultVisibleColumns,
+ onChange: onVisibleColumnsChange
+ });
+ const [detailRowId, setDetailRowId] = useState(null);
const resolvedPageSizeOptions = [...new Set([...pageSizeOptions, currentPageSize])].sort(
(left, right) => left - right
@@ -325,6 +377,34 @@ function DataTableInner(
} satisfies ColumnDef
]
: []),
+ ...(renderRowDetails
+ ? [
+ {
+ cell: ({ row }) => (
+
+ {
+ setDetailRowId(row.id);
+ }}
+ >
+ {rowDetailsLabel}
+
+
+ ),
+ enableGlobalFilter: false,
+ enableHiding: false,
+ enableSorting: false,
+ header: () => Row details ,
+ id: "__details",
+ meta: {
+ align: "end",
+ width: 108
+ } satisfies InternalColumnMeta
+ } satisfies ColumnDef
+ ]
+ : []),
...columns.map((column) => ({
accessorFn: column.accessor
? (row: TData) => getColumnAccessorValue(row, column)
@@ -334,6 +414,7 @@ function DataTableInner(
? column.cell(row.original)
: stringifySearchValue(getColumnAccessorValue(row.original, column)),
enableGlobalFilter: searchableColumns.includes(column),
+ enableHiding: column.hideable ?? true,
enableSorting: column.sortable ?? false,
header: ({ column: tanstackColumn }: HeaderContext) =>
typeof column.header === "function"
@@ -424,12 +505,21 @@ function DataTableInner(
setCurrentSorting(nextValue.map((item) => ({ desc: item.desc, id: item.id })));
},
+ onColumnVisibilityChange: (updater) => {
+ const nextValue =
+ typeof updater === "function"
+ ? updater(currentVisibleColumns)
+ : updater;
+
+ setCurrentVisibleColumns(nextValue);
+ },
state: {
globalFilter: currentSearchValue,
pagination: {
pageIndex: currentPageIndex,
pageSize: currentPageSize
} satisfies PaginationState,
+ columnVisibility: currentVisibleColumns,
rowSelection: currentSelection,
sorting: currentSorting as SortingState
}
@@ -437,11 +527,16 @@ function DataTableInner(
const totalColumns = table.getAllLeafColumns().length;
const filteredRowCount = table.getFilteredRowModel().rows.length;
+ const hideableColumns = table
+ .getAllLeafColumns()
+ .filter((column) => !column.id.startsWith("__") && column.getCanHide());
const selectedRows = table.getSelectedRowModel().rows.map((row) => row.original);
const pageCount = table.getPageCount();
const isEmpty = !loading && filteredRowCount === 0;
const shouldRenderSearch = searchableColumns.length > 0;
- const shouldRenderToolbar = shouldRenderSearch || toolbarActions !== undefined;
+ const shouldRenderViewOptions = hideableColumns.length > 0;
+ const shouldRenderToolbar =
+ shouldRenderSearch || toolbarActions !== undefined || shouldRenderViewOptions;
const pageStart =
filteredRowCount === 0 ? 0 : currentPageIndex * currentPageSize + 1;
const pageEnd = Math.min((currentPageIndex + 1) * currentPageSize, filteredRowCount);
@@ -454,6 +549,25 @@ function DataTableInner(
}
}, [currentPageIndex, pageCount, setCurrentPageIndex]);
+ const detailRow = renderRowDetails
+ ? table.getPrePaginationRowModel().rows.find((row) => row.id === detailRowId)
+ : undefined;
+ const detailRowOriginal = detailRow?.original;
+ const resolvedDetailTitle =
+ detailRowOriginal && rowDetailsTitle
+ ? typeof rowDetailsTitle === "function"
+ ? rowDetailsTitle(detailRowOriginal)
+ : rowDetailsTitle
+ : detailRowOriginal
+ ? "Row details"
+ : null;
+ const resolvedDetailDescription =
+ detailRowOriginal && rowDetailsDescription
+ ? typeof rowDetailsDescription === "function"
+ ? rowDetailsDescription(detailRowOriginal)
+ : rowDetailsDescription
+ : null;
+
return (
(
selected: selectedRows.length > 0
})}
className={cn(dataTableRootVariants(), className)}
+ data-density={currentDensity}
ref={ref}
>
{shouldRenderToolbar ? (
@@ -483,7 +598,53 @@ function DataTableInner(
) : null}
- {toolbarActions ?