@opencorestack/opengridx 1.0.7 → 1.2.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 (38) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.d.ts +100 -12
  3. package/dist/opengridx.css +1 -1
  4. package/dist/opengridx.es.js +3199 -3015
  5. package/dist/opengridx.es.js.map +1 -1
  6. package/dist/opengridx.umd.js +12 -12
  7. package/dist/opengridx.umd.js.map +1 -1
  8. package/docs/API_REFERENCE.md +85 -0
  9. package/docs/components/cell.md +14 -0
  10. package/docs/components/filter-panel.md +13 -0
  11. package/docs/components/pagination.md +26 -0
  12. package/docs/components/row.md +26 -0
  13. package/docs/features/pdf-export.md +110 -0
  14. package/docs/roadmap.md +8 -0
  15. package/lib/components/Cell/Cell.tsx +44 -44
  16. package/lib/components/Cell/CellErrorBoundary.tsx +53 -0
  17. package/lib/components/DataGrid/DataGrid.css +11 -0
  18. package/lib/components/DataGrid/DataGrid.tsx +30 -6
  19. package/lib/components/DataGrid/GridListView.tsx +4 -0
  20. package/lib/components/DataGrid/GridPinnedRows.tsx +4 -0
  21. package/lib/components/DataGrid/GridVirtualRows.tsx +4 -0
  22. package/lib/components/Pagination/Pagination.test.tsx +64 -0
  23. package/lib/components/Pagination/Pagination.tsx +15 -4
  24. package/lib/components/Row/Row.tsx +6 -3
  25. package/lib/hooks/core/useGridColumns.tsx +11 -11
  26. package/lib/hooks/core/useGridControlledState.test.ts +86 -0
  27. package/lib/hooks/core/useGridKeyboardNavigation.test.ts +176 -0
  28. package/lib/hooks/core/useGridRowPipeline.test.ts +117 -0
  29. package/lib/hooks/useRowGrouping.test.ts +84 -0
  30. package/lib/hooks/useRowGrouping.ts +38 -13
  31. package/lib/hooks/useTreeData.ts +22 -2
  32. package/lib/index.ts +5 -1
  33. package/lib/types/index.ts +107 -12
  34. package/lib/utils/export/exportToPdf.test.ts +121 -0
  35. package/lib/utils/export/exportToPdf.ts +277 -0
  36. package/lib/utils/export/index.ts +3 -0
  37. package/lib/utils/export/pdf-types.d.ts +19 -0
  38. package/package.json +9 -1
