@jielga/tmdatagrid 0.3.0 → 0.5.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 (43) hide show
  1. package/dist/index.d.ts +947 -64
  2. package/dist/index.js +2474 -1423
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/package.json +2 -2
  6. package/skills/columns/SKILL.md +1 -1
  7. package/skills/features/SKILL.md +61 -11
  8. package/skills/getting-started/SKILL.md +1 -1
  9. package/skills/options/SKILL.md +12 -2
  10. package/skills/server-side/SKILL.md +1 -1
  11. package/src/tmdatagrid/TMDataGridContext.ts +26 -0
  12. package/src/tmdatagrid/components/TMDataGrid.module.css +10 -0
  13. package/src/tmdatagrid/components/TMDataGrid.tsx +23 -1
  14. package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +5 -5
  15. package/src/tmdatagrid/components/TMDataGridDetailsColumn.module.css +33 -0
  16. package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +160 -0
  17. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +111 -33
  18. package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +5 -4
  19. package/src/tmdatagrid/components/TMDataGridFooter.tsx +11 -19
  20. package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +23 -11
  21. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +15 -3
  22. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +181 -52
  23. package/src/tmdatagrid/components/TMDataGridSearch.tsx +122 -0
  24. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +23 -13
  25. package/src/tmdatagrid/components/TMDataGridTable.module.css +127 -6
  26. package/src/tmdatagrid/components/TMDataGridTable.tsx +1003 -30
  27. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +30 -7
  28. package/src/tmdatagrid/components/icons.ts +1 -0
  29. package/src/tmdatagrid/components/sticky.module.css +77 -16
  30. package/src/tmdatagrid/core/autosize.ts +125 -0
  31. package/src/tmdatagrid/core/capabilities.ts +41 -0
  32. package/src/tmdatagrid/core/cellExport.ts +321 -0
  33. package/src/tmdatagrid/core/cellNavigation.ts +103 -0
  34. package/src/tmdatagrid/core/cellRange.ts +118 -0
  35. package/src/tmdatagrid/core/columnOptions.ts +123 -0
  36. package/src/tmdatagrid/core/columnUtils.ts +18 -0
  37. package/src/tmdatagrid/core/expanding.ts +93 -0
  38. package/src/tmdatagrid/core/filterOperators.ts +176 -15
  39. package/src/tmdatagrid/core/labels.ts +211 -0
  40. package/src/tmdatagrid/core/labelsSv.ts +108 -0
  41. package/src/tmdatagrid/core/summary.ts +35 -0
  42. package/src/tmdatagrid/index.ts +70 -0
  43. package/src/tmdatagrid/useTMDataGrid.tsx +328 -12
