@api7/ui-components 0.7.0 → 0.8.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.
Files changed (74) hide show
  1. package/README.md +169 -8
  2. package/dist/components/anchor/anchor.d.ts +28 -0
  3. package/dist/components/anchor/anchor.js +249 -0
  4. package/dist/components/breadcrumb/breadcrumb.d.ts +15 -0
  5. package/dist/components/breadcrumb/breadcrumb.js +60 -0
  6. package/dist/components/button/button.d.ts +1 -1
  7. package/dist/components/button/button.js +1 -1
  8. package/dist/components/calendar/calendar.js +24 -3
  9. package/dist/components/cascader/cascader-data.d.ts +26 -0
  10. package/dist/components/cascader/cascader-data.js +110 -0
  11. package/dist/components/cascader/cascader.d.ts +41 -0
  12. package/dist/components/cascader/cascader.js +513 -0
  13. package/dist/components/code/code.d.ts +9 -0
  14. package/dist/components/code/code.js +20 -0
  15. package/dist/components/code/copy-button.js +42 -0
  16. package/dist/components/code-block/code-block.d.ts +16 -0
  17. package/dist/components/code-block/code-block.js +45 -0
  18. package/dist/components/code-block/highlight.js +67 -0
  19. package/dist/components/collapsible/collapsible.d.ts +14 -0
  20. package/dist/components/collapsible/collapsible.js +36 -0
  21. package/dist/components/combobox/combobox-loading.d.ts +18 -0
  22. package/dist/components/combobox/combobox-loading.js +148 -0
  23. package/dist/components/combobox/combobox.d.ts +13 -6
  24. package/dist/components/combobox/combobox.js +58 -10
  25. package/dist/components/data-table/data-table.d.ts +27 -3
  26. package/dist/components/data-table/data-table.js +155 -40
  27. package/dist/components/data-table/expandable-row.js +40 -0
  28. package/dist/components/data-table/filter-header.d.ts +28 -5
  29. package/dist/components/data-table/filter-header.js +103 -40
  30. package/dist/components/data-table/index.d.ts +3 -3
  31. package/dist/components/data-table/use-table-layout.js +84 -0
  32. package/dist/components/date-picker/date-picker.js +11 -2
  33. package/dist/components/description-list/description-list.d.ts +1 -1
  34. package/dist/components/dialog/dialog.js +1 -1
  35. package/dist/components/drawer/drawer.js +1 -1
  36. package/dist/components/empty/empty.d.ts +1 -1
  37. package/dist/components/field/labeled-field.js +1 -1
  38. package/dist/components/file-upload/file-upload.d.ts +44 -0
  39. package/dist/components/file-upload/file-upload.js +195 -0
  40. package/dist/components/form/choice-fields.js +2 -2
  41. package/dist/components/form/date-fields.d.ts +1 -1
  42. package/dist/components/form/date-fields.js +1 -1
  43. package/dist/components/form/form-item.js +1 -1
  44. package/dist/components/form/form.js +1 -1
  45. package/dist/components/form/index.d.ts +2 -2
  46. package/dist/components/form/index.js +4 -4
  47. package/dist/components/form/text-field.js +2 -2
  48. package/dist/components/form/toggle-fields.d.ts +1 -1
  49. package/dist/components/form/toggle-fields.js +3 -3
  50. package/dist/components/pagination/pagination.d.ts +20 -4
  51. package/dist/components/pagination/pagination.js +83 -32
  52. package/dist/components/relative-date-range-picker/relative-date-range-picker.d.ts +14 -0
  53. package/dist/components/relative-date-range-picker/relative-date-range-picker.js +245 -0
  54. package/dist/components/relative-date-range-picker/relative-date-range.d.ts +26 -0
  55. package/dist/components/relative-date-range-picker/relative-date-range.js +109 -0
  56. package/dist/components/select/select.js +1 -1
  57. package/dist/components/sidebar/sidebar.js +2 -2
  58. package/dist/components/steps/steps.d.ts +23 -0
  59. package/dist/components/steps/steps.js +86 -0
  60. package/dist/components/table/table.d.ts +2 -1
  61. package/dist/components/table/table.js +2 -1
  62. package/dist/components/toast/toast.d.ts +1 -1
  63. package/dist/components/toast/toast.js +1 -1
  64. package/dist/config/config-provider.d.ts +2 -2
  65. package/dist/config/messages.d.ts +115 -1
  66. package/dist/config/messages.js +140 -2
  67. package/dist/hooks/use-clipboard.d.ts +11 -0
  68. package/dist/hooks/use-clipboard.js +75 -0
  69. package/dist/hooks/use-field-label-with-text.js +32 -0
  70. package/dist/index.d.ts +26 -13
  71. package/dist/index.js +15 -4
  72. package/dist/tokens/index.d.ts +1 -1
  73. package/dist/tokens/index.js +1 -1
  74. package/package.json +7 -6
@@ -1,24 +1,30 @@
1
1
  "use client";
2
- import { useMessages } from "../../config/config-provider.js";
3
- import { cn } from "../../lib/cn.js";
2
+ import { ExpandableRow } from "./expandable-row.js";
3
+ import { FilterHeader } from "./filter-header.js";
4
+ import { SortHeader } from "./sort-header.js";
5
+ import { useTableLayout } from "./use-table-layout.js";
4
6
  import { DEFAULT_PAGE_SIZE_OPTIONS, Pagination } from "../pagination/pagination.js";
