@datum-cloud/datum-ui 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/components/features/data-table/core/filter-engine.d.ts +5 -2
  2. package/dist/components/features/data-table/core/filter-engine.d.ts.map +1 -1
  3. package/dist/components/features/data-table/index.d.ts +4 -0
  4. package/dist/components/features/data-table/index.d.ts.map +1 -1
  5. package/dist/components/features/grouped-table/components/grouped-skeleton.d.ts +7 -0
  6. package/dist/components/features/grouped-table/components/grouped-skeleton.d.ts.map +1 -0
  7. package/dist/components/features/grouped-table/components/grouped-toolbar.d.ts +9 -0
  8. package/dist/components/features/grouped-table/components/grouped-toolbar.d.ts.map +1 -0
  9. package/dist/components/features/grouped-table/grouped-table.d.ts +3 -0
  10. package/dist/components/features/grouped-table/grouped-table.d.ts.map +1 -0
  11. package/dist/components/features/grouped-table/index.d.ts +3 -0
  12. package/dist/components/features/grouped-table/index.d.ts.map +1 -0
  13. package/dist/components/features/grouped-table/lib/bucket-rows.d.ts +14 -0
  14. package/dist/components/features/grouped-table/lib/bucket-rows.d.ts.map +1 -0
  15. package/dist/components/features/grouped-table/lib/compose-columns.d.ts +11 -0
  16. package/dist/components/features/grouped-table/lib/compose-columns.d.ts.map +1 -0
  17. package/dist/components/features/grouped-table/lib/sort-rows.d.ts +7 -0
  18. package/dist/components/features/grouped-table/lib/sort-rows.d.ts.map +1 -0
  19. package/dist/components/features/grouped-table/lib/use-controllable-state.d.ts +8 -0
  20. package/dist/components/features/grouped-table/lib/use-controllable-state.d.ts.map +1 -0
  21. package/dist/components/features/grouped-table/types.d.ts +74 -0
  22. package/dist/components/features/grouped-table/types.d.ts.map +1 -0
  23. package/dist/components/features/grouped-table/use-grouped-expansion.d.ts +10 -0
  24. package/dist/components/features/grouped-table/use-grouped-expansion.d.ts.map +1 -0
  25. package/dist/data-table/index.mjs +2 -1588
  26. package/dist/data-table-BTIxzB7O.mjs +1588 -0
  27. package/dist/grouped-table/index.mjs +352 -0
  28. package/package.json +8 -3
