@jielga/tmdatagrid 0.1.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 (47) hide show
  1. package/README.md +35 -2
  2. package/dist/index.d.ts +792 -327
  3. package/dist/index.js +1581 -892
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/package.json +15 -7
  7. package/skills/columns/SKILL.md +8 -7
  8. package/skills/features/SKILL.md +104 -16
  9. package/skills/getting-started/SKILL.md +78 -20
  10. package/skills/options/SKILL.md +12 -7
  11. package/skills/server-side/SKILL.md +5 -2
  12. package/src/tmdatagrid/TMDataGridContext.ts +1 -1
  13. package/src/tmdatagrid/components/TMDataGrid.module.css +73 -0
  14. package/src/tmdatagrid/{TMDataGrid.tsx → components/TMDataGrid.tsx} +15 -4
  15. package/src/tmdatagrid/components/TMDataGridColumnsPanel.module.css +25 -0
  16. package/src/tmdatagrid/{TMDataGridColumnsPanel.tsx → components/TMDataGridColumnsPanel.tsx} +10 -4
  17. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +35 -0
  18. package/src/tmdatagrid/{TMDataGridFilterPanel.tsx → components/TMDataGridFilterPanel.tsx} +91 -14
  19. package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
  20. package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +135 -0
  21. package/src/tmdatagrid/components/TMDataGridFooter.module.css +18 -0
  22. package/src/tmdatagrid/components/TMDataGridFooter.tsx +171 -0
  23. package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
  24. package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +147 -0
  25. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +159 -0
  26. package/src/tmdatagrid/{TMDataGridHeaderCell.tsx → components/TMDataGridHeaderCell.tsx} +99 -14
  27. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +177 -0
  28. package/src/tmdatagrid/components/TMDataGridTable.module.css +137 -0
  29. package/src/tmdatagrid/components/TMDataGridTable.tsx +720 -0
  30. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +12 -0
  31. package/src/tmdatagrid/{TMDataGridToolbar.tsx → components/TMDataGridToolbar.tsx} +8 -5
  32. package/src/tmdatagrid/{icons.ts → components/icons.ts} +5 -0
  33. package/src/tmdatagrid/components/sticky.module.css +45 -0
  34. package/src/tmdatagrid/core/capabilities.ts +215 -0
  35. package/src/tmdatagrid/{columnOrdering.ts → core/columnOrdering.ts} +2 -2
  36. package/src/tmdatagrid/{columnUtils.ts → core/columnUtils.ts} +1 -1
  37. package/src/tmdatagrid/{filterOperators.ts → core/filterOperators.ts} +25 -0
  38. package/src/tmdatagrid/core/grouping.ts +21 -0
  39. package/src/tmdatagrid/{persistence.ts → core/persistence.ts} +92 -2
  40. package/src/tmdatagrid/core/rowSelection.ts +202 -0
  41. package/src/tmdatagrid/index.ts +39 -9
  42. package/src/tmdatagrid/useTMDataGrid.tsx +318 -66
  43. package/src/tmdatagrid/TMDataGrid.module.css +0 -410
  44. package/src/tmdatagrid/TMDataGridFooter.tsx +0 -74
  45. package/src/tmdatagrid/TMDataGridTable.tsx +0 -297
  46. package/src/tmdatagrid/capabilities.ts +0 -117
  47. /package/src/tmdatagrid/{sizes.ts → core/sizes.ts} +0 -0
