@iloveagents/foundry-web-ui 0.19.0 → 0.20.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 (29) hide show
  1. package/README.md +80 -0
  2. package/dist/components/data-table/data-table-faceted-filter.js +11 -4
  3. package/dist/components/data-table/data-table-frame.d.ts +16 -1
  4. package/dist/components/data-table/data-table-frame.js +32 -7
  5. package/dist/components/data-table/data-table-selection-bar.d.ts +18 -0
  6. package/dist/components/data-table/data-table-selection-bar.js +15 -0
  7. package/dist/components/data-table/data-table-toolbar.d.ts +13 -4
  8. package/dist/components/data-table/data-table-toolbar.js +17 -4
  9. package/dist/components/data-table/data-table-view-options.d.ts +1 -1
  10. package/dist/components/data-table/data-table-view-options.js +1 -1
  11. package/dist/components/data-table/data-table.d.ts +43 -2
  12. package/dist/components/data-table/data-table.js +264 -28
  13. package/dist/components/data-table/date-buckets.d.ts +44 -0
  14. package/dist/components/data-table/date-buckets.js +178 -0
  15. package/dist/components/data-table/facets.d.ts +34 -2
  16. package/dist/components/data-table/facets.js +44 -2
  17. package/dist/components/data-table/use-data-table.d.ts +13 -1
  18. package/dist/components/data-table/use-data-table.js +47 -0
  19. package/dist/components/data-table/use-element-width.d.ts +7 -0
  20. package/dist/components/data-table/use-element-width.js +36 -0
  21. package/dist/components/data-table/use-pinned-offsets.d.ts +9 -0
  22. package/dist/components/data-table/use-pinned-offsets.js +79 -0
  23. package/dist/index.d.ts +6 -2
  24. package/dist/index.js +6 -2
  25. package/dist/lib/app-store.d.ts +11 -1
  26. package/dist/lib/app-store.js +42 -5
  27. package/dist/styles.css +91 -0
  28. package/dist/ui/dropdown-menu.js +1 -1
  29. package/package.json +3 -3
package/README.md CHANGED
@@ -61,6 +61,86 @@ its values, `meta.label` names the column in the View menu, and
61
61
  (with `parseDataTableState` / `mergeDataTableUrlState`) to keep the whole list
62
62
  state in the URL.
63
63
 