@@ -0,0 +1,277 @@
1
+ import type {
2
+ GridColDef,
3
+ GridRowModel,
4
+ GridFilterModel,
5
+ PdfExportOptions,
6
+ } from '../../types';
7
+ import { formatAggregationValue } from '../../hooks/features/useAggregation';
8
+
9
+ // jsPDF plugin augments the jsPDF prototype — we type only what we use
10
+ interface JsPDFDoc {
11
+ autoTable: (opts: AutoTableOptions) => void;
12
+ save: (filename: string) => void;
13
+ setFontSize: (size: number) => void;
14
+ setFont: (name: string, style: string) => void;
15
+ setTextColor: (r: number, g: number, b: number) => void;
16
+ text: (text: string, x: number, y: number, opts?: { align?: string }) => void;
17
+ addImage: (data: string, format: string, x: number, y: number, w: number, h: number) => void;
18
+ line: (x1: number, y1: number, x2: number, y2: number) => void;
19
+ setDrawColor: (r: number, g: number, b: number) => void;
20
+ setLineWidth: (w: number) => void;
21
+ setPage: (page: number) => void;
22
+ lastAutoTable: { finalY: number };
23
+ internal: {
24
+ pageSize: { getWidth: () => number; getHeight: () => number };
25
+ getNumberOfPages: () => number;
26
+ };
27
+ getNumberOfPages: () => number;
28
+ }
29
+
30
+ interface AutoTableOptions {
31
+ head: string[][];
32
+ body: string[][];
33
+ foot?: string[][];
34
+ startY?: number;
35
+ styles?: Record<string, unknown>;
36
+ headStyles?: Record<string, unknown>;
37
+ alternateRowStyles?: Record<string, unknown>;
38
+ footStyles?: Record<string, unknown>;
39
+ columnStyles?: Record<number, { cellWidth: number }>;
40
+ showFoot?: string;
41
+ showHead?: string;
42
+ margin?: { top: number; left: number; right: number; bottom: number };
43
+ }
44
+
45
+ function hexToRgb(hex: string): [number, number, number] {
46
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
47
+ if (!result) return [79, 70, 229];
48
+ return [
49
+ parseInt(result[1], 16),
50
+ parseInt(result[2], 16),
51
+ parseInt(result[3], 16),
52
+ ];
53
+ }
54
+
55
+ function getExportColumns<R extends GridRowModel>(columns: GridColDef<R>[]): GridColDef<R>[] {
56
+ return columns.filter(col => {
57
+ if (col.exportable === false) return false;
58
+ if (col.field === '__check__' || col.field === '__actions__') return false;
59
+ return true;
60
+ });
61
+ }
62
+
63
+ function resolveValue<R extends GridRowModel>(row: R, col: GridColDef<R>): string {
64
+ let value: unknown = (row as GridRowModel)[col.field];
65
+ if (col.valueGetter) {
66
+ value = col.valueGetter({ row, field: col.field, value });
67
+ }
68
+ if (col.valueFormatter && value !== undefined && value !== null) {
69
+ value = col.valueFormatter({ value, row, field: col.field });
70
+ }
71
+ return String(value ?? '');
72
+ }
73
+
74
+ function colWeight<R extends GridRowModel>(col: GridColDef<R>): number {
75
+ const w = typeof col.width === 'number' ? col.width : undefined;
76
+ return w ?? (col.flex ?? 1) * 100;
77
+ }
78
+
79
+ function computeColumnStyles<R extends GridRowModel>(
80
+ columns: GridColDef<R>[],
81
+ usableWidth: number
82
+ ): Record<number, { cellWidth: number }> {
83
+ const totalWeight = columns.reduce((sum, col) => sum + colWeight(col), 0);
84
+ const styles: Record<number, { cellWidth: number }> = {};
85
+ columns.forEach((col, i) => {
86
+ styles[i] = { cellWidth: (colWeight(col) / totalWeight) * usableWidth };
87
+ });
88
+ return styles;
89
+ }
90
+
91
+ function formatFilterSummary<R extends GridRowModel>(
92
+ filterModel: GridFilterModel,
93
+ columns: GridColDef<R>[]
94
+ ): string {
95
+ if (!filterModel.items || filterModel.items.length === 0) return '';
96
+ return filterModel.items
97
+ .flatMap(item => {
98
+ // Only handle leaf GridFilterItem — skip GridFilterGroup (which has 'items')
99
+ if ('items' in item) return [];
100
+ const col = columns.find(c => c.field === item.field);
101
+ const label = col?.headerName ?? item.field;
102
+ const val = item.value !== undefined && item.value !== null ? String(item.value) : '';
103
+ return [`${label} ${item.operator} "${val}"`];
104
+ })
105
+ .join(' • ');
106
+ }
107
+
108
+ export async function exportToPdf<R extends GridRowModel>(
109
+ rows: R[],
110
+ columns: GridColDef<R>[],
111
+ options: PdfExportOptions = {}
112
+ ): Promise<void> {
113
+ // Lazy-load peer deps — provides a clear error if not installed
114
+ let JsPDF: new (opts: Record<string, unknown>) => JsPDFDoc;
115
+ try {
116
+ const mod = await import('jspdf');
117
+ JsPDF = mod.default as new (opts: Record<string, unknown>) => JsPDFDoc;
118
+ await import('jspdf-autotable');
119
+ } catch {
120
+ throw new Error(
121
+ "exportToPdf requires 'jspdf' and 'jspdf-autotable'. Run: npm install jspdf jspdf-autotable"
122
+ );
123
+ }
124
+
125
+ const {
126
+ fileName = 'export',
127
+ title,
128
+ logoUrl,
129
+ orientation = 'landscape',
130
+ selectedRows,
131
+ aggregationResult,
132
+ aggregationModel,
133
+ filterModel,
134
+ alternateRowColor = true,
135
+ headerBackgroundColor = '#4f46e5',
136
+ headerTextColor = '#ffffff',
137
+ fontSize = 9,
138
+ } = options;
139
+
140
+ const doc = new JsPDF({ orientation, unit: 'mm', format: 'a4' });
141
+ const pageWidth = doc.internal.pageSize.getWidth();
142
+ const pageHeight = doc.internal.pageSize.getHeight();
143
+ const MARGIN = 14;
144
+ const usableWidth = pageWidth - MARGIN * 2;
145
+
146
+ const exportColumns = getExportColumns(columns);
147
+ const rowsToExport = selectedRows && selectedRows.length > 0
148
+ ? rows.filter(r => selectedRows.includes(r.id as string | number))
149
+ : rows;
150
+
151
+ // --- Header block ---
152
+ let startY = MARGIN;
153
+
154
+ if (title) {
155
+ let cursorX = MARGIN;
156
+ let cursorY = MARGIN + 8;
157
+
158
+ // Logo
159
+ if (logoUrl) {
160
+ try {
161
+ doc.addImage(logoUrl, 'PNG', cursorX, MARGIN, 10, 10);
162
+ cursorX += 14;
163
+ } catch {
164
+ // logo failed to load — skip silently
165
+ }
166
+ }
167
+
168
+ // Title
169
+ doc.setFontSize(16);
170
+ doc.setFont('helvetica', 'bold');
171
+ doc.setTextColor(30, 41, 59); // #1e293b
172
+ doc.text(title, cursorX, cursorY);
173
+ cursorY += 6;
174
+
175
+ // Subtitle: date + row count
176
+ doc.setFontSize(8);
177
+ doc.setFont('helvetica', 'normal');
178
+ doc.setTextColor(100, 116, 139); // #64748b
179
+ const exportedAt = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
180
+ doc.text(
181
+ `Exported: ${exportedAt} • ${rowsToExport.length} row${rowsToExport.length !== 1 ? 's' : ''}`,
182
+ cursorX,
183
+ cursorY
184
+ );
185
+ cursorY += 5;
186
+
187
+ // Filter summary
188
+ if (filterModel) {
189
+ const summary = formatFilterSummary(filterModel, columns);
190
+ if (summary) {
191
+ doc.text(`Filters: ${summary}`, cursorX, cursorY);
192
+ cursorY += 5;
193
+ }
194
+ }
195
+
196
+ // Separator line
197
+ doc.setDrawColor(226, 232, 240); // #e2e8f0
198
+ doc.setLineWidth(0.3);
199
+ doc.line(MARGIN, cursorY + 2, pageWidth - MARGIN, cursorY + 2);
200
+ startY = cursorY + 6;
201
+ }
202
+
203
+ // --- Build table data ---
204
+ const head = [exportColumns.map(col => col.headerName ?? col.field)];
205
+
206
+ const body = rowsToExport.map(row =>
207
+ exportColumns.map(col => resolveValue(row, col))
208
+ );
209
+
210
+ // --- Aggregation footer ---
211
+ let foot: string[][] | undefined;
212
+ if (aggregationResult && aggregationModel) {
213
+ const footRow = exportColumns.map((col, i) => {
214
+ if (i === 0) {
215
+ // First column shows TOTAL label if any aggregation exists
216
+ const hasAgg = exportColumns.some(c => aggregationModel[c.field]);
217
+ return hasAgg ? 'TOTAL' : '';
218
+ }
219
+ const aggVal = aggregationResult[col.field];
220
+ if (aggVal === undefined || aggVal === null) return '';
221
+ let formatted: unknown = aggVal;
222
+ if (col.valueFormatter) {
223
+ formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
224
+ } else if (aggregationModel[col.field]) {
225
+ formatted = formatAggregationValue(aggVal, aggregationModel[col.field] as string);
226
+ }
227
+ return String(formatted);
228
+ });
229
+ foot = [footRow];
230
+ }
231
+
232
+ // --- Column widths ---
233
+ const columnStyles = computeColumnStyles(exportColumns, usableWidth);
234
+
235
+ const [hr, hg, hb] = hexToRgb(headerBackgroundColor);
236
+ const [tr, tg, tb] = hexToRgb(headerTextColor);
237
+
238
+ doc.autoTable({
239
+ head,
240
+ body,
241
+ ...(foot ? { foot } : {}),
242
+ startY,
243
+ styles: { fontSize, cellPadding: 3, font: 'helvetica', overflow: 'linebreak' },
244
+ headStyles: {
245
+ fillColor: [hr, hg, hb],
246
+ textColor: [tr, tg, tb],
247
+ fontStyle: 'bold',
248
+ },
249
+ ...(alternateRowColor
250
+ ? { alternateRowStyles: { fillColor: [248, 250, 252] } }
251
+ : {}),
252
+ ...(foot
253
+ ? { footStyles: { fillColor: [241, 245, 249], fontStyle: 'bold' } }
254
+ : {}),
255
+ columnStyles,
256
+ showFoot: foot ? 'lastPage' : 'never',
257
+ showHead: 'everyPage',
258
+ margin: { top: MARGIN, left: MARGIN, right: MARGIN, bottom: MARGIN + 6 },
259
+ });
260
+
261
+ // Two-pass page numbering — stamp all pages after the table is fully rendered
262
+ // so each stamp reads the correct final total (not the running count during draw)
263
+ const totalPages = doc.getNumberOfPages();
264
+ for (let i = 1; i <= totalPages; i++) {
265
+ doc.setPage(i);
266
+ doc.setFontSize(8);
267
+ doc.setTextColor(150, 150, 150);
268
+ doc.text(
269
+ `Page ${i} of ${totalPages}`,
270
+ pageWidth - MARGIN,
271
+ pageHeight - 8,
272
+ { align: 'right' }
273
+ );
274
+ }
275
+
276
+ doc.save(`${fileName}.pdf`);
277
+ }
@@ -553,3 +553,6 @@ function downloadFile(content: string, fileName: string, mimeType: string): void
553
553
  document.body.removeChild(link);