5
7
  import { Skeleton } from "../skeleton/skeleton.js";
6
8
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../table/table.js";
7
- import { FilterHeader } from "./filter-header.js";
8
- import { SortHeader } from "./sort-header.js";
9
- import { useEffect, useState } from "react";
9
+ import { useMessages } from "../../config/config-provider.js";
10
+ import { cn } from "../../lib/cn.js";
11
+ import { useEffect, useId, useState } from "react";
10
12
  import { jsx, jsxs } from "react/jsx-runtime";
11
- import { columnFilteringFeature, columnVisibilityFeature, constructFilterFn, createColumnHelper, createFilteredRowModel, createPaginatedRowModel, createSortedRowModel, flexRender, rowPaginationFeature, rowSortingFeature, sortFn_alphanumeric, sortFn_basic, sortFn_datetime, sortFn_text, tableFeatures, useTable } from "@tanstack/react-table";
13
+ import { columnFilteringFeature, columnPinningFeature, columnSizingFeature, columnVisibilityFeature, constructFilterFn, createColumnHelper, createFilteredRowModel, createPaginatedRowModel, createSortedRowModel, flexRender, rowExpandingFeature, rowPaginationFeature, rowSortingFeature, sortFn_alphanumeric, sortFn_basic, sortFn_datetime, sortFn_text, tableFeatures, useTable } from "@tanstack/react-table";
12
14
  // src/components/data-table/data-table.tsx
13
15
  const LOADING_ROW_COUNT = 4;
16
+ const EXPAND_COLUMN_WIDTH = 48;
14
17
  const filterFn_oneOf = constructFilterFn({
15
18
  filter: (value, chosen) => chosen.includes(String(value)),
16
19
  autoRemove: (chosen) => !Array.isArray(chosen) || chosen.length === 0
17
20
  });
