@iloveagents/foundry-web-ui 0.19.0 → 0.20.1
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/README.md +80 -0
- package/dist/components/data-table/data-table-faceted-filter.js +11 -4
- package/dist/components/data-table/data-table-frame.d.ts +16 -1
- package/dist/components/data-table/data-table-frame.js +32 -7
- package/dist/components/data-table/data-table-selection-bar.d.ts +18 -0
- package/dist/components/data-table/data-table-selection-bar.js +15 -0
- package/dist/components/data-table/data-table-toolbar.d.ts +13 -4
- package/dist/components/data-table/data-table-toolbar.js +17 -4
- package/dist/components/data-table/data-table-view-options.d.ts +1 -1
- package/dist/components/data-table/data-table-view-options.js +1 -1
- package/dist/components/data-table/data-table.d.ts +43 -2
- package/dist/components/data-table/data-table.js +264 -28
- package/dist/components/data-table/date-buckets.d.ts +44 -0
- package/dist/components/data-table/date-buckets.js +178 -0
- package/dist/components/data-table/facets.d.ts +34 -2
- package/dist/components/data-table/facets.js +44 -2
- package/dist/components/data-table/use-data-table.d.ts +13 -1
- package/dist/components/data-table/use-data-table.js +47 -0
- package/dist/components/data-table/use-element-width.d.ts +7 -0
- package/dist/components/data-table/use-element-width.js +36 -0
- package/dist/components/data-table/use-pinned-offsets.d.ts +9 -0
- package/dist/components/data-table/use-pinned-offsets.js +79 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/lib/app-store.d.ts +11 -1
- package/dist/lib/app-store.js +42 -5
- package/dist/styles.css +91 -0
- package/dist/ui/dropdown-menu.js +1 -1
- 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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
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(() => ({
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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" }), "
|
|
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
|
|
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;
|