@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.
- package/README.md +32 -0
- package/dist/canvas/cell-renderer.d.ts +45 -0
- package/dist/canvas/cell-renderer.d.ts.map +1 -0
- package/dist/canvas/grid-renderer.d.ts +29 -0
- package/dist/canvas/grid-renderer.d.ts.map +1 -0
- package/dist/canvas/header-renderer.d.ts +58 -0
- package/dist/canvas/header-renderer.d.ts.map +1 -0
- package/dist/canvas/hit-testing.d.ts +81 -0
- package/dist/canvas/hit-testing.d.ts.map +1 -0
- package/dist/canvas/index.d.ts +9 -0
- package/dist/canvas/index.d.ts.map +1 -0
- package/dist/canvas/renderer.d.ts +140 -0
- package/dist/canvas/renderer.d.ts.map +1 -0
- package/dist/canvas/selection-renderer.d.ts +55 -0
- package/dist/canvas/selection-renderer.d.ts.map +1 -0
- package/dist/canvas/text-renderer.d.ts +49 -0
- package/dist/canvas/text-renderer.d.ts.map +1 -0
- package/dist/canvas/types.d.ts +200 -0
- package/dist/canvas/types.d.ts.map +1 -0
- package/dist/collaboration/firebase-provider.d.ts +13 -0
- package/dist/collaboration/firebase-provider.d.ts.map +1 -0
- package/dist/collaboration/index.d.ts +3 -0
- package/dist/collaboration/index.d.ts.map +1 -0
- package/dist/collaboration/types.d.ts +34 -0
- package/dist/collaboration/types.d.ts.map +1 -0
- package/dist/event-emitter.d.ts +13 -0
- package/dist/event-emitter.d.ts.map +1 -0
- package/dist/export/csv.d.ts +5 -0
- package/dist/export/csv.d.ts.map +1 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/features/filter.d.ts +58 -0
- package/dist/features/filter.d.ts.map +1 -0
- package/dist/features/freeze.d.ts +86 -0
- package/dist/features/freeze.d.ts.map +1 -0
- package/dist/features/index.d.ts +4 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/sort.d.ts +15 -0
- package/dist/features/sort.d.ts.map +1 -0
- package/dist/format-pool.d.ts +17 -0
- package/dist/format-pool.d.ts.map +1 -0
- package/dist/formula-graph.d.ts +12 -0
- package/dist/formula-graph.d.ts.map +1 -0
- package/dist/formula-parser/cell-reference.d.ts +7 -0
- package/dist/formula-parser/cell-reference.d.ts.map +1 -0
- package/dist/formula-parser/formula-adjust.d.ts +13 -0
- package/dist/formula-parser/formula-adjust.d.ts.map +1 -0
- package/dist/formula-parser/formula-ranges.d.ts +22 -0
- package/dist/formula-parser/formula-ranges.d.ts.map +1 -0
- package/dist/formula-parser/index.d.ts +6 -0
- package/dist/formula-parser/index.d.ts.map +1 -0
- package/dist/formula-parser/parser.d.ts +18 -0
- package/dist/formula-parser/parser.d.ts.map +1 -0
- package/dist/formula-parser/types.d.ts +33 -0
- package/dist/formula-parser/types.d.ts.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +5823 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +5885 -0
- package/dist/index.js.map +1 -0
- package/dist/sheet.d.ts +119 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/style-pool.d.ts +17 -0
- package/dist/style-pool.d.ts.map +1 -0
- package/dist/types.d.ts +260 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/cell-key.d.ts +7 -0
- package/dist/utils/cell-key.d.ts.map +1 -0
- package/dist/utils/format-utils.d.ts +75 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/range.d.ts +13 -0
- package/dist/utils/range.d.ts.map +1 -0
- package/dist/workbook.d.ts +155 -0
- package/dist/workbook.d.ts.map +1 -0
- package/package.json +46 -0
- package/src/canvas/cell-renderer.ts +181 -0
- package/src/canvas/grid-renderer.ts +238 -0
- package/src/canvas/header-renderer.ts +402 -0
- package/src/canvas/hit-testing.ts +537 -0
- package/src/canvas/index.ts +16 -0
- package/src/canvas/renderer.ts +1056 -0
- package/src/canvas/selection-renderer.ts +604 -0
- package/src/canvas/text-renderer.ts +321 -0
- package/src/canvas/types.ts +289 -0
- package/src/collaboration/firebase-provider.ts +48 -0
- package/src/collaboration/index.ts +5 -0
- package/src/collaboration/types.ts +38 -0
- package/src/event-emitter.ts +73 -0
- package/src/export/csv.ts +101 -0
- package/src/export/index.ts +4 -0
- package/src/features/filter.ts +231 -0
- package/src/features/freeze.ts +271 -0
- package/src/features/index.ts +5 -0
- package/src/features/sort.ts +282 -0
- package/src/format-pool.ts +61 -0
- package/src/formula-graph.ts +84 -0
- package/src/formula-parser/cell-reference.ts +99 -0
- package/src/formula-parser/formula-adjust.ts +129 -0
- package/src/formula-parser/formula-ranges.ts +159 -0
- package/src/formula-parser/index.ts +8 -0
- package/src/formula-parser/parser.ts +438 -0
- package/src/formula-parser/types.ts +39 -0
- package/src/index.ts +25 -0
- package/src/sheet.ts +502 -0
- package/src/style-pool.ts +62 -0
- package/src/types.ts +291 -0
- package/src/utils/cell-key.ts +19 -0
- package/src/utils/format-utils.ts +515 -0
- package/src/utils/range.ts +53 -0
- 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,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
|
+
|