64
+ ### A column says where it earns its place
65
+
66
+ ```tsx
67
+ const column: ColumnDef<Risk> = {
68
+ id: "worst_case",
69
+ accessorKey: "worst_case",
70
+ meta: {
71
+ breakpoint: "lg", // the narrowest LIST width it needs (not the window)
72
+ showIn: "focus", // only when the list has the screen to itself
73
+ pinned: "left", // frozen against the left edge while the rest scrolls
74
+ defaultHidden: true, // in the Columns menu, off until asked for*
75
+ align: "right",
76
+ },
77
+ };
78
+ ```
79
+
80
+ Widths are measured on the table itself, so a list beside an open detail pane
81
+ sheds exactly the columns a narrow window would and focus mode brings them
82
+ back — no media queries in the host. `DATA_TABLE_MIN_WIDTH` publishes the
83
+ bands (`sm` 448 · `md` 672 · `lg` 896 · `xl` 1024 · `2xl` 1280) for a host that
84
+ derives them from a schema.
85
+
86
+ \* `useDataTable` applies `defaultHidden` to its own initial state. A list
87
+ whose state lives elsewhere (the URL) passes `defaultHiddenColumns(columns)`
88
+ into that layer's `defaults` instead — a hidden-only URL records nothing when
89
+ a column is _shown_, so re-applying the default on every render would undo the
90
+ user the moment they bring one out.
91
+
92
+ Freezing is a run from the left edge: every column up to the last `pinned` one
93
+ comes along, so a leading checkbox never slides out from under its rows.
94
+
95
+ That gating is render-time and deliberately **not** TanStack `columnVisibility`
96
+ (which belongs to the user and rides in the URL), so anything that must agree
97
+ with what is on screen reads `onRenderedColumnsChange` rather than
98
+ `column.getIsVisible()`.
99
+
100
+ ### Numbers and dates you can tick
101
+
102
+ `meta.facetBuckets` turns a raw value into the keys a person actually filters
103
+ by; counts, OR-within, AND-across, the URL and the agent tools work on those
104
+ keys unchanged.
105
+
106
+ ```tsx
107
+ const probability: ColumnDef<Risk> = {
108
+ id: "probability",
109
+ accessorKey: "probability",
110
+ filterFn: facetFilterFn,
111
+ meta: {
112
+ facetBuckets: (value) => [Number(value) >= 0.5 ? "high" : "low"],
113
+ facetLabels: { high: "50 % or more", low: "under 50 %" },
114
+ },
115
+ };
116
+ ```
117
+
118
+ Dates get the buckets every list wants: `ageBucket` (`today` · `week` ·
119
+ `month` · `older` · `never`), `dueState` (`overdue` · `soon` · `later` ·
120
+ `undated`) and `relativeAge` for the cell. `parseDateish` underneath reads ISO
121
+ and the dotted European form and refuses `03/04/2026` — March in one country,
122
+ April in another — rather than guessing.
123
+
124
+ ### Selecting, opening, and the keyboard
125
+
126
+ A click selects (`onRowClick`), Enter or a double click opens (`onRowOpen`) —
127
+ the desktop-list split, so brushing a row never opens a pane by surprise.
128
+ `selectionColumn()` adds the tick boxes; `DataTableSelectionBar` is the one
129
+ place bulk actions live (and the selection a chat agent reads).
130
+ `onActiveRowChange` + `activeRowId` walk the list with ↑/↓/Home/End, Escape
131
+ clears, and the active row stays scrolled into view.
132
+
133
+ `useAppStore.setContextItem(key, item | null)` keeps one context chip per key,
134
+ so a list can keep the chat in step with its selection instead of piling up
135
+ stale chips.
136
+
137
+ ### Room to scroll
138
+
139
+ `DataTable` takes `maxHeight` to give a list its own viewport (sticky header,
140
+ both axes) instead of running down the page; inside a focused `DataTableFrame`
141
+ it fills the layer. `spacer` (default) keeps columns at their natural width
142
+ with the slack at the end, the way a spreadsheet looks.
143
+
64
144
  ## Extension Points
65
145
 
66
146
  Feature modules (like SPACES) plug into `@iloveagents/foundry-web-ui` via registries — no direct imports needed:
@@ -14,11 +14,18 @@ export function DataTableFacetedFilter({ column, title, options: declared, searc
14
14
  const needle = query.trim().toLowerCase();
15
15
  return needle ? options.filter((o) => o.label.toLowerCase().includes(needle)) : options;
16
16
  }, [options, query]);
17
+ // A functional update, not one derived from this render's `selected`: with
18
+ // URL-backed state the re-render lands a tick later, so two quick clicks
19
+ // would otherwise both start from the same value and the first would be
20
+ // lost.
17
21
  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
+ column.setFilterValue((current) => {
23
+ const values = normalizeFacetValue(current);
24
+ const next = values.includes(value)
25
+ ? values.filter((item) => item !== value)
26
+ : [...values, value];
27
+ return next.length ? next : undefined;
28
+ });
22
29
  };
