@pagent-libs/core 0.1.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 (111) hide show
  1. package/README.md +32 -0
  2. package/dist/canvas/cell-renderer.d.ts +45 -0
  3. package/dist/canvas/cell-renderer.d.ts.map +1 -0
  4. package/dist/canvas/grid-renderer.d.ts +29 -0
  5. package/dist/canvas/grid-renderer.d.ts.map +1 -0
  6. package/dist/canvas/header-renderer.d.ts +58 -0
  7. package/dist/canvas/header-renderer.d.ts.map +1 -0
  8. package/dist/canvas/hit-testing.d.ts +81 -0
  9. package/dist/canvas/hit-testing.d.ts.map +1 -0
  10. package/dist/canvas/index.d.ts +9 -0
  11. package/dist/canvas/index.d.ts.map +1 -0
  12. package/dist/canvas/renderer.d.ts +140 -0
  13. package/dist/canvas/renderer.d.ts.map +1 -0
  14. package/dist/canvas/selection-renderer.d.ts +55 -0
  15. package/dist/canvas/selection-renderer.d.ts.map +1 -0
  16. package/dist/canvas/text-renderer.d.ts +49 -0
  17. package/dist/canvas/text-renderer.d.ts.map +1 -0
  18. package/dist/canvas/types.d.ts +200 -0
  19. package/dist/canvas/types.d.ts.map +1 -0
  20. package/dist/collaboration/firebase-provider.d.ts +13 -0
  21. package/dist/collaboration/firebase-provider.d.ts.map +1 -0
  22. package/dist/collaboration/index.d.ts +3 -0
  23. package/dist/collaboration/index.d.ts.map +1 -0
  24. package/dist/collaboration/types.d.ts +34 -0
  25. package/dist/collaboration/types.d.ts.map +1 -0
  26. package/dist/event-emitter.d.ts +13 -0
  27. package/dist/event-emitter.d.ts.map +1 -0
  28. package/dist/export/csv.d.ts +5 -0
  29. package/dist/export/csv.d.ts.map +1 -0
  30. package/dist/export/index.d.ts +2 -0
  31. package/dist/export/index.d.ts.map +1 -0
  32. package/dist/features/filter.d.ts +58 -0
  33. package/dist/features/filter.d.ts.map +1 -0
  34. package/dist/features/freeze.d.ts +86 -0
  35. package/dist/features/freeze.d.ts.map +1 -0
  36. package/dist/features/index.d.ts +4 -0
  37. package/dist/features/index.d.ts.map +1 -0
  38. package/dist/features/sort.d.ts +15 -0
  39. package/dist/features/sort.d.ts.map +1 -0
  40. package/dist/format-pool.d.ts +17 -0
  41. package/dist/format-pool.d.ts.map +1 -0
  42. package/dist/formula-graph.d.ts +12 -0
  43. package/dist/formula-graph.d.ts.map +1 -0
  44. package/dist/formula-parser/cell-reference.d.ts +7 -0
  45. package/dist/formula-parser/cell-reference.d.ts.map +1 -0
  46. package/dist/formula-parser/formula-adjust.d.ts +13 -0
  47. package/dist/formula-parser/formula-adjust.d.ts.map +1 -0
  48. package/dist/formula-parser/formula-ranges.d.ts +22 -0
  49. package/dist/formula-parser/formula-ranges.d.ts.map +1 -0
  50. package/dist/formula-parser/index.d.ts +6 -0
  51. package/dist/formula-parser/index.d.ts.map +1 -0
  52. package/dist/formula-parser/parser.d.ts +18 -0
  53. package/dist/formula-parser/parser.d.ts.map +1 -0
  54. package/dist/formula-parser/types.d.ts +33 -0
  55. package/dist/formula-parser/types.d.ts.map +1 -0
  56. package/dist/index.d.ts +15 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.esm.js +5823 -0
  59. package/dist/index.esm.js.map +1 -0
  60. package/dist/index.js +5885 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/sheet.d.ts +119 -0
  63. package/dist/sheet.d.ts.map +1 -0
  64. package/dist/style-pool.d.ts +17 -0
  65. package/dist/style-pool.d.ts.map +1 -0
  66. package/dist/types.d.ts +260 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/utils/cell-key.d.ts +7 -0
  69. package/dist/utils/cell-key.d.ts.map +1 -0
  70. package/dist/utils/format-utils.d.ts +75 -0
  71. package/dist/utils/format-utils.d.ts.map +1 -0
  72. package/dist/utils/range.d.ts +13 -0
  73. package/dist/utils/range.d.ts.map +1 -0
  74. package/dist/workbook.d.ts +155 -0
  75. package/dist/workbook.d.ts.map +1 -0
  76. package/package.json +46 -0
  77. package/src/canvas/cell-renderer.ts +181 -0
  78. package/src/canvas/grid-renderer.ts +238 -0
  79. package/src/canvas/header-renderer.ts +402 -0
  80. package/src/canvas/hit-testing.ts +537 -0
  81. package/src/canvas/index.ts +16 -0
  82. package/src/canvas/renderer.ts +1056 -0
  83. package/src/canvas/selection-renderer.ts +604 -0
  84. package/src/canvas/text-renderer.ts +321 -0
  85. package/src/canvas/types.ts +289 -0
  86. package/src/collaboration/firebase-provider.ts +48 -0
  87. package/src/collaboration/index.ts +5 -0
  88. package/src/collaboration/types.ts +38 -0
  89. package/src/event-emitter.ts +73 -0
  90. package/src/export/csv.ts +101 -0
  91. package/src/export/index.ts +4 -0
  92. package/src/features/filter.ts +231 -0
  93. package/src/features/freeze.ts +271 -0
  94. package/src/features/index.ts +5 -0
  95. package/src/features/sort.ts +282 -0
  96. package/src/format-pool.ts +61 -0
  97. package/src/formula-graph.ts +84 -0
  98. package/src/formula-parser/cell-reference.ts +99 -0
  99. package/src/formula-parser/formula-adjust.ts +129 -0
  100. package/src/formula-parser/formula-ranges.ts +159 -0
  101. package/src/formula-parser/index.ts +8 -0
  102. package/src/formula-parser/parser.ts +438 -0
  103. package/src/formula-parser/types.ts +39 -0
  104. package/src/index.ts +25 -0
  105. package/src/sheet.ts +502 -0
  106. package/src/style-pool.ts +62 -0
  107. package/src/types.ts +291 -0
  108. package/src/utils/cell-key.ts +19 -0
  109. package/src/utils/format-utils.ts +515 -0
  110. package/src/utils/range.ts +53 -0
  111. package/src/workbook.ts +1031 -0
