@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
@@ -1,18 +1,36 @@
1
1
  import type {
2
2
  GridColDef,
3
3
  GridRowModel,
4
+ GridValidRowModel,
4
5
  GridFilterModel,
6
+ GridFilterGroup,
7
+ GridFilterItem,
5
8
  PdfExportOptions,
6
9
  GridGroupedExportRow,
7
10
  } from '../../types';
8
- import { formatAggregationValue } from '../../hooks/features/useAggregation';
11
+ import { groupHeaderLabel } from './groupLabel';
12
+ import {
13
+ aggregationForExport,
14
+ formatExportAggregate,
15
+ formatExportValue,
16
+ getExportColumns,
17
+ getRawExportValue,
18
+ hasSelection,
19
+ rowsForExport,
20
+ shouldExportGrouped,
21
+ summaryLabelText,
22
+ } from './exportShared';
23
+ import { toWinAnsi } from './pdfText';
9
24
 
10
25
  interface JsPDFDoc {
11
26
  save: (filename: string) => void;
12
27
  setFontSize: (size: number) => void;
13
28
  setFont: (name: string, style: string) => void;
14
29
  setTextColor: (r: number, g: number, b: number) => void;
15
- text: (text: string, x: number, y: number, opts?: { align?: string }) => void;
30
+ text: (text: string | string[], x: number, y: number, opts?: { align?: string }) => void;
31
+ splitTextToSize: (text: string, maxWidth: number) => string[];
32
+ addFileToVFS: (fileName: string, data: string) => void;
33
+ addFont: (fileName: string, fontName: string, fontStyle: string) => void;
16
34
  addImage: (data: string, format: string, x: number, y: number, w: number, h: number) => void;
17
35
  line: (x1: number, y1: number, x2: number, y2: number) => void;
18
36
  setDrawColor: (r: number, g: number, b: number) => void;
@@ -41,33 +59,22 @@ interface AutoTableOptions {
41
59
  margin?: { top: number; left: number; right: number; bottom: number };
42
60
  }
43
61
 
44
- function hexToRgb(hex: string): [number, number, number] {
45
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
46
- if (!result) return [79, 70, 229];
47
- return [
48
- parseInt(result[1], 16),
49
- parseInt(result[2], 16),
50
- parseInt(result[3], 16),
51
- ];
52
- }
62
+ type Rgb = [number, number, number];
53
63
 
54
- function getExportColumns<R extends GridRowModel>(columns: GridColDef<R>[]): GridColDef<R>[] {
55
- return columns.filter(col => {
56
- if (col.exportable === false) return false;
57
- if (col.field === '__check__' || col.field === '__actions__') return false;
58
- return true;
59
- });
64
+ const DEFAULT_HEADER_BACKGROUND: Rgb = [79, 70, 229];
65
+ const DEFAULT_HEADER_TEXT: Rgb = [255, 255, 255];
66
+
67
+ /** '#rrggbb' or '#rgb' as RGB; invalid input gives `fallback` (the option's own default). */
68
+ function hexToRgb(hex: string, fallback: Rgb): Rgb {
69
+ const clean = hex.trim().replace(/^#/, '');
70
+ const full = /^[a-f\d]{3}$/i.test(clean) ? clean.split('').map(c => c + c).join('') : clean;
71
+ const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(full);
72
+ if (!result) return fallback;
73
+ return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
60
74
  }
61
75
 
62
76
  function resolveValue<R extends GridRowModel>(row: R, col: GridColDef<R>): string {
63
- let value: unknown = (row as GridRowModel)[col.field];
64
- if (col.valueGetter) {
65
- value = col.valueGetter({ row, field: col.field, value });
66
- }
67
- if (col.valueFormatter && value !== undefined && value !== null) {
68
- value = col.valueFormatter({ value, row, field: col.field });
69
- }
70
- return String(value ?? '');
77
+ return formatExportValue(row, col, getRawExportValue(row, col));
71
78
  }
72
79
 
73
80
  function colWeight<R extends GridRowModel>(col: GridColDef<R>): number {
@@ -87,27 +94,60 @@ function computeColumnStyles<R extends GridRowModel>(
87
94
  return styles;
88
95
  }
89
96
 
97
+ const NO_VALUE_OPERATORS = new Set(['isEmpty', 'isNotEmpty']);
98
+
99
+ function describeFilterItem<R extends GridRowModel>(item: GridFilterItem, columns: GridColDef<R>[]): string | null {
100
+ const col = columns.find(c => c.field === item.field);
101
+ const label = col?.headerName ?? item.field;
102
+ if (NO_VALUE_OPERATORS.has(item.operator)) return `${label} ${item.operator}`;
103
+ const { value } = item;
104
+ // An item without a value is an unfinished rule in the filter panel, not a real condition.
105
+ if (value == null || value === '' || (Array.isArray(value) && value.length === 0)) return null;
106
+ const text = Array.isArray(value) ? value.map(String).join(', ') : String(value);
107
+ return `${label} ${item.operator} "${text}"`;
108
+ }
109
+
110
+ function describeFilterItems<R extends GridRowModel>(
111
+ items: (GridFilterItem | GridFilterGroup)[],
112
+ logicOperator: 'and' | 'or',
113
+ columns: GridColDef<R>[],
114
+ nested: boolean,
115
+ ): string {
116
+ const parts = items
117
+ .map(item => ('items' in item
118
+ ? describeFilterItems(item.items, item.logicOperator, columns, true)
119
+ : describeFilterItem(item, columns)))
120
+ .filter((part): part is string => !!part);
121
+ if (parts.length === 0) return '';
122
+ const joined = parts.join(logicOperator === 'or' ? ' OR ' : ' AND ');
123
+ return nested && parts.length > 1 ? `(${joined})` : joined;
124
+ }
125
+
126
+ /**
127
+ * The applied filter as text: nested groups in parentheses, joined with their logic operator,
128
+ * plus the quick-search terms.
129
+ */
90
130
  function formatFilterSummary<R extends GridRowModel>(
91
131
  filterModel: GridFilterModel,
92
132
  columns: GridColDef<R>[]
93
133
  ): string {
94
- if (!filterModel.items || filterModel.items.length === 0) return '';
95
- return filterModel.items
96
- .flatMap(item => {
97
- // Only handle leaf GridFilterItem — skip GridFilterGroup (which has 'items')
98
- if ('items' in item) return [];
99
- const col = columns.find(c => c.field === item.field);
100
- const label = col?.headerName ?? item.field;
101
- const val = item.value !== undefined && item.value !== null ? String(item.value) : '';
102
- return [`${label} ${item.operator} "${val}"`];
103
- })
104
- .join(' • ');
134
+ const parts: string[] = [];
135
+ const items = describeFilterItems(filterModel.items ?? [], filterModel.logicOperator ?? 'and', columns, false);
136
+ if (items) parts.push(items);
137
+ const search = (filterModel.quickFilterValues ?? []).filter(v => v.trim() !== '');
138
+ if (search.length > 0) parts.push(`Search: ${search.map(v => `"${v}"`).join(' ')}`);
139
+ return parts.join(' • ');
105
140
  }
106
141
 
142
+ /** jsPDF's default line height factor, in mm per point of font size. */
143
+ const LINE_HEIGHT_MM_PER_PT = 1.15 * 0.3528;
144
+
145
+ export function exportToPdf<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef<R>[], options?: PdfExportOptions<R>): Promise<void>;
146
+ export function exportToPdf<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef[], options?: PdfExportOptions<R>): Promise<void>;
107
147
  export async function exportToPdf<R extends GridRowModel>(
108
148
  rows: R[],
109
149
  columns: GridColDef<R>[],
110
- options: PdfExportOptions = {}
150
+ options: PdfExportOptions<R> = {}
111
151
  ): Promise<void> {
112
152
  // Lazy-load peer deps — provides a clear error if not installed
113
153
  let JsPDF: new (opts: Record<string, unknown>) => JsPDFDoc;
@@ -131,6 +171,7 @@ export async function exportToPdf<R extends GridRowModel>(
131
171
  logoUrl,
132
172
  orientation = 'landscape',
133
173
  selectedRows,
174
+ getRowId,
134
175
  aggregationResult,
135
176
  aggregationModel,
136
177
  filterModel,
@@ -139,6 +180,7 @@ export async function exportToPdf<R extends GridRowModel>(
139
180
  headerTextColor = '#ffffff',
140
181
  fontSize = 9,
141
182
  groupedRows,
183
+ font,
142
184
  } = options;
143
185
 
144
186
  const doc = new JsPDF({ orientation, unit: 'mm', format: 'a4' });
@@ -147,10 +189,33 @@ export async function exportToPdf<R extends GridRowModel>(
147
189
  const MARGIN = 14;
148
190
  const usableWidth = pageWidth - MARGIN * 2;
149
191
 
192
+ // --- Font ---
193
+ let fontName = 'helvetica';
194
+ if (font) {
195
+ doc.addFileToVFS(`${font.name}-normal.ttf`, font.data);
196
+ doc.addFont(`${font.name}-normal.ttf`, font.name, 'normal');
197
+ if (font.boldData) doc.addFileToVFS(`${font.name}-bold.ttf`, font.boldData);
198
+ doc.addFont(font.boldData ? `${font.name}-bold.ttf` : `${font.name}-normal.ttf`, font.name, 'bold');
199
+ fontName = font.name;
200
+ }
201
+
202
+ // Helvetica cannot draw text outside WinAnsi; keep what it can and replace the rest.
203
+ let replacedCharacters = false;
204
+ const pdfText = (text: string): string => {
205
+ if (font) return text;
206
+ const converted = toWinAnsi(text);
207
+ if (converted.lossy) replacedCharacters = true;
208
+ return converted.text;
209
+ };
210
+ const drawWrapped = (text: string, x: number, y: number, maxWidth: number, sizePt: number): number => {
211
+ const lines = doc.splitTextToSize(pdfText(text), maxWidth);
212
+ doc.text(lines.length === 1 ? lines[0] : lines, x, y);
213
+ return Math.max(0, lines.length - 1) * sizePt * LINE_HEIGHT_MM_PER_PT;
214
+ };
215
+
150
216
  const exportColumns = getExportColumns(columns);
151
- const rowsToExport = selectedRows && selectedRows.length > 0
152
- ? rows.filter(r => selectedRows.includes(r.id as string | number))
153
- : rows;
217
+ const useGrouped = shouldExportGrouped(groupedRows, selectedRows);
218
+ const rowsToExport = rowsForExport(rows, selectedRows, getRowId);
154
219
 
155
220
  // --- Header block ---
156
221
  let startY = MARGIN;
@@ -167,22 +232,23 @@ export async function exportToPdf<R extends GridRowModel>(
167
232
  // logo failed to load — skip silently
168
233
  }
169
234
  }
235
+ const textWidth = pageWidth - MARGIN - cursorX;
170
236
 
171
237
  doc.setFontSize(16);
172
- doc.setFont('helvetica', 'bold');
238
+ doc.setFont(fontName, 'bold');
173
239
  doc.setTextColor(30, 41, 59);
174
- doc.text(title, cursorX, cursorY);
240
+ cursorY += drawWrapped(title, cursorX, cursorY, textWidth, 16);
175
241
  cursorY += 6;
176
242
 
177
243
  doc.setFontSize(8);
178
- doc.setFont('helvetica', 'normal');
244
+ doc.setFont(fontName, 'normal');
179
245
  doc.setTextColor(100, 116, 139);
180
246
  const exportedAt = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
181
- const rowCount = groupedRows
247
+ const rowCount = useGrouped
182
248
  ? groupedRows.filter(e => e.type === 'leaf').length
183
249
  : rowsToExport.length;
184
250
  doc.text(
185
- `Exported: ${exportedAt} • ${rowCount} row${rowCount !== 1 ? 's' : ''}`,
251
+ pdfText(`Exported: ${exportedAt} • ${rowCount} row${rowCount !== 1 ? 's' : ''}`),
186
252
  cursorX,
187
253
  cursorY
188
254
  );
@@ -191,7 +257,7 @@ export async function exportToPdf<R extends GridRowModel>(
191
257
  if (filterModel) {
192
258
  const summary = formatFilterSummary(filterModel, columns);
193
259
  if (summary) {
194
- doc.text(`Filters: ${summary}`, cursorX, cursorY);
260
+ cursorY += drawWrapped(`Filters: ${summary}`, cursorX, cursorY, textWidth, 8);
195
261
  cursorY += 5;
196
262
  }
197
263
  }
@@ -203,83 +269,68 @@ export async function exportToPdf<R extends GridRowModel>(
203
269
  }
204
270
 
205
271
  // --- Build table data ---
206
- const head = [exportColumns.map(col => col.headerName ?? col.field)];
272
+ const head = [exportColumns.map(col => pdfText(col.headerName ?? col.field))];
273
+ const aggMod = aggregationModel || {};
274
+ const aggregateTexts = (values: Record<string, unknown>): string[] =>
275
+ exportColumns.map(col => formatExportAggregate(col, aggMod[col.field], values[col.field], values));
207
276
 
208
277
  let body: string[][];
209
278
  let foot: string[][] | undefined;
210
279
 
211
- if (groupedRows && groupedRows.length > 0) {
280
+ if (useGrouped) {
212
281
  // Grouped export: flatten the ordered list into body rows with indentation
213
282
  body = [];
214
- const aggMod = aggregationModel || {};
215
283
  groupedRows.forEach((entry: GridGroupedExportRow) => {
284
+ const indent = ' '.repeat(entry.depth * 2);
216
285
  if (entry.type === 'group-header') {
217
- const indent = ' '.repeat(entry.depth * 2);
218
- const label = `${indent}${entry.groupField ?? ''}: ${String(entry.groupValue ?? '')}`;
286
+ const label = `${indent}${groupHeaderLabel(entry, columns)}`;
219
287
  body.push(exportColumns.map((_, i) => i === 0 ? label : ''));
220
288
  } else if (entry.type === 'leaf' && entry.row) {
221
289
  const row = entry.row as R;
222
- const indent = ' '.repeat(entry.depth * 2);
223
290
  body.push(exportColumns.map((col, i) => (i === 0 ? indent : '') + resolveValue(row, col)));
224
291
  } else if (entry.type === 'group-subtotal' && entry.aggregatedValues) {
225
- const indent = ' '.repeat(entry.depth * 2);
226
- body.push(exportColumns.map((col, i) => {
227
- if (i === 0) return `${indent}Subtotal`;
228
- const aggVal = entry.aggregatedValues![col.field];
229
- if (aggVal === undefined || aggVal === null) return '';
230
- let formatted: unknown = aggVal;
231
- if (col.valueFormatter) formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
232
- else if (aggMod[col.field]) formatted = formatAggregationValue(aggVal, aggMod[col.field] as string);
233
- return String(formatted);
234
- }));
292
+ const texts = aggregateTexts(entry.aggregatedValues);
293
+ body.push(texts.map((text, i) => (i === 0 ? `${indent}${summaryLabelText('Subtotal', text)}` : text)));
235
294
  } else if (entry.type === 'grand-total' && entry.aggregatedValues) {
236
- const footRow = exportColumns.map((col, i) => {
237
- if (i === 0) return 'Grand Total';
238
- const aggVal = entry.aggregatedValues![col.field];
239
- if (aggVal === undefined || aggVal === null) return '';
240
- let formatted: unknown = aggVal;
241
- if (col.valueFormatter) formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
242
- else if (aggMod[col.field]) formatted = formatAggregationValue(aggVal, aggMod[col.field] as string);
243
- return String(formatted);
244
- });
245
- foot = [footRow];
295
+ const texts = aggregateTexts(entry.aggregatedValues);
296
+ foot = [texts.map((text, i) => (i === 0 ? summaryLabelText('Grand Total', text) : text))];
246
297
  }
247
298
  });
248
299
  } else {
249
300
  body = rowsToExport.map(row => exportColumns.map(col => resolveValue(row, col)));
250
301
 
251
- if (aggregationResult && aggregationModel) {
252
- const footRow = exportColumns.map((col, i) => {
253
- if (i === 0) {
254
- const hasAgg = exportColumns.some(c => aggregationModel[c.field]);
255
- return hasAgg ? 'TOTAL' : '';
256
- }
257
- const aggVal = aggregationResult[col.field];
258
- if (aggVal === undefined || aggVal === null) return '';
259
- let formatted: unknown = aggVal;
260
- if (col.valueFormatter) {
261
- formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
262
- } else if (aggregationModel[col.field]) {
263
- formatted = formatAggregationValue(aggVal, aggregationModel[col.field] as string);
264
- }
265
- return String(formatted);
266
- });
267
- foot = [footRow];
302
+ const totals = aggregationModel
303
+ ? aggregationForExport(rowsToExport, hasSelection(selectedRows), columns, aggregationResult, aggregationModel)
304
+ : null;
305
+ if (totals && aggregationModel) {
306
+ const hasAgg = exportColumns.some(c => aggregationModel[c.field]);
307
+ const texts = aggregateTexts(totals);
308
+ foot = [texts.map((text, i) => (i === 0 && hasAgg ? summaryLabelText('TOTAL', text) : text))];
268
309
  }
269
310
  }
270
311
 
312
+ body = body.map(cells => cells.map(pdfText));
313
+ if (foot) foot = foot.map(cells => cells.map(pdfText));
314
+
315
+ if (replacedCharacters) {
316
+ console.warn(
317
+ '[exportToPdf] Some characters cannot be drawn with the built-in Helvetica font and were replaced with "?". ' +
318
+ 'Pass the `font` option (a Unicode .ttf, e.g. Noto Sans) to draw them.'
319
+ );
320
+ }
321
+
271
322
  // --- Column widths ---
272
323
  const columnStyles = computeColumnStyles(exportColumns, usableWidth);
273
324
 
274
- const [hr, hg, hb] = hexToRgb(headerBackgroundColor);
275
- const [tr, tg, tb] = hexToRgb(headerTextColor);
325
+ const [hr, hg, hb] = hexToRgb(headerBackgroundColor, DEFAULT_HEADER_BACKGROUND);
326
+ const [tr, tg, tb] = hexToRgb(headerTextColor, DEFAULT_HEADER_TEXT);
276
327
 
277
328
  autoTable(doc, {
278
329
  head,
279
330
  body,
280
331
  ...(foot ? { foot } : {}),
281
332
  startY,
282
- styles: { fontSize, cellPadding: 3, font: 'helvetica', overflow: 'linebreak' },
333
+ styles: { fontSize, cellPadding: 3, font: fontName, overflow: 'linebreak' },
283
334
  headStyles: {
284
335
  fillColor: [hr, hg, hb],
285
336
  textColor: [tr, tg, tb],
@@ -0,0 +1,14 @@
1
+ import type { GridColDef, GridGroupedExportRow, GridRowModel } from '../../types';
2
+
3
+ /**
4
+ * The label for a grouped-export group-header row, matching what the grid shows:
5
+ * the entry's own groupLabel (set by getGroupedExportRows), else the grouping column's
6
+ * groupingValueFormatter, else the documented "field: value" default.
7
+ */
8
+ export function groupHeaderLabel<R extends GridRowModel>(entry: GridGroupedExportRow, columns: GridColDef<R>[]): string {
9
+ if (entry.groupLabel !== undefined) return entry.groupLabel;
10
+ const field = entry.groupField ?? '';
11
+ const formatter = columns.find(col => col.field === field)?.groupingValueFormatter;
12
+ if (formatter) return formatter({ field, value: entry.groupValue });
13
+ return `${field}: ${String(entry.groupValue ?? '')}`;
14
+ }