argent-grid 0.1.0 → 0.3.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 (122) hide show
  1. package/.github/workflows/ci.yml +69 -0
  2. package/.github/workflows/pages.yml +6 -12
  3. package/.storybook/main.ts +20 -0
  4. package/.storybook/preview.ts +18 -0
  5. package/.storybook/tsconfig.json +24 -0
  6. package/AGENTS.md +70 -27
  7. package/README.md +51 -34
  8. package/angular.json +66 -0
  9. package/biome.json +66 -0
  10. package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
  11. package/docs/AG-GRID-COMPARISON.md +725 -0
  12. package/docs/CELL-RENDERER-GUIDE.md +241 -0
  13. package/docs/CONTEXT-MENU-GUIDE.md +371 -0
  14. package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
  15. package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
  16. package/docs/PERFORMANCE-REVIEW.md +571 -0
  17. package/docs/RESEARCH-STATUS.md +234 -0
  18. package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
  19. package/docs/STORYBOOK-REFACTOR.md +215 -0
  20. package/docs/STORYBOOK-STATUS.md +156 -0
  21. package/docs/TEST-COVERAGE-REPORT.md +276 -0
  22. package/docs/THEME-API-GUIDE.md +445 -0
  23. package/docs/THEME-API-PLAN.md +364 -0
  24. package/e2e/advanced.spec.ts +109 -0
  25. package/e2e/argentgrid.spec.ts +65 -0
  26. package/e2e/benchmark.spec.ts +52 -0
  27. package/e2e/cell-renderers.spec.ts +152 -0
  28. package/e2e/debug-streaming.spec.ts +31 -0
  29. package/e2e/dnd.spec.ts +73 -0
  30. package/e2e/screenshots.spec.ts +52 -0
  31. package/e2e/theming.spec.ts +35 -0
  32. package/e2e/visual.spec.ts +112 -0
  33. package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
  34. package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
  35. package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
  36. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  37. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  38. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  39. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  40. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  41. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  42. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  43. package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
  44. package/package.json +21 -7
  45. package/plan.md +56 -28
  46. package/playwright.config.ts +38 -0
  47. package/setup-vitest.ts +10 -13
  48. package/src/lib/argent-grid.module.ts +10 -12
  49. package/src/lib/components/argent-grid.component.css +281 -321
  50. package/src/lib/components/argent-grid.component.html +295 -207
  51. package/src/lib/components/argent-grid.component.spec.ts +120 -160
  52. package/src/lib/components/argent-grid.component.ts +1193 -290
  53. package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
  54. package/src/lib/components/argent-grid.selection.spec.ts +132 -0
  55. package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
  56. package/src/lib/components/set-filter/set-filter.component.ts +307 -0
  57. package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
  58. package/src/lib/directives/click-outside.directive.ts +19 -0
  59. package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
  60. package/src/lib/rendering/canvas-renderer.ts +456 -452
  61. package/src/lib/rendering/live-data-handler.ts +110 -0
  62. package/src/lib/rendering/live-data-optimizations.ts +133 -0
  63. package/src/lib/rendering/render/blit.spec.ts +16 -27
  64. package/src/lib/rendering/render/blit.ts +48 -36
  65. package/src/lib/rendering/render/cells.spec.ts +132 -0
  66. package/src/lib/rendering/render/cells.ts +167 -28
  67. package/src/lib/rendering/render/column-utils.ts +95 -0
  68. package/src/lib/rendering/render/hit-test.ts +50 -0
  69. package/src/lib/rendering/render/index.ts +88 -76
  70. package/src/lib/rendering/render/lines.ts +53 -47
  71. package/src/lib/rendering/render/primitives.ts +423 -0
  72. package/src/lib/rendering/render/theme.spec.ts +8 -12
  73. package/src/lib/rendering/render/theme.ts +7 -10
  74. package/src/lib/rendering/render/types.ts +3 -2
  75. package/src/lib/rendering/render/walk.spec.ts +35 -38
  76. package/src/lib/rendering/render/walk.ts +94 -64
  77. package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
  78. package/src/lib/rendering/utils/damage-tracker.ts +6 -18
  79. package/src/lib/rendering/utils/index.ts +1 -1
  80. package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
  81. package/src/lib/services/grid.service.spec.ts +1241 -201
  82. package/src/lib/services/grid.service.ts +1204 -235
  83. package/src/lib/themes/parts/color-schemes.ts +132 -0
  84. package/src/lib/themes/parts/icon-sets.ts +258 -0
  85. package/src/lib/themes/theme-builder.ts +347 -0
  86. package/src/lib/themes/theme-quartz.ts +72 -0
  87. package/src/lib/themes/types.ts +238 -0
  88. package/src/lib/types/ag-grid-types.ts +573 -14
  89. package/src/public-api.ts +39 -9
  90. package/src/stories/Advanced.stories.ts +249 -0
  91. package/src/stories/ArgentGrid.stories.ts +301 -0
  92. package/src/stories/Benchmark.stories.ts +76 -0
  93. package/src/stories/CellRenderers.stories.ts +395 -0
  94. package/src/stories/Filtering.stories.ts +292 -0
  95. package/src/stories/Grouping.stories.ts +290 -0
  96. package/src/stories/Streaming.stories.ts +57 -0
  97. package/src/stories/Theming.stories.ts +137 -0
  98. package/src/stories/Tooltips.stories.ts +381 -0
  99. package/src/stories/benchmark-wrapper.component.ts +355 -0
  100. package/src/stories/story-utils.ts +88 -0
  101. package/src/stories/streaming-wrapper.component.ts +441 -0
  102. package/tsconfig.json +1 -0
  103. package/tsconfig.storybook.json +10 -0
  104. package/vitest.config.ts +9 -9
  105. package/demo-app/README.md +0 -70
  106. package/demo-app/angular.json +0 -78
  107. package/demo-app/e2e/benchmark.spec.ts +0 -53
  108. package/demo-app/e2e/demo-page.spec.ts +0 -77
  109. package/demo-app/e2e/grid-features.spec.ts +0 -269
  110. package/demo-app/package-lock.json +0 -14023
  111. package/demo-app/package.json +0 -36
  112. package/demo-app/playwright-test-menu.js +0 -19
  113. package/demo-app/playwright.config.ts +0 -23
  114. package/demo-app/src/app/app.component.ts +0 -10
  115. package/demo-app/src/app/app.config.ts +0 -13
  116. package/demo-app/src/app/app.routes.ts +0 -7
  117. package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
  118. package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
  119. package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
  120. package/demo-app/src/index.html +0 -19
  121. package/demo-app/src/main.ts +0 -6
  122. package/demo-app/tsconfig.json +0 -31
