@jielga/tmdatagrid 0.2.0 → 0.3.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 (35) hide show
  1. package/dist/index.d.ts +397 -37
  2. package/dist/index.js +1550 -978
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/package.json +1 -1
  6. package/skills/columns/SKILL.md +1 -1
  7. package/skills/features/SKILL.md +1 -1
  8. package/skills/getting-started/SKILL.md +41 -6
  9. package/skills/options/SKILL.md +1 -1
  10. package/skills/server-side/SKILL.md +1 -1
  11. package/src/tmdatagrid/components/TMDataGrid.module.css +11 -1
  12. package/src/tmdatagrid/components/TMDataGrid.tsx +6 -0
  13. package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +7 -1
  14. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +15 -0
  15. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +87 -10
  16. package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
  17. package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +135 -0
  18. package/src/tmdatagrid/components/TMDataGridFooter.module.css +8 -0
  19. package/src/tmdatagrid/components/TMDataGridFooter.tsx +70 -42
  20. package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
  21. package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +147 -0
  22. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +6 -0
  23. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +77 -1
  24. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +83 -6
  25. package/src/tmdatagrid/components/TMDataGridTable.module.css +38 -4
  26. package/src/tmdatagrid/components/TMDataGridTable.tsx +396 -32
  27. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +3 -0
  28. package/src/tmdatagrid/components/icons.ts +5 -0
  29. package/src/tmdatagrid/core/capabilities.ts +81 -11
  30. package/src/tmdatagrid/core/filterOperators.ts +25 -0
  31. package/src/tmdatagrid/core/grouping.ts +21 -0
  32. package/src/tmdatagrid/core/persistence.ts +12 -0
  33. package/src/tmdatagrid/core/rowSelection.ts +202 -0
  34. package/src/tmdatagrid/index.ts +26 -2
  35. package/src/tmdatagrid/useTMDataGrid.tsx +240 -35