@@ -0,0 +1,202 @@
1
+ import type { Row, RowData, RowSelectionState } from "@tanstack/react-table";
2
+ import type { TMDataGridFeatureFlags } from "./capabilities";
3
+ import { getGroupDataRows } from "./grouping";
4
+ import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
5
+
6
+ /**
7
+ * Rows in the order they are displayed, which is the order a shift-click range
8
+ * is measured over — so a range follows the active sort and skips filtered-out
9
+ * rows. Under pagination it is the current page, so a range cannot cross a page
10
+ * boundary.
11
+ *
12
+ * Once a column is grouped this includes the group rows, and only the leaves of
13
+ * groups that are open — and paging is suspended, so it is the whole tree. That
14
+ * is the right list for a range: it is what the user can see, and a range that
15
+ * swept collapsed rows would select things off screen. What a group row in the
16
+ * range contributes is decided by {@link getSelectableRowIds}.
17
+ */
18
+ export function getDisplayedRows<TData extends RowData>(
19
+ table: TMDataGridTable<TData>,
20
+ features: TMDataGridFeatureFlags,
21
+ ): Array<Row<TMDataGridFeatures, TData>> {
22
+ return isPagingActive(table, features)
23
+ ? table.getPaginatedRowModel().rows
24
+ : table.getPrePaginatedRowModel().rows;
25
+ }
26
+
27
+ /**
28
+ * Whether the built-in pager is actually slicing anything.
29
+ *
30
+ * Grouping suspends it. A page can only count one kind of thing, and once the
31
+ * rows are a tree neither answer works: counting every row splits a group
32
+ * across a page boundary and strands the rest of the tree on pages the user has
33
+ * to go looking for, and counting only the top-level rows quietly redefines
34
+ * "rows per page" as groups per page, so a page of 25 can hold thousands of
35
+ * rows. Rather than pick, the grid renders the whole tree and leans on the
36
+ * virtualizer — which is its default mode anyway, pagination being opt-in.
37
+ *
38
+ * `TMDataGrid.Footer` reads this too, and greys the pager out rather than
39
+ * hiding it, so the pager going quiet is visible instead of mysterious.
40
+ */
41
+ export function isPagingActive<TData extends RowData>(
42
+ table: TMDataGridTable<TData>,
43
+ features: TMDataGridFeatureFlags,
44
+ ): boolean {
45
+ return features.pagination && table.store.state.grouping.length === 0;
46
+ }
47
+
48
+ /**
49
+ * Which modifiers were held. Named for what they mean rather than for the keys,
50
+ * because the keys differ per platform — `toggle` is Ctrl on Windows/Linux and
51
+ * Cmd on macOS.
52
+ */
53
+ export type TMDataGridRowClickModifiers = {
54
+ toggle: boolean;
55
+ extend: boolean;
56
+ };
57
+
58
+ export type ResolveRowSelectionClickArgs<TData extends RowData> = {
59
+ /** Displayed order — see {@link getDisplayedRows}. */
60
+ rows: ReadonlyArray<Row<TMDataGridFeatures, TData>>;
61
+ rowId: string;
62
+ /** The pivot a shift-click extends from. */
63
+ anchorRowId: string | null;
64
+ modifiers: TMDataGridRowClickModifiers;
65
+ selection: RowSelectionState;
66
+ /**
67
+ * Whether this gesture is allowed to clear rows it did not touch.
68
+ *
69
+ * `true` for a bare row click, where replacing is the whole point. `false` for
70
+ * a checkbox, which is only ever additive — ticking one box has never cleared
71
+ * the others, and shift-clicking one adds the range rather than becoming it.
72
+ */
73
+ canReplaceSelection: boolean;
74
+ };
75
+
76
+ export type ResolvedRowSelection = {
77
+ selection: RowSelectionState;
78
+ anchorRowId: string | null;
79
+ };
80
+
81
+ /**
82
+ * The row ids one gesture on this row selects.
83
+ *
84
+ * A group row is never selected itself: it holds no record, and TanStack
85
+ * derives its checked state from its descendants. So ticking a group means
86
+ * ticking every selectable leaf under it, which is also what makes the group's
87
+ * own box light up.
88
+ *
89
+ * Leaves rather than direct children, so one click on a top-level group takes
90
+ * the whole subtree however deeply it nests.
91
+ */
92
+ export function getSelectableRowIds<TData extends RowData>(
93
+ row: Row<TMDataGridFeatures, TData>,
94
+ ): Array<string> {
95
+ return getGroupDataRows(row)
96
+ .filter((dataRow) => dataRow.getCanSelect())
97
+ .map((dataRow) => dataRow.id);
98
+ }
99
+
100
+ /**
101
+ * Turns one click into the next selection, following the convention every
102
+ * desktop list uses:
103
+ *
104
+ * | Gesture | Effect |
105
+ * | ------- | ------ |
106
+ * | Click | selection becomes only this row |
107
+ * | Ctrl/Cmd + click | toggle this row, leave the rest |
108
+ * | Shift + click | selection becomes the range anchor → this row |
109
+ * | Ctrl/Cmd + Shift + click | add that range to the selection |
110
+ *
111
+ * The anchor is a *pivot*, not "the last row touched": only a plain or
112
+ * Ctrl-click moves it, which is what lets a run of shift-clicks grow and shrink
113
+ * the same range from a fixed point.
114
+ *
115
+ * Selection state is rebuilt here and handed to `setRowSelection` wholesale
116
+ * rather than going through `row.toggleSelected()` per row, which would publish
117
+ * one state update each. That skips TanStack's sub-row cascade, so the cascade
118
+ * is done here instead: every row in the gesture is expanded through
119
+ * {@link getSelectableRowIds} before it reaches the map. Under grouping that is
120
+ * what makes one tick on a group select all of it.
121
+ */
122
+ export function resolveRowSelectionClick<TData extends RowData>({
123
+ rows,
124
+ rowId,
125
+ anchorRowId,
126
+ modifiers,
127
+ selection,
128
+ canReplaceSelection,
129
+ }: ResolveRowSelectionClickArgs<TData>): ResolvedRowSelection {
130
+ const index = rows.findIndex((row) => row.id === rowId);
131
+ const clickedRow = rows[index];
132
+ // Clicked row is not in the displayed set — nothing sensible to resolve.
133
+ if (index === -1 || clickedRow === undefined) return { selection, anchorRowId };
134
+
135
+ const targets = getSelectableRowIds(clickedRow);
136
+ // Nothing selectable under this gesture: a row the predicate refuses, or a
137
+ // group whose every leaf refuses.
138
+ if (targets.length === 0) return { selection, anchorRowId };
139
+
140
+ // `enableMultiRowSelection: false`, or the per-row predicate saying no: one
141
+ // row at a time, so the modifiers have nothing to build on and every gesture
142
+ // collapses to a single id.
143
+ //
144
+ // This has to be enforced here. Rebuilding the map and handing it to
145
+ // `setRowSelection` skips `mutateRowIsSelected`, which is where TanStack
146
+ // clears the other rows — so without this, Ctrl and Shift would quietly
147
+ // produce the multi-selection the option ruled out.
148
+ if (!clickedRow.getCanMultiSelect()) {
149
+ // A group stands for several rows, which single-select cannot express. The
150
+ // checkbox column drops the box on those rows for the same reason.
151
+ const only = targets.length === 1 ? targets[0] : undefined;
152
+ if (only === undefined) return { selection, anchorRowId };
153
+ // A checkbox must still be able to untick the one selected row; a row click
154
+ // only ever sets, so re-clicking it cannot empty the selection.
155
+ const untick = !canReplaceSelection && selection[only] === true;
156
+ return {
157
+ selection: untick ? {} : { [only]: true },
158
+ anchorRowId: rowId,
159
+ };
160
+ }
161
+
162
+ const anchorIndex =
163
+ anchorRowId === null
164
+ ? -1
165
+ : rows.findIndex((row) => row.id === anchorRowId);
166
+
167
+ // A range needs a pivot that is still on screen. When the anchor has been
168
+ // filtered or paged away, fall through and treat this as a fresh click.
169
+ if (modifiers.extend && anchorIndex !== -1) {
170
+ const from = Math.min(anchorIndex, index);
171
+ const to = Math.max(anchorIndex, index);
172
+ // Ctrl+Shift adds to what is already selected; Shift alone becomes the
173
+ // range — unless the gesture may not replace, in which case it also adds.
174
+ const next: RowSelectionState =
175
+ modifiers.toggle || !canReplaceSelection ? { ...selection } : {};
176
+ for (const row of rows.slice(from, to + 1)) {
177
+ // A group row inside the range brings its whole subtree, including leaves
178
+ // that are collapsed out of `rows` — sweeping past a closed group selects
179
+ // it entire, which is the only reading that matches what it displays.
180
+ for (const id of getSelectableRowIds(row)) next[id] = true;
181
+ }
182
+ // The pivot deliberately stays put.
183
+ return { selection: next, anchorRowId };
184
+ }
185
+
186
+ if (modifiers.toggle || !canReplaceSelection) {
187
+ const next: RowSelectionState = { ...selection };
188
+ // Asked of the targets rather than of `rowId`: a group row carries no id in
189
+ // the map, so it counts as selected exactly when all of its leaves do —
190
+ // which is also when its checkbox shows a tick rather than a dash.
191
+ const allSelected = targets.every((id) => selection[id] === true);
192
+ for (const id of targets) {
193
+ if (allSelected) delete next[id];
194
+ else next[id] = true;
195
+ }
196
+ return { selection: next, anchorRowId: rowId };
197
+ }
198
+
199
+ const next: RowSelectionState = {};
200
+ for (const id of targets) next[id] = true;
201
+ return { selection: next, anchorRowId: rowId };
202
+ }
@@ -1,13 +1,17 @@
1
- export { TMDataGrid, type TMDataGridProps } from "./TMDataGrid";
1
+ export { TMDataGrid, type TMDataGridProps } from "./components/TMDataGrid";
2
2
  export {
3
3
  useTMDataGridContext,
4
4
  type TMDataGridContextValue,
5
5
  type TMDataGridRowData,
6
6
  } from "./TMDataGridContext";