23
30
  return (_jsxs(Popover, { onOpenChange: (open) => {
24
31
  if (open)
@@ -3,6 +3,19 @@
3
3
  * fullscreen layer (Escape leaves it). `DataTableToolbar` shows the focus
4
4
  * button whenever it renders inside a frame — users with wide lists get a
5
5
  * way out of a cramped page without the host doing anything.
6
+ *
7
+ * The focus layer stays where it is in the tree — only its classes change —
8
+ * so entering and leaving focus never remounts the list (its rows, drawer,
9
+ * selection and in-flight edits survive) and it keeps inheriting the host's
10
+ * theme scope. It never scrolls itself: it is a fixed flex column whose
11
+ * growing child — the table — owns the scrolling, which keeps the toolbar in
12
+ * place while the rows move under a sticky header (see `DataTable`, which
13
+ * reads this context).
14
+ *
15
+ * A `position: fixed` layer is relative to the viewport unless an ancestor
16
+ * establishes a containing block (`transform`, `filter`, `perspective`,
17
+ * `contain: paint`); a host that does that on a list's ancestor has to opt
18
+ * out of focus mode.
6
19
  */
7
20
  import { type HTMLAttributes, type ReactNode } from "react";
8
21
  export interface DataTableFrameContextValue {
@@ -16,5 +29,7 @@ export interface DataTableFrameProps extends Omit<HTMLAttributes<HTMLDivElement>
16
29
  /** Classes applied in focus mode on top of the fullscreen layer. */
17
30
  focusClassName?: string;
18
31
  defaultFocused?: boolean;
32
+ /** Told when focus mode turns on or off — a host with room to spare can show more. */
33
+ onFocusChange?: (focused: boolean) => void;
19
34
  }
20
- export declare function DataTableFrame({ children, className, focusClassName, defaultFocused, ...rest }: DataTableFrameProps): import("react/jsx-runtime").JSX.Element;
35
+ export declare const DataTableFrame: import("react").ForwardRefExoticComponent<DataTableFrameProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -4,22 +4,41 @@ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  * fullscreen layer (Escape leaves it). `DataTableToolbar` shows the focus
5
5
  * button whenever it renders inside a frame — users with wide lists get a
6
6
  * way out of a cramped page without the host doing anything.
7
+ *
8
+ * The focus layer stays where it is in the tree — only its classes change —
9
+ * so entering and leaving focus never remounts the list (its rows, drawer,
10
+ * selection and in-flight edits survive) and it keeps inheriting the host's
11
+ * theme scope. It never scrolls itself: it is a fixed flex column whose
12
+ * growing child — the table — owns the scrolling, which keeps the toolbar in
13
+ * place while the rows move under a sticky header (see `DataTable`, which
14
+ * reads this context).
15
+ *
16
+ * A `position: fixed` layer is relative to the viewport unless an ancestor
17
+ * establishes a containing block (`transform`, `filter`, `perspective`,
18
+ * `contain: paint`); a host that does that on a list's ancestor has to opt
19
+ * out of focus mode.
7
20
  */
8
- import { createContext, useContext, useEffect, useMemo, useState, } from "react";
21
+ import { createContext, forwardRef, useContext, useEffect, useMemo, useRef, useState, } from "react";
9
22
  import { cn } from "@iloveagents/foundry-web-primitives";
10
23
  const DataTableFrameContext = createContext(null);
11
24
  /** The enclosing frame's focus state, or `null` outside a frame. */
12
25
  export function useDataTableFrame() {
13
26
  return useContext(DataTableFrameContext);
14
27
  }
15
- export function DataTableFrame({ children, className, focusClassName, defaultFocused = false, ...rest }) {
28
+ export const DataTableFrame = forwardRef(function DataTableFrame({ children, className, focusClassName, defaultFocused = false, onFocusChange, ...rest }, ref) {
16
29
  const [focused, setFocused] = useState(defaultFocused);
30
+ const changeRef = useRef(onFocusChange);
31
+ changeRef.current = onFocusChange;
17
32
  useEffect(() => {
18
33
  if (!focused)
19
34
  return;
20
35
  const onKeyDown = (event) => {
21
- if (event.key === "Escape")
36
+ // Through the same door as the button: a host told about focus mode
37
+ // must hear about it however it ended.
38
+ if (event.key === "Escape") {
22
39
  setFocused(false);
40
+ changeRef.current?.(false);
41
+ }
23
42
  };
24
43
  document.addEventListener("keydown", onKeyDown);
25
44
  const previousOverflow = document.body.style.overflow;
@@ -29,8 +48,14 @@ export function DataTableFrame({ children, className, focusClassName, defaultFoc
29
48
  document.body.style.overflow = previousOverflow;
30
49
  };
31
50
  }, [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)
51
+ const value = useMemo(() => ({
52
+ focused,
53
+ setFocused: (next) => {
54
+ setFocused(next);
55
+ changeRef.current?.(next);
56
+ },
57
+ }), [focused]);
58
+ return (_jsx(DataTableFrameContext.Provider, { value: value, children: _jsx("div", { ...rest, ref: ref, "data-focused": focused ? "" : undefined, className: focused
59
+ ? cn("fixed inset-0 z-50 flex flex-col gap-3 overflow-hidden bg-background p-4 sm:p-6", focusClassName)
35
60
  : className, children: children }) }));
36
- }
61
+ });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What you can do with the rows you ticked. Appears only while something is
3
+ * selected, states the count in words, and hosts drop their bulk actions in —
4
+ * the same place in every list, so "select some rows, then do a thing" is one
5
+ * habit across the platform (and the same set the chat agent reads).
6
+ */
7
+ import type { Row, Table } from "@tanstack/react-table";
8
+ import type { ReactNode } from "react";
9
+ export interface DataTableSelectionBarProps<T> {
10
+ table: Table<T>;
11
+ /** Bulk actions for the selected rows. */
12
+ children?: ReactNode | ((rows: Row<T>[]) => ReactNode);
13
+ /** What one row is called ("risk"); the plural is derived, or give `plural`. */
14
+ label?: string;
15
+ plural?: string;
16
+ className?: string;
17
+ }
18
+ export declare function DataTableSelectionBar<T>({ table, children, label, plural, className, }: DataTableSelectionBarProps<T>): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,15 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { X } from "lucide-react";
3
+ import { Button, cn } from "@iloveagents/foundry-web-primitives";
4
+ export function DataTableSelectionBar({ table, children, label = "row", plural, className, }) {
5
+ // Every selected row, not only the ones the current filter shows: a
6
+ // selection the user cannot see is still a selection, and hiding the bar
7
+ // would take away the only way to clear it.
8
+ const rows = table.getSelectedRowModel().rows;
9
+ if (rows.length === 0)
10
+ return null;
11
+ return (_jsxs("div", { "data-selection-bar": "", role: "status", className: cn("flex min-h-8 flex-wrap items-center gap-2 rounded-xl bg-primary/5 px-3 text-sm", className), children: [_jsxs("span", { className: "font-medium", children: [rows.length, " ", rows.length === 1 ? label : (plural ?? `${label}s`), " selected"] }), _jsx("div", { className: "flex flex-wrap items-center gap-2", children: typeof children === "function" ? children(rows) : children }), _jsxs(Button, { type: "button", variant: "ghost", size: "sm",
12
+ // `true` = the blank state, not the caller's `initialState`, which
13
+ // would make "Clear" resurrect rows.
14
+ onClick: () => table.resetRowSelection(true), className: "ml-auto h-8 rounded-lg px-2.5", children: ["Clear", _jsx(X, { className: "size-4" })] })] }));
15
+ }
@@ -2,7 +2,7 @@
2
2
  * Two bands, so a list with many filters still reads as ordered rather than
3
3
  * as a ragged wrap:
4
4
  *
5
- * row 1 search …………………………………… Filters (n) · actions · View · ⤢
5
+ * row 1 search ……………………………… Filters (n) · actions · Columns · ⤢
6
6
  * row 2 facet · facet · facet · + Filter · Reset
7
7
  *
8
8
  * Row one never wraps (its right group is fixed); filters live on their own
@@ -37,12 +37,15 @@ export interface DataTableToolbarProps<T> {
37
37
  collapsibleFilters?: boolean;
38
38
  /** Whether the filter row starts open (default `true`; when closed the toggle still shows the active count). */
39
39
  defaultFiltersOpen?: boolean;
40
- /** Facets always visible when none declares `pinned` (default 3; `Infinity` shows all). */
40
+ /**
41
+ * Facets always visible when none declares `pinned` (default 3; `Infinity`
42
+ * shows all). Focus mode shows every facet unless a number is given.
43
+ */
41
44
  pinnedFacets?: number;
42
45
  searchPlaceholder?: string;
43
46
  /** Hide the search box. */
44
47
  search?: boolean;
45
- /** Hide the View (column visibility) menu. */
48
+ /** Hide the Columns (column visibility) menu. */
46
49
  viewOptions?: boolean;
47
50
  /** Hide the focus (fullscreen) button that appears inside a `DataTableFrame`. */
48
51
  focus?: boolean;
@@ -50,6 +53,12 @@ export interface DataTableToolbarProps<T> {
50
53
  children?: ReactNode;
51
54
  /** Controls rendered on the right, before the View menu. */
52
55
  actions?: ReactNode;
56
+ /**
57
+ * Shown *in place of* the filter row while rows are ticked — the mail-app
58
+ * move, so the list never jumps when a selection appears. Receives nothing;
59
+ * compose `DataTableSelectionBar` or your own summary.
60
+ */
61
+ selectionBar?: ReactNode;
53
62
  className?: string;
54
63
  }
55
64
  /** Which facets render as buttons: pinned ones, revealed ones, and any with a value. */
@@ -57,4 +66,4 @@ export declare function visibleFacets<T>(table: Table<T>, facets: DataTableFacet
57
66
  shown: DataTableFacet[];
58
67
  hidden: DataTableFacet[];
59
68
  };
60
- export declare function DataTableToolbar<T>({ table, facets, layout, collapsibleFilters, defaultFiltersOpen, pinnedFacets, searchPlaceholder, search, viewOptions, focus, children, actions, className, }: DataTableToolbarProps<T>): import("react/jsx-runtime").JSX.Element;
69
+ export declare function DataTableToolbar<T>({ table, facets, layout, collapsibleFilters, defaultFiltersOpen, pinnedFacets, searchPlaceholder, search, viewOptions, focus, children, actions, selectionBar, className, }: DataTableToolbarProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -9,6 +9,12 @@ import { DataTableViewOptions } from "./data-table-view-options.js";
9
9
  import { columnLabel, normalizeFacetValue } from "./facets.js";
10
10
  /** Which facets render as buttons: pinned ones, revealed ones, and any with a value. */
11
11
  export function visibleFacets(table, facets, revealed, pinnedFacets) {
12
+ // "Show everything" beats a per-facet `pinned: false`: focus mode has the
13
+ // room, and the promise is that nothing stays behind the menu there.
14
+ if (pinnedFacets === Number.POSITIVE_INFINITY) {
15
+ const shown = facets.filter((facet) => table.getColumn(facet.columnId));
16
+ return { shown, hidden: [] };
17
+ }
12
18
  const explicit = facets.some((facet) => facet.pinned !== undefined);
13
19
  const shown = [];
14
20
  const hidden = [];
@@ -22,14 +28,21 @@ export function visibleFacets(table, facets, revealed, pinnedFacets) {
22
28
  });
23
29
  return { shown, hidden };
24
30
  }
25
- export function DataTableToolbar({ table, facets = [], layout = "auto", collapsibleFilters, defaultFiltersOpen = true, pinnedFacets = 3, searchPlaceholder = "Search…", search = true, viewOptions = true, focus = true, children, actions, className, }) {
31
+ export function DataTableToolbar({ table, facets = [], layout = "auto", collapsibleFilters, defaultFiltersOpen = true, pinnedFacets, searchPlaceholder = "Search…", search = true, viewOptions = true, focus = true, children, actions, selectionBar, className, }) {
26
32
  const [revealed, setRevealed] = useState(() => new Set());
27
33
  const [filtersOpen, setFiltersOpen] = useState(defaultFiltersOpen);
28
34
  const frame = useDataTableFrame();
29
35
  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;
30
36
  const state = table.getState();
37
+ // `DataTableSelectionBar` renders null with nothing selected, so the
38
+ // element being present says nothing. Ask the table instead — otherwise
39
+ // the filter row disappears behind an empty selection row.
40
+ const hasSelection = table.getSelectedRowModel().rows.length > 0;
41
+ const showSelectionBar = Boolean(selectionBar) && hasSelection;
31
42
  const isFiltered = state.columnFilters.length > 0 || Boolean(state.globalFilter) || revealed.size > 0;
32
- const { shown, hidden } = visibleFacets(table, facets, revealed, pinnedFacets);
43
+ // Focus mode has the screen to itself: show every facet unless told otherwise.
44
+ const pinnedCount = pinnedFacets ?? (frame?.focused ? Number.POSITIVE_INFINITY : 3);
45
+ const { shown, hidden } = visibleFacets(table, facets, revealed, pinnedCount);
33
46
  // `children` alone (extra controls, no facets) is not a reason to open a
34
47
  // second band — it rides in the filter row only when there is one.
35
48
  const stacked = layout === "stacked" || (layout === "auto" && facets.length > 0);
@@ -62,7 +75,7 @@ export function DataTableToolbar({ table, facets = [], layout = "auto", collapsi
62
75
  const filtersToggle = canCollapse ? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", "data-filters-toggle": "", "aria-expanded": showFilters, onClick: () => setFiltersOpen((current) => !current), className: cn("h-8 rounded-xl border-border/45 bg-background/65 shadow-none hover:bg-muted/26", activeFilters > 0 && "border-primary/30"), children: [_jsx(SlidersHorizontal, { className: "size-4" }), "Filters", activeFilters > 0 ? (_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: activeFilters }), _jsxs("span", { className: "sr-only", children: [activeFilters, " active"] })] })) : null] })) : null;
63
76
  const rightGroup = actions || viewOptions || focusButton || filtersToggle ? (_jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [filtersToggle, actions, viewOptions ? _jsx(DataTableViewOptions, { table: table }) : null, focusButton] })) : null;
64
77
  if (!stacked) {
65
- 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: [searchNode, facetNodes, addFilterNode, children, resetNode] }), rightGroup ? _jsx("div", { className: "ml-auto", children: rightGroup }) : null] }));
78
+ return (_jsxs("div", { className: cn("flex flex-col gap-2", className), children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-2", children: [searchNode, facetNodes, addFilterNode, children, resetNode] }), rightGroup ? _jsx("div", { className: "ml-auto", children: rightGroup }) : null] }), showSelectionBar ? _jsx("div", { "data-selection-row": "", children: selectionBar }) : null] }));
66
79
  }
67
- return (_jsxs("div", { className: cn("flex flex-col gap-2", className), children: [_jsxs("div", { className: "flex items-center gap-2", children: [searchNode, rightGroup ? _jsx("div", { className: "ml-auto", children: rightGroup }) : null] }), showFilters ? (_jsxs("div", { "data-filter-row": "", className: "flex flex-wrap items-center gap-2", children: [facetNodes, addFilterNode, children, resetNode] })) : null] }));
80
+ return (_jsxs("div", { className: cn("flex flex-col gap-2", className), children: [_jsxs("div", { className: "flex items-center gap-2", children: [searchNode, rightGroup ? _jsx("div", { className: "ml-auto", children: rightGroup }) : null] }), showSelectionBar ? (_jsx("div", { "data-selection-row": "", children: selectionBar })) : showFilters ? (_jsxs("div", { "data-filter-row": "", className: "flex flex-wrap items-center gap-2", children: [facetNodes, addFilterNode, children, resetNode] })) : null] }));
68
81
  }
@@ -1,4 +1,4 @@
1
- /** The "View" menu: toggle column visibility. */
1
+ /** The "Columns" menu: toggle column visibility. */
2
2
  import type { Table } from "@tanstack/react-table";
3
3
  export interface DataTableViewOptionsProps<T> {
4
4
  table: Table<T>;
@@ -7,5 +7,5 @@ export function DataTableViewOptions({ table, className }) {
7
7
  const columns = table.getAllLeafColumns().filter((column) => column.getCanHide());
8
8
  if (!columns.length)
9
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)))] })] }));
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" }), "Columns"] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-48 rounded-xl border-border/45 bg-popover/95 shadow-lg", children: [_jsx(DropdownMenuLabel, { children: "Show columns" }), _jsx(DropdownMenuSeparator, {}), columns.map((column) => (_jsx(DropdownMenuCheckboxItem, { checked: column.getIsVisible(), onCheckedChange: (checked) => column.toggleVisibility(Boolean(checked)), onSelect: (event) => event.preventDefault(), children: columnLabel(column) }, column.id)))] })] }));
11
11
  }