package/dist/styles.css CHANGED
@@ -1,2 +1,2 @@
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}
1
+ ._root_qequm_8[data-size=xs]{--dg-row-height:34px;--dg-header-height:32px;--dg-font-size:var(--mantine-font-size-xs);--dg-padding:6px}._root_qequm_8[data-size=sm]{--dg-row-height:42px;--dg-header-height:38px;--dg-font-size:var(--mantine-font-size-sm);--dg-padding:8px}._root_qequm_8[data-size=md]{--dg-row-height:52px;--dg-header-height:44px;--dg-font-size:var(--mantine-font-size-sm);--dg-padding:10px}._root_qequm_8[data-size=lg]{--dg-row-height:62px;--dg-header-height:52px;--dg-font-size:var(--mantine-font-size-md);--dg-padding:14px}._root_qequm_8[data-size=xl]{--dg-row-height:72px;--dg-header-height:60px;--dg-font-size:var(--mantine-font-size-lg);--dg-padding:18px}._root_qequm_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);--dg-z-pinned-cell:2;--dg-z-focused-cell:3;--dg-z-summary-row:4;--dg-z-summary-pinned-cell:5;--dg-z-header:6;--dg-z-header-pinned-cell:7;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}._detailsToggle_n39tz_3{width:100%;height:100%;color:inherit;justify-content:center;align-items:center;display:flex}._detailsToggle_n39tz_3:hover ._chevron_n39tz_12{color:var(--mantine-color-text)}._chevron_n39tz_12{color:var(--mantine-color-dimmed);align-items:center;transition:transform .12s;display:flex}._chevron_n39tz_12[data-expanded=true]{transform:rotate(90deg)}@media (prefers-reduced-motion:reduce){._chevron_n39tz_12{transition:none}}._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_1nt60_2{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}._scrollContainer_1nt60_10{flex:1;min-height:0;position:relative;overflow:auto}._grid_1nt60_18{display:grid}._headerRow_1nt60_24{grid-column:1/-1;grid-template-columns:subgrid;z-index:var(--dg-z-header,6);display:grid;position:sticky;top:0}._bodyRow_1nt60_33{--row-bg:var(--mantine-color-body);grid-column:1/-1;grid-template-columns:subgrid;align-items:stretch;display:grid}._bodyRow_1nt60_33:hover{--row-bg:var(--mantine-color-default-hover)}._bodyRow_1nt60_33[data-grouped=true]{--row-bg:var(--dg-row-group-bg)}._bodyRow_1nt60_33[data-context-menu=true]{--row-bg:var(--mantine-color-default-hover)}._bodyRow_1nt60_33[data-selected-bg=true]{--row-bg:var(--dg-row-selected-bg)}._bodyRow_1nt60_33[data-highlighted=true]{--row-bg:var(--dg-row-highlight-bg)}._bodyRow_1nt60_33[data-selects-on-click=true]{-webkit-user-select:none;user-select:none}._bodyRow_1nt60_33:focus-visible{outline:2px solid var(--mantine-primary-color-filled);outline-offset:-2px}._bodyCell_1nt60_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_1nt60_91[data-control-column=true]{padding:0}._grid_1nt60_18[data-range-dragging]{-webkit-user-select:none;user-select:none}._bodyCell_1nt60_91[data-selected=true]{background-color:color-mix(in srgb, var(--mantine-primary-color-filled) 14%, var(--row-bg));--dg-edge-top:transparent;--dg-edge-bottom:transparent;--dg-edge-left:transparent;--dg-edge-right:transparent;box-shadow:inset 0 1px 0 0 var(--dg-edge-top), inset 0 -1px 0 0 var(--dg-edge-bottom), inset 1px 0 0 0 var(--dg-edge-left), inset -1px 0 0 0 var(--dg-edge-right)}._bodyCell_1nt60_91[data-edge-top=true]{--dg-edge-top:var(--mantine-primary-color-filled)}._bodyCell_1nt60_91[data-edge-bottom=true]{--dg-edge-bottom:var(--mantine-primary-color-filled)}._bodyCell_1nt60_91[data-edge-left=true]{--dg-edge-left:var(--mantine-primary-color-filled)}._bodyCell_1nt60_91[data-edge-right=true]{--dg-edge-right:var(--mantine-primary-color-filled)}._bodyCell_1nt60_91[data-focused=true]{outline:2px solid var(--mantine-primary-color-filled);outline-offset:-2px}._cellContent_1nt60_89{white-space:nowrap;text-overflow:ellipsis;min-width:0;overflow:hidden}._bodyCell_1nt60_91[data-align=right]{justify-content:flex-end}._bodyCell_1nt60_91[data-align=center]{justify-content:center}._bodyCell_1nt60_91[data-align=left] ._cellContent_1nt60_89{flex:auto}._detailsCell_1nt60_204{border-bottom:1px solid var(--mantine-color-default-border);background-color:var(--row-bg);padding:var(--dg-padding);-webkit-user-select:text;user-select:text;cursor:auto;grid-column:1/-1}._messageRow_1nt60_215{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}._summaryRow_1nt60_228{grid-column:1/-1;grid-template-columns:subgrid;z-index:var(--dg-z-summary-row,4);display:grid;position:sticky;bottom:0}._summaryCell_1nt60_237{padding:0 var(--dg-padding);border-top:1px solid var(--mantine-color-default-border);background-color:var(--mantine-color-body);font-size:var(--dg-font-size);align-items:center;font-weight:600;display:flex}._summaryCell_1nt60_237[data-control-column=true]{padding:0}._summaryCell_1nt60_237[data-align=right]{justify-content:flex-end}._summaryCell_1nt60_237[data-align=center]{justify-content:center}._stickyLeft_6z1iq_16:after,._stickyRight_6z1iq_17:after{content:"";pointer-events:none;width:8px;position:absolute;top:0;bottom:0}._stickyLeft_6z1iq_16:after{background:linear-gradient(90deg,var(--lightningcss-light,#00000012)var(--lightningcss-dark,#00000042),var(--lightningcss-light,#00000006)var(--lightningcss-dark,#00000017) 45%,#0000);left:100%}._stickyRight_6z1iq_17:after{background:linear-gradient(270deg,var(--lightningcss-light,#00000012)var(--lightningcss-dark,#00000042),var(--lightningcss-light,#00000006)var(--lightningcss-dark,#00000017) 45%,#0000);right:100%}@supports (animation-timeline:scroll()){._stickyLeft_6z1iq_16:after,._stickyRight_6z1iq_17:after{--dg-sticky-edge-range:20px;opacity:0;animation-timing-function:linear;animation-fill-mode:both;animation-timeline:scroll(inline)}._stickyLeft_6z1iq_16:after{animation-name:_stickyEdgeIn_6z1iq_1;animation-range:0 var(--dg-sticky-edge-range)}._stickyRight_6z1iq_17:after{animation-name:_stickyEdgeOut_6z1iq_1;animation-range:calc(100% - var(--dg-sticky-edge-range)) 100%}}@keyframes _stickyEdgeIn_6z1iq_1{0%{opacity:0}to{opacity:1}}@keyframes _stickyEdgeOut_6z1iq_1{0%{opacity:1}to{opacity:0}}._headerCell_1b6k8_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_1b6k8_1[data-align=right]{justify-content:flex-end}._headerCell_1b6k8_1[data-align=center]{justify-content:center}._headerCell_1b6k8_1[data-control-column=true]{padding:0}._headerCell_1b6k8_1[data-active=true] ._headerTitle_1b6k8_32{color:var(--mantine-primary-color-filled)}._headerTitle_1b6k8_32{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}._headerCell_1b6k8_1[data-align=right] ._headerTitle_1b6k8_32{text-align:right}._headerCell_1b6k8_1[data-align=center] ._headerTitle_1b6k8_32{justify-content:center;display:flex}._headerActions_1b6k8_55{flex-shrink:0;align-items:center;gap:0;margin-left:auto;display:flex}._headerCell_1b6k8_1[data-align=right] ._headerActions_1b6k8_55{order:-1;margin-left:0;margin-right:auto}._headerAction_1b6k8_55{display:none}._headerCell_1b6k8_1:hover ._headerAction_1b6k8_55,._headerCell_1b6k8_1:focus-within ._headerAction_1b6k8_55,._headerAction_1b6k8_55[data-pinned-visible=true]{display:flex}._headerCellSortable_1b6k8_84{cursor:pointer}._sortIndex_1b6k8_90{color:var(--mantine-color-dimmed);text-align:center;min-width:10px;font-size:10px;font-weight:600;line-height:1}._headerCellDragging_1b6k8_102{opacity:.4}._headerCellDropBefore_1b6k8_106:before,._headerCellDropAfter_1b6k8_107:before{content:"";background-color:var(--mantine-primary-color-filled);z-index:5;pointer-events:none;width:2px;position:absolute;top:0;bottom:0}._headerCellDropBefore_1b6k8_106:before{left:0}._headerCellDropAfter_1b6k8_107:before{right:0}._columnSeparator_1b6k8_127{z-index:1;justify-content:center;width:10px;height:50%;display:flex;position:absolute;top:25%;right:0;transform:translate(50%)}._columnSeparatorResizable_1b6k8_139{cursor:col-resize;touch-action:none}._columnSeparator_1b6k8_127:after{content:"";background-color:#0000;width:1px;height:100%;transition:background-color .12s,width .12s}._headerCell_1b6k8_1:hover ._columnSeparator_1b6k8_127:after,._headerCell_1b6k8_1:focus-within ._columnSeparator_1b6k8_127:after{background-color:var(--mantine-color-default-border)}._columnSeparatorResizable_1b6k8_139:hover:after,._columnSeparatorActive_1b6k8_162:after{background-color:var(--mantine-primary-color-filled);width:2px}._menuItemActive_1b6k8_168{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.3.0",
3
+ "version": "0.5.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 @@
11
11
  "type": "git",
12
12
  "url": "git+https://github.com/Jielga/TMDataGrid.git"
13
13
  },
14
- "homepage": "https://github.com/Jielga/TMDataGrid#readme",
14
+ "homepage": "https://jielga.github.io/TMDataGrid/",
15
15
  "bugs": {
16
16
  "url": "https://github.com/Jielga/TMDataGrid/issues"
17
17
  },
@@ -11,7 +11,7 @@ description: >
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '0.3.0'
14
+ library_version: '0.5.0'
15
15
  sources:
16
16
  - 'Jielga/TMDataGrid:src/docs/columns.md'
17
17
  - 'Jielga/TMDataGrid:src/tmdatagrid/core/filterOperators.ts'
@@ -5,19 +5,20 @@ description: >
5
5
  table and column options, and reuse the same checks in your own components.
6
6
  Covers the enableSorting/enableColumnFilters/enableHiding/enableColumnPinning/
7
7
  enableColumnResizing/enableColumnOrdering/enableRowSelection matrix, the
8
- checkbox and row selection modes behind rowSelectionMode, the
9
- highlightSelectedRows flag and the --dg-row-selected-bg colour, the
10
- default-off enablePagination switch and its three modes (none, client,
11
- manual), getGridCapabilities and getColumnCapabilities, why the features
12
- argument is required for reactivity under the React Compiler, column pinning,
13
- column ordering with moveColumn and moveColumnByStep, and always-on
8
+ selection modes behind rowSelectionMode, the highlightSelectedRows flag and
9
+ the --dg-row-selected-bg colour, the default-off enablePagination switch and
10
+ its three modes (none, client, manual), getGridCapabilities and
11
+ getColumnCapabilities, why the features argument is required for reactivity
12
+ under the React Compiler, column ordering with moveColumn and
13
+ moveColumnByStep, cell selection with arrow-key navigation, drag-selected
14
+ ranges, Ctrl+C copy and Excel-compatible CSV export, and always-on
14
15
  virtualization. Load when building a read-only grid, hiding grid chrome,
15
- enabling pagination, reordering columns from code, or writing a custom toolbar
16
- button.
16
+ enabling pagination, reordering columns from code, adding cell navigation or
17
+ copy/export, or writing a custom toolbar button.
17
18
  metadata:
18
19
  type: core
19
20
  library: '@jielga/tmdatagrid'
20
- library_version: '0.3.0'
21
+ library_version: '0.5.0'
21
22
  sources:
22
23
  - 'Jielga/TMDataGrid:src/docs/features.md'
23
24
  - 'Jielga/TMDataGrid:src/tmdatagrid/core/capabilities.ts'
@@ -242,10 +243,59 @@ const next = getStepTargetColumn({ table, columnId: "salary", direction: 1 });
242
243
  Both movers are no-ops for a move that is not allowed, including one across
243
244
  regions.
244
245
 
246
+ ## Cell selection
247
+
248
+ Off by default. `cellSelection: "single"` gives one focused cell moved with the
249
+ arrow keys; `"range"` adds a selectable rectangle, Ctrl+C and CSV export.
250
+
251
+ ```tsx
252
+ const grid = useTMDataGrid({ data, columns, cellSelection: "range" });
253
+ ```
254
+
255
+ On, the body's tab stop moves from the row to a cell (the whole grid is one Tab
256
+ stop), the container reports `role="grid"` with `gridcell` children, and cells
257
+ carry `data-focused` / `data-selected` / `data-edge-*`.
258
+
259
+ | Key | Does |
260
+ | --- | --- |
261
+ | Arrows | one cell, clamped at the edges |
262
+ | Shift+arrows | extends the rectangle from its anchor |
263
+ | PageUp / PageDown | one viewport of rows |
264
+ | Home / End, Ctrl+Home / Ctrl+End | ends of the row, corners of the grid |
265
+ | Enter or F2 | steps into the cell's control; Escape steps back out |
266
+ | Space | selects the row |
267
+ | Ctrl+C | copies the block as tab-separated text |
268
+
269
+ The body is one tab stop: controls inside body cells take `tabindex="-1"` while
270
+ cell selection is on, so Tab leaves the grid instead of walking one control per
271
+ mounted row. Enter/F2 steps in, Escape steps out, Space ticks the row from any
272
+ cell. Custom cell controls should do the same —
273
+ `tabIndex={useCellControlTabIndex()}`. Header controls are untouched.
274
+
275
+ The generated lanes (checkbox, tree, details) are selectable and navigable —
276
+ they take the tint and the outline — but never exported; a block covering only
277
+ those disables Copy and Export.
278
+
279
+ Drag across cells to select a rectangle; Shift+click extends one. The state is
280
+ `ui.state.focusedCell` and `ui.state.cellRange`, both held as `{ rowId,
281
+ columnId }` pairs so sorting and filtering carry the selection with the cells.
282
+ Move them with `ui.actions.setFocusedCell` / `setCellRange`, follow them with
283
+ `onFocusedCellChange`. One rectangle at a time.
284
+
285
+ Right-clicking inside the selection offers Copy, "Export as CSV for Excel" and
286
+ an Include headers toggle; a consumer's `rowContextMenu` items follow a divider.
287
+ The CSV is Nordic-Excel shaped — `sep=;` line, UTF-8 BOM, CRLF, semicolons,
288
+ decimal comma — and `cellExport` on `TMDataGrid.Table` changes any of that.
289
+ Generated lanes (checkbox, tree, details) are never exported, and what is
290
+ written is the cell's value rather than what it renders.
291
+
245
292
  ## Virtualization
246
293
 
247
- Always enabled and not configurable. Only rows within the viewport, plus a small
248
- overscan, are mounted, so page size does not affect how much is rendered.
294
+ Always enabled. Only rows within the viewport, plus a small overscan, are
295
+ mounted, so page size does not affect how much is rendered.
296
+
297
+ `overscan` on `useTMDataGrid` (default `6`) sets how many rows are kept mounted
298
+ on each side of the viewport — the one configurable part.
249
299
 
250
300
  Row height comes from `meta.rowHeight`, or from the `size` prop when unset. Rows
251
301
  are fixed height; an accurate value keeps scrolling precise.
@@ -10,7 +10,7 @@ description: >
10
10
  metadata:
11
11
  type: core
12
12
  library: '@jielga/tmdatagrid'
13
- library_version: '0.3.0'
13
+ library_version: '0.5.0'
14
14
  sources:
15
15
  - 'Jielga/TMDataGrid:src/docs/getting-started.md'
16
16
  - 'Jielga/TMDataGrid:src/docs/components.md'
@@ -13,7 +13,7 @@ description: >
13
13
  metadata:
14
14
  type: core
15
15
  library: '@jielga/tmdatagrid'
16
- library_version: '0.3.0'
16
+ library_version: '0.5.0'
17
17
  sources:
18
18
  - 'Jielga/TMDataGrid:src/docs/use-tm-data-grid.md'
19
19
  - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
@@ -57,6 +57,9 @@ rather than forwarded to TanStack.
57
57
  | `enableColumnResizing` | `boolean` | `true` | Enables resizing for the table. |
58
58
  | `enableColumnOrdering` | `boolean` | `true` | Enables header dragging and the move menu items. Defined by the grid. |
59
59
  | `enablePagination` | `boolean` | `false` | Enables client-side paging and the `Footer` pager. Implied by `manualPagination`. Defined by the grid. |
60
+ | `cellSelection` | `"none" \| "single" \| "range"` | `"none"` | Cell cursor; `"range"` adds a drag-selectable rectangle, Ctrl+C and CSV export. Defined by the grid. |
61
+ | `onFocusedCellChange` | `(cell \| null) => void` | – | Fires whenever the focused cell moves. |
62
+ | `overscan` | `number` | `6` | Rows the virtualizer keeps mounted above and below the viewport. Defined by the grid. |
60
63
  | `columnResizeMode` | `"onChange" \| "onEnd"` | `"onChange"` | Resize update strategy. |
61
64
  | `initialState` | `Partial<TableState>` | See below | Merged over the grid defaults. |
62
65
  | `meta` | `TMDataGridTableMeta` | `{}` | Grid configuration, see below. |
@@ -168,8 +171,15 @@ const filterPanelOpen = useSelector(grid.ui, (state) => state.filterPanelOpen);
168
171
  | `toggleColumnsPanel` | `() => void` |
169
172
  | `startColumnDrag` | `(columnId: string) => void` |
170
173
  | `endColumnDrag` | `() => void` |
174
+ | `setHighlightedRow` | `(rowId: string \| null) => void` |
175
+ | `setSelectionAnchor` | `(rowId: string \| null) => void` |
176
+ | `setFocusedCell` | `(cell: TMDataGridCellPosition \| null) => void` |
177
+ | `setCellRange` | `(range: TMDataGridCellRange \| null) => void` |
171
178
 
172
- The last two are called by the header cells while a column is being dragged, and
179
+ The last two move the cell cursor and the selected rectangle under
180
+ `cellSelection`; DOM focus follows `focusedCell` and scrolls its row into view.
181
+
182
+ `startColumnDrag` / `endColumnDrag` are called by the header cells while a column is being dragged, and
173
183
  `ui.draggedColumnId` holds the column being moved — browsers keep `dataTransfer`
174
184
  unreadable until the drop.
175
185
 
@@ -10,7 +10,7 @@ description: >
10
10
  metadata:
11
11
  type: core
12
12
  library: '@jielga/tmdatagrid'
13
- library_version: '0.3.0'
13
+ library_version: '0.5.0'
14
14
  sources:
15
15
  - 'Jielga/TMDataGrid:src/docs/server-side.md'
16
16
  - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
@@ -30,3 +30,29 @@ export function useTMDataGridContext(): TMDataGridContextValue {
30
30
  }
31
31
  return api as TMDataGridContextValue;
32
32
  }
