@iloveagents/foundry-web-ui 0.14.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/components/assistant-chat.js +1 -1
- package/dist/components/chat-message-parts.d.ts +12 -7
- package/dist/components/chat-message-parts.js +52 -17
- package/dist/components/data-table/data-table-column-header.d.ts +8 -0
- package/dist/components/data-table/data-table-column-header.js +12 -0
- package/dist/components/data-table/data-table-faceted-filter.d.ts +16 -0
- package/dist/components/data-table/data-table-faceted-filter.js +32 -0
- package/dist/components/data-table/data-table-pagination.d.ts +8 -0
- package/dist/components/data-table/data-table-pagination.js +15 -0
- package/dist/components/data-table/data-table-row-actions.d.ts +16 -0
- package/dist/components/data-table/data-table-row-actions.js +11 -0
- package/dist/components/data-table/data-table-toolbar.d.ts +27 -0
- package/dist/components/data-table/data-table-toolbar.js +18 -0
- package/dist/components/data-table/data-table-view-options.d.ts +7 -0
- package/dist/components/data-table/data-table-view-options.js +11 -0
- package/dist/components/data-table/data-table.d.ts +25 -0
- package/dist/components/data-table/data-table.js +65 -0
- package/dist/components/data-table/facets.d.ts +82 -0
- package/dist/components/data-table/facets.js +156 -0
- package/dist/components/data-table/selection-column.d.ts +4 -0
- package/dist/components/data-table/selection-column.js +19 -0
- package/dist/components/data-table/state.d.ts +41 -0
- package/dist/components/data-table/state.js +148 -0
- package/dist/components/data-table/use-data-table.d.ts +22 -0
- package/dist/components/data-table/use-data-table.js +102 -0
- package/dist/components/message-disclosure.d.ts +6 -18
- package/dist/components/message-disclosure.js +24 -9
- package/dist/components/reasoning-effort-picker.js +38 -2
- package/dist/components/reasoning-part.d.ts +8 -5
- package/dist/components/reasoning-part.js +15 -11
- package/dist/components/sidebar.js +22 -20
- package/dist/components/turn-activity.d.ts +33 -0
- package/dist/components/turn-activity.js +110 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +14 -2
- package/dist/lib/nav-config.d.ts +16 -0
- package/dist/ui/dropdown-menu.d.ts +1 -0
- package/dist/ui/dropdown-menu.js +4 -1
- package/package.json +5 -4
- package/dist/components/tool-run-group.d.ts +0 -29
- package/dist/components/tool-run-group.js +0 -56
package/README.md
CHANGED
|
@@ -38,6 +38,29 @@ components are generated:
|
|
|
38
38
|
@source "../node_modules/@iloveagents/foundry-web-ui/dist";
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
+
## Lists
|
|
42
|
+
|
|
43
|
+
Every list renders through one `DataTable` family (TanStack Table underneath):
|
|
44
|
+
AND-of-tokens search, faceted filters with live counts (OR within a facet, AND
|
|
45
|
+
across facets, array cells count per element, empty cells as "None"), sortable
|
|
46
|
+
headers, column visibility, row selection, optional paging, optional grouping,
|
|
47
|
+
and a URL codec that keeps `q` / `sort` / `f_<column>` / `hide` / `page` / `size`.
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
const table = useDataTable({ data: rows, columns, getRowId: (row) => row.id, pageSize: 20 });
|
|
51
|
+
|
|
52
|
+
<DataTableToolbar table={table} facets={[{ columnId: "status" }, { columnId: "priority" }]} />
|
|
53
|
+
<DataTable table={table} onRowClick={(row) => open(row.original)} />
|
|
54
|
+
<DataTablePagination table={table} />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Give a column `filterFn: facetFilterFn` (or the string `"facet"` in untyped
|
|
58
|
+
code) to use it as a facet; `meta.facetLabels` names
|
|
59
|
+
its values, `meta.label` names the column in the View menu, and
|
|
60
|
+
`DataTableColumnHeader` makes a header sortable. Pass `state` + `onStateChange`
|
|
61
|
+
(with `parseDataTableState` / `mergeDataTableUrlState`) to keep the whole list
|
|
62
|
+
state in the URL.
|
|
63
|
+
|
|
41
64
|
## Extension Points
|
|
42
65
|
|
|
43
66
|
Feature modules (like SPACES) plug into `@iloveagents/foundry-web-ui` via registries — no direct imports needed:
|
|
@@ -120,7 +120,7 @@ export function ChatContent({ starterSuggestions = DEFAULT_STARTER_SUGGESTIONS,
|
|
|
120
120
|
const collapseChat = useChatBubbleStore((s) => s.collapse);
|
|
121
121
|
const showPagePanel = useChatBubbleStore((s) => s.showPagePanel);
|
|
122
122
|
const togglePagePanel = useChatBubbleStore((s) => s.togglePagePanel);
|
|
123
|
-
return (_jsxs(ThreadPrimitive.Root, { className: cn("flex flex-col flex-1", "bg-background text-foreground", "overflow-hidden"), children: [isExpanded && (_jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-2 border-b border-border", children: [_jsxs("span", { className: "text-sm font-medium text-foreground flex items-center gap-2", children: [_jsx(Bot, { className: "size-4 text-primary" }), "Chat"] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(TooltipIconButton, { tooltip: showPagePanel ? "Hide page panel" : "Show page panel", size: "icon", onClick: togglePagePanel, className: "hidden xl:inline-flex", children: showPagePanel ? (_jsx(PanelRightClose, { className: "size-4" })) : (_jsx(PanelRightOpen, { className: "size-4" })) }), _jsx(TooltipIconButton, { tooltip: "Minimize to bubble", size: "icon", onClick: collapseChat, children: _jsx(Minimize2, { className: "size-4" }) })] })] })), _jsxs(DropZone, { children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsxs(ThreadPrimitive.Viewport, { className: "absolute inset-0 overflow-y-auto bg-background", children: [_jsx(ThreadPrimitive.Empty, { children: _jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-foreground text-center mb-6", children: "How can I help you today?" }), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
123
|
+
return (_jsxs(ThreadPrimitive.Root, { className: cn("flex flex-col flex-1", "bg-background text-foreground", "overflow-hidden"), children: [isExpanded && (_jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-2 border-b border-border", children: [_jsxs("span", { className: "text-sm font-medium text-foreground flex items-center gap-2", children: [_jsx(Bot, { className: "size-4 text-primary" }), "Chat"] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(TooltipIconButton, { tooltip: showPagePanel ? "Hide page panel" : "Show page panel", size: "icon", onClick: togglePagePanel, className: "hidden xl:inline-flex", children: showPagePanel ? (_jsx(PanelRightClose, { className: "size-4" })) : (_jsx(PanelRightOpen, { className: "size-4" })) }), _jsx(TooltipIconButton, { tooltip: "Minimize to bubble", size: "icon", onClick: collapseChat, children: _jsx(Minimize2, { className: "size-4" }) })] })] })), _jsxs(DropZone, { children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsxs(ThreadPrimitive.Viewport, { className: "absolute inset-0 overflow-y-auto bg-background", children: [_jsx(ThreadPrimitive.Empty, { children: _jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-foreground text-center mb-6", children: "How can I help you today?" }), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8 pb-4", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
124
124
|
UserMessage,
|
|
125
125
|
EditComposer,
|
|
126
126
|
AssistantMessage,
|
|
@@ -10,13 +10,18 @@
|
|
|
10
10
|
* hand-rolling a parts config is what keeps them honest — there is now only
|
|
11
11
|
* one place to drift from.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* over the cards, collapsed; see that component
|
|
13
|
+
* Each stretch of machinery folds into one {@link TurnActivity} — a single
|
|
14
|
+
* summary row over the cards and the thinking, collapsed; see that component
|
|
15
|
+
* for why.
|
|
15
16
|
*
|
|
16
|
-
* Reading order is
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Reading order is CHRONOLOGICAL, throughout. It briefly was not: text
|
|
18
|
+
* carried a flex `order` that floated every prose part to the bottom of the
|
|
19
|
+
* message, to keep a reasoning row from sitting above a one-line answer.
|
|
20
|
+
* With the machinery folded into a quiet 20px row that argument is spent,
|
|
21
|
+
* and the hoist actively hurt — in a multi-step turn it stripped the
|
|
22
|
+
* narration out from between the work and stacked the bare rows into the
|
|
23
|
+
* ladder this component exists to prevent. It also glued consecutive prose
|
|
24
|
+
* parts into one run-on paragraph. Parts now render where the model put
|
|
25
|
+
* them.
|
|
21
26
|
*/
|
|
22
27
|
export declare function ChatMessageParts(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -3,12 +3,21 @@ import { MessagePrimitive, groupPartByType } from "@assistant-ui/react";
|
|
|
3
3
|
import { MarkdownText } from "./markdown-text.js";
|
|
4
4
|
import { ReasoningMessagePartComponent } from "./reasoning-part.js";
|
|
5
5
|
import { ToolFallback } from "./tool-fallback.js";
|
|
6
|
-
import {
|
|
6
|
+
import { TurnActivity } from "./turn-activity.js";
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* A turn's machinery nests under a `group-activity` node; inside it, adjacent
|
|
9
|
+
* tool calls still coalesce into `group-tool`.
|
|
10
|
+
*
|
|
11
|
+
* Text is deliberately absent from this map. The model's prose stays
|
|
12
|
+
* ungrouped, so it can never end up inside the fold — and because grouping
|
|
13
|
+
* coalesces ADJACENT parts, each stretch of prose also ends one activity node
|
|
14
|
+
* and starts the next. That is the intent: a multi-step turn reads as work,
|
|
15
|
+
* what it learned, work, what it learned, answer.
|
|
10
16
|
*/
|
|
11
|
-
const groupBy = groupPartByType({
|
|
17
|
+
const groupBy = groupPartByType({
|
|
18
|
+
"tool-call": ["group-activity", "group-tool"],
|
|
19
|
+
reasoning: ["group-activity"],
|
|
20
|
+
});
|
|
12
21
|
/**
|
|
13
22
|
* The parts of an assistant message — ONE renderer, used by every chat
|
|
14
23
|
* surface.
|
|
@@ -21,27 +30,53 @@ const groupBy = groupPartByType({ "tool-call": ["group-tool"] });
|
|
|
21
30
|
* hand-rolling a parts config is what keeps them honest — there is now only
|
|
22
31
|
* one place to drift from.
|
|
23
32
|
*
|
|
24
|
-
*
|
|
25
|
-
* over the cards, collapsed; see that component
|
|
33
|
+
* Each stretch of machinery folds into one {@link TurnActivity} — a single
|
|
34
|
+
* summary row over the cards and the thinking, collapsed; see that component
|
|
35
|
+
* for why.
|
|
26
36
|
*
|
|
27
|
-
* Reading order is
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
37
|
+
* Reading order is CHRONOLOGICAL, throughout. It briefly was not: text
|
|
38
|
+
* carried a flex `order` that floated every prose part to the bottom of the
|
|
39
|
+
* message, to keep a reasoning row from sitting above a one-line answer.
|
|
40
|
+
* With the machinery folded into a quiet 20px row that argument is spent,
|
|
41
|
+
* and the hoist actively hurt — in a multi-step turn it stripped the
|
|
42
|
+
* narration out from between the work and stacked the bare rows into the
|
|
43
|
+
* ladder this component exists to prevent. It also glued consecutive prose
|
|
44
|
+
* parts into one run-on paragraph. Parts now render where the model put
|
|
45
|
+
* them.
|
|
32
46
|
*/
|
|
33
47
|
export function ChatMessageParts() {
|
|
34
48
|
return (_jsx(MessagePrimitive.GroupedParts, { groupBy: groupBy, indicator: "never", children: ({ part, children }) => {
|
|
35
49
|
switch (part.type) {
|
|
50
|
+
case "group-activity":
|
|
51
|
+
return (_jsx(TurnActivity, { indices: part.indices, isRunning: part.status?.type === "running", children: children }));
|
|
36
52
|
case "group-tool":
|
|
37
|
-
|
|
53
|
+
// Inside the activity fold, so NOT another collapsed row —
|
|
54
|
+
// opening one disclosure only to meet a second is a maze. The
|
|
55
|
+
// cards render directly; the count already showed on the row
|
|
56
|
+
// above.
|
|
57
|
+
return _jsx("div", { className: "flex flex-col gap-2", children: children });
|
|
38
58
|
case "text":
|
|
39
|
-
// The
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
59
|
+
// The wrapper is load-bearing, though it carries no classes.
|
|
60
|
+
// `MarkdownText` renders `<ReactMarkdown>` directly, which emits
|
|
61
|
+
// its top-level blocks into a FRAGMENT — without an element to
|
|
62
|
+
// hold them, every paragraph, list and code block becomes a
|
|
63
|
+
// direct child of the message's `gap-4` flex container and picks
|
|
64
|
+
// up 16px of message-part spacing between them, on top of their
|
|
65
|
+
// own margins. It used to be the `order-2` div; the ordering went
|
|
66
|
+
// away, the wrapper must not.
|
|
67
|
+
return (_jsx("div", { children: _jsx(MarkdownText, {}) }));
|
|
43
68
|
case "reasoning":
|
|
44
|
-
|
|
69
|
+
// Plain inside the fold, rather than behind a second disclosure.
|
|
70
|
+
//
|
|
71
|
+
// Its POSITION is only as chronological as the parts array it is
|
|
72
|
+
// given. Replayed history preserves the model's interleaving;
|
|
73
|
+
// the live adapter does not — it accumulates all reasoning into a
|
|
74
|
+
// single part emitted ahead of every tool call, so a live
|
|
75
|
+
// reasoning A → tool A → reasoning B → tool B arrives as
|
|
76
|
+
// reasoning A+B → tool A → tool B. That is the adapter's to fix,
|
|
77
|
+
// and rendering in place is right either way; this note is here
|
|
78
|
+
// so the next reader does not take the ordering on faith.
|
|
79
|
+
return _jsx(ReasoningMessagePartComponent, { text: part.text, nested: true });
|
|
45
80
|
case "tool-call":
|
|
46
81
|
return part.toolUI ?? _jsx(ToolFallback, { ...part });
|
|
47
82
|
default:
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Sortable column header with an asc / desc / hide menu. */
|
|
2
|
+
import type { Column } from "@tanstack/react-table";
|
|
3
|
+
export interface DataTableColumnHeaderProps<T> {
|
|
4
|
+
column: Column<T, unknown>;
|
|
5
|
+
title: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function DataTableColumnHeader<T>({ column, title, className, }: DataTableColumnHeaderProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from "lucide-react";
|
|
3
|
+
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
4
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu.js";
|
|
5
|
+
export function DataTableColumnHeader({ column, title, className, }) {
|
|
6
|
+
if (!column.getCanSort()) {
|
|
7
|
+
return _jsx("div", { className: cn("font-medium", className), children: title });
|
|
8
|
+
}
|
|
9
|
+
const sorted = column.getIsSorted();
|
|
10
|
+
const align = column.columnDef.meta?.align;
|
|
11
|
+
return (_jsx("div", { className: cn("flex items-center", align === "right" && "justify-end", className), children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": `${title}: ${sorted === "asc" ? "sorted ascending" : sorted === "desc" ? "sorted descending" : "not sorted"}`, "data-sorted": sorted || undefined, className: "-ml-3 h-8 gap-1.5 rounded-lg px-2 text-xs font-medium text-muted-foreground data-[state=open]:bg-accent data-[sorted]:text-foreground", children: [_jsx("span", { children: title }), sorted === "desc" ? (_jsx(ArrowDown, { className: "size-3.5" })) : sorted === "asc" ? (_jsx(ArrowUp, { className: "size-3.5" })) : (_jsx(ChevronsUpDown, { className: "size-3.5 opacity-60" }))] }) }), _jsxs(DropdownMenuContent, { align: "start", className: "rounded-xl border-border/45 bg-popover/95 shadow-lg", children: [_jsxs(DropdownMenuItem, { onClick: () => column.toggleSorting(false), children: [_jsx(ArrowUp, { className: "size-3.5 text-muted-foreground" }), "Asc"] }), _jsxs(DropdownMenuItem, { onClick: () => column.toggleSorting(true), children: [_jsx(ArrowDown, { className: "size-3.5 text-muted-foreground" }), "Desc"] }), sorted ? (_jsxs(DropdownMenuItem, { onClick: () => column.clearSorting(), children: [_jsx(ChevronsUpDown, { className: "size-3.5 text-muted-foreground" }), "Unsorted"] })) : null, column.getCanHide() ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => column.toggleVisibility(false), children: [_jsx(EyeOff, { className: "size-3.5 text-muted-foreground" }), "Hide"] })] })) : null] })] }) }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One facet: a dashed "+ Title" button that opens a checklist of options
|
|
3
|
+
* with live counts. Picking several options ORs them; the table ANDs the
|
|
4
|
+
* facets. Options come from the data unless declared.
|
|
5
|
+
*/
|
|
6
|
+
import type { Column } from "@tanstack/react-table";
|
|
7
|
+
import { type DataTableFacetOption } from "./facets.js";
|
|
8
|
+
export interface DataTableFacetedFilterProps<T> {
|
|
9
|
+
column: Column<T, unknown>;
|
|
10
|
+
title?: string;
|
|
11
|
+
options?: DataTableFacetOption[];
|
|
12
|
+
/** Show a search box inside the list once there are more options than this (default 7). */
|
|
13
|
+
searchableFrom?: number;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function DataTableFacetedFilter<T>({ column, title, options: declared, searchableFrom, className, }: DataTableFacetedFilterProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Check, PlusCircle } from "lucide-react";
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover.js";
|
|
6
|
+
import { columnLabel, facetOptions, normalizeFacetValue, } from "./facets.js";
|
|
7
|
+
export function DataTableFacetedFilter({ column, title, options: declared, searchableFrom = 7, className, }) {
|
|
8
|
+
const [query, setQuery] = useState("");
|
|
9
|
+
const label = title ?? columnLabel(column);
|
|
10
|
+
const selected = normalizeFacetValue(column.getFilterValue());
|
|
11
|
+
const selectedSet = new Set(selected);
|
|
12
|
+
const options = facetOptions(column, declared);
|
|
13
|
+
const shown = useMemo(() => {
|
|
14
|
+
const needle = query.trim().toLowerCase();
|
|
15
|
+
return needle ? options.filter((o) => o.label.toLowerCase().includes(needle)) : options;
|
|
16
|
+
}, [options, query]);
|
|
17
|
+
const toggle = (value) => {
|
|
18
|
+
const next = selectedSet.has(value)
|
|
19
|
+
? selected.filter((item) => item !== value)
|
|
20
|
+
: [...selected, value];
|
|
21
|
+
column.setFilterValue(next.length ? next : undefined);
|
|
22
|
+
};
|
|
23
|
+
return (_jsxs(Popover, { onOpenChange: (open) => !open && setQuery(""), children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", "data-facet": column.id, "data-active": selected.length ? "" : undefined, className: cn("h-8 rounded-xl border-dashed border-border/60 bg-background/65 shadow-none hover:bg-muted/26", selected.length && "border-solid border-primary/30", className), children: [_jsx(PlusCircle, { className: "size-4" }), label, selected.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "mx-0.5 h-4 w-px bg-border/60", "aria-hidden": "true" }), _jsx("span", { className: "rounded-md bg-muted px-1.5 py-0.5 text-xs font-normal lg:hidden", children: selected.length }), _jsx("span", { className: "hidden gap-1 lg:flex", children: selected.length > 2 ? (_jsxs("span", { className: "rounded-md bg-muted px-1.5 py-0.5 text-xs font-normal", children: [selected.length, " selected"] })) : (options
|
|
24
|
+
.filter((option) => selectedSet.has(option.value))
|
|
25
|
+
.map((option) => (_jsx("span", { className: "rounded-md bg-muted px-1.5 py-0.5 text-xs font-normal", children: option.label }, option.value)))) })] })) : null] }) }), _jsxs(PopoverContent, { align: "start", className: "w-56 p-1.5", children: [options.length >= searchableFrom ? (_jsx("input", { value: query, onChange: (event) => setQuery(event.target.value), placeholder: label, "aria-label": `Search ${label} options`, className: "mb-1 h-8 w-full rounded-lg border border-border/45 bg-background/72 px-2 text-sm outline-none focus:border-primary/30" })) : null, _jsx("div", { role: "group", "aria-label": `${label} filter`, className: "max-h-72 space-y-0.5 overflow-y-auto", children: shown.length === 0 ? (_jsx("div", { className: "px-2 py-4 text-center text-xs text-muted-foreground", children: "No options." })) : (shown.map((option) => {
|
|
26
|
+
const checked = selectedSet.has(option.value);
|
|
27
|
+
const Icon = option.icon;
|
|
28
|
+
return (_jsxs("button", { type: "button", role: "checkbox", "aria-checked": checked, onClick: () => toggle(option.value), className: cn("flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors hover:bg-accent", option.count === 0 && !checked && "text-muted-foreground"), children: [_jsx("span", { className: cn("flex size-4 shrink-0 items-center justify-center rounded border border-border", checked
|
|
29
|
+
? "border-primary bg-primary text-primary-foreground"
|
|
30
|
+
: "opacity-60 [&_svg]:invisible"), children: _jsx(Check, { className: "size-3.5" }) }), Icon ? _jsx(Icon, { className: "size-4 text-muted-foreground" }) : null, _jsx("span", { className: "min-w-0 flex-1 truncate", children: option.label }), _jsx("span", { className: "ml-auto font-mono text-xs tabular-nums text-muted-foreground", children: option.count })] }, option.value));
|
|
31
|
+
})) }), selected.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "my-1 h-px bg-border/45" }), _jsx("button", { type: "button", onClick: () => column.setFilterValue(undefined), className: "w-full rounded-lg px-2 py-1.5 text-center text-sm hover:bg-accent", children: "Clear filter" })] })) : null] })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Selection summary, rows-per-page, page x of y, first/prev/next/last. */
|
|
2
|
+
import type { Table } from "@tanstack/react-table";
|
|
3
|
+
export interface DataTablePaginationProps<T> {
|
|
4
|
+
table: Table<T>;
|
|
5
|
+
pageSizeOptions?: number[];
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function DataTablePagination<T>({ table, pageSizeOptions, className, }: DataTablePaginationProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
|
|
3
|
+
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
4
|
+
import { Select } from "../../ui/select.js";
|
|
5
|
+
export function DataTablePagination({ table, pageSizeOptions = [10, 20, 50, 100], className, }) {
|
|
6
|
+
const total = table.getFilteredRowModel().rows.length;
|
|
7
|
+
const selected = table.getFilteredSelectedRowModel().rows.length;
|
|
8
|
+
const { pageIndex, pageSize } = table.getState().pagination;
|
|
9
|
+
const pageCount = Math.max(1, table.getPageCount());
|
|
10
|
+
const sizes = pageSizeOptions.includes(pageSize)
|
|
11
|
+
? pageSizeOptions
|
|
12
|
+
: [...pageSizeOptions, pageSize].sort((a, b) => a - b);
|
|
13
|
+
const iconButton = "size-8 rounded-lg border-border/45 bg-background/65 p-0 shadow-none hover:bg-muted/26";
|
|
14
|
+
return (_jsxs("div", { className: cn("flex flex-wrap items-center gap-x-6 gap-y-2 text-sm", className), children: [_jsx("div", { className: "flex-1 text-muted-foreground", "aria-live": "polite", children: selected > 0 ? `${selected} of ${total} row(s) selected.` : `${total} row(s)` }), _jsxs("label", { className: "flex items-center gap-2", children: [_jsx("span", { className: "font-medium", children: "Rows per page" }), _jsx(Select, { value: pageSize, onChange: (event) => table.setPageSize(Number(event.target.value)), className: "h-8 w-[4.5rem] rounded-lg px-2 py-0", children: sizes.map((size) => (_jsx("option", { value: size, children: size }, size))) })] }), _jsxs("div", { className: "font-medium", children: ["Page ", pageIndex + 1, " of ", pageCount] }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs(Button, { type: "button", variant: "outline", className: cn(iconButton, "hidden lg:inline-flex"), onClick: () => table.setPageIndex(0), disabled: !table.getCanPreviousPage(), children: [_jsx("span", { className: "sr-only", children: "Go to first page" }), _jsx(ChevronsLeft, { className: "size-4" })] }), _jsxs(Button, { type: "button", variant: "outline", className: iconButton, onClick: () => table.previousPage(), disabled: !table.getCanPreviousPage(), children: [_jsx("span", { className: "sr-only", children: "Go to previous page" }), _jsx(ChevronLeft, { className: "size-4" })] }), _jsxs(Button, { type: "button", variant: "outline", className: iconButton, onClick: () => table.nextPage(), disabled: !table.getCanNextPage(), children: [_jsx("span", { className: "sr-only", children: "Go to next page" }), _jsx(ChevronRight, { className: "size-4" })] }), _jsxs(Button, { type: "button", variant: "outline", className: cn(iconButton, "hidden lg:inline-flex"), onClick: () => table.setPageIndex(pageCount - 1), disabled: !table.getCanNextPage(), children: [_jsx("span", { className: "sr-only", children: "Go to last page" }), _jsx(ChevronsRight, { className: "size-4" })] })] })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
export interface DataTableRowAction {
|
|
3
|
+
label: string;
|
|
4
|
+
onSelect: () => void;
|
|
5
|
+
icon?: ComponentType<{
|
|
6
|
+
className?: string;
|
|
7
|
+
}>;
|
|
8
|
+
destructive?: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface DataTableRowActionsProps {
|
|
12
|
+
actions: Array<DataTableRowAction | "separator">;
|
|
13
|
+
label?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function DataTableRowActions({ actions, label, className, }: DataTableRowActionsProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** The "…" menu at the end of a row. Items are data, so agents and tests can read them. */
|
|
3
|
+
import { MoreHorizontal } from "lucide-react";
|
|
4
|
+
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
5
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu.js";
|
|
6
|
+
export function DataTableRowActions({ actions, label = "Open menu", className, }) {
|
|
7
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", "data-no-row-click": "", className: cn("size-8 rounded-lg p-0 data-[state=open]:bg-accent", className), children: [_jsx(MoreHorizontal, { className: "size-4" }), _jsx("span", { className: "sr-only", children: label })] }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-44 rounded-xl border-border/45 bg-popover/95 shadow-lg", children: actions.map((action, index) => action === "separator" ? (_jsx(DropdownMenuSeparator, {}, `sep-${index}`)) : (_jsxs(DropdownMenuItem, { disabled: action.disabled, onClick: (event) => {
|
|
8
|
+
event.stopPropagation();
|
|
9
|
+
action.onSelect();
|
|
10
|
+
}, className: cn(action.destructive && "text-destructive focus:text-destructive"), children: [action.icon ? _jsx(action.icon, { className: "size-4 text-muted-foreground" }) : null, action.label] }, action.label))) })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search + facets + Reset on the left, actions + View menu on the right.
|
|
3
|
+
* Facets are declared by column id; everything else is read off the table.
|
|
4
|
+
*/
|
|
5
|
+
import type { Table } from "@tanstack/react-table";
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
import type { DataTableFacetOption } from "./facets.js";
|
|
8
|
+
export interface DataTableFacet {
|
|
9
|
+
columnId: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
options?: DataTableFacetOption[];
|
|
12
|
+
}
|
|
13
|
+
export interface DataTableToolbarProps<T> {
|
|
14
|
+
table: Table<T>;
|
|
15
|
+
facets?: DataTableFacet[];
|
|
16
|
+
searchPlaceholder?: string;
|
|
17
|
+
/** Hide the search box. */
|
|
18
|
+
search?: boolean;
|
|
19
|
+
/** Hide the View (column visibility) menu. */
|
|
20
|
+
viewOptions?: boolean;
|
|
21
|
+
/** Extra controls rendered next to the facets (left side). */
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
/** Controls rendered on the right, before the View menu. */
|
|
24
|
+
actions?: ReactNode;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function DataTableToolbar<T>({ table, facets, searchPlaceholder, search, viewOptions, children, actions, className, }: DataTableToolbarProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Search, X } from "lucide-react";
|
|
3
|
+
import { Button, Input, cn } from "@iloveagents/foundry-web-primitives";
|
|
4
|
+
import { DataTableFacetedFilter } from "./data-table-faceted-filter.js";
|
|
5
|
+
import { DataTableViewOptions } from "./data-table-view-options.js";
|
|
6
|
+
export function DataTableToolbar({ table, facets = [], searchPlaceholder = "Search…", search = true, viewOptions = true, children, actions, className, }) {
|
|
7
|
+
const state = table.getState();
|
|
8
|
+
const isFiltered = state.columnFilters.length > 0 || Boolean(state.globalFilter);
|
|
9
|
+
return (_jsxs("div", { className: cn("flex flex-wrap items-center gap-2", className), children: [search ? (_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { type: "search", value: state.globalFilter ?? "", onChange: (event) => table.setGlobalFilter(event.target.value), placeholder: searchPlaceholder, "aria-label": searchPlaceholder, className: "h-8 w-[160px] rounded-xl border-border/45 bg-background/72 pl-8 text-sm shadow-none lg:w-[250px]" })] })) : null, facets.map((facet) => {
|
|
10
|
+
const column = table.getColumn(facet.columnId);
|
|
11
|
+
if (!column)
|
|
12
|
+
return null;
|
|
13
|
+
return (_jsx(DataTableFacetedFilter, { column: column, title: facet.title, options: facet.options }, facet.columnId));
|
|
14
|
+
}), children, isFiltered ? (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
|
|
15
|
+
table.resetColumnFilters();
|
|
16
|
+
table.setGlobalFilter("");
|
|
17
|
+
}, className: "h-8 rounded-xl px-2.5", children: ["Reset", _jsx(X, { className: "size-4" })] })) : null, actions || viewOptions ? (_jsxs("div", { className: "ml-auto flex items-center gap-2", children: [actions, viewOptions ? _jsx(DataTableViewOptions, { table: table }) : null] })) : null] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The "View" menu: toggle column visibility. */
|
|
2
|
+
import type { Table } from "@tanstack/react-table";
|
|
3
|
+
export interface DataTableViewOptionsProps<T> {
|
|
4
|
+
table: Table<T>;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function DataTableViewOptions<T>({ table, className }: DataTableViewOptionsProps<T>): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Settings2 } from "lucide-react";
|
|
3
|
+
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
4
|
+
import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../../ui/dropdown-menu.js";
|
|
5
|
+
import { columnLabel } from "./facets.js";
|
|
6
|
+
export function DataTableViewOptions({ table, className }) {
|
|
7
|
+
const columns = table.getAllLeafColumns().filter((column) => column.getCanHide());
|
|
8
|
+
if (!columns.length)
|
|
9
|
+
return null;
|
|
10
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: cn("h-8 rounded-xl border-border/45 bg-background/65 shadow-none hover:bg-muted/26", className), children: [_jsx(Settings2, { className: "size-4" }), "View"] }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-48 rounded-xl border-border/45 bg-popover/95 shadow-lg", children: [_jsx(DropdownMenuLabel, { children: "Toggle columns" }), _jsx(DropdownMenuSeparator, {}), columns.map((column) => (_jsx(DropdownMenuCheckboxItem, { checked: column.getIsVisible(), onCheckedChange: (checked) => column.toggleVisibility(Boolean(checked)), onSelect: (event) => event.preventDefault(), className: "capitalize", children: columnLabel(column) }, column.id)))] })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grid. Renders whatever a `useDataTable` instance says: header groups,
|
|
3
|
+
* flat rows, or group header rows with their expanded children. Row clicks
|
|
4
|
+
* ignore interactive descendants so a selection checkbox or a row menu
|
|
5
|
+
* never doubles as "open".
|
|
6
|
+
*/
|
|
7
|
+
import { type Row, type Table } from "@tanstack/react-table";
|
|
8
|
+
import type { KeyboardEvent, MouseEvent, ReactNode } from "react";
|
|
9
|
+
export interface DataTableProps<T> {
|
|
10
|
+
table: Table<T>;
|
|
11
|
+
onRowClick?: (row: Row<T>, event: MouseEvent | KeyboardEvent) => void;
|
|
12
|
+
/** The row whose details are open elsewhere (drawer, panel) — highlighted, `aria-current`. */
|
|
13
|
+
activeRowId?: string | null;
|
|
14
|
+
rowClassName?: (row: Row<T>) => string | undefined;
|
|
15
|
+
/** Content of a group header row (default: label and count). */
|
|
16
|
+
renderGroup?: (row: Row<T>) => ReactNode;
|
|
17
|
+
/** Declared order of group keys; unknown keys follow in data order. */
|
|
18
|
+
groupOrder?: string[];
|
|
19
|
+
emptyState?: ReactNode;
|
|
20
|
+
stickyHeader?: boolean;
|
|
21
|
+
dense?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
"aria-label"?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function DataTable<T>({ table, onRowClick, activeRowId, rowClassName, renderGroup, groupOrder, emptyState, stickyHeader, dense, className, "aria-label": ariaLabel, }: DataTableProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The grid. Renders whatever a `useDataTable` instance says: header groups,
|
|
4
|
+
* flat rows, or group header rows with their expanded children. Row clicks
|
|
5
|
+
* ignore interactive descendants so a selection checkbox or a row menu
|
|
6
|
+
* never doubles as "open".
|
|
7
|
+
*/
|
|
8
|
+
import { flexRender } from "@tanstack/react-table";
|
|
9
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
10
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
11
|
+
const INTERACTIVE = "a, button, input, select, textarea, [role='menuitem'], [data-no-row-click]";
|
|
12
|
+
function orderedRows(table, groupOrder) {
|
|
13
|
+
const rows = table.getRowModel().rows;
|
|
14
|
+
if (!table.getState().grouping.length)
|
|
15
|
+
return rows;
|
|
16
|
+
const top = rows.filter((row) => row.depth === 0);
|
|
17
|
+
const rank = new Map((groupOrder ?? []).map((key, index) => [key, index]));
|
|
18
|
+
const sorted = groupOrder
|
|
19
|
+
? [...top].sort((a, b) => {
|
|
20
|
+
const ra = rank.get(String(a.groupingValue ?? "")) ?? Number.MAX_SAFE_INTEGER;
|
|
21
|
+
const rb = rank.get(String(b.groupingValue ?? "")) ?? Number.MAX_SAFE_INTEGER;
|
|
22
|
+
return ra - rb;
|
|
23
|
+
})
|
|
24
|
+
: top;
|
|
25
|
+
return sorted.flatMap((row) => row.getIsGrouped() && row.getIsExpanded() ? [row, ...row.subRows] : [row]);
|
|
26
|
+
}
|
|
27
|
+
export function DataTable({ table, onRowClick, activeRowId, rowClassName, renderGroup, groupOrder, emptyState = "No results.", stickyHeader = false, dense = false, className, "aria-label": ariaLabel, }) {
|
|
28
|
+
const rows = orderedRows(table, groupOrder);
|
|
29
|
+
const columnCount = table.getVisibleLeafColumns().length;
|
|
30
|
+
const cellPad = dense ? "px-3 py-1.5" : "px-3 py-2.5";
|
|
31
|
+
const activate = (row, event) => {
|
|
32
|
+
if (!onRowClick)
|
|
33
|
+
return;
|
|
34
|
+
const target = event.target;
|
|
35
|
+
if (target?.closest(INTERACTIVE))
|
|
36
|
+
return;
|
|
37
|
+
onRowClick(row, event);
|
|
38
|
+
};
|
|
39
|
+
return (_jsx("div", { className: cn("overflow-hidden rounded-2xl border border-border/45 bg-card/60", className), children: _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full caption-bottom text-sm", "aria-label": ariaLabel, children: [_jsx("thead", { className: cn(stickyHeader && "sticky top-0 z-10 bg-card"), children: table.getHeaderGroups().map((headerGroup) => (_jsx("tr", { className: "border-b border-border/45", children: headerGroup.headers.map((header) => {
|
|
40
|
+
const meta = header.column.columnDef.meta;
|
|
41
|
+
return (_jsx("th", { colSpan: header.colSpan, scope: "col", style: header.getSize() !== 150 ? { width: header.getSize() } : undefined, className: cn("h-10 px-3 text-left align-middle text-xs font-medium text-muted-foreground", meta?.align === "right" && "text-right", meta?.align === "center" && "text-center", meta?.headerClassName), children: header.isPlaceholder
|
|
42
|
+
? null
|
|
43
|
+
: flexRender(header.column.columnDef.header, header.getContext()) }, header.id));
|
|
44
|
+
}) }, headerGroup.id))) }), _jsx("tbody", { children: rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columnCount, className: "h-24 text-center text-sm text-muted-foreground", children: emptyState }) })) : (rows.map((row) => row.getIsGrouped() ? (_jsx("tr", { "data-group-row": "", className: "border-t border-border/45 bg-muted/35 text-sm font-medium", children: _jsx("td", { colSpan: columnCount, className: "px-3 py-1.5", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", onClick: row.getToggleExpandedHandler(), "aria-expanded": row.getIsExpanded(), "aria-label": row.getIsExpanded() ? "Collapse group" : "Expand group", className: "rounded-md p-0.5 text-muted-foreground hover:bg-accent hover:text-foreground", children: row.getIsExpanded() ? (_jsx(ChevronDown, { className: "size-4" })) : (_jsx(ChevronRight, { className: "size-4" })) }), _jsx("div", { className: "min-w-0 flex-1", children: renderGroup ? renderGroup(row) : _jsx(DefaultGroupLabel, { row: row }) })] }) }) }, row.id)) : (_jsx("tr", { "data-row-id": row.id, "data-state": row.getIsSelected() ? "selected" : undefined, "data-active": activeRowId != null && row.id === activeRowId ? "" : undefined, "aria-current": activeRowId != null && row.id === activeRowId ? "true" : undefined, "aria-selected": row.getIsSelected() || undefined, tabIndex: onRowClick ? 0 : undefined, onClick: (event) => activate(row, event), onKeyDown: (event) => {
|
|
45
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
46
|
+
if (event.target.closest(INTERACTIVE))
|
|
47
|
+
return;
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
activate(row, event);
|
|
50
|
+
}
|
|
51
|
+
}, className: cn("border-t border-border/35 transition-colors", onRowClick &&
|
|
52
|
+
"cursor-pointer hover:bg-muted/30 focus-visible:bg-muted/30 focus-visible:outline-none", "data-[state=selected]:bg-primary/6 data-[active]:bg-primary/8", rowClassName?.(row)), children: row.getVisibleCells().map((cell) => {
|
|
53
|
+
const meta = cell.column.columnDef.meta;
|
|
54
|
+
return (_jsx("td", { className: cn(cellPad, "align-middle", meta?.align === "right" && "text-right", meta?.align === "center" && "text-center", meta?.className), children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
|
|
55
|
+
}) }, row.id)))) })] }) }) }));
|
|
56
|
+
}
|
|
57
|
+
function DefaultGroupLabel({ row }) {
|
|
58
|
+
const grouping = row
|
|
59
|
+
.getAllCells()
|
|
60
|
+
.map((cell) => cell.column)
|
|
61
|
+
.find((column) => column.getIsGrouped());
|
|
62
|
+
const key = String(row.groupingValue ?? "");
|
|
63
|
+
const label = grouping ? (grouping.columnDef.meta?.facetLabels?.[key] ?? key) : key;
|
|
64
|
+
return (_jsxs("span", { children: [label || "None", " ", _jsxs("span", { className: "font-normal text-muted-foreground", children: ["(", row.subRows.length, ")"] })] }));
|
|
65
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facet semantics shared by every DataTable: how a cell value turns into
|
|
3
|
+
* facet keys, how a facet filter matches (OR within one facet — the table
|
|
4
|
+
* ANDs facets together), and how option counts are derived from the rows
|
|
5
|
+
* that every *other* filter lets through (TanStack's faceted row model), so
|
|
6
|
+
* a count always reads "rows you'd see if you added this option".
|
|
7
|
+
*/
|
|
8
|
+
import type { Column, FilterFn, Row, RowData } from "@tanstack/react-table";
|
|
9
|
+
import type { ComponentType } from "react";
|
|
10
|
+
declare module "@tanstack/react-table" {
|
|
11
|
+
interface ColumnMeta<TData extends RowData, TValue> {
|
|
12
|
+
/** Label for the View menu, facet titles and agent descriptions (defaults to a string header, then the id). */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Class for body cells of this column. */
|
|
15
|
+
className?: string;
|
|
16
|
+
/** Class for the header cell of this column. */
|
|
17
|
+
headerClassName?: string;
|
|
18
|
+
/** Horizontal alignment of header and cells. */
|
|
19
|
+
align?: "left" | "center" | "right";
|
|
20
|
+
/** Text the global search sees for this column instead of the cell value. */
|
|
21
|
+
searchText?: (row: TData) => string;
|
|
22
|
+
/** Labels for facet keys and group headers (`value → label`). */
|
|
23
|
+
facetLabels?: Record<string, string>;
|
|
24
|
+
/** Keep unused type parameter referenced for declaration merging. */
|
|
25
|
+
__cellValue?: TValue;
|
|
26
|
+
/** Hosts may carry their own keys in `meta` (e.g. a server sort key). */
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Facet key for empty cells (`null`, `undefined`, `""`, `[]`). */
|
|
31
|
+
export declare const FACET_EMPTY = "__none__";
|
|
32
|
+
export interface DataTableFacetOption {
|
|
33
|
+
value: string;
|
|
34
|
+
label: string;
|
|
35
|
+
icon?: ComponentType<{
|
|
36
|
+
className?: string;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export interface DataTableFacetCount extends DataTableFacetOption {
|
|
40
|
+
count: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Keys a cell value contributes to a facet — arrays contribute one key per
|
|
44
|
+
* element. A real value that spells the empty sentinel is escaped with a
|
|
45
|
+
* leading backslash so it never masquerades as "no value".
|
|
46
|
+
*/
|
|
47
|
+
export declare function facetKeys(value: unknown): string[];
|
|
48
|
+
/** A facet filter value is a list of selected keys; anything else means "no filter". */
|
|
49
|
+
export declare function normalizeFacetValue(filterValue: unknown): string[];
|
|
50
|
+
/**
|
|
51
|
+
* OR within a facet: a row passes when any of its keys is selected. Typed
|
|
52
|
+
* loosely so `filterFn: facetFilterFn` fits any row type; `useDataTable`
|
|
53
|
+
* also registers it as the string `"facet"` for untyped (JSX) columns.
|
|
54
|
+
*/
|
|
55
|
+
export declare const facetFilterFn: FilterFn<any>;
|
|
56
|
+
/** Rows per facet key, counted over the rows every OTHER filter lets through. */
|
|
57
|
+
export declare function facetCounts<T>(column: Column<T, unknown>): Map<string, number>;
|
|
58
|
+
export interface FacetOptionsOptions {
|
|
59
|
+
/** Label for keys that are neither declared nor in `meta.facetLabels`. */
|
|
60
|
+
labelFor?: (key: string) => string;
|
|
61
|
+
/** Label for the empty-cell option (default "None"). */
|
|
62
|
+
emptyLabel?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Declared options first (in declared order, kept even at count 0), then
|
|
66
|
+
* every undeclared key seen in the data, then the empty option when some
|
|
67
|
+
* rows have no value.
|
|
68
|
+
*/
|
|
69
|
+
export declare function facetOptions<T>(column: Column<T, unknown>, declared?: DataTableFacetOption[], options?: FacetOptionsOptions): DataTableFacetCount[];
|
|
70
|
+
/** Plain-text projection of a cell value for search and agent read-outs. */
|
|
71
|
+
export declare function cellText(value: unknown): string;
|
|
72
|
+
/**
|
|
73
|
+
* Lower-cased text of every searchable cell of a row. Cached per row object
|
|
74
|
+
* and invalidated when the row's cells change identity (new columns or a
|
|
75
|
+
* changed `meta.searchText`), since TanStack keeps row objects across
|
|
76
|
+
* column changes.
|
|
77
|
+
*/
|
|
78
|
+
export declare function rowSearchText<T>(row: Row<T>): string;
|
|
79
|
+
/** Every whitespace-separated token must appear somewhere in the row (AND across tokens). */
|
|
80
|
+
export declare const tokenSearchFilterFn: FilterFn<any>;
|
|
81
|
+
/** Human label of a column: `meta.label`, then a string header, then the id. */
|
|
82
|
+
export declare function columnLabel<T>(column: Column<T, unknown>): string;
|