@iloveagents/foundry-web-ui 0.17.0 → 0.18.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.
- package/dist/components/data-table/data-table-faceted-filter.d.ts +3 -1
- package/dist/components/data-table/data-table-faceted-filter.js +10 -4
- package/dist/components/data-table/data-table-frame.d.ts +20 -0
- package/dist/components/data-table/data-table-frame.js +36 -0
- package/dist/components/data-table/data-table-toolbar.d.ts +20 -4
- package/dist/components/data-table/data-table-toolbar.js +46 -12
- package/dist/components/data-table/data-table.js +11 -2
- package/dist/components/data-table/facets.d.ts +4 -0
- package/dist/components/data-table/state.d.ts +4 -1
- package/dist/components/data-table/state.js +30 -19
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/package.json +3 -3
|
@@ -11,6 +11,8 @@ export interface DataTableFacetedFilterProps<T> {
|
|
|
11
11
|
options?: DataTableFacetOption[];
|
|
12
12
|
/** Show a search box inside the list once there are more options than this (default 7). */
|
|
13
13
|
searchableFrom?: number;
|
|
14
|
+
/** Called when the popover closes with nothing selected (a revealed facet goes back into the menu). */
|
|
15
|
+
onEmptyClose?: () => void;
|
|
14
16
|
className?: string;
|
|
15
17
|
}
|
|
16
|
-
export declare function DataTableFacetedFilter<T>({ column, title, options: declared, searchableFrom, className, }: DataTableFacetedFilterProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export declare function DataTableFacetedFilter<T>({ column, title, options: declared, searchableFrom, onEmptyClose, className, }: DataTableFacetedFilterProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -4,7 +4,7 @@ import { useMemo, useState } from "react";
|
|
|
4
4
|
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
5
5
|
import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover.js";
|
|
6
6
|
import { columnLabel, facetOptions, normalizeFacetValue, } from "./facets.js";
|
|
7
|
-
export function DataTableFacetedFilter({ column, title, options: declared, searchableFrom = 7, className, }) {
|
|
7
|
+
export function DataTableFacetedFilter({ column, title, options: declared, searchableFrom = 7, onEmptyClose, className, }) {
|
|
8
8
|
const [query, setQuery] = useState("");
|
|
9
9
|
const label = title ?? columnLabel(column);
|
|
10
10
|
const selected = normalizeFacetValue(column.getFilterValue());
|
|
@@ -20,9 +20,15 @@ export function DataTableFacetedFilter({ column, title, options: declared, searc
|
|
|
20
20
|
: [...selected, value];
|
|
21
21
|
column.setFilterValue(next.length ? next : undefined);
|
|
22
22
|
};
|
|
23
|
-
return (_jsxs(Popover, { onOpenChange: (open) =>
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
return (_jsxs(Popover, { onOpenChange: (open) => {
|
|
24
|
+
if (open)
|
|
25
|
+
return;
|
|
26
|
+
setQuery("");
|
|
27
|
+
if (normalizeFacetValue(column.getFilterValue()).length === 0)
|
|
28
|
+
onEmptyClose?.();
|
|
29
|
+
}, 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" }), selected.length > 2 ? (_jsxs("span", { className: "rounded-md bg-muted px-1.5 py-0.5 text-xs font-normal tabular-nums", children: [_jsx("span", { "aria-hidden": "true", children: selected.length }), _jsxs("span", { className: "sr-only", children: [selected.length, " selected"] })] })) : (options
|
|
30
|
+
.filter((option) => selectedSet.has(option.value))
|
|
31
|
+
.map((option) => (_jsx("span", { title: option.label, className: "max-w-[8rem] truncate 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
32
|
const checked = selectedSet.has(option.value);
|
|
27
33
|
const Icon = option.icon;
|
|
28
34
|
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
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame around a list: normally a plain container, in focus mode a
|
|
3
|
+
* fullscreen layer (Escape leaves it). `DataTableToolbar` shows the focus
|
|
4
|
+
* button whenever it renders inside a frame — users with wide lists get a
|
|
5
|
+
* way out of a cramped page without the host doing anything.
|
|
6
|
+
*/
|
|
7
|
+
import { type HTMLAttributes, type ReactNode } from "react";
|
|
8
|
+
export interface DataTableFrameContextValue {
|
|
9
|
+
focused: boolean;
|
|
10
|
+
setFocused: (focused: boolean) => void;
|
|
11
|
+
}
|
|
12
|
+
/** The enclosing frame's focus state, or `null` outside a frame. */
|
|
13
|
+
export declare function useDataTableFrame(): DataTableFrameContextValue | null;
|
|
14
|
+
export interface DataTableFrameProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
/** Classes applied in focus mode on top of the fullscreen layer. */
|
|
17
|
+
focusClassName?: string;
|
|
18
|
+
defaultFocused?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function DataTableFrame({ children, className, focusClassName, defaultFocused, ...rest }: DataTableFrameProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The frame around a list: normally a plain container, in focus mode a
|
|
4
|
+
* fullscreen layer (Escape leaves it). `DataTableToolbar` shows the focus
|
|
5
|
+
* button whenever it renders inside a frame — users with wide lists get a
|
|
6
|
+
* way out of a cramped page without the host doing anything.
|
|
7
|
+
*/
|
|
8
|
+
import { createContext, useContext, useEffect, useMemo, useState, } from "react";
|
|
9
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
10
|
+
const DataTableFrameContext = createContext(null);
|
|
11
|
+
/** The enclosing frame's focus state, or `null` outside a frame. */
|
|
12
|
+
export function useDataTableFrame() {
|
|
13
|
+
return useContext(DataTableFrameContext);
|
|
14
|
+
}
|
|
15
|
+
export function DataTableFrame({ children, className, focusClassName, defaultFocused = false, ...rest }) {
|
|
16
|
+
const [focused, setFocused] = useState(defaultFocused);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!focused)
|
|
19
|
+
return;
|
|
20
|
+
const onKeyDown = (event) => {
|
|
21
|
+
if (event.key === "Escape")
|
|
22
|
+
setFocused(false);
|
|
23
|
+
};
|
|
24
|
+
document.addEventListener("keydown", onKeyDown);
|
|
25
|
+
const previousOverflow = document.body.style.overflow;
|
|
26
|
+
document.body.style.overflow = "hidden";
|
|
27
|
+
return () => {
|
|
28
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
29
|
+
document.body.style.overflow = previousOverflow;
|
|
30
|
+
};
|
|
31
|
+
}, [focused]);
|
|
32
|
+
const value = useMemo(() => ({ focused, setFocused }), [focused]);
|
|
33
|
+
return (_jsx(DataTableFrameContext.Provider, { value: value, children: _jsx("div", { ...rest, "data-focused": focused ? "" : undefined, className: focused
|
|
34
|
+
? cn("fixed inset-0 z-50 flex flex-col gap-3 overflow-auto bg-background p-4 sm:p-6", focusClassName)
|
|
35
|
+
: className, children: children }) }));
|
|
36
|
+
}
|
|
@@ -1,27 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Search + facets + Reset on the left, actions + View menu on the right.
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Many facets stay calm: only the *pinned* ones (explicit `pinned`, else the
|
|
5
|
+
* first `pinnedFacets`) are always visible; the rest wait behind one
|
|
6
|
+
* "+ Filter" menu and appear as a facet button once chosen or active. So
|
|
7
|
+
* row one holds search, a few facets, "+ Filter" and the actions, and a
|
|
8
|
+
* filter the user added reads as a chip in the same row.
|
|
4
9
|
*/
|
|
5
10
|
import type { Table } from "@tanstack/react-table";
|
|
6
|
-
import type
|
|
7
|
-
import type
|
|
11
|
+
import { type ReactNode } from "react";
|
|
12
|
+
import { type DataTableFacetOption } from "./facets.js";
|
|
8
13
|
export interface DataTableFacet {
|
|
9
14
|
columnId: string;
|
|
10
15
|
title?: string;
|
|
11
16
|
options?: DataTableFacetOption[];
|
|
17
|
+
/** Always show this facet; unset → the first `pinnedFacets` are pinned. */
|
|
18
|
+
pinned?: boolean;
|
|
12
19
|
}
|
|
13
20
|
export interface DataTableToolbarProps<T> {
|
|
14
21
|
table: Table<T>;
|
|
15
22
|
facets?: DataTableFacet[];
|
|
23
|
+
/** Facets always visible when none declares `pinned` (default 3; `Infinity` shows all). */
|
|
24
|
+
pinnedFacets?: number;
|
|
16
25
|
searchPlaceholder?: string;
|
|
17
26
|
/** Hide the search box. */
|
|
18
27
|
search?: boolean;
|
|
19
28
|
/** Hide the View (column visibility) menu. */
|
|
20
29
|
viewOptions?: boolean;
|
|
30
|
+
/** Hide the focus (fullscreen) button that appears inside a `DataTableFrame`. */
|
|
31
|
+
focus?: boolean;
|
|
21
32
|
/** Extra controls rendered next to the facets (left side). */
|
|
22
33
|
children?: ReactNode;
|
|
23
34
|
/** Controls rendered on the right, before the View menu. */
|
|
24
35
|
actions?: ReactNode;
|
|
25
36
|
className?: string;
|
|
26
37
|
}
|
|
27
|
-
|
|
38
|
+
/** Which facets render as buttons: pinned ones, revealed ones, and any with a value. */
|
|
39
|
+
export declare function visibleFacets<T>(table: Table<T>, facets: DataTableFacet[], revealed: ReadonlySet<string>, pinnedFacets: number): {
|
|
40
|
+
shown: DataTableFacet[];
|
|
41
|
+
hidden: DataTableFacet[];
|
|
42
|
+
};
|
|
43
|
+
export declare function DataTableToolbar<T>({ table, facets, pinnedFacets, searchPlaceholder, search, viewOptions, focus, children, actions, className, }: DataTableToolbarProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,18 +1,52 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Search, X } from "lucide-react";
|
|
2
|
+
import { Maximize2, Minimize2, Plus, Search, X } from "lucide-react";
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { Button, Input, cn } from "@iloveagents/foundry-web-primitives";
|
|
5
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu.js";
|
|
4
6
|
import { DataTableFacetedFilter } from "./data-table-faceted-filter.js";
|
|
7
|
+
import { useDataTableFrame } from "./data-table-frame.js";
|
|
5
8
|
import { DataTableViewOptions } from "./data-table-view-options.js";
|
|
6
|
-
|
|
9
|
+
import { columnLabel, normalizeFacetValue } from "./facets.js";
|
|
10
|
+
/** Which facets render as buttons: pinned ones, revealed ones, and any with a value. */
|
|
11
|
+
export function visibleFacets(table, facets, revealed, pinnedFacets) {
|
|
12
|
+
const explicit = facets.some((facet) => facet.pinned !== undefined);
|
|
13
|
+
const shown = [];
|
|
14
|
+
const hidden = [];
|
|
15
|
+
facets.forEach((facet, index) => {
|
|
16
|
+
const column = table.getColumn(facet.columnId);
|
|
17
|
+
if (!column)
|
|
18
|
+
return;
|
|
19
|
+
const pinned = explicit ? facet.pinned === true : index < pinnedFacets;
|
|
20
|
+
const active = normalizeFacetValue(column.getFilterValue()).length > 0;
|
|
21
|
+
(pinned || active || revealed.has(facet.columnId) ? shown : hidden).push(facet);
|
|
22
|
+
});
|
|
23
|
+
return { shown, hidden };
|
|
24
|
+
}
|
|
25
|
+
export function DataTableToolbar({ table, facets = [], pinnedFacets = 3, searchPlaceholder = "Search…", search = true, viewOptions = true, focus = true, children, actions, className, }) {
|
|
26
|
+
const [revealed, setRevealed] = useState(() => new Set());
|
|
27
|
+
const frame = useDataTableFrame();
|
|
28
|
+
const focusButton = focus && frame ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-pressed": frame.focused, "aria-label": frame.focused ? "Leave focus mode" : "Focus on the list", title: frame.focused ? "Leave focus mode (Esc)" : "Focus on the list", onClick: () => frame.setFocused(!frame.focused), className: "size-8 rounded-xl border-border/45 bg-background/65 p-0 shadow-none hover:bg-muted/26", children: frame.focused ? _jsx(Minimize2, { className: "size-4" }) : _jsx(Maximize2, { className: "size-4" }) })) : null;
|
|
7
29
|
const state = table.getState();
|
|
8
|
-
const isFiltered = state.columnFilters.length > 0 || Boolean(state.globalFilter);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
const isFiltered = state.columnFilters.length > 0 || Boolean(state.globalFilter) || revealed.size > 0;
|
|
31
|
+
const { shown, hidden } = visibleFacets(table, facets, revealed, pinnedFacets);
|
|
32
|
+
const facetTitle = (facet) => {
|
|
33
|
+
const column = table.getColumn(facet.columnId);
|
|
34
|
+
return facet.title ?? (column ? columnLabel(column) : facet.columnId);
|
|
35
|
+
};
|
|
36
|
+
return (_jsxs("div", { className: cn("flex items-start gap-2", className), children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-2", 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, shown.map((facet) => {
|
|
37
|
+
const column = table.getColumn(facet.columnId);
|
|
38
|
+
if (!column)
|
|
39
|
+
return null;
|
|
40
|
+
return (_jsx(DataTableFacetedFilter, { column: column, title: facet.title, options: facet.options, onEmptyClose: revealed.has(facet.columnId)
|
|
41
|
+
? () => setRevealed((current) => {
|
|
42
|
+
const next = new Set(current);
|
|
43
|
+
next.delete(facet.columnId);
|
|
44
|
+
return next;
|
|
45
|
+
})
|
|
46
|
+
: undefined }, facet.columnId));
|
|
47
|
+
}), hidden.length > 0 ? (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", "data-facet-menu": "", className: "h-8 rounded-xl px-2.5 text-muted-foreground hover:text-foreground", children: [_jsx(Plus, { className: "size-4" }), "Filter"] }) }), _jsxs(DropdownMenuContent, { align: "start", className: "w-48 rounded-xl border-border/45 bg-popover/95 shadow-lg", children: [_jsx(DropdownMenuLabel, { children: "Filter by" }), _jsx(DropdownMenuSeparator, {}), hidden.map((facet) => (_jsx(DropdownMenuItem, { "data-facet-menu-item": facet.columnId, onSelect: () => setRevealed((current) => new Set([...current, facet.columnId])), children: facetTitle(facet) }, facet.columnId)))] })] })) : null, children, isFiltered ? (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
48
|
+
table.resetColumnFilters();
|
|
49
|
+
table.setGlobalFilter("");
|
|
50
|
+
setRevealed(new Set());
|
|
51
|
+
}, className: "h-8 rounded-xl px-2.5", children: ["Reset", _jsx(X, { className: "size-4" })] })) : null] }), actions || viewOptions || focusButton ? (_jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: [actions, viewOptions ? _jsx(DataTableViewOptions, { table: table }) : null, focusButton] })) : null] }));
|
|
18
52
|
}
|
|
@@ -9,6 +9,15 @@ import { flexRender } from "@tanstack/react-table";
|
|
|
9
9
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
10
10
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
11
11
|
const INTERACTIVE = "a, button, input, select, textarea, [role='menuitem'], [data-no-row-click]";
|
|
12
|
+
/** Literal classes so Tailwind generates them: hide the column below the breakpoint. */
|
|
13
|
+
const BREAKPOINT_CLASS = {
|
|
14
|
+
sm: "max-sm:hidden",
|
|
15
|
+
md: "max-md:hidden",
|
|
16
|
+
lg: "max-lg:hidden",
|
|
17
|
+
xl: "max-xl:hidden",
|
|
18
|
+
"2xl": "max-2xl:hidden",
|
|
19
|
+
};
|
|
20
|
+
const breakpointClass = (breakpoint) => breakpoint ? BREAKPOINT_CLASS[breakpoint] : undefined;
|
|
12
21
|
function orderedRows(table, groupOrder) {
|
|
13
22
|
const rows = table.getRowModel().rows;
|
|
14
23
|
if (!table.getState().grouping.length)
|
|
@@ -38,7 +47,7 @@ export function DataTable({ table, onRowClick, activeRowId, rowClassName, render
|
|
|
38
47
|
};
|
|
39
48
|
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
49
|
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
|
|
50
|
+
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", breakpointClass(meta?.breakpoint), meta?.headerClassName), children: header.isPlaceholder
|
|
42
51
|
? null
|
|
43
52
|
: flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
|
|
44
53
|
}) }, 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) => {
|
|
@@ -51,7 +60,7 @@ export function DataTable({ table, onRowClick, activeRowId, rowClassName, render
|
|
|
51
60
|
}, className: cn("border-t border-border/35 transition-colors", onRowClick &&
|
|
52
61
|
"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
62
|
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));
|
|
63
|
+
return (_jsx("td", { className: cn(cellPad, "align-middle", meta?.align === "right" && "text-right", meta?.align === "center" && "text-center", breakpointClass(meta?.breakpoint), meta?.className), children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
|
|
55
64
|
}) }, row.id)))) })] }) }) }));
|
|
56
65
|
}
|
|
57
66
|
function DefaultGroupLabel({ row }) {
|
|
@@ -21,12 +21,16 @@ declare module "@tanstack/react-table" {
|
|
|
21
21
|
searchText?: (row: TData) => string;
|
|
22
22
|
/** Labels for facet keys and group headers (`value → label`). */
|
|
23
23
|
facetLabels?: Record<string, string>;
|
|
24
|
+
/** Smallest viewport the column is shown at; below it the column is hidden (the View menu can still force it). */
|
|
25
|
+
breakpoint?: DataTableBreakpoint;
|
|
24
26
|
/** Keep unused type parameter referenced for declaration merging. */
|
|
25
27
|
__cellValue?: TValue;
|
|
26
28
|
/** Hosts may carry their own keys in `meta` (e.g. a server sort key). */
|
|
27
29
|
[key: string]: unknown;
|
|
28
30
|
}
|
|
29
31
|
}
|
|
32
|
+
/** Tailwind breakpoints a column can be gated on. */
|
|
33
|
+
export type DataTableBreakpoint = "sm" | "md" | "lg" | "xl" | "2xl";
|
|
30
34
|
/** Facet key for empty cells (`null`, `undefined`, `""`, `[]`). */
|
|
31
35
|
export declare const FACET_EMPTY = "__none__";
|
|
32
36
|
export interface DataTableFacetOption {
|
|
@@ -19,8 +19,11 @@ export interface DataTableState {
|
|
|
19
19
|
}
|
|
20
20
|
export declare const DEFAULT_PAGE_SIZE = 20;
|
|
21
21
|
export declare const EMPTY_DATA_TABLE_STATE: DataTableState;
|
|
22
|
-
|
|
22
|
+
/** `prefix` namespaces every key (`risks.q`, `risks.f_status`) so two lists can share a page. */
|
|
23
|
+
export declare function isDataTableUrlKey(key: string, prefix?: string): boolean;
|
|
23
24
|
export interface DataTableUrlOptions {
|
|
25
|
+
/** Key namespace for pages with several lists (`"risks."` → `risks.q`, `risks.f_status`). */
|
|
26
|
+
prefix?: string;
|
|
24
27
|
/** Page size that is implied when `size` is absent (default 20). */
|
|
25
28
|
defaultPageSize?: number;
|
|
26
29
|
/**
|
|
@@ -9,9 +9,13 @@ export const EMPTY_DATA_TABLE_STATE = {
|
|
|
9
9
|
expanded: true,
|
|
10
10
|
};
|
|
11
11
|
const FILTER_PREFIX = "f_";
|
|
12
|
-
const OWN_KEYS =
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const OWN_KEYS = ["q", "sort", "hide", "page", "size"];
|
|
13
|
+
/** `prefix` namespaces every key (`risks.q`, `risks.f_status`) so two lists can share a page. */
|
|
14
|
+
export function isDataTableUrlKey(key, prefix = "") {
|
|
15
|
+
if (!key.startsWith(prefix))
|
|
16
|
+
return false;
|
|
17
|
+
const bare = key.slice(prefix.length);
|
|
18
|
+
return OWN_KEYS.includes(bare) || bare.startsWith(FILTER_PREFIX);
|
|
15
19
|
}
|
|
16
20
|
function encodePart(value) {
|
|
17
21
|
return value.replace(/%/g, "%25").replace(/,/g, "%2C");
|
|
@@ -48,12 +52,13 @@ function hiddenKey(visibility) {
|
|
|
48
52
|
*/
|
|
49
53
|
export function serializeDataTableState(state, options) {
|
|
50
54
|
const defaults = options?.defaults ?? {};
|
|
55
|
+
const prefix = options?.prefix ?? "";
|
|
51
56
|
const params = new URLSearchParams();
|
|
52
57
|
if (state.globalFilter !== undefined && state.globalFilter !== (defaults.globalFilter ?? "")) {
|
|
53
|
-
params.set(
|
|
58
|
+
params.set(`${prefix}q`, state.globalFilter);
|
|
54
59
|
}
|
|
55
60
|
if (state.sorting !== undefined && sortKey(state.sorting) !== sortKey(defaults.sorting)) {
|
|
56
|
-
params.set(
|
|
61
|
+
params.set(`${prefix}sort`, sortKey(state.sorting));
|
|
57
62
|
}
|
|
58
63
|
if (state.columnFilters !== undefined) {
|
|
59
64
|
const current = new Map(state.columnFilters.map((f) => [f.id, filterValues(f.value).map(encodePart).join(",")]));
|
|
@@ -64,29 +69,32 @@ export function serializeDataTableState(state, options) {
|
|
|
64
69
|
for (const id of new Set([...current.keys(), ...base.keys()])) {
|
|
65
70
|
const value = current.get(id) ?? "";
|
|
66
71
|
if (value !== (base.get(id) ?? ""))
|
|
67
|
-
params.set(`${FILTER_PREFIX}${id}`, value);
|
|
72
|
+
params.set(`${prefix}${FILTER_PREFIX}${id}`, value);
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
if (state.columnVisibility !== undefined &&
|
|
71
76
|
hiddenKey(state.columnVisibility) !== hiddenKey(defaults.columnVisibility)) {
|
|
72
|
-
params.set(
|
|
77
|
+
params.set(`${prefix}hide`, hiddenKey(state.columnVisibility));
|
|
73
78
|
}
|
|
74
79
|
const pagination = state.pagination;
|
|
75
80
|
if (pagination) {
|
|
76
81
|
const defaultIndex = defaults.pagination?.pageIndex ?? 0;
|
|
77
|
-
if (pagination.pageIndex !== defaultIndex)
|
|
78
|
-
params.set(
|
|
82
|
+
if (pagination.pageIndex !== defaultIndex) {
|
|
83
|
+
params.set(`${prefix}page`, String(pagination.pageIndex + 1));
|
|
84
|
+
}
|
|
79
85
|
const defaultSize = options?.defaultPageSize ?? defaults.pagination?.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
80
86
|
if (pagination.pageSize !== defaultSize)
|
|
81
|
-
params.set(
|
|
87
|
+
params.set(`${prefix}size`, String(pagination.pageSize));
|
|
82
88
|
}
|
|
83
89
|
return params;
|
|
84
90
|
}
|
|
85
91
|
/** Parse a URL into a full state; `defaults` fill what the URL doesn't say. */
|
|
86
92
|
export function parseDataTableState(params, options) {
|
|
87
93
|
const defaults = { ...EMPTY_DATA_TABLE_STATE, ...options?.defaults };
|
|
94
|
+
const prefix = options?.prefix ?? "";
|
|
95
|
+
const filterPrefix = `${prefix}${FILTER_PREFIX}`;
|
|
88
96
|
const defaultSize = options?.defaultPageSize ?? defaults.pagination.pageSize;
|
|
89
|
-
const sortRaw = params.get(
|
|
97
|
+
const sortRaw = params.get(`${prefix}sort`);
|
|
90
98
|
const sorting = sortRaw == null
|
|
91
99
|
? defaults.sorting
|
|
92
100
|
: sortRaw
|
|
@@ -97,9 +105,9 @@ export function parseDataTableState(params, options) {
|
|
|
97
105
|
: { id: decodePart(part), desc: false });
|
|
98
106
|
const filtersById = new Map(defaults.columnFilters.map((filter) => [filter.id, filter]));
|
|
99
107
|
for (const [key, raw] of params.entries()) {
|
|
100
|
-
if (!key.startsWith(
|
|
108
|
+
if (!key.startsWith(filterPrefix))
|
|
101
109
|
continue;
|
|
102
|
-
const id = key.slice(
|
|
110
|
+
const id = key.slice(filterPrefix.length);
|
|
103
111
|
const values = raw.split(",").filter(Boolean).map(decodePart);
|
|
104
112
|
if (values.length)
|
|
105
113
|
filtersById.set(id, { id, value: values });
|
|
@@ -108,7 +116,7 @@ export function parseDataTableState(params, options) {
|
|
|
108
116
|
}
|
|
109
117
|
const columnFilters = Array.from(filtersById.values());
|
|
110
118
|
const columnVisibility = { ...defaults.columnVisibility };
|
|
111
|
-
const hideRaw = params.get(
|
|
119
|
+
const hideRaw = params.get(`${prefix}hide`);
|
|
112
120
|
if (hideRaw != null) {
|
|
113
121
|
for (const id of Object.keys(columnVisibility)) {
|
|
114
122
|
if (columnVisibility[id] === false)
|
|
@@ -117,12 +125,14 @@ export function parseDataTableState(params, options) {
|
|
|
117
125
|
for (const id of hideRaw.split(",").filter(Boolean).map(decodePart))
|
|
118
126
|
columnVisibility[id] = false;
|
|
119
127
|
}
|
|
120
|
-
const pageRaw = params.get(
|
|
128
|
+
const pageRaw = params.get(`${prefix}page`);
|
|
121
129
|
const page = Number.parseInt(pageRaw ?? "", 10);
|
|
122
|
-
const size = Number.parseInt(params.get(
|
|
130
|
+
const size = Number.parseInt(params.get(`${prefix}size`) ?? "", 10);
|
|
123
131
|
return {
|
|
124
132
|
...defaults,
|
|
125
|
-
globalFilter: params.has(
|
|
133
|
+
globalFilter: params.has(`${prefix}q`)
|
|
134
|
+
? (params.get(`${prefix}q`) ?? "")
|
|
135
|
+
: defaults.globalFilter,
|
|
126
136
|
sorting,
|
|
127
137
|
columnFilters,
|
|
128
138
|
columnVisibility,
|
|
@@ -139,9 +149,10 @@ export function parseDataTableState(params, options) {
|
|
|
139
149
|
/** Replace this table's keys in `params`, leaving every other key untouched. */
|
|
140
150
|
export function mergeDataTableUrlState(params, state, options) {
|
|
141
151
|
const next = new URLSearchParams();
|
|
142
|
-
for (const [key, value] of params.entries())
|
|
143
|
-
if (!isDataTableUrlKey(key))
|
|
152
|
+
for (const [key, value] of params.entries()) {
|
|
153
|
+
if (!isDataTableUrlKey(key, options?.prefix))
|
|
144
154
|
next.append(key, value);
|
|
155
|
+
}
|
|
145
156
|
for (const [key, value] of serializeDataTableState(state, options).entries())
|
|
146
157
|
next.set(key, value);
|
|
147
158
|
return next;
|
package/dist/index.d.ts
CHANGED
|
@@ -34,7 +34,8 @@ 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
36
|
export { DataTable, type DataTableProps } from "./components/data-table/data-table.js";
|
|
37
|
-
export {
|
|
37
|
+
export { DataTableFrame, useDataTableFrame, type DataTableFrameProps, type DataTableFrameContextValue, } from "./components/data-table/data-table-frame.js";
|
|
38
|
+
export { DataTableToolbar, visibleFacets, type DataTableFacet, type DataTableToolbarProps, } from "./components/data-table/data-table-toolbar.js";
|
|
38
39
|
export { DataTableFacetedFilter, type DataTableFacetedFilterProps, } from "./components/data-table/data-table-faceted-filter.js";
|
|
39
40
|
export { DataTableViewOptions, type DataTableViewOptionsProps, } from "./components/data-table/data-table-view-options.js";
|
|
40
41
|
export { DataTableColumnHeader, type DataTableColumnHeaderProps, } from "./components/data-table/data-table-column-header.js";
|
|
@@ -43,7 +44,7 @@ export { DataTableRowActions, type DataTableRowAction, type DataTableRowActionsP
|
|
|
43
44
|
export { selectionColumn, SELECTION_COLUMN_ID } from "./components/data-table/selection-column.js";
|
|
44
45
|
export { useDataTable, type UseDataTableOptions } from "./components/data-table/use-data-table.js";
|
|
45
46
|
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 { FACET_EMPTY, cellText, columnLabel, facetCounts, facetFilterFn, facetKeys, facetOptions, normalizeFacetValue, rowSearchText, tokenSearchFilterFn, type DataTableBreakpoint, type DataTableFacetCount, type DataTableFacetOption, } from "./components/data-table/facets.js";
|
|
47
48
|
export { createColumnHelper, flexRender, type Column, type ColumnDef, type ColumnFiltersState, type Row, type RowSelectionState, type SortingState, type Table, type VisibilityState, } from "@tanstack/react-table";
|
|
48
49
|
export { ThemeRuntimeProvider, ThemeScope, ThemeDocumentMetadata, useThemeRuntime, } from "./components/theme-runtime-provider.js";
|
|
49
50
|
export { JsonViewer } from "./components/json-viewer.js";
|
package/dist/index.js
CHANGED
|
@@ -32,7 +32,8 @@ 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
34
|
export { DataTable } from "./components/data-table/data-table.js";
|
|
35
|
-
export {
|
|
35
|
+
export { DataTableFrame, useDataTableFrame, } from "./components/data-table/data-table-frame.js";
|
|
36
|
+
export { DataTableToolbar, visibleFacets, } from "./components/data-table/data-table-toolbar.js";
|
|
36
37
|
export { DataTableFacetedFilter, } from "./components/data-table/data-table-faceted-filter.js";
|
|
37
38
|
export { DataTableViewOptions, } from "./components/data-table/data-table-view-options.js";
|
|
38
39
|
export { DataTableColumnHeader, } from "./components/data-table/data-table-column-header.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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": [
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
"react-markdown": "^10.0.0",
|
|
72
72
|
"remark-gfm": "^4.0.0",
|
|
73
73
|
"tailwind-merge": "^3.5.0",
|
|
74
|
-
"@iloveagents/foundry-agent": "^0.
|
|
75
|
-
"@iloveagents/foundry-web-primitives": "^0.
|
|
74
|
+
"@iloveagents/foundry-agent": "^0.18.0",
|
|
75
|
+
"@iloveagents/foundry-web-primitives": "^0.18.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@ag-ui/client": "^0.0.52",
|