@jielga/tmdatagrid 0.5.0 → 1.0.0
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/README.md +25 -2
- package/dist/index.d.ts +856 -14
- package/dist/index.js +3370 -1546
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.layer.css +1 -0
- package/package.json +12 -2
- package/skills/columns/SKILL.md +1 -1
- package/skills/features/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +1 -1
- package/skills/options/SKILL.md +1 -1
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +198 -0
- package/src/tmdatagrid/components/TMDataGrid.module.css +11 -0
- package/src/tmdatagrid/components/TMDataGrid.tsx +34 -0
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +256 -0
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +35 -12
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +3 -0
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +62 -0
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +237 -0
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +164 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +77 -62
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +6 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +10 -3
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +2 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +15 -2
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +21 -4
- package/src/tmdatagrid/components/TMDataGridRowNumberColumn.tsx +44 -0
- package/src/tmdatagrid/components/TMDataGridSearch.tsx +2 -0
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +3 -0
- package/src/tmdatagrid/components/TMDataGridTable.module.css +206 -0
- package/src/tmdatagrid/components/TMDataGridTable.tsx +841 -57
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +12 -7
- package/src/tmdatagrid/components/editors/TMDataGridBooleanEditor.tsx +29 -0
- package/src/tmdatagrid/components/editors/TMDataGridDateEditor.tsx +57 -0
- package/src/tmdatagrid/components/editors/TMDataGridMultiSelectEditor.tsx +45 -0
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +38 -0
- package/src/tmdatagrid/components/editors/TMDataGridSelectEditor.tsx +53 -0
- package/src/tmdatagrid/components/editors/TMDataGridStringEditor.tsx +31 -0
- package/src/tmdatagrid/components/editors/editorShared.ts +48 -0
- package/src/tmdatagrid/components/filters/DgAutocompleteFilter.tsx +46 -0
- package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +43 -0
- package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +52 -0
- package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +38 -0
- package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +122 -0
- package/src/tmdatagrid/components/icons.ts +4 -0
- package/src/tmdatagrid/core/autosize.ts +1 -1
- package/src/tmdatagrid/core/capabilities.ts +37 -1
- package/src/tmdatagrid/core/cellNavigation.ts +41 -0
- package/src/tmdatagrid/core/columnUtils.ts +31 -6
- package/src/tmdatagrid/core/editEngine.ts +857 -0
- package/src/tmdatagrid/core/filterControls.ts +45 -0
- package/src/tmdatagrid/core/filterOperators.ts +72 -4
- package/src/tmdatagrid/core/labels.ts +68 -1
- package/src/tmdatagrid/core/labelsSv.ts +24 -1
- package/src/tmdatagrid/core/matchHighlight.ts +96 -0
- package/src/tmdatagrid/core/persistence.ts +97 -3
- package/src/tmdatagrid/core/quickSearch.ts +144 -0
- package/src/tmdatagrid/core/rowPinning.ts +37 -0
- package/src/tmdatagrid/core/rowSelection.ts +10 -1
- package/src/tmdatagrid/index.ts +51 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +456 -15
|
@@ -99,6 +99,7 @@ export function TMDataGridSearch({
|
|
|
99
99
|
onChange={(event) => handleChange(event.currentTarget.value)}
|
|
100
100
|
placeholder={placeholder ?? labels.searchPlaceholder}
|
|
101
101
|
aria-label={labels.searchLabel}
|
|
102
|
+
data-dg-part="search"
|
|
102
103
|
leftSection={<SearchIcon size={16} stroke={1.6} />}
|
|
103
104
|
rightSection={
|
|
104
105
|
draft !== "" ? (
|
|
@@ -107,6 +108,7 @@ export function TMDataGridSearch({
|
|
|
107
108
|
color="gray"
|
|
108
109
|
size="sm"
|
|
109
110
|
aria-label={labels.clearSearch}
|
|
111
|
+
data-dg-part="search-clear"
|
|
110
112
|
onClick={() => {
|
|
111
113
|
clearTimeout(timeoutRef.current);
|
|
112
114
|
setDraft("");
|
|
@@ -46,6 +46,7 @@ function SelectAllCheckbox<TData extends RowData>({
|
|
|
46
46
|
<Checkbox
|
|
47
47
|
size="xs"
|
|
48
48
|
aria-label={labels.selectAllRows}
|
|
49
|
+
data-dg-part="select-all"
|
|
49
50
|
checked={allSelected}
|
|
50
51
|
indeterminate={someSelected && !allSelected}
|
|
51
52
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
@@ -111,6 +112,8 @@ function SelectRowCheckbox<TData extends RowData>({
|
|
|
111
112
|
size="xs"
|
|
112
113
|
tabIndex={tabIndex}
|
|
113
114
|
aria-label={isGroupRow ? labels.selectGroup : labels.selectRow}
|
|
115
|
+
data-dg-part="select-row"
|
|
116
|
+
data-row-id={row.id}
|
|
114
117
|
checked={selected}
|
|
115
118
|
disabled={selectableIds.length === 0}
|
|
116
119
|
indeterminate={someSelected && !selected}
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
flex: 1;
|
|
13
13
|
min-height: 0;
|
|
14
14
|
overflow: auto;
|
|
15
|
+
/* Lets .messageRow size itself to the scrollport (100cqi) instead of the
|
|
16
|
+
full column-track width, so its content centres in the visible window. */
|
|
17
|
+
container-type: inline-size;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
/* Column tracks are set inline via gridTemplateColumns. */
|
|
@@ -30,6 +33,46 @@
|
|
|
30
33
|
z-index: var(--dg-z-header, 6);
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
/* The scrolled-under shadow: a soft band under the header, only while body
|
|
37
|
+
rows are actually beneath it. On the last header row — the header/body
|
|
38
|
+
boundary — not on every stacked group row. A scroll-driven animation, like
|
|
39
|
+
the pinned-lane gradients: the shadow tracks the scroll on the compositor
|
|
40
|
+
with no listener and no render, and an inactive timeline (nothing to
|
|
41
|
+
scroll) leaves `opacity: 0` standing, so a grid that fits shows nothing.
|
|
42
|
+
Where unsupported there is simply no shadow — the header's border already
|
|
43
|
+
draws the boundary. */
|
|
44
|
+
.headerRow[data-dg-header-last]::after {
|
|
45
|
+
content: "";
|
|
46
|
+
position: absolute;
|
|
47
|
+
inset: 100% 0 auto 0;
|
|
48
|
+
height: 6px;
|
|
49
|
+
pointer-events: none;
|
|
50
|
+
background: linear-gradient(
|
|
51
|
+
to bottom,
|
|
52
|
+
var(--dg-header-shadow-color, rgba(0, 0, 0, 0.14)),
|
|
53
|
+
transparent
|
|
54
|
+
);
|
|
55
|
+
opacity: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@supports (animation-timeline: scroll()) {
|
|
59
|
+
.headerRow[data-dg-header-last]::after {
|
|
60
|
+
animation: dg-header-shadow linear both;
|
|
61
|
+
animation-timeline: scroll(nearest block);
|
|
62
|
+
/* Arrives over the first rows leaving, reads as depth not as a fade. */
|
|
63
|
+
animation-range: 0px 24px;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@keyframes dg-header-shadow {
|
|
68
|
+
from {
|
|
69
|
+
opacity: 0;
|
|
70
|
+
}
|
|
71
|
+
to {
|
|
72
|
+
opacity: 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
33
76
|
.bodyRow {
|
|
34
77
|
--row-bg: var(--mantine-color-body);
|
|
35
78
|
display: grid;
|
|
@@ -38,6 +81,15 @@
|
|
|
38
81
|
align-items: stretch;
|
|
39
82
|
}
|
|
40
83
|
|
|
84
|
+
/* Every second row, under the Table's `striped` prop. Painted from the row's
|
|
85
|
+
own position (`data-striped`), never `:nth-child`: virtualization mounts a
|
|
86
|
+
moving window of rows, so document position says nothing about data
|
|
87
|
+
position. Declared before the hover rule, which is the stronger statement
|
|
88
|
+
and wins at equal specificity. */
|
|
89
|
+
.bodyRow[data-striped="true"] {
|
|
90
|
+
--row-bg: var(--dg-row-striped-bg);
|
|
91
|
+
}
|
|
92
|
+
|
|
41
93
|
.bodyRow:hover {
|
|
42
94
|
--row-bg: var(--mantine-color-default-hover);
|
|
43
95
|
}
|
|
@@ -182,6 +234,53 @@
|
|
|
182
234
|
text-overflow: ellipsis;
|
|
183
235
|
}
|
|
184
236
|
|
|
237
|
+
/* The editing cell hands its box to the editor: the text padding comes off so
|
|
238
|
+
the input can fill the track, and the input's own chrome takes over. */
|
|
239
|
+
.bodyCell[data-editing] {
|
|
240
|
+
padding: 0 calc(var(--dg-padding) / 2);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.cellEditor {
|
|
244
|
+
display: flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
width: 100%;
|
|
247
|
+
min-width: 0;
|
|
248
|
+
gap: 2px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* cellConfirm's ✓ / ✕ pair, beside the input. */
|
|
252
|
+
.cellEditorActions {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
flex: 0 0 auto;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Corner triangles for a cell's edit state, drawn like a spreadsheet's: a
|
|
259
|
+
draft not yet saved is blue, a value failing validation is red. The cell
|
|
260
|
+
needs a positioning context; pinned and focused cells set their own inline
|
|
261
|
+
`position`, which wins over this. */
|
|
262
|
+
.bodyCell[data-dirty],
|
|
263
|
+
.bodyCell[data-invalid] {
|
|
264
|
+
position: relative;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.bodyCell[data-dirty]::after,
|
|
268
|
+
.bodyCell[data-invalid]::after {
|
|
269
|
+
content: "";
|
|
270
|
+
position: absolute;
|
|
271
|
+
top: 0;
|
|
272
|
+
right: 0;
|
|
273
|
+
border: 5px solid transparent;
|
|
274
|
+
border-top-color: var(--mantine-primary-color-filled);
|
|
275
|
+
border-right-color: var(--mantine-primary-color-filled);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Invalid wins over dirty — a cell is usually both. */
|
|
279
|
+
.bodyCell[data-invalid]::after {
|
|
280
|
+
border-top-color: var(--mantine-color-error);
|
|
281
|
+
border-right-color: var(--mantine-color-error);
|
|
282
|
+
}
|
|
283
|
+
|
|
185
284
|
.bodyCell[data-align="right"] {
|
|
186
285
|
justify-content: flex-end;
|
|
187
286
|
}
|
|
@@ -212,8 +311,16 @@
|
|
|
212
311
|
cursor: auto;
|
|
213
312
|
}
|
|
214
313
|
|
|
314
|
+
/* Spans every column track, but the rendered box is pinned to the scrollport:
|
|
315
|
+
sticky at the left edge and exactly one container-width wide, so the
|
|
316
|
+
centred content stays in the middle of the window however far the grid is
|
|
317
|
+
scrolled horizontally. */
|
|
215
318
|
.messageRow {
|
|
216
319
|
grid-column: 1 / -1;
|
|
320
|
+
position: sticky;
|
|
321
|
+
left: 0;
|
|
322
|
+
width: 100cqi;
|
|
323
|
+
justify-self: start;
|
|
217
324
|
display: flex;
|
|
218
325
|
flex-direction: column;
|
|
219
326
|
align-items: center;
|
|
@@ -222,6 +329,105 @@
|
|
|
222
329
|
padding: var(--mantine-spacing-xl);
|
|
223
330
|
}
|
|
224
331
|
|
|
332
|
+
/* Marked deleted under batch: struck through and inert until submitAll
|
|
333
|
+
reports it. Interaction off wholesale — a row about to go away must not
|
|
334
|
+
keep taking edits. */
|
|
335
|
+
.bodyRow[data-deleted="true"] {
|
|
336
|
+
text-decoration: line-through;
|
|
337
|
+
opacity: 0.55;
|
|
338
|
+
pointer-events: none;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* The entry block — one row per edit.addRow(), under the header on the same
|
|
342
|
+
subgrid tracks as everything else. The *block* is the sticky element, not
|
|
343
|
+
the rows: sticky siblings would all compete for the same `top` and stack
|
|
344
|
+
onto one spot, while a single sticky wrapper lets its rows flow normally
|
|
345
|
+
inside it. Subgrid twice over keeps the cells on the outer tracks.
|
|
346
|
+
`--dg-header-height` is measured only while the block exists. */
|
|
347
|
+
.entryBlock {
|
|
348
|
+
display: grid;
|
|
349
|
+
grid-column: 1 / -1;
|
|
350
|
+
grid-template-columns: subgrid;
|
|
351
|
+
position: sticky;
|
|
352
|
+
top: var(--dg-header-height, 0px);
|
|
353
|
+
z-index: var(--dg-z-pinned-row, 4);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.entryRow {
|
|
357
|
+
display: grid;
|
|
358
|
+
grid-column: 1 / -1;
|
|
359
|
+
grid-template-columns: subgrid;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* The pinned edge blocks — user-pinned rows held at the top or bottom of the
|
|
363
|
+
body, on the entry block's sticky mechanics: the block is the sticky
|
|
364
|
+
element, its rows flow normally inside it. The top block stacks under the
|
|
365
|
+
entry block when both exist; `--dg-entry-height` is measured only then, so
|
|
366
|
+
the offset is otherwise the header alone. The bottom block stacks above the
|
|
367
|
+
summary row the same way. Rows are opaque already (`--row-bg` paints the
|
|
368
|
+
body colour), so the blocks only add the edge borders. */
|
|
369
|
+
.pinnedTopBlock {
|
|
370
|
+
display: grid;
|
|
371
|
+
grid-column: 1 / -1;
|
|
372
|
+
grid-template-columns: subgrid;
|
|
373
|
+
position: sticky;
|
|
374
|
+
top: calc(var(--dg-header-height, 0px) + var(--dg-entry-height, 0px));
|
|
375
|
+
z-index: var(--dg-z-pinned-row, 4);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.pinnedTopBlock > :last-child > [role="cell"] {
|
|
379
|
+
border-bottom: 2px solid var(--mantine-color-default-border);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.pinnedBottomBlock {
|
|
383
|
+
display: grid;
|
|
384
|
+
grid-column: 1 / -1;
|
|
385
|
+
grid-template-columns: subgrid;
|
|
386
|
+
position: sticky;
|
|
387
|
+
bottom: var(--dg-summary-height, 0px);
|
|
388
|
+
z-index: var(--dg-z-pinned-row, 4);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.pinnedBottomBlock > :first-child > [role="cell"] {
|
|
392
|
+
border-top: 2px solid var(--mantine-color-default-border);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.entryCell {
|
|
396
|
+
display: flex;
|
|
397
|
+
align-items: center;
|
|
398
|
+
padding: 0 calc(var(--dg-padding) / 2);
|
|
399
|
+
border-bottom: 2px solid var(--mantine-primary-color-filled);
|
|
400
|
+
/* Opaque: the body scrolls underneath it. */
|
|
401
|
+
background-color: var(--mantine-color-body);
|
|
402
|
+
font-size: var(--dg-font-size);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.entryCell[data-control-column="true"] {
|
|
406
|
+
padding: 0;
|
|
407
|
+
justify-content: center;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.entryCell[data-align="right"] {
|
|
411
|
+
justify-content: flex-end;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.entryCell[data-align="center"] {
|
|
415
|
+
justify-content: center;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/* The matched slice of a cell's text, while `enableMatchHighlighting` is on
|
|
419
|
+
and a contains-family filter or the quick search is active. `mark` resets
|
|
420
|
+
the browser's default so the colour is the grid's to state — override with
|
|
421
|
+
`--dg-match-highlight-bg` on the grid. */
|
|
422
|
+
.matchHighlight {
|
|
423
|
+
background-color: var(
|
|
424
|
+
--dg-match-highlight-bg,
|
|
425
|
+
light-dark(var(--mantine-color-yellow-2), rgba(250, 176, 5, 0.35))
|
|
426
|
+
);
|
|
427
|
+
color: inherit;
|
|
428
|
+
border-radius: 2px;
|
|
429
|
+
}
|
|
430
|
+
|
|
225
431
|
/* The summary row — one sticky row of column footers along the bottom edge,
|
|
226
432
|
rendered only while at least one column defines `footer`. Same subgrid
|
|
227
433
|
placement as the header, so its cells sit exactly on the body's tracks. */
|