33
+
34
+ /**
35
+ * What a control inside a *body* cell should put in its `tabIndex`.
36
+ *
37
+ * `-1` once cell selection is on, and this is what makes the promise of one tab
38
+ * stop true. Without it the browser walks Tab into the checkbox of every
39
+ * mounted row — a grid showing twenty rows would be twenty tab stops, and
40
+ * scrolling would change how many. Enter or F2 steps into the cell instead,
41
+ * which reaches a `-1` control perfectly well.
42
+ *
43
+ * Header controls are not covered: the header row is not part of cell
44
+ * navigation, so its sort buttons and menus stay in the tab order, where they
45
+ * are the only way to reach them.
46
+ *
47
+ * A custom cell renderer with a control in it wants the same:
48
+ *
49
+ * ```tsx
50
+ * cell: ({ row }) => (
51
+ * <Button tabIndex={useCellControlTabIndex()} onClick={...}>Open</Button>
52
+ * )
53
+ * ```
54
+ */
55
+ export function useCellControlTabIndex(): 0 | -1 {
56
+ const { features } = useTMDataGridContext();
57
+ return features.cellSelection ? -1 : 0;
58
+ }
@@ -60,6 +60,16 @@
60
60
  band of their own. */
61
61
  --dg-row-group-bg: var(--mantine-color-body);