18
21
  const dataTableFeatures = tableFeatures({
19
22
  columnFilteringFeature,
23
+ columnPinningFeature,
24
+ columnSizingFeature,
20
25
  columnVisibilityFeature,
21
26
  rowSortingFeature,
27
+ rowExpandingFeature,
22
28
  rowPaginationFeature,
23
29
  filteredRowModel: createFilteredRowModel(),
24
30
  sortedRowModel: createSortedRowModel(),
@@ -34,8 +40,20 @@ const dataTableFeatures = tableFeatures({
34
40
  function createDataTableColumnHelper() {
35
41
  return createColumnHelper();
36
42
  }
37
- function DataTable({ columns, data, getRowId, sorting, defaultSorting, onSortingChange, manualSorting, columnFilters, onColumnFiltersChange, manualFiltering, pagination, defaultPagination, onPaginationChange, manualPagination, rowCount, pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS, hidePagination, paginationLabel, loading, empty, toolbar, className }) {
43
+ function hasColumnSize(columns) {
44
+ return columns.some((column) => column.size !== void 0 || "columns" in column && hasColumnSize(column.columns ?? []));
45
+ }
46
+ function DataTable({ columns, data, getRowId, renderExpandedRow, getRowCanExpand, expanded, defaultExpanded, onExpandedChange, columnPinning, defaultColumnPinning, onColumnPinningChange, sorting, defaultSorting, onSortingChange, manualSorting, columnFilters, onColumnFiltersChange, manualFiltering, pagination, defaultPagination, onPaginationChange, manualPagination, rowCount, pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS, hidePagination, paginationLabel, loading, showHeader = true, empty, toolbar, className }) {
38
47
  const messages = useMessages();
48
+ const expansionColumnId = useId();
49
+ const expandable = renderExpandedRow !== void 0;
50
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded ?? {});
51
+ const expandedState = expanded ?? internalExpanded;
52
+ const [internalPinning, setInternalPinning] = useState(defaultColumnPinning ?? {
53
+ start: [],
54
+ end: []
55
+ });
56
+ const pinningState = columnPinning ?? internalPinning;
39
57
  const [internalSorting, setInternalSorting] = useState(defaultSorting ?? []);
40
58
  const sortingState = sorting ?? internalSorting;
41
59
  const [internalFilters, setInternalFilters] = useState([]);
@@ -62,9 +80,23 @@ function DataTable({ columns, data, getRowId, sorting, defaultSorting, onSorting
62
80
  columns,
63
81
  getRowId,
64
82
  state: {
83
+ expanded: expandedState,
65
84
  sorting: sortingState,
66
85
  columnFilters: filtersState,
67
- pagination: shownPagination
86
+ pagination: shownPagination,
87
+ columnPinning: pinningState
88
+ },
89
+ getRowCanExpand: (row) => expandable && (getRowCanExpand?.(row.original) ?? true),
90
+ autoResetExpanded: false,
91
+ onExpandedChange: (updater) => {
92
+ const next = typeof updater === "function" ? updater(expandedState) : updater;
93
+ if (expanded === void 0) setInternalExpanded(next);
94
+ onExpandedChange?.(next);
95
+ },
96
+ onColumnPinningChange: (updater) => {
97
+ const next = typeof updater === "function" ? updater(pinningState) : updater;
98
+ if (columnPinning === void 0) setInternalPinning(next);
99
+ onColumnPinningChange?.(next);
68
100
  },
69
101
  onColumnFiltersChange: (updater) => {
70
102
  const next = typeof updater === "function" ? updater(filtersState) : updater;
@@ -109,8 +141,25 @@ function DataTable({ columns, data, getRowId, sorting, defaultSorting, onSorting
109
141
  paginationState.pageSize,
110
142
  hidePagination
111
143
  ]);
144
+ const startColumns = table.getStartVisibleLeafColumns();
145
+ const endColumns = table.getEndVisibleLeafColumns();
146
+ const visibleColumns = [
147
+ ...startColumns,
148
+ ...table.getCenterVisibleLeafColumns(),
149
+ ...endColumns
150
+ ];
151
+ const sized = visibleColumns.some((column) => column.getIsPinned()) || hasColumnSize(columns);
152
+ const { tableRef, containerRef, containerWidth, columnStyles, cellClass: pinnedCellClass } = useTableLayout(JSON.stringify([
153
+ sized,
154
+ expandable,
155
+ visibleColumns.map((column) => [column.id, column.getSize()])
156
+ ]), expandable && startColumns.length > 0 ? [{
157
+ id: expansionColumnId,
158
+ getSize: () => EXPAND_COLUMN_WIDTH
159
+ }, ...startColumns] : startColumns, endColumns);
160
+ const cellClass = (column, header = false) => cn(sized && "[overflow-wrap:anywhere]", pinnedCellClass(column.id, header), column.columnDef.meta?.className);
112
161
  return /* @__PURE__ */ jsxs("div", {
113
- className: cn("w-full", className),
162
+ className: cn("w-full min-w-0", className),
114
163
  "data-slot": "data-table",
115
164
  children: [
116
165
  toolbar ? /* @__PURE__ */ jsx("div", {
@@ -118,38 +167,104 @@ function DataTable({ columns, data, getRowId, sorting, defaultSorting, onSorting
118
167
  "data-slot": "data-table-toolbar",
119
168
  children: toolbar
120
169
  }) : null,
121
- /* @__PURE__ */ jsxs(Table, { children: [/* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx(TableRow, { children: headerGroup.headers.map((header) => {
122
- const canSort = header.column.getCanSort();
123
- const sorted = canSort ? header.column.getIsSorted() : false;
124
- const ariaSort = sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : void 0;
125
- const filter = header.column.columnDef.meta?.filter;
126
- const content = flexRender(header.column.columnDef.header, header.getContext());
127
- const title = canSort ? /* @__PURE__ */ jsx(SortHeader, {
128
- column: header.column,
129
- children: content
130
- }) : content;
131
- return /* @__PURE__ */ jsx(TableHead, {
132
- className: header.column.columnDef.meta?.className,
133
- "aria-sort": ariaSort,
134
- children: header.isPlaceholder ? null : filter ? /* @__PURE__ */ jsxs("div", {
135
- className: "flex items-center gap-1",
136
- children: [title, /* @__PURE__ */ jsx(FilterHeader, {
137
- column: header.column,
138
- filter
170
+ /* @__PURE__ */ jsxs(Table, {
171
+ ref: tableRef,
172
+ containerRef,
173
+ className: sized ? cn("table-fixed border-separate border-spacing-0 [&_td]:border-b [&_td]:border-border", showHeader && "[&_th]:border-b [&_th]:border-border") : void 0,
174
+ style: sized ? { minWidth: table.getTotalSize() + (expandable ? EXPAND_COLUMN_WIDTH : 0) } : void 0,
175
+ children: [
176
+ (sized || expandable) && /* @__PURE__ */ jsxs("colgroup", { children: [expandable && /* @__PURE__ */ jsx("col", {
177
+ "data-column-id": expansionColumnId,
178
+ style: { width: EXPAND_COLUMN_WIDTH }
179
+ }), visibleColumns.map((column) => /* @__PURE__ */ jsx("col", {
180
+ "data-column-id": column.id,
181
+ style: sized ? { width: expandable ? `${column.getSize() / table.getTotalSize() * 100}%` : column.getSize() } : void 0
182
+ }, column.id))] }),
183
+ /* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup, index) => /* @__PURE__ */ jsxs(TableRow, {
184
+ className: showHeader ? void 0 : "border-b-0",
185
+ children: [expandable && index === 0 && /* @__PURE__ */ jsx(TableHead, {
186
+ rowSpan: table.getHeaderGroups().length,
187
+ className: cn(showHeader ? "px-2" : "h-0 p-0", pinnedCellClass(expansionColumnId, true)),
188
+ style: columnStyles.get(expansionColumnId),
189
+ children: /* @__PURE__ */ jsx("span", {
190
+ className: "sr-only",
191
+ children: messages.dataTable.rowDetails
192
+ })
193
+ }), headerGroup.headers.map((header) => {
194
+ const canSort = header.column.getCanSort();
195
+ const sorted = canSort ? header.column.getIsSorted() : false;
196
+ const ariaSort = sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : void 0;
197
+ if (!showHeader) {
198
+ const definition = header.column.columnDef;
199
+ const label = definition.meta?.accessibleLabel ?? (typeof definition.header === "string" ? definition.header : header.column.id);
200
+ return /* @__PURE__ */ jsx(TableHead, {
201
+ colSpan: header.colSpan,
202
+ "aria-sort": ariaSort,
203
+ className: "h-0 p-0",
204
+ children: header.isPlaceholder ? null : /* @__PURE__ */ jsx("span", {
205
+ className: "sr-only",
206
+ children: label
207
+ })
208
+ }, header.id);
209
+ }
210
+ const filter = header.column.columnDef.meta?.filter;
211
+ const content = flexRender(header.column.columnDef.header, header.getContext());
212
+ const title = canSort ? /* @__PURE__ */ jsx(SortHeader, {
213
+ column: header.column,
214
+ children: content
215
+ }) : content;
216
+ return /* @__PURE__ */ jsx(TableHead, {
217
+ colSpan: header.colSpan,
218
+ className: cellClass(header.column, true),
219
+ style: columnStyles.get(header.column.id),
220
+ "aria-sort": ariaSort,
221
+ children: header.isPlaceholder ? null : filter ? /* @__PURE__ */ jsxs("div", {
222
+ className: "flex items-center gap-1",
223
+ children: [title, /* @__PURE__ */ jsx(FilterHeader, {
224
+ column: header.column,
225
+ filter
226
+ })]
227
+ }) : title
228
+ }, header.id);
139
229
  })]
140
- }) : title
141
- }, header.id);
142
- }) }, headerGroup.id)) }), /* @__PURE__ */ jsx(TableBody, { children: loading ? Array.from({ length: LOADING_ROW_COUNT }).map((_, rowIndex) => /* @__PURE__ */ jsx(TableRow, {
143
- className: "h-14",
144
- children: table.getVisibleLeafColumns().map((column) => /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }, column.id))
145
- }, `skeleton-${rowIndex}`)) : table.getRowModel().rows.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
146
- colSpan: table.getVisibleLeafColumns().length,
147
- className: "h-[200px] text-center text-xl font-medium text-muted-foreground",
148
- children: empty ?? messages.dataTable.empty
149
- }) }) : table.getRowModel().rows.map((row) => /* @__PURE__ */ jsx(TableRow, { children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(TableCell, {
150
- className: cell.column.columnDef.meta?.className,
151
- children: flexRender(cell.column.columnDef.cell, cell.getContext())
152
- }, cell.id)) }, row.id)) })] }),
230
+ }, headerGroup.id)) }),
231
+ /* @__PURE__ */ jsx(TableBody, {
232
+ className: "[&_[data-expanded-row]:hover]:bg-transparent",
233
+ children: loading ? Array.from({ length: LOADING_ROW_COUNT }).map((_, rowIndex) => /* @__PURE__ */ jsxs(TableRow, {
234
+ className: "h-14",
235
+ children: [expandable && /* @__PURE__ */ jsx(TableCell, {
236
+ className: pinnedCellClass(expansionColumnId),
237
+ style: columnStyles.get(expansionColumnId)
238
+ }), visibleColumns.map((column) => /* @__PURE__ */ jsx(TableCell, {
239
+ className: cellClass(column),
240
+ style: columnStyles.get(column.id),
241
+ children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" })
242
+ }, column.id))]
243
+ }, `skeleton-${rowIndex}`)) : table.getRowModel().rows.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
244
+ colSpan: visibleColumns.length + (expandable ? 1 : 0),
245
+ className: "h-[200px] text-center text-xl font-medium text-muted-foreground",
246
+ children: empty ?? messages.dataTable.empty
247
+ }) }) : table.getRowModel().rows.map((row) => {
248
+ const cells = row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(TableCell, {
249
+ className: cellClass(cell.column),
250
+ style: columnStyles.get(cell.column.id),
251
+ children: flexRender(cell.column.columnDef.cell, cell.getContext())
252
+ }, cell.id));
253
+ return renderExpandedRow ? /* @__PURE__ */ jsx(ExpandableRow, {
254
+ canExpand: row.getCanExpand(),
255
+ expanded: row.getIsExpanded(),
256
+ onToggle: row.getToggleExpandedHandler(),
257
+ renderDetails: () => renderExpandedRow(row.original),
258
+ colSpan: visibleColumns.length + 1,
259
+ detailsWidth: startColumns.length || endColumns.length ? containerWidth : void 0,
260
+ controlClassName: pinnedCellClass(expansionColumnId),
261
+ controlStyle: columnStyles.get(expansionColumnId),
262
+ children: cells
263
+ }, row.id) : /* @__PURE__ */ jsx(TableRow, { children: cells }, row.id);
264
+ })
265
+ })
266
+ ]
267
+ }),
153
268
  hidePagination ? null : /* @__PURE__ */ jsx(Pagination, {
154
269
  label: paginationLabel,
155
270
  pageIndex: shownPagination.pageIndex,
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ import { TableCell, TableRow } from "../table/table.js";
3
+ import { useMessages } from "../../config/config-provider.js";
4
+ import { cn } from "../../lib/cn.js";
5
+ import { useId, useState } from "react";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ // src/components/data-table/expandable-row.tsx
8
+ function ExpandableRow({ children, canExpand, expanded, onToggle, renderDetails, colSpan, detailsWidth, controlClassName, controlStyle }) {
9
+ const messages = useMessages();
10
+ const detailsId = useId();
11
+ const [visited, setVisited] = useState(expanded);
12
+ if (expanded && !visited) setVisited(true);
13
+ const hasDetails = canExpand && (expanded || visited);
14
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(TableRow, { children: [/* @__PURE__ */ jsx(TableCell, {
15
+ className: cn("px-2 text-center", controlClassName),
16
+ style: controlStyle,
17
+ children: canExpand && /* @__PURE__ */ jsx("button", {
18
+ type: "button",
19
+ "aria-label": expanded ? messages.dataTable.collapseRow : messages.dataTable.expandRow,
20
+ "aria-expanded": expanded,
21
+ "aria-controls": hasDetails ? detailsId : void 0,
22
+ onClick: onToggle,
23
+ className: cn("relative inline-flex size-4 cursor-pointer align-sub rounded-sm border border-border bg-background text-foreground outline-none hover:border-primary-text hover:text-primary-text focus-visible:ring-2 focus-visible:ring-ring/40", "before:absolute before:inset-x-[3px] before:top-1/2 before:h-px before:-translate-y-1/2 before:bg-current before:content-['']", "after:absolute after:inset-y-[3px] after:left-1/2 after:w-px after:-translate-x-1/2 after:bg-current after:transition-transform after:duration-300 after:content-[''] motion-reduce:after:transition-none", expanded && "after:rotate-90")
24
+ })
25
+ }), children] }), hasDetails && /* @__PURE__ */ jsx(TableRow, {
26
+ id: detailsId,
27
+ hidden: !expanded,
28
+ "data-expanded-row": "",
29
+ children: /* @__PURE__ */ jsx(TableCell, {
30
+ colSpan,
31
+ className: "bg-foreground/[0.02] p-4 leading-[1.5714285714]",
32
+ children: detailsWidth === void 0 ? renderDetails() : /* @__PURE__ */ jsx("div", {
33
+ className: "sticky start-0 -m-4 overflow-hidden p-4",
34
+ style: { width: detailsWidth || void 0 },
35
+ children: renderDetails()
36
+ })
37
+ })
38
+ })] });
39
+ }
40
+ export { ExpandableRow };
@@ -1,20 +1,43 @@
1
1
  import "./data-table.js";
