@opencorestack/opengridx 2.1.0 → 3.0.1

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 (225) hide show
  1. package/CHANGELOG.md +399 -0
  2. package/README.md +95 -72
  3. package/dist/index.d.ts +590 -131
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +7891 -5631
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +17 -23
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +348 -166
  10. package/docs/README.md +6 -1
  11. package/docs/architecture/datagrid-orchestration.md +81 -0
  12. package/docs/architecture/grid-row-meta.md +102 -0
  13. package/docs/architecture/use-grid-columns.md +128 -0
  14. package/docs/architecture/use-grid-controlled-state.md +157 -0
  15. package/docs/architecture/use-grid-row-pipeline.md +120 -0
  16. package/docs/architecture/use-grid-scroll-sync.md +111 -0
  17. package/docs/architecture/use-grid-state-snapshot.md +90 -0
  18. package/docs/architecture/use-grid-virtualization.md +115 -0
  19. package/docs/architecture/use-grid-visible-rows.md +85 -0
  20. package/docs/assets/banner-alt.png +0 -0
  21. package/docs/assets/banner.png +0 -0
  22. package/docs/assets/hero.png +0 -0
  23. package/docs/assets/logo.png +0 -0
  24. package/docs/components/aggregation-footer.md +15 -10
  25. package/docs/components/cell.md +16 -6
  26. package/docs/components/column-group-header.md +55 -28
  27. package/docs/components/column-resize.md +17 -13
  28. package/docs/components/column-visibility.md +6 -7
  29. package/docs/components/datagrid.md +15 -13
  30. package/docs/components/empty-state.md +2 -1
  31. package/docs/components/error-overlay.md +12 -7
  32. package/docs/components/filter-panel.md +28 -8
  33. package/docs/components/header.md +28 -9
  34. package/docs/components/pagination.md +14 -8
  35. package/docs/components/row.md +21 -5
  36. package/docs/components/toolbar.md +27 -9
  37. package/docs/components/tooltip.md +16 -7
  38. package/docs/customization/slots-api.md +90 -38
  39. package/docs/customization/theming.md +66 -18
  40. package/docs/features/aggregation-pivot.md +75 -9
  41. package/docs/features/cell-spanning.md +80 -125
  42. package/docs/features/clipboard.md +41 -16
  43. package/docs/features/custom-pagination.md +38 -18
  44. package/docs/features/data-source.md +36 -13
  45. package/docs/features/editing-reordering.md +125 -16
  46. package/docs/features/export-guide.md +126 -39
  47. package/docs/features/filtering.md +44 -6
  48. package/docs/features/infinite-scroll.md +54 -46
  49. package/docs/features/keyboard-navigation.md +83 -0
  50. package/docs/features/list-view.md +58 -5
  51. package/docs/features/loading-states.md +18 -20
  52. package/docs/features/master-detail.md +18 -5
  53. package/docs/features/pdf-export.md +72 -19
  54. package/docs/features/pinning.md +24 -5
  55. package/docs/features/selection.md +50 -10
  56. package/docs/features/sorting-pagination.md +64 -14
  57. package/docs/features/state-persistence.md +78 -10
  58. package/docs/features/toolbar-customization.md +26 -17
  59. package/docs/features/tree-data-grouping.md +36 -8
  60. package/docs/features/virtualization.md +37 -14
  61. package/docs/migration/v1-to-v2.md +115 -0
  62. package/docs/migration/v2-to-v3.md +1233 -0
  63. package/docs/performance.md +124 -0
  64. package/docs/roadmap.md +5 -5
  65. package/lib/components/Cell/Cell.css +17 -1
  66. package/lib/components/Cell/Cell.tsx +129 -57
  67. package/lib/components/Cell/CellErrorBoundary.tsx +12 -6
  68. package/lib/components/Cell/GridEditInputCell.tsx +114 -41
  69. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +7 -1
  70. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +15 -2
  71. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +104 -38
  72. package/lib/components/ColumnResizeHandle/clampResizeWidth.ts +13 -0
  73. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +6 -0
  74. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +21 -13
  75. package/lib/components/DataGrid/DataGrid.css +85 -12
  76. package/lib/components/DataGrid/DataGrid.tsx +582 -876
  77. package/lib/components/DataGrid/GridAggregationFooter.tsx +53 -17
  78. package/lib/components/DataGrid/GridErrorOverlay.tsx +15 -12
  79. package/lib/components/DataGrid/GridListView.tsx +165 -24
  80. package/lib/components/DataGrid/GridLiveRegion.tsx +31 -0
  81. package/lib/components/DataGrid/GridLoadingOverlay.tsx +27 -0
  82. package/lib/components/DataGrid/GridPaginationArea.tsx +44 -0
  83. package/lib/components/DataGrid/GridPinnedRows.tsx +62 -11
  84. package/lib/components/DataGrid/GridStandaloneColumnPanel.tsx +40 -38
  85. package/lib/components/DataGrid/GridToolbarSlot.tsx +51 -0
  86. package/lib/components/DataGrid/GridVirtualRows.tsx +40 -24
  87. package/lib/components/DetailPanel/DetailPanel.css +0 -6
  88. package/lib/components/DetailPanel/DetailPanel.tsx +34 -5
  89. package/lib/components/DetailPanel/DetailPanelContent.tsx +17 -0
  90. package/lib/components/FilterPanel/FilterPanel.css +13 -2
  91. package/lib/components/FilterPanel/FilterPanel.tsx +272 -99
  92. package/lib/components/Header/ColumnMenu.tsx +91 -39
  93. package/lib/components/Header/Header.css +6 -3
  94. package/lib/components/Header/Header.tsx +181 -160
  95. package/lib/components/ListView/ListView.css +25 -0
  96. package/lib/components/ListView/ListViewRow.tsx +76 -10
  97. package/lib/components/Pagination/Pagination.tsx +15 -7
  98. package/lib/components/Row/Row.tsx +177 -77
  99. package/lib/components/Toolbar/GlobalSearch.css +6 -3
  100. package/lib/components/Toolbar/GlobalSearch.tsx +26 -27
  101. package/lib/components/Toolbar/GridToolbar.css +12 -11
  102. package/lib/components/Toolbar/GridToolbar.tsx +133 -89
  103. package/lib/components/Toolbar/PivotPanel.tsx +89 -49
  104. package/lib/components/Tooltip/Tooltip.css +20 -0
  105. package/lib/components/Tooltip/Tooltip.tsx +68 -35
  106. package/lib/components/ui/Button.tsx +3 -0
  107. package/lib/components/ui/Checkbox.tsx +2 -1
  108. package/lib/components/ui/ExpandIcon.css +0 -9
  109. package/lib/components/ui/ExpandIcon.tsx +4 -1
  110. package/lib/hooks/core/gridApiPlaceholder.ts +34 -0
  111. package/lib/hooks/core/gridToolbarHostContext.ts +14 -0
  112. package/lib/hooks/core/useDataGrid.ts +115 -173
  113. package/lib/hooks/core/useGridAggregationApi.ts +72 -0
  114. package/lib/hooks/core/useGridApiMethods.ts +115 -0
  115. package/lib/hooks/core/useGridApiRef.ts +19 -4
  116. package/lib/hooks/core/useGridApiRefBinding.ts +18 -0
  117. package/lib/hooks/core/useGridAriaRows.ts +38 -0
  118. package/lib/hooks/core/useGridClipboardApi.ts +38 -0
  119. package/lib/hooks/core/useGridColumnLookup.ts +30 -0
  120. package/lib/hooks/core/useGridColumns.tsx +279 -76
  121. package/lib/hooks/core/useGridColumnsPanel.ts +77 -0
  122. package/lib/hooks/core/useGridControlledState.ts +99 -6
  123. package/lib/hooks/core/useGridDetailPanel.ts +51 -0
  124. package/lib/hooks/core/useGridDevWarnings.ts +104 -5
  125. package/lib/hooks/core/useGridFocusHandlers.ts +50 -0
  126. package/lib/hooks/core/useGridGroupingColumn.ts +62 -0
  127. package/lib/hooks/core/useGridHeaderHandlers.ts +92 -0
  128. package/lib/hooks/core/useGridHierarchy.ts +81 -0
  129. package/lib/hooks/core/useGridKeyboardNavigation.ts +447 -243
  130. package/lib/hooks/core/useGridLiveRowSelection.ts +67 -0
  131. package/lib/hooks/core/useGridPageCorrection.ts +55 -0
  132. package/lib/hooks/core/useGridRowIdOf.ts +32 -0
  133. package/lib/hooks/core/useGridRowInteractions.ts +81 -0
  134. package/lib/hooks/core/useGridRowPipeline.ts +69 -39
  135. package/lib/hooks/core/useGridRowSelection.ts +91 -0
  136. package/lib/hooks/core/useGridScrollSync.ts +80 -32
  137. package/lib/hooks/core/useGridScrollToIndexesApi.ts +45 -0
  138. package/lib/hooks/core/useGridStateSnapshot.ts +40 -5
  139. package/lib/hooks/core/useGridThemeDimensions.ts +49 -0
  140. package/lib/hooks/core/useGridToolbarProps.ts +97 -0
  141. package/lib/hooks/core/useGridViewport.ts +38 -0
  142. package/lib/hooks/core/useGridViewportSize.ts +26 -0
  143. package/lib/hooks/core/useGridVisibleRows.ts +17 -5
  144. package/lib/hooks/core/useLayout.ts +215 -195
  145. package/lib/hooks/core/useRowsScrollEndLatch.ts +41 -0
  146. package/lib/hooks/core/useStableColumns.ts +34 -0
  147. package/lib/hooks/features/useAggregation.ts +82 -51
  148. package/lib/hooks/features/useColumnGroupReorderGuard.ts +34 -0
  149. package/lib/hooks/features/useDetailPanelHeights.ts +29 -0
  150. package/lib/hooks/features/useGridClipboard.ts +133 -73
  151. package/lib/hooks/features/useGridDataSource.ts +354 -128
  152. package/lib/hooks/features/useGridEditing.ts +211 -51
  153. package/lib/hooks/features/useGridPivot.ts +92 -0
  154. package/lib/hooks/features/useGridSpanRenderWindow.ts +104 -0
  155. package/lib/hooks/features/useGridSpanning.ts +410 -306
  156. package/lib/hooks/features/usePivot.ts +0 -0
  157. package/lib/hooks/features/useServerTreeChildren.ts +50 -0
  158. package/lib/hooks/useColumnReorder.ts +38 -44
  159. package/lib/hooks/useRowGrouping.ts +298 -160
  160. package/lib/hooks/useRowReorder.ts +62 -31
  161. package/lib/hooks/useTreeData.ts +287 -220
  162. package/lib/index.ts +28 -2
  163. package/lib/state/index.ts +1 -0
  164. package/lib/state/types.ts +7 -1
  165. package/lib/state/useGridStateStorage.ts +88 -29
  166. package/lib/theme/DataGridThemeProvider.tsx +143 -38
  167. package/lib/theme/gridThemeContext.ts +5 -0
  168. package/lib/theme/index.ts +5 -0
  169. package/lib/theme/presets.ts +5 -4
  170. package/lib/theme/types.ts +40 -2
  171. package/lib/types/index.ts +313 -116
  172. package/lib/utils/aggregation/index.ts +119 -24
  173. package/lib/utils/aria/index.ts +40 -0
  174. package/lib/utils/columnGroups.ts +110 -0
  175. package/lib/utils/columnLookup.ts +33 -0
  176. package/lib/utils/columnWidth.ts +22 -0
  177. package/lib/utils/dataSource/index.ts +46 -0
  178. package/lib/utils/detailPanel.ts +66 -0
  179. package/lib/utils/dragData.ts +44 -0
  180. package/lib/utils/editing/index.ts +30 -0
  181. package/lib/utils/export/exportShared.ts +197 -0
  182. package/lib/utils/export/exportToExcelAdvanced.ts +278 -139
  183. package/lib/utils/export/exportToPdf.ts +143 -92
  184. package/lib/utils/export/groupLabel.ts +14 -0
  185. package/lib/utils/export/index.ts +278 -272
  186. package/lib/utils/export/pdfText.ts +50 -0
  187. package/lib/utils/filtering/index.ts +277 -148
  188. package/lib/utils/focus/index.ts +115 -0
  189. package/lib/utils/gridApiRows.ts +35 -0
  190. package/lib/utils/gridModes.ts +77 -0
  191. package/lib/utils/grouping/groupedExportRows.ts +91 -0
  192. package/lib/utils/pagination/index.ts +56 -0
  193. package/lib/utils/pinning/index.ts +77 -33
  194. package/lib/utils/pivot/index.ts +368 -0
  195. package/lib/utils/scroll/index.ts +55 -5
  196. package/lib/utils/scroll/scrollEnd.ts +18 -0
  197. package/lib/utils/sorting/index.ts +154 -30
  198. package/lib/utils/syntheticRows.ts +12 -0
  199. package/lib/utils/values.ts +206 -0
  200. package/lib/utils/viewport.ts +14 -0
  201. package/llms.txt +261 -164
  202. package/package.json +13 -5
  203. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +0 -220
  204. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.test.tsx +0 -85
  205. package/lib/components/DataGrid/DataGrid.defects.test.tsx +0 -103
  206. package/lib/components/DataGrid/DataGrid.regression.test.tsx +0 -132
  207. package/lib/components/DataGrid/DataGrid.smoke.test.tsx +0 -58
  208. package/lib/components/DataGrid/DataGrid.v2.test.tsx +0 -127
  209. package/lib/components/DataGrid/DataGrid.virtualization.browser.test.tsx +0 -183
  210. package/lib/components/Header/Header.test.tsx +0 -135
  211. package/lib/components/Pagination/Pagination.test.tsx +0 -64
  212. package/lib/hooks/core/useDataGrid.regression.test.ts +0 -66
  213. package/lib/hooks/core/useGridControlledState.test.ts +0 -86
  214. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +0 -176
  215. package/lib/hooks/core/useGridRowPipeline.test.ts +0 -117
  216. package/lib/hooks/features/useAggregation.test.ts +0 -175
  217. package/lib/hooks/features/usePivot.test.ts +0 -202
  218. package/lib/hooks/useRowGrouping.test.ts +0 -298
  219. package/lib/hooks/useTreeData.test.ts +0 -83
  220. package/lib/utils/aggregation/aggregation.test.ts +0 -16
  221. package/lib/utils/export/exportToExcelAdvanced.test.ts +0 -109
  222. package/lib/utils/export/exportToPdf.test.ts +0 -120
  223. package/lib/utils/export/groupedExport.test.ts +0 -144
  224. package/lib/utils/filtering/filtering.test.ts +0 -178
  225. package/lib/utils/sorting/sorting.test.ts +0 -88