7
+ export { SELECT_COLUMN_ID } from "./components/TMDataGridSelectColumn";
8
+ export {
9
+ formatGroupValue,
10
+ GROUP_COLUMN_ID,
11
+ } from "./components/TMDataGridGroupColumn";
7
12
  export {
8
13
  createTMDataGridColumnHelper,
9
14
  openColumnFilter,
10
- SELECT_COLUMN_ID,
11
15
  tmDataGridFeatures,
12
16
  type TMDataGridApi,
13
17
  type TMDataGridColumnMeta,
@@ -20,18 +24,24 @@ export {
20
24
  useTMDataGrid,
21
25
  type UseTMDataGridOptions,
22
26
  } from "./useTMDataGrid";
27
+ export {
28
+ TMDataGridFilterPills,
29
+ type TMDataGridFilterPillsProps,
30
+ } from "./components/TMDataGridFilterPills";
23
31
  export {
24
32
  type TMDataGridColumnType,
25
33
  type TMDataGridFilterOperator,
26
34
  type TMDataGridFilterValue,
27
35
  FILTER_OPERATOR_LABELS,
36
+ formatFilterLabel,
28
37
  isFilterActive,
29
- } from "./filterOperators";
38
+ } from "./core/filterOperators";
30
39
  export {
31
40
  getColumnLabel,
32
41
  getColumnType,
33
42
  isColumnReorderable,
34
- } from "./columnUtils";
43
+ } from "./core/columnUtils";
44
+ export { getGroupDataRows } from "./core/grouping";
35
45
  export {
36
46
  getColumnRegion,
37
47
  getStepTargetColumn,
@@ -41,7 +51,7 @@ export {
41
51
  type MoveColumnArgs,
42
52
  type TMDataGridColumnRegion,
43
53
  type TMDataGridDropSide,
44
- } from "./columnOrdering";
54
+ } from "./core/columnOrdering";
45
55
  export {
46
56
  getColumnCapabilities,
47
57
  getGridCapabilities,
@@ -49,13 +59,23 @@ export {
49
59
  type TMDataGridCapabilities,
50
60
  type TMDataGridColumnCapabilities,
51
61
  type TMDataGridFeatureFlags,
52
- } from "./capabilities";
62
+ type TMDataGridSelectionMode,
63
+ } from "./core/capabilities";
64
+ export {
65
+ getDisplayedRows,
66
+ getSelectableRowIds,
67
+ isPagingActive,
68
+ resolveRowSelectionClick,
69
+ type ResolvedRowSelection,
70
+ type ResolveRowSelectionClickArgs,
71
+ type TMDataGridRowClickModifiers,
72
+ } from "./core/rowSelection";
53
73
  export {
54
74
  DEFAULT_TMDATAGRID_SIZE,
55
75
  SIZE_CONTROL_SIZE,
56
76
  SIZE_ROW_HEIGHT,
57
77
  type TMDataGridSize,
58
- } from "./sizes";
78
+ } from "./core/sizes";
59
79
  export {
60
80
  DATA_STATE_SLICES,
61
81
  SETTINGS_STATE_SLICES,
@@ -64,5 +84,15 @@ export {
64
84
  type TMDataGridPersistKey,
65
85
  type TMDataGridSettingsSlice,
66
86
  type TMDataGridStorageMode,
67
- } from "./persistence";
68
- export type { TMDataGridColumnLayout } from "./TMDataGridTable";
87
+ } from "./core/persistence";
88
+ export type {
89
+ TMDataGridColumnLayout,
90
+ TMDataGridRowContextMenu,
91
+ TMDataGridRowContextMenuArgs,
92
+ TMDataGridTableProps,
93
+ } from "./components/TMDataGridTable";
94
+ export {
95
+ getTMDataGridPaginationApi,
96
+ type TMDataGridFooterProps,
97
+ type TMDataGridPaginationApi,
98
+ } from "./components/TMDataGridFooter";