2
2
  import { ReactNode } from "react";
3
- import { RowData } from "@tanstack/react-table";
3
+ import "@tanstack/react-table";
4
4
  export interface DataTableColumnFilterOption {
5
5
  value: string;
6
6
  label: ReactNode;
7
7
  /** Shown at the end of the option, for example how many rows have this value. */
8
8
  count?: number;
9
9
  }
10
- export interface DataTableColumnFilter {
10
+ export interface DataTableFilterPanelProps {
11
+ /** Current choices, including uncommitted edits when applyOn is `close`. */
12
+ value: string[];
13
+ /** Updates the selection, or only the draft when applyOn is `close`. */
14
+ onValueChange: (value: string[]) => void;
15
+ /** Restores defaultValue or clears the selection, following the same commit timing. */
16
+ reset: () => void;
17
+ /** Closes the panel and commits a changed draft once when applyOn is `close`. */
18
+ close: () => void;
19
+ }
20
+ interface DataTableColumnFilterBase {
11
21
  /** Names the filter for assistive technology, for example `Status`. */
12
22
  label: string;
13
- options: DataTableColumnFilterOption[];
23
+ /** Classes for the custom panel surface, for example a compact, unpadded grouped list. */
24
+ contentClassName?: string;
25
+ /** Defaults to immediate updates. `close` keeps a draft until dismissal (including Escape). */
26
+ applyOn?: 'change' | 'close';
27
+ onOpenChange?: (open: boolean) => void;
14
28
  /**
15
29
  * The values that are checked while the column has no filter, for a filter whose unfiltered state is "every
16
- * value", so that what is checked agrees with the rows on screen. A column with a default never has nothing
30
+ * value", so that what is checked agrees with the rows on screen. A column with nonempty defaults never has nothing
17
31
  * checked: the last checked value cannot be unchecked, and Reset checks the default again.
18
32
  */
19
33
  defaultValue?: string[];
20
- }
34
+ }
35
+ export type DataTableColumnFilter = DataTableColumnFilterBase & ({
36
+ options: DataTableColumnFilterOption[];
37
+ render?: never;
38
+ } | {
39
+ /** Custom controls in a Popover, sharing the column's selection and commit timing. */
40
+ render: (props: DataTableFilterPanelProps) => ReactNode;
41
+ options?: never;
42
+ });
43
+ export {};
@@ -1,54 +1,117 @@
1
1
  "use client";