@@ -0,0 +1,197 @@
1
+ import type { GridAggregationModel, GridColDef, GridGroupedExportRow, GridRowId, GridRowModel } from '../../types';
2
+ import { computeAggregations, formatAggregateForColumn } from '../aggregation';
3
+ import { formatValueByType } from '../values';
4
+ import { isSystemField } from '../focus';
5
+
6
+ /**
7
+ * Helpers shared by every exporter (CSV, HTML .xls, JSON, print, PDF and advanced XLSX), so the
8
+ * formats agree on which columns and rows are exported and how values and aggregates are written.
9
+ */
10
+
11
+ type SelectedRowIds = readonly (string | number)[] | undefined;
12
+
13
+ /**
14
+ * Fields the grid generates itself and never exports: the checkbox, detail-panel and drag-handle
15
+ * columns, and the row-grouping `__group__` column (grouped exports write group labels from
16
+ * `groupedRows` instead). `__check__` / `__actions__` are older names kept for compatibility.
17
+ */
18
+ const NON_EXPORTED_FIELDS = new Set(['__group__', '__check__', '__actions__']);
19
+
20
+ /** Columns that appear in an export: not `exportable: false`, not a spacer, not a system column. */
21
+ export function getExportColumns<R extends GridRowModel>(columns: GridColDef<R>[]): GridColDef<R>[] {
22
+ return columns.filter(col =>
23
+ col.exportable !== false &&
24
+ !col.isSpacer &&
25
+ !isSystemField(col.field) &&
26
+ !NON_EXPORTED_FIELDS.has(col.field)
27
+ );
28
+ }
29
+
30
+ export function hasSelection(selectedRows: SelectedRowIds): selectedRows is readonly (string | number)[] {
31
+ return !!selectedRows && selectedRows.length > 0;
32
+ }
33
+
34
+ /**
35
+ * The rows whose id is in `selectedRows`, in row order. Linear time: the ids go into a Set once,
36
+ * so "select all" on a large grid does not scan the selection for every row. `getRowId` must be
37
+ * the grid's getRowId when it has one: the grid keys rows with it and never writes it to `row.id`.
38
+ */
39
+ export function pickSelectedRows<R extends GridRowModel>(
40
+ rows: R[],
41
+ selectedRows: readonly (string | number)[],
42
+ getRowId: (row: R) => GridRowId = (row) => row.id,
43
+ ): R[] {
44
+ const selected = new Set<unknown>(selectedRows);
45
+ return rows.filter(row => selected.has(getRowId(row)));
46
+ }
47
+
48
+ /** `selectedRows` when a selection is given, else every row. */
49
+ export function rowsForExport<R extends GridRowModel>(rows: R[], selectedRows: SelectedRowIds, getRowId?: (row: R) => GridRowId): R[] {
50
+ return hasSelection(selectedRows) ? pickSelectedRows(rows, selectedRows, getRowId) : rows;
51
+ }
52
+
53
+ /**
54
+ * Whether an exporter writes the grouped layout. A non-empty `selectedRows` takes precedence over
55
+ * `groupedRows`: the selected rows are exported flat, as `exportToExcelAdvanced` does for
56
+ * `rows: 'selected'` sheets.
57
+ */
58
+ export function shouldExportGrouped(groupedRows: GridGroupedExportRow[] | undefined, selectedRows: SelectedRowIds): groupedRows is GridGroupedExportRow[] {
59
+ return !!groupedRows && groupedRows.length > 0 && !hasSelection(selectedRows);
60
+ }
61
+
62
+ /**
63
+ * The totals to write under the exported rows. The caller's `aggregationResult` covers every row it
64
+ * was computed from; when a selection narrowed the export, the totals are recomputed over the
65
+ * exported rows with the grid's own aggregation functions so they match what is in the file.
66
+ */
67
+ export function aggregationForExport<R extends GridRowModel>(
68
+ exportedRows: R[],
69
+ selectionApplied: boolean,
70
+ columns: GridColDef<R>[],
71
+ aggregationResult: Record<string, unknown> | null | undefined,
72
+ aggregationModel: GridAggregationModel | null | undefined,
73
+ ): Record<string, unknown> | null {
74
+ if (!aggregationResult) return null;
75
+ if (!selectionApplied || !aggregationModel || Object.keys(aggregationModel).length === 0) return aggregationResult;
76
+ const lookup = new Map<string, GridColDef<R>>(columns.map(col => [col.field, col]));
77
+ return computeAggregations(exportedRows, aggregationModel, lookup, 'export');
78
+ }
79
+
80
+ /** The cell's raw value: `row[field]`, then the column's `valueGetter`. */
81
+ export function getRawExportValue<R extends GridRowModel>(row: R, col: GridColDef<R>): unknown {
82
+ const value: unknown = (row as GridRowModel)[col.field];
83
+ return col.valueGetter ? col.valueGetter({ row, field: col.field, value }) : value;
84
+ }
85
+
86
+ /**
87
+ * The cell's display text, matching the grid (`Cell.tsx`): `valueFormatter` runs whenever it is set,
88
+ * including for null/undefined, so placeholder text such as "Unassigned" is exported. A formatter
89
+ * that throws for a missing value exports an empty cell, as before; any other throw propagates.
90
+ * Without a formatter the column type decides, as in the grid (`formatValueByType`): a locale date,
91
+ * Yes / No, or the singleSelect option label.
92
+ */
93
+ export function formatExportValue<R extends GridRowModel>(row: R, col: GridColDef<R>, value: unknown): string {
94
+ if (!col.valueFormatter) return formatValueByType(value, col);
95
+ if (value == null) {
96
+ try {
97
+ return String(col.valueFormatter({ value, row, field: col.field }) ?? '');
98
+ } catch {
99
+ return '';
100
+ }
101
+ }
102
+ return String(col.valueFormatter({ value, row, field: col.field }) ?? '');
103
+ }
104
+
105
+ export { isCountAggregation, normalizeAggregateValue } from '../aggregation';
106
+
107
+ /**
108
+ * Text for an aggregate cell, shared by the text exporters: the grid's own aggregate formatting
109
+ * (`formatAggregateForColumn`, also used by the footer, group rows and pivot cells), with an empty cell
110
+ * for a missing value.
111
+ * - `count` / `unique` are plain counts; the column's `valueFormatter` is not applied (a count of a
112
+ * currency column is not an amount).
113
+ * - `sum`, `avg`, `min`, `max` go through the column's `valueFormatter`, whose `row` is the record
114
+ * of aggregated values (as in the grid's group rows). A formatter that throws falls back to the
115
+ * footer formatting instead of aborting the export.
116
+ */
117
+ export function formatExportAggregate<R extends GridRowModel>(
118
+ col: GridColDef<R>,
119
+ fnName: string | undefined,
120
+ value: unknown,
121
+ aggregates: Record<string, unknown>,
122
+ ): string {
123
+ if (value == null) return '';
124
+ return formatAggregateForColumn(value, fnName ?? '', col, aggregates);
125
+ }
126
+
127
+ /**
128
+ * Text for the first cell of a subtotal / total row. The label is written there, and when the first
129
+ * column carries its own aggregate the value is kept next to it instead of being overwritten.
130
+ */
131
+ export function summaryLabelText(label: string, firstColumnValue: string): string {
132
+ return firstColumnValue ? `${label}: ${firstColumnValue}` : label;
133
+ }
134
+
135
+ /** Only the aggregates of exported columns, so `exportable: false` fields do not leak. */
136
+ export function pickExportedAggregates<R extends GridRowModel>(
137
+ aggregates: Record<string, unknown>,
138
+ exportColumns: GridColDef<R>[],
139
+ ): Record<string, unknown> {
140
+ const picked: Record<string, unknown> = {};
141
+ exportColumns.forEach(col => {
142
+ if (Object.prototype.hasOwnProperty.call(aggregates, col.field)) picked[col.field] = aggregates[col.field];
143
+ });
144
+ return picked;
145
+ }
146
+
147
+ const FORMULA_TRIGGER = /^[=+\-@\t\r]/;
148
+ // Optional sign, then only digits, separators and whitespace (\s includes NBSP and U+202F):
149
+ // such text cannot reference cells or call functions.
150
+ const PLAIN_NUMBER = /^[-+]?[\d.,\s]*\d[\d.,\s]*%?$/;
151
+
152
+ /**
153
+ * Neutralise spreadsheet formula injection (CSV injection). Text that starts with `=`, `+`, `-`,
154
+ * `@`, tab or CR is prefixed with `'` so spreadsheet apps treat it as text. Plain signed numbers
155
+ * ("-12.50") and text formatted from a number, boolean or Date (`fromNonText`) are left alone.
156
+ */
157
+ export function neutralizeFormula(text: string, fromNonText = false): string {
158
+ if (fromNonText || !FORMULA_TRIGGER.test(text) || PLAIN_NUMBER.test(text)) return text;
159
+ return `'${text}`;
160
+ }
161
+
162
+ /** Whether a raw value is a number, bigint, boolean or Date, whose formatted text is not user-typed. */
163
+ export function isNonTextValue(value: unknown): boolean {
164
+ return typeof value === 'number' || typeof value === 'bigint' || typeof value === 'boolean' || value instanceof Date;
165
+ }
166
+
167
+ export function escapeHTML(value: string): string {
168
+ return value
169
+ .replace(/&/g, '&amp;')
170
+ .replace(/</g, '&lt;')
171
+ .replace(/>/g, '&gt;')
172
+ .replace(/"/g, '&quot;')
173
+ .replace(/'/g, '&#039;');
174
+ }
175
+
176
+ const INVALID_SHEET_CHARS = /[\\/?*:[\]]/g;
177
+ const MAX_SHEET_NAME = 31;
178
+
179
+ /**
180
+ * A sheet name Excel accepts: the characters `\ / ? * : [ ]` become `-`, leading and trailing
181
+ * apostrophes are dropped, the name is cut to 31 characters, and a name already in `used`
182
+ * (case-insensitive, plus Excel's reserved "History") gets a " (2)", " (3)"… suffix. The chosen
183
+ * name is added to `used`.
184
+ */
185
+ export function sanitizeSheetName(name: string | undefined, used: Set<string> = new Set()): string {
186
+ let base = String(name ?? '').replace(INVALID_SHEET_CHARS, '-').trim().replace(/^'+|'+$/g, '').trim();
187
+ if (!base) base = 'Sheet';
188
+ base = base.slice(0, MAX_SHEET_NAME).trim();
189
+ const taken = (candidate: string) => used.has(candidate.toLowerCase()) || candidate.toLowerCase() === 'history';
190
+ let result = base;
191
+ for (let n = 2; taken(result); n++) {
192
+ const suffix = ` (${n})`;
193
+ result = `${base.slice(0, MAX_SHEET_NAME - suffix.length).trim()}${suffix}`;
194
+ }
195
+ used.add(result.toLowerCase());
196
+ return result;
197
+ }