@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.
Files changed (62) hide show
  1. package/README.md +25 -2
  2. package/dist/index.d.ts +856 -14
  3. package/dist/index.js +3370 -1546
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/dist/styles.layer.css +1 -0
  7. package/package.json +12 -2
  8. package/skills/columns/SKILL.md +1 -1
  9. package/skills/features/SKILL.md +1 -1
  10. package/skills/getting-started/SKILL.md +1 -1
  11. package/skills/options/SKILL.md +1 -1
  12. package/skills/server-side/SKILL.md +1 -1
  13. package/skills/testing/SKILL.md +198 -0
  14. package/src/tmdatagrid/components/TMDataGrid.module.css +11 -0
  15. package/src/tmdatagrid/components/TMDataGrid.tsx +34 -0
  16. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +256 -0
  17. package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +35 -12
  18. package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +3 -0
  19. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +62 -0
  20. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +237 -0
  21. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +164 -0
  22. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +77 -62
  23. package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +6 -0
  24. package/src/tmdatagrid/components/TMDataGridFooter.tsx +10 -3
  25. package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +2 -0
  26. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +15 -2
  27. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +21 -4
  28. package/src/tmdatagrid/components/TMDataGridRowNumberColumn.tsx +44 -0
  29. package/src/tmdatagrid/components/TMDataGridSearch.tsx +2 -0
  30. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +3 -0
  31. package/src/tmdatagrid/components/TMDataGridTable.module.css +206 -0
  32. package/src/tmdatagrid/components/TMDataGridTable.tsx +841 -57
  33. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +12 -7
  34. package/src/tmdatagrid/components/editors/TMDataGridBooleanEditor.tsx +29 -0
  35. package/src/tmdatagrid/components/editors/TMDataGridDateEditor.tsx +57 -0
  36. package/src/tmdatagrid/components/editors/TMDataGridMultiSelectEditor.tsx +45 -0
  37. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +38 -0
  38. package/src/tmdatagrid/components/editors/TMDataGridSelectEditor.tsx +53 -0
  39. package/src/tmdatagrid/components/editors/TMDataGridStringEditor.tsx +31 -0
  40. package/src/tmdatagrid/components/editors/editorShared.ts +48 -0
  41. package/src/tmdatagrid/components/filters/DgAutocompleteFilter.tsx +46 -0
  42. package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +43 -0
  43. package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +52 -0
  44. package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +38 -0
  45. package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +122 -0
  46. package/src/tmdatagrid/components/icons.ts +4 -0
  47. package/src/tmdatagrid/core/autosize.ts +1 -1
  48. package/src/tmdatagrid/core/capabilities.ts +37 -1
  49. package/src/tmdatagrid/core/cellNavigation.ts +41 -0
  50. package/src/tmdatagrid/core/columnUtils.ts +31 -6
  51. package/src/tmdatagrid/core/editEngine.ts +857 -0
  52. package/src/tmdatagrid/core/filterControls.ts +45 -0
  53. package/src/tmdatagrid/core/filterOperators.ts +72 -4
  54. package/src/tmdatagrid/core/labels.ts +68 -1
  55. package/src/tmdatagrid/core/labelsSv.ts +24 -1
  56. package/src/tmdatagrid/core/matchHighlight.ts +96 -0
  57. package/src/tmdatagrid/core/persistence.ts +97 -3
  58. package/src/tmdatagrid/core/quickSearch.ts +144 -0
  59. package/src/tmdatagrid/core/rowPinning.ts +37 -0
  60. package/src/tmdatagrid/core/rowSelection.ts +10 -1
  61. package/src/tmdatagrid/index.ts +51 -0
  62. 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. */