@@ -0,0 +1,352 @@
1
+ import { t as cn } from "../cn-dlASUkDY.mjs";
2
+ import { t as Icon } from "../icon-wrapper-DKfJlJd0.mjs";
3
+ import { n as CollapsibleContent, r as CollapsibleTrigger, t as Collapsible } from "../collapsible-BeCdkxTJ.mjs";
4
+ import { t as Input } from "../input-DBzgl-pN.mjs";
5
+ import { t as Skeleton } from "../skeleton-CGU89HPB.mjs";
6
+ import { c as TableRow, i as TableCell, n as TableBody, o as TableHead, s as TableHeader } from "../table-BR3mwU8X.mjs";
7
+ import { f as rowMatchesSearch, h as createSelectionColumn, m as DataTableColumnHeader, p as DataTableRowActions } from "../data-table-BTIxzB7O.mjs";
8
+ import { ChevronRight } from "lucide-react";
9
+ import { useCallback, useEffect, useMemo, useState } from "react";
10
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
+ import { flexRender, getCoreRowModel, getFilteredRowModel, useReactTable } from "@tanstack/react-table";
12
+ //#region src/components/features/grouped-table/components/grouped-skeleton.tsx
13
+ function GroupedSkeleton({ columns = 4, groups = 2, rowsPerGroup = 3 }) {
14
+ return /* @__PURE__ */ jsx("div", {
15
+ "data-slot": "gt-skeleton",
16
+ children: Array.from({ length: groups }, (_, g) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
17
+ "data-slot": "gt-skeleton-band",
18
+ className: "flex items-center gap-2 bg-muted/40 px-3 py-2 [&:not(:first-child)]:border-t",
19
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "size-4 rounded" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" })]
20
+ }), Array.from({ length: rowsPerGroup }, (_, r) => /* @__PURE__ */ jsx("div", {
21
+ "data-slot": "gt-skeleton-row",
22
+ className: "flex items-center gap-3 px-3 py-2 [&:not(:last-child)]:border-b",
23
+ children: Array.from({ length: columns }, (_, c) => /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }, c))
24
+ }, r))] }, g))
25
+ });
26
+ }
27
+ //#endregion
28
+ //#region src/components/features/grouped-table/components/grouped-toolbar.tsx
29
+ const DEFAULT_DEBOUNCE_MS = 300;
30
+ function GroupedToolbar({ search, onSearchChange, placeholder = "Search...", debounceMs = DEFAULT_DEBOUNCE_MS, className }) {
31
+ const [value, setValue] = useState(search);
32
+ useEffect(() => {
33
+ setValue(search);
34
+ }, [search]);
35
+ useEffect(() => {
36
+ const timer = setTimeout(() => {
37
+ if (value !== search) onSearchChange(value);
38
+ }, debounceMs);
39
+ return () => clearTimeout(timer);
40
+ }, [
41
+ value,
42
+ debounceMs,
43
+ search,
44
+ onSearchChange
45
+ ]);
46
+ return /* @__PURE__ */ jsx("div", {
47
+ className: cn("pb-3", className),
48
+ "data-slot": "gt-toolbar",
49
+ children: /* @__PURE__ */ jsx(Input, {
50
+ placeholder,
51
+ value,
52
+ onChange: (e) => setValue(e.target.value),
53
+ "aria-label": placeholder,
54
+ "data-slot": "gt-search"
55
+ })
56
+ });
57
+ }
58
+ //#endregion
59
+ //#region src/components/features/grouped-table/lib/bucket-rows.ts
60
+ /**
61
+ * Build per-group buckets of `filteredRows`, preserving the filtered order.
62
+ * `coreRows` are the unfiltered rows in group-concatenated order; we use their
63
+ * positions to learn which row id belongs to which group, so bucketing is correct
64
+ * regardless of a custom getRowId.
65
+ */
66
+ function bucketRows(groups, coreRows, filteredRows) {
67
+ const idToGroup = /* @__PURE__ */ new Map();
68
+ let offset = 0;
69
+ for (const group of groups) {
70
+ for (let i = 0; i < group.rows.length; i++) {
71
+ const core = coreRows[offset + i];
72
+ if (core) idToGroup.set(core.id, group.id);
73
+ }
74
+ offset += group.rows.length;
75
+ }
76
+ const buckets = new Map(groups.map((g) => [g.id, []]));
77
+ for (const row of filteredRows) {
78
+ const groupId = idToGroup.get(row.id);
79
+ if (groupId) buckets.get(groupId).push(row);
80
+ }
81
+ return buckets;
82
+ }
83
+ //#endregion
84
+ //#region src/components/features/grouped-table/lib/compose-columns.tsx
85
+ /** Wrap plain string headers in the sortable header; leave all other columns untouched. */
86
+ function withSortableHeaders(columns) {
87
+ return columns.map((col) => {
88
+ if (typeof col.header !== "string") return col;
89
+ const title = col.header;
90
+ return {
91
+ ...col,
92
+ enableSorting: col.enableSorting ?? true,
93
+ header: ({ column }) => /* @__PURE__ */ jsx(DataTableColumnHeader, {
94
+ column,
95
+ title
96
+ })
97
+ };
98
+ });
99
+ }
100
+ function composeColumns(columns, options) {
101
+ const { enableRowSelection, enableSorting, rowActions, rowActionsSheetTitle } = options;
102
+ let cols = columns;
103
+ if (enableSorting) cols = withSortableHeaders(cols);
104
+ if (enableRowSelection) cols = [createSelectionColumn(typeof enableRowSelection === "object" ? enableRowSelection : {}), ...cols];
105
+ if (rowActions) cols = [...cols, {
106
+ id: "actions",
107
+ size: 44,
108
+ enableSorting: false,
109
+ cell: ({ row }) => /* @__PURE__ */ jsx(DataTableRowActions, {
110
+ row,
111
+ actions: rowActions(row.original),
112
+ sheetTitle: rowActionsSheetTitle
113
+ })
114
+ }];
115
+ return cols;
116
+ }
117
+ //#endregion
118
+ //#region src/components/features/grouped-table/lib/sort-rows.ts
119
+ function compare(a, b) {
120
+ if (a == null && b == null) return 0;
121
+ if (a == null) return -1;
122
+ if (b == null) return 1;
123
+ if (typeof a === "number" && typeof b === "number") return a - b;
124
+ return String(a).localeCompare(String(b));
125
+ }
126
+ /**
127
+ * Sort a single group's rows by the active sort (first entry; v1 is single-sort).
128
+ * Returns the same reference when there is no sort, so React can skip re-renders.
129
+ */
130
+ function sortRows(rows, sorting) {
131
+ if (sorting.length === 0) return rows;
132
+ const { id, desc } = sorting[0];
133
+ const sorted = [...rows].sort((ra, rb) => compare(ra.getValue(id), rb.getValue(id)));
134
+ if (desc) sorted.reverse();
135
+ return sorted;
136
+ }
137
+ //#endregion
138
+ //#region src/components/features/grouped-table/lib/use-controllable-state.ts
139
+ /**
140
+ * Uncontrolled-by-default state with an optional controlled override.
141
+ * When `controlled` is undefined the hook owns the value; otherwise the prop wins.
142
+ * `onChange` always fires. Accepts TanStack-style updater functions.
143
+ */
144
+ function useControllableState(controlled, defaultValue, onChange) {
145
+ const [internal, setInternal] = useState(defaultValue);
146
+ const isControlled = controlled !== void 0;
147
+ const value = isControlled ? controlled : internal;
148
+ return [value, useCallback((next) => {
149
+ const resolved = typeof next === "function" ? next(value) : next;
150
+ if (!isControlled) setInternal(resolved);
151
+ onChange?.(resolved);
152
+ }, [
153
+ isControlled,
154
+ onChange,
155
+ value
156
+ ])];
157
+ }
158
+ //#endregion
159
+ //#region src/components/features/grouped-table/use-grouped-expansion.ts
160
+ function defaultFor(group, def) {
161
+ const fallback = def === "all" ? true : def === "none" ? false : Array.isArray(def) ? def.includes(group.id) : true;
162
+ return group.defaultOpen ?? fallback;
163
+ }
164
+ function useGroupedExpansion(groups, opts) {
165
+ const { defaultExpanded = "all", expanded, onExpandedChange } = opts;
166
+ const controlled = expanded !== void 0;
167
+ const [overrides, setOverrides] = useState({});
168
+ const isOpen = useCallback((id) => {
169
+ if (controlled) return expanded.includes(id);
170
+ if (id in overrides) return overrides[id];
171
+ const g = groups.find((x) => x.id === id);
172
+ return g ? defaultFor(g, defaultExpanded) : false;
173
+ }, [
174
+ controlled,
175
+ expanded,
176
+ overrides,
177
+ groups,
178
+ defaultExpanded
179
+ ]);
180
+ return {
181
+ isOpen,
182
+ toggle: useCallback((id) => {
183
+ const now = isOpen(id);
184
+ if (!controlled) setOverrides((o) => ({
185
+ ...o,
186
+ [id]: !now
187
+ }));
188
+ const next = groups.filter((g) => g.id === id ? !now : isOpen(g.id)).map((g) => g.id);
189
+ onExpandedChange?.(next);
190
+ }, [
191
+ controlled,
192
+ groups,
193
+ isOpen,
194
+ onExpandedChange
195
+ ])
196
+ };
197
+ }
198
+ //#endregion
199
+ //#region src/components/features/grouped-table/grouped-table.tsx
200
+ /** Floor width for unsized (flex) columns so they keep their share instead of collapsing on narrow viewports. */
201
+ const MIN_FLEX_COLUMN_WIDTH = 120;
202
+ function columnWidth(col) {
203
+ return typeof col.size === "number" ? `${col.size}px` : "auto";
204
+ }
205
+ /** Minimum width the table track needs so every column keeps a usable size; below this the area scrolls horizontally. */
206
+ function trackMinWidth(resolvedColumns) {
207
+ return resolvedColumns.reduce((total, col) => total + (typeof col.size === "number" ? col.size : MIN_FLEX_COLUMN_WIDTH), 0);
208
+ }
209
+ function renderColGroup(resolvedColumns) {
210
+ return /* @__PURE__ */ jsx("colgroup", { children: resolvedColumns.map((col, i) => /* @__PURE__ */ jsx("col", { style: { width: columnWidth(col) } }, `col-${i}`)) });
211
+ }
212
+ /** Resolve a static or per-item className override (mirrors data-table). */
213
+ function resolveClassName(value, item) {
214
+ return typeof value === "function" ? value(item) : value;
215
+ }
216
+ function GroupedTable(props) {
217
+ const { columns, groups, defaultExpanded, expanded, onExpandedChange, getRowId, enableRowSelection, rowSelection: rowSelectionProp, onRowSelectionChange, rowActions, rowActionsSheetTitle, enableSorting, sorting: sortingProp, onSortingChange, enableSearch, searchPlaceholder, searchableColumns, searchFn, search: searchProp, onSearchChange, searchDebounceMs, isLoading, empty, className, toolbarClassName, tableClassName, headerRowClassName, headerCellClassName, groupHeaderClassName, bodyClassName, rowClassName, cellClassName } = props;
218
+ const [sorting, setSorting] = useControllableState(sortingProp, [], onSortingChange);
219
+ const [rowSelection, setRowSelection] = useControllableState(rowSelectionProp, {}, onRowSelectionChange);
220
+ const [search, setSearch] = useControllableState(searchProp, "", onSearchChange);
221
+ const isSearching = search.trim().length > 0;
222
+ const { isOpen, toggle } = useGroupedExpansion(groups, {
223
+ defaultExpanded,
224
+ expanded,
225
+ onExpandedChange
226
+ });
227
+ const resolvedColumns = useMemo(() => composeColumns(columns, {
228
+ enableRowSelection,
229
+ enableSorting,
230
+ rowActions,
231
+ rowActionsSheetTitle
232
+ }), [
233
+ columns,
234
+ enableRowSelection,
235
+ enableSorting,
236
+ rowActions,
237
+ rowActionsSheetTitle
238
+ ]);
239
+ const minWidth = useMemo(() => trackMinWidth(resolvedColumns), [resolvedColumns]);
240
+ const flatData = useMemo(() => groups.flatMap((g) => g.rows), [groups]);
241
+ const table = useReactTable({
242
+ data: flatData,
243
+ columns: resolvedColumns,
244
+ state: {
245
+ sorting,
246
+ rowSelection,
247
+ globalFilter: search
248
+ },
249
+ onSortingChange: setSorting,
250
+ onRowSelectionChange: setRowSelection,
251
+ onGlobalFilterChange: setSearch,
252
+ enableRowSelection: Boolean(enableRowSelection),
253
+ manualSorting: true,
254
+ enableMultiSort: false,
255
+ globalFilterFn: (row, _columnId, value) => rowMatchesSearch(row.original, String(value ?? ""), {
256
+ searchFn,
257
+ searchableColumns
258
+ }),
259
+ getRowId,
260
+ getCoreRowModel: getCoreRowModel(),
261
+ getFilteredRowModel: getFilteredRowModel()
262
+ });
263
+ const headerGroups = table.getHeaderGroups();
264
+ const coreRows = table.getCoreRowModel().rows;
265
+ const filteredRows = table.getRowModel().rows;
266
+ const buckets = useMemo(() => bucketRows(groups, coreRows, filteredRows), [
267
+ groups,
268
+ coreRows,
269
+ filteredRows
270
+ ]);
271
+ const slices = useMemo(() => groups.map((g) => ({
272
+ group: g,
273
+ id: g.id,
274
+ title: g.title,
275
+ meta: g.meta,
276
+ rows: sortRows(buckets.get(g.id) ?? [], sorting)
277
+ })), [
278
+ groups,
279
+ buckets,
280
+ sorting
281
+ ]);
282
+ const visibleSlices = isSearching ? slices.filter((s) => s.rows.length > 0) : slices;
283
+ const renderShell = (body, scrollable) => /* @__PURE__ */ jsxs("div", {
284
+ className: cn("w-full", className),
285
+ children: [enableSearch && /* @__PURE__ */ jsx(GroupedToolbar, {
286
+ search,
287
+ onSearchChange: setSearch,
288
+ placeholder: searchPlaceholder,
289
+ debounceMs: searchDebounceMs,
290
+ className: toolbarClassName
291
+ }), /* @__PURE__ */ jsx("div", {
292
+ className: cn("w-full rounded-md border", scrollable ? "overflow-x-auto" : "overflow-hidden"),
293
+ children: scrollable ? /* @__PURE__ */ jsx("div", {
294
+ style: { minWidth },
295
+ children: body
296
+ }) : body
297
+ })]
298
+ });
299
+ if (isLoading) return renderShell(/* @__PURE__ */ jsx(GroupedSkeleton, { columns: resolvedColumns.length }), false);
300
+ if (flatData.length === 0 || isSearching && visibleSlices.length === 0) return renderShell(empty ?? null, false);
301
+ return renderShell(/* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("table", {
302
+ className: cn("w-full table-fixed text-sm", tableClassName),
303
+ children: [renderColGroup(resolvedColumns), /* @__PURE__ */ jsx(TableHeader, { children: headerGroups.map((hg) => /* @__PURE__ */ jsx(TableRow, {
304
+ className: headerRowClassName,
305
+ children: hg.headers.map((header) => /* @__PURE__ */ jsx(TableHead, {
306
+ scope: "col",
307
+ className: headerCellClassName,
308
+ children: header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())
309
+ }, header.id))
310
+ }, hg.id)) })]
311
+ }), visibleSlices.map((slice) => {
312
+ const open = isSearching ? true : isOpen(slice.id);
313
+ return /* @__PURE__ */ jsxs(Collapsible, {
314
+ open,
315
+ onOpenChange: () => toggle(slice.id),
316
+ children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
317
+ className: cn("flex h-10 w-full items-center gap-2 border-b bg-muted/40 px-2 text-left align-middle text-sm font-medium text-muted-foreground transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", resolveClassName(groupHeaderClassName, slice.group)),
318
+ children: [
319
+ /* @__PURE__ */ jsx(Icon, {
320
+ icon: ChevronRight,
321
+ "aria-hidden": true,
322
+ className: cn("size-4 shrink-0 transition-transform", open && "rotate-90")
323
+ }),
324
+ /* @__PURE__ */ jsx("span", { children: slice.title }),
325
+ slice.meta != null && /* @__PURE__ */ jsx("span", {
326
+ className: "ml-auto flex items-center gap-2 font-medium",
327
+ children: slice.meta
328
+ })
329
+ ]
330
+ }), /* @__PURE__ */ jsx(CollapsibleContent, {
331
+ className: "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
332
+ children: /* @__PURE__ */ jsxs("table", {
333
+ className: cn("w-full table-fixed text-sm", tableClassName),
334
+ "aria-label": typeof slice.title === "string" ? slice.title : void 0,
335
+ children: [renderColGroup(resolvedColumns), /* @__PURE__ */ jsx(TableBody, {
336
+ className: bodyClassName,
337
+ children: slice.rows.map((row) => /* @__PURE__ */ jsx(TableRow, {
338
+ "data-state": row.getIsSelected() ? "selected" : void 0,
339
+ className: resolveClassName(rowClassName, row),
340
+ children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(TableCell, {
341
+ className: resolveClassName(cellClassName, cell),
342
+ children: flexRender(cell.column.columnDef.cell, cell.getContext())
343
+ }, cell.id))
344
+ }, row.id))
345
+ })]
346
+ })
347
+ })]
348
+ }, slice.id);
349
+ })] }), true);
350
+ }
351
+ //#endregion
352
+ export { GroupedTable };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datum-cloud/datum-ui",
3
3
  "type": "module",
4
- "version": "1.0.0",
4
+ "version": "1.2.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "url": "https://github.com/datum-cloud/datum-ui"
@@ -193,6 +193,11 @@
193
193
  "types": "./dist/components/base/table/index.d.ts",
194
194
  "default": "./dist/table/index.mjs"
195
195
  },
196
+ "./grouped-table": {
197
+ "source": "./src/components/features/grouped-table/index.ts",
198
+ "types": "./dist/components/features/grouped-table/index.d.ts",
199
+ "default": "./dist/grouped-table/index.mjs"
200
+ },
196
201
  "./tabs": {
197
202
  "source": "./src/components/base/tabs/index.ts",
198
203
  "types": "./dist/components/base/tabs/index.d.ts",
@@ -625,8 +630,8 @@
625
630
  "typescript": "^6.0.3",
626
631
  "vitest": "^4.1.8",
627
632
  "zod": "^4.4.3",
628
- "@repo/config": "0.0.0",
629
- "@repo/shadcn": "0.0.0"
633
+ "@repo/shadcn": "0.0.0",
634
+ "@repo/config": "0.0.0"
630
635
  },
631
636
  "publishConfig": {
632
637
  "access": "public"