@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,321 @@
1
+ // Text Renderer - Handles text measurement and rendering
2
+
3
+ import type { CanvasTheme, TextStyle, Rect } from './types';
4
+
5
+ /**
6
+ * Cached text metrics for a specific font configuration
7
+ */
8
+ interface FontMetricsCache {
9
+ font: string;
10
+ lineHeight: number;
11
+ baseline: number;
12
+ }
13
+
14
+ /**
15
+ * Handles text measurement and rendering with caching
16
+ */
17
+ export class TextRenderer {
18
+ private theme: CanvasTheme;
19
+ private metricsCache: Map<string, FontMetricsCache> = new Map();
20
+ private measureCanvas: HTMLCanvasElement;
21
+ private measureCtx: CanvasRenderingContext2D;
22
+
23
+ constructor(theme: CanvasTheme) {
24
+ this.theme = theme;
25
+
26
+ // Create offscreen canvas for text measurement
27
+ this.measureCanvas = document.createElement('canvas');
28
+ const ctx = this.measureCanvas.getContext('2d');
29
+ if (!ctx) {
30
+ throw new Error('Failed to create measurement context');
31
+ }
32
+ this.measureCtx = ctx;
33
+ }
34
+
35
+ /**
36
+ * Build CSS font string from style
37
+ */
38
+ buildFontString(style: Partial<TextStyle>): string {
39
+ const fontWeight = style.fontWeight ?? 'normal';
40
+ const fontStyle = style.fontStyle ?? 'normal';
41
+ const fontSize = style.fontSize ?? this.theme.cellFontSize;
42
+ const fontFamily = style.fontFamily ?? this.theme.cellFont;
43
+
44
+ return `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
45
+ }
46
+
47
+ /**
48
+ * Get font metrics (with caching)
49
+ */
50
+ getFontMetrics(font: string): FontMetricsCache {
51
+ const cached = this.metricsCache.get(font);
52
+ if (cached) {
53
+ return cached;
54
+ }
55
+
56
+ // Measure font metrics
57
+ this.measureCtx.font = font;
58
+ const metrics = this.measureCtx.measureText('Mg');
59
+
60
+ // Approximate line height as 1.2 * font size
61
+ const fontSize = parseInt(font.match(/(\d+)px/)?.[1] ?? '11', 10);
62
+ const lineHeight = fontSize * 1.2;
63
+
64
+ // Use actual metrics if available, otherwise approximate
65
+ const baseline = metrics.actualBoundingBoxAscent ?? fontSize * 0.8;
66
+
67
+ const fontMetrics: FontMetricsCache = {
68
+ font,
69
+ lineHeight,
70
+ baseline,
71
+ };
72
+
73
+ this.metricsCache.set(font, fontMetrics);
74
+ return fontMetrics;
75
+ }
76
+
77
+ /**
78
+ * Measure text width
79
+ */
80
+ measureText(text: string, style: Partial<TextStyle>): number {
81
+ const font = this.buildFontString(style);
82
+ this.measureCtx.font = font;
83
+ return this.measureCtx.measureText(text).width;
84
+ }
85
+
86
+ /**
87
+ * Truncate text to fit within maxWidth, adding ellipsis if needed
88
+ */
89
+ truncateText(text: string, maxWidth: number, style: Partial<TextStyle>): string {
90
+ const font = this.buildFontString(style);
91
+ this.measureCtx.font = font;
92
+
93
+ const fullWidth = this.measureCtx.measureText(text).width;
94
+ if (fullWidth <= maxWidth) {
95
+ return text;
96
+ }
97
+
98
+ const ellipsis = '…';
99
+ const ellipsisWidth = this.measureCtx.measureText(ellipsis).width;
100
+ const availableWidth = maxWidth - ellipsisWidth;
101
+
102
+ if (availableWidth <= 0) {
103
+ return ellipsis;
104
+ }
105
+
106
+ // Binary search for the right truncation point
107
+ let low = 0;
108
+ let high = text.length;
109
+
110
+ while (low < high) {
111
+ const mid = Math.ceil((low + high) / 2);
112
+ const truncated = text.substring(0, mid);
113
+ const width = this.measureCtx.measureText(truncated).width;
114
+
115
+ if (width <= availableWidth) {
116
+ low = mid;
117
+ } else {
118
+ high = mid - 1;
119
+ }
120
+ }
121
+
122
+ return text.substring(0, low) + ellipsis;
123
+ }
124
+
125
+ /**
126
+ * Render text within bounds
127
+ */
128
+ renderText(
129
+ ctx: CanvasRenderingContext2D,
130
+ text: string,
131
+ bounds: Rect,
132
+ style: Partial<TextStyle>,
133
+ padding: number = 4
134
+ ): void {
135
+ if (!text) return;
136
+
137
+ const font = this.buildFontString(style);
138
+ const metrics = this.getFontMetrics(font);
139
+
140
+ ctx.font = font;
141
+ ctx.fillStyle = style.color ?? this.theme.cellTextColor;
142
+
143
+ // Calculate available width for text
144
+ const availableWidth = bounds.width - padding * 2;
145
+
146
+ // Truncate if necessary
147
+ const displayText = this.truncateText(text, availableWidth, style);
148
+
149
+ // Calculate x position based on alignment
150
+ let x: number;
151
+ const textAlign = style.textAlign ?? 'left';
152
+
153
+ switch (textAlign) {
154
+ case 'center':
155
+ ctx.textAlign = 'center';
156
+ x = bounds.x + bounds.width / 2;
157
+ break;
158
+ case 'right':
159
+ ctx.textAlign = 'right';
160
+ x = bounds.x + bounds.width - padding;
161
+ break;
162
+ case 'left':
163
+ default:
164
+ ctx.textAlign = 'left';
165
+ x = bounds.x + padding;
166
+ break;
167
+ }
168
+
169
+ // Calculate y position based on vertical alignment
170
+ let y: number;
171
+ const verticalAlign = style.verticalAlign ?? 'middle';
172
+
173
+ switch (verticalAlign) {
174
+ case 'top':
175
+ y = bounds.y + padding + metrics.baseline;
176
+ break;
177
+ case 'bottom':
178
+ y = bounds.y + bounds.height - padding;
179
+ break;
180
+ case 'middle':
181
+ default:
182
+ y = bounds.y + (bounds.height + metrics.baseline) / 2;
183
+ break;
184
+ }
185
+
186
+ // Apply text decoration
187
+ ctx.fillText(displayText, x, y);
188
+
189
+ // Render text decoration (underline, strikethrough)
190
+ if (style.textDecoration && style.textDecoration !== 'none') {
191
+ const textWidth = this.measureCtx.measureText(displayText).width;
192
+
193
+ // Adjust x for decoration based on alignment
194
+ let decorationX: number;
195
+ switch (textAlign) {
196
+ case 'center':
197
+ decorationX = x - textWidth / 2;
198
+ break;
199
+ case 'right':
200
+ decorationX = x - textWidth;
201
+ break;
202
+ default:
203
+ decorationX = x;
204
+ }
205
+
206
+ ctx.strokeStyle = style.color ?? this.theme.cellTextColor;
207
+ ctx.lineWidth = 1;
208
+ ctx.beginPath();
209
+
210
+ if (style.textDecoration === 'underline') {
211
+ const underlineY = y + 2;
212
+ ctx.moveTo(decorationX, underlineY);
213
+ ctx.lineTo(decorationX + textWidth, underlineY);
214
+ } else if (style.textDecoration === 'line-through') {
215
+ const strikeY = y - metrics.baseline / 3;
216
+ ctx.moveTo(decorationX, strikeY);
217
+ ctx.lineTo(decorationX + textWidth, strikeY);
218
+ }
219
+
220
+ ctx.stroke();
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Render wrapped text (for cells with text wrapping enabled)
226
+ */
227
+ renderWrappedText(
228
+ ctx: CanvasRenderingContext2D,
229
+ text: string,
230
+ bounds: Rect,
231
+ style: Partial<TextStyle>,
232
+ padding: number = 4
233
+ ): void {
234
+ if (!text) return;
235
+
236
+ const font = this.buildFontString(style);
237
+ const metrics = this.getFontMetrics(font);
238
+
239
+ ctx.font = font;
240
+ ctx.fillStyle = style.color ?? this.theme.cellTextColor;
241
+
242
+ const availableWidth = bounds.width - padding * 2;
243
+ const words = text.split(' ');
244
+ const lines: string[] = [];
245
+ let currentLine = '';
246
+
247
+ // Word wrap
248
+ for (const word of words) {
249
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
250
+ const testWidth = this.measureCtx.measureText(testLine).width;
251
+
252
+ if (testWidth <= availableWidth) {
253
+ currentLine = testLine;
254
+ } else {
255
+ if (currentLine) {
256
+ lines.push(currentLine);
257
+ }
258
+ currentLine = word;
259
+ }
260
+ }
261
+ if (currentLine) {
262
+ lines.push(currentLine);
263
+ }
264
+
265
+ // Calculate starting y position
266
+ const totalTextHeight = lines.length * metrics.lineHeight;
267
+ let startY: number;
268
+ const verticalAlign = style.verticalAlign ?? 'middle';
269
+
270
+ switch (verticalAlign) {
271
+ case 'top':
272
+ startY = bounds.y + padding + metrics.baseline;
273
+ break;
274
+ case 'bottom':
275
+ startY = bounds.y + bounds.height - padding - totalTextHeight + metrics.baseline;
276
+ break;
277
+ case 'middle':
278
+ default:
279
+ startY = bounds.y + (bounds.height - totalTextHeight) / 2 + metrics.baseline;
280
+ break;
281
+ }
282
+
283
+ // Calculate x position based on alignment
284
+ const textAlign = style.textAlign ?? 'left';
285
+ let x: number;
286
+
287
+ switch (textAlign) {
288
+ case 'center':
289
+ ctx.textAlign = 'center';
290
+ x = bounds.x + bounds.width / 2;
291
+ break;
292
+ case 'right':
293
+ ctx.textAlign = 'right';
294
+ x = bounds.x + bounds.width - padding;
295
+ break;
296
+ case 'left':
297
+ default:
298
+ ctx.textAlign = 'left';
299
+ x = bounds.x + padding;
300
+ break;
301
+ }
302
+
303
+ // Render each line
304
+ let y = startY;
305
+ for (const line of lines) {
306
+ // Only render if within bounds
307
+ if (y + metrics.lineHeight > bounds.y && y - metrics.baseline < bounds.y + bounds.height) {
308
+ ctx.fillText(line, x, y);
309
+ }
310
+ y += metrics.lineHeight;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Clear the metrics cache
316
+ */
317
+ clearCache(): void {
318
+ this.metricsCache.clear();
319
+ }
320
+ }
321
+
@@ -0,0 +1,289 @@
1
+ // Canvas rendering types for pagent-sheets
2
+
3
+ import type { Cell, CellStyle, CellFormat, Selection, ColumnFilter } from '../types';
4
+
5
+ /**
6
+ * Rectangle bounds
7
+ */
8
+ export interface Rect {
9
+ x: number;
10
+ y: number;
11
+ width: number;
12
+ height: number;
13
+ }
14
+
15
+ /**
16
+ * Point coordinates
17
+ */
18
+ export interface Point {
19
+ x: number;
20
+ y: number;
21
+ }
22
+
23
+ /**
24
+ * Cell position in the grid
25
+ */
26
+ export interface CellPosition {
27
+ row: number;
28
+ col: number;
29
+ }
30
+
31
+ /**
32
+ * Viewport state - what's currently visible
33
+ */
34
+ export interface Viewport {
35
+ scrollTop: number;
36
+ scrollLeft: number;
37
+ width: number;
38
+ height: number;
39
+ startRow: number;
40
+ endRow: number;
41
+ startCol: number;
42
+ endCol: number;
43
+ }
44
+
45
+ /**
46
+ * Text style for rendering
47
+ */
48
+ export interface TextStyle {
49
+ fontFamily: string;
50
+ fontSize: number;
51
+ fontWeight: 'normal' | 'bold';
52
+ fontStyle: 'normal' | 'italic';
53
+ color: string;
54
+ textAlign: 'left' | 'center' | 'right';
55
+ verticalAlign: 'top' | 'middle' | 'bottom';
56
+ textDecoration?: 'none' | 'underline' | 'line-through';
57
+ }
58
+
59
+ /**
60
+ * Border style for a single edge
61
+ */
62
+ export interface BorderStyle {
63
+ width: number;
64
+ color: string;
65
+ style: 'solid' | 'dashed' | 'dotted';
66
+ }
67
+
68
+ /**
69
+ * Complete border configuration for a cell
70
+ */
71
+ export interface CellBorders {
72
+ top?: BorderStyle;
73
+ right?: BorderStyle;
74
+ bottom?: BorderStyle;
75
+ left?: BorderStyle;
76
+ }
77
+
78
+ /**
79
+ * Header hit test result
80
+ */
81
+ export interface HeaderHit {
82
+ type: 'row' | 'column';
83
+ index: number;
84
+ isResize: boolean;
85
+ }
86
+
87
+ /**
88
+ * Resize handle hit test result
89
+ */
90
+ export interface ResizeHandle {
91
+ type: 'row' | 'column';
92
+ index: number;
93
+ }
94
+
95
+ /**
96
+ * Theme colors for the canvas
97
+ */
98
+ export interface CanvasTheme {
99
+ // Grid
100
+ gridLineColor: string;
101
+ gridLineWidth: number;
102
+
103
+ // Headers
104
+ headerBackgroundColor: string;
105
+ headerTextColor: string;
106
+ headerBorderColor: string;
107
+ headerFont: string;
108
+ headerFontSize: number;
109
+
110
+ // Cells
111
+ cellBackgroundColor: string;
112
+ cellTextColor: string;
113
+ cellFont: string;
114
+ cellFontSize: number;
115
+
116
+ // Selection
117
+ selectionBorderColor: string;
118
+ selectionBorderWidth: number;
119
+ selectionFillColor: string;
120
+ activeCellBorderColor: string;
121
+ activeCellBorderWidth: number;
122
+
123
+ // Fill handle
124
+ fillHandleColor: string;
125
+ fillHandleSize: number;
126
+
127
+ // Formula reference highlighting colors (border, fill pairs)
128
+ formulaReferenceColors: Array<{ border: string; fill: string }>;
129
+
130
+ // Freeze panes
131
+ freezeDividerColor: string;
132
+ freezeDividerWidth: number;
133
+ freezeShadowColor: string;
134
+ }
135
+
136
+ /**
137
+ * Default theme
138
+ */
139
+ export const DEFAULT_THEME: CanvasTheme = {
140
+ // Grid
141
+ gridLineColor: '#e2e2e2',
142
+ gridLineWidth: 1,
143
+
144
+ // Headers
145
+ headerBackgroundColor: '#f8f9fa',
146
+ headerTextColor: '#5f6368',
147
+ headerBorderColor: '#e8eaed',
148
+ headerFont: 'Arial',
149
+ headerFontSize: 11,
150
+
151
+ // Cells
152
+ cellBackgroundColor: '#ffffff',
153
+ cellTextColor: '#000000',
154
+ cellFont: 'Arial',
155
+ cellFontSize: 11,
156
+
157
+ // Selection
158
+ selectionBorderColor: '#1a73e8',
159
+ selectionBorderWidth: 2,
160
+ selectionFillColor: 'rgba(26, 115, 232, 0.1)',
161
+ activeCellBorderColor: '#1a73e8',
162
+ activeCellBorderWidth: 2,
163
+
164
+ // Fill handle
165
+ fillHandleColor: '#1a73e8',
166
+ fillHandleSize: 6,
167
+
168
+ // Formula reference highlighting colors (like Google Sheets)
169
+ formulaReferenceColors: [
170
+ { border: '#4285F4', fill: 'rgba(66, 133, 244, 0.15)' }, // Blue
171
+ { border: '#EA4335', fill: 'rgba(234, 67, 53, 0.15)' }, // Red
172
+ { border: '#FBBC04', fill: 'rgba(251, 188, 4, 0.15)' }, // Yellow
173
+ { border: '#34A853', fill: 'rgba(52, 168, 83, 0.15)' }, // Green
174
+ { border: '#9C27B0', fill: 'rgba(156, 39, 176, 0.15)' }, // Purple
175
+ { border: '#FF9800', fill: 'rgba(255, 152, 0, 0.15)' }, // Orange
176
+ { border: '#00BCD4', fill: 'rgba(0, 188, 212, 0.15)' }, // Cyan
177
+ { border: '#E91E63', fill: 'rgba(233, 30, 99, 0.15)' }, // Pink
178
+ ],
179
+
180
+ // Freeze panes
181
+ freezeDividerColor: '#c0c0c0',
182
+ freezeDividerWidth: 2,
183
+ freezeShadowColor: 'rgba(0, 0, 0, 0.08)',
184
+ };
185
+
186
+ /**
187
+ * Configuration for the canvas renderer
188
+ */
189
+ export interface CanvasRendererConfig {
190
+ canvas: HTMLCanvasElement;
191
+ devicePixelRatio?: number;
192
+ defaultRowHeight: number;
193
+ defaultColWidth: number;
194
+ headerHeight: number;
195
+ headerWidth: number;
196
+ theme?: Partial<CanvasTheme>;
197
+ }
198
+
199
+ /**
200
+ * Formula range for highlighting cell references in formulas
201
+ */
202
+ export interface FormulaRangeHighlight {
203
+ startRow: number;
204
+ startCol: number;
205
+ endRow: number;
206
+ endCol: number;
207
+ colorIndex: number; // Index for color selection
208
+ }
209
+
210
+ /**
211
+ * State required for rendering
212
+ */
213
+ export interface RenderState {
214
+ cells: Map<string, Cell>;
215
+ styles: Map<string, CellStyle>;
216
+ formats: Map<string, CellFormat>;
217
+ selection: Selection | null;
218
+ activeCell: CellPosition | null;
219
+ editingCell: CellPosition | null;
220
+ rowHeights: Map<number, number>;
221
+ colWidths: Map<number, number>;
222
+ rowCount: number;
223
+ colCount: number;
224
+ /** Formula ranges to highlight (cell references in formulas) */
225
+ formulaRanges?: FormulaRangeHighlight[];
226
+ /** Hidden rows (Set of row indices) */
227
+ hiddenRows?: Set<number>;
228
+ /** Hidden columns (Set of column indices) */
229
+ hiddenCols?: Set<number>;
230
+ /** Number of frozen rows (rows that stay visible when scrolling vertically) */
231
+ frozenRows?: number;
232
+ /** Number of frozen columns (columns that stay visible when scrolling horizontally) */
233
+ frozenCols?: number;
234
+ /** Active filters (column -> filter) */
235
+ filters?: Map<number, ColumnFilter>;
236
+ /** Filtered rows (rows that should be visible after filtering) */
237
+ filteredRows?: Set<number>;
238
+ }
239
+
240
+ /**
241
+ * Region that needs to be redrawn
242
+ */
243
+ export interface DirtyRegion {
244
+ type: 'all' | 'cells' | 'headers' | 'selection';
245
+ bounds?: Rect;
246
+ }
247
+
248
+ /**
249
+ * Cursor types for different interactions
250
+ */
251
+ export type CursorType =
252
+ | 'default'
253
+ | 'pointer'
254
+ | 'cell'
255
+ | 'col-resize'
256
+ | 'row-resize'
257
+ | 'crosshair'
258
+ | 'grab'
259
+ | 'grabbing';
260
+
261
+ /**
262
+ * Mouse event data with grid coordinates
263
+ */
264
+ export interface CanvasMouseEvent {
265
+ // Screen coordinates relative to canvas
266
+ x: number;
267
+ y: number;
268
+ // Grid coordinates (if over a cell)
269
+ cell: CellPosition | null;
270
+ // Header info (if over a header)
271
+ header: HeaderHit | null;
272
+ // Resize handle (if near one)
273
+ resizeHandle: ResizeHandle | null;
274
+ // Fill handle
275
+ isFillHandle: boolean;
276
+ // Original event
277
+ originalEvent: MouseEvent;
278
+ }
279
+
280
+ /**
281
+ * Scroll event data
282
+ */
283
+ export interface CanvasScrollEvent {
284
+ scrollTop: number;
285
+ scrollLeft: number;
286
+ deltaX: number;
287
+ deltaY: number;
288
+ }
289
+
@@ -0,0 +1,48 @@
1
+ // Firebase collaboration provider
2
+ // disabling eslint for this file because it is a placeholder for the actual implementation. remove when implemented.
3
+ /* eslint-disable */
4
+ // @ts-nocheck
5
+ import type { CollaborationProvider, Presence } from './types';
6
+
7
+ export class FirebaseCollaborationProvider implements CollaborationProvider {
8
+ private db: any; // Firebase Realtime Database or Firestore
9
+ private handlers: Map<string, Set<(data: unknown) => void>> = new Map();
10
+ private presences: Map<string, Presence> = new Map();
11
+
12
+ constructor() {
13
+ // Initialize Firebase
14
+ // this.db = initializeFirebase(firebaseConfig);
15
+ }
16
+
17
+ async connect(workbookId: string): Promise<void> {
18
+ // Connect to Firebase Realtime Database or Firestore
19
+ // Set up listeners for changes, presence, etc.
20
+ }
21
+
22
+ disconnect(): void {
23
+ // Clean up Firebase listeners
24
+ this.handlers.clear();
25
+ this.presences.clear();
26
+ }
27
+
28
+ on(event: 'change' | 'presence' | 'cursor', handler: (data: unknown) => void): () => void {
29
+ if (!this.handlers.has(event)) {
30
+ this.handlers.set(event, new Set());
31
+ }
32
+ this.handlers.get(event)!.add(handler);
33
+
34
+ return () => {
35
+ this.handlers.get(event)?.delete(handler);
36
+ };
37
+ }
38
+
39
+ emit(event: 'change' | 'presence' | 'cursor', data: unknown): void {
40
+ // Send data to Firebase
41
+ // Firebase will broadcast to other clients
42
+ }
43
+
44
+ getPresences(): Presence[] {
45
+ return Array.from(this.presences.values());
46
+ }
47
+ }
48
+
@@ -0,0 +1,5 @@
1
+ // Collaboration package exports
2
+
3
+ export * from './types';
4
+ export * from './firebase-provider';
5
+
@@ -0,0 +1,38 @@
1
+ // Collaboration types
2
+
3
+ export interface Presence {
4
+ userId: string;
5
+ username: string;
6
+ color: string;
7
+ selection?: {
8
+ row: number;
9
+ col: number;
10
+ };
11
+ cursor?: {
12
+ row: number;
13
+ col: number;
14
+ };
15
+ }
16
+
17
+ export interface CollaborationOperation {
18
+ type: 'cellChange' | 'selectionChange' | 'sheetChange';
19
+ sheetId: string;
20
+ row?: number;
21
+ col?: number;
22
+ value?: unknown;
23
+ selection?: {
24
+ row: number;
25
+ col: number;
26
+ };
27
+ timestamp: number;
28
+ userId: string;
29
+ }
30
+
31
+ export interface CollaborationProvider {
32
+ connect(workbookId: string): Promise<void>;
33
+ disconnect(): void;
34
+ on(event: 'change' | 'presence' | 'cursor', handler: (data: unknown) => void): () => void;
35
+ emit(event: 'change' | 'presence' | 'cursor', data: unknown): void;
36
+ getPresences(): Presence[];
37
+ }
38
+