argent-grid 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/.github/workflows/pages.yml +68 -0
- package/AGENTS.md +179 -0
- package/README.md +222 -0
- package/demo-app/README.md +70 -0
- package/demo-app/angular.json +78 -0
- package/demo-app/e2e/benchmark.spec.ts +53 -0
- package/demo-app/e2e/demo-page.spec.ts +77 -0
- package/demo-app/e2e/grid-features.spec.ts +269 -0
- package/demo-app/package-lock.json +14023 -0
- package/demo-app/package.json +36 -0
- package/demo-app/playwright-test-menu.js +19 -0
- package/demo-app/playwright.config.ts +23 -0
- package/demo-app/src/app/app.component.ts +10 -0
- package/demo-app/src/app/app.config.ts +13 -0
- package/demo-app/src/app/app.routes.ts +7 -0
- package/demo-app/src/app/demo-page/demo-page.component.css +313 -0
- package/demo-app/src/app/demo-page/demo-page.component.html +124 -0
- package/demo-app/src/app/demo-page/demo-page.component.ts +366 -0
- package/demo-app/src/index.html +19 -0
- package/demo-app/src/main.ts +6 -0
- package/demo-app/tsconfig.json +31 -0
- package/ng-package.json +8 -0
- package/package.json +60 -0
- package/plan.md +131 -0
- package/setup-vitest.ts +18 -0
- package/src/lib/argent-grid.module.ts +21 -0
- package/src/lib/components/argent-grid.component.css +483 -0
- package/src/lib/components/argent-grid.component.html +320 -0
- package/src/lib/components/argent-grid.component.spec.ts +189 -0
- package/src/lib/components/argent-grid.component.ts +1188 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +92 -0
- package/src/lib/rendering/canvas-renderer.ts +962 -0
- package/src/lib/rendering/render/blit.spec.ts +453 -0
- package/src/lib/rendering/render/blit.ts +393 -0
- package/src/lib/rendering/render/cells.ts +369 -0
- package/src/lib/rendering/render/index.ts +105 -0
- package/src/lib/rendering/render/lines.ts +363 -0
- package/src/lib/rendering/render/theme.spec.ts +282 -0
- package/src/lib/rendering/render/theme.ts +201 -0
- package/src/lib/rendering/render/types.ts +279 -0
- package/src/lib/rendering/render/walk.spec.ts +360 -0
- package/src/lib/rendering/render/walk.ts +360 -0
- package/src/lib/rendering/utils/damage-tracker.spec.ts +444 -0
- package/src/lib/rendering/utils/damage-tracker.ts +423 -0
- package/src/lib/rendering/utils/index.ts +7 -0
- package/src/lib/services/grid.service.spec.ts +1039 -0
- package/src/lib/services/grid.service.ts +1284 -0
- package/src/lib/types/ag-grid-types.ts +970 -0
- package/src/public-api.ts +22 -0
- package/tsconfig.json +32 -0
- package/tsconfig.lib.json +11 -0
- package/tsconfig.spec.json +8 -0
- package/vitest.config.ts +55 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid Lines Rendering for Canvas Renderer
|
|
3
|
+
*
|
|
4
|
+
* Draws grid lines (borders) efficiently.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Column, IRowNode } from '../../types/ag-grid-types';
|
|
8
|
+
import { GridTheme, Rectangle } from './types';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// LINE DRAWING UTILITIES
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Draw a crisp 1px line (accounts for sub-pixel rendering)
|
|
16
|
+
*/
|
|
17
|
+
export function drawCrispLine(
|
|
18
|
+
ctx: CanvasRenderingContext2D,
|
|
19
|
+
x1: number,
|
|
20
|
+
y1: number,
|
|
21
|
+
x2: number,
|
|
22
|
+
y2: number
|
|
23
|
+
): void {
|
|
24
|
+
ctx.beginPath();
|
|
25
|
+
// Add 0.5 to pixel coordinates for crisp 1px lines
|
|
26
|
+
ctx.moveTo(Math.floor(x1) + 0.5, Math.floor(y1) + 0.5);
|
|
27
|
+
ctx.lineTo(Math.floor(x2) + 0.5, Math.floor(y2) + 0.5);
|
|
28
|
+
ctx.stroke();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Draw a horizontal line
|
|
33
|
+
*/
|
|
34
|
+
export function drawHorizontalLine(
|
|
35
|
+
ctx: CanvasRenderingContext2D,
|
|
36
|
+
y: number,
|
|
37
|
+
x1: number,
|
|
38
|
+
x2: number
|
|
39
|
+
): void {
|
|
40
|
+
drawCrispLine(ctx, x1, y, x2, y);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Draw a vertical line
|
|
45
|
+
*/
|
|
46
|
+
export function drawVerticalLine(
|
|
47
|
+
ctx: CanvasRenderingContext2D,
|
|
48
|
+
x: number,
|
|
49
|
+
y1: number,
|
|
50
|
+
y2: number
|
|
51
|
+
): void {
|
|
52
|
+
drawCrispLine(ctx, x, y1, x, y2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// GRID LINE RENDERING
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Draw all horizontal row lines
|
|
61
|
+
*/
|
|
62
|
+
export function drawRowLines(
|
|
63
|
+
ctx: CanvasRenderingContext2D,
|
|
64
|
+
startRow: number,
|
|
65
|
+
endRow: number,
|
|
66
|
+
rowHeight: number,
|
|
67
|
+
scrollTop: number,
|
|
68
|
+
viewportWidth: number,
|
|
69
|
+
theme: GridTheme
|
|
70
|
+
): void {
|
|
71
|
+
ctx.strokeStyle = theme.borderColor || theme.gridLineColor;
|
|
72
|
+
ctx.lineWidth = 1;
|
|
73
|
+
|
|
74
|
+
ctx.beginPath();
|
|
75
|
+
|
|
76
|
+
for (let row = startRow; row <= endRow; row++) {
|
|
77
|
+
const y = Math.floor(row * rowHeight - scrollTop) + 0.5;
|
|
78
|
+
ctx.moveTo(0, y);
|
|
79
|
+
ctx.lineTo(viewportWidth, y);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ctx.stroke();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Draw all vertical column lines
|
|
87
|
+
*/
|
|
88
|
+
export function drawColumnLines(
|
|
89
|
+
ctx: CanvasRenderingContext2D,
|
|
90
|
+
columns: Column[],
|
|
91
|
+
scrollX: number,
|
|
92
|
+
scrollTop: number,
|
|
93
|
+
viewportWidth: number,
|
|
94
|
+
viewportHeight: number,
|
|
95
|
+
leftPinnedWidth: number,
|
|
96
|
+
rightPinnedWidth: number,
|
|
97
|
+
theme: GridTheme,
|
|
98
|
+
startRow: number = 0,
|
|
99
|
+
endRow: number = 0,
|
|
100
|
+
rowHeight: number = 32
|
|
101
|
+
): void {
|
|
102
|
+
ctx.strokeStyle = theme.borderColor || theme.gridLineColor;
|
|
103
|
+
ctx.lineWidth = 1;
|
|
104
|
+
|
|
105
|
+
const columnPositions = getColumnBorderPositions(
|
|
106
|
+
columns,
|
|
107
|
+
scrollX,
|
|
108
|
+
viewportWidth,
|
|
109
|
+
leftPinnedWidth,
|
|
110
|
+
rightPinnedWidth
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Calculate Y range for drawing
|
|
114
|
+
const drawY1 = Math.max(0, Math.floor(startRow * rowHeight - scrollTop));
|
|
115
|
+
const drawY2 = Math.min(viewportHeight, Math.floor(endRow * rowHeight - scrollTop));
|
|
116
|
+
|
|
117
|
+
ctx.beginPath();
|
|
118
|
+
|
|
119
|
+
for (const x of columnPositions) {
|
|
120
|
+
const borderX = Math.floor(x) + 0.5;
|
|
121
|
+
ctx.moveTo(borderX, drawY1);
|
|
122
|
+
ctx.lineTo(borderX, drawY2);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
ctx.stroke();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get column border X positions
|
|
130
|
+
*/
|
|
131
|
+
export function getColumnBorderPositions(
|
|
132
|
+
columns: Column[],
|
|
133
|
+
scrollX: number,
|
|
134
|
+
viewportWidth: number,
|
|
135
|
+
leftPinnedWidth: number,
|
|
136
|
+
rightPinnedWidth: number
|
|
137
|
+
): number[] {
|
|
138
|
+
const positions: number[] = [];
|
|
139
|
+
|
|
140
|
+
const leftPinned = columns.filter(c => c.pinned === 'left');
|
|
141
|
+
const rightPinned = columns.filter(c => c.pinned === 'right');
|
|
142
|
+
const centerColumns = columns.filter(c => !c.pinned);
|
|
143
|
+
|
|
144
|
+
// Left pinned column borders
|
|
145
|
+
let x = 0;
|
|
146
|
+
for (const col of leftPinned) {
|
|
147
|
+
x += col.width;
|
|
148
|
+
positions.push(x);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Center column borders
|
|
152
|
+
x = leftPinnedWidth - scrollX;
|
|
153
|
+
for (const col of centerColumns) {
|
|
154
|
+
x += col.width;
|
|
155
|
+
// Only include if visible
|
|
156
|
+
if (x > leftPinnedWidth && x < viewportWidth - rightPinnedWidth) {
|
|
157
|
+
positions.push(x);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Right pinned column borders
|
|
162
|
+
x = viewportWidth - rightPinnedWidth;
|
|
163
|
+
for (const col of rightPinned) {
|
|
164
|
+
x += col.width;
|
|
165
|
+
positions.push(x);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return positions;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Draw grid lines for a region
|
|
173
|
+
*/
|
|
174
|
+
export function drawGridLines(
|
|
175
|
+
ctx: CanvasRenderingContext2D,
|
|
176
|
+
columns: Column[],
|
|
177
|
+
startRow: number,
|
|
178
|
+
endRow: number,
|
|
179
|
+
rowHeight: number,
|
|
180
|
+
scrollX: number,
|
|
181
|
+
scrollTop: number,
|
|
182
|
+
viewportWidth: number,
|
|
183
|
+
viewportHeight: number,
|
|
184
|
+
leftPinnedWidth: number,
|
|
185
|
+
rightPinnedWidth: number,
|
|
186
|
+
theme: GridTheme
|
|
187
|
+
): void {
|
|
188
|
+
// Draw horizontal lines
|
|
189
|
+
drawRowLines(ctx, startRow, endRow, rowHeight, scrollTop, viewportWidth, theme);
|
|
190
|
+
|
|
191
|
+
// Draw vertical lines
|
|
192
|
+
drawColumnLines(
|
|
193
|
+
ctx,
|
|
194
|
+
columns,
|
|
195
|
+
scrollX,
|
|
196
|
+
scrollTop,
|
|
197
|
+
viewportWidth,
|
|
198
|
+
viewportHeight,
|
|
199
|
+
leftPinnedWidth,
|
|
200
|
+
rightPinnedWidth,
|
|
201
|
+
theme,
|
|
202
|
+
startRow,
|
|
203
|
+
endRow,
|
|
204
|
+
rowHeight
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// BORDER RENDERING
|
|
210
|
+
// ============================================================================
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Draw border around a region
|
|
214
|
+
*/
|
|
215
|
+
export function drawBorder(
|
|
216
|
+
ctx: CanvasRenderingContext2D,
|
|
217
|
+
rect: Rectangle,
|
|
218
|
+
color: string,
|
|
219
|
+
lineWidth: number = 1
|
|
220
|
+
): void {
|
|
221
|
+
ctx.strokeStyle = color;
|
|
222
|
+
ctx.lineWidth = lineWidth;
|
|
223
|
+
ctx.strokeRect(
|
|
224
|
+
Math.floor(rect.x) + 0.5,
|
|
225
|
+
Math.floor(rect.y) + 0.5,
|
|
226
|
+
Math.floor(rect.width),
|
|
227
|
+
Math.floor(rect.height)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Draw selection border around a cell
|
|
233
|
+
*/
|
|
234
|
+
export function drawCellSelectionBorder(
|
|
235
|
+
ctx: CanvasRenderingContext2D,
|
|
236
|
+
x: number,
|
|
237
|
+
y: number,
|
|
238
|
+
width: number,
|
|
239
|
+
height: number,
|
|
240
|
+
color: string = '#1976d2'
|
|
241
|
+
): void {
|
|
242
|
+
ctx.strokeStyle = color;
|
|
243
|
+
ctx.lineWidth = 2;
|
|
244
|
+
ctx.strokeRect(
|
|
245
|
+
Math.floor(x) + 1,
|
|
246
|
+
Math.floor(y) + 1,
|
|
247
|
+
Math.floor(width) - 2,
|
|
248
|
+
Math.floor(height) - 2
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Draw range selection border
|
|
254
|
+
*/
|
|
255
|
+
export function drawRangeSelectionBorder(
|
|
256
|
+
ctx: CanvasRenderingContext2D,
|
|
257
|
+
rect: Rectangle,
|
|
258
|
+
options: {
|
|
259
|
+
color?: string;
|
|
260
|
+
fillColor?: string;
|
|
261
|
+
lineWidth?: number;
|
|
262
|
+
} = {}
|
|
263
|
+
): void {
|
|
264
|
+
const {
|
|
265
|
+
color = '#1976d2',
|
|
266
|
+
fillColor = 'rgba(25, 118, 210, 0.1)',
|
|
267
|
+
lineWidth = 1
|
|
268
|
+
} = options;
|
|
269
|
+
|
|
270
|
+
// Draw fill
|
|
271
|
+
if (fillColor) {
|
|
272
|
+
ctx.fillStyle = fillColor;
|
|
273
|
+
ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Draw border
|
|
277
|
+
ctx.strokeStyle = color;
|
|
278
|
+
ctx.lineWidth = lineWidth;
|
|
279
|
+
ctx.strokeRect(
|
|
280
|
+
Math.floor(rect.x) + 0.5,
|
|
281
|
+
Math.floor(rect.y) + 0.5,
|
|
282
|
+
Math.floor(rect.width),
|
|
283
|
+
Math.floor(rect.height)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// PINNED REGION BORDERS
|
|
289
|
+
// ============================================================================
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Draw shadow/border for pinned regions
|
|
293
|
+
*/
|
|
294
|
+
export function drawPinnedRegionBorders(
|
|
295
|
+
ctx: CanvasRenderingContext2D,
|
|
296
|
+
viewportWidth: number,
|
|
297
|
+
viewportHeight: number,
|
|
298
|
+
leftPinnedWidth: number,
|
|
299
|
+
rightPinnedWidth: number,
|
|
300
|
+
theme: GridTheme
|
|
301
|
+
): void {
|
|
302
|
+
ctx.strokeStyle = theme.headerBorderColor;
|
|
303
|
+
|
|
304
|
+
// Left pinned border
|
|
305
|
+
if (leftPinnedWidth > 0) {
|
|
306
|
+
ctx.beginPath();
|
|
307
|
+
const x = Math.floor(leftPinnedWidth) + 0.5;
|
|
308
|
+
ctx.moveTo(x, 0);
|
|
309
|
+
ctx.lineTo(x, viewportHeight);
|
|
310
|
+
ctx.stroke();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Right pinned border
|
|
314
|
+
if (rightPinnedWidth > 0) {
|
|
315
|
+
ctx.beginPath();
|
|
316
|
+
const x = Math.floor(viewportWidth - rightPinnedWidth) + 0.5;
|
|
317
|
+
ctx.moveTo(x, 0);
|
|
318
|
+
ctx.lineTo(x, viewportHeight);
|
|
319
|
+
ctx.stroke();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Draw pinned region shadows (subtle depth effect)
|
|
325
|
+
*/
|
|
326
|
+
export function drawPinnedRegionShadows(
|
|
327
|
+
ctx: CanvasRenderingContext2D,
|
|
328
|
+
viewportWidth: number,
|
|
329
|
+
viewportHeight: number,
|
|
330
|
+
leftPinnedWidth: number,
|
|
331
|
+
rightPinnedWidth: number
|
|
332
|
+
): void {
|
|
333
|
+
// Left shadow (on the right edge of left pinned)
|
|
334
|
+
if (leftPinnedWidth > 0) {
|
|
335
|
+
const gradient = ctx.createLinearGradient(
|
|
336
|
+
leftPinnedWidth,
|
|
337
|
+
0,
|
|
338
|
+
leftPinnedWidth + 4,
|
|
339
|
+
0
|
|
340
|
+
);
|
|
341
|
+
gradient.addColorStop(0, 'rgba(0, 0, 0, 0.1)');
|
|
342
|
+
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
343
|
+
|
|
344
|
+
ctx.fillStyle = gradient;
|
|
345
|
+
ctx.fillRect(leftPinnedWidth, 0, 4, viewportHeight);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Right shadow (on the left edge of right pinned)
|
|
349
|
+
if (rightPinnedWidth > 0) {
|
|
350
|
+
const shadowX = viewportWidth - rightPinnedWidth;
|
|
351
|
+
const gradient = ctx.createLinearGradient(
|
|
352
|
+
shadowX - 4,
|
|
353
|
+
0,
|
|
354
|
+
shadowX,
|
|
355
|
+
0
|
|
356
|
+
);
|
|
357
|
+
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
|
|
358
|
+
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.1)');
|
|
359
|
+
|
|
360
|
+
ctx.fillStyle = gradient;
|
|
361
|
+
ctx.fillRect(shadowX - 4, 0, 4, viewportHeight);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Theme System
|
|
3
|
+
*
|
|
4
|
+
* Tests for theme definitions, merging, and utilities.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_THEME,
|
|
9
|
+
DARK_THEME,
|
|
10
|
+
THEME_PRESETS,
|
|
11
|
+
mergeTheme,
|
|
12
|
+
getFontFromTheme,
|
|
13
|
+
getRowTheme,
|
|
14
|
+
getCellBackgroundColor,
|
|
15
|
+
getThemePreset,
|
|
16
|
+
createTheme,
|
|
17
|
+
} from './theme';
|
|
18
|
+
import { GridTheme, PartialTheme } from './types';
|
|
19
|
+
|
|
20
|
+
describe('Theme System', () => {
|
|
21
|
+
describe('DEFAULT_THEME', () => {
|
|
22
|
+
it('should have all required properties', () => {
|
|
23
|
+
expect(DEFAULT_THEME.bgCell).toBeDefined();
|
|
24
|
+
expect(DEFAULT_THEME.bgCellEven).toBeDefined();
|
|
25
|
+
expect(DEFAULT_THEME.bgHeader).toBeDefined();
|
|
26
|
+
expect(DEFAULT_THEME.bgSelection).toBeDefined();
|
|
27
|
+
expect(DEFAULT_THEME.textCell).toBeDefined();
|
|
28
|
+
expect(DEFAULT_THEME.textHeader).toBeDefined();
|
|
29
|
+
expect(DEFAULT_THEME.fontFamily).toBeDefined();
|
|
30
|
+
expect(DEFAULT_THEME.fontSize).toBeDefined();
|
|
31
|
+
expect(DEFAULT_THEME.borderColor).toBeDefined();
|
|
32
|
+
expect(DEFAULT_THEME.cellPadding).toBeDefined();
|
|
33
|
+
expect(DEFAULT_THEME.headerHeight).toBeDefined();
|
|
34
|
+
expect(DEFAULT_THEME.rowHeight).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should have valid color values', () => {
|
|
38
|
+
// Colors should be valid hex or rgba
|
|
39
|
+
const colorPattern = /^(#[0-9a-fA-F]{6}|rgba?\([^)]+\))$/;
|
|
40
|
+
expect(DEFAULT_THEME.bgCell).toMatch(colorPattern);
|
|
41
|
+
expect(DEFAULT_THEME.textCell).toMatch(colorPattern);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should have sensible defaults', () => {
|
|
45
|
+
expect(DEFAULT_THEME.fontSize).toBeGreaterThan(10);
|
|
46
|
+
expect(DEFAULT_THEME.fontSize).toBeLessThan(20);
|
|
47
|
+
expect(DEFAULT_THEME.rowHeight).toBeGreaterThanOrEqual(24);
|
|
48
|
+
expect(DEFAULT_THEME.cellPadding).toBeGreaterThan(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('DARK_THEME', () => {
|
|
53
|
+
it('should be a valid partial theme', () => {
|
|
54
|
+
expect(DARK_THEME.bgCell).toBeDefined();
|
|
55
|
+
expect(DARK_THEME.textCell).toBeDefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should have darker background colors', () => {
|
|
59
|
+
// Dark theme should have darker backgrounds than default
|
|
60
|
+
expect(DARK_THEME.bgCell).not.toBe(DEFAULT_THEME.bgCell);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('mergeTheme', () => {
|
|
65
|
+
it('should return base theme when no overrides', () => {
|
|
66
|
+
const result = mergeTheme(DEFAULT_THEME);
|
|
67
|
+
expect(result).toEqual(DEFAULT_THEME);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should override single property', () => {
|
|
71
|
+
const result = mergeTheme(DEFAULT_THEME, { bgCell: '#ff0000' });
|
|
72
|
+
expect(result.bgCell).toBe('#ff0000');
|
|
73
|
+
expect(result.textCell).toBe(DEFAULT_THEME.textCell);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should override multiple properties', () => {
|
|
77
|
+
const result = mergeTheme(DEFAULT_THEME, {
|
|
78
|
+
bgCell: '#ff0000',
|
|
79
|
+
textCell: '#00ff00',
|
|
80
|
+
fontSize: 16,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result.bgCell).toBe('#ff0000');
|
|
84
|
+
expect(result.textCell).toBe('#00ff00');
|
|
85
|
+
expect(result.fontSize).toBe(16);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should apply multiple overrides in order', () => {
|
|
89
|
+
const result = mergeTheme(
|
|
90
|
+
DEFAULT_THEME,
|
|
91
|
+
{ bgCell: '#ff0000' },
|
|
92
|
+
{ bgCell: '#00ff00' }
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(result.bgCell).toBe('#00ff00');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should ignore undefined overrides', () => {
|
|
99
|
+
const result = mergeTheme(DEFAULT_THEME, undefined as any, { bgCell: '#ff0000' });
|
|
100
|
+
expect(result.bgCell).toBe('#ff0000');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should not mutate base theme', () => {
|
|
104
|
+
const original = { ...DEFAULT_THEME };
|
|
105
|
+
mergeTheme(DEFAULT_THEME, { bgCell: '#ff0000' });
|
|
106
|
+
expect(DEFAULT_THEME.bgCell).toBe(original.bgCell);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('getFontFromTheme', () => {
|
|
111
|
+
it('should create font string from theme', () => {
|
|
112
|
+
const font = getFontFromTheme(DEFAULT_THEME);
|
|
113
|
+
expect(font).toContain(`${DEFAULT_THEME.fontSize}px`);
|
|
114
|
+
expect(font).toContain(DEFAULT_THEME.fontFamily);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should include font weight when specified', () => {
|
|
118
|
+
const theme: GridTheme = {
|
|
119
|
+
...DEFAULT_THEME,
|
|
120
|
+
fontWeight: 'bold',
|
|
121
|
+
};
|
|
122
|
+
const font = getFontFromTheme(theme);
|
|
123
|
+
expect(font).toContain('bold');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('getRowTheme', () => {
|
|
128
|
+
it('should return selection theme when selected', () => {
|
|
129
|
+
const result = getRowTheme(DEFAULT_THEME, { isSelected: true });
|
|
130
|
+
expect(result.bgCell).toBe(DEFAULT_THEME.bgSelection);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should return hover theme when hovered', () => {
|
|
134
|
+
const result = getRowTheme(DEFAULT_THEME, { isHovered: true });
|
|
135
|
+
expect(result.bgCell).toBe(DEFAULT_THEME.bgHover);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return group theme for group rows', () => {
|
|
139
|
+
const result = getRowTheme(DEFAULT_THEME, { isGroup: true });
|
|
140
|
+
expect(result.bgCell).toBe(DEFAULT_THEME.bgGroupRow || DEFAULT_THEME.bgHeader);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return even row theme for even rows', () => {
|
|
144
|
+
const result = getRowTheme(DEFAULT_THEME, { isEvenRow: true });
|
|
145
|
+
expect(result.bgCell).toBe(DEFAULT_THEME.bgCellEven);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return default cell color for odd rows', () => {
|
|
149
|
+
const result = getRowTheme(DEFAULT_THEME, { isEvenRow: false });
|
|
150
|
+
expect(result.bgCell).toBe(DEFAULT_THEME.bgCell);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should prioritize selection over other states', () => {
|
|
154
|
+
const result = getRowTheme(DEFAULT_THEME, {
|
|
155
|
+
isSelected: true,
|
|
156
|
+
isHovered: true,
|
|
157
|
+
isEvenRow: true,
|
|
158
|
+
});
|
|
159
|
+
expect(result.bgCell).toBe(DEFAULT_THEME.bgSelection);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('getCellBackgroundColor', () => {
|
|
164
|
+
it('should return selection color when selected', () => {
|
|
165
|
+
const color = getCellBackgroundColor(DEFAULT_THEME, { isSelected: true });
|
|
166
|
+
expect(color).toBe(DEFAULT_THEME.bgSelection);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should return hover color when hovered', () => {
|
|
170
|
+
const color = getCellBackgroundColor(DEFAULT_THEME, { isHovered: true });
|
|
171
|
+
expect(color).toBe(DEFAULT_THEME.bgHover);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should return group color for groups', () => {
|
|
175
|
+
const color = getCellBackgroundColor(DEFAULT_THEME, { isGroup: true });
|
|
176
|
+
expect(color).toBe(DEFAULT_THEME.bgGroupRow || DEFAULT_THEME.bgHeader);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should return even row color for even rows', () => {
|
|
180
|
+
const color = getCellBackgroundColor(DEFAULT_THEME, { isEvenRow: true });
|
|
181
|
+
expect(color).toBe(DEFAULT_THEME.bgCellEven);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should return default color for regular cells', () => {
|
|
185
|
+
const color = getCellBackgroundColor(DEFAULT_THEME, {});
|
|
186
|
+
expect(color).toBe(DEFAULT_THEME.bgCell);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should prioritize selection over hover', () => {
|
|
190
|
+
const color = getCellBackgroundColor(DEFAULT_THEME, {
|
|
191
|
+
isSelected: true,
|
|
192
|
+
isHovered: true,
|
|
193
|
+
});
|
|
194
|
+
expect(color).toBe(DEFAULT_THEME.bgSelection);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('THEME_PRESETS', () => {
|
|
199
|
+
it('should have default preset', () => {
|
|
200
|
+
expect(THEME_PRESETS.default).toBeDefined();
|
|
201
|
+
expect(Object.keys(THEME_PRESETS.default).length).toBe(0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should have dark preset', () => {
|
|
205
|
+
expect(THEME_PRESETS.dark).toBeDefined();
|
|
206
|
+
expect(THEME_PRESETS.dark.bgCell).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should have compact preset', () => {
|
|
210
|
+
expect(THEME_PRESETS.compact).toBeDefined();
|
|
211
|
+
expect(THEME_PRESETS.compact.rowHeight).toBeLessThan(DEFAULT_THEME.rowHeight);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should have comfortable preset', () => {
|
|
215
|
+
expect(THEME_PRESETS.comfortable).toBeDefined();
|
|
216
|
+
expect(THEME_PRESETS.comfortable.rowHeight).toBeGreaterThan(DEFAULT_THEME.rowHeight);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('getThemePreset', () => {
|
|
221
|
+
it('should return preset by name', () => {
|
|
222
|
+
expect(getThemePreset('dark')).toBe(THEME_PRESETS.dark);
|
|
223
|
+
expect(getThemePreset('compact')).toBe(THEME_PRESETS.compact);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should return empty object for unknown preset', () => {
|
|
227
|
+
expect(getThemePreset('nonexistent')).toEqual({});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('createTheme', () => {
|
|
232
|
+
it('should create default theme with no arguments', () => {
|
|
233
|
+
const theme = createTheme();
|
|
234
|
+
expect(theme).toEqual(DEFAULT_THEME);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should create theme from preset', () => {
|
|
238
|
+
const theme = createTheme('dark');
|
|
239
|
+
expect(theme.bgCell).toBe(DARK_THEME.bgCell);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should create theme from preset with overrides', () => {
|
|
243
|
+
const theme = createTheme('dark', { bgCell: '#custom' });
|
|
244
|
+
expect(theme.bgCell).toBe('#custom');
|
|
245
|
+
expect(theme.textCell).toBe(DARK_THEME.textCell);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should create theme with custom overrides only', () => {
|
|
249
|
+
const theme = createTheme('default', { rowHeight: 40 });
|
|
250
|
+
expect(theme.rowHeight).toBe(40);
|
|
251
|
+
expect(theme.bgCell).toBe(DEFAULT_THEME.bgCell);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('Theme consistency', () => {
|
|
256
|
+
it('dark theme should have all colors defined', () => {
|
|
257
|
+
const darkFull = mergeTheme(DEFAULT_THEME, DARK_THEME);
|
|
258
|
+
|
|
259
|
+
expect(darkFull.bgCell).toBeDefined();
|
|
260
|
+
expect(darkFull.bgCellEven).toBeDefined();
|
|
261
|
+
expect(darkFull.bgHeader).toBeDefined();
|
|
262
|
+
expect(darkFull.bgSelection).toBeDefined();
|
|
263
|
+
expect(darkFull.textCell).toBeDefined();
|
|
264
|
+
expect(darkFull.textHeader).toBeDefined();
|
|
265
|
+
expect(darkFull.borderColor).toBeDefined();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('compact theme should have smaller dimensions', () => {
|
|
269
|
+
const compactFull = mergeTheme(DEFAULT_THEME, THEME_PRESETS.compact);
|
|
270
|
+
|
|
271
|
+
expect(compactFull.rowHeight).toBeLessThan(DEFAULT_THEME.rowHeight);
|
|
272
|
+
expect(compactFull.fontSize).toBeLessThan(DEFAULT_THEME.fontSize);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('comfortable theme should have larger dimensions', () => {
|
|
276
|
+
const comfortableFull = mergeTheme(DEFAULT_THEME, THEME_PRESETS.comfortable);
|
|
277
|
+
|
|
278
|
+
expect(comfortableFull.rowHeight).toBeGreaterThan(DEFAULT_THEME.rowHeight);
|
|
279
|
+
expect(comfortableFull.fontSize).toBeGreaterThan(DEFAULT_THEME.fontSize);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|