62
62
 
63
+ /* The stacking ladder, stated once. Body cells sit at auto (0); everything
64
+ sticky stacks over them in this order, and a sticky row crossing a sticky
65
+ column takes the row's slot plus one. */
66
+ --dg-z-pinned-cell: 2;
67
+ --dg-z-focused-cell: 3;
68
+ --dg-z-summary-row: 4;
69
+ --dg-z-summary-pinned-cell: 5;
70
+ --dg-z-header: 6;
71
+ --dg-z-header-pinned-cell: 7;
72
+
63
73
  display: flex;
64
74
  flex-direction: column;
65
75
  min-height: 0;
@@ -6,10 +6,12 @@ import { TMDataGridContextProvider } from "../TMDataGridContext";
6
6
  import { TMDataGridFilterPanel } from "./TMDataGridFilterPanel";
7
7
  import { TMDataGridFilterPills } from "./TMDataGridFilterPills";
8
8
  import { TMDataGridFooter } from "./TMDataGridFooter";
9
+ import { TMDataGridSearch } from "./TMDataGridSearch";
9
10
  import { TMDataGridTable } from "./TMDataGridTable";
10
11
  import {
11
12
  TMDataGridColumnsButton,
12
13
  TMDataGridFilterButton,
14
+ TMDataGridLoadingIndicator,
13
15
  TMDataGridSummaryCount,
14
16
  TMDataGridToolbar,
15
17
  TMDataGridToolbarSpacer,
@@ -60,6 +62,10 @@ function TMDataGridRoot<TData extends RowData>({
60
62
  table,
61
63
  ui,
62
64
  features,
65
+ labels,
66
+ renderDetails,
67
+ renderDetailsEstHeight,
68
+ overscan,
63
69
  size = DEFAULT_TMDATAGRID_SIZE,
64
70
  children,
65
71
  className,
@@ -72,11 +78,25 @@ function TMDataGridRoot<TData extends RowData>({
72
78
  table,
73
79
  ui,
74
80
  features,
81
+ labels,
82
+ renderDetails,
83
+ renderDetailsEstHeight,
84
+ overscan,
75
85
  size,
76
86
  rowHeight,
77
87
  controlSize: SIZE_CONTROL_SIZE[size],
78
88
  }),
79
- [table, ui, features, size, rowHeight],
89
+ [
90
+ table,
91
+ ui,
92
+ features,
93
+ labels,
94
+ renderDetails,
95
+ renderDetailsEstHeight,
96
+ overscan,
97
+ size,
98
+ rowHeight,
99
+ ],
80
100
  );
81
101
 
82
102
  return (
@@ -96,6 +116,8 @@ export const TMDataGrid = Object.assign(TMDataGridRoot, {
96
116
  Toolbar: TMDataGridToolbar,
97
117
  Spacer: TMDataGridToolbarSpacer,
98
118
  SummaryCount: TMDataGridSummaryCount,
119
+ LoadingIndicator: TMDataGridLoadingIndicator,
120
+ Search: TMDataGridSearch,
99
121
  ColumnsButton: TMDataGridColumnsButton,
100
122
  FilterButton: TMDataGridFilterButton,
101
123
  Table: TMDataGridTable,
@@ -13,7 +13,7 @@ import { SearchIcon } from "./icons";
13
13
  * `TMDataGrid.ColumnsButton`.
14
14
  */
15
15
  export function TMDataGridColumnsPanel() {
16
- const { table, controlSize } = useTMDataGridContext();
16
+ const { table, labels, controlSize } = useTMDataGridContext();
17
17
  const [search, setSearch] = useState("");
18
18
 
19
19
  const columnVisibility = useSelector(
@@ -45,7 +45,7 @@ export function TMDataGridColumnsPanel() {
45
45
  <TextInput
46
46
  value={search}
47
47
  onChange={(event) => setSearch(event.currentTarget.value)}
48
- placeholder="Search"
48
+ placeholder={labels.columnsSearchPlaceholder}
49
49
  leftSection={<SearchIcon size={16} stroke={1.6} />}
50
50
  size={controlSize}
51
51
  data-autofocus
@@ -68,7 +68,7 @@ export function TMDataGridColumnsPanel() {
68
68
  ))}
69
69
  {visibleInPanel.length === 0 && (
70
70
  <Text size={controlSize} c="dimmed">
71
- No columns match “{search}”
71
+ {labels.columnsNoMatch(search)}
72
72
  </Text>
73
73
  )}
74
74
  </div>
@@ -77,7 +77,7 @@ export function TMDataGridColumnsPanel() {
77
77
  <div className={classes.columnsPanelFooter}>
78
78
  <Checkbox
79
79
  size={controlSize}
80
- label="Show/Hide All"
80
+ label={labels.columnsShowHideAll}
81
81
  checked={shownCount === hideableColumns.length}
82
82
  indeterminate={shownCount > 0 && shownCount < hideableColumns.length}
83
83
  onChange={(event) =>
@@ -89,7 +89,7 @@ export function TMDataGridColumnsPanel() {
89
89
  size="compact-sm"
90
90
  onClick={() => table.resetColumnVisibility()}
91
91
  >
92
- RESET
92
+ {labels.columnsReset}
93
93
  </Button>
94
94
  </div>
95
95
  </div>
@@ -0,0 +1,33 @@
1
+ /* The details lane is a fixed track holding one control, so the button fills it
2
+ and the whole cell is the hit target. */
3
+ .detailsToggle {
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ width: 100%;
8
+ height: 100%;
9
+ color: inherit;
10
+ }
11
+
12
+ .detailsToggle:hover .chevron {
13
+ color: var(--mantine-color-text);
14
+ }
15
+
16
+ .chevron {
17
+ display: flex;
18
+ align-items: center;
19
+ color: var(--mantine-color-dimmed);
20
+ transition: transform 120ms ease;
21
+ }
22
+
23
+ .chevron[data-expanded="true"] {
24
+ transform: rotate(90deg);
25
+ }
26
+
27
+ /* One icon rotated rather than two swapped: the turn is what reads as the
28
+ expand, and a swap would jump. */
29
+ @media (prefers-reduced-motion: reduce) {
30
+ .chevron {
31
+ transition: none;
32
+ }
33
+ }
@@ -0,0 +1,160 @@
1
+ import { UnstyledButton } from "@mantine/core";
2
+ import {
3
+ useCellControlTabIndex,
4
+ useTMDataGridContext,
5
+ } from "../TMDataGridContext";
6
+ import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
7
+ import { useSelector } from "@tanstack/react-store";
8
+ import classes from "./TMDataGridDetailsColumn.module.css";
9
+ import { ChevronRightIcon } from "./icons";
10
+ import { areAllRowsExpanded, resolveExpandAll } from "../core/expanding";
11
+ import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
12
+
13
+ export const DETAILS_COLUMN_ID = "__details__";
14
+
15
+ /**
16
+ * The chevron that opens a row's detail panel.
17
+ *
18
+ * Expansion is read through a subscription rather than from
19
+ * `row.getIsExpanded()` in the component body: the `row` identity survives an
20
+ * expand, so the React Compiler would cache the call along with it and the
21
+ * chevron would never turn. Same reason the select checkbox subscribes.
22
+ */
23
+ function DetailsCell<TData extends RowData>({
24
+ row,
25
+ }: {
26
+ row: Row<TMDataGridFeatures, TData>;
27
+ }) {
28
+ const { labels } = useTMDataGridContext();
29
+ const expanded = useSelector(row.table.store, () => row.getIsExpanded());
30
+ // See useCellControlTabIndex: a body control is reached by stepping into its
31
+ // cell, not by Tab, or the grid would have one tab stop per mounted row.
32
+ const tabIndex = useCellControlTabIndex();
33
+
34
+ // Group rows open into their children, not into a panel — see the body's
35
+ // `showsDetails`. Their cells are aggregated, so this lane is already blank
36
+ // on them; the guard is what makes that a decision rather than a side effect.
37
+ if (row.getIsGrouped()) return null;
38
+
39
+ return (
40
+ <UnstyledButton
41
+ className={classes.detailsToggle}
42
+ tabIndex={tabIndex}
43
+ aria-expanded={expanded}
44
+ aria-label={expanded ? labels.hideDetails : labels.showDetails}
45
+ // The row underneath may select or highlight on click; opening a panel is
46
+ // its own gesture and must not also trigger those.
47
+ onClick={(event) => {
48
+ event.stopPropagation();
49
+ row.toggleExpanded();
50
+ }}
51
+ >
52
+ <span className={classes.chevron} data-expanded={expanded}>
53
+ <ChevronRightIcon size={16} stroke={1.6} />
54
+ </span>
55
+ </UnstyledButton>
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Expand-all / collapse-all for the lane, the way the checkbox column's header
61
+ * selects and clears every row.
62
+ *
63
+ * Deliberately not `table.toggleAllRowsExpanded()`: that writes the `expanded`
64
+ * state's whole-table form, and one state holds both the tree and the panels —
65
+ * so it would unfold every group as well. Only the data rows are touched here;
66
+ * whatever the tree was showing, it goes on showing. See resolveExpandAll.
67
+ */
68
+ function DetailsHeader<TData extends RowData>({
69
+ table,
70
+ }: {
71
+ table: TMDataGridTable<TData>;
72
+ }) {
73
+ const { labels } = useTMDataGridContext();
74
+ // Pre-paginated: expand-all means every row the filters left, not the page
75
+ // that happens to be on screen. Same model `getCanSomeRowsExpand` reads.
76
+ const detailRows = () => table.getPrePaginatedRowModel().flatRows;
77
+
78
+ const allExpanded = useSelector(table.store, (state) =>
79
+ areAllRowsExpanded({
80
+ rows: detailRows(),
81
+ expanded: state.expanded,
82
+ target: "details",
83
+ }),
84
+ );
85
+
86
+ return (
87
+ <UnstyledButton
88
+ className={classes.detailsToggle}
89
+ aria-expanded={allExpanded}
90
+ aria-label={
91
+ allExpanded ? labels.collapseAllDetails : labels.expandAllDetails
92
+ }
93
+ // Partly expanded opens the rest rather than closing what is already
94
+ // open — the same reading as an indeterminate select-all box.
95
+ onClick={() =>
96
+ table.setExpanded(
97
+ resolveExpandAll({
98
+ rows: detailRows(),
99
+ expanded: table.store.state.expanded,
100
+ target: "details",
101
+ expand: !allExpanded,
102
+ }),
103
+ )
104
+ }
105
+ >
106
+ <span className={classes.chevron} data-expanded={allExpanded}>
107
+ <ChevronRightIcon size={16} stroke={1.6} />
108
+ </span>
109
+ </UnstyledButton>
110
+ );
111
+ }
112
+
113
+ /**
114
+ * The generated details lane, prepended whenever `renderDetails` is set.
115
+ *
116
+ * Structural, like the checkbox and tree columns: fixed width, pinned to the
117
+ * left after both of them, not hideable, not movable and not resizable. A
118
+ * toggle that wandered off to the right of the grid — or hid itself — would
119
+ * leave rows with panels no one can open.
120
+ *
121
+ * Last of the three because it acts on one record: the checkbox picks rows out
122
+ * and the tree says which group they are in, and only then is there a row to
123
+ * open.
124
+ *
125
+ * Nothing stops a second toggle elsewhere: `row.toggleExpanded()` is the whole
126
+ * interface, and this lane is only the one the grid ships.
127
+ */
128
+ export function createDetailsColumn<TData extends RowData>(
129
+ label = "Details",
130
+ ): ColumnDef<TMDataGridFeatures, TData, unknown> {
131
+ return {
132
+ id: DETAILS_COLUMN_ID,
133
+ meta: {
134
+ label,
135
+ align: "center",
136
+ // Structurally the last of the generated lanes.
137
+ enableOrdering: false,
138
+ },
139
+ // A system lane: as wide as the control it holds and no wider. Fixed at
140
+ // every scale — the control does not grow with the font size, so neither
141
+ // should its track.
142
+ size: 36,
143
+ minSize: 36,
144
+ maxSize: 36,
145
+ enableResizing: false,
146
+ enableSorting: false,
147
+ enableColumnFilter: false,
148
+ enableGlobalFilter: false,
149
+ // Its visibility is not the user's to set: hiding it would strand every
150
+ // panel behind a control that is no longer there.
151
+ enableHiding: false,
152
+ // Structurally pinned to the left; users shouldn't be able to move it.
153
+ enablePinning: false,
154
+ header: ({ table }) => <DetailsHeader table={table} />,
155
+ cell: ({ row }) => <DetailsCell row={row} />,
156
+ // Deliberately no `aggregatedCell`: on a group row every cell outside the
157
+ // grouped column counts as aggregated, and blank is the right answer here —
158
+ // groups expand into their rows, not into a panel.
159
+ };
160
+ }