@dxos/react-ui-table 0.3.3-main.006ca4d

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 (36) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +3 -0
  3. package/dist/lib/browser/index.mjs +844 -0
  4. package/dist/lib/browser/index.mjs.map +7 -0
  5. package/dist/lib/browser/meta.json +1 -0
  6. package/dist/types/src/components/ColumnMenu.d.ts +13 -0
  7. package/dist/types/src/components/ColumnMenu.d.ts.map +1 -0
  8. package/dist/types/src/components/ColumnMenu.stories.d.ts +16 -0
  9. package/dist/types/src/components/ColumnMenu.stories.d.ts.map +1 -0
  10. package/dist/types/src/components/Table.d.ts +29 -0
  11. package/dist/types/src/components/Table.d.ts.map +1 -0
  12. package/dist/types/src/components/Table.stories.d.ts +97 -0
  13. package/dist/types/src/components/Table.stories.d.ts.map +1 -0
  14. package/dist/types/src/components/index.d.ts +3 -0
  15. package/dist/types/src/components/index.d.ts.map +1 -0
  16. package/dist/types/src/helpers.d.ts +81 -0
  17. package/dist/types/src/helpers.d.ts.map +1 -0
  18. package/dist/types/src/index.d.ts +6 -0
  19. package/dist/types/src/index.d.ts.map +1 -0
  20. package/dist/types/src/schema.d.ts +21 -0
  21. package/dist/types/src/schema.d.ts.map +1 -0
  22. package/dist/types/src/theme.d.ts +34 -0
  23. package/dist/types/src/theme.d.ts.map +1 -0
  24. package/dist/types/src/types.d.ts +23 -0
  25. package/dist/types/src/types.d.ts.map +1 -0
  26. package/package.json +56 -0
  27. package/src/components/ColumnMenu.stories.tsx +35 -0
  28. package/src/components/ColumnMenu.tsx +239 -0
  29. package/src/components/Table.stories.tsx +328 -0
  30. package/src/components/Table.tsx +547 -0
  31. package/src/components/index.ts +6 -0
  32. package/src/helpers.tsx +443 -0
  33. package/src/index.ts +9 -0
  34. package/src/schema.ts +39 -0
  35. package/src/theme.ts +70 -0
  36. package/src/types.ts +33 -0
