@dxos/react-ui-table 0.3.7 → 0.3.8-main.0694b57

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 (56) hide show
  1. package/dist/lib/browser/index.mjs +546 -475
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/Cell/Cell.d.ts +12 -0
  5. package/dist/types/src/components/Cell/Cell.d.ts.map +1 -0
  6. package/dist/types/src/components/Cell/CellCombobox.d.ts +12 -0
  7. package/dist/types/src/components/Cell/CellCombobox.d.ts.map +1 -0
  8. package/dist/types/src/components/Cell/index.d.ts +2 -0
  9. package/dist/types/src/components/Cell/index.d.ts.map +1 -0
  10. package/dist/types/src/components/Table/Table.d.ts +3 -0
  11. package/dist/types/src/components/Table/Table.d.ts.map +1 -0
  12. package/dist/types/src/components/{Table.stories.d.ts → Table/Table.stories.d.ts} +4 -7
  13. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -0
  14. package/dist/types/src/components/Table/TableBody.d.ts +11 -0
  15. package/dist/types/src/components/Table/TableBody.d.ts.map +1 -0
  16. package/dist/types/src/components/Table/TableContext.d.ts +14 -0
  17. package/dist/types/src/components/Table/TableContext.d.ts.map +1 -0
  18. package/dist/types/src/components/Table/TableFooter.d.ts +7 -0
  19. package/dist/types/src/components/Table/TableFooter.d.ts.map +1 -0
  20. package/dist/types/src/components/Table/TableHead.d.ts +8 -0
  21. package/dist/types/src/components/Table/TableHead.d.ts.map +1 -0
  22. package/dist/types/src/components/Table/index.d.ts +3 -0
  23. package/dist/types/src/components/Table/index.d.ts.map +1 -0
  24. package/dist/types/src/components/Table/props.d.ts +37 -0
  25. package/dist/types/src/components/Table/props.d.ts.map +1 -0
  26. package/dist/types/src/components/index.d.ts +1 -0
  27. package/dist/types/src/components/index.d.ts.map +1 -1
  28. package/dist/types/src/helpers.d.ts +20 -14
  29. package/dist/types/src/helpers.d.ts.map +1 -1
  30. package/dist/types/src/theme.d.ts +32 -33
  31. package/dist/types/src/theme.d.ts.map +1 -1
  32. package/dist/types/src/translations.d.ts +10 -0
  33. package/dist/types/src/translations.d.ts.map +1 -0
  34. package/dist/types/src/types.d.ts +13 -10
  35. package/dist/types/src/types.d.ts.map +1 -1
  36. package/package.json +12 -10
  37. package/src/components/Cell/Cell.tsx +43 -0
  38. package/src/components/Cell/CellCombobox.tsx +97 -0
  39. package/src/components/Cell/index.ts +5 -0
  40. package/src/components/{Table.stories.tsx → Table/Table.stories.tsx} +51 -50
  41. package/src/components/Table/Table.tsx +179 -0
  42. package/src/components/Table/TableBody.tsx +69 -0
  43. package/src/components/Table/TableContext.tsx +28 -0
  44. package/src/components/Table/TableFooter.tsx +42 -0
  45. package/src/components/Table/TableHead.tsx +79 -0
  46. package/src/components/Table/index.ts +6 -0
  47. package/src/components/Table/props.ts +53 -0
  48. package/src/components/index.ts +1 -0
  49. package/src/helpers.tsx +250 -282
  50. package/src/theme.ts +95 -63
  51. package/src/translations.ts +15 -0
  52. package/src/types.ts +15 -10
  53. package/dist/types/src/components/Table.d.ts +0 -29
  54. package/dist/types/src/components/Table.d.ts.map +0 -1
  55. package/dist/types/src/components/Table.stories.d.ts.map +0 -1
  56. package/src/components/Table.tsx +0 -551
