@iloveagents/foundry-web-ui 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +23 -0
  2. package/dist/components/data-table/data-table-column-header.d.ts +8 -0
  3. package/dist/components/data-table/data-table-column-header.js +12 -0
  4. package/dist/components/data-table/data-table-faceted-filter.d.ts +16 -0
  5. package/dist/components/data-table/data-table-faceted-filter.js +32 -0
  6. package/dist/components/data-table/data-table-pagination.d.ts +8 -0
  7. package/dist/components/data-table/data-table-pagination.js +15 -0
  8. package/dist/components/data-table/data-table-row-actions.d.ts +16 -0
  9. package/dist/components/data-table/data-table-row-actions.js +11 -0
  10. package/dist/components/data-table/data-table-toolbar.d.ts +27 -0
  11. package/dist/components/data-table/data-table-toolbar.js +18 -0
  12. package/dist/components/data-table/data-table-view-options.d.ts +7 -0
  13. package/dist/components/data-table/data-table-view-options.js +11 -0
  14. package/dist/components/data-table/data-table.d.ts +25 -0
  15. package/dist/components/data-table/data-table.js +65 -0
  16. package/dist/components/data-table/facets.d.ts +82 -0
  17. package/dist/components/data-table/facets.js +156 -0
  18. package/dist/components/data-table/selection-column.d.ts +4 -0
  19. package/dist/components/data-table/selection-column.js +19 -0
  20. package/dist/components/data-table/state.d.ts +41 -0
  21. package/dist/components/data-table/state.js +148 -0
  22. package/dist/components/data-table/use-data-table.d.ts +22 -0
  23. package/dist/components/data-table/use-data-table.js +102 -0
  24. package/dist/index.d.ts +14 -2
  25. package/dist/index.js +14 -2
  26. package/dist/ui/dropdown-menu.d.ts +1 -0
  27. package/dist/ui/dropdown-menu.js +4 -1
  28. package/package.json +5 -4
package/README.md CHANGED
@@ -38,6 +38,29 @@ components are generated:
38
38
  @source "../node_modules/@iloveagents/foundry-web-ui/dist";
