@dxos/react-ui-table 0.3.8-next.a5a9bfc → 0.3.8

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 (54) hide show
  1. package/dist/lib/browser/index.mjs +511 -462
  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/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 -1
  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/types.d.ts +13 -10
  33. package/dist/types/src/types.d.ts.map +1 -1
  34. package/package.json +12 -10
  35. package/src/components/Cell/Cell.tsx +43 -0
  36. package/src/components/{ComboboxCell.tsx → Cell/CellCombobox.tsx} +6 -6
  37. package/src/components/Cell/index.ts +5 -0
  38. package/src/components/{Table.stories.tsx → Table/Table.stories.tsx} +51 -51
  39. package/src/components/Table/Table.tsx +179 -0
  40. package/src/components/Table/TableBody.tsx +69 -0
  41. package/src/components/Table/TableContext.tsx +28 -0
  42. package/src/components/Table/TableFooter.tsx +42 -0
  43. package/src/components/Table/TableHead.tsx +79 -0
  44. package/src/components/Table/index.ts +6 -0
  45. package/src/components/Table/props.ts +53 -0
  46. package/src/components/index.ts +1 -1
  47. package/src/helpers.tsx +255 -206
  48. package/src/theme.ts +95 -63
  49. package/src/types.ts +15 -10
  50. package/dist/types/src/components/ComboboxCell.d.ts.map +0 -1
  51. package/dist/types/src/components/Table.d.ts +0 -29
  52. package/dist/types/src/components/Table.d.ts.map +0 -1
  53. package/dist/types/src/components/Table.stories.d.ts.map +0 -1
  54. package/src/components/Table.tsx +0 -551
package/src/helpers.tsx CHANGED
@@ -2,36 +2,28 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { useFocusFinders } from '@fluentui/react-tabster';
6
5
  import { Check, ClipboardText, type Icon, X } from '@phosphor-icons/react';
7
- import { createColumnHelper, type ColumnDef, type ColumnMeta, type RowData } from '@tanstack/react-table';
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
17
  import React, { 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
22
  import { stripUndefinedValues } from '@dxos/util';
17
23
 
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
- };
24
+ import { CellCombobox } from './components';
25
+ import { useTableContext } from './components/Table/TableContext';
26
+ import { textPadding } from './theme';
35
27
 
36
28
  export type ValueUpdater<TData extends RowData, TValue> = (row: TData, id: string, value: TValue) => void;
37
29
 
@@ -46,22 +38,23 @@ export const createColumnBuilder = <TData extends RowData>() => ({
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?: {
@@ -96,31 +89,178 @@ const defaults = <TData extends RowData, TValue>(
96
89
  return stripUndefinedValues(defaultsDeep({}, options, ...sources));
97
90
  };
98
91
 
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);
119
+ };
120
+
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
+ );
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
+ }
178
+
179
+ return (
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>
231
+ );
232
+ };
233
+
99
234
  /**
100
235
  * Util to create column definitions.
101
236
  */
102
237
  export class ColumnBuilder<TData extends RowData> {
103
238
  /**
104
- * Select value
239
+ * Combobox value
105
240
  */
106
- 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>> {
107
248
  return defaults(props, {
108
249
  minSize: 100,
250
+ meta: {
251
+ model,
252
+ onUpdate,
253
+ cell: { classNames },
254
+ },
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
  });
126
266
  }
@@ -128,69 +268,21 @@ export class ColumnBuilder<TData extends RowData> {
128
268
  /**
129
269
  * String formats.
130
270
  */
131
- 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
+ > {
132
274
  return defaults(props, {
133
275
  minSize: 100,
276
+ meta: { onUpdate, cell: { classNames } },
134
277
  // TODO(burdon): Default.
135
278
  header: (column) => {
136
- return <div className={'truncate'}>{label ?? column.header.id}</div>;
279
+ return label ?? column.header.id;
137
280
  },
138
281
  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
- }
282
+ ? StringBuilderCell
191
283
  : (cell) => {
192
284
  const value = cell.getValue();
193
- return <div className={mx('truncate', className)}>{value}</div>;
285
+ return <div className={mx('truncate', textPadding, classNames)}>{value}</div>;
194
286
  },
195
287
  });
196
288
  }
@@ -198,65 +290,15 @@ export class ColumnBuilder<TData extends RowData> {
198
290
  /**
199
291
  * Number formats.
200
292
  */
201
- number({ label, minSize, className, digits, onUpdate, ...props }: NumberColumnOptions<TData> = {}): Partial<
293
+ number({ label, minSize, classNames, digits, onUpdate, ...props }: NumberColumnOptions<TData> = {}): Partial<
202
294
  ColumnDef<TData, number>
203
295
  > {
204
296
  return defaults(props, {
205
297
  size: 100,
206
298
  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
- );
259
- },
299
+ meta: { onUpdate, digits, header: { classNames: 'text-end' }, cell: { classNames } },
300
+ header: (cell) => label ?? cell.header.id,
301
+ cell: NumberBuilderCell,
260
302
  });
261
303
  }
