@dxos/react-ui-table 0.3.8-next.f4e0086 → 0.3.9-main.03b62b6

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