554
554
  URL.revokeObjectURL(url);
555
555
  }
556
+
557
+ export { exportToPdf } from './exportToPdf';
558
+ export type { PdfExportOptions } from '../../types';
@@ -0,0 +1,19 @@
1
+ // Ambient declarations for optional peer dependencies.
2
+ // These packages are listed in peerDependenciesMeta as optional and are NOT
3
+ // installed in devDependencies. The declarations here let TypeScript accept
4
+ // the dynamic imports without requiring the packages at build time.
5
+ // This file is excluded from the shipped .d.ts rollup so it does not
6
+ // conflict with a consumer's real jspdf types.
7
+
8
+ declare module 'jspdf' {
9
+ // Minimal shape — exportToPdf.ts only uses the JsPDFDoc interface it defines.
10
+ // The full jsPDF types are available when the consumer installs the package.
11
+ const JsPDF: new (opts?: Record<string, unknown>) => unknown;
12
+ export default JsPDF;
13
+ }
14
+
15
+ declare module 'jspdf-autotable' {
16
+ // Side-effect import that augments jsPDF.prototype.autoTable.
17
+ const autoTable: unknown;
18
+ export default autoTable;
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencorestack/opengridx",
3
- "version": "1.0.7",
3
+ "version": "1.2.0",
4
4
  "description": "OpenGridX: High-performance React data infrastructure. Unlock advanced Row Grouping, Excel Export, and Column Pinning without the usual \"Pro\" gatekeeping. Built for speed, scale, and complete architectural freedom. Fully open, virtualization-ready, and feature-complete.",
5
5
  "type": "module",
6
6
  "main": "./dist/opengridx.umd.js",
@@ -78,12 +78,20 @@
78
78
  },
79
79
  "peerDependencies": {
80
80
  "exceljs": ">=4.0.0",
81
+ "jspdf": ">=2.5.0",
82
+ "jspdf-autotable": ">=3.8.0",
81
83
  "react": ">=18.0.0",
82
84
  "react-dom": ">=18.0.0"
83
85
  },
84
86
  "peerDependenciesMeta": {
85
87
  "exceljs": {
86
88
  "optional": true
89
+ },
90
+ "jspdf": {
91
+ "optional": true
92
+ },
93
+ "jspdf-autotable": {
94
+ "optional": true
87
95
  }
88
96
  },
89
97
  "devDependencies": {