@@ -4,7 +4,7 @@
4
4
  * Draws grid lines (borders) efficiently.
5
5
  */
6
6
 
7
- import { Column, IRowNode } from '../../types/ag-grid-types';
7
+ import { Column, GridApi } from '../../types/ag-grid-types';
8
8
  import { GridTheme, Rectangle } from './types';
9
9
 
10
10
  // ============================================================================
@@ -22,9 +22,14 @@ export function drawCrispLine(
22
22
  y2: number
23
23
  ): void {
24
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);
25
+ // For 1px lines, we want the center of the line to be at X.5
26
+ const snapX1 = Math.floor(x1) + 0.5;
27
+ const snapY1 = Math.floor(y1) + 0.5;
28
+ const snapX2 = Math.floor(x2) + 0.5;
29
+ const snapY2 = Math.floor(y2) + 0.5;
30
+
31
+ ctx.moveTo(snapX1, snapY1);
32
+ ctx.lineTo(snapX2, snapY2);
28
33
  ctx.stroke();
29
34
  }
30
35
 
@@ -66,7 +71,8 @@ export function drawRowLines(
66
71
  rowHeight: number,
67
72
  scrollTop: number,
68
73
  viewportWidth: number,
69
- theme: GridTheme
74
+ theme: GridTheme,
75
+ api?: GridApi
70
76
  ): void {
71
77
  ctx.strokeStyle = theme.borderColor || theme.gridLineColor;
72
78
  ctx.lineWidth = 1;
@@ -74,9 +80,12 @@ export function drawRowLines(
74
80
  ctx.beginPath();
75
81
 
76
82
  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);
83
+ const y = Math.floor(api ? api.getRowY(row) - scrollTop : row * rowHeight - scrollTop);
84
+ // Draw border at the bottom of the row (y-0.5) to match DOM border-bottom
85
+ const borderY = y - 0.5;
86
+ if (borderY < 0) continue; // Skip top border if it's outside
87
+ ctx.moveTo(0, borderY);
88
+ ctx.lineTo(viewportWidth, borderY);
80
89
  }
81
90
 