@@ -0,0 +1,402 @@
1
+ // Header Renderer - Handles row/column headers and corner cell
2
+
3
+ import type { CanvasTheme, CellPosition } from './types';
4
+ import type { Selection, ColumnFilter } from '../types';
5
+
6
+ /**
7
+ * Renders row and column headers
8
+ */
9
+ export class HeaderRenderer {
10
+ private theme: CanvasTheme;
11
+
12
+ constructor(theme: CanvasTheme) {
13
+ this.theme = theme;
14
+ }
15
+
16
+ /**
17
+ * Convert column index to letter label (0 -> A, 1 -> B, ..., 26 -> AA, etc.)
18
+ */
19
+ columnIndexToLabel(index: number): string {
20
+ let label = '';
21
+ let n = index;
22
+
23
+ while (n >= 0) {
24
+ label = String.fromCharCode(65 + (n % 26)) + label;
25
+ n = Math.floor(n / 26) - 1;
26
+ }
27
+
28
+ return label;
29
+ }
30
+
31
+ /**
32
+ * Check if a column is in the selection
33
+ */
34
+ private isColumnSelected(
35
+ col: number,
36
+ selection: Selection | null,
37
+ activeCell: CellPosition | null
38
+ ): boolean {
39
+ if (activeCell && activeCell.col === col) {
40
+ return true;
41
+ }
42
+
43
+ if (!selection) {
44
+ return false;
45
+ }
46
+
47
+ for (const range of selection.ranges) {
48
+ if (col >= range.startCol && col <= range.endCol) {
49
+ return true;
50
+ }
51
+ }
52
+
53
+ return false;
54
+ }
55
+
56
+ /**
57
+ * Check if a row is in the selection
58
+ */
59
+ private isRowSelected(
60
+ row: number,
61
+ selection: Selection | null,
62
+ activeCell: CellPosition | null
63
+ ): boolean {
64
+ if (activeCell && activeCell.row === row) {
65
+ return true;
66
+ }
67
+
68
+ if (!selection) {
69
+ return false;
70
+ }
71
+
72
+ for (const range of selection.ranges) {
73
+ if (row >= range.startRow && row <= range.endRow) {
74
+ return true;
75
+ }
76
+ }
77
+
78
+ return false;
79
+ }
80
+
81
+ /**
82
+ * Check if there are hidden columns immediately before the given column
83
+ */
84
+ private hasHiddenColumnsBefore(col: number, hiddenCols: Set<number>): boolean {
85
+ // Check if column-1 is hidden
86
+ return col > 0 && hiddenCols.has(col - 1);
87
+ }
88
+
89
+ /**
90
+ * Render column headers
91
+ */
92
+ renderColumnHeaders(
93
+ ctx: CanvasRenderingContext2D,
94
+ startCol: number,
95
+ endCol: number,
96
+ scrollLeft: number,
97
+ colWidths: Map<number, number>,
98
+ defaultColWidth: number,
99
+ headerWidth: number,
100
+ headerHeight: number,
101
+ selection: Selection | null,
102
+ activeCell: CellPosition | null,
103
+ hiddenCols?: Set<number>,
104
+ skipAccumulationBefore?: number,
105
+ filters?: Map<number, ColumnFilter>
106
+ ): void {
107
+ const hidCols = hiddenCols ?? new Set<number>();
108
+
109
+ // Calculate starting x position (skip hidden columns)
110
+ // If skipAccumulationBefore is set, don't accumulate widths before that column
111
+ // (used when rendering scrollable columns after frozen columns)
112
+ let startX = headerWidth;
113
+ const accumulateFrom = skipAccumulationBefore ?? 0;
114
+ for (let c = accumulateFrom; c < startCol; c++) {
115
+ if (!hidCols.has(c)) {
116
+ startX += colWidths.get(c) ?? defaultColWidth;
117
+ }
118
+ }
119
+ startX -= scrollLeft;
120
+
121
+ // Render each column header (skip hidden columns)
122
+ let x = startX;
123
+ for (let col = startCol; col < endCol; col++) {
124
+ // Skip hidden columns
125
+ if (hidCols.has(col)) continue;
126
+
127
+ const width = colWidths.get(col) ?? defaultColWidth;
128
+ const isSelected = this.isColumnSelected(col, selection, activeCell);
129
+
130
+ // Check if there are hidden columns before this one
131
+ const hasHiddenBefore = this.hasHiddenColumnsBefore(col, hidCols);
132
+
133
+ // Background
134
+ ctx.fillStyle = isSelected
135
+ ? this.darkenColor(this.theme.headerBackgroundColor, 0.1)
136
+ : this.theme.headerBackgroundColor;
137
+ ctx.fillRect(x, 0, width, headerHeight);
138
+
139
+ // Bottom border
140
+ ctx.strokeStyle = this.theme.headerBorderColor;
141
+ ctx.lineWidth = 1;
142
+ ctx.beginPath();
143
+ ctx.moveTo(x, headerHeight - 0.5);
144
+ ctx.lineTo(x + width, headerHeight - 0.5);
145
+ ctx.stroke();
146
+
147
+ // Right border
148
+ ctx.beginPath();
149
+ ctx.moveTo(x + width - 0.5, 0);
150
+ ctx.lineTo(x + width - 0.5, headerHeight);
151
+ ctx.stroke();
152
+
153
+ // Hidden column indicator (double blue line on left edge)
154
+ if (hasHiddenBefore) {
155
+ ctx.strokeStyle = this.theme.selectionBorderColor;
156
+ ctx.lineWidth = 2;
157
+ // First line
158
+ ctx.beginPath();
159
+ ctx.moveTo(x + 1.5, 2);
160
+ ctx.lineTo(x + 1.5, headerHeight - 2);
161
+ ctx.stroke();
162
+ // Second line (double line effect)
163
+ ctx.beginPath();
164
+ ctx.moveTo(x + 4.5, 2);
165
+ ctx.lineTo(x + 4.5, headerHeight - 2);
166
+ ctx.stroke();
167
+ }
168
+
169
+ // Label
170
+ const label = this.columnIndexToLabel(col);
171
+ ctx.font = `${this.theme.headerFontSize}px ${this.theme.headerFont}`;
172
+ ctx.fillStyle = isSelected
173
+ ? this.darkenColor(this.theme.headerTextColor, 0.2)
174
+ : this.theme.headerTextColor;
175
+ ctx.textAlign = 'center';
176
+ ctx.textBaseline = 'middle';
177
+ ctx.fillText(label, x + width / 2, headerHeight / 2);
178
+
179
+ // Filter icon (if column has active filter)
180
+ if (filters?.has(col)) {
181
+ this.renderFilterIcon(ctx, x, 0, width);
182
+ }
183
+
184
+ x += width;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Check if there are hidden rows immediately before the given row
190
+ */
191
+ private hasHiddenRowsBefore(row: number, hiddenRows: Set<number>): boolean {
192
+ // Check if row-1 is hidden
193
+ return row > 0 && hiddenRows.has(row - 1);
194
+ }
195
+
196
+ /**
197
+ * Render row headers
198
+ */
199
+ renderRowHeaders(
200
+ ctx: CanvasRenderingContext2D,
201
+ startRow: number,
202
+ endRow: number,
203
+ scrollTop: number,
204
+ rowHeights: Map<number, number>,
205
+ defaultRowHeight: number,
206
+ headerWidth: number,
207
+ headerHeight: number,
208
+ selection: Selection | null,
209
+ activeCell: CellPosition | null,
210
+ hiddenRows?: Set<number>,
211
+ skipAccumulationBefore?: number
212
+ ): void {
213
+ const hidRows = hiddenRows ?? new Set<number>();
214
+
215
+ // Calculate starting y position (skip hidden rows)
216
+ // If skipAccumulationBefore is set, don't accumulate heights before that row
217
+ // (used when rendering scrollable rows after frozen rows)
218
+ let startY = headerHeight;
219
+ const accumulateFrom = skipAccumulationBefore ?? 0;
220
+ for (let r = accumulateFrom; r < startRow; r++) {
221
+ if (!hidRows.has(r)) {
222
+ startY += rowHeights.get(r) ?? defaultRowHeight;
223
+ }
224
+ }
225
+ startY -= scrollTop;
226
+
227
+ // Render each row header (skip hidden rows)
228
+ let y = startY;
229
+ for (let row = startRow; row < endRow; row++) {
230
+ // Skip hidden rows
231
+ if (hidRows.has(row)) continue;
232
+
233
+ const height = rowHeights.get(row) ?? defaultRowHeight;
234
+ const isSelected = this.isRowSelected(row, selection, activeCell);
235
+
236
+ // Check if there are hidden rows before this one
237
+ const hasHiddenBefore = this.hasHiddenRowsBefore(row, hidRows);
238
+
239
+ // Background
240
+ ctx.fillStyle = isSelected
241
+ ? this.darkenColor(this.theme.headerBackgroundColor, 0.1)
242
+ : this.theme.headerBackgroundColor;
243
+ ctx.fillRect(0, y, headerWidth, height);
244
+
245
+ // Bottom border
246
+ ctx.strokeStyle = this.theme.headerBorderColor;
247
+ ctx.lineWidth = 1;
248
+ ctx.beginPath();
249
+ ctx.moveTo(0, y + height - 0.5);
250
+ ctx.lineTo(headerWidth, y + height - 0.5);
251
+ ctx.stroke();
252
+
253
+ // Right border
254
+ ctx.beginPath();
255
+ ctx.moveTo(headerWidth - 0.5, y);
256
+ ctx.lineTo(headerWidth - 0.5, y + height);
257
+ ctx.stroke();
258
+
259
+ // Hidden row indicator (double blue line on top edge)
260
+ if (hasHiddenBefore) {
261
+ ctx.strokeStyle = this.theme.selectionBorderColor;
262
+ ctx.lineWidth = 2;
263
+ // First line
264
+ ctx.beginPath();
265
+ ctx.moveTo(2, y + 1.5);
266
+ ctx.lineTo(headerWidth - 2, y + 1.5);
267
+ ctx.stroke();
268
+ // Second line (double line effect)
269
+ ctx.beginPath();
270
+ ctx.moveTo(2, y + 4.5);
271
+ ctx.lineTo(headerWidth - 2, y + 4.5);
272
+ ctx.stroke();
273
+ }
274
+
275
+ // Label (1-indexed for display)
276
+ const label = String(row + 1);
277
+ ctx.font = `${this.theme.headerFontSize}px ${this.theme.headerFont}`;
278
+ ctx.fillStyle = isSelected
279
+ ? this.darkenColor(this.theme.headerTextColor, 0.2)
280
+ : this.theme.headerTextColor;
281
+ ctx.textAlign = 'center';
282
+ ctx.textBaseline = 'middle';
283
+ ctx.fillText(label, headerWidth / 2, y + height / 2);
284
+
285
+ y += height;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Render the corner cell (intersection of row and column headers)
291
+ */
292
+ renderCornerCell(
293
+ ctx: CanvasRenderingContext2D,
294
+ headerWidth: number,
295
+ headerHeight: number
296
+ ): void {
297
+ // Background
298
+ ctx.fillStyle = this.theme.headerBackgroundColor;
299
+ ctx.fillRect(0, 0, headerWidth, headerHeight);
300
+
301
+ // Right border
302
+ ctx.strokeStyle = this.theme.headerBorderColor;
303
+ ctx.lineWidth = 1;
304
+ ctx.beginPath();
305
+ ctx.moveTo(headerWidth - 0.5, 0);
306
+ ctx.lineTo(headerWidth - 0.5, headerHeight);
307
+ ctx.stroke();
308
+
309
+ // Bottom border
310
+ ctx.beginPath();
311
+ ctx.moveTo(0, headerHeight - 0.5);
312
+ ctx.lineTo(headerWidth, headerHeight - 0.5);
313
+ ctx.stroke();
314
+ }
315
+
316
+ /**
317
+ * Render resize handles for columns
318
+ */
319
+ renderColumnResizeHandle(
320
+ ctx: CanvasRenderingContext2D,
321
+ x: number,
322
+ headerHeight: number
323
+ ): void {
324
+ ctx.strokeStyle = this.theme.selectionBorderColor;
325
+ ctx.lineWidth = 2;
326
+ ctx.beginPath();
327
+ ctx.moveTo(x, 0);
328
+ ctx.lineTo(x, headerHeight);
329
+ ctx.stroke();
330
+ }
331
+
332
+ /**
333
+ * Render resize handles for rows
334
+ */
335
+ renderRowResizeHandle(
336
+ ctx: CanvasRenderingContext2D,
337
+ y: number,
338
+ headerWidth: number
339
+ ): void {
340
+ ctx.strokeStyle = this.theme.selectionBorderColor;
341
+ ctx.lineWidth = 2;
342
+ ctx.beginPath();
343
+ ctx.moveTo(0, y);
344
+ ctx.lineTo(headerWidth, y);
345
+ ctx.stroke();
346
+ }
347
+
348
+ /**
349
+ * Render filter icon in column header
350
+ */
351
+ private renderFilterIcon(
352
+ ctx: CanvasRenderingContext2D,
353
+ x: number,
354
+ y: number,
355
+ width: number
356
+ ): void {
357
+ const iconSize = 8;
358
+ const iconX = x + width - iconSize - 3; // 3px padding from right edge
359
+ const iconY = y + 3; // 3px padding from top
360
+
361
+ // Draw a small funnel/filter icon
362
+ ctx.fillStyle = this.theme.headerTextColor;
363
+ ctx.beginPath();
364
+
365
+ // Top triangle (wide part of funnel)
366
+ ctx.moveTo(iconX, iconY);
367
+ ctx.lineTo(iconX + iconSize, iconY);
368
+ ctx.lineTo(iconX + iconSize / 2, iconY + iconSize / 2);
369
+ ctx.closePath();
370
+ ctx.fill();
371
+
372
+ // Bottom triangle (narrow part of funnel)
373
+ ctx.beginPath();
374
+ ctx.moveTo(iconX + iconSize / 2 - 1, iconY + iconSize / 2);
375
+ ctx.lineTo(iconX + iconSize / 2 + 1, iconY + iconSize / 2);
376
+ ctx.lineTo(iconX + iconSize / 2, iconY + iconSize);
377
+ ctx.closePath();
378
+ ctx.fill();
379
+ }
380
+
381
+ /**
382
+ * Darken a hex color by a percentage
383
+ */
384
+ private darkenColor(hex: string, percent: number): string {
385
+ // Remove # if present
386
+ hex = hex.replace('#', '');
387
+
388
+ // Parse RGB
389
+ const r = parseInt(hex.substring(0, 2), 16);
390
+ const g = parseInt(hex.substring(2, 4), 16);
391
+ const b = parseInt(hex.substring(4, 6), 16);
392
+
393
+ // Darken
394
+ const newR = Math.max(0, Math.floor(r * (1 - percent)));
395
+ const newG = Math.max(0, Math.floor(g * (1 - percent)));
396
+ const newB = Math.max(0, Math.floor(b * (1 - percent)));
397
+
398
+ // Convert back to hex
399
+ return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
400
+ }
401
+ }
402
+