@@ -0,0 +1,844 @@
1
+ // packages/ui/react-ui-table/src/components/ColumnMenu.tsx
2
+ import { Check, CaretDown, Trash, X } from "@phosphor-icons/react";
3
+ import React, { useRef, useState } from "react";
4
+ import { Button, DensityProvider, Input, Popover, Select, Separator } from "@dxos/react-ui";
5
+ import { getSize, mx } from "@dxos/react-ui-theme";
6
+ import { safeParseInt } from "@dxos/util";
7
+
8
+ // packages/ui/react-ui-table/src/schema.ts
9
+ var columnTypes = /* @__PURE__ */ new Map([
10
+ [
11
+ "string",
12
+ "Text"
13
+ ],
14
+ [
15
+ "boolean",
16
+ "Checkbox"
17
+ ],
18
+ [
19
+ "number",
20
+ "Number"
21
+ ],
22
+ [
23
+ "date",
24
+ "Date"
25
+ ],
26
+ [
27
+ "ref",
28
+ "Reference"
29
+ ]
30
+ ]);
31
+
32
+ // packages/ui/react-ui-table/src/components/ColumnMenu.tsx
33
+ var ColumnMenu = ({ column, ...props }) => {
34
+ const title = column.label?.length ? column.label : column.id;
35
+ return /* @__PURE__ */ React.createElement("div", {
36
+ className: "flex grow items-center overflow-hidden"
37
+ }, /* @__PURE__ */ React.createElement("div", {
38
+ className: "truncate",
39
+ title
40
+ }, title), /* @__PURE__ */ React.createElement("div", {
41
+ className: "grow"
42
+ }), /* @__PURE__ */ React.createElement("div", {
43
+ className: "flex shrink-0"
44
+ }, /* @__PURE__ */ React.createElement(ColumnPanel, {
45
+ ...props,
46
+ column
47
+ })));
48
+ };
49
+ var Section = ({ children, className }) => /* @__PURE__ */ React.createElement("div", {
50
+ role: "none",
51
+ className: mx("p-2", className)
52
+ }, children);
53
+ var ColumnPanel = ({ context, tableDefs, tableDef, column, onUpdate, onDelete }) => {
54
+ const [open, setOpen] = useState(false);
55
+ const [prop, setProp] = useState(column.id);
56
+ const [refTable, setRefTable] = useState(column.refTable);
57
+ const [refProp, setRefProp] = useState(column.refProp);
58
+ const [type, setType] = useState(String(column.type));
59
+ const [label, setLabel] = useState(column.label);
60
+ const [digits, setDigits] = useState(String(column.digits ?? "0"));
61
+ const propRef = useRef(null);
62
+ const handleCancel = () => {
63
+ setProp(column.id);
64
+ setOpen(false);
65
+ };
66
+ const handleSave = () => {
67
+ if (!prop.length || !prop.match(/^[a-zA-Z_].+/i)) {
68
+ propRef.current?.focus();
69
+ return;
70
+ }
71
+ const current = tableDef.columns.find((c) => c.id !== column.id && c.id === prop);
72
+ if (current) {
73
+ if (current.type !== "ref") {
74
+ propRef.current?.focus();
75
+ return;
76
+ }
77
+ }
78
+ onUpdate?.(prop, {
79
+ id: prop,
80
+ prop,
81
+ label,
82
+ type,
83
+ refTable,
84
+ refProp,
85
+ digits: safeParseInt(digits)
86
+ });
87
+ setOpen(false);
88
+ };
89
+ return /* @__PURE__ */ React.createElement(Popover.Root, {
90
+ open,
91
+ onOpenChange: (nextOpen) => setOpen(nextOpen)
92
+ }, /* @__PURE__ */ React.createElement(Popover.Trigger, {
93
+ asChild: true
94
+ }, /* @__PURE__ */ React.createElement(Button, {
95
+ variant: "ghost",
96
+ classNames: "p-0"
97
+ }, /* @__PURE__ */ React.createElement(CaretDown, {
98
+ className: getSize(4)
99
+ }))), /* @__PURE__ */ React.createElement(Popover.Portal, null, /* @__PURE__ */ React.createElement(Popover.Content, null, /* @__PURE__ */ React.createElement(Popover.Viewport, {
100
+ classNames: "w-60"
101
+ }, /* @__PURE__ */ React.createElement(DensityProvider, {
102
+ density: "fine"
103
+ }, /* @__PURE__ */ React.createElement("div", {
104
+ className: "flex flex-col gap-1"
105
+ }, /* @__PURE__ */ React.createElement(Section, null, /* @__PURE__ */ React.createElement(Input.Root, null, /* @__PURE__ */ React.createElement(Input.Label, {
106
+ classNames: "mbe-1"
107
+ }, "Label"), /* @__PURE__ */ React.createElement(Input.TextInput, {
108
+ placeholder: "Column label",
109
+ value: label ?? "",
110
+ onChange: (event) => setLabel(event.target.value),
111
+ autoFocus: true
112
+ })), /* @__PURE__ */ React.createElement(Input.Root, null, /* @__PURE__ */ React.createElement(Input.Label, {
113
+ classNames: "mbe-1 mbs-3"
114
+ }, "Property"), /* @__PURE__ */ React.createElement(Input.TextInput, {
115
+ ref: propRef,
116
+ placeholder: "Property key",
117
+ // TODO(burdon): Provide hooks for value normalization, ENTER, ESC, etc.
118
+ value: prop ?? "",
119
+ onChange: (event) => setProp(event.target.value.replace(/[^\w_]/g, ""))
120
+ })), /* @__PURE__ */ React.createElement(Input.Root, null, /* @__PURE__ */ React.createElement(Input.Label, {
121
+ classNames: "mbe-1 mbs-3"
122
+ }, "Type"), /* @__PURE__ */ React.createElement(Select.Root, {
123
+ value: type,
124
+ onValueChange: setType
125
+ }, /* @__PURE__ */ React.createElement(Select.TriggerButton, {
126
+ placeholder: "Type",
127
+ classNames: "is-full"
128
+ }), /* @__PURE__ */ React.createElement(Select.Portal, null, /* @__PURE__ */ React.createElement(Select.Content, null, /* @__PURE__ */ React.createElement(Select.Viewport, null, Array.from(columnTypes.entries()).map(([type2, label2]) => /* @__PURE__ */ React.createElement(Select.Option, {
129
+ key: type2,
130
+ value: type2
131
+ }, label2)))))))), type === "number" && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Section, null, /* @__PURE__ */ React.createElement(Input.Root, null, /* @__PURE__ */ React.createElement(Input.Label, {
132
+ classNames: "mbe-1 mbs-3"
133
+ }, "Decimal places"), /* @__PURE__ */ React.createElement(Input.TextInput, {
134
+ value: digits ?? "",
135
+ onChange: (event) => setDigits(event.target.value)
136
+ })))), type === "ref" && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Section, null, /* @__PURE__ */ React.createElement(Input.Root, null, /* @__PURE__ */ React.createElement(Input.Label, {
137
+ classNames: "mbe-1 mbs-3"
138
+ }, "Table"), /* @__PURE__ */ React.createElement(Select.Root, {
139
+ value: refTable,
140
+ onValueChange: setRefTable
141
+ }, /* @__PURE__ */ React.createElement(Select.TriggerButton, {
142
+ placeholder: "Table",
143
+ classNames: "is-full"
144
+ }), /* @__PURE__ */ React.createElement(Select.Portal, null, /* @__PURE__ */ React.createElement(Select.Content, null, /* @__PURE__ */ React.createElement(Select.Viewport, null, tableDefs.filter((t) => t.id !== tableDef.id).map(({ id, name }) => /* @__PURE__ */ React.createElement(Select.Option, {
145
+ key: id,
146
+ value: id
147
+ }, name ?? id))))))), refTable && /* @__PURE__ */ React.createElement(Input.Root, null, /* @__PURE__ */ React.createElement(Input.Label, {
148
+ classNames: "mbe-1 mbs-3"
149
+ }, "Table property"), /* @__PURE__ */ React.createElement(Select.Root, {
150
+ value: refProp,
151
+ onValueChange: setRefProp
152
+ }, /* @__PURE__ */ React.createElement(Select.TriggerButton, {
153
+ placeholder: "Property",
154
+ classNames: "is-full"
155
+ }), /* @__PURE__ */ React.createElement(Select.Portal, null, /* @__PURE__ */ React.createElement(Select.Content, null, /* @__PURE__ */ React.createElement(Select.Viewport, null, tableDefs.find((tableDef2) => tableDef2.id === refTable)?.columns.map(({ id, label: label2 }) => /* @__PURE__ */ React.createElement(Select.Option, {
156
+ key: id,
157
+ value: id
158
+ }, label2 ?? id))))))))), /* @__PURE__ */ React.createElement(Separator, {
159
+ classNames: "mli-2"
160
+ }), /* @__PURE__ */ React.createElement(Section, {
161
+ className: "space-b-1.5"
162
+ }, /* @__PURE__ */ React.createElement(Button, {
163
+ variant: "primary",
164
+ classNames: "is-full flex gap-2",
165
+ onClick: handleSave
166
+ }, /* @__PURE__ */ React.createElement("span", null, "Save"), /* @__PURE__ */ React.createElement("div", {
167
+ className: "grow"
168
+ }), /* @__PURE__ */ React.createElement(Check, {
169
+ className: getSize(5)
170
+ })), /* @__PURE__ */ React.createElement(Button, {
171
+ classNames: "is-full flex gap-2",
172
+ onClick: handleCancel
173
+ }, /* @__PURE__ */ React.createElement("span", null, "Cancel"), /* @__PURE__ */ React.createElement("div", {
174
+ className: "grow"
175
+ }), /* @__PURE__ */ React.createElement(X, {
176
+ className: getSize(5)
177
+ })), /* @__PURE__ */ React.createElement(Button, {
178
+ classNames: "is-full flex gap-2",
179
+ onClick: () => onDelete?.(column.id)
180
+ }, /* @__PURE__ */ React.createElement("span", null, "Delete"), /* @__PURE__ */ React.createElement("div", {
181
+ className: "grow"
182
+ }), /* @__PURE__ */ React.createElement(Trash, {
183
+ className: getSize(5)
184
+ })))))), /* @__PURE__ */ React.createElement(Popover.Arrow, null))));
185
+ };
186
+
187
+ // packages/ui/react-ui-table/src/components/Table.tsx
188
+ import { flexRender, getCoreRowModel, getGroupedRowModel, useReactTable } from "@tanstack/react-table";
189
+ import React2, { Fragment, useEffect, useRef as useRef2, useState as useState2 } from "react";
190
+ import { debounce } from "@dxos/async";
191
+ import { groupBorder, inputSurface, mx as mx2 } from "@dxos/react-ui-theme";
192
+
193
+ // packages/ui/react-ui-table/src/theme.ts
194
+ import { chromeSurface } from "@dxos/react-ui-theme";
195
+ var defaultTableSlots = {
196
+ // TODO(burdon): head/body/table rows.
197
+ header: {
198
+ className: [
199
+ chromeSurface,
200
+ "px-2 font-light select-none"
201
+ ]
202
+ },
203
+ footer: {
204
+ className: [
205
+ chromeSurface,
206
+ "px-2 font-light"
207
+ ]
208
+ },
209
+ // cell: { className: 'px-2' },
210
+ // TODO(burdon): Compact mode.
211
+ // TODO(burdon): No hover if editing.
212
+ // row: { className: 'cursor-pointer hover:bg-neutral-50 dark:hover:bg-neutral-800' },
213
+ group: {
214
+ className: "px-2 font-light text-xs text-left"
215
+ },
216
+ focus: {
217
+ className: "ring ring-primary-600 ring-inset"
218
+ },
219
+ selected: {
220
+ className: "!bg-teal-100 dark:!bg-teal-700"
221
+ }
222
+ };
223
+
224
+ // packages/ui/react-ui-table/src/components/Table.tsx
225
+ var updateSelection = (selectionState, id, selection) => {
226
+ switch (selection) {
227
+ case "single": {
228
+ return {
229
+ [id]: true
230
+ };
231
+ }
232
+ case "single-toggle": {
233
+ return selectionState[id] ? {} : {
234
+ [id]: true
235
+ };
236
+ }
237
+ case "multiple": {
238
+ return {
239
+ [id]: true,
240
+ ...selectionState
241
+ };
242
+ }
243
+ case "multiple-toggle": {
244
+ if (selectionState[id]) {
245
+ const newSelectionState = {
246
+ ...selectionState
247
+ };
248
+ delete newSelectionState[id];
249
+ return newSelectionState;
250
+ } else {
251
+ return {
252
+ [id]: true,
253
+ ...selectionState
254
+ };
255
+ }
256
+ }
257
+ }
258
+ return selectionState;
259
+ };
260
+ var Table = ({ slots = defaultTableSlots, ...props }) => {
261
+ const { data = [], columns = [], onColumnResize, columnVisibility, header = true, footer, select, selected, onSelectedChange, fullWidth, pinToBottom, debug } = props;
262
+ const [focus, setFocus] = useState2();
263
+ const [rowSelection, setRowSelection] = useState2({});
264
+ useEffect(() => {
265
+ setRowSelection(selected?.reduce((selectionState, selected2) => {
266
+ const row = table.getRowModel().rows.find((row2) => row2.original === selected2);
267
+ if (row) {
268
+ selectionState[row.id] = true;
269
+ }
270
+ return selectionState;
271
+ }, {}) ?? {});
272
+ }, [
273
+ select,
274
+ selected
275
+ ]);
276
+ const [columnSizingInfo, setColumnSizingInfo] = useState2({});
277
+ const onColumnResizeDebounced = debounce((info) => onColumnResize?.(info), 500);
278
+ useEffect(() => {
279
+ if (columnSizingInfo.columnSizingStart?.length === 0) {
280
+ onColumnResizeDebounced(table.getState().columnSizing);
281
+ }
282
+ }, [
283
+ columnSizingInfo
284
+ ]);
285
+ const [grouping, setGrouping] = useState2(props.grouping ?? []);
286
+ useEffect(() => setGrouping(props.grouping ?? []), [
287
+ props.grouping
288
+ ]);
289
+ const table = useReactTable({
290
+ data,
291
+ columns,
292
+ defaultColumn: {
293
+ size: 400,
294
+ maxSize: 800
295
+ },
296
+ getCoreRowModel: getCoreRowModel(),
297
+ meta: {},
298
+ // TODO(burdon): Pagination.
299
+ // TODO(burdon): Sorting.
300
+ // TODO(burdon): Filtering.
301
+ state: {
302
+ columnVisibility,
303
+ columnSizingInfo,
304
+ rowSelection,
305
+ grouping
306
+ },
307
+ // Grouping.
308
+ getGroupedRowModel: getGroupedRowModel(),
309
+ onGroupingChange: setGrouping,
310
+ // Selection.
311
+ enableRowSelection: select === "single" || select === "single-toggle",
312
+ enableMultiRowSelection: select === "multiple" || select === "multiple-toggle",
313
+ onRowSelectionChange: (rows) => {
314
+ setRowSelection(rows);
315
+ },
316
+ // Resize columns.
317
+ // TODO(burdon): Drag to re-order columns.
318
+ columnResizeMode: "onChange",
319
+ enableColumnResizing: true,
320
+ onColumnSizingInfoChange: setColumnSizingInfo,
321
+ debugTable: debug
322
+ });
323
+ const expand = false;
324
+ const containerRef = usePinToBottom(data, pinToBottom);
325
+ const handleSelect = (row) => {
326
+ if (select) {
327
+ setRowSelection((selectionState) => {
328
+ const newSelectionState = updateSelection(selectionState, row.id, select);
329
+ if (onSelectedChange) {
330
+ const rows = Object.keys(newSelectionState).map((id) => table.getRowModel().rowsById[id].original);
331
+ onSelectedChange(rows);
332
+ }
333
+ return newSelectionState;
334
+ });
335
+ } else {
336
+ onSelectedChange?.([
337
+ row.original
338
+ ]);
339
+ }
340
+ };
341
+ return /* @__PURE__ */ React2.createElement("div", {
342
+ ref: containerRef,
343
+ className: mx2("grow overflow-auto", slots?.root?.className)
344
+ }, /* @__PURE__ */ React2.createElement("table", {
345
+ // Styles:
346
+ // table-fixed: Prevents fixed sized columns from shrinking.
347
+ className: mx2(!fullWidth && "table-fixed", inputSurface, slots?.table?.className),
348
+ style: {
349
+ width: fullWidth ? "100%" : table.getTotalSize()
350
+ }
351
+ }, /* @__PURE__ */ React2.createElement(TableHead, {
352
+ ...props,
353
+ slots,
354
+ header,
355
+ state: table.getState(),
356
+ headers: table.getHeaderGroups()
357
+ }), grouping.length === 0 && /* @__PURE__ */ React2.createElement(TableBody, {
358
+ ...props,
359
+ slots,
360
+ rowSelection,
361
+ expand,
362
+ focus,
363
+ onFocus: setFocus,
364
+ onSelect: handleSelect,
365
+ rows: table.getRowModel().rows
366
+ }), grouping.length !== 0 && table.getGroupedRowModel().rows.map((row, i) => {
367
+ return /* @__PURE__ */ React2.createElement(Fragment, {
368
+ key: i
369
+ }, /* @__PURE__ */ React2.createElement("thead", null, /* @__PURE__ */ React2.createElement("tr", null, slots?.margin && /* @__PURE__ */ React2.createElement("th", {
370
+ className: mx2(slots?.margin?.className)
371
+ }), debug && /* @__PURE__ */ React2.createElement("th", null), /* @__PURE__ */ React2.createElement("th", {
372
+ // TODO(burdon): Calculate row span.
373
+ colSpan: table.getHeaderGroups()[0].headers.length,
374
+ className: mx2("text-left", slots?.group?.className)
375
+ }, table.getState().grouping[0], "[", String(row.getGroupingValue(table.getState().grouping[0])), "]"))), /* @__PURE__ */ React2.createElement(TableBody, {
376
+ ...props,
377
+ slots,
378
+ rowSelection,
379
+ expand,
380
+ focus,
381
+ onFocus: setFocus,
382
+ onSelect: handleSelect,
383
+ rows: row.subRows
384
+ }));
385
+ }), footer && /* @__PURE__ */ React2.createElement(TableFoot, {
386
+ ...props,
387
+ slots,
388
+ footers: table.getFooterGroups()
389
+ })), debug && /* @__PURE__ */ React2.createElement("pre", {
390
+ className: "font-mono text-xs text-neutral-500 my-4 p-2 ring"
391
+ }, /* @__PURE__ */ React2.createElement("code", null, JSON.stringify(table.getState(), void 0, 2))));
392
+ };
393
+ var usePinToBottom = (data, pinToBottom) => {
394
+ const containerRef = useRef2(null);
395
+ const [stickyScrolling, setStickyScrolling] = useState2(true);
396
+ useEffect(() => {
397
+ const container = containerRef.current;
398
+ if (!pinToBottom || !container) {
399
+ return;
400
+ }
401
+ const handler = () => {
402
+ const bottom = container.scrollHeight - container.scrollTop - container.clientHeight === 0;
403
+ setStickyScrolling(bottom);
404
+ };
405
+ container.addEventListener("scroll", handler);
406
+ return () => container.removeEventListener("scroll", handler);
407
+ }, []);
408
+ useEffect(() => {
409
+ if (!pinToBottom || !containerRef.current || !stickyScrolling) {
410
+ return;
411
+ }
412
+ containerRef.current?.scroll({
413
+ top: containerRef.current.scrollHeight
414
+ });
415
+ }, [
416
+ data
417
+ ]);
418
+ return containerRef;
419
+ };
420
+ var TableHead = ({ state, headers, expand, header, debug, fullWidth, border, slots }) => {
421
+ return /* @__PURE__ */ React2.createElement("thead", {
422
+ className: mx2(header ? [
423
+ "sticky top-0 z-10"
424
+ ] : "collapse")
425
+ }, headers.map((headerGroup) => {
426
+ return (
427
+ // Group element to hover resize handles.
428
+ /* @__PURE__ */ React2.createElement("tr", {
429
+ key: headerGroup.id,
430
+ className: "font-light group"
431
+ }, slots?.margin && /* @__PURE__ */ React2.createElement("th", {
432
+ className: mx2(slots?.margin?.className)
433
+ }), debug && /* @__PURE__ */ React2.createElement("th", {
434
+ className: "text-left",
435
+ style: {
436
+ width: 32
437
+ }
438
+ }, "#"), headerGroup.headers.map((header2) => {
439
+ return /* @__PURE__ */ React2.createElement("th", {
440
+ key: header2.id,
441
+ style: {
442
+ // Don't set width if fullWidth and no explicit size.
443
+ width: fullWidth && header2.column.columnDef.meta?.expand ? void 0 : header2.getSize()
444
+ },
445
+ // Relative for resize handle.
446
+ // TODO(burdon): Border scrolls with main content.
447
+ // https://stackoverflow.com/questions/50361698/border-style-do-not-work-with-sticky-position-element
448
+ className: mx2("relative text-left", border && groupBorder, slots?.header?.className, header2.column.columnDef.meta?.slots?.header?.className)
449
+ }, !header2 || header2.isPlaceholder ? null : flexRender(header2.column.columnDef.header, header2.getContext()), header2.column.columnDef.meta?.resizable && /* @__PURE__ */ React2.createElement("div", {
450
+ className: mx2("absolute top-0 pl-1 h-full z-[10] w-[7px] -right-[5px]", "cursor-col-resize select-none touch-none opacity-20 hover:opacity-100", header2.column.getIsResizing() && "hidden"),
451
+ style: {
452
+ transform: header2.column.getIsResizing() ? `translateX(${state.columnSizingInfo.deltaOffset}px)` : void 0
453
+ },
454
+ onMouseDown: header2.getResizeHandler(),
455
+ onTouchStart: header2.getResizeHandler()
456
+ }, /* @__PURE__ */ React2.createElement("div", {
457
+ className: "flex group-hover:bg-neutral-700 -ml-[2px] w-[1px] h-full"
458
+ })));
459
+ }), expand && /* @__PURE__ */ React2.createElement("th", null), slots?.margin && /* @__PURE__ */ React2.createElement("th", {
460
+ className: mx2(slots?.margin?.className)
461
+ }))
462
+ );
463
+ }));
464
+ };
465
+ var TableBody = ({ keyAccessor, rows, rowSelection, focus, onFocus, onSelect, debug, border, expand, slots }) => {
466
+ return /* @__PURE__ */ React2.createElement("tbody", null, rows.map((row) => {
467
+ return /* @__PURE__ */ React2.createElement("tr", {
468
+ key: keyAccessor ? keyAccessor(row.original) : row.id,
469
+ onClick: () => onSelect?.(row),
470
+ role: "button",
471
+ className: mx2("group", rowSelection[row.id] && slots?.selected?.className, focus === row.id && slots?.focus?.className, slots?.row?.className)
472
+ }, slots?.margin && /* @__PURE__ */ React2.createElement("td", {
473
+ className: mx2(slots?.margin?.className)
474
+ }, /* @__PURE__ */ React2.createElement("button", {
475
+ role: "button",
476
+ style: {
477
+ width: 1,
478
+ height: 1
479
+ },
480
+ className: "focus:outline-none",
481
+ onFocus: () => onFocus?.(keyAccessor ? keyAccessor(row.original) : row.id),
482
+ onBlur: () => onFocus?.(void 0),
483
+ onKeyDown: (event) => {
484
+ switch (event.key) {
485
+ case "ArrowUp": {
486
+ break;
487
+ }
488
+ case "ArrowDown": {
489
+ break;
490
+ }
491
+ }
492
+ }
493
+ })), debug && /* @__PURE__ */ React2.createElement("td", null, row.id), row.getVisibleCells().map((cell) => {
494
+ return /* @__PURE__ */ React2.createElement("td", {
495
+ key: cell.id,
496
+ className: mx2(border && groupBorder, cell.column.columnDef.meta?.slots?.cell?.className)
497
+ }, flexRender(cell.column.columnDef.cell, {
498
+ className: "px-2",
499
+ ...cell.getContext()
500
+ }));
501
+ }), expand && /* @__PURE__ */ React2.createElement("td", null), slots?.margin && /* @__PURE__ */ React2.createElement("td", {
502
+ className: mx2(slots?.margin?.className)
503
+ }));
504
+ }));
505
+ };
506
+ var TableFoot = ({ footers, expand, slots, debug, border }) => {
507
+ return /* @__PURE__ */ React2.createElement("tfoot", {
508
+ className: mx2("sticky bottom-0 z-[10]", slots?.footer?.className)
509
+ }, footers.map((footerGroup) => /* @__PURE__ */ React2.createElement("tr", {
510
+ key: footerGroup.id,
511
+ className: "font-thin"
512
+ }, slots?.margin && /* @__PURE__ */ React2.createElement("th", {
513
+ className: mx2(slots?.margin?.className)
514
+ }), debug && /* @__PURE__ */ React2.createElement("th", null), footerGroup.headers.map((footer) => {
515
+ return /* @__PURE__ */ React2.createElement("th", {
516
+ key: footer.id,
517
+ className: mx2(border && groupBorder, "text-left", slots?.footer?.className, footer.column.columnDef.meta?.slots?.footer?.className)
518
+ }, footer.isPlaceholder ? null : flexRender(footer.column.columnDef.footer, footer.getContext()));
519
+ }), expand && /* @__PURE__ */ React2.createElement("th", null), slots?.margin && /* @__PURE__ */ React2.createElement("th", {
520
+ className: mx2(slots?.margin?.className)
521
+ }))));
522
+ };
523
+
524
+ // packages/ui/react-ui-table/src/helpers.tsx
525
+ import { useFocusFinders } from "@fluentui/react-tabster";
526
+ import { Check as Check2, ClipboardText, X as X2 } from "@phosphor-icons/react";
527
+ import { createColumnHelper } from "@tanstack/react-table";
528
+ import format from "date-fns/format";
529
+ import formatDistanceToNow from "date-fns/formatDistanceToNow";
530
+ import defaultsDeep from "lodash.defaultsdeep";
531
+ import React3, { useEffect as useEffect2, useRef as useRef3, useState as useState3 } from "react";
532
+ import { ComboBox, Input as Input2, Tooltip } from "@dxos/react-ui";
533
+ import { getSize as getSize2, mx as mx3 } from "@dxos/react-ui-theme";
534
+ import { stripUndefinedValues } from "@dxos/util";
535
+ var findNextFocusable = (findFirstFocusable, container) => {
536
+ const next = findFirstFocusable(container);
537
+ if (next) {
538
+ if (next?.tagName === "INPUT") {
539
+ return next;
540
+ }
541
+ if (container.nextElementSibling) {
542
+ return findNextFocusable(findFirstFocusable, container.nextSibling);
543
+ }
544
+ }
545
+ };
546
+ var createColumnBuilder = () => ({
547
+ helper: createColumnHelper(),
548
+ builder: new ColumnBuilder()
549
+ });
550
+ var defaults = (options, ...sources) => {
551
+ return stripUndefinedValues(defaultsDeep({}, options, ...sources));
552
+ };
553
+ var CellSelector = ({ model, value: _value, onUpdate }) => {
554
+ const [edit, setEdit] = useState3(false);
555
+ const [value, setValue] = useState3();
556
+ useEffect2(() => setValue(_value ? {
557
+ id: model.getId(_value),
558
+ label: model.getText(_value)
559
+ } : void 0), [
560
+ _value
561
+ ]);
562
+ const [items, setItems] = useState3([]);
563
+ const handleUpdate = async (text) => {
564
+ const items2 = await model.query(text);
565
+ setItems(items2.map((item) => ({
566
+ id: model.getId(item),
567
+ label: model.getText(item),
568
+ data: item
569
+ })));
570
+ };
571
+ if (!edit) {
572
+ const text = _value ? model.getText(_value) : void 0;
573
+ return (
574
+ // TODO(burdon): Hack to prevent div from collapsing.
575
+ /* @__PURE__ */ React3.createElement("div", {
576
+ className: mx3("w-full px-2", !text && "opacity-0"),
577
+ onClick: () => setEdit(true)
578
+ }, text ?? "-")
579
+ );
580
+ }
581
+ return /* @__PURE__ */ React3.createElement(ComboBox.Root, {
582
+ items,
583
+ value,
584
+ onChange: (value2) => onUpdate(value2?.data),
585
+ onInputChange: handleUpdate
586
+ }, /* @__PURE__ */ React3.createElement(ComboBox.Input, null), /* @__PURE__ */ React3.createElement(ComboBox.Content, null, items.map((item) => /* @__PURE__ */ React3.createElement(ComboBox.Item, {
587
+ key: item.id,
588
+ item
589
+ }, item.label))));
590
+ };
591
+ var ColumnBuilder = class {
592
+ /**
593
+ * Select value
594
+ */
595
+ select({ label, className, model, onUpdate, ...props }) {
596
+ return defaults(props, {
597
+ minSize: 100,
598
+ header: (column) => {
599
+ return /* @__PURE__ */ React3.createElement("div", {
600
+ className: "truncate"
601
+ }, label ?? column.header.id);
602
+ },
603
+ cell: model && onUpdate ? (cell) => /* @__PURE__ */ React3.createElement(CellSelector, {
604
+ model,
605
+ value: cell.getValue(),
606
+ onUpdate: (value) => onUpdate?.(cell.row.original, cell.column.id, value)
607
+ }) : (cell) => {
608
+ const value = cell.getValue();
609
+ return /* @__PURE__ */ React3.createElement("div", {
610
+ className: mx3("truncate", className)
611
+ }, value ? model.getText(value) : "");
612
+ }
613
+ });
614
+ }
615
+ /**
616
+ * String formats.
617
+ */
618
+ string({ label, className, onUpdate, ...props } = {}) {
619
+ return defaults(props, {
620
+ minSize: 100,
621
+ // TODO(burdon): Default.
622
+ header: (column) => {
623
+ return /* @__PURE__ */ React3.createElement("div", {
624
+ className: "truncate"
625
+ }, label ?? column.header.id);
626
+ },
627
+ cell: onUpdate ? (cell) => {
628
+ const initialValue = cell.getValue();
629
+ const [value, setValue] = useState3(initialValue);
630
+ const inputRef = useRef3(null);
631
+ const { findFirstFocusable } = useFocusFinders();
632
+ const handleSave = (focusNext = false) => {
633
+ if (value === initialValue) {
634
+ return;
635
+ }
636
+ onUpdate?.(cell.row.original, cell.column.id, value);
637
+ if (focusNext) {
638
+ const rowElement = inputRef.current?.parentElement?.parentElement;
639
+ setTimeout(() => {
640
+ const next = findNextFocusable(findFirstFocusable, rowElement);
641
+ next?.focus();
642
+ });
643
+ }
644
+ };
645
+ const handleCancel = () => {
646
+ setValue(initialValue);
647
+ };
648
+ const rows = cell.table.getRowModel().flatRows;
649
+ const columns = cell.table.getVisibleFlatColumns();
650
+ const placeholder = cell.row.index === rows.length - 1 && columns[0].id === cell.column.id;
651
+ return /* @__PURE__ */ React3.createElement(Input2.Root, null, /* @__PURE__ */ React3.createElement(Input2.TextInput, {
652
+ ref: inputRef,
653
+ variant: "subdued",
654
+ placeholder: placeholder ? "Add row..." : void 0,
655
+ classNames: "px-2",
656
+ value: value ?? "",
657
+ onBlur: () => handleSave(false),
658
+ onChange: (event) => setValue(event.target.value),
659
+ onKeyDown: (event) => event.key === "Enter" && handleSave(true) || event.key === "Escape" && handleCancel()
660
+ }));
661
+ } : (cell) => {
662
+ const value = cell.getValue();
663
+ return /* @__PURE__ */ React3.createElement("div", {
664
+ className: mx3("truncate", className)
665
+ }, value);
666
+ }
667
+ });
668
+ }
669
+ /**
670
+ * Number formats.
671
+ */
672
+ number({ label, minSize, className, digits, onUpdate, ...props } = {}) {
673
+ return defaults(props, {
674
+ size: 100,
675
+ minSize: 100,
676
+ header: (cell) => /* @__PURE__ */ React3.createElement("div", {
677
+ className: "w-full truncate text-right"
678
+ }, label ?? cell.header.id),
679
+ cell: (cell) => {
680
+ const value = cell.getValue();
681
+ const [text, setText] = useState3();
682
+ const handleEdit = () => {
683
+ if (onUpdate) {
684
+ setText(value !== void 0 ? String(value) : "");
685
+ }
686
+ };
687
+ const handleSave = () => {
688
+ const value2 = text?.trim().length ? Number(text) : NaN;
689
+ onUpdate?.(cell.row.original, cell.column.id, isNaN(value2) ? void 0 : value2);
690
+ setText(void 0);
691
+ };
692
+ const handleCancel = () => {
693
+ setText(void 0);
694
+ };
695
+ if (text !== void 0) {
696
+ return /* @__PURE__ */ React3.createElement("div", {
697
+ className: mx3("grow text-right font-mono", className)
698
+ }, /* @__PURE__ */ React3.createElement(Input2.Root, null, /* @__PURE__ */ React3.createElement(Input2.TextInput, {
699
+ autoFocus: true,
700
+ value: text,
701
+ classNames: "px-2",
702
+ onBlur: handleSave,
703
+ onChange: (event) => setText(event.target.value),
704
+ onKeyDown: (event) => event.key === "Escape" && handleCancel() || event.key === "Enter" && handleSave()
705
+ })));
706
+ }
707
+ return /* @__PURE__ */ React3.createElement("div", {
708
+ className: mx3('grow w-full text-right font-mono empty:after:content-["\xA0"]', className),
709
+ onClick: handleEdit
710
+ }, value?.toLocaleString(void 0, {
711
+ minimumFractionDigits: digits ?? 0,
712
+ maximumFractionDigits: digits ?? 2
713
+ }));
714
+ }
715
+ });
716
+ }
717
+ /**
718
+ * Date formats.
719
+ */
720
+ // TODO(burdon): Date picker (pluggable renderers?)
721
+ date({ label, format: formatSpec, relative, className, ...props } = {}) {
722
+ return defaults(props, {
723
+ size: 220,
724
+ minSize: 100,
725
+ header: (cell) => /* @__PURE__ */ React3.createElement("div", null, label ?? cell.header.id),
726
+ cell: (cell) => {
727
+ const value = cell.getValue();
728
+ try {
729
+ const str = value ? formatSpec ? format(value, formatSpec) : relative ? formatDistanceToNow(value, {
730
+ addSuffix: true
731
+ }) : value.toISOString() : void 0;
732
+ return /* @__PURE__ */ React3.createElement("div", {
733
+ className: mx3("font-mono", className)
734
+ }, str);
735
+ } catch (err) {
736
+ console.log(value);
737
+ return null;
738
+ }
739
+ }
740
+ });
741
+ }
742
+ /**
743
+ * PublicKey with tooltip.
744
+ */
745
+ key({ label, tooltip, ...props } = {}) {
746
+ return defaults(props, {
747
+ size: 86,
748
+ minSize: 86,
749
+ header: (cell) => /* @__PURE__ */ React3.createElement("div", null, label ?? cell.header.id),
750
+ cell: (cell) => {
751
+ const value = cell.getValue();
752
+ if (!value) {
753
+ return;
754
+ }
755
+ const element = /* @__PURE__ */ React3.createElement("div", {
756
+ className: "font-mono font-thin text-green-500 dark:text-green-300"
757
+ }, value.truncate());
758
+ if (!tooltip) {
759
+ return element;
760
+ }
761
+ return /* @__PURE__ */ React3.createElement("div", {
762
+ className: "group inline-flex gap-2 items-center"
763
+ }, /* @__PURE__ */ React3.createElement(Tooltip.Provider, null, /* @__PURE__ */ React3.createElement(Tooltip.Root, null, /* @__PURE__ */ React3.createElement(Tooltip.Trigger, {
764
+ asChild: true
765
+ }, element), /* @__PURE__ */ React3.createElement(Tooltip.Content, {
766
+ side: "right"
767
+ }, /* @__PURE__ */ React3.createElement(Tooltip.Arrow, null), /* @__PURE__ */ React3.createElement(ClipboardText, {
768
+ onClick: (ev) => {
769
+ ev.stopPropagation();
770
+ void navigator.clipboard.writeText(value.toHex());
771
+ }
772
+ })))));
773
+ }
774
+ });
775
+ }
776
+ /**
777
+ * Checkbox.
778
+ */
779
+ checkbox({ label, className, onUpdate, ...props } = {}) {
780
+ return defaults(props, {
781
+ size: 40,
782
+ minSize: 40,
783
+ header: (column) => /* @__PURE__ */ React3.createElement("div", {
784
+ className: "flex grow justify-center"
785
+ }, label ?? column.header.id),
786
+ cell: (cell) => {
787
+ const value = cell.getValue();
788
+ return /* @__PURE__ */ React3.createElement("div", {
789
+ className: "flex grow justify-center"
790
+ }, /* @__PURE__ */ React3.createElement(Input2.Root, null, /* @__PURE__ */ React3.createElement(Input2.Checkbox, {
791
+ onClick: (event) => event.stopPropagation(),
792
+ classNames: className,
793
+ disabled: !onUpdate,
794
+ checked: !!value,
795
+ onCheckedChange: (value2) => {
796
+ onUpdate?.(cell.row.original, cell.column.id, !!value2);
797
+ }
798
+ })));
799
+ }
800
+ });
801
+ }
802
+ /**
803
+ * Icon based on boolean value.
804
+ */
805
+ icon({ label, size, on, off, ...props } = {}) {
806
+ const IconOn = on?.Icon ?? Check2;
807
+ const IconOff = off?.Icon ?? X2;
808
+ return defaults(props, {
809
+ size: size ?? 32,
810
+ header: (column) => /* @__PURE__ */ React3.createElement("div", {
811
+ className: "justify-center"
812
+ }, label ?? column.header.id),
813
+ cell: (cell) => {
814
+ const value = cell.getValue();
815
+ if (value) {
816
+ return /* @__PURE__ */ React3.createElement("div", {
817
+ className: "flex grow justify-center"
818
+ }, /* @__PURE__ */ React3.createElement(IconOn, {
819
+ className: mx3(getSize2(6), on?.className ?? "text-green-700")
820
+ }));
821
+ } else if (value === false) {
822
+ return /* @__PURE__ */ React3.createElement("div", {
823
+ className: "flex grow justify-center"
824
+ }, /* @__PURE__ */ React3.createElement(IconOff, {
825
+ className: mx3(getSize2(6), off?.className ?? "text-red-700")
826
+ }));
827
+ } else {
828
+ return null;
829
+ }
830
+ }
831
+ });
832
+ }
833
+ };
834
+ export {
835
+ ColumnBuilder,
836
+ ColumnMenu,
837
+ ColumnPanel,
838
+ Table,
839
+ columnTypes,
840
+ createColumnBuilder,
841
+ defaultTableSlots,
842
+ updateSelection
843
+ };
844
+ //# sourceMappingURL=index.mjs.map