39
39
  ```
40
40
 
41
+ ## Lists
42
+
43
+ Every list renders through one `DataTable` family (TanStack Table underneath):
44
+ AND-of-tokens search, faceted filters with live counts (OR within a facet, AND
45
+ across facets, array cells count per element, empty cells as "None"), sortable
46
+ headers, column visibility, row selection, optional paging, optional grouping,
47
+ and a URL codec that keeps `q` / `sort` / `f_<column>` / `hide` / `page` / `size`.
48
+
49
+ ```tsx
50
+ const table = useDataTable({ data: rows, columns, getRowId: (row) => row.id, pageSize: 20 });
51
+
52
+ <DataTableToolbar table={table} facets={[{ columnId: "status" }, { columnId: "priority" }]} />
53
+ <DataTable table={table} onRowClick={(row) => open(row.original)} />
54
+ <DataTablePagination table={table} />
55
+ ```
56
+
57
+ Give a column `filterFn: facetFilterFn` (or the string `"facet"` in untyped
58
+ code) to use it as a facet; `meta.facetLabels` names
59
+ its values, `meta.label` names the column in the View menu, and
60
+ `DataTableColumnHeader` makes a header sortable. Pass `state` + `onStateChange`
61
+ (with `parseDataTableState` / `mergeDataTableUrlState`) to keep the whole list
62
+ state in the URL.
63
+
41
64
  ## Extension Points
42
65
 
43
66
  Feature modules (like SPACES) plug into `@iloveagents/foundry-web-ui` via registries — no direct imports needed:
@@ -0,0 +1,8 @@
1
+ /** Sortable column header with an asc / desc / hide menu. */
2
+ import type { Column } from "@tanstack/react-table";
3
+ export interface DataTableColumnHeaderProps<T> {
4
+ column: Column<T, unknown>;
5
+ title: string;
6
+ className?: string;
7
+ }
8
+ export declare function DataTableColumnHeader<T>({ column, title, className, }: DataTableColumnHeaderProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react";
3
+ import { Button, cn } from "@iloveagents/foundry-web-primitives";
4
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu.js";
5
+ export function DataTableColumnHeader({ column, title, className, }) {
6
+ if (!column.getCanSort()) {
7
+ return _jsx("div", { className: cn("font-medium", className), children: title });
8
+ }
9
+ const sorted = column.getIsSorted();
10
+ const align = column.columnDef.meta?.align;
11
+ return (_jsx("div", { className: cn("flex items-center", align === "right" && "justify-end", className), children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": `${title}: ${sorted === "asc" ? "sorted ascending" : sorted === "desc" ? "sorted descending" : "not sorted"}`, "data-sorted": sorted || undefined, className: "-ml-3 h-8 gap-1.5 rounded-lg px-2 text-xs font-medium text-muted-foreground data-[state=open]:bg-accent data-[sorted]:text-foreground", children: [_jsx("span", { children: title }), sorted === "desc" ? (_jsx(ArrowDown, { className: "size-3.5" })) : sorted === "asc" ? (_jsx(ArrowUp, { className: "size-3.5" })) : (_jsx(ChevronsUpDown, { className: "size-3.5 opacity-60" }))] }) }), _jsxs(DropdownMenuContent, { align: "start", className: "rounded-xl border-border/45 bg-popover/95 shadow-lg", children: [_jsxs(DropdownMenuItem, { onClick: () => column.toggleSorting(false), children: [_jsx(ArrowUp, { className: "size-3.5 text-muted-foreground" }), "Asc"] }), _jsxs(DropdownMenuItem, { onClick: () => column.toggleSorting(true), children: [_jsx(ArrowDown, { className: "size-3.5 text-muted-foreground" }), "Desc"] }), sorted ? (_jsxs(DropdownMenuItem, { onClick: () => column.clearSorting(), children: [_jsx(ChevronsUpDown, { className: "size-3.5 text-muted-foreground" }), "Unsorted"] })) : null, column.getCanHide() ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => column.toggleVisibility(false), children: [_jsx(EyeOff, { className: "size-3.5 text-muted-foreground" }), "Hide"] })] })) : null] })] }) }));
12
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * One facet: a dashed "+ Title" button that opens a checklist of options
3
+ * with live counts. Picking several options ORs them; the table ANDs the
4
+ * facets. Options come from the data unless declared.
5
+ */
6
+ import type { Column } from "@tanstack/react-table";
7
+ import { type DataTableFacetOption } from "./facets.js";
8
+ export interface DataTableFacetedFilterProps<T> {
9
+ column: Column<T, unknown>;
10
+ title?: string;
11
+ options?: DataTableFacetOption[];
12
+ /** Show a search box inside the list once there are more options than this (default 7). */
13
+ searchableFrom?: number;
14
+ className?: string;
15
+ }
16
+ export declare function DataTableFacetedFilter<T>({ column, title, options: declared, searchableFrom, className, }: DataTableFacetedFilterProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Check, PlusCircle } from "lucide-react";
3
+ import { useMemo, useState } from "react";
4
+ import { Button, cn } from "@iloveagents/foundry-web-primitives";
5
+ import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover.js";
6
+ import { columnLabel, facetOptions, normalizeFacetValue, } from "./facets.js";
7
+ export function DataTableFacetedFilter({ column, title, options: declared, searchableFrom = 7, className, }) {
8
+ const [query, setQuery] = useState("");
9
+ const label = title ?? columnLabel(column);
10
+ const selected = normalizeFacetValue(column.getFilterValue());
11
+ const selectedSet = new Set(selected);
12
+ const options = facetOptions(column, declared);
13
+ const shown = useMemo(() => {
14
+ const needle = query.trim().toLowerCase();
15
+ return needle ? options.filter((o) => o.label.toLowerCase().includes(needle)) : options;
16
+ }, [options, query]);
17
+ const toggle = (value) => {
18
+ const next = selectedSet.has(value)
19
+ ? selected.filter((item) => item !== value)
20
+ : [...selected, value];
21
+ column.setFilterValue(next.length ? next : undefined);
22
+ };
23
+ return (_jsxs(Popover, { onOpenChange: (open) => !open && setQuery(""), children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", "data-facet": column.id, "data-active": selected.length ? "" : undefined, className: cn("h-8 rounded-xl border-dashed border-border/60 bg-background/65 shadow-none hover:bg-muted/26", selected.length && "border-solid border-primary/30", className), children: [_jsx(PlusCircle, { className: "size-4" }), label, selected.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "mx-0.5 h-4 w-px bg-border/60", "aria-hidden": "true" }), _jsx("span", { className: "rounded-md bg-muted px-1.5 py-0.5 text-xs font-normal lg:hidden", children: selected.length }), _jsx("span", { className: "hidden gap-1 lg:flex", children: selected.length > 2 ? (_jsxs("span", { className: "rounded-md bg-muted px-1.5 py-0.5 text-xs font-normal", children: [selected.length, " selected"] })) : (options
24
+ .filter((option) => selectedSet.has(option.value))
25
+ .map((option) => (_jsx("span", { className: "rounded-md bg-muted px-1.5 py-0.5 text-xs font-normal", children: option.label }, option.value)))) })] })) : null] }) }), _jsxs(PopoverContent, { align: "start", className: "w-56 p-1.5", children: [options.length >= searchableFrom ? (_jsx("input", { value: query, onChange: (event) => setQuery(event.target.value), placeholder: label, "aria-label": `Search ${label} options`, className: "mb-1 h-8 w-full rounded-lg border border-border/45 bg-background/72 px-2 text-sm outline-none focus:border-primary/30" })) : null, _jsx("div", { role: "group", "aria-label": `${label} filter`, className: "max-h-72 space-y-0.5 overflow-y-auto", children: shown.length === 0 ? (_jsx("div", { className: "px-2 py-4 text-center text-xs text-muted-foreground", children: "No options." })) : (shown.map((option) => {
26
+ const checked = selectedSet.has(option.value);
27
+ const Icon = option.icon;
28
+ return (_jsxs("button", { type: "button", role: "checkbox", "aria-checked": checked, onClick: () => toggle(option.value), className: cn("flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent", option.count === 0 && !checked && "text-muted-foreground"), children: [_jsx("span", { className: cn("flex size-4 shrink-0 items-center justify-center rounded border border-border", checked
29
+ ? "border-primary bg-primary text-primary-foreground"
30
+ : "opacity-60 [&_svg]:invisible"), children: _jsx(Check, { className: "size-3.5" }) }), Icon ? _jsx(Icon, { className: "size-4 text-muted-foreground" }) : null, _jsx("span", { className: "min-w-0 flex-1 truncate", children: option.label }), _jsx("span", { className: "ml-auto font-mono text-xs tabular-nums text-muted-foreground", children: option.count })] }, option.value));
31
+ })) }), selected.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "my-1 h-px bg-border/45" }), _jsx("button", { type: "button", onClick: () => column.setFilterValue(undefined), className: "w-full rounded-lg px-2 py-1.5 text-center text-sm hover:bg-accent", children: "Clear filter" })] })) : null] })] }));
32
+ }
@@ -0,0 +1,8 @@
1
+ /** Selection summary, rows-per-page, page x of y, first/prev/next/last. */
2
+ import type { Table } from "@tanstack/react-table";
3
+ export interface DataTablePaginationProps<T> {
4
+ table: Table<T>;
5
+ pageSizeOptions?: number[];
6
+ className?: string;
7
+ }
8
+ export declare function DataTablePagination<T>({ table, pageSizeOptions, className, }: DataTablePaginationProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
3
+ import { Button, cn } from "@iloveagents/foundry-web-primitives";
4
+ import { Select } from "../../ui/select.js";
5
+ export function DataTablePagination({ table, pageSizeOptions = [10, 20, 50, 100], className, }) {
6
+ const total = table.getFilteredRowModel().rows.length;
7
+ const selected = table.getFilteredSelectedRowModel().rows.length;
8
+ const { pageIndex, pageSize } = table.getState().pagination;
9
+ const pageCount = Math.max(1, table.getPageCount());
10
+ const sizes = pageSizeOptions.includes(pageSize)
11
+ ? pageSizeOptions
12
+ : [...pageSizeOptions, pageSize].sort((a, b) => a - b);
13
+ const iconButton = "size-8 rounded-lg border-border/45 bg-background/65 p-0 shadow-none hover:bg-muted/26";
14
+ return (_jsxs("div", { className: cn("flex flex-wrap items-center gap-x-6 gap-y-2 text-sm", className), children: [_jsx("div", { className: "flex-1 text-muted-foreground", "aria-live": "polite", children: selected > 0 ? `${selected} of ${total} row(s) selected.` : `${total} row(s)` }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-medium", children: "Rows per page" }), _jsx(Select, { value: pageSize, onChange: (event) => table.setPageSize(Number(event.target.value)), className: "h-8 w-[4.5rem] rounded-lg px-2 py-0", children: sizes.map((size) => (_jsx("option", { value: size, children: size }, size))) })] }), _jsxs("div", { className: "font-medium", children: ["Page ", pageIndex + 1, " of ", pageCount] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Button, { type: "button", variant: "outline", className: cn(iconButton, "hidden lg:inline-flex"), onClick: () => table.setPageIndex(0), disabled: !table.getCanPreviousPage(), children: [_jsx("span", { className: "sr-only", children: "Go to first page" }), _jsx(ChevronsLeft, { className: "size-4" })] }), _jsxs(Button, { type: "button", variant: "outline", className: iconButton, onClick: () => table.previousPage(), disabled: !table.getCanPreviousPage(), children: [_jsx("span", { className: "sr-only", children: "Go to previous page" }), _jsx(ChevronLeft, { className: "size-4" })] }), _jsxs(Button, { type: "button", variant: "outline", className: iconButton, onClick: () => table.nextPage(), disabled: !table.getCanNextPage(), children: [_jsx("span", { className: "sr-only", children: "Go to next page" }), _jsx(ChevronRight, { className: "size-4" })] }), _jsxs(Button, { type: "button", variant: "outline", className: cn(iconButton, "hidden lg:inline-flex"), onClick: () => table.setPageIndex(pageCount - 1), disabled: !table.getCanNextPage(), children: [_jsx("span", { className: "sr-only", children: "Go to last page" }), _jsx(ChevronsRight, { className: "size-4" })] })] })] }));
15
+ }
@@ -0,0 +1,16 @@
1
+ import type { ComponentType } from "react";
2
+ export interface DataTableRowAction {
3
+ label: string;
4
+ onSelect: () => void;
5
+ icon?: ComponentType<{
6
+ className?: string;
7
+ }>;
8
+ destructive?: boolean;
9
+ disabled?: boolean;
10
+ }
11
+ export interface DataTableRowActionsProps {
12
+ actions: Array<DataTableRowAction | "separator">;
13
+ label?: string;
14
+ className?: string;
15
+ }
16
+ export declare function DataTableRowActions({ actions, label, className, }: DataTableRowActionsProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** The "…" menu at the end of a row. Items are data, so agents and tests can read them. */
3
+ import { MoreHorizontal } from "lucide-react";
4
+ import { Button, cn } from "@iloveagents/foundry-web-primitives";
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu.js";
6
+ export function DataTableRowActions({ actions, label = "Open menu", className, }) {
7
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", "data-no-row-click": "", className: cn("size-8 rounded-lg p-0 data-[state=open]:bg-accent", className), children: [_jsx(MoreHorizontal, { className: "size-4" }), _jsx("span", { className: "sr-only", children: label })] }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-44 rounded-xl border-border/45 bg-popover/95 shadow-lg", children: actions.map((action, index) => action === "separator" ? (_jsx(DropdownMenuSeparator, {}, `sep-${index}`)) : (_jsxs(DropdownMenuItem, { disabled: action.disabled, onClick: (event) => {
8
+ event.stopPropagation();
9
+ action.onSelect();
10
+ }, className: cn(action.destructive && "text-destructive focus:text-destructive"), children: [action.icon ? _jsx(action.icon, { className: "size-4 text-muted-foreground" }) : null, action.label] }, action.label))) })] }));
11
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Search + facets + Reset on the left, actions + View menu on the right.
3
+ * Facets are declared by column id; everything else is read off the table.
4
+ */
5
+ import type { Table } from "@tanstack/react-table";
6
+ import type { ReactNode } from "react";
7
+ import type { DataTableFacetOption } from "./facets.js";
8
+ export interface DataTableFacet {
9
+ columnId: string;
10
+ title?: string;
11
+ options?: DataTableFacetOption[];
12
+ }
13
+ export interface DataTableToolbarProps<T> {
14
+ table: Table<T>;
15
+ facets?: DataTableFacet[];
16
+ searchPlaceholder?: string;
17
+ /** Hide the search box. */
18
+ search?: boolean;
19
+ /** Hide the View (column visibility) menu. */
20
+ viewOptions?: boolean;
21
+ /** Extra controls rendered next to the facets (left side). */
22
+ children?: ReactNode;
23
+ /** Controls rendered on the right, before the View menu. */
24
+ actions?: ReactNode;
25
+ className?: string;
26
+ }
27
+ export declare function DataTableToolbar<T>({ table, facets, searchPlaceholder, search, viewOptions, children, actions, className, }: DataTableToolbarProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Search, X } from "lucide-react";
3
+ import { Button, Input, cn } from "@iloveagents/foundry-web-primitives";
4
+ import { DataTableFacetedFilter } from "./data-table-faceted-filter.js";
5
+ import { DataTableViewOptions } from "./data-table-view-options.js";
6
+ export function DataTableToolbar({ table, facets = [], searchPlaceholder = "Search…", search = true, viewOptions = true, children, actions, className, }) {
7
+ const state = table.getState();
8
+ const isFiltered = state.columnFilters.length > 0 || Boolean(state.globalFilter);
9
+ return (_jsxs("div", { className: cn("flex flex-wrap items-center gap-2", className), children: [search ? (_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { type: "search", value: state.globalFilter ?? "", onChange: (event) => table.setGlobalFilter(event.target.value), placeholder: searchPlaceholder, "aria-label": searchPlaceholder, className: "h-8 w-[160px] rounded-xl border-border/45 bg-background/72 pl-8 text-sm shadow-none lg:w-[250px]" })] })) : null, facets.map((facet) => {
10
+ const column = table.getColumn(facet.columnId);
11
+ if (!column)
12
+ return null;
13
+ return (_jsx(DataTableFacetedFilter, { column: column, title: facet.title, options: facet.options }, facet.columnId));
14
+ }), children, isFiltered ? (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
15
+ table.resetColumnFilters();
16
+ table.setGlobalFilter("");
17
+ }, className: "h-8 rounded-xl px-2.5", children: ["Reset", _jsx(X, { className: "size-4" })] })) : null, actions || viewOptions ? (_jsxs("div", { className: "ml-auto flex items-center gap-2", children: [actions, viewOptions ? _jsx(DataTableViewOptions, { table: table }) : null] })) : null] }));
18
+ }
@@ -0,0 +1,7 @@
1
+ /** The "View" menu: toggle column visibility. */
2
+ import type { Table } from "@tanstack/react-table";
3
+ export interface DataTableViewOptionsProps<T> {
4
+ table: Table<T>;
5
+ className?: string;
6
+ }
7
+ export declare function DataTableViewOptions<T>({ table, className }: DataTableViewOptionsProps<T>): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Settings2 } from "lucide-react";
3
+ import { Button, cn } from "@iloveagents/foundry-web-primitives";
4
+ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu.js";
5
+ import { columnLabel } from "./facets.js";
6
+ export function DataTableViewOptions({ table, className }) {
7
+ const columns = table.getAllLeafColumns().filter((column) => column.getCanHide());
8
+ if (!columns.length)
9
+ return null;
10
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: cn("h-8 rounded-xl border-border/45 bg-background/65 shadow-none hover:bg-muted/26", className), children: [_jsx(Settings2, { className: "size-4" }), "View"] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-48 rounded-xl border-border/45 bg-popover/95 shadow-lg", children: [_jsx(DropdownMenuLabel, { children: "Toggle columns" }), _jsx(DropdownMenuSeparator, {}), columns.map((column) => (_jsx(DropdownMenuCheckboxItem, { checked: column.getIsVisible(), onCheckedChange: (checked) => column.toggleVisibility(Boolean(checked)), onSelect: (event) => event.preventDefault(), className: "capitalize", children: columnLabel(column) }, column.id)))] })] }));
11
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The grid. Renders whatever a `useDataTable` instance says: header groups,
3
+ * flat rows, or group header rows with their expanded children. Row clicks
4
+ * ignore interactive descendants so a selection checkbox or a row menu
5
+ * never doubles as "open".
6
+ */
7
+ import { type Row, type Table } from "@tanstack/react-table";
8
+ import type { KeyboardEvent, MouseEvent, ReactNode } from "react";
9
+ export interface DataTableProps<T> {
10
+ table: Table<T>;
11
+ onRowClick?: (row: Row<T>, event: MouseEvent | KeyboardEvent) => void;
12
+ /** The row whose details are open elsewhere (drawer, panel) — highlighted, `aria-current`. */
13
+ activeRowId?: string | null;
14
+ rowClassName?: (row: Row<T>) => string | undefined;
15
+ /** Content of a group header row (default: label and count). */
16
+ renderGroup?: (row: Row<T>) => ReactNode;
17
+ /** Declared order of group keys; unknown keys follow in data order. */
18
+ groupOrder?: string[];
19
+ emptyState?: ReactNode;
20
+ stickyHeader?: boolean;
21
+ dense?: boolean;
22
+ className?: string;
23
+ "aria-label"?: string;
24
+ }
25
+ export declare function DataTable<T>({ table, onRowClick, activeRowId, rowClassName, renderGroup, groupOrder, emptyState, stickyHeader, dense, className, "aria-label": ariaLabel, }: DataTableProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The grid. Renders whatever a `useDataTable` instance says: header groups,
4
+ * flat rows, or group header rows with their expanded children. Row clicks
5
+ * ignore interactive descendants so a selection checkbox or a row menu
6
+ * never doubles as "open".
7
+ */
8
+ import { flexRender } from "@tanstack/react-table";
9
+ import { ChevronDown, ChevronRight } from "lucide-react";
10
+ import { cn } from "@iloveagents/foundry-web-primitives";
11
+ const INTERACTIVE = "a, button, input, select, textarea, [role='menuitem'], [data-no-row-click]";
12
+ function orderedRows(table, groupOrder) {
13
+ const rows = table.getRowModel().rows;
14
+ if (!table.getState().grouping.length)
15
+ return rows;
16
+ const top = rows.filter((row) => row.depth === 0);
17
+ const rank = new Map((groupOrder ?? []).map((key, index) => [key, index]));
18
+ const sorted = groupOrder
19
+ ? [...top].sort((a, b) => {
20
+ const ra = rank.get(String(a.groupingValue ?? "")) ?? Number.MAX_SAFE_INTEGER;
21
+ const rb = rank.get(String(b.groupingValue ?? "")) ?? Number.MAX_SAFE_INTEGER;
22
+ return ra - rb;
23
+ })
24
+ : top;
25
+ return sorted.flatMap((row) => row.getIsGrouped() && row.getIsExpanded() ? [row, ...row.subRows] : [row]);
26
+ }
27
+ export function DataTable({ table, onRowClick, activeRowId, rowClassName, renderGroup, groupOrder, emptyState = "No results.", stickyHeader = false, dense = false, className, "aria-label": ariaLabel, }) {
28
+ const rows = orderedRows(table, groupOrder);
29
+ const columnCount = table.getVisibleLeafColumns().length;
30
+ const cellPad = dense ? "px-3 py-1.5" : "px-3 py-2.5";
31
+ const activate = (row, event) => {
32
+ if (!onRowClick)
33
+ return;
34
+ const target = event.target;
35
+ if (target?.closest(INTERACTIVE))
36
+ return;
37
+ onRowClick(row, event);
38
+ };
39
+ return (_jsx("div", { className: cn("overflow-hidden rounded-2xl border border-border/45 bg-card/60", className), children: _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full caption-bottom text-sm", "aria-label": ariaLabel, children: [_jsx("thead", { className: cn(stickyHeader && "sticky top-0 z-10 bg-card"), children: table.getHeaderGroups().map((headerGroup) => (_jsx("tr", { className: "border-b border-border/45", children: headerGroup.headers.map((header) => {
40
+ const meta = header.column.columnDef.meta;
41
+ return (_jsx("th", { colSpan: header.colSpan, scope: "col", style: header.getSize() !== 150 ? { width: header.getSize() } : undefined, className: cn("h-10 px-3 text-left align-middle text-xs font-medium text-muted-foreground", meta?.align === "right" && "text-right", meta?.align === "center" && "text-center", meta?.headerClassName), children: header.isPlaceholder
42
+ ? null
43
+ : flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
44
+ }) }, headerGroup.id))) }), _jsx("tbody", { children: rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columnCount, className: "h-24 text-center text-sm text-muted-foreground", children: emptyState }) })) : (rows.map((row) => row.getIsGrouped() ? (_jsx("tr", { "data-group-row": "", className: "border-t border-border/45 bg-muted/35 text-sm font-medium", children: _jsx("td", { colSpan: columnCount, className: "px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", onClick: row.getToggleExpandedHandler(), "aria-expanded": row.getIsExpanded(), "aria-label": row.getIsExpanded() ? "Collapse group" : "Expand group", className: "rounded-md p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground", children: row.getIsExpanded() ? (_jsx(ChevronDown, { className: "size-4" })) : (_jsx(ChevronRight, { className: "size-4" })) }), _jsx("div", { className: "min-w-0 flex-1", children: renderGroup ? renderGroup(row) : _jsx(DefaultGroupLabel, { row: row }) })] }) }) }, row.id)) : (_jsx("tr", { "data-row-id": row.id, "data-state": row.getIsSelected() ? "selected" : undefined, "data-active": activeRowId != null && row.id === activeRowId ? "" : undefined, "aria-current": activeRowId != null && row.id === activeRowId ? "true" : undefined, "aria-selected": row.getIsSelected() || undefined, tabIndex: onRowClick ? 0 : undefined, onClick: (event) => activate(row, event), onKeyDown: (event) => {
45
+ if (event.key === "Enter" || event.key === " ") {
46
+ if (event.target.closest(INTERACTIVE))
47
+ return;
48
+ event.preventDefault();
49
+ activate(row, event);
50
+ }
51
+ }, className: cn("border-t border-border/35 transition-colors", onRowClick &&
52
+ "cursor-pointer hover:bg-muted/30 focus-visible:bg-muted/30 focus-visible:outline-none", "data-[state=selected]:bg-primary/6 data-[active]:bg-primary/8", rowClassName?.(row)), children: row.getVisibleCells().map((cell) => {
53
+ const meta = cell.column.columnDef.meta;
54
+ return (_jsx("td", { className: cn(cellPad, "align-middle", meta?.align === "right" && "text-right", meta?.align === "center" && "text-center", meta?.className), children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
55
+ }) }, row.id)))) })] }) }) }));
56
+ }
57
+ function DefaultGroupLabel({ row }) {
58
+ const grouping = row
59
+ .getAllCells()
60
+ .map((cell) => cell.column)
61
+ .find((column) => column.getIsGrouped());
62
+ const key = String(row.groupingValue ?? "");
63
+ const label = grouping ? (grouping.columnDef.meta?.facetLabels?.[key] ?? key) : key;
64
+ return (_jsxs("span", { children: [label || "None", " ", _jsxs("span", { className: "font-normal text-muted-foreground", children: ["(", row.subRows.length, ")"] })] }));
65
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Facet semantics shared by every DataTable: how a cell value turns into
3
+ * facet keys, how a facet filter matches (OR within one facet — the table
4
+ * ANDs facets together), and how option counts are derived from the rows
5
+ * that every *other* filter lets through (TanStack's faceted row model), so
6
+ * a count always reads "rows you'd see if you added this option".
7
+ */
8
+ import type { Column, FilterFn, Row, RowData } from "@tanstack/react-table";
9
+ import type { ComponentType } from "react";
10
+ declare module "@tanstack/react-table" {
11
+ interface ColumnMeta<TData extends RowData, TValue> {
12
+ /** Label for the View menu, facet titles and agent descriptions (defaults to a string header, then the id). */
13
+ label?: string;
14
+ /** Class for body cells of this column. */
15
+ className?: string;
16
+ /** Class for the header cell of this column. */
17
+ headerClassName?: string;
18
+ /** Horizontal alignment of header and cells. */
19
+ align?: "left" | "center" | "right";
20
+ /** Text the global search sees for this column instead of the cell value. */
21
+ searchText?: (row: TData) => string;
22
+ /** Labels for facet keys and group headers (`value → label`). */
23
+ facetLabels?: Record<string, string>;
24
+ /** Keep unused type parameter referenced for declaration merging. */
25
+ __cellValue?: TValue;
26
+ /** Hosts may carry their own keys in `meta` (e.g. a server sort key). */
27
+ [key: string]: unknown;
28
+ }
29
+ }
30
+ /** Facet key for empty cells (`null`, `undefined`, `""`, `[]`). */
31
+ export declare const FACET_EMPTY = "__none__";
32
+ export interface DataTableFacetOption {
33
+ value: string;
34
+ label: string;
35
+ icon?: ComponentType<{
36
+ className?: string;
37
+ }>;
38
+ }
39
+ export interface DataTableFacetCount extends DataTableFacetOption {
40
+ count: number;
41
+ }
42
+ /**
43
+ * Keys a cell value contributes to a facet — arrays contribute one key per
44
+ * element. A real value that spells the empty sentinel is escaped with a
45
+ * leading backslash so it never masquerades as "no value".
46
+ */
47
+ export declare function facetKeys(value: unknown): string[];
48
+ /** A facet filter value is a list of selected keys; anything else means "no filter". */
49
+ export declare function normalizeFacetValue(filterValue: unknown): string[];
50
+ /**
51
+ * OR within a facet: a row passes when any of its keys is selected. Typed
52
+ * loosely so `filterFn: facetFilterFn` fits any row type; `useDataTable`
53
+ * also registers it as the string `"facet"` for untyped (JSX) columns.
54
+ */
55
+ export declare const facetFilterFn: FilterFn<any>;
56
+ /** Rows per facet key, counted over the rows every OTHER filter lets through. */
57
+ export declare function facetCounts<T>(column: Column<T, unknown>): Map<string, number>;
58
+ export interface FacetOptionsOptions {
59
+ /** Label for keys that are neither declared nor in `meta.facetLabels`. */
60
+ labelFor?: (key: string) => string;
61
+ /** Label for the empty-cell option (default "None"). */
62
+ emptyLabel?: string;
63
+ }
64
+ /**
65
+ * Declared options first (in declared order, kept even at count 0), then
66
+ * every undeclared key seen in the data, then the empty option when some
67
+ * rows have no value.
68
+ */
69
+ export declare function facetOptions<T>(column: Column<T, unknown>, declared?: DataTableFacetOption[], options?: FacetOptionsOptions): DataTableFacetCount[];
70
+ /** Plain-text projection of a cell value for search and agent read-outs. */
71
+ export declare function cellText(value: unknown): string;
72
+ /**
73
+ * Lower-cased text of every searchable cell of a row. Cached per row object
74
+ * and invalidated when the row's cells change identity (new columns or a
75
+ * changed `meta.searchText`), since TanStack keeps row objects across
76
+ * column changes.
77
+ */
78
+ export declare function rowSearchText<T>(row: Row<T>): string;
79
+ /** Every whitespace-separated token must appear somewhere in the row (AND across tokens). */
80
+ export declare const tokenSearchFilterFn: FilterFn<any>;
81
+ /** Human label of a column: `meta.label`, then a string header, then the id. */
82
+ export declare function columnLabel<T>(column: Column<T, unknown>): string;
@@ -0,0 +1,156 @@
1
+ /** Facet key for empty cells (`null`, `undefined`, `""`, `[]`). */
2
+ export const FACET_EMPTY = "__none__";
3
+ /**
4
+ * Keys a cell value contributes to a facet — arrays contribute one key per
5
+ * element. A real value that spells the empty sentinel is escaped with a
6
+ * leading backslash so it never masquerades as "no value".
7
+ */
8
+ export function facetKeys(value) {
9
+ if (value == null || value === "")
10
+ return [FACET_EMPTY];
11
+ if (value === FACET_EMPTY)
12
+ return [`\\${FACET_EMPTY}`];
13
+ if (Array.isArray(value)) {
14
+ const keys = value.flatMap((item) => facetKeys(item)).filter((key) => key !== FACET_EMPTY);
15
+ return keys.length ? Array.from(new Set(keys)) : [FACET_EMPTY];
16
+ }
17
+ if (typeof value === "object") {
18
+ const record = value;
19
+ const key = record.id ?? record.value ?? record.name;
20
+ return key == null || key === "" ? [FACET_EMPTY] : [String(key)];
21
+ }
22
+ return [String(value)];
23
+ }
24
+ /** A facet filter value is a list of selected keys; anything else means "no filter". */
25
+ export function normalizeFacetValue(filterValue) {
26
+ if (Array.isArray(filterValue))
27
+ return filterValue.map(String).filter(Boolean);
28
+ if (filterValue == null || filterValue === "")
29
+ return [];
30
+ return [String(filterValue)];
31
+ }
32
+ /**
33
+ * OR within a facet: a row passes when any of its keys is selected. Typed
34
+ * loosely so `filterFn: facetFilterFn` fits any row type; `useDataTable`
35
+ * also registers it as the string `"facet"` for untyped (JSX) columns.
36
+ */
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ export const facetFilterFn = (row, columnId, filterValue) => {
39
+ const wanted = normalizeFacetValue(filterValue);
40
+ if (!wanted.length)
41
+ return true;
42
+ return facetKeys(row.getValue(columnId)).some((key) => wanted.includes(key));
43
+ };
44
+ facetFilterFn.autoRemove = (value) => normalizeFacetValue(value).length === 0;
45
+ /** Rows per facet key, counted over the rows every OTHER filter lets through. */
46
+ export function facetCounts(column) {
47
+ const counts = new Map();
48
+ for (const row of column.getFacetedRowModel().flatRows) {
49
+ for (const key of facetKeys(row.getValue(column.id))) {
50
+ counts.set(key, (counts.get(key) ?? 0) + 1);
51
+ }
52
+ }
53
+ return counts;
54
+ }
55
+ /**
56
+ * Declared options first (in declared order, kept even at count 0), then
57
+ * every undeclared key seen in the data, then the empty option when some
58
+ * rows have no value.
59
+ */
60
+ export function facetOptions(column, declared, options) {
61
+ const counts = facetCounts(column);
62
+ const labels = column.columnDef.meta?.facetLabels ?? {};
63
+ const labelFor = (key) => labels[key] ?? options?.labelFor?.(key) ?? key;
64
+ const seen = new Set();
65
+ const out = [];
66
+ for (const option of declared ?? []) {
67
+ seen.add(option.value);
68
+ out.push({ ...option, count: counts.get(option.value) ?? 0 });
69
+ }
70
+ const rest = Array.from(counts.keys())
71
+ .filter((key) => !seen.has(key) && key !== FACET_EMPTY)
72
+ .sort((a, b) => labelFor(a).localeCompare(labelFor(b)));
73
+ for (const key of rest)
74
+ out.push({ value: key, label: labelFor(key), count: counts.get(key) ?? 0 });
75
+ if (counts.has(FACET_EMPTY) && !seen.has(FACET_EMPTY)) {
76
+ out.push({
77
+ value: FACET_EMPTY,
78
+ label: labels[FACET_EMPTY] ?? options?.emptyLabel ?? "None",
79
+ count: counts.get(FACET_EMPTY) ?? 0,
80
+ });
81
+ }
82
+ return out;
83
+ }
84
+ /** Plain-text projection of a cell value for search and agent read-outs. */
85
+ export function cellText(value) {
86
+ if (value == null)
87
+ return "";
88
+ if (typeof value === "string")
89
+ return value;
90
+ if (typeof value === "number" || typeof value === "bigint")
91
+ return String(value);
92
+ if (typeof value === "boolean")
93
+ return value ? "yes" : "no";
94
+ if (value instanceof Date)
95
+ return value.toISOString();
96
+ if (Array.isArray(value))
97
+ return value.map(cellText).filter(Boolean).join(" ");
98
+ if (typeof value === "object") {
99
+ const record = value;
100
+ if (typeof record.name === "string")
101
+ return record.name;
102
+ if (typeof record.label === "string")
103
+ return record.label;
104
+ try {
105
+ return JSON.stringify(value);
106
+ }
107
+ catch {
108
+ return "";
109
+ }
110
+ }
111
+ return String(value);
112
+ }
113
+ const searchTextCache = new WeakMap();
114
+ /**
115
+ * Lower-cased text of every searchable cell of a row. Cached per row object
116
+ * and invalidated when the row's cells change identity (new columns or a
117
+ * changed `meta.searchText`), since TanStack keeps row objects across
118
+ * column changes.
119
+ */
120
+ export function rowSearchText(row) {
121
+ const cells = row.getAllCells();
122
+ const cached = searchTextCache.get(row);
123
+ if (cached && cached.cells === cells)
124
+ return cached.text;
125
+ const text = cells
126
+ .filter((cell) => cell.column.getCanGlobalFilter())
127
+ .map((cell) => {
128
+ const custom = cell.column.columnDef.meta?.searchText;
129
+ return custom ? custom(row.original) : cellText(cell.getValue());
130
+ })
131
+ .join(" ")
132
+ .toLowerCase();
133
+ searchTextCache.set(row, { cells, text });
134
+ return text;
135
+ }
136
+ /** Every whitespace-separated token must appear somewhere in the row (AND across tokens). */
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ export const tokenSearchFilterFn = (row, _columnId, filterValue) => {
139
+ const tokens = String(filterValue ?? "")
140
+ .toLowerCase()
141
+ .split(/\s+/)
142
+ .filter(Boolean);
143
+ if (!tokens.length)
144
+ return true;
145
+ const hay = rowSearchText(row);
146
+ return tokens.every((token) => hay.includes(token));
147
+ };
148
+ tokenSearchFilterFn.autoRemove = (value) => !String(value ?? "").trim();
149
+ /** Human label of a column: `meta.label`, then a string header, then the id. */
150
+ export function columnLabel(column) {
151
+ const meta = column.columnDef.meta;
152
+ if (meta?.label)
153
+ return meta.label;
154
+ const header = column.columnDef.header;
155
+ return typeof header === "string" ? header : column.id;
156
+ }
@@ -0,0 +1,4 @@
1
+ /** A leading checkbox column: header selects the page, cells select the row. */
2
+ import type { ColumnDef } from "@tanstack/react-table";
3
+ export declare const SELECTION_COLUMN_ID = "__select";
4
+ export declare function selectionColumn<T>(): ColumnDef<T, unknown>;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Checkbox } from "@iloveagents/foundry-web-primitives";
3
+ export const SELECTION_COLUMN_ID = "__select";
4
+ export function selectionColumn() {
5
+ return {
6
+ id: SELECTION_COLUMN_ID,
7
+ size: 36,
8
+ enableSorting: false,
9
+ enableHiding: false,
10
+ enableGlobalFilter: false,
11
+ header: ({ table }) => (_jsx(Checkbox, { checked: table.getIsAllPageRowsSelected()
12
+ ? true
13
+ : table.getIsSomePageRowsSelected()
14
+ ? "indeterminate"
15
+ : false, onCheckedChange: (value) => table.toggleAllPageRowsSelected(Boolean(value)), "aria-label": "Select all", className: "translate-y-0.5" })),
16
+ cell: ({ row }) => (_jsx(Checkbox, { checked: row.getIsSelected(), disabled: !row.getCanSelect(), onCheckedChange: (value) => row.toggleSelected(Boolean(value)), "aria-label": "Select row", className: "translate-y-0.5" })),
17
+ meta: { label: "Select", headerClassName: "w-9", className: "w-9" },
18
+ };
19
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The whole user-facing state of a DataTable as one value, plus a URL codec.
3
+ *
4
+ * URL keys are shared with the older collection pages (`q`, `sort`, `f_<id>`)
5
+ * so a link that worked before keeps working; `hide`, `page` and `size` are
6
+ * new. Selection and expansion are never put in the URL. A present-but-empty
7
+ * key (`f_status=`, `sort=`, `hide=`, `q=`) means "cleared", so a default the
8
+ * host configured can be switched off by the user and survive a reload.
9
+ */
10
+ import type { ColumnFiltersState, ExpandedState, PaginationState, RowSelectionState, SortingState, VisibilityState } from "@tanstack/react-table";
11
+ export interface DataTableState {
12
+ globalFilter: string;
13
+ columnFilters: ColumnFiltersState;
14
+ sorting: SortingState;
15
+ columnVisibility: VisibilityState;
16
+ rowSelection: RowSelectionState;
17
+ pagination: PaginationState;
18
+ expanded: ExpandedState;
19
+ }
20
+ export declare const DEFAULT_PAGE_SIZE = 20;
21
+ export declare const EMPTY_DATA_TABLE_STATE: DataTableState;
22
+ export declare function isDataTableUrlKey(key: string): boolean;
23
+ export interface DataTableUrlOptions {
24
+ /** Page size that is implied when `size` is absent (default 20). */
25
+ defaultPageSize?: number;
26
+ /**
27
+ * State implied when the URL says nothing. Serializing writes an explicit
28
+ * empty key when the user cleared a value the defaults would bring back.
29
+ */
30
+ defaults?: Partial<DataTableState>;
31
+ }
32
+ /**
33
+ * Serialize the URL-worthy part of the state. A key whose value equals the
34
+ * default is omitted; a key the user cleared while a default exists is
35
+ * written empty; a key absent from a partial `state` is omitted.
36
+ */
37
+ export declare function serializeDataTableState(state: Partial<DataTableState>, options?: DataTableUrlOptions): URLSearchParams;
38
+ /** Parse a URL into a full state; `defaults` fill what the URL doesn't say. */
39
+ export declare function parseDataTableState(params: URLSearchParams, options?: DataTableUrlOptions): DataTableState;
40
+ /** Replace this table's keys in `params`, leaving every other key untouched. */
41
+ export declare function mergeDataTableUrlState(params: URLSearchParams, state: Partial<DataTableState>, options?: DataTableUrlOptions): URLSearchParams;
@@ -0,0 +1,148 @@
1
+ export const DEFAULT_PAGE_SIZE = 20;
2
+ export const EMPTY_DATA_TABLE_STATE = {
3
+ globalFilter: "",
4
+ columnFilters: [],
5
+ sorting: [],
6
+ columnVisibility: {},
7
+ rowSelection: {},
8
+ pagination: { pageIndex: 0, pageSize: DEFAULT_PAGE_SIZE },
9
+ expanded: true,
10
+ };
11
+ const FILTER_PREFIX = "f_";
12
+ const OWN_KEYS = new Set(["q", "sort", "hide", "page", "size"]);
13
+ export function isDataTableUrlKey(key) {
14
+ return OWN_KEYS.has(key) || key.startsWith(FILTER_PREFIX);
15
+ }
16
+ function encodePart(value) {
17
+ return value.replace(/%/g, "%25").replace(/,/g, "%2C");
18
+ }
19
+ function decodePart(value) {
20
+ return value.replace(/%2C/gi, ",").replace(/%2D/gi, "-").replace(/%25/g, "%");
21
+ }
22
+ /** Sort ids may contain commas or start with "-"; both are escaped. */
23
+ function encodeSortId(id) {
24
+ const encoded = encodePart(id);
25
+ return encoded.startsWith("-") ? `%2D${encoded.slice(1)}` : encoded;
26
+ }
27
+ function filterValues(value) {
28
+ if (Array.isArray(value))
29
+ return value.map(String).filter(Boolean);
30
+ return value == null || value === "" ? [] : [String(value)];
31
+ }
32
+ function sortKey(sorting) {
33
+ return (sorting ?? [])
34
+ .map((s) => (s.desc ? `-${encodeSortId(s.id)}` : encodeSortId(s.id)))
35
+ .join(",");
36
+ }
37
+ function hiddenKey(visibility) {
38
+ return Object.entries(visibility ?? {})
39
+ .filter(([, visible]) => visible === false)
40
+ .map(([id]) => encodePart(id))
41
+ .sort()
42
+ .join(",");
43
+ }
44
+ /**
45
+ * Serialize the URL-worthy part of the state. A key whose value equals the
46
+ * default is omitted; a key the user cleared while a default exists is
47
+ * written empty; a key absent from a partial `state` is omitted.
48
+ */
49
+ export function serializeDataTableState(state, options) {
50
+ const defaults = options?.defaults ?? {};
51
+ const params = new URLSearchParams();
52
+ if (state.globalFilter !== undefined && state.globalFilter !== (defaults.globalFilter ?? "")) {
53
+ params.set("q", state.globalFilter);
54
+ }
55
+ if (state.sorting !== undefined && sortKey(state.sorting) !== sortKey(defaults.sorting)) {
56
+ params.set("sort", sortKey(state.sorting));
57
+ }
58
+ if (state.columnFilters !== undefined) {
59
+ const current = new Map(state.columnFilters.map((f) => [f.id, filterValues(f.value).map(encodePart).join(",")]));
60
+ const base = new Map((defaults.columnFilters ?? []).map((f) => [
61
+ f.id,
62
+ filterValues(f.value).map(encodePart).join(","),
63
+ ]));
64
+ for (const id of new Set([...current.keys(), ...base.keys()])) {
65
+ const value = current.get(id) ?? "";
66
+ if (value !== (base.get(id) ?? ""))
67
+ params.set(`${FILTER_PREFIX}${id}`, value);
68
+ }
69
+ }
70
+ if (state.columnVisibility !== undefined &&
71
+ hiddenKey(state.columnVisibility) !== hiddenKey(defaults.columnVisibility)) {
72
+ params.set("hide", hiddenKey(state.columnVisibility));
73
+ }
74
+ const pagination = state.pagination;
75
+ if (pagination) {
76
+ const defaultIndex = defaults.pagination?.pageIndex ?? 0;
77
+ if (pagination.pageIndex !== defaultIndex)
78
+ params.set("page", String(pagination.pageIndex + 1));
79
+ const defaultSize = options?.defaultPageSize ?? defaults.pagination?.pageSize ?? DEFAULT_PAGE_SIZE;
80
+ if (pagination.pageSize !== defaultSize)
81
+ params.set("size", String(pagination.pageSize));
82
+ }
83
+ return params;
84
+ }
85
+ /** Parse a URL into a full state; `defaults` fill what the URL doesn't say. */
86
+ export function parseDataTableState(params, options) {
87
+ const defaults = { ...EMPTY_DATA_TABLE_STATE, ...options?.defaults };
88
+ const defaultSize = options?.defaultPageSize ?? defaults.pagination.pageSize;
89
+ const sortRaw = params.get("sort");
90
+ const sorting = sortRaw == null
91
+ ? defaults.sorting
92
+ : sortRaw
93
+ .split(",")
94
+ .filter(Boolean)
95
+ .map((part) => part.startsWith("-")
96
+ ? { id: decodePart(part.slice(1)), desc: true }
97
+ : { id: decodePart(part), desc: false });
98
+ const filtersById = new Map(defaults.columnFilters.map((filter) => [filter.id, filter]));
99
+ for (const [key, raw] of params.entries()) {
100
+ if (!key.startsWith(FILTER_PREFIX))
101
+ continue;
102
+ const id = key.slice(FILTER_PREFIX.length);
103
+ const values = raw.split(",").filter(Boolean).map(decodePart);
104
+ if (values.length)
105
+ filtersById.set(id, { id, value: values });
106
+ else
107
+ filtersById.delete(id);
108
+ }
109
+ const columnFilters = Array.from(filtersById.values());
110
+ const columnVisibility = { ...defaults.columnVisibility };
111
+ const hideRaw = params.get("hide");
112
+ if (hideRaw != null) {
113
+ for (const id of Object.keys(columnVisibility)) {
114
+ if (columnVisibility[id] === false)
115
+ delete columnVisibility[id];
116
+ }
117
+ for (const id of hideRaw.split(",").filter(Boolean).map(decodePart))
118
+ columnVisibility[id] = false;
119
+ }
120
+ const pageRaw = params.get("page");
121
+ const page = Number.parseInt(pageRaw ?? "", 10);
122
+ const size = Number.parseInt(params.get("size") ?? "", 10);
123
+ return {
124
+ ...defaults,
125
+ globalFilter: params.has("q") ? (params.get("q") ?? "") : defaults.globalFilter,
126
+ sorting,
127
+ columnFilters,
128
+ columnVisibility,
129
+ pagination: {
130
+ pageIndex: pageRaw == null
131
+ ? defaults.pagination.pageIndex
132
+ : Number.isFinite(page) && page > 1
133
+ ? page - 1
134
+ : 0,
135
+ pageSize: Number.isFinite(size) && size > 0 ? size : defaultSize,
136
+ },
137
+ };
138
+ }
139
+ /** Replace this table's keys in `params`, leaving every other key untouched. */
140
+ export function mergeDataTableUrlState(params, state, options) {
141
+ const next = new URLSearchParams();
142
+ for (const [key, value] of params.entries())
143
+ if (!isDataTableUrlKey(key))
144
+ next.append(key, value);
145
+ for (const [key, value] of serializeDataTableState(state, options).entries())
146
+ next.set(key, value);
147
+ return next;
148
+ }
@@ -0,0 +1,22 @@
1
+ import { type ColumnDef, type FilterFn, type Row, type Table } from "@tanstack/react-table";
2
+ import { type DataTableState } from "./state.js";
3
+ export interface UseDataTableOptions<T> {
4
+ data: T[];
5
+ columns: ColumnDef<T, any>[];
6
+ /** Stable row id (defaults to the row index). Required for selection that survives re-sorting. */
7
+ getRowId?: (row: T, index: number, parent?: Row<T>) => string;
8
+ /** Controlled state — pair with `onStateChange` (e.g. URL-bound). */
9
+ state?: Partial<DataTableState>;
10
+ onStateChange?: (next: DataTableState) => void;
11
+ /** Uncontrolled start state. */
12
+ initialState?: Partial<DataTableState>;
13
+ /** Rows per page; omit (or `false`) for an unpaginated table. Ignored while grouped. */
14
+ pageSize?: number | false;
15
+ /** Column id to group rows by (one level). */
16
+ groupBy?: string | null;
17
+ enableRowSelection?: boolean | ((row: Row<T>) => boolean);
18
+ enableMultiRowSelection?: boolean;
19
+ /** Replace the default AND-of-tokens search. */
20
+ globalFilterFn?: FilterFn<T>;
21
+ }
22
+ export declare function useDataTable<T>(options: UseDataTableOptions<T>): Table<T>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * One hook that turns rows + columns into a fully wired TanStack table:
3
+ * token search, faceted filters, sorting, column visibility, selection,
4
+ * optional pagination and optional single-level grouping — with the whole
5
+ * state as ONE value so it can live in component state or in the URL.
6
+ */
7
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8
+ import { getCoreRowModel, getExpandedRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getGroupedRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table";
9
+ import { facetFilterFn, tokenSearchFilterFn } from "./facets.js";
10
+ import { DEFAULT_PAGE_SIZE, EMPTY_DATA_TABLE_STATE } from "./state.js";
11
+ const PAGE_RESET_KEYS = new Set(["globalFilter", "columnFilters", "sorting"]);
12
+ export function useDataTable(options) {
13
+ const { data, columns, getRowId, state: controlledState, onStateChange, initialState, pageSize, groupBy, enableRowSelection = false, enableMultiRowSelection = true, globalFilterFn, } = options;
14
+ const controlled = controlledState !== undefined && onStateChange !== undefined;
15
+ const basePageSize = pageSize || DEFAULT_PAGE_SIZE;
16
+ const [innerState, setInnerState] = useState(() => ({
17
+ ...EMPTY_DATA_TABLE_STATE,
18
+ ...initialState,
19
+ pagination: { pageIndex: 0, pageSize: basePageSize, ...initialState?.pagination },
20
+ }));
21
+ const state = useMemo(() => controlled
22
+ ? {
23
+ ...EMPTY_DATA_TABLE_STATE,
24
+ ...controlledState,
25
+ pagination: { pageIndex: 0, pageSize: basePageSize, ...controlledState.pagination },
26
+ }
27
+ : innerState, [basePageSize, controlled, controlledState, innerState]);
28
+ const stateRef = useRef(state);
29
+ stateRef.current = state;
30
+ const tableRef = useRef(null);
31
+ const update = useCallback((key, updater) => {
32
+ const current = stateRef.current;
33
+ const value = typeof updater === "function"
34
+ ? updater(current[key])
35
+ : updater;
36
+ if (Object.is(value, current[key]))
37
+ return;
38
+ const next = { ...current, [key]: value };
39
+ if (PAGE_RESET_KEYS.has(key) && next.pagination.pageIndex !== 0) {
40
+ next.pagination = { ...next.pagination, pageIndex: 0 };
41
+ }
42
+ stateRef.current = next;
43
+ // Apply to the instance now, not on the next render: a caller that
44
+ // reads row models right after a mutation (agent tools, tests) sees
45
+ // the new state even while the host owns it (URL, store).
46
+ tableRef.current?.setOptions((prev) => ({
47
+ ...prev,
48
+ state: { ...prev.state, [key]: value, pagination: next.pagination },
49
+ }));
50
+ if (controlled)
51
+ onStateChange(next);
52
+ else
53
+ setInnerState(next);
54
+ }, [controlled, onStateChange]);
55
+ const grouped = Boolean(groupBy);
56
+ const tableOptions = {
57
+ data,
58
+ columns,
59
+ getRowId,
60
+ state: { ...state, grouping: groupBy ? [groupBy] : [] },
61
+ onGlobalFilterChange: (updater) => update("globalFilter", updater),
62
+ onColumnFiltersChange: (updater) => update("columnFilters", updater),
63
+ onSortingChange: (updater) => update("sorting", updater),
64
+ onColumnVisibilityChange: (updater) => update("columnVisibility", updater),
65
+ onRowSelectionChange: (updater) => update("rowSelection", updater),
66
+ onPaginationChange: (updater) => update("pagination", updater),
67
+ onExpandedChange: (updater) => update("expanded", updater),
68
+ // String alias for untyped column defs (JSX renderers): `filterFn: "facet"`.
69
+ filterFns: { facet: facetFilterFn },
70
+ globalFilterFn: globalFilterFn ?? tokenSearchFilterFn,
71
+ getColumnCanGlobalFilter: (column) => column.columnDef.enableGlobalFilter ?? column.accessorFn != null,
72
+ enableRowSelection,
73
+ enableMultiRowSelection,
74
+ groupedColumnMode: false,
75
+ autoResetPageIndex: false,
76
+ autoResetExpanded: false,
77
+ getCoreRowModel: getCoreRowModel(),
78
+ getFilteredRowModel: getFilteredRowModel(),
79
+ getSortedRowModel: getSortedRowModel(),
80
+ getFacetedRowModel: getFacetedRowModel(),
81
+ getFacetedUniqueValues: getFacetedUniqueValues(),
82
+ };
83
+ if (grouped) {
84
+ tableOptions.getGroupedRowModel = getGroupedRowModel();
85
+ tableOptions.getExpandedRowModel = getExpandedRowModel();
86
+ }
87
+ else if (pageSize) {
88
+ tableOptions.getPaginationRowModel = getPaginationRowModel();
89
+ }
90
+ const table = useReactTable(tableOptions);
91
+ tableRef.current = table;
92
+ // A shrinking data set (or a tighter filter) can strand a later page.
93
+ useEffect(() => {
94
+ if (grouped || !pageSize)
95
+ return;
96
+ const index = table.getState().pagination.pageIndex;
97
+ const count = table.getPageCount();
98
+ if (index > 0 && index >= count)
99
+ table.setPageIndex(Math.max(0, count - 1));
100
+ });
101
+ return table;
102
+ }
package/dist/index.d.ts CHANGED
@@ -33,6 +33,18 @@ export { CollectionEmptyState } from "./components/collection-empty-state.js";
33
33
  export { CollectionSkeleton } from "./components/collection-skeleton.js";
34
34
  export { InfiniteScrollSentinel } from "./components/infinite-scroll-sentinel.js";
35
35
  export { CollectionSurface } from "./components/collection-surface.js";
36
+ export { DataTable, type DataTableProps } from "./components/data-table/data-table.js";
37
+ export { DataTableToolbar, type DataTableFacet, type DataTableToolbarProps, } from "./components/data-table/data-table-toolbar.js";
38
+ export { DataTableFacetedFilter, type DataTableFacetedFilterProps, } from "./components/data-table/data-table-faceted-filter.js";
39
+ export { DataTableViewOptions, type DataTableViewOptionsProps, } from "./components/data-table/data-table-view-options.js";
40
+ export { DataTableColumnHeader, type DataTableColumnHeaderProps, } from "./components/data-table/data-table-column-header.js";
41
+ export { DataTablePagination, type DataTablePaginationProps, } from "./components/data-table/data-table-pagination.js";
42
+ export { DataTableRowActions, type DataTableRowAction, type DataTableRowActionsProps, } from "./components/data-table/data-table-row-actions.js";
43
+ export { selectionColumn, SELECTION_COLUMN_ID } from "./components/data-table/selection-column.js";
44
+ export { useDataTable, type UseDataTableOptions } from "./components/data-table/use-data-table.js";
45
+ export { DEFAULT_PAGE_SIZE, EMPTY_DATA_TABLE_STATE, isDataTableUrlKey, mergeDataTableUrlState, parseDataTableState, serializeDataTableState, type DataTableState, type DataTableUrlOptions, } from "./components/data-table/state.js";
46
+ export { FACET_EMPTY, cellText, columnLabel, facetCounts, facetFilterFn, facetKeys, facetOptions, normalizeFacetValue, rowSearchText, tokenSearchFilterFn, type DataTableFacetCount, type DataTableFacetOption, } from "./components/data-table/facets.js";
47
+ export { createColumnHelper, flexRender, type Column, type ColumnDef, type ColumnFiltersState, type Row, type RowSelectionState, type SortingState, type Table, type VisibilityState, } from "@tanstack/react-table";
36
48
  export { ThemeRuntimeProvider, ThemeScope, ThemeDocumentMetadata, useThemeRuntime, } from "./components/theme-runtime-provider.js";
37
49
  export { JsonViewer } from "./components/json-viewer.js";
38
50
  export { UserMenu } from "./components/user-menu.js";
@@ -42,7 +54,7 @@ export { NameDialog } from "./components/name-dialog.js";
42
54
  export { SurfaceCard, surfaceCardVariants } from "./components/surface-card.js";
43
55
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip.js";
44
56
  export { Collapsible, CollapsibleTrigger, CollapsibleContent } from "./ui/collapsible.js";
45
- export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, } from "./ui/dropdown-menu.js";
57
+ export { DropdownMenu, DropdownMenuTrigger, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, } from "./ui/dropdown-menu.js";
46
58
  export { Dialog, DialogPortal, DialogOverlay, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, } from "./ui/dialog.js";
47
59
  export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent } from "./ui/popover.js";
48
60
  export { Select, selectClassName } from "./ui/select.js";
@@ -66,7 +78,7 @@ export { usePageTools } from "./lib/use-page-tools.js";
66
78
  export { useNewConversation } from "./lib/use-new-conversation.js";
67
79
  export { AGUIAdapterSDK } from "./lib/ag-ui-adapter.js";
68
80
  export { FileAttachmentAdapter, resolveMimeType } from "./lib/attachment-adapter.js";
69
- export { registerAttachmentAdapter, useAttachmentAdapterStore, } from "./lib/attachment-registry.js";
81
+ export { registerAttachmentAdapter, useAttachmentAdapterStore } from "./lib/attachment-registry.js";
70
82
  export { ReasoningPart, ReasoningMessagePartComponent } from "./components/reasoning-part.js";
71
83
  export { ReasoningEffortPicker } from "./components/reasoning-effort-picker.js";
72
84
  export { AuthProvider } from "./lib/auth-provider.js";
package/dist/index.js CHANGED
@@ -31,6 +31,18 @@ export { CollectionEmptyState } from "./components/collection-empty-state.js";
31
31
  export { CollectionSkeleton } from "./components/collection-skeleton.js";
32
32
  export { InfiniteScrollSentinel } from "./components/infinite-scroll-sentinel.js";
33
33
  export { CollectionSurface } from "./components/collection-surface.js";
34
+ export { DataTable } from "./components/data-table/data-table.js";
35
+ export { DataTableToolbar, } from "./components/data-table/data-table-toolbar.js";
36
+ export { DataTableFacetedFilter, } from "./components/data-table/data-table-faceted-filter.js";
37
+ export { DataTableViewOptions, } from "./components/data-table/data-table-view-options.js";
38
+ export { DataTableColumnHeader, } from "./components/data-table/data-table-column-header.js";
39
+ export { DataTablePagination, } from "./components/data-table/data-table-pagination.js";
40
+ export { DataTableRowActions, } from "./components/data-table/data-table-row-actions.js";
41
+ export { selectionColumn, SELECTION_COLUMN_ID } from "./components/data-table/selection-column.js";
42
+ export { useDataTable } from "./components/data-table/use-data-table.js";
43
+ export { DEFAULT_PAGE_SIZE, EMPTY_DATA_TABLE_STATE, isDataTableUrlKey, mergeDataTableUrlState, parseDataTableState, serializeDataTableState, } from "./components/data-table/state.js";
44
+ export { FACET_EMPTY, cellText, columnLabel, facetCounts, facetFilterFn, facetKeys, facetOptions, normalizeFacetValue, rowSearchText, tokenSearchFilterFn, } from "./components/data-table/facets.js";
45
+ export { createColumnHelper, flexRender, } from "@tanstack/react-table";
34
46
  export { ThemeRuntimeProvider, ThemeScope, ThemeDocumentMetadata, useThemeRuntime, } from "./components/theme-runtime-provider.js";
35
47
  export { JsonViewer } from "./components/json-viewer.js";
36
48
  export { UserMenu } from "./components/user-menu.js";
@@ -46,7 +58,7 @@ export { SurfaceCard, surfaceCardVariants } from "./components/surface-card.js";
46
58
  // package. Tracked for a follow-up under #95/#96.
47
59
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip.js";
48
60
  export { Collapsible, CollapsibleTrigger, CollapsibleContent } from "./ui/collapsible.js";
49
- export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, } from "./ui/dropdown-menu.js";
61
+ export { DropdownMenu, DropdownMenuTrigger, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, } from "./ui/dropdown-menu.js";
50
62
  export { Dialog, DialogPortal, DialogOverlay, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, } from "./ui/dialog.js";
51
63
  export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent } from "./ui/popover.js";
52
64
  export { Select, selectClassName } from "./ui/select.js";
@@ -74,7 +86,7 @@ export { useNewConversation } from "./lib/use-new-conversation.js";
74
86
  // --- Adapters ---
75
87
  export { AGUIAdapterSDK } from "./lib/ag-ui-adapter.js";
76
88
  export { FileAttachmentAdapter, resolveMimeType } from "./lib/attachment-adapter.js";
77
- export { registerAttachmentAdapter, useAttachmentAdapterStore, } from "./lib/attachment-registry.js";
89
+ export { registerAttachmentAdapter, useAttachmentAdapterStore } from "./lib/attachment-registry.js";
78
90
  export { ReasoningPart, ReasoningMessagePartComponent } from "./components/reasoning-part.js";
79
91
  export { ReasoningEffortPicker } from "./components/reasoning-effort-picker.js";
80
92
  // --- Auth ---
@@ -5,3 +5,4 @@ export declare const DropdownMenuContent: import("react").ForwardRefExoticCompon
5
5
  export declare const DropdownMenuItem: import("react").ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuItemProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
6
6
  export declare const DropdownMenuSeparator: import("react").ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuSeparatorProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
7
7
  export declare const DropdownMenuLabel: import("react").ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuLabelProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
8
+ export declare const DropdownMenuCheckboxItem: import("react").ForwardRefExoticComponent<Omit<DropdownMenuPrimitive.DropdownMenuCheckboxItemProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
@@ -1,6 +1,7 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
3
3
  import { forwardRef } from "react";
4
+ import { Check } from "lucide-react";
4
5
  import { cn } from "@iloveagents/foundry-web-primitives";
5
6
  export const DropdownMenu = DropdownMenuPrimitive.Root;
6
7
  export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
@@ -17,3 +18,5 @@ export const DropdownMenuSeparator = forwardRef(({ className, ...props }, ref) =
17
18
  DropdownMenuSeparator.displayName = "DropdownMenuSeparator";
18
19
  export const DropdownMenuLabel = forwardRef(({ className, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Label, { ref: ref, className: cn("px-2 py-1.5 text-sm font-semibold", className), ...props })));
19
20
  DropdownMenuLabel.displayName = "DropdownMenuLabel";
21
+ export const DropdownMenuCheckboxItem = forwardRef(({ className, children, checked, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.CheckboxItem, { ref: ref, checked: checked, className: cn("relative flex cursor-default select-none items-center gap-2 rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none", "transition-colors focus:bg-accent focus:text-accent-foreground", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex size-4 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Check, { className: "size-4" }) }) }), children] })));
22
+ DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-web-ui",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "license": "MIT",
5
5
  "description": "React agent UI core for Foundry UI — chat, composer, AG-UI adapter for assistant-ui, tool cards, panels, sidebar, theme runtime, and UI stores.",
6
6
  "keywords": [
@@ -65,13 +65,14 @@
65
65
  "@radix-ui/react-popover": "^1.1.15",
66
66
  "@radix-ui/react-slot": "^1.2.0",
67
67
  "@radix-ui/react-tooltip": "^1.2.0",
68
+ "@tanstack/react-table": "^8.21.3",
68
69
  "class-variance-authority": "^0.7.0",
69
70
  "clsx": "^2.1.0",
70
- "tailwind-merge": "^3.5.0",
71
71
  "react-markdown": "^10.0.0",
72
72
  "remark-gfm": "^4.0.0",
73
- "@iloveagents/foundry-agent": "^0.16.0",
74
- "@iloveagents/foundry-web-primitives": "^0.16.0"
73
+ "tailwind-merge": "^3.5.0",
74
+ "@iloveagents/foundry-agent": "^0.17.0",
75
+ "@iloveagents/foundry-web-primitives": "^0.17.0"
75
76
  },
76
77
  "devDependencies": {
77
78
  "@ag-ui/client": "^0.0.52",