262
304
 
@@ -264,13 +306,14 @@ export class ColumnBuilder<TData extends RowData> {
264
306
  * Date formats.
265
307
  */
266
308
  // TODO(burdon): Date picker (pluggable renderers?)
267
- date({ label, format: formatSpec, relative, className, ...props }: DateColumnOptions<TData> = {}): Partial<
309
+ date({ label, format: formatSpec, relative, classNames, ...props }: DateColumnOptions<TData> = {}): Partial<
268
310
  ColumnDef<TData, Date>
269
311
  > {
270
312
  return defaults(props, {
271
313
  size: 220, // TODO(burdon): Depends on format.
272
314
  minSize: 100,
273
- header: (cell) => <div>{label ?? cell.header.id}</div>,
315
+ meta: { cell: { classNames } },
316
+ header: (cell) => label ?? cell.header.id,
274
317
  cell: (cell) => {
275
318
  const value = cell.getValue();
276
319
 
@@ -283,7 +326,7 @@ export class ColumnBuilder<TData extends RowData> {
283
326
  : value.toISOString()
284
327
  : undefined;
285
328
 
286
- return <div className={mx('font-mono', className)}>{str}</div>;
329
+ return <div className={mx(textPadding, classNames)}>{str}</div>;
287
330
  } catch (err) {
288
331
  console.log(value);
289
332
  return null;
@@ -297,9 +340,10 @@ export class ColumnBuilder<TData extends RowData> {
297
340
  */
298
341
  key({ label, tooltip, ...props }: KeyColumnOptions<TData> = {}): Partial<ColumnDef<TData, PublicKey>> {
299
342
  return defaults(props, {
300
- size: 86,
301
- minSize: 86,
302
- 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,
303
347
  cell: (cell) => {
304
348
  const value = cell.getValue();
305
349
  if (!value) {
@@ -307,64 +351,77 @@ export class ColumnBuilder<TData extends RowData> {
307
351
  }
308
352
 
309
353
  // 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
- );
354
+ const element = value.truncate();
313
355
  if (!tooltip) {
314
356
  return element;
315
357
  }
316
358
 
317
359
  return (
318
- <div className='group inline-flex gap-2 items-center'>
319
- <Tooltip.Provider>
320
- <Tooltip.Root>
321
- <Tooltip.Trigger asChild>{element}</Tooltip.Trigger>
322
- <Tooltip.Content side='right'>
323
- <Tooltip.Arrow />
324
- <ClipboardText
325
- onClick={(ev) => {
326
- ev.stopPropagation(); // Prevent select row.
327
- void navigator.clipboard.writeText(value.toHex());
328
- }}
329
- />
330
- </Tooltip.Content>
331
- </Tooltip.Root>
332
- </Tooltip.Provider>
333
- </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>
334
374
  );
335
375
  },
336
376
  });
337
377
  }
338
378
 
339
379
  /**
340
- * Checkbox.
380
+ * Row selector
381
+ */
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
341
415
  */
342
- checkbox({ label, className, onUpdate, ...props }: BooleanColumnOptions<TData> = {}): Partial<
416
+ switch({ label, classNames, onUpdate, ...props }: SwitchColumnOptions<TData> = {}): Partial<
343
417
  ColumnDef<TData, boolean>
344
418
  > {
345
419
  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
- },
420
+ size: 50,
421
+ minSize: 50,
422
+ meta: { onUpdate, cell: { classNames: [textPadding, classNames] } },
423
+ header: (column) => label ?? column.header.id,
424
+ cell: SwitchBuilderCell,
368
425
  });
369
426
  }
370
427
 
@@ -380,17 +437,9 @@ export class ColumnBuilder<TData extends RowData> {
380
437
  cell: (cell) => {
381
438
  const value = cell.getValue();
382
439
  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
- );
440
+ return <IconOn className={mx('block mli-auto', getSize(6), on?.className ?? 'text-green-700')} />;
388
441
  } 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
- );
442
+ return <IconOff className={mx('block mli-auto', getSize(6), off?.className ?? 'text-red-700')} />;
394
443
  } else {
395
444
  return null;
396
445
  }