package/src/helpers.tsx CHANGED
@@ -2,35 +2,28 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { useFocusFinders } from '@fluentui/react-tabster';
6
- import { CaretDown, Check, ClipboardText, type Icon, X } from '@phosphor-icons/react';
7
- import { createColumnHelper, type ColumnDef, type ColumnMeta, type RowData } from '@tanstack/react-table';
5
+ import { Check, ClipboardText, type Icon, X } from '@phosphor-icons/react';
6
+ import {
7
+ createColumnHelper,
8
+ type ColumnDef,
9
+ type ColumnMeta,
10
+ type RowData,
11
+ type ColumnHelper,
12
+ type CellContext,
13
+ } from '@tanstack/react-table';
8
14
  import format from 'date-fns/format';
9
15
  import formatDistanceToNow from 'date-fns/formatDistanceToNow';
10
16
  import defaultsDeep from 'lodash.defaultsdeep';
11
- import React, { useCallback, useRef, useState } from 'react';
17
+ import React, { useRef, useState } from 'react';
12
18
 
13
19
  import { type PublicKey } from '@dxos/keys';
14
- import { Button, Input, Popover, Tooltip } from '@dxos/react-ui';
15
- import { SearchList } from '@dxos/react-ui-searchlist';
16
- import { descriptionText, getSize, mx } from '@dxos/react-ui-theme';
20
+ import { type ClassNameValue, Input, Tooltip } from '@dxos/react-ui';
21
+ import { getSize, mx } from '@dxos/react-ui-theme';
17
22
  import { stripUndefinedValues } from '@dxos/util';
18
23
 
19
- // TODO(burdon): Factor out hack to find next focusable element (extend useFocusFinders)?
20
- const findNextFocusable = (
21
- findFirstFocusable: (container: HTMLElement) => HTMLElement | null | undefined,
22
- container: HTMLElement,
23
- ): HTMLElement | undefined => {
24
- const next = findFirstFocusable(container as any);
25
- if (next) {
26
- if (next?.tagName === 'INPUT') {
27
- return next;
28
- }
29
- if (container.nextElementSibling) {
30
- return findNextFocusable(findFirstFocusable, container.nextSibling as any);
31
- }
32
- }
33
- };
24
+ import { CellCombobox } from './components';
25
+ import { useTableContext } from './components/Table/TableContext';
26
+ import { textPadding } from './theme';
34
27
 
35
28
  export type ValueUpdater<TData extends RowData, TValue> = (row: TData, id: string, value: TValue) => void;
36
29
 