2
- import { useMessages } from "../../config/config-provider.js";
3
2
  import { Button } from "../button/button.js";
4
3
  import { Menu, MenuCheckboxItem, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from "../menu/menu.js";
4
+ import { Popover, PopoverContent, PopoverTrigger } from "../popover/popover.js";
5
+ import { useMessages } from "../../config/config-provider.js";
6
+ import { cn } from "../../lib/cn.js";
7
+ import { useRef, useState } from "react";
5
8
  import { jsx, jsxs } from "react/jsx-runtime";
6
9
  import { Funnel } from "lucide-react";
7
10
  // src/components/data-table/filter-header.tsx
8
11
  function FilterHeader({ column, filter }) {
9
12
  const messages = useMessages();
10
13
  const filterValue = column.getFilterValue();
11
- const active = filterValue !== void 0 && filterValue.length > 0;
12
- const chosen = active ? filterValue : filter.defaultValue ?? [];
13
- const required = filter.defaultValue !== void 0;
14
+ const committed = filterValue?.length ? filterValue : filter.defaultValue ?? [];
15
+ const [open, setOpen] = useState(false);
16
+ const [draft, setDraft] = useState();
17
+ const pending = useRef(void 0);
18
+ const deferred = filter.applyOn === "close";
19
+ const chosen = deferred && open ? draft ?? committed : committed;
20
+ const required = Boolean(filter.defaultValue?.length);
21
+ const normalize = (next) => {
22
+ const isDefault = required && equalValues(filter.defaultValue, next);
23
+ return next.length > 0 && !isDefault ? next : void 0;
24
+ };
25
+ const commit = (next) => {
26
+ const normalized = normalize(next);
27
+ if (!equalValues(normalized ?? [], filterValue ?? [])) column.setFilterValue(normalized);
28
+ };
14
29
  const change = (next) => {
15
- const isDefault = required && filter.defaultValue.length === next.length && filter.defaultValue.every((value) => next.includes(value));
16
- column.setFilterValue(next.length > 0 && !isDefault ? next : void 0);
30
+ if (required && next.length === 0) return;
31
+ if (deferred) {
32
+ pending.current = next;
33
+ setDraft(next);
34
+ } else commit(next);
17
35
  };
18
- return /* @__PURE__ */ jsxs(Menu, { children: [/* @__PURE__ */ jsx(MenuTrigger, {
19
- render: /* @__PURE__ */ jsx(Button, {
20
- variant: "ghost",
21
- size: "icon",
22
- className: "size-6",
23
- "aria-label": active ? messages.dataTable.filterActive({
24
- label: filter.label,
25
- count: chosen.length
26
- }) : messages.dataTable.filter({ label: filter.label })
27
- }),
28
- children: /* @__PURE__ */ jsx(Funnel, {
29
- "aria-hidden": "true",
30
- className: active ? "text-primary-text fill-current" : "text-muted-foreground"
31
- })
32
- }), /* @__PURE__ */ jsxs(MenuContent, {
33
- align: "start",
34
- className: "min-w-48",
35
- children: [
36
- filter.options.map((option) => /* @__PURE__ */ jsxs(MenuCheckboxItem, {
37
- checked: chosen.includes(option.value),
38
- disabled: required && chosen.length === 1 && chosen.includes(option.value),
39
- onCheckedChange: (on) => change(on ? [...chosen, option.value] : chosen.filter((value) => value !== option.value)),
40
- children: [option.label, option.count !== void 0 && /* @__PURE__ */ jsx("span", {
41
- className: "ml-auto pl-4 text-muted-foreground",
42
- children: option.count
43
- })]
44
- }, option.value)),
45
- /* @__PURE__ */ jsx(MenuSeparator, {}),
46
- /* @__PURE__ */ jsx(MenuItem, {
47
- disabled: !active,
48
- onClick: () => column.setFilterValue(void 0),
49
- children: messages.dataTable.resetFilter
36
+ const changeOpen = (next) => {
37
+ if (next === open) return;
38
+ if (next) {
39
+ pending.current = void 0;
40
+ setDraft(void 0);
41
+ } else if (deferred && pending.current !== void 0) {
42
+ commit(pending.current);
43
+ pending.current = void 0;
44
+ }
45
+ setOpen(next);
46
+ filter.onOpenChange?.(next);
47
+ };
48
+ const reset = () => change(filter.defaultValue ?? []);
49
+ const indicated = deferred && open ? normalize(chosen) : filterValue;
50
+ const active = Boolean(indicated?.length);
51
+ const trigger = /* @__PURE__ */ jsx(Button, {
52
+ variant: "ghost",
53
+ size: "icon",
54
+ className: "size-6",
55
+ "aria-label": active ? messages.dataTable.filterActive({
56
+ label: filter.label,
57
+ count: indicated.length
58
+ }) : messages.dataTable.filter({ label: filter.label })
59
+ });
60
+ const icon = /* @__PURE__ */ jsx(Funnel, {
61
+ "aria-hidden": "true",
62
+ className: active ? "text-primary-text fill-current" : "text-muted-foreground"
63
+ });
64
+ if (filter.render) return /* @__PURE__ */ jsxs(Popover, {
65
+ open,
66
+ onOpenChange: changeOpen,
67
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
68
+ render: trigger,
69
+ children: icon
70
+ }), /* @__PURE__ */ jsx(PopoverContent, {
71
+ "aria-label": messages.dataTable.filter({ label: filter.label }),
72
+ className: cn("max-h-80 overflow-y-auto", filter.contentClassName),
73
+ children: /* @__PURE__ */ jsx(CustomFilterContent, {
74
+ render: filter.render,
75
+ value: chosen,
76
+ onValueChange: change,
77
+ reset,
78
+ close: () => changeOpen(false)
50
79
  })
51
- ]
52
- })] });
80
+ })]
81
+ });
82
+ return /* @__PURE__ */ jsxs(Menu, {
83
+ open,
84
+ onOpenChange: changeOpen,
85
+ children: [/* @__PURE__ */ jsx(MenuTrigger, {
86
+ render: trigger,
87
+ children: icon
88
+ }), /* @__PURE__ */ jsxs(MenuContent, {
89
+ align: "start",
90
+ className: "min-w-48",
91
+ children: [
92
+ filter.options.map((option) => /* @__PURE__ */ jsxs(MenuCheckboxItem, {
93
+ checked: chosen.includes(option.value),
94
+ disabled: required && chosen.length === 1 && chosen.includes(option.value),
95
+ onCheckedChange: (on) => change(on ? [...chosen, option.value] : chosen.filter((value) => value !== option.value)),
96
+ children: [option.label, option.count !== void 0 && /* @__PURE__ */ jsx("span", {
97
+ className: "ml-auto pl-4 text-muted-foreground",
98
+ children: option.count
99
+ })]
100
+ }, option.value)),
101
+ /* @__PURE__ */ jsx(MenuSeparator, {}),
102
+ /* @__PURE__ */ jsx(MenuItem, {
103
+ disabled: normalize(chosen) === void 0,
104
+ onClick: reset,
105
+ children: messages.dataTable.resetFilter
106
+ })
107
+ ]
108
+ })]
109
+ });
110
+ }
111
+ function CustomFilterContent({ render, ...props }) {
112
+ return render(props);
113
+ }
114
+ function equalValues(a, b) {
115
+ return a.length === b.length && a.every((value) => b.includes(value));
53
116
  }
