@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.
- package/LICENSE +8 -0
- package/README.md +3 -0
- package/dist/lib/browser/index.mjs +844 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/types/src/components/ColumnMenu.d.ts +13 -0
- package/dist/types/src/components/ColumnMenu.d.ts.map +1 -0
- package/dist/types/src/components/ColumnMenu.stories.d.ts +16 -0
- package/dist/types/src/components/ColumnMenu.stories.d.ts.map +1 -0
- package/dist/types/src/components/Table.d.ts +29 -0
- package/dist/types/src/components/Table.d.ts.map +1 -0
- package/dist/types/src/components/Table.stories.d.ts +97 -0
- package/dist/types/src/components/Table.stories.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +3 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/helpers.d.ts +81 -0
- package/dist/types/src/helpers.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +6 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/schema.d.ts +21 -0
- package/dist/types/src/schema.d.ts.map +1 -0
- package/dist/types/src/theme.d.ts +34 -0
- package/dist/types/src/theme.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +23 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/package.json +56 -0
- package/src/components/ColumnMenu.stories.tsx +35 -0
- package/src/components/ColumnMenu.tsx +239 -0
- package/src/components/Table.stories.tsx +328 -0
- package/src/components/Table.tsx +547 -0
- package/src/components/index.ts +6 -0
- package/src/helpers.tsx +443 -0
- package/src/index.ts +9 -0
- package/src/schema.ts +39 -0
- package/src/theme.ts +70 -0
- package/src/types.ts +33 -0
package/src/helpers.tsx
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useFocusFinders } from '@fluentui/react-tabster';
|
|
6
|
+
import { Check, ClipboardText, type Icon, X } from '@phosphor-icons/react';
|
|
7
|
+
import { createColumnHelper, type ColumnDef, type ColumnMeta, type RowData } from '@tanstack/react-table';
|
|
8
|
+
import format from 'date-fns/format';
|
|
9
|
+
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
|
10
|
+
import defaultsDeep from 'lodash.defaultsdeep';
|
|
11
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
12
|
+
|
|
13
|
+
import { type PublicKey } from '@dxos/keys';
|
|
14
|
+
import { ComboBox, type ComboBoxItem, Input, Tooltip } from '@dxos/react-ui';
|
|
15
|
+
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
16
|
+
import { stripUndefinedValues } from '@dxos/util';
|
|
17
|
+
|
|
18
|
+
// TODO(burdon): Factor out hack to find next focusable element (extend useFocusFinders)?
|
|
19
|
+
const findNextFocusable = (
|
|
20
|
+
findFirstFocusable: (container: HTMLElement) => HTMLElement | null | undefined,
|
|
21
|
+
container: HTMLElement,
|
|
22
|
+
): HTMLElement | undefined => {
|
|
23
|
+
const next = findFirstFocusable(container as any);
|
|
24
|
+
if (next) {
|
|
25
|
+
if (next?.tagName === 'INPUT') {
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
if (container.nextElementSibling) {
|
|
29
|
+
return findNextFocusable(findFirstFocusable, container.nextSibling as any);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ValueUpdater<TData extends RowData, TValue> = (row: TData, id: string, value: TValue) => void;
|
|
35
|
+
|
|
36
|
+
export const createColumnBuilder = <TData extends RowData>() => ({
|
|
37
|
+
helper: createColumnHelper<TData>(),
|
|
38
|
+
builder: new ColumnBuilder<TData>(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* NOTE: Can use `meta` for custom properties.
|
|
43
|
+
*/
|
|
44
|
+
export type BaseColumnOptions<TData, TValue> = Partial<ColumnDef<TData, TValue>> & {
|
|
45
|
+
meta?: ColumnMeta<TData, TValue>;
|
|
46
|
+
label?: string;
|
|
47
|
+
className?: string;
|
|
48
|
+
onUpdate?: ValueUpdater<TData, TValue | undefined>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// TODO(burdon): Better abstraction?
|
|
52
|
+
export type SelectQueryModel<TData extends RowData> = {
|
|
53
|
+
getId(object: TData): string;
|
|
54
|
+
getText(object: TData): string;
|
|
55
|
+
query(text?: string): Promise<TData[]>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type SelectColumnOptions<TData extends RowData> = BaseColumnOptions<TData, any> & {
|
|
59
|
+
model: SelectQueryModel<TData>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type StringColumnOptions<TData extends RowData> = BaseColumnOptions<TData, string> & {};
|
|
63
|
+
|
|
64
|
+
export type NumberColumnOptions<TData extends RowData> = BaseColumnOptions<TData, number> & {
|
|
65
|
+
digits?: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type KeyColumnOptions<TData extends RowData> = BaseColumnOptions<TData, PublicKey> & {
|
|
69
|
+
tooltip?: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type DateColumnOptions<TData extends RowData> = BaseColumnOptions<TData, Date> & {
|
|
73
|
+
format?: string;
|
|
74
|
+
relative?: boolean;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type BooleanColumnOptions<TData extends RowData> = BaseColumnOptions<TData, boolean> & {};
|
|
78
|
+
|
|
79
|
+
export type IconColumnOptions<TData extends RowData> = BaseColumnOptions<TData, boolean> & {
|
|
80
|
+
on?: {
|
|
81
|
+
Icon?: Icon;
|
|
82
|
+
className?: string;
|
|
83
|
+
};
|
|
84
|
+
off?: {
|
|
85
|
+
Icon?: Icon;
|
|
86
|
+
className?: string;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const defaults = <TData extends RowData, TValue>(
|
|
91
|
+
options: Partial<ColumnDef<TData, TValue>>,
|
|
92
|
+
...sources: Partial<ColumnDef<TData, TValue>>[]
|
|
93
|
+
): Partial<ColumnDef<TData, TValue>> => {
|
|
94
|
+
return stripUndefinedValues(defaultsDeep({}, options, ...sources));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Lazy cell selector (click to activate).
|
|
99
|
+
*/
|
|
100
|
+
const CellSelector = <TData extends RowData>({
|
|
101
|
+
model,
|
|
102
|
+
value: _value,
|
|
103
|
+
onUpdate,
|
|
104
|
+
}: {
|
|
105
|
+
model: SelectQueryModel<TData>;
|
|
106
|
+
value: TData;
|
|
107
|
+
onUpdate: (value: TData) => void;
|
|
108
|
+
}) => {
|
|
109
|
+
const [edit, setEdit] = useState(false);
|
|
110
|
+
const [value, setValue] = useState<ComboBoxItem>();
|
|
111
|
+
useEffect(() => setValue(_value ? { id: model.getId(_value), label: model.getText(_value) } : undefined), [_value]);
|
|
112
|
+
const [items, setItems] = useState<ComboBoxItem[]>([]);
|
|
113
|
+
const handleUpdate = async (text?: string) => {
|
|
114
|
+
const items = await model.query(text);
|
|
115
|
+
setItems(items.map((item) => ({ id: model.getId(item), label: model.getText(item), data: item })));
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (!edit) {
|
|
119
|
+
const text = _value ? model.getText(_value) : undefined;
|
|
120
|
+
return (
|
|
121
|
+
// TODO(burdon): Hack to prevent div from collapsing.
|
|
122
|
+
<div className={mx('w-full px-2', !text && 'opacity-0')} onClick={() => setEdit(true)}>
|
|
123
|
+
{text ?? '-'}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<ComboBox.Root items={items} value={value} onChange={(value) => onUpdate(value?.data)} onInputChange={handleUpdate}>
|
|
130
|
+
<ComboBox.Input />
|
|
131
|
+
<ComboBox.Content>
|
|
132
|
+
{items.map((item) => (
|
|
133
|
+
<ComboBox.Item key={item.id} item={item}>
|
|
134
|
+
{item.label}
|
|
135
|
+
</ComboBox.Item>
|
|
136
|
+
))}
|
|
137
|
+
</ComboBox.Content>
|
|
138
|
+
</ComboBox.Root>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Util to create column definitions.
|
|
144
|
+
*/
|
|
145
|
+
export class ColumnBuilder<TData extends RowData> {
|
|
146
|
+
/**
|
|
147
|
+
* Select value
|
|
148
|
+
*/
|
|
149
|
+
select({ label, className, model, onUpdate, ...props }: SelectColumnOptions<TData>): Partial<ColumnDef<TData, any>> {
|
|
150
|
+
return defaults(props, {
|
|
151
|
+
minSize: 100,
|
|
152
|
+
header: (column) => {
|
|
153
|
+
return <div className={'truncate'}>{label ?? column.header.id}</div>;
|
|
154
|
+
},
|
|
155
|
+
cell:
|
|
156
|
+
model && onUpdate
|
|
157
|
+
? (cell) => (
|
|
158
|
+
<CellSelector<any>
|
|
159
|
+
model={model}
|
|
160
|
+
value={cell.getValue()}
|
|
161
|
+
onUpdate={(value) => onUpdate?.(cell.row.original, cell.column.id, value)}
|
|
162
|
+
/>
|
|
163
|
+
)
|
|
164
|
+
: (cell) => {
|
|
165
|
+
const value = cell.getValue();
|
|
166
|
+
return <div className={mx('truncate', className)}>{value ? model.getText(value) : ''}</div>;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* String formats.
|
|
173
|
+
*/
|
|
174
|
+
string({ label, className, onUpdate, ...props }: StringColumnOptions<TData> = {}): Partial<ColumnDef<TData, string>> {
|
|
175
|
+
return defaults(props, {
|
|
176
|
+
minSize: 100,
|
|
177
|
+
// TODO(burdon): Default.
|
|
178
|
+
header: (column) => {
|
|
179
|
+
return <div className={'truncate'}>{label ?? column.header.id}</div>;
|
|
180
|
+
},
|
|
181
|
+
cell: onUpdate
|
|
182
|
+
? (cell) => {
|
|
183
|
+
// https://tanstack.com/table/v8/docs/examples/react/editable-data
|
|
184
|
+
const initialValue = cell.getValue();
|
|
185
|
+
const [value, setValue] = useState(initialValue);
|
|
186
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
187
|
+
const { findFirstFocusable } = useFocusFinders();
|
|
188
|
+
|
|
189
|
+
const handleSave = (focusNext = false) => {
|
|
190
|
+
if (value === initialValue) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
onUpdate?.(cell.row.original, cell.column.id, value);
|
|
195
|
+
|
|
196
|
+
// TODO(burdon): More generally support keyboard navigation.
|
|
197
|
+
if (focusNext) {
|
|
198
|
+
const rowElement = inputRef.current?.parentElement?.parentElement;
|
|
199
|
+
// TODO(burdon): Hack to wait for next row to render.
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
const next = findNextFocusable(findFirstFocusable, rowElement as HTMLElement);
|
|
202
|
+
next?.focus();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const handleCancel = () => {
|
|
208
|
+
setValue(initialValue);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Check if first input column of last row.
|
|
212
|
+
const rows = cell.table.getRowModel().flatRows;
|
|
213
|
+
const columns = cell.table.getVisibleFlatColumns();
|
|
214
|
+
const placeholder = cell.row.index === rows.length - 1 && columns[0].id === cell.column.id;
|
|
215
|
+
|
|
216
|
+
// TODO(burdon): Don't render inputs unless mouse over (Show ellipsis when div)?
|
|
217
|
+
return (
|
|
218
|
+
<Input.Root>
|
|
219
|
+
<Input.TextInput
|
|
220
|
+
ref={inputRef}
|
|
221
|
+
variant='subdued'
|
|
222
|
+
placeholder={placeholder ? 'Add row...' : undefined}
|
|
223
|
+
classNames={'px-2'}
|
|
224
|
+
value={(value as string) ?? ''}
|
|
225
|
+
onBlur={() => handleSave(false)}
|
|
226
|
+
onChange={(event) => setValue(event.target.value)}
|
|
227
|
+
onKeyDown={(event) =>
|
|
228
|
+
(event.key === 'Enter' && handleSave(true)) || (event.key === 'Escape' && handleCancel())
|
|
229
|
+
}
|
|
230
|
+
/>
|
|
231
|
+
</Input.Root>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
: (cell) => {
|
|
235
|
+
const value = cell.getValue();
|
|
236
|
+
return <div className={mx('truncate', className)}>{value}</div>;
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Number formats.
|
|
243
|
+
*/
|
|
244
|
+
number({ label, minSize, className, digits, onUpdate, ...props }: NumberColumnOptions<TData> = {}): Partial<
|
|
245
|
+
ColumnDef<TData, number>
|
|
246
|
+
> {
|
|
247
|
+
return defaults(props, {
|
|
248
|
+
size: 100,
|
|
249
|
+
minSize: 100,
|
|
250
|
+
header: (cell) => <div className='w-full truncate text-right'>{label ?? cell.header.id}</div>,
|
|
251
|
+
cell: (cell) => {
|
|
252
|
+
const value = cell.getValue();
|
|
253
|
+
const [text, setText] = useState<string>();
|
|
254
|
+
|
|
255
|
+
const handleEdit = () => {
|
|
256
|
+
if (onUpdate) {
|
|
257
|
+
setText(value !== undefined ? String(value) : '');
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// TODO(burdon): Property is encoded as float (e.g., 6.1 => 6.099)
|
|
262
|
+
const handleSave = () => {
|
|
263
|
+
const value = text?.trim().length ? Number(text) : NaN;
|
|
264
|
+
onUpdate?.(cell.row.original, cell.column.id, isNaN(value) ? undefined : value);
|
|
265
|
+
setText(undefined);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const handleCancel = () => {
|
|
269
|
+
setText(undefined);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (text !== undefined) {
|
|
273
|
+
return (
|
|
274
|
+
<div className={mx('grow text-right font-mono', className)}>
|
|
275
|
+
<Input.Root>
|
|
276
|
+
<Input.TextInput
|
|
277
|
+
autoFocus
|
|
278
|
+
value={text}
|
|
279
|
+
classNames={'px-2'}
|
|
280
|
+
onBlur={handleSave}
|
|
281
|
+
onChange={(event) => setText(event.target.value)}
|
|
282
|
+
onKeyDown={(event) =>
|
|
283
|
+
(event.key === 'Escape' && handleCancel()) || (event.key === 'Enter' && handleSave())
|
|
284
|
+
}
|
|
285
|
+
/>
|
|
286
|
+
</Input.Root>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div
|
|
293
|
+
className={mx('grow w-full text-right font-mono empty:after:content-[" "]', className)}
|
|
294
|
+
onClick={handleEdit}
|
|
295
|
+
>
|
|
296
|
+
{value?.toLocaleString(undefined, {
|
|
297
|
+
minimumFractionDigits: digits ?? 0,
|
|
298
|
+
maximumFractionDigits: digits ?? 2,
|
|
299
|
+
})}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Date formats.
|
|
308
|
+
*/
|
|
309
|
+
// TODO(burdon): Date picker (pluggable renderers?)
|
|
310
|
+
date({ label, format: formatSpec, relative, className, ...props }: DateColumnOptions<TData> = {}): Partial<
|
|
311
|
+
ColumnDef<TData, Date>
|
|
312
|
+
> {
|
|
313
|
+
return defaults(props, {
|
|
314
|
+
size: 220, // TODO(burdon): Depends on format.
|
|
315
|
+
minSize: 100,
|
|
316
|
+
header: (cell) => <div>{label ?? cell.header.id}</div>,
|
|
317
|
+
cell: (cell) => {
|
|
318
|
+
const value = cell.getValue();
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const str = value
|
|
322
|
+
? formatSpec
|
|
323
|
+
? format(value, formatSpec)
|
|
324
|
+
: relative
|
|
325
|
+
? formatDistanceToNow(value, { addSuffix: true })
|
|
326
|
+
: value.toISOString()
|
|
327
|
+
: undefined;
|
|
328
|
+
|
|
329
|
+
return <div className={mx('font-mono', className)}>{str}</div>;
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.log(value);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* PublicKey with tooltip.
|
|
340
|
+
*/
|
|
341
|
+
key({ label, tooltip, ...props }: KeyColumnOptions<TData> = {}): Partial<ColumnDef<TData, PublicKey>> {
|
|
342
|
+
return defaults(props, {
|
|
343
|
+
size: 86,
|
|
344
|
+
minSize: 86,
|
|
345
|
+
header: (cell) => <div>{label ?? cell.header.id}</div>,
|
|
346
|
+
cell: (cell) => {
|
|
347
|
+
const value = cell.getValue();
|
|
348
|
+
if (!value) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// TODO(burdon): Factor out styles.
|
|
353
|
+
const element = (
|
|
354
|
+
<div className='font-mono font-thin text-green-500 dark:text-green-300'>{value.truncate()}</div>
|
|
355
|
+
);
|
|
356
|
+
if (!tooltip) {
|
|
357
|
+
return element;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<div className='group inline-flex gap-2 items-center'>
|
|
362
|
+
<Tooltip.Provider>
|
|
363
|
+
<Tooltip.Root>
|
|
364
|
+
<Tooltip.Trigger asChild>{element}</Tooltip.Trigger>
|
|
365
|
+
<Tooltip.Content side='right'>
|
|
366
|
+
<Tooltip.Arrow />
|
|
367
|
+
<ClipboardText
|
|
368
|
+
onClick={(ev) => {
|
|
369
|
+
ev.stopPropagation(); // Prevent select row.
|
|
370
|
+
void navigator.clipboard.writeText(value.toHex());
|
|
371
|
+
}}
|
|
372
|
+
/>
|
|
373
|
+
</Tooltip.Content>
|
|
374
|
+
</Tooltip.Root>
|
|
375
|
+
</Tooltip.Provider>
|
|
376
|
+
</div>
|
|
377
|
+
);
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Checkbox.
|
|
384
|
+
*/
|
|
385
|
+
checkbox({ label, className, onUpdate, ...props }: BooleanColumnOptions<TData> = {}): Partial<
|
|
386
|
+
ColumnDef<TData, boolean>
|
|
387
|
+
> {
|
|
388
|
+
return defaults(props, {
|
|
389
|
+
size: 40,
|
|
390
|
+
minSize: 40,
|
|
391
|
+
header: (column) => <div className={'flex grow justify-center'}>{label ?? column.header.id}</div>,
|
|
392
|
+
cell: (cell) => {
|
|
393
|
+
const value = cell.getValue();
|
|
394
|
+
// TODO(burdon): Center.
|
|
395
|
+
return (
|
|
396
|
+
<div className='flex grow justify-center'>
|
|
397
|
+
<Input.Root>
|
|
398
|
+
<Input.Checkbox
|
|
399
|
+
onClick={(event) => event.stopPropagation()}
|
|
400
|
+
classNames={className}
|
|
401
|
+
disabled={!onUpdate}
|
|
402
|
+
checked={!!value}
|
|
403
|
+
onCheckedChange={(value) => {
|
|
404
|
+
onUpdate?.(cell.row.original, cell.column.id, !!value);
|
|
405
|
+
}}
|
|
406
|
+
/>
|
|
407
|
+
</Input.Root>
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Icon based on boolean value.
|
|
416
|
+
*/
|
|
417
|
+
icon({ label, size, on, off, ...props }: IconColumnOptions<TData> = {}): Partial<ColumnDef<TData, boolean>> {
|
|
418
|
+
const IconOn = on?.Icon ?? Check;
|
|
419
|
+
const IconOff = off?.Icon ?? X;
|
|
420
|
+
return defaults(props, {
|
|
421
|
+
size: size ?? 32,
|
|
422
|
+
header: (column) => <div className={'justify-center'}>{label ?? column.header.id}</div>,
|
|
423
|
+
cell: (cell) => {
|
|
424
|
+
const value = cell.getValue();
|
|
425
|
+
if (value) {
|
|
426
|
+
return (
|
|
427
|
+
<div className='flex grow justify-center'>
|
|
428
|
+
<IconOn className={mx(getSize(6), on?.className ?? 'text-green-700')} />
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
} else if (value === false) {
|
|
432
|
+
return (
|
|
433
|
+
<div className='flex grow justify-center'>
|
|
434
|
+
<IconOff className={mx(getSize(6), off?.className ?? 'text-red-700')} />
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
} else {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
package/src/index.ts
ADDED
package/src/schema.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export type TableDef = {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string; // TODO(burdon): Required?
|
|
8
|
+
columns: ColumnProps[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ColumnType = 'number' | 'boolean' | 'date' | 'string' | 'ref';
|
|
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
|
+
export type ColumnProps = {
|
|
22
|
+
id: string;
|
|
23
|
+
prop: string;
|
|
24
|
+
type: ColumnType;
|
|
25
|
+
size?: number;
|
|
26
|
+
label?: string;
|
|
27
|
+
|
|
28
|
+
// type = number
|
|
29
|
+
digits?: number;
|
|
30
|
+
|
|
31
|
+
// type = ref
|
|
32
|
+
refTable?: string;
|
|
33
|
+
refProp?: string;
|
|
34
|
+
|
|
35
|
+
// TODO(burdon): Move to meta.
|
|
36
|
+
fixed?: boolean;
|
|
37
|
+
editable?: boolean;
|
|
38
|
+
resizable?: boolean;
|
|
39
|
+
};
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { chromeSurface } from '@dxos/react-ui-theme';
|
|
6
|
+
|
|
7
|
+
// TODO(burdon): Remove nested classNames? Add to theme?
|
|
8
|
+
export type TableSlots = {
|
|
9
|
+
root?: {
|
|
10
|
+
className?: string | string[];
|
|
11
|
+
};
|
|
12
|
+
table?: {
|
|
13
|
+
className?: string | string[];
|
|
14
|
+
};
|
|
15
|
+
header?: {
|
|
16
|
+
className?: string | string[];
|
|
17
|
+
};
|
|
18
|
+
footer?: {
|
|
19
|
+
className?: string | string[];
|
|
20
|
+
};
|
|
21
|
+
group?: {
|
|
22
|
+
className?: string | string[];
|
|
23
|
+
};
|
|
24
|
+
row?: {
|
|
25
|
+
className?: string | string[];
|
|
26
|
+
};
|
|
27
|
+
cell?: {
|
|
28
|
+
className?: string | string[];
|
|
29
|
+
};
|
|
30
|
+
focus?: {
|
|
31
|
+
className?: string | string[];
|
|
32
|
+
};
|
|
33
|
+
selected?: {
|
|
34
|
+
className?: string | string[];
|
|
35
|
+
};
|
|
36
|
+
margin?: {
|
|
37
|
+
className?: string | string[];
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// TODO(burdon): Change to groups.
|
|
42
|
+
/*
|
|
43
|
+
head: {
|
|
44
|
+
default:
|
|
45
|
+
cell:
|
|
46
|
+
}
|
|
47
|
+
row: {
|
|
48
|
+
default: '',
|
|
49
|
+
selected: ''
|
|
50
|
+
}
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
// TODO(burdon): Scrollbar area.
|
|
54
|
+
// TODO(burdon): Overscroll horizontal (full width).
|
|
55
|
+
|
|
56
|
+
// TODO(burdon): Integrate with DXOS UI theme (direct dependency -- see react-ui-composer, tailwind.ts).
|
|
57
|
+
// See Link.tsx const { tx } = useThemeContext();
|
|
58
|
+
// Reuse button fragments for hoverColors, selected, primary, etc.
|
|
59
|
+
export const defaultTableSlots: TableSlots = {
|
|
60
|
+
// TODO(burdon): head/body/table rows.
|
|
61
|
+
header: { className: [chromeSurface, 'px-2 font-light select-none'] },
|
|
62
|
+
footer: { className: [chromeSurface, 'px-2 font-light'] },
|
|
63
|
+
// cell: { className: 'px-2' },
|
|
64
|
+
// TODO(burdon): Compact mode.
|
|
65
|
+
// TODO(burdon): No hover if editing.
|
|
66
|
+
// row: { className: 'cursor-pointer hover:bg-neutral-50 dark:hover:bg-neutral-800' },
|
|
67
|
+
group: { className: 'px-2 font-light text-xs text-left' },
|
|
68
|
+
focus: { className: 'ring ring-primary-600 ring-inset' },
|
|
69
|
+
selected: { className: '!bg-teal-100 dark:!bg-teal-700' },
|
|
70
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type ColumnDef, type RowData } from '@tanstack/react-table';
|
|
6
|
+
|
|
7
|
+
// Define custom meta definitions.
|
|
8
|
+
declare module '@tanstack/react-table' {
|
|
9
|
+
// Access via table.options.meta.
|
|
10
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
11
|
+
interface TableMeta<TData extends RowData> {}
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
14
|
+
interface ColumnMeta<TData extends RowData, TValue> {
|
|
15
|
+
expand?: boolean;
|
|
16
|
+
resizable?: boolean;
|
|
17
|
+
slots?: {
|
|
18
|
+
header?: {
|
|
19
|
+
className?: string;
|
|
20
|
+
};
|
|
21
|
+
footer?: {
|
|
22
|
+
className?: string;
|
|
23
|
+
};
|
|
24
|
+
cell?: {
|
|
25
|
+
className?: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type KeyValue<TData extends RowData> = (row: TData) => string;
|
|
32
|
+
|
|
33
|
+
export type TableColumnDef<TData extends RowData, TValue = unknown> = ColumnDef<TData, TValue>;
|