package/dist/styles.css CHANGED
@@ -1,2 +1,2 @@
1
- ._root_1jc4n_8[data-size=xs]{--dg-row-height:34px;--dg-header-height:32px;--dg-font-size:var(--mantine-font-size-xs);--dg-padding:6px}._root_1jc4n_8[data-size=sm]{--dg-row-height:42px;--dg-header-height:38px;--dg-font-size:var(--mantine-font-size-sm);--dg-padding:8px}._root_1jc4n_8[data-size=md]{--dg-row-height:52px;--dg-header-height:44px;--dg-font-size:var(--mantine-font-size-sm);--dg-padding:10px}._root_1jc4n_8[data-size=lg]{--dg-row-height:62px;--dg-header-height:52px;--dg-font-size:var(--mantine-font-size-md);--dg-padding:14px}._root_1jc4n_8[data-size=xl]{--dg-row-height:72px;--dg-header-height:60px;--dg-font-size:var(--mantine-font-size-lg);--dg-padding:18px}._root_1jc4n_8{--dg-row-height:52px;--dg-header-height:44px;--dg-font-size:var(--mantine-font-size-sm);--dg-padding:10px;--dg-row-selected-bg:var(--mantine-primary-color-light);text-align:left;border:1px solid var(--mantine-color-default-border);border-radius:var(--mantine-radius-md);background-color:var(--mantine-color-body);flex-direction:column;min-height:0;display:flex;overflow:hidden}._columnsPanel_1492o_1{flex-direction:column;width:280px;display:flex}._columnsPanelSearch_1492o_7{padding:var(--mantine-spacing-sm)}._columnsPanelList_1492o_11{gap:var(--mantine-spacing-sm);padding:0 var(--mantine-spacing-sm) var(--mantine-spacing-sm);flex-direction:column;display:flex}._columnsPanelFooter_1492o_18{justify-content:space-between;align-items:center;gap:var(--mantine-spacing-sm);padding:var(--mantine-spacing-sm);border-top:1px solid var(--mantine-color-default-border);display:flex}._filterPanel_1jw69_4{top:calc(var(--dg-header-height) + 2px);left:var(--mantine-spacing-md);z-index:5;padding:var(--mantine-spacing-sm);border-radius:var(--mantine-radius-md);border:1px solid var(--mantine-color-default-border);background-color:var(--mantine-color-body);box-shadow:var(--mantine-shadow-md);position:absolute}._filterRow_1jw69_16{align-items:flex-end;gap:var(--mantine-spacing-xs);display:flex}._footer_6r35a_1{justify-content:flex-end;align-items:center;gap:var(--mantine-spacing-lg);padding:4px var(--mantine-spacing-sm);border-top:1px solid var(--mantine-color-default-border);flex-shrink:0;min-height:48px;display:flex}._tableWrapper_utldp_2{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}._scrollContainer_utldp_10{flex:1;min-height:0;position:relative;overflow:auto}._grid_utldp_18{display:grid}._headerRow_utldp_24{grid-column:1/-1;grid-template-columns:subgrid;z-index:3;display:grid;position:sticky;top:0}._bodyRow_utldp_33{--row-bg:var(--mantine-color-body);grid-column:1/-1;grid-template-columns:subgrid;align-items:stretch;display:grid}._bodyRow_utldp_33:hover{--row-bg:var(--mantine-color-default-hover)}._bodyRow_utldp_33[data-highlighted=true]{--row-bg:var(--dg-row-selected-bg)}._bodyRow_utldp_33[data-selects-on-click=true]{-webkit-user-select:none;user-select:none}._bodyRow_utldp_33:focus-visible{outline:2px solid var(--mantine-primary-color-filled);outline-offset:-2px}._bodyCell_utldp_65{padding:0 var(--dg-padding);border-bottom:1px solid var(--mantine-color-default-border);background-color:var(--row-bg);font-size:var(--dg-font-size);align-items:center;display:flex}._cellContent_utldp_63{white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}._bodyCell_utldp_65[data-align=right]{justify-content:flex-end}._bodyCell_utldp_65[data-align=center]{justify-content:center}._bodyCell_utldp_65[data-align=left] ._cellContent_utldp_63{flex:auto}._messageRow_utldp_95{justify-content:center;align-items:center;gap:var(--mantine-spacing-xs);padding:var(--mantine-spacing-xl);flex-direction:column;grid-column:1/-1;display:flex}._stickyLeft_1iqrf_11{border-right:1px solid var(--mantine-color-default-border)}._stickyRight_1iqrf_15{border-left:1px solid var(--mantine-color-default-border)}._stickyLeft_1iqrf_11:after,._stickyRight_1iqrf_15:after{content:"";pointer-events:none;width:4px;position:absolute;top:0;bottom:0}._stickyLeft_1iqrf_11:after{background:linear-gradient(90deg,var(--lightningcss-light,#00000014)var(--lightningcss-dark,#00000047),#0000);left:100%}._stickyRight_1iqrf_15:after{background:linear-gradient(270deg,var(--lightningcss-light,#00000014)var(--lightningcss-dark,#00000047),#0000);right:100%}._headerCell_1oqkm_1{padding:0 var(--dg-padding);min-width:0;min-height:var(--dg-header-height);font-weight:600;font-size:var(--dg-font-size);background-color:var(--mantine-color-body);border-bottom:1px solid var(--mantine-color-default-border);-webkit-user-select:none;user-select:none;align-items:center;gap:2px;display:flex;position:relative}._headerCell_1oqkm_1[data-align=right]{justify-content:flex-end}._headerCell_1oqkm_1[data-align=center]{justify-content:center}._headerCell_1oqkm_1[data-active=true] ._headerTitle_1oqkm_25{color:var(--mantine-primary-color-filled)}._headerTitle_1oqkm_25{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}._headerCell_1oqkm_1[data-align=right] ._headerTitle_1oqkm_25{text-align:right}._headerCell_1oqkm_1[data-align=center] ._headerTitle_1oqkm_25{justify-content:center;display:flex}._headerActions_1oqkm_48{flex-shrink:0;align-items:center;gap:0;margin-left:auto;display:flex}._headerCell_1oqkm_1[data-align=right] ._headerActions_1oqkm_48{order:-1;margin-left:0;margin-right:auto}._headerAction_1oqkm_48{display:none}._headerCell_1oqkm_1:hover ._headerAction_1oqkm_48,._headerCell_1oqkm_1:focus-within ._headerAction_1oqkm_48,._headerAction_1oqkm_48[data-pinned-visible=true]{display:flex}._headerCellSortable_1oqkm_77{cursor:pointer}._headerCellDragging_1oqkm_84{opacity:.4}._headerCellDropBefore_1oqkm_88:before,._headerCellDropAfter_1oqkm_89:before{content:"";background-color:var(--mantine-primary-color-filled);z-index:5;pointer-events:none;width:2px;position:absolute;top:0;bottom:0}._headerCellDropBefore_1oqkm_88:before{left:0}._headerCellDropAfter_1oqkm_89:before{right:0}._columnSeparator_1oqkm_109{z-index:1;justify-content:center;width:10px;height:50%;display:flex;position:absolute;top:25%;right:0;transform:translate(50%)}._columnSeparatorResizable_1oqkm_121{cursor:col-resize;touch-action:none}._columnSeparator_1oqkm_109:after{content:"";background-color:#0000;width:1px;height:100%;transition:background-color .12s,width .12s}._headerCell_1oqkm_1:hover ._columnSeparator_1oqkm_109:after,._headerCell_1oqkm_1:focus-within ._columnSeparator_1oqkm_109:after{background-color:var(--mantine-color-default-border)}._columnSeparatorResizable_1oqkm_121:hover:after,._columnSeparatorActive_1oqkm_144:after{background-color:var(--mantine-primary-color-filled);width:2px}._menuItemActive_1oqkm_150{background-color:var(--mantine-color-default-hover);font-weight:600}._toolbar_4my8t_1{align-items:center;gap:var(--mantine-spacing-xs);padding:6px var(--mantine-spacing-xs);flex-shrink:0;min-height:42px;display:flex}._toolbarSpacer_4my8t_10{flex:1}
1
+ ._root_13dg1_8[data-size=xs]{--dg-row-height:34px;--dg-header-height:32px;--dg-font-size:var(--mantine-font-size-xs);--dg-padding:6px}._root_13dg1_8[data-size=sm]{--dg-row-height:42px;--dg-header-height:38px;--dg-font-size:var(--mantine-font-size-sm);--dg-padding:8px}._root_13dg1_8[data-size=md]{--dg-row-height:52px;--dg-header-height:44px;--dg-font-size:var(--mantine-font-size-sm);--dg-padding:10px}._root_13dg1_8[data-size=lg]{--dg-row-height:62px;--dg-header-height:52px;--dg-font-size:var(--mantine-font-size-md);--dg-padding:14px}._root_13dg1_8[data-size=xl]{--dg-row-height:72px;--dg-header-height:60px;--dg-font-size:var(--mantine-font-size-lg);--dg-padding:18px}._root_13dg1_8{--dg-row-height:52px;--dg-header-height:44px;--dg-font-size:var(--mantine-font-size-sm);--dg-padding:10px;--dg-row-selected-bg:var(--mantine-primary-color-light);--dg-row-highlight-bg:var(--mantine-primary-color-light-hover);--dg-row-group-bg:var(--mantine-color-body);text-align:left;border:1px solid var(--mantine-color-default-border);border-radius:var(--mantine-radius-md);background-color:var(--mantine-color-body);flex-direction:column;min-height:0;display:flex;overflow:hidden}._columnsPanel_1492o_1{flex-direction:column;width:280px;display:flex}._columnsPanelSearch_1492o_7{padding:var(--mantine-spacing-sm)}._columnsPanelList_1492o_11{gap:var(--mantine-spacing-sm);padding:0 var(--mantine-spacing-sm) var(--mantine-spacing-sm);flex-direction:column;display:flex}._columnsPanelFooter_1492o_18{justify-content:space-between;align-items:center;gap:var(--mantine-spacing-sm);padding:var(--mantine-spacing-sm);border-top:1px solid var(--mantine-color-default-border);display:flex}._groupToggle_h06xv_4{width:100%;min-width:0;font-size:var(--dg-font-size);color:inherit;align-items:center;gap:6px;display:flex}._chevron_h06xv_14{color:var(--mantine-color-dimmed);flex:none;align-items:center;transition:transform .12s;display:flex}._chevron_h06xv_14[data-expanded=true]{transform:rotate(90deg)}@media (prefers-reduced-motion:reduce){._chevron_h06xv_14{transition:none}}._groupLabel_h06xv_34{white-space:nowrap;text-overflow:ellipsis;min-width:0;font-weight:600;overflow:hidden}._groupCount_h06xv_44{color:var(--mantine-color-dimmed);font-variant-numeric:tabular-nums;flex:none}._filterPanel_izakx_4{top:calc(var(--dg-header-height) + 2px);left:var(--mantine-spacing-md);z-index:5;padding:var(--mantine-spacing-sm);border-radius:var(--mantine-radius-md);border:1px solid var(--mantine-color-default-border);background-color:var(--mantine-color-body);box-shadow:var(--mantine-shadow-md);position:absolute}._filterRow_izakx_16{align-items:flex-end;gap:var(--mantine-spacing-xs);display:flex}._filterPanelHeader_izakx_22{justify-content:space-between;align-items:center;gap:var(--mantine-spacing-md);padding-bottom:var(--mantine-spacing-xs);display:flex}._filterPanelFooter_izakx_30{justify-content:space-between;align-items:center;gap:var(--mantine-spacing-md);display:flex}._filterPills_1yybb_3{align-items:center;gap:var(--mantine-spacing-xs);flex-wrap:wrap;display:flex}._filterPills_1yybb_3 ._pill_1yybb_15{background-color:var(--lightningcss-light,var(--mantine-color-gray-1))var(--lightningcss-dark,var(--mantine-color-dark-5));color:var(--lightningcss-light,var(--mantine-color-black))var(--lightningcss-dark,var(--mantine-color-dark-0));border:1px solid var(--lightningcss-light,var(--mantine-color-gray-3))var(--lightningcss-dark,var(--mantine-color-dark-3))}._pillLabel_1yybb_27{font:inherit;color:inherit;cursor:pointer}._pillLabel_1yybb_27:hover{text-decoration:underline}._footer_149ca_1{justify-content:flex-end;align-items:center;gap:var(--mantine-spacing-lg);padding:4px var(--mantine-spacing-sm);border-top:1px solid var(--mantine-color-default-border);flex-shrink:0;min-height:48px;display:flex}._footer_149ca_1[data-paging-suspended=true]{opacity:.55}._tableWrapper_chabz_2{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}._scrollContainer_chabz_10{flex:1;min-height:0;position:relative;overflow:auto}._grid_chabz_18{display:grid}._headerRow_chabz_24{grid-column:1/-1;grid-template-columns:subgrid;z-index:3;display:grid;position:sticky;top:0}._bodyRow_chabz_33{--row-bg:var(--mantine-color-body);grid-column:1/-1;grid-template-columns:subgrid;align-items:stretch;display:grid}._bodyRow_chabz_33:hover{--row-bg:var(--mantine-color-default-hover)}._bodyRow_chabz_33[data-grouped=true]{--row-bg:var(--dg-row-group-bg)}._bodyRow_chabz_33[data-context-menu=true]{--row-bg:var(--mantine-color-default-hover)}._bodyRow_chabz_33[data-selected-bg=true]{--row-bg:var(--dg-row-selected-bg)}._bodyRow_chabz_33[data-highlighted=true]{--row-bg:var(--dg-row-highlight-bg)}._bodyRow_chabz_33[data-selects-on-click=true]{-webkit-user-select:none;user-select:none}._bodyRow_chabz_33:focus-visible{outline:2px solid var(--mantine-primary-color-filled);outline-offset:-2px}._bodyCell_chabz_91{padding:0 var(--dg-padding);border-bottom:1px solid var(--mantine-color-default-border);background-color:var(--row-bg);font-size:var(--dg-font-size);align-items:center;display:flex}._bodyCell_chabz_91[data-select-column=true]{padding:0}._cellContent_chabz_89{white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}._bodyCell_chabz_91[data-align=right]{justify-content:flex-end}._bodyCell_chabz_91[data-align=center]{justify-content:center}._bodyCell_chabz_91[data-align=left] ._cellContent_chabz_89{flex:auto}._messageRow_chabz_129{justify-content:center;align-items:center;gap:var(--mantine-spacing-xs);padding:var(--mantine-spacing-xl);flex-direction:column;grid-column:1/-1;display:flex}._stickyLeft_1iqrf_11{border-right:1px solid var(--mantine-color-default-border)}._stickyRight_1iqrf_15{border-left:1px solid var(--mantine-color-default-border)}._stickyLeft_1iqrf_11:after,._stickyRight_1iqrf_15:after{content:"";pointer-events:none;width:4px;position:absolute;top:0;bottom:0}._stickyLeft_1iqrf_11:after{background:linear-gradient(90deg,var(--lightningcss-light,#00000014)var(--lightningcss-dark,#00000047),#0000);left:100%}._stickyRight_1iqrf_15:after{background:linear-gradient(270deg,var(--lightningcss-light,#00000014)var(--lightningcss-dark,#00000047),#0000);right:100%}._headerCell_49xbo_1{padding:0 var(--dg-padding);min-width:0;min-height:var(--dg-header-height);font-weight:600;font-size:var(--dg-font-size);background-color:var(--mantine-color-body);border-bottom:1px solid var(--mantine-color-default-border);-webkit-user-select:none;user-select:none;align-items:center;gap:2px;display:flex;position:relative}._headerCell_49xbo_1[data-align=right]{justify-content:flex-end}._headerCell_49xbo_1[data-align=center]{justify-content:center}._headerCell_49xbo_1[data-select-column=true]{padding:0}._headerCell_49xbo_1[data-active=true] ._headerTitle_49xbo_31{color:var(--mantine-primary-color-filled)}._headerTitle_49xbo_31{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}._headerCell_49xbo_1[data-align=right] ._headerTitle_49xbo_31{text-align:right}._headerCell_49xbo_1[data-align=center] ._headerTitle_49xbo_31{justify-content:center;display:flex}._headerActions_49xbo_54{flex-shrink:0;align-items:center;gap:0;margin-left:auto;display:flex}._headerCell_49xbo_1[data-align=right] ._headerActions_49xbo_54{order:-1;margin-left:0;margin-right:auto}._headerAction_49xbo_54{display:none}._headerCell_49xbo_1:hover ._headerAction_49xbo_54,._headerCell_49xbo_1:focus-within ._headerAction_49xbo_54,._headerAction_49xbo_54[data-pinned-visible=true]{display:flex}._headerCellSortable_49xbo_83{cursor:pointer}._headerCellDragging_49xbo_90{opacity:.4}._headerCellDropBefore_49xbo_94:before,._headerCellDropAfter_49xbo_95:before{content:"";background-color:var(--mantine-primary-color-filled);z-index:5;pointer-events:none;width:2px;position:absolute;top:0;bottom:0}._headerCellDropBefore_49xbo_94:before{left:0}._headerCellDropAfter_49xbo_95:before{right:0}._columnSeparator_49xbo_115{z-index:1;justify-content:center;width:10px;height:50%;display:flex;position:absolute;top:25%;right:0;transform:translate(50%)}._columnSeparatorResizable_49xbo_127{cursor:col-resize;touch-action:none}._columnSeparator_49xbo_115:after{content:"";background-color:#0000;width:1px;height:100%;transition:background-color .12s,width .12s}._headerCell_49xbo_1:hover ._columnSeparator_49xbo_115:after,._headerCell_49xbo_1:focus-within ._columnSeparator_49xbo_115:after{background-color:var(--mantine-color-default-border)}._columnSeparatorResizable_49xbo_127:hover:after,._columnSeparatorActive_49xbo_150:after{background-color:var(--mantine-primary-color-filled);width:2px}._menuItemActive_49xbo_156{background-color:var(--mantine-color-default-hover);font-weight:600}._toolbar_4my8t_1{align-items:center;gap:var(--mantine-spacing-xs);padding:6px var(--mantine-spacing-xs);flex-shrink:0;min-height:42px;display:flex}._toolbarSpacer_4my8t_10{flex:1}
2
2
  /*$vite$:1*/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jielga/tmdatagrid",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A React data grid built on TanStack Table v9 and Mantine — always virtualized, with resizable, reorderable, sortable, filterable, hideable and pinnable columns.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,7 +11,7 @@ description: >
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '0.2.0'
14
+ library_version: '0.3.0'
15
15
  sources:
16
16
  - 'Jielga/TMDataGrid:src/docs/columns.md'
17
17
  - 'Jielga/TMDataGrid:src/tmdatagrid/core/filterOperators.ts'
@@ -17,7 +17,7 @@ description: >
17
17
  metadata:
18
18
  type: core
19
19
  library: '@jielga/tmdatagrid'
20
- library_version: '0.2.0'
20
+ library_version: '0.3.0'
21
21
  sources:
22
22
  - 'Jielga/TMDataGrid:src/docs/features.md'
23
23
  - 'Jielga/TMDataGrid:src/tmdatagrid/core/capabilities.ts'
@@ -4,13 +4,13 @@ description: >
4
4
  Set up TMDataGrid, a compound React data grid built on TanStack Table v9 and
5
5
  Mantine. Covers useTMDataGrid, the TMDataGrid root, context, the component
6
6
  catalog (Table, Footer, Toolbar, Spacer, SummaryCount, FilterButton,
7
- ColumnsButton, FilterPanel, ColumnsPanel), the size scale and the
7
+ ColumnsButton, FilterPanel, FilterPills, ColumnsPanel), the size scale and the
8
8
  bounded-height layout requirement. Load when adding a grid, choosing which
9
9
  parts to render, or when rows do not appear.
10
10
  metadata:
11
11
  type: core
12
12
  library: '@jielga/tmdatagrid'
13
- library_version: '0.2.0'
13
+ library_version: '0.3.0'
14
14
  sources:
15
15
  - 'Jielga/TMDataGrid:src/docs/getting-started.md'
16
16
  - 'Jielga/TMDataGrid:src/docs/components.md'
@@ -123,21 +123,24 @@ takes a fixed pixel width once resized or pinned.
123
123
 
124
124
  ## Components
125
125
 
126
- All read the grid from context and must be rendered inside `TMDataGrid`. Order
127
- and presence are up to you — the root is a plain flex column.
126
+ All read the grid from context and must be rendered inside `TMDataGrid`, except
127
+ `TMDataGrid.FilterPills`, which takes the grid as an `api` prop and can live
128
+ anywhere on the page. Order and presence are up to you — the root is a plain
129
+ flex column.
128
130
 
129
131
  | Component | Props | Notes |
130
132
  | --- | --- | --- |
131
133
  | `TMDataGrid` | `table`, `ui`, `features`, `size`, `className`, `style` | Root. Provides context. `style` also takes CSS variables: `--dg-row-selected-bg`, `--dg-row-height`, `--dg-header-height`, `--dg-font-size`, `--dg-padding`. |
132
- | `TMDataGrid.Table` | `onRowClick(row)` | Header, virtualized body, filter panel. `onRowClick` runs in addition to selection under `rowSelectionMode: "row"`. |
134
+ | `TMDataGrid.Table` | `onRowClick(row)`, `rowContextMenu` render prop, `rowContextMenuProps` | Header, virtualized body, filter panel. `onRowClick` runs in addition to selection under `rowSelectionMode: "row"`. |
133
135
  | `TMDataGrid.Footer` | `pageSizeOptions` (default `[10, 25, 50, 100]`), `pagination` render prop | Pagination controls. Renders nothing unless pagination is enabled. |
134
136
  | `TMDataGrid.Toolbar` | `children` | Flex row above the grid. |
135
137
  | `TMDataGrid.Spacer` | — | Pushes later toolbar items right. |
136
138
  | `TMDataGrid.SummaryCount` | `children` | Visible rows out of total. |
137
139
  | `TMDataGrid.FilterButton` | — | Toggles filter panel. Renders nothing if no column is filterable. |
138
140
  | `TMDataGrid.ColumnsButton` | — | Opens column manager. Renders nothing if no column is hideable. |
139
- | `TMDataGrid.FilterPanel` | — | Rendered by `.Table`; exported for custom layouts. |
141
+ | `TMDataGrid.FilterPanel` | — | Rendered by `.Table`; exported for custom layouts. Header close button, Escape, click-away, "Add filter" and "Clear all". |
140
142
  | `TMDataGrid.ColumnsPanel` | — | Rendered by `.ColumnsButton`; exported for custom layouts. |
143
+ | `TMDataGrid.FilterPills` | `api`, `size` (default `"sm"`), `showClearAll` (default `true`), `onPillClick(columnId)`, `className` | One pill per active filter, ✕ to clear it. Takes the api as a prop, so it can be rendered outside the grid. Also exported as `TMDataGridFilterPills`. |
141
144
 
142
145
  Pass the row type so `onRowClick` stays typed:
143
146
 
@@ -145,6 +148,37 @@ Pass the row type so `onRowClick` stays typed:
145
148
  <TMDataGrid.Table<Employee> onRowClick={(row) => open(row.original.id)} />
146
149
  ```
147
150
 
151
+ `rowContextMenu` fills the dropdown of a Mantine `Menu` the grid opens at the
152
+ pointer on a right-click (or a long press). The grid owns the `Menu` — opening,
153
+ positioning, and closing on Escape, outside click, body scroll or an item pick —
154
+ so the render prop only says what goes in it:
155
+
156
+ ```tsx
157
+ <TMDataGrid.Table<Employee>
158
+ rowContextMenu={({ table, row, cell, close }) => (
159
+ <>
160
+ <Menu.Label>{row.original.firstName}</Menu.Label>
161
+ <Menu.Item onClick={() => open(row.original.id)}>Open</Menu.Item>
162
+ <Menu.Item
163
+ onClick={() =>
164
+ navigator.clipboard.writeText(String(cell?.getValue() ?? ""))
165
+ }
166
+ >
167
+ Copy cell value
168
+ </Menu.Item>
169
+ </>
170
+ )}
171
+ />
172
+ ```
173
+
174
+ `cell` is the one that was right-clicked, `table` is there for actions that read
175
+ the selection, and `close` is for dropdown content that is not a `Menu.Item`
176
+ (those close themselves). Return `null` to leave a row without a menu. It is
177
+ called during render, and only for the open row, so it must stay pure — put the
178
+ work in the item handlers. Right-clicking does not change the selection or the
179
+ highlight; the row carries `data-context-menu` while its menu is open. Pass
180
+ `rowContextMenuProps` for `width`, `shadow`, `position` and the rest.
181
+
148
182
  `TMDataGrid.Footer` reads totals from `table.getRowCount()`, which prefers
149
183
  `options.rowCount`. `TMDataGrid.SummaryCount` uses `meta.totalRowCount` when
150
184
  provided, otherwise the pre-filtered row count.
@@ -194,6 +228,7 @@ mirrors them. Set `meta.rowHeight` for a height outside the scale.
194
228
  | `openColumnFilter(api, columnId)` | Adds an empty filter for a column and opens the panel. |
195
229
  | `getColumnLabel(column)` | `meta.label`, a string header, or the column id. |
196
230
  | `getColumnType(column)` | `meta.type`, defaulting to `"string"`. |
231
+ | `formatFilterLabel({ label, type, filter })` | The one-line filter description used on the pills. |
197
232
  | `moveColumn({ table, columnId, targetId, side })` | Moves a column next to another. See the `features` skill. |
198
233
  | `moveColumnByStep({ table, columnId, direction })` | Moves a column one position within its region. |
199
234
  | `SELECT_COLUMN_ID` | Id of the generated checkbox column. |
@@ -13,7 +13,7 @@ description: >
13
13
  metadata:
14
14
  type: core
15
15
  library: '@jielga/tmdatagrid'
16
- library_version: '0.2.0'
16
+ library_version: '0.3.0'
17
17
  sources:
18
18
  - 'Jielga/TMDataGrid:src/docs/use-tm-data-grid.md'
19
19
  - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
@@ -10,7 +10,7 @@ description: >
10
10
  metadata:
11
11
  type: core
12
12
  library: '@jielga/tmdatagrid'
13
- library_version: '0.2.0'
13
+ library_version: '0.3.0'
14
14
  sources:
15
15
  - 'Jielga/TMDataGrid:src/docs/server-side.md'
16
16
  - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
@@ -46,9 +46,19 @@
46
46
  --dg-header-height: 44px;
47
47
  --dg-font-size: var(--mantine-font-size-sm);
48
48
  --dg-padding: 10px;
49
- /* Background of a highlighted (selected) row. Override it on the grid:
49
+ /* Background of a selected row, when `showSelectedBackground` is on. Override
50
+ it on the grid:
50
51
  <TMDataGrid style={{ "--dg-row-selected-bg": "var(--mantine-color-blue-0)" }} /> */
51
52
  --dg-row-selected-bg: var(--mantine-primary-color-light);
53
+ /* Background of the highlighted row, under a selectionMode that has one.
54
+ Stronger than the selected background by default, since under
55
+ "checkboxAndHighlight" the two show up together. */
56
+ --dg-row-highlight-bg: var(--mantine-primary-color-light-hover);
57
+ /* Background of a group row. Transparent by default — the bold label and the
58
+ chevron are enough, and with the tree collapsed every row on screen is a
59
+ group row, so a tint would grey out the whole grid. Set it to give groups a
60
+ band of their own. */
61
+ --dg-row-group-bg: var(--mantine-color-body);
52
62
 
53
63
  display: flex;
54
64
  flex-direction: column;
@@ -4,6 +4,7 @@ import classes from "./TMDataGrid.module.css";
4
4
  import { TMDataGridColumnsPanel } from "./TMDataGridColumnsPanel";
5
5
  import { TMDataGridContextProvider } from "../TMDataGridContext";
6
6
  import { TMDataGridFilterPanel } from "./TMDataGridFilterPanel";
7
+ import { TMDataGridFilterPills } from "./TMDataGridFilterPills";
7
8
  import { TMDataGridFooter } from "./TMDataGridFooter";
8
9
  import { TMDataGridTable } from "./TMDataGridTable";
9
10
  import {
@@ -101,6 +102,11 @@ export const TMDataGrid = Object.assign(TMDataGridRoot, {
101
102
  Footer: TMDataGridFooter,
102
103
  /** Rendered by `TMDataGrid.Table`; exported for custom layouts. */
103
104
  FilterPanel: TMDataGridFilterPanel,
105
+ /**
106
+ * Takes the grid as an `api` prop rather than from context, so it can be
107
+ * rendered outside `<TMDataGrid>` — a page header, for instance.
108
+ */
109
+ FilterPills: TMDataGridFilterPills,
104
110
  /** Rendered by `TMDataGrid.ColumnsButton`; exported for custom layouts. */
105
111
  ColumnsPanel: TMDataGridColumnsPanel,
106
112
  });
@@ -4,6 +4,7 @@ import { useState } from "react";
4
4
  import classes from "./TMDataGridColumnsPanel.module.css";
5
5
  import { useTMDataGridContext } from "../TMDataGridContext";
6
6
  import { getColumnLabel } from "../core/columnUtils";
7
+ import { GROUP_COLUMN_ID } from "./TMDataGridGroupColumn";
7
8
  import { SearchIcon } from "./icons";
8
9
 
9
10
  /**
@@ -20,7 +21,12 @@ export function TMDataGridColumnsPanel() {
20
21
  (state) => state.columnVisibility,
21
22
  );
22
23
 
23
- const columns = table.getAllLeafColumns();
24
+ // The tree column is left out rather than listed and disabled: its visibility
25
+ // is not a setting at all, it follows the grouping state, so an unchecked box
26
+ // that cannot be ticked would only invite the question.
27
+ const columns = table
28
+ .getAllLeafColumns()
29
+ .filter((column) => column.id !== GROUP_COLUMN_ID);
24
30
  const needle = search.trim().toLowerCase();
25
31
  const visibleInPanel = needle
26
32
  ? columns.filter((column) =>
@@ -18,3 +18,18 @@
18
18
  align-items: flex-end;
19
19
  gap: var(--mantine-spacing-xs);
20
20
  }
21
+
22
+ .filterPanelHeader {
23
+ display: flex;
24
+ align-items: center;
25
+ justify-content: space-between;
26
+ gap: var(--mantine-spacing-md);
27
+ padding-bottom: var(--mantine-spacing-xs);
28
+ }
29
+
30
+ .filterPanelFooter {
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: space-between;
34
+ gap: var(--mantine-spacing-md);
35
+ }
@@ -1,5 +1,6 @@
1
- import { ActionIcon, Button, Select, Stack, TextInput } from "@mantine/core";
1
+ import { ActionIcon, Button, Select, Stack, Text, TextInput } from "@mantine/core";
2
2
  import { useSelector } from "@tanstack/react-store";
3
+ import { useEffect, useRef } from "react";
3
4
  import classes from "./TMDataGridFilterPanel.module.css";
4
5
  import { useTMDataGridContext } from "../TMDataGridContext";
5
6
  import { getColumnLabel, getColumnType } from "../core/columnUtils";
@@ -33,6 +34,27 @@ export function TMDataGridFilterPanel() {
33
34
  table.store,
34
35
  (state) => state.columnFilters,
35
36
  );
37
+ const panelRef = useRef<HTMLDivElement>(null);
38
+
39
+ // Clicking away hides the panel, the way any floating surface behaves. On
40
+ // pointerdown rather than click, so a press that starts outside dismisses it
41
+ // even when the pointer is released somewhere else.
42
+ useEffect(() => {
43
+ if (!opened) return;
44
+
45
+ const handlePointerDown = (event: PointerEvent) => {
46
+ const target = event.target;
47
+ if (!(target instanceof Element)) return;
48
+ if (panelRef.current?.contains(target)) return;
49
+ // The toolbar button toggles the panel itself. Closing from here first
50
+ // would leave its click reopening what the user meant to close.
51
+ if (target.closest("[data-dg-filter-toggle]")) return;
52
+ ui.actions.closeFilterPanel();
53
+ };
54
+
55
+ document.addEventListener("pointerdown", handlePointerDown);
56
+ return () => document.removeEventListener("pointerdown", handlePointerDown);
57
+ }, [opened, ui]);
36
58
 
37
59
  if (!opened) return null;
38
60
 
@@ -86,6 +108,14 @@ export function TMDataGridFilterPanel() {
86
108
  );
87
109
  }
88
110
 
111
+ /** Clears every filter, including half-typed ones the pills never showed. */
112
+ function clearAllFilters() {
113
+ table.setColumnFilters([]);
114
+ // Same exit as removing the last filter row by hand: an empty panel has
115
+ // nothing to show but its own buttons.
116
+ ui.actions.closeFilterPanel();
117
+ }
118
+
89
119
  function addFilter() {
90
120
  const nextColumn = filterableColumns.find(
91
121
  (column) => !columnFilters.some((filter) => filter.id === column.id),
@@ -103,8 +133,39 @@ export function TMDataGridFilterPanel() {
103
133
  ]);
104
134
  }
105
135
 
136
+ const canAddFilter = filterableColumns.some(
137
+ (column) => !columnFilters.some((filter) => filter.id === column.id),
138
+ );
139
+
106
140
  return (
107
- <div className={classes.filterPanel}>
141
+ <div
142
+ ref={panelRef}
143
+ role="group"
144
+ aria-label="Filters"
145
+ className={classes.filterPanel}
146
+ // Escape is what closes a floating surface, and every control that can
147
+ // hold focus in here sits inside this element.
148
+ onKeyDown={(event) => {
149
+ if (event.key !== "Escape") return;
150
+ event.stopPropagation();
151
+ ui.actions.closeFilterPanel();
152
+ }}
153
+ >
154
+ <div className={classes.filterPanelHeader}>
155
+ <Text size={controlSize} fw={600}>
156
+ Filters
157
+ </Text>
158
+ <ActionIcon
159
+ variant="subtle"
160
+ color="gray"
161
+ size="sm"
162
+ aria-label="Close filters"
163
+ onClick={ui.actions.closeFilterPanel}
164
+ >
165
+ <CloseIcon size={16} stroke={1.6} />
166
+ </ActionIcon>
167
+ </div>
168
+
108
169
  <Stack gap="xs">
109
170
  {columnFilters.map((filter) => {
110
171
  const column = table.getColumn(filter.id);
@@ -129,6 +190,10 @@ export function TMDataGridFilterPanel() {
129
190
  size={controlSize}
130
191
  w={160}
131
192
  allowDeselect={false}
193
+ // Both dropdowns render inside the panel: a portalled one is
194
+ // outside it in the DOM, and picking an option would read as a
195
+ // click away and close the panel under the user.
196
+ comboboxProps={{ withinPortal: false }}
132
197
  data={columnOptions}
133
198
  value={filter.id}
134
199
  onChange={(next) => next && changeFilterColumn(filter.id, next)}
@@ -139,6 +204,7 @@ export function TMDataGridFilterPanel() {
139
204
  size={controlSize}
140
205
  w={170}
141
206
  allowDeselect={false}
207
+ comboboxProps={{ withinPortal: false }}
142
208
  data={getOperatorsForType(type).map((operator) => ({
143
209
  value: operator,
144
210
  label: FILTER_OPERATOR_LABELS[operator],
@@ -168,14 +234,25 @@ export function TMDataGridFilterPanel() {
168
234
  );
169
235
  })}
170
236
 
171
- <Button
172
- variant="subtle"
173
- size="compact-sm"
174
- style={{ alignSelf: "flex-start" }}
175
- onClick={addFilter}
176
- >
177
- Add filter
178
- </Button>
237
+ <div className={classes.filterPanelFooter}>
238
+ <Button
239
+ variant="subtle"
240
+ size="compact-sm"
241
+ disabled={!canAddFilter}
242
+ onClick={addFilter}
243
+ >
244
+ Add filter
245
+ </Button>
246
+ <Button
247
+ variant="subtle"
248
+ color="gray"
249
+ size="compact-sm"
250
+ disabled={columnFilters.length === 0}
251
+ onClick={clearAllFilters}
252
+ >
253
+ Clear all
254
+ </Button>
255
+ </div>
179
256
  </Stack>
180
257
  </div>
181
258
  );
@@ -0,0 +1,35 @@
1
+ /* Wraps rather than scrolls: the pills are a summary, and a summary that goes
2
+ off-screen is not one. */
3
+ .filterPills {
4
+ display: flex;
5
+ align-items: center;
6
+ flex-wrap: wrap;
7
+ gap: var(--mantine-spacing-xs);
8
+ }
9
+
10
+ /* Mantine's dark-scheme pill is `dark-7` — the same colour as the body — so a
11
+ pill sitting on the page rather than inside a `PillsInput` disappears. Both
12
+ schemes are restated here so the two stay a pair, with a border to keep the
13
+ shape readable against any surface. Scoped through `.filterPills` for the
14
+ specificity to beat Mantine's own rule whichever stylesheet loads last. */
15
+ .filterPills .pill {
16
+ background-color: light-dark(
17
+ var(--mantine-color-gray-1),
18
+ var(--mantine-color-dark-5)
19
+ );
20
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
21
+ border: 1px solid
22
+ light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-3));
23
+ }
24
+
25
+ /* The label is a button so a pill can be clicked back open, but it must read as
26
+ part of the pill — Mantine's own label styles carry the font. */
27
+ .pillLabel {
28
+ font: inherit;
29
+ color: inherit;
30
+ cursor: pointer;
31
+ }
32
+
33
+ .pillLabel:hover {
34
+ text-decoration: underline;
35
+ }
@@ -0,0 +1,135 @@
1
+ import { Button, Pill, UnstyledButton } from "@mantine/core";
2
+ import type { RowData } from "@tanstack/react-table";
3
+ import { useSelector } from "@tanstack/react-store";
4
+ import classes from "./TMDataGridFilterPills.module.css";
5
+ import { getColumnLabel, getColumnType } from "../core/columnUtils";
6
+ import {
7
+ formatFilterLabel,
8
+ isFilterActive,
9
+ isTMDataGridFilterValue,
10
+ } from "../core/filterOperators";
11
+ import type { TMDataGridSize } from "../core/sizes";
12
+ import { openColumnFilter, type TMDataGridApi } from "../useTMDataGrid";
13
+
14
+ export type TMDataGridFilterPillsProps<TData extends RowData> = {
15
+ /** The object returned by `useTMDataGrid`. */
16
+ api: TMDataGridApi<TData>;
17
+ /** Mantine size of the pills. Defaults to `"sm"`. */
18
+ size?: TMDataGridSize;
19
+ /** "Clear all" button, shown once two filters are active. Defaults to `true`. */
20
+ showClearAll?: boolean;
21
+ /**
22
+ * Replaces what a click on a pill's label does. The default opens the grid's
23
+ * filter panel on that column.
24
+ */
25
+ onPillClick?: (columnId: string) => void;
26
+ className?: string;
27
+ };
28
+
29
+ /**
30
+ * One pill per active filter — `First name: Sofia ✕` — with the ✕ clearing that
31
+ * filter and a click on the label opening the filter panel on its column.
32
+ *
33
+ * The one grid component that takes the api as a prop instead of reading
34
+ * context, so it can live anywhere: a page header, a card title, a breadcrumb
35
+ * row. Renders nothing while no filter is active.
36
+ *
37
+ * ```tsx
38
+ * const grid = useTMDataGrid({ data, columns });
39
+ *
40
+ * <Group>
41
+ * <Title order={3}>Employees</Title>
42
+ * <TMDataGridFilterPills api={grid} />
43
+ * </Group>
44
+ * ```
45
+ *
46
+ * Half-typed filters are left out: a filter that is not narrowing the rows yet
47
+ * has nothing to report, which is the same rule the funnel indicator uses.
48
+ */
49
+ export function TMDataGridFilterPills<TData extends RowData>({
50
+ api,
51
+ size = "sm",
52
+ showClearAll = true,
53
+ onPillClick,
54
+ className,
55
+ }: TMDataGridFilterPillsProps<TData>) {
56
+ const { table } = api;
57
+ const columnFilters = useSelector(
58
+ table.store,
59
+ (state) => state.columnFilters,
60
+ );
61
+
62
+ const activeFilters = columnFilters.filter((filter) =>
63
+ isFilterActive(filter.value),
64
+ );
65
+ if (activeFilters.length === 0) return null;
66
+
67
+ return (
68
+ <div
69
+ role="group"
70
+ aria-label="Active filters"
71
+ className={[classes.filterPills, className].filter(Boolean).join(" ")}
72
+ >
73
+ <Pill.Group size={size}>
74
+ {activeFilters.map((filter) => {
75
+ const column = table.getColumn(filter.id);
76
+ const label = column ? getColumnLabel(column) : filter.id;
77
+ const value = filter.value;
78
+ // isFilterActive already vouched for the shape; this narrows it.
79
+ if (!isTMDataGridFilterValue(value)) return null;
80
+
81
+ return (
82
+ <Pill
83
+ key={filter.id}
84
+ size={size}
85
+ className={classes.pill}
86
+ withRemoveButton
87
+ onRemove={() =>
88
+ table.setColumnFilters(
89
+ columnFilters.filter((other) => other.id !== filter.id),
90
+ )
91
+ }
92
+ // Mantine hides its remove button from assistive tech and from
93
+ // the tab order, because inside a `PillsInput` Backspace is the
94
+ // way out. These pills stand on their own, so the ✕ has to be a
95
+ // real button.
96
+ removeButtonProps={{
97
+ "aria-label": `Clear ${label} filter`,
98
+ "aria-hidden": false,
99
+ tabIndex: 0,
100
+ }}
101
+ >
102
+ <UnstyledButton
103
+ className={classes.pillLabel}
104
+ onClick={() =>
105
+ onPillClick
106
+ ? onPillClick(filter.id)
107
+ : openColumnFilter(api, filter.id)
108
+ }
109
+ >
110
+ {formatFilterLabel({
111
+ label,
112
+ type: column ? getColumnType(column) : "string",
113
+ filter: value,
114
+ })}
115
+ </UnstyledButton>
116
+ </Pill>
117
+ );
118
+ })}
119
+ </Pill.Group>
120
+
121
+ {showClearAll && activeFilters.length > 1 && (
122
+ // Below two pills their own ✕ is the shorter path, so the button would
123
+ // only be a second way to do the same thing.
124
+ <Button
125
+ variant="subtle"
126
+ color="gray"
127
+ size="compact-xs"
128
+ onClick={() => table.setColumnFilters([])}
129
+ >
130
+ Clear all
131
+ </Button>
132
+ )}
133
+ </div>
134
+ );
135
+ }
@@ -8,3 +8,11 @@
8
8
  flex-shrink: 0;
9
9
  border-top: 1px solid var(--mantine-color-default-border);
10
10
  }
11
+
12
+ /* Grouping suspends paging. The pager stays put and fades rather than
13
+ disappearing, so the footer reads as switched off rather than as having
14
+ vanished — and the row it holds does not change height under the grid. Its
15
+ controls carry their own `disabled`; this is the label half. */
16
+ .footer[data-paging-suspended="true"] {
17
+ opacity: 0.55;
18
+ }