54
117
  export { FilterHeader };
@@ -1,4 +1,4 @@
1
- import { DataTableColumnFilter, DataTableColumnFilterOption } from "./filter-header.js";
2
1
  import { DataTable, DataTableColumnDef, DataTableFeatures, DataTableProps, DataTableRowActions, createDataTableColumnHelper, dataTableFeatures } from "./data-table.js";
3
- import { ColumnFiltersState, PaginationState, SortingState } from "@tanstack/react-table";
4
- export { type ColumnFiltersState, DataTable, type DataTableColumnDef, type DataTableColumnFilter, type DataTableColumnFilterOption, type DataTableFeatures, type DataTableProps, DataTableRowActions, type PaginationState, type SortingState, createDataTableColumnHelper, dataTableFeatures };
2
+ import { DataTableColumnFilter, DataTableColumnFilterOption, DataTableFilterPanelProps } from "./filter-header.js";
3
+ import { ColumnFiltersState, ColumnPinningState, ExpandedState, PaginationState, SortingState } from "@tanstack/react-table";
4
+ export { type ColumnFiltersState, type ColumnPinningState, DataTable, type DataTableColumnDef, type DataTableColumnFilter, type DataTableColumnFilterOption, type DataTableFeatures, type DataTableFilterPanelProps, type DataTableProps, DataTableRowActions, type ExpandedState, type PaginationState, type SortingState, createDataTableColumnHelper, dataTableFeatures };
@@ -0,0 +1,84 @@
1
+ "use client";
2
+ import { cn } from "../../lib/cn.js";
3
+ import { useLayoutEffect, useRef, useState } from "react";
4
+ // src/components/data-table/use-table-layout.ts
5
+ /** Measure the rendered colgroup: table layout may distribute spare width beyond column.size. */
6
+ function useTableLayout(columnKey, startColumns, endColumns) {
7
+ const tableRef = useRef(null);
8
+ const containerRef = useRef(null);
9
+ const [layout, setLayout] = useState({
10
+ widths: {},
11
+ start: false,
12
+ end: false,
13
+ rtl: false,
14
+ containerWidth: 0
15
+ });
16
+ useLayoutEffect(() => {
17
+ const table = tableRef.current;
18
+ const container = containerRef.current;
19
+ if (!table || !container) return;
20
+ const scrollEdges = () => {
21
+ const offset = Math.abs(container.scrollLeft);
22
+ return {
23
+ start: offset > 1,
24
+ end: container.scrollWidth - container.clientWidth - offset > 1
25
+ };
26
+ };
27
+ const updateScrollEdges = () => {
28
+ const edges = scrollEdges();
29
+ setLayout((previous) => previous.start === edges.start && previous.end === edges.end ? previous : {
30
+ ...previous,
31
+ ...edges
32
+ });
33
+ };
34
+ const measure = () => {
35
+ const widths = Object.fromEntries([...table.querySelectorAll("col[data-column-id]")].map((col) => [col.dataset.columnId, col.getBoundingClientRect().width]));
36
+ const containerWidth = container.clientWidth;
37
+ const edges = scrollEdges();
38
+ const rtl = getComputedStyle(container).direction === "rtl";
39
+ setLayout((previous) => {
40
+ return Object.keys(previous.widths).length === Object.keys(widths).length && Object.entries(widths).every(([id, width]) => previous.widths[id] === width) && previous.start === edges.start && previous.end === edges.end && previous.rtl === rtl && previous.containerWidth === containerWidth ? previous : {
41
+ widths,
42
+ ...edges,
43
+ rtl,
44
+ containerWidth
45
+ };
46
+ });
47
+ };
48
+ measure();
49
+ const observer = new ResizeObserver(measure);
50
+ observer.observe(container);
51
+ observer.observe(table);
52
+ container.addEventListener("scroll", updateScrollEdges, { passive: true });
53
+ return () => {
54
+ observer.disconnect();
55
+ container.removeEventListener("scroll", updateScrollEdges);
56
+ };
57
+ }, [columnKey]);
58
+ const columnStyles = /* @__PURE__ */ new Map();
59
+ for (const side of ["start", "end"]) {
60
+ const region = side === "start" ? startColumns : [...endColumns].reverse();
61
+ let offset = 0;
62
+ for (const [index, column] of region.entries()) {
63
+ const shadow = index === region.length - 1 && layout[side];
64
+ const shadowDirection = side === "start" !== layout.rtl ? 1 : -1;
65
+ columnStyles.set(column.id, {
66
+ position: "sticky",
67
+ [side === "start" ? "insetInlineStart" : "insetInlineEnd"]: offset,
68
+ zIndex: 1,
69
+ "--data-table-shadow": shadow ? `inset ${shadowDirection * 10}px 0 8px -8px color-mix(in srgb, var(--color-foreground) 7%, transparent)` : "none",
70
+ "--data-table-shadow-start": side === "start" ? "100%" : "-30px"
71
+ });
72
+ offset += layout.widths[column.id] ?? column.getSize();
73
+ }
74
+ }
75
+ const cellClass = (columnId, header = false) => columnStyles.has(columnId) ? cn("after:pointer-events-none after:absolute after:top-0 after:-bottom-px after:start-[var(--data-table-shadow-start)] after:w-[30px] after:shadow-[var(--data-table-shadow)] after:content-['']", !header && "bg-background group-hover/row:bg-[color-mix(in_srgb,var(--color-muted)_50%,var(--color-background))]") : void 0;
76
+ return {
77
+ tableRef,
78
+ containerRef,
79
+ containerWidth: layout.containerWidth,
80
+ columnStyles,
81
+ cellClass
82
+ };
83
+ }
84
+ export { useTableLayout };
@@ -1,8 +1,9 @@
1
1
  "use client";