@@ -45,22 +38,23 @@ export const createColumnBuilder = <TData extends RowData>() => ({
45
38
  export type BaseColumnOptions<TData, TValue> = Partial<ColumnDef<TData, TValue>> & {
46
39
  meta?: ColumnMeta<TData, TValue>;
47
40
  label?: string;
48
- className?: string;
41
+ classNames?: ClassNameValue;
49
42
  onUpdate?: ValueUpdater<TData, TValue | undefined>;
50
43
  };
51
44
 
52
45
  // TODO(burdon): Better abstraction?
53
- export type SelectQueryModel<TData extends RowData> = {
46
+ export type SearchListQueryModel<TData extends RowData> = {
54
47
  getId(object: TData): string;
55
48
  getText(object: TData): string;
56
49
  query(text?: string): Promise<TData[]>;
57
50
  };
58
51
 
59
- export type SelectColumnOptions<TData extends RowData> = BaseColumnOptions<TData, any> & {
60
- model: SelectQueryModel<TData>;
52
+ export type ComboboxColumnOptions<TData extends RowData> = BaseColumnOptions<TData, any> & {
53
+ model: SearchListQueryModel<TData>;
61
54
  };
62
55
 
63
56
  export type StringColumnOptions<TData extends RowData> = BaseColumnOptions<TData, string> & {};
57
+ export type SelectRowColumnOptions<TData extends RowData> = BaseColumnOptions<TData, string> & {};
64
58
 
65
59
  export type NumberColumnOptions<TData extends RowData> = BaseColumnOptions<TData, number> & {
66
60
  digits?: number;
@@ -75,7 +69,7 @@ export type DateColumnOptions<TData extends RowData> = BaseColumnOptions<TData,
75
69
  relative?: boolean;
76
70
  };
77
71
 
78
- export type BooleanColumnOptions<TData extends RowData> = BaseColumnOptions<TData, boolean> & {};
72
+ export type SwitchColumnOptions<TData extends RowData> = BaseColumnOptions<TData, boolean> & {};
79
73
 
80
74
  export type IconColumnOptions<TData extends RowData> = BaseColumnOptions<TData, boolean> & {
81
75
  on?: {
@@ -95,85 +89,145 @@ const defaults = <TData extends RowData, TValue>(
95
89
  return stripUndefinedValues(defaultsDeep({}, options, ...sources));
96
90
  };
97
91
 
98
- type CellSelectorItems<TData extends RowData> = Record<string, { label: string; data: TData }>;
99
-
100
- const initialItems = <TData extends RowData>(model: SelectQueryModel<TData>, value?: TData) =>
101
- value
102
- ? {
103
- [model.getId(value)]: { label: model.getText(value), data: value },
104
- }
105
- : {};
106
-
107
- const CellSelector = <TData extends RowData>({
108
- model,
109
- value,
110
- onValueChange,
111
- }: {
112
- model: SelectQueryModel<TData>;
113
- value: TData;
114
- onValueChange: (value: TData) => void;
115
- }) => {
116
- const [items, setItems] = useState<CellSelectorItems<TData>>(initialItems(model, value));
117
- const [loading, setLoading] = useState(false);
118
- const [searchInputValue, setSearchInputValue] = useState('');
119
- const handleSearchInputValueChange = async (text?: string) => {
120
- setSearchInputValue(text ?? '');
121
- setLoading(true);
122
- const items = await model.query(text);
123
- setItems(
124
- items.reduce((acc: CellSelectorItems<TData>, item) => {
125
- acc[model.getId(item)] = {
126
- label: model.getText(item),
127
- data: item,
128
- };
129
- return acc;
130
- }, initialItems(model, value)),
131
- );
132
- setLoading(false);
92
+ const ComboboxBuilderCell = <TData extends RowData>(cellContext: CellContext<TData, TData>) => {
93
+ const { model, onUpdate } = cellContext.column.columnDef.meta as ColumnMeta<TData, TData>;
94
+ return (
95
+ <CellCombobox<TData>
96
+ model={model!}
97
+ value={cellContext.getValue()}
98
+ onValueChange={(value) => onUpdate?.(cellContext.row.original, cellContext.column.id, value)}
99
+ />
100
+ );
101
+ };
102
+
103
+ const StringBuilderCell = <TData extends RowData>(cellContext: CellContext<TData, string>) => {
104
+ const { onUpdate } = cellContext.column.columnDef.meta as ColumnMeta<TData, string>;
105
+ // https://tanstack.com/table/v8/docs/examples/react/editable-data
106
+ const initialValue = cellContext.getValue();
107
+ const [value, setValue] = useState(initialValue);
108
+ const inputRef = useRef<HTMLInputElement>(null);
109
+
110
+ const handleSave = () => {
111
+ if (value === initialValue) {
112
+ return;
113
+ }
114
+ onUpdate?.(cellContext.row.original, cellContext.column.id, value);
115
+ };
116
+
117
+ const handleCancel = () => {
118
+ setValue(initialValue);
133
119
  };
134
120
 
135
- const handleValueChange = useCallback(
136
- (id: string) => {
137
- onValueChange(items[id]?.data);
138
- },
139
- [items, onValueChange],
121
+ // Check if first input column of last row.
122
+ const rows = cellContext.table.getRowModel().flatRows;
123
+ const columns = cellContext.table.getVisibleFlatColumns();
124
+ const placeholder = cellContext.row.index === rows.length - 1 && columns[0].id === cellContext.column.id;
125
+
126
+ // TODO(burdon): Don't render inputs unless mouse over (Show ellipsis when div)?
127
+ return (
128
+ <Input.Root>
129
+ <Input.TextInput
130
+ ref={inputRef}
131
+ variant='subdued'
132
+ placeholder={placeholder ? 'Add row...' : undefined}
133
+ classNames={['is-full', textPadding]}
134
+ value={value ?? ''}
135
+ onBlur={() => handleSave()}
136
+ onChange={(event) => setValue(event.target.value)}
137
+ onKeyDown={(event) => (event.key === 'Enter' && handleSave()) || (event.key === 'Escape' && handleCancel())}
138
+ />
139
+ </Input.Root>
140
140
  );
141
+ };
142
+
143
+ const NumberBuilderCell = <TData extends RowData>(cellContext: CellContext<TData, number>) => {
144
+ const { onUpdate, cell, digits } = cellContext.column.columnDef.meta as ColumnMeta<TData, string>;
145
+ const value = cellContext.getValue();
146
+ const [text, setText] = useState<string>();
147
+
148
+ const handleEdit = () => {
149
+ if (onUpdate) {
150
+ setText(value !== undefined ? String(value) : '');
151
+ }
152
+ };
153
+
154
+ // TODO(burdon): Property is encoded as float (e.g., 6.1 => 6.099)
155
+ const handleSave = () => {
156
+ const value = text?.trim().length ? Number(text) : NaN;
157
+ onUpdate?.(cellContext.row.original, cellContext.column.id, isNaN(value) ? undefined : value);
158
+ setText(undefined);
159
+ };
160
+
161
+ const handleCancel = () => {
162
+ setText(undefined);
163
+ };
164
+
165
+ if (text !== undefined) {
166
+ return (
167
+ <Input.Root>
168
+ <Input.TextInput
169
+ value={text}
170
+ classNames={['is-full text-end font-mono']}
171
+ onBlur={handleSave}
172
+ onChange={(event) => setText(event.target.value)}
173
+ onKeyDown={(event) => (event.key === 'Escape' && handleCancel()) || (event.key === 'Enter' && handleSave())}
174
+ />
175
+ </Input.Root>
176
+ );
177
+ }
141
178
 
142
179
  return (
143
- <Popover.Root>
144
- <Popover.Trigger asChild>
145
- <Button variant='ghost' classNames='is-full font-normal'>
146
- <span className='flex-1 text-start truncate'>{value ? model.getText(value) : ' '}</span>
147
- <CaretDown />
148
- </Button>
149
- </Popover.Trigger>
150
- <Popover.Portal>
151
- <Popover.Content side='bottom' collisionPadding={48} classNames='is-[--radix-popover-trigger-width]'>
152
- <SearchList.Root shouldFilter={false}>
153
- <SearchList.Input value={searchInputValue} onValueChange={handleSearchInputValueChange} />
154
- <Popover.Viewport>
155
- <SearchList.Content>
156
- {searchInputValue.length < 1 ? (
157
- <p className={mx('text-center', descriptionText)}>Start typing to search</p>
158
- ) : loading ? (
159
- <p className={mx('text-center', descriptionText)}>Searching…</p>
160
- ) : (
161
- <>
162
- <SearchList.Empty classNames={['text-center', descriptionText]}>No results</SearchList.Empty>
163
- {Object.entries(items).map(([id, { label }]) => (
164
- <SearchList.Item key={id} value={id} onSelect={handleValueChange} classNames='truncate'>
165
- {label}
166
- </SearchList.Item>
167
- ))}
168
- </>
169
- )}
170
- </SearchList.Content>
171
- </Popover.Viewport>
172
- </SearchList.Root>
173
- <Popover.Arrow />
174
- </Popover.Content>
175
- </Popover.Portal>
176
- </Popover.Root>
180
+ <div
181
+ className={mx('is-full text-end font-mono empty:after:content-[" "]', textPadding, cell?.classNames)}
182
+ onClick={handleEdit}
183
+ >
184
+ {value?.toLocaleString(undefined, {
185
+ minimumFractionDigits: digits ?? 0,
186
+ maximumFractionDigits: digits ?? 2,
187
+ })}
188
+ </div>
189
+ );
190
+ };
191
+
192
+ const RowSelectorBuilderCell = <TData extends RowData>(cellContext: CellContext<TData, unknown>) => {
193
+ const { cell } = cellContext.column.columnDef.meta as ColumnMeta<TData, unknown>;
194
+ const { row } = cellContext;
195
+
196
+ const checked = row.getCanSelect()
197
+ ? row.getIsSelected()
198
+ : row.getCanSelectSubRows() && (row.getIsSomeSelected() ? 'indeterminate' : row.getIsAllSubRowsSelected());
199
+ return (
200
+ <Input.Root>
201
+ <Input.Checkbox
202
+ size={4}
203
+ classNames={['mli-auto', cell?.classNames]}
204
+ checked={checked}
205
+ onCheckedChange={(event) => {
206
+ if (row.getCanSelect()) {
207
+ row.getToggleSelectedHandler()(event);
208
+ }
209
+ }}
210
+ disabled={!(row.getCanSelect() || row.getCanSelectSubRows())}
211
+ />
212
+ </Input.Root>
213
+ );
214
+ };
215
+
216
+ const SwitchBuilderCell = <TData extends RowData>(cellContext: CellContext<TData, boolean>) => {
217
+ const { onUpdate } = cellContext.column.columnDef.meta as ColumnMeta<TData, boolean>;
218
+ const value = cellContext.getValue();
219
+ return (
220
+ <Input.Root>
221
+ <Input.Switch
222
+ classNames='block mli-auto'
223
+ onClick={(event) => event.stopPropagation()}
224
+ disabled={!onUpdate}
225
+ checked={!!value}
226
+ onCheckedChange={(value) => {
227
+ onUpdate?.(cellContext.row.original, cellContext.column.id, !!value);
228
+ }}
229
+ />
230
+ </Input.Root>
177
231
  );
178
232
  };
179
233
 
@@ -182,26 +236,31 @@ const CellSelector = <TData extends RowData>({
182
236
  */
183
237
  export class ColumnBuilder<TData extends RowData> {
184
238
  /**
185
- * Select value
239
+ * Combobox value
186
240
  */
187
- select({ label, className, model, onUpdate, ...props }: SelectColumnOptions<TData>): Partial<ColumnDef<TData, any>> {
241
+ combobox({
242
+ label,
243
+ classNames,
244
+ model,
245
+ onUpdate,
246
+ ...props
247
+ }: ComboboxColumnOptions<TData>): Partial<ColumnDef<TData, any>> {
188
248
  return defaults(props, {
189
249
  minSize: 100,
250
+ meta: {
251
+ model,
252
+ onUpdate,
253
+ cell: { classNames },
254
+ },
190
255
  header: (column) => {
191
- return <div className={'truncate'}>{label ?? column.header.id}</div>;
256
+ return label ?? column.header.id;
192
257
  },
193
258
  cell:
194
259
  model && onUpdate
195
- ? (cell) => (
196
- <CellSelector<any>
197
- model={model}
198
- value={cell.getValue()}
199
- onValueChange={(value) => onUpdate?.(cell.row.original, cell.column.id, value)}
200
- />
201
- )
202
- : (cell) => {
203
- const value = cell.getValue();
204
- return <div className={mx('truncate', className)}>{value ? model.getText(value) : ''}</div>;
260
+ ? ComboboxBuilderCell
261
+ : (cellContext) => {
262
+ const cellValue = cellContext.getValue();
263
+ return <div className={mx('truncate', classNames)}>{cellValue ? model?.getText(cellValue) : ''}</div>;
205
264
  },
206
265
  });
207
266
  }
@@ -209,69 +268,21 @@ export class ColumnBuilder<TData extends RowData> {
209
268
  /**
210
269
  * String formats.
211
270
  */
212
- string({ label, className, onUpdate, ...props }: StringColumnOptions<TData> = {}): Partial<ColumnDef<TData, string>> {
271
+ string({ label, classNames, onUpdate, ...props }: StringColumnOptions<TData> = {}): Partial<
272
+ ColumnDef<TData, string>
273
+ > {
213
274
  return defaults(props, {
214
275
  minSize: 100,
276
+ meta: { onUpdate, cell: { classNames } },
215
277
  // TODO(burdon): Default.
216
278
  header: (column) => {
217
- return <div className={'truncate'}>{label ?? column.header.id}</div>;
279
+ return label ?? column.header.id;
218
280
  },
219
281
  cell: onUpdate
220
- ? (cell) => {
221
- // https://tanstack.com/table/v8/docs/examples/react/editable-data
222
- const initialValue = cell.getValue();
223
- const [value, setValue] = useState(initialValue);
224
- const inputRef = useRef<HTMLInputElement>(null);
225
- const { findFirstFocusable } = useFocusFinders();
226
-
227
- const handleSave = (focusNext = false) => {
228
- if (value === initialValue) {
229
- return;
230
- }
231
-
232
- onUpdate?.(cell.row.original, cell.column.id, value);
233
-
234
- // TODO(burdon): More generally support keyboard navigation.
235
- if (focusNext) {
236
- const rowElement = inputRef.current?.parentElement?.parentElement;
237
- // TODO(burdon): Hack to wait for next row to render.
238
- setTimeout(() => {
239
- const next = findNextFocusable(findFirstFocusable, rowElement as HTMLElement);
240
- next?.focus();
241
- });
242
- }
243
- };
244
-
245
- const handleCancel = () => {
246
- setValue(initialValue);
247
- };
248
-
249
- // Check if first input column of last row.
250
- const rows = cell.table.getRowModel().flatRows;
251
- const columns = cell.table.getVisibleFlatColumns();
252
- const placeholder = cell.row.index === rows.length - 1 && columns[0].id === cell.column.id;
253
-
254
- // TODO(burdon): Don't render inputs unless mouse over (Show ellipsis when div)?
255
- return (
256
- <Input.Root>
257
- <Input.TextInput
258
- ref={inputRef}
259
- variant='subdued'
260
- placeholder={placeholder ? 'Add row...' : undefined}
261
- classNames={'px-2'}
262
- value={(value as string) ?? ''}
263
- onBlur={() => handleSave(false)}
264
- onChange={(event) => setValue(event.target.value)}
265
- onKeyDown={(event) =>
266
- (event.key === 'Enter' && handleSave(true)) || (event.key === 'Escape' && handleCancel())
267
- }
268
- />
269
- </Input.Root>
270
- );
271
- }
282
+ ? StringBuilderCell
272
283
  : (cell) => {
273
284
  const value = cell.getValue();
274
- return <div className={mx('truncate', className)}>{value}</div>;
285
+ return <div className={mx('truncate', textPadding, classNames)}>{value}</div>;
275
286
  },
276
287
  });
277
288
  }
@@ -279,65 +290,15 @@ export class ColumnBuilder<TData extends RowData> {
279
290
  /**
280
291
  * Number formats.
281
292
  */
282
- number({ label, minSize, className, digits, onUpdate, ...props }: NumberColumnOptions<TData> = {}): Partial<
293
+ number({ label, minSize, classNames, digits, onUpdate, ...props }: NumberColumnOptions<TData> = {}): Partial<
283
294
  ColumnDef<TData, number>
284
295
  > {
285
296
  return defaults(props, {
286
297
  size: 100,
287
298
  minSize: 100,
288
- header: (cell) => <div className='w-full truncate text-right'>{label ?? cell.header.id}</div>,
289
- cell: (cell) => {
290
- const value = cell.getValue();
291
- const [text, setText] = useState<string>();
292
-
293
- const handleEdit = () => {
294
- if (onUpdate) {
295
- setText(value !== undefined ? String(value) : '');
296
- }
297
- };
298
-
299
- // TODO(burdon): Property is encoded as float (e.g., 6.1 => 6.099)
300
- const handleSave = () => {
301
- const value = text?.trim().length ? Number(text) : NaN;
302
- onUpdate?.(cell.row.original, cell.column.id, isNaN(value) ? undefined : value);
303
- setText(undefined);
304
- };
305
-
306
- const handleCancel = () => {
307
- setText(undefined);
308
- };
309
-
310
- if (text !== undefined) {
311
- return (
312
- <div className={mx('grow text-right font-mono', className)}>
313
- <Input.Root>
314
- <Input.TextInput
315
- autoFocus
316
- value={text}
317
- classNames={'px-2'}
318
- onBlur={handleSave}
319
- onChange={(event) => setText(event.target.value)}
320
- onKeyDown={(event) =>
321
- (event.key === 'Escape' && handleCancel()) || (event.key === 'Enter' && handleSave())
322
- }
323
- />
324
- </Input.Root>
325
- </div>
326
- );
327
- }
328
-
329
- return (
330
- <div
331
- className={mx('grow w-full text-right font-mono empty:after:content-[" "]', className)}
332
- onClick={handleEdit}
333
- >
334
- {value?.toLocaleString(undefined, {
335
- minimumFractionDigits: digits ?? 0,
336
- maximumFractionDigits: digits ?? 2,
337
- })}
338
- </div>
339
- );
340
- },
299
+ meta: { onUpdate, digits, header: { classNames: 'text-end' }, cell: { classNames } },
300
+ header: (cell) => label ?? cell.header.id,
301
+ cell: NumberBuilderCell,
341
302
  });
342
303
  }
343
304
 
@@ -345,13 +306,14 @@ export class ColumnBuilder<TData extends RowData> {
345
306
  * Date formats.
346
307
  */
347
308
  // TODO(burdon): Date picker (pluggable renderers?)
348
- date({ label, format: formatSpec, relative, className, ...props }: DateColumnOptions<TData> = {}): Partial<
309
+ date({ label, format: formatSpec, relative, classNames, ...props }: DateColumnOptions<TData> = {}): Partial<
349
310
  ColumnDef<TData, Date>
350
311
  > {
351
312
  return defaults(props, {
352
313
  size: 220, // TODO(burdon): Depends on format.
353
314
  minSize: 100,
354
- header: (cell) => <div>{label ?? cell.header.id}</div>,
315
+ meta: { cell: { classNames } },
316
+ header: (cell) => label ?? cell.header.id,
355
317
  cell: (cell) => {
356
318
  const value = cell.getValue();
357
319
 
@@ -364,7 +326,7 @@ export class ColumnBuilder<TData extends RowData> {
364
326
  : value.toISOString()
365
327
  : undefined;
366
328
 
367
- return <div className={mx('font-mono', className)}>{str}</div>;
329
+ return <div className={mx(textPadding, classNames)}>{str}</div>;
368
330
  } catch (err) {
369
331
  console.log(value);
370
332
  return null;
@@ -378,9 +340,10 @@ export class ColumnBuilder<TData extends RowData> {
378
340
  */
379
341
  key({ label, tooltip, ...props }: KeyColumnOptions<TData> = {}): Partial<ColumnDef<TData, PublicKey>> {
380
342
  return defaults(props, {
381
- size: 86,
382
- minSize: 86,
383
- header: (cell) => <div>{label ?? cell.header.id}</div>,
343
+ size: 94,
344
+ minSize: 94,
345
+ meta: { cell: { classNames: ['font-mono', textPadding] } },
346
+ header: (cell) => label ?? cell.header.id,
384
347
  cell: (cell) => {
385
348
  const value = cell.getValue();
386
349
  if (!value) {
@@ -388,64 +351,77 @@ export class ColumnBuilder<TData extends RowData> {
388
351
  }
389
352
 
390
353
  // TODO(burdon): Factor out styles.
391
- const element = (
392
- <div className='font-mono font-thin text-green-500 dark:text-green-300'>{value.truncate()}</div>
393
- );
354
+ const element = value.truncate();
394
355
  if (!tooltip) {
395
356
  return element;
396
357
  }
397
358
 
398
359
  return (
399
- <div className='group inline-flex gap-2 items-center'>
400
- <Tooltip.Provider>
401
- <Tooltip.Root>
402
- <Tooltip.Trigger asChild>{element}</Tooltip.Trigger>
403
- <Tooltip.Content side='right'>
404
- <Tooltip.Arrow />
405
- <ClipboardText
406
- onClick={(ev) => {
407
- ev.stopPropagation(); // Prevent select row.
408
- void navigator.clipboard.writeText(value.toHex());
409
- }}
410
- />
411
- </Tooltip.Content>
412
- </Tooltip.Root>
413
- </Tooltip.Provider>
414
- </div>
360
+ <Tooltip.Provider>
361
+ <Tooltip.Root>
362
+ <Tooltip.Trigger>{element}</Tooltip.Trigger>
363
+ <Tooltip.Content side='right'>
364
+ <Tooltip.Arrow />
365
+ <ClipboardText
366
+ onClick={(ev) => {
367
+ ev.stopPropagation(); // Prevent select row.
368
+ void navigator.clipboard.writeText(value.toHex());
369
+ }}
370
+ />
371
+ </Tooltip.Content>
372
+ </Tooltip.Root>
373
+ </Tooltip.Provider>
415
374
  );
416
375
  },
417
376
  });
418
377
  }
419
378
 
420
379
  /**
421
- * Checkbox.
380
+ * Row selector
422
381
  */
423
- checkbox({ label, className, onUpdate, ...props }: BooleanColumnOptions<TData> = {}): Partial<
382
+ selectRow({
383
+ label,
384
+ classNames,
385
+ onUpdate,
386
+ id = 'selectRow',
387
+ ...props
388
+ }: SelectRowColumnOptions<TData> = {}): Parameters<ColumnHelper<TData>['display']>[0] {
389
+ return {
390
+ id,
391
+ size: 32,
392
+ minSize: 32,
393
+ maxSize: 32,
394
+ meta: { onUpdate, cell: { classNames } },
395
+ header: ({ table }) => {
396
+ const { rowsSelectable } = useTableContext('HELPER_SELECT_ROW_HEADER_CELL');
397
+ const checked = table.getIsSomeRowsSelected() ? 'indeterminate' : table.getIsAllRowsSelected();
398
+ return rowsSelectable === 'multi' ? (
399
+ <Input.Root>
400
+ <Input.Checkbox
401
+ size={4}
402
+ classNames={['mli-auto', classNames]}
403
+ checked={checked}
404
+ onCheckedChange={() => table.toggleAllRowsSelected()}
405
+ />
406
+ </Input.Root>
407
+ ) : null;
408
+ },
409
+ cell: RowSelectorBuilderCell,
410
+ };
411
+ }
412
+
413
+ /**
414
+ * Switch
415
+ */
416
+ switch({ label, classNames, onUpdate, ...props }: SwitchColumnOptions<TData> = {}): Partial<
424
417
  ColumnDef<TData, boolean>
425
418
  > {
426
419
  return defaults(props, {
427
- size: 40,
428
- minSize: 40,
429
- header: (column) => <div className={'flex grow justify-center'}>{label ?? column.header.id}</div>,
430
- cell: (cell) => {
431
- const value = cell.getValue();
432
- // TODO(burdon): Center.
433
- return (
434
- <div className='flex grow justify-center'>
435
- <Input.Root>
436
- <Input.Checkbox
437
- onClick={(event) => event.stopPropagation()}
438
- classNames={className}
439
- disabled={!onUpdate}
440
- checked={!!value}
441
- onCheckedChange={(value) => {
442
- onUpdate?.(cell.row.original, cell.column.id, !!value);
443
- }}
444
- />
445
- </Input.Root>
446
- </div>
447
- );
448
- },
420
+ size: 50,
421
+ minSize: 50,
422
+ meta: { onUpdate, cell: { classNames: [textPadding, classNames] } },
423
+ header: (column) => label ?? column.header.id,
424
+ cell: SwitchBuilderCell,
449
425
  });
450
426
  }
451
427
 
@@ -461,17 +437,9 @@ export class ColumnBuilder<TData extends RowData> {
461
437
  cell: (cell) => {
462
438
  const value = cell.getValue();
463
439
  if (value) {
464
- return (
465
- <div className='flex grow justify-center'>
466
- <IconOn className={mx(getSize(6), on?.className ?? 'text-green-700')} />
467
- </div>
468
- );
440
+ return <IconOn className={mx('block mli-auto', getSize(6), on?.className ?? 'text-green-700')} />;
469
441
  } else if (value === false) {
470
- return (
471
- <div className='flex grow justify-center'>
472
- <IconOff className={mx(getSize(6), off?.className ?? 'text-red-700')} />
473
- </div>
474
- );
442
+ return <IconOff className={mx('block mli-auto', getSize(6), off?.className ?? 'text-red-700')} />;
475
443
  } else {
476
444
  return null;
477
445
  }