@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.
- 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
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The list width at which each band starts earning its columns. Measured on
|
|
3
|
+
* the list itself, not the viewport — a list in a side pane is narrow even on
|
|
4
|
+
* a wide screen. Exported so a host that derives breakpoints from a schema
|
|
5
|
+
* measures against the same numbers the table renders by.
|
|
6
|
+
*/
|
|
7
|
+
export const DATA_TABLE_MIN_WIDTH = {
|
|
8
|
+
sm: 448,
|
|
9
|
+
md: 672,
|
|
10
|
+
lg: 896,
|
|
11
|
+
xl: 1024,
|
|
12
|
+
"2xl": 1280,
|
|
13
|
+
};
|
|
1
14
|
/** Facet key for empty cells (`null`, `undefined`, `""`, `[]`). */
|
|
2
15
|
export const FACET_EMPTY = "__none__";
|
|
3
16
|
/**
|
|
@@ -5,6 +18,16 @@ export const FACET_EMPTY = "__none__";
|
|
|
5
18
|
* element. A real value that spells the empty sentinel is escaped with a
|
|
6
19
|
* leading backslash so it never masquerades as "no value".
|
|
7
20
|
*/
|
|
21
|
+
export function columnFacetKeys(column, value) {
|
|
22
|
+
const buckets = column.columnDef.meta?.facetBuckets;
|
|
23
|
+
if (!buckets)
|
|
24
|
+
return facetKeys(value);
|
|
25
|
+
// Unique, like `facetKeys` is for arrays: a bucket rule that overlaps
|
|
26
|
+
// itself would otherwise count one row twice and report a facet with more
|
|
27
|
+
// matches than there are rows.
|
|
28
|
+
const keys = Array.from(new Set(buckets(value).filter((key) => key !== "")));
|
|
29
|
+
return keys.length ? keys : [FACET_EMPTY];
|
|
30
|
+
}
|
|
8
31
|
export function facetKeys(value) {
|
|
9
32
|
if (value == null || value === "")
|
|
10
33
|
return [FACET_EMPTY];
|
|
@@ -21,6 +44,21 @@ export function facetKeys(value) {
|
|
|
21
44
|
}
|
|
22
45
|
return [String(value)];
|
|
23
46
|
}
|
|
47
|
+
// TanStack memoizes a row's cells, so the array identity is a safe cache key:
|
|
48
|
+
// the id → column map is built once per row instead of scanned once per row
|
|
49
|
+
// per active filter (a scan that is O(columns) on every pass over the data).
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
const rowColumnCache = new WeakMap();
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
function columnOfRow(row, columnId) {
|
|
54
|
+
const cells = row.getAllCells();
|
|
55
|
+
let byId = rowColumnCache.get(cells);
|
|
56
|
+
if (!byId) {
|
|
57
|
+
byId = new Map(cells.map((cell) => [cell.column.id, cell.column]));
|
|
58
|
+
rowColumnCache.set(cells, byId);
|
|
59
|
+
}
|
|
60
|
+
return byId.get(columnId);
|
|
61
|
+
}
|
|
24
62
|
/** A facet filter value is a list of selected keys; anything else means "no filter". */
|
|
25
63
|
export function normalizeFacetValue(filterValue) {
|
|
26
64
|
if (Array.isArray(filterValue))
|
|
@@ -39,14 +77,18 @@ export const facetFilterFn = (row, columnId, filterValue) => {
|
|
|
39
77
|
const wanted = normalizeFacetValue(filterValue);
|
|
40
78
|
if (!wanted.length)
|
|
41
79
|
return true;
|
|
42
|
-
|
|
80
|
+
const column = columnOfRow(row, columnId);
|
|
81
|
+
const keys = column
|
|
82
|
+
? columnFacetKeys(column, row.getValue(columnId))
|
|
83
|
+
: facetKeys(row.getValue(columnId));
|
|
84
|
+
return keys.some((key) => wanted.includes(key));
|
|
43
85
|
};
|
|
44
86
|
facetFilterFn.autoRemove = (value) => normalizeFacetValue(value).length === 0;
|
|
45
87
|
/** Rows per facet key, counted over the rows every OTHER filter lets through. */
|
|
46
88
|
export function facetCounts(column) {
|
|
47
89
|
const counts = new Map();
|
|
48
90
|
for (const row of column.getFacetedRowModel().flatRows) {
|
|
49
|
-
for (const key of
|
|
91
|
+
for (const key of columnFacetKeys(column, row.getValue(column.id))) {
|
|
50
92
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
51
93
|
}
|
|
52
94
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ColumnDef, type FilterFn, type Row, type Table } from "@tanstack/react-table";
|
|
1
|
+
import { type ColumnDef, type FilterFn, type Row, type Table, type VisibilityState } from "@tanstack/react-table";
|
|
2
2
|
import { type DataTableState } from "./state.js";
|
|
3
3
|
export interface UseDataTableOptions<T> {
|
|
4
4
|
data: T[];
|
|
@@ -19,4 +19,16 @@ export interface UseDataTableOptions<T> {
|
|
|
19
19
|
/** Replace the default AND-of-tokens search. */
|
|
20
20
|
globalFilterFn?: FilterFn<T>;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* `{ id: false }` for every column whose `meta.defaultHidden` says it starts
|
|
24
|
+
* out of the way — the leaves, since visibility is a leaf property and a
|
|
25
|
+
* grouping header has no state to hide.
|
|
26
|
+
*
|
|
27
|
+
* `useDataTable` applies this to its own initial state. A list whose state
|
|
28
|
+
* lives somewhere else (the URL) must pass it as part of that layer's
|
|
29
|
+
* `defaults` instead: re-applying it on every render would undo the user the
|
|
30
|
+
* moment they bring a column out, because "visible" is exactly what a
|
|
31
|
+
* hidden-only URL does not record.
|
|
32
|
+
*/
|
|
33
|
+
export declare function defaultHiddenColumns<T>(columns: readonly ColumnDef<T, any>[]): VisibilityState;
|
|
22
34
|
export declare function useDataTable<T>(options: UseDataTableOptions<T>): Table<T>;
|
|
@@ -9,13 +9,60 @@ import { getCoreRowModel, getExpandedRowModel, getFacetedRowModel, getFacetedUni
|
|
|
9
9
|
import { facetFilterFn, tokenSearchFilterFn } from "./facets.js";
|
|
10
10
|
import { DEFAULT_PAGE_SIZE, EMPTY_DATA_TABLE_STATE } from "./state.js";
|
|
11
11
|
const PAGE_RESET_KEYS = new Set(["globalFilter", "columnFilters", "sorting"]);
|
|
12
|
+
/**
|
|
13
|
+
* TanStack's own rule for a column's id, mirrored so `meta.defaultHidden` can
|
|
14
|
+
* be resolved before the table exists: an explicit `id`, else the
|
|
15
|
+
* `accessorKey` with EVERY dot turned into an underscore (`user.profile.name`
|
|
16
|
+
* → `user_profile_name`), else a string header. Anything else has no stable
|
|
17
|
+
* id to key visibility by.
|
|
18
|
+
*/
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
function columnId(column) {
|
|
21
|
+
if (column.id)
|
|
22
|
+
return column.id;
|
|
23
|
+
const accessorKey = "accessorKey" in column ? column.accessorKey : undefined;
|
|
24
|
+
if (accessorKey != null)
|
|
25
|
+
return String(accessorKey).replace(/\./g, "_");
|
|
26
|
+
return typeof column.header === "string" ? column.header : undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `{ id: false }` for every column whose `meta.defaultHidden` says it starts
|
|
30
|
+
* out of the way — the leaves, since visibility is a leaf property and a
|
|
31
|
+
* grouping header has no state to hide.
|
|
32
|
+
*
|
|
33
|
+
* `useDataTable` applies this to its own initial state. A list whose state
|
|
34
|
+
* lives somewhere else (the URL) must pass it as part of that layer's
|
|
35
|
+
* `defaults` instead: re-applying it on every render would undo the user the
|
|
36
|
+
* moment they bring a column out, because "visible" is exactly what a
|
|
37
|
+
* hidden-only URL does not record.
|
|
38
|
+
*/
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
export function defaultHiddenColumns(columns) {
|
|
41
|
+
const hidden = {};
|
|
42
|
+
const walk = (defs) => {
|
|
43
|
+
for (const column of defs) {
|
|
44
|
+
const children = "columns" in column ? column.columns : undefined;
|
|
45
|
+
if (children?.length) {
|
|
46
|
+
walk(children);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const id = columnId(column);
|
|
50
|
+
if (id && column.meta?.defaultHidden)
|
|
51
|
+
hidden[id] = false;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
walk(columns);
|
|
55
|
+
return hidden;
|
|
56
|
+
}
|
|
12
57
|
export function useDataTable(options) {
|
|
13
58
|
const { data, columns, getRowId, state: controlledState, onStateChange, initialState, pageSize, groupBy, enableRowSelection = false, enableMultiRowSelection = true, globalFilterFn, } = options;
|
|
14
59
|
const controlled = controlledState !== undefined && onStateChange !== undefined;
|
|
15
60
|
const basePageSize = pageSize || DEFAULT_PAGE_SIZE;
|
|
61
|
+
const defaultHidden = useMemo(() => defaultHiddenColumns(columns), [columns]);
|
|
16
62
|
const [innerState, setInnerState] = useState(() => ({
|
|
17
63
|
...EMPTY_DATA_TABLE_STATE,
|
|
18
64
|
...initialState,
|
|
65
|
+
columnVisibility: { ...defaultHidden, ...initialState?.columnVisibility },
|
|
19
66
|
pagination: { pageIndex: 0, pageSize: basePageSize, ...initialState?.pagination },
|
|
20
67
|
}));
|
|
21
68
|
const state = useMemo(() => controlled
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The measured width of an element, or `undefined` until it has one. Lists
|
|
3
|
+
* use it to decide which columns fit: a table beside an open detail pane is
|
|
4
|
+
* narrower than the window, and only the table knows by how much.
|
|
5
|
+
*/
|
|
6
|
+
import { type RefObject } from "react";
|
|
7
|
+
export declare function useElementWidth(ref: RefObject<HTMLElement | null>): number | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The measured width of an element, or `undefined` until it has one. Lists
|
|
3
|
+
* use it to decide which columns fit: a table beside an open detail pane is
|
|
4
|
+
* narrower than the window, and only the table knows by how much.
|
|
5
|
+
*/
|
|
6
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
7
|
+
export function useElementWidth(ref) {
|
|
8
|
+
const [width, setWidth] = useState(undefined);
|
|
9
|
+
// The ref OBJECT is stable while the node it holds may not be: a caller
|
|
10
|
+
// that swaps the element keeps the same ref, and an observer bound once
|
|
11
|
+
// would go on measuring the detached node forever. So the element itself
|
|
12
|
+
// is state, refreshed after every render that changed it.
|
|
13
|
+
const [element, setElement] = useState(null);
|
|
14
|
+
const seen = useRef(null);
|
|
15
|
+
useLayoutEffect(() => {
|
|
16
|
+
if (ref.current === seen.current)
|
|
17
|
+
return;
|
|
18
|
+
seen.current = ref.current;
|
|
19
|
+
setElement(ref.current);
|
|
20
|
+
});
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!element || typeof ResizeObserver === "undefined")
|
|
23
|
+
return;
|
|
24
|
+
const read = () => {
|
|
25
|
+
const next = element.getBoundingClientRect().width;
|
|
26
|
+
// A zero width means "not laid out yet" (or a test environment); treat
|
|
27
|
+
// it as unmeasured so nothing is hidden on a guess.
|
|
28
|
+
setWidth((current) => (next > 0 && Math.abs((current ?? -1) - next) > 0.5 ? next : current));
|
|
29
|
+
};
|
|
30
|
+
const observer = new ResizeObserver(read);
|
|
31
|
+
observer.observe(element);
|
|
32
|
+
read();
|
|
33
|
+
return () => observer.disconnect();
|
|
34
|
+
}, [element]);
|
|
35
|
+
return width;
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface PinnedOffsets {
|
|
2
|
+
/** `columnId → left offset in px`, for the columns that are pinned. */
|
|
3
|
+
offsets: Record<string, number>;
|
|
4
|
+
/** The id of the last pinned column, which carries the edge shadow. */
|
|
5
|
+
lastPinned: string | null;
|
|
6
|
+
/** Attach to each pinned header cell so its width is tracked. */
|
|
7
|
+
measure: (columnId: string) => (element: HTMLElement | null) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function usePinnedOffsets(pinnedIds: readonly string[]): PinnedOffsets;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Left offsets for pinned columns. A pinned column sticks to the left edge
|
|
3
|
+
* of the scroll container; the ones after it stack behind their predecessors,
|
|
4
|
+
* so each needs the summed width of the pinned columns before it. Widths are
|
|
5
|
+
* measured from the rendered header cells (a column's declared `size` is a
|
|
6
|
+
* hint, not the used width) and kept current with a `ResizeObserver`.
|
|
7
|
+
*/
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
export function usePinnedOffsets(pinnedIds) {
|
|
10
|
+
const elements = useRef(new Map());
|
|
11
|
+
const [widths, setWidths] = useState({});
|
|
12
|
+
const key = pinnedIds.join(",");
|
|
13
|
+
const read = useCallback(() => {
|
|
14
|
+
setWidths((current) => {
|
|
15
|
+
let changed = false;
|
|
16
|
+
const next = {};
|
|
17
|
+
for (const [id, element] of elements.current) {
|
|
18
|
+
const width = element.getBoundingClientRect().width;
|
|
19
|
+
next[id] = width;
|
|
20
|
+
if (Math.abs((current[id] ?? -1) - width) > 0.5)
|
|
21
|
+
changed = true;
|
|
22
|
+
}
|
|
23
|
+
if (!changed && Object.keys(next).length === Object.keys(current).length)
|
|
24
|
+
return current;
|
|
25
|
+
return next;
|
|
26
|
+
});
|
|
27
|
+
}, []);
|
|
28
|
+
const observerRef = useRef(null);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (typeof ResizeObserver === "undefined")
|
|
31
|
+
return;
|
|
32
|
+
const observer = new ResizeObserver(read);
|
|
33
|
+
observerRef.current = observer;
|
|
34
|
+
for (const element of elements.current.values())
|
|
35
|
+
observer.observe(element);
|
|
36
|
+
read();
|
|
37
|
+
return () => {
|
|
38
|
+
observer.disconnect();
|
|
39
|
+
observerRef.current = null;
|
|
40
|
+
};
|
|
41
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
+
}, [key, read]);
|
|
43
|
+
// One stable callback per column: a fresh ref callback on every render
|
|
44
|
+
// would make React detach and re-attach the ref each time.
|
|
45
|
+
const callbacks = useRef(new Map());
|
|
46
|
+
const measure = useCallback((columnId) => {
|
|
47
|
+
const existing = callbacks.current.get(columnId);
|
|
48
|
+
if (existing)
|
|
49
|
+
return existing;
|
|
50
|
+
const callback = (element) => {
|
|
51
|
+
const previous = elements.current.get(columnId);
|
|
52
|
+
if (previous === element)
|
|
53
|
+
return;
|
|
54
|
+
if (previous)
|
|
55
|
+
observerRef.current?.unobserve(previous);
|
|
56
|
+
if (element) {
|
|
57
|
+
elements.current.set(columnId, element);
|
|
58
|
+
observerRef.current?.observe(element);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
elements.current.delete(columnId);
|
|
62
|
+
}
|
|
63
|
+
read();
|
|
64
|
+
};
|
|
65
|
+
callbacks.current.set(columnId, callback);
|
|
66
|
+
return callback;
|
|
67
|
+
}, [read]);
|
|
68
|
+
const offsets = {};
|
|
69
|
+
let running = 0;
|
|
70
|
+
for (const id of pinnedIds) {
|
|
71
|
+
offsets[id] = running;
|
|
72
|
+
running += widths[id] ?? 0;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
offsets,
|
|
76
|
+
lastPinned: pinnedIds.length ? pinnedIds[pinnedIds.length - 1] : null,
|
|
77
|
+
measure,
|
|
78
|
+
};
|
|
79
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export { InfiniteScrollSentinel } from "./components/infinite-scroll-sentinel.js
|
|
|
35
35
|
export { CollectionSurface } from "./components/collection-surface.js";
|
|
36
36
|
export { DataTable, type DataTableProps } from "./components/data-table/data-table.js";
|
|
37
37
|
export { DataTableFrame, useDataTableFrame, type DataTableFrameProps, type DataTableFrameContextValue, } from "./components/data-table/data-table-frame.js";
|
|
38
|
+
export { DataTableSelectionBar, type DataTableSelectionBarProps, } from "./components/data-table/data-table-selection-bar.js";
|
|
38
39
|
export { DataTableToolbar, visibleFacets, type DataTableFacet, type DataTableToolbarLayout, type DataTableToolbarProps, } from "./components/data-table/data-table-toolbar.js";
|
|
39
40
|
export { DataTableFacetedFilter, type DataTableFacetedFilterProps, } from "./components/data-table/data-table-faceted-filter.js";
|
|
40
41
|
export { DataTableViewOptions, type DataTableViewOptionsProps, } from "./components/data-table/data-table-view-options.js";
|
|
@@ -42,9 +43,12 @@ export { DataTableColumnHeader, type DataTableColumnHeaderProps, } from "./compo
|
|
|
42
43
|
export { DataTablePagination, type DataTablePaginationProps, } from "./components/data-table/data-table-pagination.js";
|
|
43
44
|
export { DataTableRowActions, type DataTableRowAction, type DataTableRowActionsProps, } from "./components/data-table/data-table-row-actions.js";
|
|
44
45
|
export { selectionColumn, SELECTION_COLUMN_ID } from "./components/data-table/selection-column.js";
|
|
45
|
-
export { useDataTable, type UseDataTableOptions } from "./components/data-table/use-data-table.js";
|
|
46
|
+
export { useDataTable, defaultHiddenColumns, type UseDataTableOptions, } from "./components/data-table/use-data-table.js";
|
|
47
|
+
export { usePinnedOffsets, type PinnedOffsets, } from "./components/data-table/use-pinned-offsets.js";
|
|
48
|
+
export { useElementWidth } from "./components/data-table/use-element-width.js";
|
|
46
49
|
export { DEFAULT_PAGE_SIZE, EMPTY_DATA_TABLE_STATE, isDataTableUrlKey, mergeDataTableUrlState, parseDataTableState, serializeDataTableState, type DataTableState, type DataTableUrlOptions, } from "./components/data-table/state.js";
|
|
47
|
-
export {
|
|
50
|
+
export { ageBucket, ageBuckets, dueState, parseDateish, relativeAge, type AgeBucket, type DateBucketOptions, type DueState, type DueStateOptions, } from "./components/data-table/date-buckets.js";
|
|
51
|
+
export { DATA_TABLE_MIN_WIDTH, FACET_EMPTY, cellText, columnFacetKeys, columnLabel, facetCounts, facetFilterFn, facetKeys, facetOptions, normalizeFacetValue, rowSearchText, tokenSearchFilterFn, type DataTableBreakpoint, type DataTableFacetCount, type DataTableFacetOption, } from "./components/data-table/facets.js";
|
|
48
52
|
export { createColumnHelper, flexRender, type Column, type ColumnDef, type ColumnFiltersState, type Row, type RowSelectionState, type SortingState, type Table, type VisibilityState, } from "@tanstack/react-table";
|
|
49
53
|
export { ThemeRuntimeProvider, ThemeScope, ThemeDocumentMetadata, useThemeRuntime, } from "./components/theme-runtime-provider.js";
|
|
50
54
|
export { JsonViewer } from "./components/json-viewer.js";
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ export { InfiniteScrollSentinel } from "./components/infinite-scroll-sentinel.js
|
|
|
33
33
|
export { CollectionSurface } from "./components/collection-surface.js";
|
|
34
34
|
export { DataTable } from "./components/data-table/data-table.js";
|
|
35
35
|
export { DataTableFrame, useDataTableFrame, } from "./components/data-table/data-table-frame.js";
|
|
36
|
+
export { DataTableSelectionBar, } from "./components/data-table/data-table-selection-bar.js";
|
|
36
37
|
export { DataTableToolbar, visibleFacets, } from "./components/data-table/data-table-toolbar.js";
|
|
37
38
|
export { DataTableFacetedFilter, } from "./components/data-table/data-table-faceted-filter.js";
|
|
38
39
|
export { DataTableViewOptions, } from "./components/data-table/data-table-view-options.js";
|
|
@@ -40,9 +41,12 @@ export { DataTableColumnHeader, } from "./components/data-table/data-table-colum
|
|
|
40
41
|
export { DataTablePagination, } from "./components/data-table/data-table-pagination.js";
|
|
41
42
|
export { DataTableRowActions, } from "./components/data-table/data-table-row-actions.js";
|
|
42
43
|
export { selectionColumn, SELECTION_COLUMN_ID } from "./components/data-table/selection-column.js";
|
|
43
|
-
export { useDataTable } from "./components/data-table/use-data-table.js";
|
|
44
|
+
export { useDataTable, defaultHiddenColumns, } from "./components/data-table/use-data-table.js";
|
|
45
|
+
export { usePinnedOffsets, } from "./components/data-table/use-pinned-offsets.js";
|
|
46
|
+
export { useElementWidth } from "./components/data-table/use-element-width.js";
|
|
44
47
|
export { DEFAULT_PAGE_SIZE, EMPTY_DATA_TABLE_STATE, isDataTableUrlKey, mergeDataTableUrlState, parseDataTableState, serializeDataTableState, } from "./components/data-table/state.js";
|
|
45
|
-
export {
|
|
48
|
+
export { ageBucket, ageBuckets, dueState, parseDateish, relativeAge, } from "./components/data-table/date-buckets.js";
|
|
49
|
+
export { DATA_TABLE_MIN_WIDTH, FACET_EMPTY, cellText, columnFacetKeys, columnLabel, facetCounts, facetFilterFn, facetKeys, facetOptions, normalizeFacetValue, rowSearchText, tokenSearchFilterFn, } from "./components/data-table/facets.js";
|
|
46
50
|
export { createColumnHelper, flexRender, } from "@tanstack/react-table";
|
|
47
51
|
export { ThemeRuntimeProvider, ThemeScope, ThemeDocumentMetadata, useThemeRuntime, } from "./components/theme-runtime-provider.js";
|
|
48
52
|
export { JsonViewer } from "./components/json-viewer.js";
|
package/dist/lib/app-store.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export type ContextPayload = TextPayload | PagePayload | ReferencePayload;
|
|
|
51
51
|
export type ContextItemType = "selection" | "page" | "ref" | "note";
|
|
52
52
|
export interface ContextItem {
|
|
53
53
|
id: string;
|
|
54
|
+
/** Set for context that tracks the UI (a list's selection): one item per key. */
|
|
55
|
+
key?: string;
|
|
54
56
|
type: ContextItemType;
|
|
55
57
|
label: string;
|
|
56
58
|
payload: ContextPayload;
|
|
@@ -103,7 +105,15 @@ interface AppState {
|
|
|
103
105
|
clearHighlights: () => void;
|
|
104
106
|
contextItems: ContextItem[];
|
|
105
107
|
sentContext: Record<string, ContextItem[]>;
|
|
106
|
-
|
|
108
|
+
/** Context the user pinned. Keyed (UI-tracked) items go through
|
|
109
|
+
* `setContextItem`, which is the only thing that replaces by key. */
|
|
110
|
+
addContextItem: (item: Omit<ContextItem, "id" | "createdAt" | "key">) => void;
|
|
111
|
+
/**
|
|
112
|
+
* Context that tracks a piece of UI rather than a user's pin — the rows
|
|
113
|
+
* selected in a list, say. One item per `key`: setting replaces, `null`
|
|
114
|
+
* clears. Without this, a chip that changes with the UI would pile up.
|
|
115
|
+
*/
|
|
116
|
+
setContextItem: (key: string, item: Omit<ContextItem, "id" | "createdAt"> | null) => void;
|
|
107
117
|
removeContextItem: (id: string) => void;
|
|
108
118
|
promoteContextItem: (id: string) => void;
|
|
109
119
|
clearAllContext: () => void;
|
package/dist/lib/app-store.js
CHANGED
|
@@ -60,10 +60,15 @@ export const useAppStore = create((set, get) => ({
|
|
|
60
60
|
const prev = get().currentPage;
|
|
61
61
|
if (prev === page)
|
|
62
62
|
return;
|
|
63
|
-
// Clear ephemeral context on navigation
|
|
63
|
+
// Clear ephemeral context on navigation — and every KEYED item, whatever
|
|
64
|
+
// its persistence: those track a piece of UI, and that UI is gone. A pin
|
|
65
|
+
// the user made survives, because promoting an item drops its key.
|
|
64
66
|
set({
|
|
65
67
|
currentPage: page,
|
|
66
|
-
contextItems: get().contextItems.filter(
|
|
68
|
+
contextItems: get().contextItems.filter(
|
|
69
|
+
// `key === undefined`, not falsy: "" is a key someone chose, and a
|
|
70
|
+
// keyed item tracks UI that this navigation just unmounted.
|
|
71
|
+
(i) => i.persistence !== "ephemeral" && i.key === undefined),
|
|
67
72
|
});
|
|
68
73
|
},
|
|
69
74
|
// Navigation context
|
|
@@ -118,9 +123,36 @@ export const useAppStore = create((set, get) => ({
|
|
|
118
123
|
],
|
|
119
124
|
});
|
|
120
125
|
},
|
|
126
|
+
setContextItem: (key, item) => {
|
|
127
|
+
const withoutKey = get().contextItems.filter((existing) => existing.key !== key);
|
|
128
|
+
if (!item) {
|
|
129
|
+
if (withoutKey.length !== get().contextItems.length)
|
|
130
|
+
set({ contextItems: withoutKey });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (withoutKey.length >= MAX_ITEMS)
|
|
134
|
+
return;
|
|
135
|
+
const label = item.label.length > MAX_LABEL_LENGTH
|
|
136
|
+
? `${item.label.slice(0, MAX_LABEL_LENGTH)}...`
|
|
137
|
+
: item.label;
|
|
138
|
+
set({
|
|
139
|
+
contextItems: [
|
|
140
|
+
...withoutKey,
|
|
141
|
+
{ ...item, key, label, id: crypto.randomUUID(), createdAt: Date.now() },
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
},
|
|
121
145
|
removeContextItem: (id) => set({ contextItems: get().contextItems.filter((i) => i.id !== id) }),
|
|
122
146
|
promoteContextItem: (id) => set({
|
|
123
|
-
contextItems: get().contextItems.map((i) =>
|
|
147
|
+
contextItems: get().contextItems.map((i) => {
|
|
148
|
+
if (i.id !== id)
|
|
149
|
+
return i;
|
|
150
|
+
// Pinning takes the item away from the UI that was tracking it: the
|
|
151
|
+
// key goes, so the next selection change replaces nothing and the
|
|
152
|
+
// pin the user made survives.
|
|
153
|
+
const { key: _tracked, ...rest } = i;
|
|
154
|
+
return { ...rest, persistence: "persistent" };
|
|
155
|
+
}),
|
|
124
156
|
}),
|
|
125
157
|
clearAllContext: () => set({ contextItems: [], sentContext: {} }),
|
|
126
158
|
consumeContextForMessage: (messageId) => {
|
|
@@ -128,9 +160,14 @@ export const useAppStore = create((set, get) => ({
|
|
|
128
160
|
if (contextItems.length === 0)
|
|
129
161
|
return [];
|
|
130
162
|
const snapshot = [...contextItems];
|
|
131
|
-
// Keep persistent, remove ephemeral
|
|
163
|
+
// Keep persistent, remove ephemeral — except context that TRACKS the UI
|
|
164
|
+
// (a keyed item, e.g. a live list selection). Those rows are still
|
|
165
|
+
// ticked after the message goes; dropping the chip would take them out
|
|
166
|
+
// of every later message while the user can still see them selected.
|
|
167
|
+
// Their owner clears them (`setContextItem(key, null)`), and navigation
|
|
168
|
+
// still does.
|
|
132
169
|
set({
|
|
133
|
-
contextItems: contextItems.filter((i) => i.persistence === "persistent"),
|
|
170
|
+
contextItems: contextItems.filter((i) => i.persistence === "persistent" || i.key !== undefined),
|
|
134
171
|
sentContext: { ...sentContext, [messageId]: snapshot },
|
|
135
172
|
});
|
|
136
173
|
return snapshot;
|
package/dist/styles.css
CHANGED
|
@@ -140,3 +140,94 @@
|
|
|
140
140
|
--code-block: oklch(0.16 0.01 286);
|
|
141
141
|
--code-block-foreground: oklch(0.9 0.01 286);
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
/* ---------------------------------------------------------------- lists --
|
|
145
|
+
* Rules the components need but a host's CSS scanner cannot infer, so they
|
|
146
|
+
* ship here. Import this stylesheet (see the README) to get them.
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
/* A long menu — every column of a wide list — stays reachable: capped to the
|
|
150
|
+
* room the trigger has and scrollable inside it. */
|
|
151
|
+
[data-menu-scroll] {
|
|
152
|
+
max-height: var(--radix-dropdown-menu-content-available-height);
|
|
153
|
+
overflow-y: auto;
|
|
154
|
+
overscroll-behavior: contain;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Frozen columns. The cell has to be opaque, since the other columns scroll
|
|
158
|
+
* underneath it, so the row's translucent states are restated opaquely. */
|
|
159
|
+
[data-pinned] {
|
|
160
|
+
position: sticky;
|
|
161
|
+
}
|
|
162
|
+
/* The frozen column slides under the sticky header, never over it: the header
|
|
163
|
+
* row carries z-10 (see `DataTable`), so a frozen body cell has to sit below
|
|
164
|
+
* that, and the frozen header cell only has to beat its own row. */
|
|
165
|
+
[data-pinned="header"] {
|
|
166
|
+
z-index: 2;
|
|
167
|
+
background-color: var(--card);
|
|
168
|
+
}
|
|
169
|
+
/* An opaque stand-in for the table's own surface (`bg-card/60` over the
|
|
170
|
+
* page), not the page background — otherwise the frozen strip reads as a
|
|
171
|
+
* different colour from the rows it belongs to, most visibly in dark mode. */
|
|
172
|
+
[data-pinned="cell"] {
|
|
173
|
+
z-index: 1;
|
|
174
|
+
/* The opaque equal of the table's own `bg-card/60`. Every row state below
|
|
175
|
+
* mixes its tint into THIS, the way a translucent tint on an ordinary cell
|
|
176
|
+
* lands on the table surface rather than on the page. */
|
|
177
|
+
--pinned-surface: color-mix(in srgb, var(--card) 60%, var(--background));
|
|
178
|
+
background-color: var(--pinned-surface);
|
|
179
|
+
}
|
|
180
|
+
/* The seam says "there is more to the left" — so it appears only once the
|
|
181
|
+
* list is actually scrolled sideways, and stays quiet otherwise. */
|
|
182
|
+
[data-scrolled-x] [data-pinned-edge] {
|
|
183
|
+
border-right: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
|
|
184
|
+
box-shadow: 6px 0 6px -6px color-mix(in srgb, var(--foreground) 12%, transparent);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* A list you can scroll should say so: keep its scrollbars visible rather
|
|
188
|
+
* than relying on the overlay ones macOS hides until you already scroll. */
|
|
189
|
+
[data-list-scroller] {
|
|
190
|
+
scrollbar-gutter: stable;
|
|
191
|
+
scrollbar-width: thin;
|
|
192
|
+
scrollbar-color: color-mix(in srgb, var(--foreground) 25%, transparent) transparent;
|
|
193
|
+
}
|
|
194
|
+
[data-list-scroller]::-webkit-scrollbar {
|
|
195
|
+
width: 10px;
|
|
196
|
+
height: 10px;
|
|
197
|
+
}
|
|
198
|
+
[data-list-scroller]::-webkit-scrollbar-track {
|
|
199
|
+
background: transparent;
|
|
200
|
+
}
|
|
201
|
+
[data-list-scroller]::-webkit-scrollbar-thumb {
|
|
202
|
+
border: 3px solid transparent;
|
|
203
|
+
border-radius: 999px;
|
|
204
|
+
background-clip: content-box;
|
|
205
|
+
background-color: color-mix(in srgb, var(--foreground) 22%, transparent);
|
|
206
|
+
}
|
|
207
|
+
[data-list-scroller]:hover::-webkit-scrollbar-thumb {
|
|
208
|
+
background-color: color-mix(in srgb, var(--foreground) 32%, transparent);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* A group header spans the scrollable width; its content sticks to the
|
|
212
|
+
* viewport so the label and total stay readable while columns scroll. */
|
|
213
|
+
[data-group-content] {
|
|
214
|
+
position: sticky;
|
|
215
|
+
left: 0;
|
|
216
|
+
}
|
|
217
|
+
/* Only rows that actually respond to a hover — `DataTable` marks those —
|
|
218
|
+
* so a read-only row never lights up its frozen half alone. */
|
|
219
|
+
tr[data-clickable]:hover > [data-pinned="cell"] {
|
|
220
|
+
background-color: color-mix(in srgb, var(--muted) 30%, var(--pinned-surface));
|
|
221
|
+
}
|
|
222
|
+
/* Only when the ROW itself is focused. A control inside a cell does not tint
|
|
223
|
+
* the ordinary cells, so tinting the frozen ones would make the strip diverge
|
|
224
|
+
* exactly while someone is using that control. */
|
|
225
|
+
tr[data-focusable]:focus-visible > [data-pinned="cell"] {
|
|
226
|
+
background-color: color-mix(in srgb, var(--muted) 30%, var(--pinned-surface));
|
|
227
|
+
}
|
|
228
|
+
tr[data-state="selected"] > [data-pinned="cell"] {
|
|
229
|
+
background-color: color-mix(in srgb, var(--primary) 6%, var(--pinned-surface));
|
|
230
|
+
}
|
|
231
|
+
tr[data-active] > [data-pinned="cell"] {
|
|
232
|
+
background-color: color-mix(in srgb, var(--primary) 8%, var(--pinned-surface));
|
|
233
|
+
}
|
package/dist/ui/dropdown-menu.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Check } from "lucide-react";
|
|
|
5
5
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
6
|
export const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
7
7
|
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
8
|
-
export const DropdownMenuContent = forwardRef(({ className, sideOffset = 4, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn("z-50 min-w-[8rem]
|
|
8
|
+
export const DropdownMenuContent = forwardRef(({ className, sideOffset = 4, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { ref: ref, sideOffset: sideOffset, "data-menu-scroll": "", className: cn("z-50 min-w-[8rem] rounded-md border border-border", "bg-popover p-1 text-popover-foreground shadow-md", "data-[state=open]:animate-in data-[state=closed]:animate-out", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2", "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", className), ...props }) })));
|
|
9
9
|
DropdownMenuContent.displayName = "DropdownMenuContent";
|
|
10
10
|
export const DropdownMenuItem = forwardRef(({ className, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Item, { ref: ref, className: cn(
|
|
11
11
|
// ``gap-2`` is the same icon-to-text spacing the framework's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "React agent UI core for Foundry UI — chat, composer, AG-UI adapter for assistant-ui, tool cards, panels, sidebar, theme runtime, and UI stores.",
|
|
6
6
|
"keywords": [
|
|
@@ -71,8 +71,8 @@
|
|
|
71
71
|
"react-markdown": "^10.0.0",
|
|
72
72
|
"remark-gfm": "^4.0.0",
|
|
73
73
|
"tailwind-merge": "^3.5.0",
|
|
74
|
-
"@iloveagents/foundry-agent": "^0.
|
|
75
|
-
"@iloveagents/foundry-web-primitives": "^0.
|
|
74
|
+
"@iloveagents/foundry-agent": "^0.20.0",
|
|
75
|
+
"@iloveagents/foundry-web-primitives": "^0.20.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@ag-ui/client": "^0.0.52",
|