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.
Files changed (53) hide show
  1. package/.github/workflows/pages.yml +68 -0
  2. package/AGENTS.md +179 -0
  3. package/README.md +222 -0
  4. package/demo-app/README.md +70 -0
  5. package/demo-app/angular.json +78 -0
  6. package/demo-app/e2e/benchmark.spec.ts +53 -0
  7. package/demo-app/e2e/demo-page.spec.ts +77 -0
  8. package/demo-app/e2e/grid-features.spec.ts +269 -0
  9. package/demo-app/package-lock.json +14023 -0
  10. package/demo-app/package.json +36 -0
  11. package/demo-app/playwright-test-menu.js +19 -0
  12. package/demo-app/playwright.config.ts +23 -0
  13. package/demo-app/src/app/app.component.ts +10 -0
  14. package/demo-app/src/app/app.config.ts +13 -0
  15. package/demo-app/src/app/app.routes.ts +7 -0
  16. package/demo-app/src/app/demo-page/demo-page.component.css +313 -0
  17. package/demo-app/src/app/demo-page/demo-page.component.html +124 -0
  18. package/demo-app/src/app/demo-page/demo-page.component.ts +366 -0
  19. package/demo-app/src/index.html +19 -0
  20. package/demo-app/src/main.ts +6 -0
  21. package/demo-app/tsconfig.json +31 -0
  22. package/ng-package.json +8 -0
  23. package/package.json +60 -0
  24. package/plan.md +131 -0
  25. package/setup-vitest.ts +18 -0
  26. package/src/lib/argent-grid.module.ts +21 -0
  27. package/src/lib/components/argent-grid.component.css +483 -0
  28. package/src/lib/components/argent-grid.component.html +320 -0
  29. package/src/lib/components/argent-grid.component.spec.ts +189 -0
  30. package/src/lib/components/argent-grid.component.ts +1188 -0
  31. package/src/lib/directives/ag-grid-compatibility.directive.ts +92 -0
  32. package/src/lib/rendering/canvas-renderer.ts +962 -0
  33. package/src/lib/rendering/render/blit.spec.ts +453 -0
  34. package/src/lib/rendering/render/blit.ts +393 -0
  35. package/src/lib/rendering/render/cells.ts +369 -0
  36. package/src/lib/rendering/render/index.ts +105 -0
  37. package/src/lib/rendering/render/lines.ts +363 -0
  38. package/src/lib/rendering/render/theme.spec.ts +282 -0
  39. package/src/lib/rendering/render/theme.ts +201 -0
  40. package/src/lib/rendering/render/types.ts +279 -0
  41. package/src/lib/rendering/render/walk.spec.ts +360 -0
  42. package/src/lib/rendering/render/walk.ts +360 -0
  43. package/src/lib/rendering/utils/damage-tracker.spec.ts +444 -0
  44. package/src/lib/rendering/utils/damage-tracker.ts +423 -0
  45. package/src/lib/rendering/utils/index.ts +7 -0
  46. package/src/lib/services/grid.service.spec.ts +1039 -0
  47. package/src/lib/services/grid.service.ts +1284 -0
  48. package/src/lib/types/ag-grid-types.ts +970 -0
  49. package/src/public-api.ts +22 -0
  50. package/tsconfig.json +32 -0
  51. package/tsconfig.lib.json +11 -0
  52. package/tsconfig.spec.json +8 -0
  53. 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
+ });