2
- import { useMessages } from "../../config/config-provider.js";
3
- import { cn } from "../../lib/cn.js";
4
2
  import { Calendar } from "../calendar/calendar.js";
5
3
  import { Popover, PopoverContent, PopoverTrigger } from "../popover/popover.js";
4
+ import { useMessages } from "../../config/config-provider.js";
5
+ import { useFieldLabelWithText } from "../../hooks/use-field-label-with-text.js";
6
+ import { cn } from "../../lib/cn.js";
6
7
  import { controlFocus, controlSurface } from "../../lib/control.js";
7
8
  import { useCallback, useState } from "react";
8
9
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -67,15 +68,19 @@ function DatePicker(props) {
67
68
  setValue(withTimeString(currentValue, timeString));
68
69
  }, [currentValue, setValue]);
69
70
  const resolvedPlaceholder = placeholder ?? messages.datePicker.placeholder;
71
+ const { ref: triggerRef, textId, labelledBy } = useFieldLabelWithText();
70
72
  return /* @__PURE__ */ jsxs(Popover, {
71
73
  onOpenChange: (open) => onOpenChange?.(open),
72
74
  children: [/* @__PURE__ */ jsx(Field.Control, {
73
75
  disabled,
74
76
  render: /* @__PURE__ */ jsxs(PopoverTrigger, {
77
+ ref: triggerRef,
78
+ "aria-labelledby": labelledBy,
75
79
  disabled,
76
80
  onBlur,
77
81
  className: cn(controlSurface, controlFocus, "flex h-8 items-center gap-2 px-3 text-left", className),
78
82
  children: [/* @__PURE__ */ jsx(CalendarIcon, { className: "size-4 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx("span", {
83
+ id: textId,
79
84
  className: cn("min-w-0 flex-1 truncate", !currentValue && "text-muted-foreground"),
80
85
  children: currentValue ? format(currentValue) : resolvedPlaceholder
81
86
  })]
@@ -159,6 +164,7 @@ function DateRangePicker(props) {
159
164
  onOpenChange?.(false);
160
165
  }, [onOpenChange, setValue]);
161
166
  const resolvedPlaceholder = placeholder ?? messages.datePicker.rangePlaceholder;
167
+ const { ref: triggerRef, textId, labelledBy } = useFieldLabelWithText();
162
168
  const displayText = currentValue?.from ? currentValue.to ? `${format(currentValue.from)} ~ ${format(currentValue.to)}` : format(currentValue.from) : void 0;
163
169
  return /* @__PURE__ */ jsxs(Popover, {
164
170
  open,
@@ -169,10 +175,13 @@ function DateRangePicker(props) {
169
175
  children: [/* @__PURE__ */ jsx(Field.Control, {
170
176
  disabled,
171
177
  render: /* @__PURE__ */ jsxs(PopoverTrigger, {
178
+ ref: triggerRef,
179
+ "aria-labelledby": labelledBy,
172
180
  disabled,
173
181
  onBlur,
174
182
  className: cn(controlSurface, controlFocus, "flex h-8 items-center gap-2 px-3 text-left", className),
175
183
  children: [/* @__PURE__ */ jsx(CalendarIcon, { className: "size-4 shrink-0 text-muted-foreground" }), /* @__PURE__ */ jsx("span", {
184
+ id: textId,
176
185
  className: cn("min-w-0 flex-1 truncate", !displayText && "text-muted-foreground"),
177
186
  children: displayText ?? resolvedPlaceholder
178
187
  })]