82
91
  ctx.stroke();
@@ -97,7 +106,9 @@ export function drawColumnLines(
97
106
  theme: GridTheme,
98
107
  startRow: number = 0,
99
108
  endRow: number = 0,
100
- rowHeight: number = 32
109
+ rowHeight: number = 32,
110
+ api?: GridApi,
111
+ availableWidth?: number
101
112
  ): void {
102
113
  ctx.strokeStyle = theme.borderColor || theme.gridLineColor;
103
114
  ctx.lineWidth = 1;
@@ -107,17 +118,20 @@ export function drawColumnLines(
107
118
  scrollX,
108
119
  viewportWidth,
109
120
  leftPinnedWidth,
110
- rightPinnedWidth
121
+ rightPinnedWidth,
122
+ availableWidth
111
123
  );
112
124
 
113
125
  // 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));
126
+ const drawY1 = Math.floor(
127
+ api ? api.getRowY(startRow) - scrollTop : startRow * rowHeight - scrollTop
128
+ );
129
+ const drawY2 = Math.floor(api ? api.getRowY(endRow) - scrollTop : endRow * rowHeight - scrollTop);
116
130
 
117
131
  ctx.beginPath();
118
132
 
119
133
  for (const x of columnPositions) {
120
- const borderX = Math.floor(x) + 0.5;
134
+ const borderX = Math.floor(x) - 0.5;
121
135
  ctx.moveTo(borderX, drawY1);
122
136
  ctx.lineTo(borderX, drawY2);
123
137
  }
@@ -133,35 +147,39 @@ export function getColumnBorderPositions(
133
147
  scrollX: number,
134
148
  viewportWidth: number,
135
149
  leftPinnedWidth: number,
136
- rightPinnedWidth: number
150
+ rightPinnedWidth: number,
151
+ availableWidth?: number
137
152
  ): number[] {
138
153
  const positions: number[] = [];
139
154
 
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);
155
+ const leftPinned = columns.filter((c) => c.pinned === 'left');
156
+ const rightPinned = columns.filter((c) => c.pinned === 'right');
157
+ const centerColumns = columns.filter((c) => !c.pinned);
158
+
159
+ const effectiveWidth = availableWidth ?? viewportWidth;
143
160
 
144
161
  // Left pinned column borders
145
162
  let x = 0;
146
163
  for (const col of leftPinned) {
147
- x += col.width;
164
+ x += Math.floor(col.width);
148
165
  positions.push(x);
149
166
  }
150
167
 
151
168
  // Center column borders
152
- x = leftPinnedWidth - scrollX;
169
+ x = Math.floor(leftPinnedWidth) - scrollX;
170
+ const centerEndX = Math.floor(effectiveWidth - rightPinnedWidth);
153
171
  for (const col of centerColumns) {
154
- x += col.width;
155
- // Only include if visible
156
- if (x > leftPinnedWidth && x < viewportWidth - rightPinnedWidth) {
172
+ x += Math.floor(col.width);
173
+ // Only include if visible in the center area
174
+ if (x > leftPinnedWidth && x < centerEndX) {
157
175
  positions.push(x);
158
176
  }
159
177
  }
160
178
 
161
179
  // Right pinned column borders
162
- x = viewportWidth - rightPinnedWidth;
180
+ x = centerEndX;
163
181
  for (const col of rightPinned) {
164
- x += col.width;
182
+ x += Math.floor(col.width);
165
183
  positions.push(x);
166
184
  }
167
185
 
@@ -183,10 +201,11 @@ export function drawGridLines(
183
201
  viewportHeight: number,
184
202
  leftPinnedWidth: number,
185
203
  rightPinnedWidth: number,
186
- theme: GridTheme
204
+ theme: GridTheme,
205
+ api?: GridApi
187
206
  ): void {
188
207
  // Draw horizontal lines
189
- drawRowLines(ctx, startRow, endRow, rowHeight, scrollTop, viewportWidth, theme);
208
+ drawRowLines(ctx, startRow, endRow, rowHeight, scrollTop, viewportWidth, theme, api);
190
209
 
191
210
  // Draw vertical lines
192
211
  drawColumnLines(
@@ -201,7 +220,8 @@ export function drawGridLines(
201
220
  theme,
202
221
  startRow,
203
222
  endRow,
204
- rowHeight
223
+ rowHeight,
224
+ api
205
225
  );
206
226
  }
207
227
 
@@ -261,11 +281,7 @@ export function drawRangeSelectionBorder(
261
281
  lineWidth?: number;
262
282
  } = {}
263
283
  ): void {
264
- const {
265
- color = '#1976d2',
266
- fillColor = 'rgba(25, 118, 210, 0.1)',
267
- lineWidth = 1
268
- } = options;
284
+ const { color = '#1976d2', fillColor = 'rgba(25, 118, 210, 0.1)', lineWidth = 1 } = options;
269
285
 
270
286
  // Draw fill
271
287
  if (fillColor) {
@@ -304,7 +320,7 @@ export function drawPinnedRegionBorders(
304
320
  // Left pinned border
305
321
  if (leftPinnedWidth > 0) {
306
322
  ctx.beginPath();
307
- const x = Math.floor(leftPinnedWidth) + 0.5;
323
+ const x = Math.floor(leftPinnedWidth) - 0.5;
308
324
  ctx.moveTo(x, 0);
309
325
  ctx.lineTo(x, viewportHeight);
310
326
  ctx.stroke();
@@ -313,7 +329,7 @@ export function drawPinnedRegionBorders(
313
329
  // Right pinned border
314
330
  if (rightPinnedWidth > 0) {
315
331
  ctx.beginPath();
316
- const x = Math.floor(viewportWidth - rightPinnedWidth) + 0.5;
332
+ const x = Math.floor(viewportWidth - rightPinnedWidth) - 0.5;
317
333
  ctx.moveTo(x, 0);
318
334
  ctx.lineTo(x, viewportHeight);
319
335
  ctx.stroke();
@@ -332,12 +348,7 @@ export function drawPinnedRegionShadows(
332
348
  ): void {
333
349
  // Left shadow (on the right edge of left pinned)
334
350
  if (leftPinnedWidth > 0) {
335
- const gradient = ctx.createLinearGradient(
336
- leftPinnedWidth,
337
- 0,
338
- leftPinnedWidth + 4,
339
- 0
340
- );
351
+ const gradient = ctx.createLinearGradient(leftPinnedWidth, 0, leftPinnedWidth + 4, 0);
341
352
  gradient.addColorStop(0, 'rgba(0, 0, 0, 0.1)');
342
353
  gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
343
354
 
@@ -348,16 +359,11 @@ export function drawPinnedRegionShadows(
348
359
  // Right shadow (on the left edge of right pinned)
349
360
  if (rightPinnedWidth > 0) {
350
361
  const shadowX = viewportWidth - rightPinnedWidth;
351
- const gradient = ctx.createLinearGradient(
352
- shadowX - 4,
353
- 0,
354
- shadowX,
355
- 0
356
- );
362
+ const gradient = ctx.createLinearGradient(shadowX - 4, 0, shadowX, 0);
357
363
  gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
358
364
  gradient.addColorStop(1, 'rgba(0, 0, 0, 0.1)');
359
365
 
360
366
  ctx.fillStyle = gradient;
361
367
  ctx.fillRect(shadowX - 4, 0, 4, viewportHeight);
362
368
  }
363
- }
369
+ }
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Rendering Primitives for Canvas Renderer
3
+ *
4
+ * Provides specialized drawing functions for grid UI elements:
5
+ * - Checkboxes
6
+ * - Group indicators (expand/collapse)
7
+ * - Sparklines
8
+ */
9
+
10
+ import {
11
+ BadgeOptions,
12
+ ButtonOptions,
13
+ ProgressOptions,
14
+ RatingOptions,
15
+ SparklineOptions,
16
+ } from '../../types/ag-grid-types';
17
+ import { GridTheme } from './types';
18
+
19
+ /**
20
+ * Draw a grid checkbox
21
+ */
22
+ export function drawCheckbox(
23
+ ctx: CanvasRenderingContext2D,
24
+ x: number,
25
+ y: number,
26
+ size: number,
27
+ checked: boolean,
28
+ theme: GridTheme
29
+ ): void {
30
+ // Draw checkbox border - Use a more obvious color than the standard grid border
31
+ ctx.strokeStyle = '#94a3b8'; // Slate-400 for better visibility
32
+ ctx.lineWidth = 1.5;
33
+ ctx.strokeRect(Math.floor(x) + 0.5, Math.floor(y) + 0.5, size, size);
34
+
35
+ // Draw checkmark if checked
36
+ if (checked) {
37
+ // Fill background when checked for even better visibility
38
+ ctx.fillStyle = '#3b82f6'; // Blue-500
39
+ ctx.fillRect(Math.floor(x) + 0.5, Math.floor(y) + 0.5, size, size);
40
+
41
+ ctx.strokeStyle = '#ffffff'; // White checkmark on blue background
42
+ ctx.lineWidth = 2;
43
+ ctx.beginPath();
44
+ const padding = 3;
45
+ const checkX = x + padding;
46
+ const checkY = y + size / 2;
47
+ const checkWidth = size - padding * 2;
48
+
49
+ // Draw checkmark
50
+ ctx.moveTo(checkX, checkY);
51
+ ctx.lineTo(checkX + checkWidth / 3, checkY + checkWidth / 3);
52
+ ctx.lineTo(checkX + checkWidth, checkY - checkWidth / 3);
53
+ ctx.stroke();
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Draw a group expand/collapse indicator
59
+ */
60
+ export function drawGroupIndicator(
61
+ ctx: CanvasRenderingContext2D,
62
+ x: number,
63
+ y: number,
64
+ rowHeight: number,
65
+ expanded: boolean,
66
+ theme: GridTheme
67
+ ): void {
68
+ ctx.beginPath();
69
+ ctx.strokeStyle = theme.textCell;
70
+ ctx.lineWidth = 1;
71
+ const centerY = Math.floor(y + rowHeight / 2);
72
+ const size = theme.groupIndicatorSize;
73
+
74
+ if (expanded) {
75
+ // Expanded: horizontal line
76
+ ctx.moveTo(Math.floor(x), centerY);
77
+ ctx.lineTo(Math.floor(x + size), centerY);
78
+ } else {
79
+ // Collapsed: plus sign
80
+ const halfSize = size / 2;
81
+ ctx.moveTo(Math.floor(x), centerY);
82
+ ctx.lineTo(Math.floor(x + size), centerY);
83
+ ctx.moveTo(Math.floor(x + halfSize), centerY - halfSize);
84
+ ctx.lineTo(Math.floor(x + halfSize), centerY + halfSize);
85
+ }
86
+ ctx.stroke();
87
+ }
88
+
89
+ /**
90
+ * Draw a button within a cell.
91
+ * Returns the bounding box so callers can perform hit-testing.
92
+ */
93
+ export function drawButton<TData = any>(
94
+ ctx: CanvasRenderingContext2D,
95
+ label: string,
96
+ x: number,
97
+ y: number,
98
+ width: number,
99
+ height: number,
100
+ options: ButtonOptions<TData> = { label }
101
+ ): { bx: number; by: number; bw: number; bh: number } {
102
+ const variant = options.variant ?? 'primary';
103
+ const borderRadius = options.borderRadius ?? 4;
104
+ const paddingX = options.paddingX ?? 12;
105
+ const fontSize = options.fontSize ?? 12;
106
+
107
+ // Variant colour defaults
108
+ const VARIANTS: Record<string, { fill: string; text: string; border?: string }> = {
109
+ primary: { fill: '#3b82f6', text: '#ffffff' },
110
+ secondary: { fill: '#f3f4f6', text: '#374151', border: '#9ca3af' },
111
+ danger: { fill: '#ef4444', text: '#ffffff' },
112
+ ghost: { fill: '#f9fafb', text: '#6b7280', border: '#d1d5db' },
113
+ };
114
+ const defaults = VARIANTS[variant] ?? VARIANTS.primary;
115
+ const fillColor = options.fill ?? defaults.fill;
116
+ const textColor = options.textColor ?? defaults.text;
117
+ const borderColor = options.borderColor ?? defaults.border;
118
+
119
+ ctx.save();
120
+ ctx.font = `500 ${fontSize}px sans-serif`;
121
+ const textW = ctx.measureText(label).width;
122
+ const bw = Math.min(textW + paddingX * 2, width - 8);
123
+ const bh = fontSize + 10;
124
+ const bx = Math.floor(x + (width - bw) / 2);
125
+ const by = Math.floor(y + (height - bh) / 2);
126
+
127
+ // Background
128
+ if (fillColor && fillColor !== 'transparent') {
129
+ ctx.fillStyle = fillColor;
130
+ ctx.beginPath();
131
+ ctx.roundRect(bx, by, bw, bh, borderRadius);
132
+ ctx.fill();
133
+ }
134
+
135
+ // Border (secondary / ghost)
136
+ if (borderColor) {
137
+ ctx.strokeStyle = borderColor;
138
+ ctx.lineWidth = 1;
139
+ ctx.beginPath();
140
+ ctx.roundRect(bx + 0.5, by + 0.5, bw - 1, bh - 1, borderRadius);
141
+ ctx.stroke();
142
+ }
143
+
144
+ // Label
145
+ ctx.fillStyle = textColor;
146
+ ctx.textBaseline = 'middle';
147
+ ctx.textAlign = 'center';
148
+ ctx.fillText(label, bx + bw / 2, by + bh / 2);
149
+
150
+ ctx.restore();
151
+ return { bx, by, bw, bh };
152
+ }
153
+
154
+ /**
155
+ * Draw a badge/pill within a cell
156
+ */
157
+ export function drawBadge(
158
+ ctx: CanvasRenderingContext2D,
159
+ value: string,
160
+ x: number,
161
+ y: number,
162
+ width: number,
163
+ height: number,
164
+ options: BadgeOptions = {}
165
+ ): void {
166
+ const colorMap = options.colorMap ?? {};
167
+ const defaultColors = options.defaultColors ?? { fill: '#f3f4f6', text: '#6b7280' };
168
+ const { fill: bgColor, text: textColor } = colorMap[value] ?? defaultColors;
169
+ const borderRadius = options.borderRadius ?? 9999;
170
+ const paddingX = options.paddingX ?? 8;
171
+ const fontSize = options.fontSize ?? 11;
172
+
173
+ ctx.save();
174
+ ctx.font = `500 ${fontSize}px sans-serif`;
175
+ const textWidth = ctx.measureText(value).width;
176
+ const badgeWidth = textWidth + paddingX * 2;
177
+ const badgeHeight = fontSize + 8;
178
+ const bx = Math.floor(x + (width - badgeWidth) / 2);
179
+ const by = Math.floor(y + (height - badgeHeight) / 2);
180
+
181
+ // Draw background pill
182
+ ctx.fillStyle = bgColor;
183
+ ctx.beginPath();
184
+ ctx.roundRect(bx, by, badgeWidth, badgeHeight, Math.min(borderRadius, badgeHeight / 2));
185
+ ctx.fill();
186
+
187
+ // Draw text
188
+ ctx.fillStyle = textColor;
189
+ ctx.textBaseline = 'middle';
190
+ ctx.textAlign = 'center';
191
+ ctx.fillText(value, bx + badgeWidth / 2, by + badgeHeight / 2);
192
+
193
+ ctx.restore();
194
+ }
195
+
196
+ /**
197
+ * Draw a progress bar within a cell
198
+ */
199
+ export function drawProgressBar(
200
+ ctx: CanvasRenderingContext2D,
201
+ value: number,
202
+ x: number,
203
+ y: number,
204
+ width: number,
205
+ height: number,
206
+ options: ProgressOptions = {}
207
+ ): void {
208
+ const min = options.min ?? 0;
209
+ const max = options.max ?? 100;
210
+ const barHeight = options.barHeight ?? 8;
211
+ const borderRadius = options.borderRadius ?? 4;
212
+ const trackColor = options.trackColor ?? '#e5e7eb';
213
+ const showLabel = options.showLabel !== false;
214
+
215
+ const pct = Math.min(1, Math.max(0, (value - min) / (max - min)));
216
+
217
+ // Layout: bar + optional label
218
+ const labelWidth = showLabel ? 40 : 0;
219
+ const labelGap = showLabel ? 8 : 0;
220
+ const trackWidth = width - labelWidth - labelGap - 8; // 8px left padding
221
+ const trackX = x + 4;
222
+ const trackY = Math.floor(y + (height - barHeight) / 2);
223
+
224
+ ctx.save();
225
+
226
+ // Draw track
227
+ ctx.fillStyle = trackColor;
228
+ ctx.beginPath();
229
+ ctx.roundRect(trackX, trackY, trackWidth, barHeight, borderRadius);
230
+ ctx.fill();
231
+
232
+ // Resolve fill color
233
+ let fillColor: string;
234
+ if (typeof options.fill === 'function') {
235
+ fillColor = options.fill(value);
236
+ } else if (options.fill) {
237
+ fillColor = options.fill;
238
+ } else {
239
+ // Default traffic-light coloring
240
+ fillColor = pct >= 0.8 ? '#22c55e' : pct >= 0.6 ? '#eab308' : '#ef4444';
241
+ }
242
+
243
+ // Draw filled portion
244
+ if (pct > 0) {
245
+ ctx.fillStyle = fillColor;
246
+ ctx.beginPath();
247
+ ctx.roundRect(
248
+ trackX,
249
+ trackY,
250
+ Math.max(borderRadius * 2, trackWidth * pct),
251
+ barHeight,
252
+ borderRadius
253
+ );
254
+ ctx.fill();
255
+ }
256
+
257
+ // Draw label
258
+ if (showLabel) {
259
+ const label = options.labelFormatter ? options.labelFormatter(value) : `${value}%`;
260
+ ctx.fillStyle = fillColor;
261
+ ctx.font = 'bold 11px sans-serif';
262
+ ctx.textBaseline = 'middle';
263
+ ctx.textAlign = 'left';
264
+ ctx.fillText(label, trackX + trackWidth + labelGap, y + height / 2);
265
+ }
266
+
267
+ ctx.restore();
268
+ }
269
+
270
+ /**
271
+ * Draw a rating (stars) within a cell
272
+ */
273
+ export function drawRating(
274
+ ctx: CanvasRenderingContext2D,
275
+ value: number,
276
+ x: number,
277
+ y: number,
278
+ width: number,
279
+ height: number,
280
+ options: RatingOptions = {}
281
+ ): void {
282
+ const max = options.max ?? 5;
283
+ const size = options.size ?? 14;
284
+ const color = options.color ?? '#ffb400';
285
+ const emptyColor = options.emptyColor ?? '#e5e7eb';
286
+ const gap = 2;
287
+
288
+ const totalWidth = max * size + (max - 1) * gap;
289
+ const startX = x + (width - totalWidth) / 2;
290
+ const centerY = y + height / 2;
291
+
292
+ ctx.save();
293
+
294
+ for (let i = 0; i < max; i++) {
295
+ const starX = startX + i * (size + gap);
296
+ const isFilled = i < Math.round(value);
297
+
298
+ ctx.fillStyle = isFilled ? color : emptyColor;
299
+ drawStar(ctx, starX + size / 2, centerY, 5, size / 2, size / 4);
300
+ ctx.fill();
301
+ }
302
+
303
+ ctx.restore();
304
+ }
305
+
306
+ /**
307
+ * Helper to draw a star shape
308
+ */
309
+ function drawStar(
310
+ ctx: CanvasRenderingContext2D,
311
+ cx: number,
312
+ cy: number,
313
+ spikes: number,
314
+ outerRadius: number,
315
+ innerRadius: number
316
+ ): void {
317
+ let rot = (Math.PI / 2) * 3;
318
+ let x = cx;
319
+ let y = cy;
320
+ const step = Math.PI / spikes;
321
+
322
+ ctx.beginPath();
323
+ ctx.moveTo(cx, cy - outerRadius);
324
+
325
+ for (let i = 0; i < spikes; i++) {
326
+ x = cx + Math.cos(rot) * outerRadius;
327
+ y = cy + Math.sin(rot) * outerRadius;
328
+ ctx.lineTo(x, y);
329
+ rot += step;
330
+
331
+ x = cx + Math.cos(rot) * innerRadius;
332
+ y = cy + Math.sin(rot) * innerRadius;
333
+ ctx.lineTo(x, y);
334
+ rot += step;
335
+ }
336
+
337
+ ctx.lineTo(cx, cy - outerRadius);
338
+ ctx.closePath();
339
+ }
340
+
341
+ /**
342
+ * Draw a sparkline within a cell
343
+ */
344
+ export function drawSparkline(
345
+ ctx: CanvasRenderingContext2D,
346
+ data: any[],
347
+ x: number,
348
+ y: number,
349
+ width: number,
350
+ height: number,
351
+ options: SparklineOptions
352
+ ): void {
353
+ if (!Array.isArray(data) || data.length === 0) return;
354
+
355
+ const padding = options.padding || { top: 4, bottom: 4, left: 4, right: 4 };
356
+ const drawX = x + (padding.left || 0);
357
+ const drawY = y + (padding.top || 0);
358
+ const drawWidth = width - (padding.left || 0) - (padding.right || 0);
359
+ const drawHeight = height - (padding.top || 0) - (padding.bottom || 0);
360
+
361
+ if (drawWidth <= 0 || drawHeight <= 0) return;
362
+
363
+ const min = Math.min(...data);
364
+ const max = Math.max(...data);
365
+ const range = max - min || 1;
366
+
367
+ const type = options.type || 'line';
368
+
369
+ ctx.save();
370
+
371
+ if (type === 'line' || type === 'area') {
372
+ ctx.beginPath();
373
+ for (let i = 0; i < data.length; i++) {
374
+ const px = drawX + (i / (data.length - 1)) * drawWidth;
375
+ const py = drawY + drawHeight - ((data[i] - min) / range) * drawHeight;
376
+
377
+ if (i === 0) ctx.moveTo(px, py);
378
+ else ctx.lineTo(px, py);
379
+ }
380
+
381
+ if (type === 'area') {
382
+ const areaOptions = options.area || {};
383
+ ctx.lineTo(drawX + drawWidth, drawY + drawHeight);
384
+ ctx.lineTo(drawX, drawY + drawHeight);
385
+ ctx.closePath();
386
+ ctx.fillStyle = areaOptions.fill || 'rgba(33, 150, 243, 0.3)';
387
+ ctx.fill();
388
+
389
+ // Stroke the top line
390
+ ctx.beginPath();
391
+ for (let i = 0; i < data.length; i++) {
392
+ const px = drawX + (i / (data.length - 1)) * drawWidth;
393
+ const py = drawY + drawHeight - ((data[i] - min) / range) * drawHeight;
394
+ if (i === 0) ctx.moveTo(px, py);
395
+ else ctx.lineTo(px, py);
396
+ }
397
+ }
398
+
399
+ const lineOptions = (type === 'area' ? options.area : options.line) || {};
400
+ ctx.strokeStyle = lineOptions.stroke || '#2196f3';
401
+ ctx.lineWidth = lineOptions.strokeWidth || 1.5;
402
+ ctx.lineJoin = 'round';
403
+ ctx.lineCap = 'round';
404
+ ctx.stroke();
405
+ } else if (type === 'column' || type === 'bar') {
406
+ const colOptions = (type === 'bar' ? options.bar || options.column : options.column) || {};
407
+ const colPadding = colOptions.padding || 0.1;
408
+ const colWidth = drawWidth / data.length;
409
+ const barWidth = colWidth * (1 - colPadding);
410
+
411
+ ctx.fillStyle = colOptions.fill || '#2196f3';
412
+
413
+ for (let i = 0; i < data.length; i++) {
414
+ const px = drawX + i * colWidth + (colWidth * colPadding) / 2;
415
+ const valHeight = ((data[i] - min) / range) * drawHeight;
416
+ const py = drawY + drawHeight - valHeight;
417
+
418
+ ctx.fillRect(Math.floor(px), Math.floor(py), Math.floor(barWidth), Math.ceil(valHeight));
419
+ }
420
+ }
421
+
422
+ ctx.restore();
423
+ }
@@ -5,17 +5,17 @@
5
5
  */
6
6
 
7
7
  import {
8
- DEFAULT_THEME,
8
+ createTheme,
9
9
  DARK_THEME,
10
- THEME_PRESETS,
11
- mergeTheme,
10
+ DEFAULT_THEME,
11
+ getCellBackgroundColor,
12
12
  getFontFromTheme,
13
13
  getRowTheme,
14
- getCellBackgroundColor,
15
14
  getThemePreset,
16
- createTheme,
15
+ mergeTheme,
16
+ THEME_PRESETS,
17
17
  } from './theme';
18
- import { GridTheme, PartialTheme } from './types';
18
+ import { GridTheme } from './types';
19
19
 
20
20
  describe('Theme System', () => {
21
21
  describe('DEFAULT_THEME', () => {
@@ -86,11 +86,7 @@ describe('Theme System', () => {
86
86
  });
87
87
 
88
88
  it('should apply multiple overrides in order', () => {
89
- const result = mergeTheme(
90
- DEFAULT_THEME,
91
- { bgCell: '#ff0000' },
92
- { bgCell: '#00ff00' }
93
- );
89
+ const result = mergeTheme(DEFAULT_THEME, { bgCell: '#ff0000' }, { bgCell: '#00ff00' });
94
90
 
95
91
  expect(result.bgCell).toBe('#00ff00');
96
92
  });
@@ -279,4 +275,4 @@ describe('Theme System', () => {
279
275
  expect(comfortableFull.fontSize).toBeGreaterThan(DEFAULT_THEME.fontSize);
280
276
  });
281
277
  });
282
- });
278
+ });