@@ -5,21 +5,62 @@
5
5
  * never doubles as "open".
6
6
  */
7
7
  import { type Row, type Table } from "@tanstack/react-table";
8
- import type { KeyboardEvent, MouseEvent, ReactNode } from "react";
8
+ import { type KeyboardEvent, type MouseEvent, type ReactNode } from "react";
9
9
  export interface DataTableProps<T> {
10
10
  table: Table<T>;
11
+ /** Single click on a row — selecting it, in the desktop sense. */
11
12
  onRowClick?: (row: Row<T>, event: MouseEvent | KeyboardEvent) => void;
13
+ /**
14
+ * Opening a row: Enter or a double click, the way a file list behaves. Keep
15
+ * it separate from `onRowClick` so brushing or clicking a row (to scroll,
16
+ * to select) never opens a detail pane by surprise.
17
+ */
18
+ onRowOpen?: (row: Row<T>, event: MouseEvent | KeyboardEvent) => void;
12
19
  /** The row whose details are open elsewhere (drawer, panel) — highlighted, `aria-current`. */
13
20
  activeRowId?: string | null;
21
+ /**
22
+ * Move the active row with ↑/↓ (Home/End for the ends) and clear it with
23
+ * Escape, scrolling it into view — the keyboard flow people expect from a
24
+ * master list beside a detail pane. Needs `activeRowId`.
25
+ */
26
+ onActiveRowChange?: (row: Row<T> | null) => void;
14
27
  rowClassName?: (row: Row<T>) => string | undefined;
15
28
  /** Content of a group header row (default: label and count). */
16
29
  renderGroup?: (row: Row<T>) => ReactNode;
17
30
  /** Declared order of group keys; unknown keys follow in data order. */
18
31
  groupOrder?: string[];
19
32
  emptyState?: ReactNode;
33
+ /**
34
+ * Keep the columns at their natural width and let the leftover room sit at
35
+ * the end, the way a spreadsheet looks, instead of stretching a text column
36
+ * across the gap. Set `false` for a table whose last column should fill.
37
+ */
38
+ spacer?: boolean;
39
+ /** Keep the header in place while the rows scroll (always on in focus mode). */
20
40
  stickyHeader?: boolean;
41
+ /**
42
+ * Make the table the scroll region of its flex-column parent, on both
43
+ * axes, instead of growing the page. Defaults to true inside a focused
44
+ * `DataTableFrame`.
45
+ */
46
+ fill?: boolean;
47
+ /**
48
+ * Give the list its own viewport of at most this height (e.g. `"70vh"`),
49
+ * so the rows scroll under a sticky header instead of running down the
50
+ * page — the way a spreadsheet-like list is expected to behave. Ignored
51
+ * while `fill` is on, which already bounds the height.
52
+ */
53
+ maxHeight?: string | number;
21
54
  dense?: boolean;
55
+ /**
56
+ * The columns the table actually drew, whenever that set changes. Width and
57
+ * focus gating happen at render time and are deliberately NOT TanStack
58
+ * visibility (that belongs to the user and rides in the URL) — so anything
59
+ * that must agree with what is on screen, above all the chat agent's view
60
+ * of the list, learns it here.
61
+ */
62
+ onRenderedColumnsChange?: (columnIds: string[]) => void;
22
63
  className?: string;
23
64
  "aria-label"?: string;
24
65
  }
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;
66
+ export declare function DataTable<T>({ table, onRowClick, onRowOpen, activeRowId, onActiveRowChange, rowClassName, renderGroup, groupOrder, emptyState, spacer, stickyHeader, fill, maxHeight, dense, onRenderedColumnsChange, className, "aria-label": ariaLabel, }: DataTableProps<T>): import("react/jsx-runtime").JSX.Element;