@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,402 @@
|
|
|
1
|
+
// Header Renderer - Handles row/column headers and corner cell
|
|
2
|
+
|
|
3
|
+
import type { CanvasTheme, CellPosition } from './types';
|
|
4
|
+
import type { Selection, ColumnFilter } from '../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders row and column headers
|
|
8
|
+
*/
|
|
9
|
+
export class HeaderRenderer {
|
|
10
|
+
private theme: CanvasTheme;
|
|
11
|
+
|
|
12
|
+
constructor(theme: CanvasTheme) {
|
|
13
|
+
this.theme = theme;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert column index to letter label (0 -> A, 1 -> B, ..., 26 -> AA, etc.)
|
|
18
|
+
*/
|
|
19
|
+
columnIndexToLabel(index: number): string {
|
|
20
|
+
let label = '';
|
|
21
|
+
let n = index;
|
|
22
|
+
|
|
23
|
+
while (n >= 0) {
|
|
24
|
+
label = String.fromCharCode(65 + (n % 26)) + label;
|
|
25
|
+
n = Math.floor(n / 26) - 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return label;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a column is in the selection
|
|
33
|
+
*/
|
|
34
|
+
private isColumnSelected(
|
|
35
|
+
col: number,
|
|
36
|
+
selection: Selection | null,
|
|
37
|
+
activeCell: CellPosition | null
|
|
38
|
+
): boolean {
|
|
39
|
+
if (activeCell && activeCell.col === col) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!selection) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const range of selection.ranges) {
|
|
48
|
+
if (col >= range.startCol && col <= range.endCol) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a row is in the selection
|
|
58
|
+
*/
|
|
59
|
+
private isRowSelected(
|
|
60
|
+
row: number,
|
|
61
|
+
selection: Selection | null,
|
|
62
|
+
activeCell: CellPosition | null
|
|
63
|
+
): boolean {
|
|
64
|
+
if (activeCell && activeCell.row === row) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!selection) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const range of selection.ranges) {
|
|
73
|
+
if (row >= range.startRow && row <= range.endRow) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if there are hidden columns immediately before the given column
|
|
83
|
+
*/
|
|
84
|
+
private hasHiddenColumnsBefore(col: number, hiddenCols: Set<number>): boolean {
|
|
85
|
+
// Check if column-1 is hidden
|
|
86
|
+
return col > 0 && hiddenCols.has(col - 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Render column headers
|
|
91
|
+
*/
|
|
92
|
+
renderColumnHeaders(
|
|
93
|
+
ctx: CanvasRenderingContext2D,
|
|
94
|
+
startCol: number,
|
|
95
|
+
endCol: number,
|
|
96
|
+
scrollLeft: number,
|
|
97
|
+
colWidths: Map<number, number>,
|
|
98
|
+
defaultColWidth: number,
|
|
99
|
+
headerWidth: number,
|
|
100
|
+
headerHeight: number,
|
|
101
|
+
selection: Selection | null,
|
|
102
|
+
activeCell: CellPosition | null,
|
|
103
|
+
hiddenCols?: Set<number>,
|
|
104
|
+
skipAccumulationBefore?: number,
|
|
105
|
+
filters?: Map<number, ColumnFilter>
|
|
106
|
+
): void {
|
|
107
|
+
const hidCols = hiddenCols ?? new Set<number>();
|
|
108
|
+
|
|
109
|
+
// Calculate starting x position (skip hidden columns)
|
|
110
|
+
// If skipAccumulationBefore is set, don't accumulate widths before that column
|
|
111
|
+
// (used when rendering scrollable columns after frozen columns)
|
|
112
|
+
let startX = headerWidth;
|
|
113
|
+
const accumulateFrom = skipAccumulationBefore ?? 0;
|
|
114
|
+
for (let c = accumulateFrom; c < startCol; c++) {
|
|
115
|
+
if (!hidCols.has(c)) {
|
|
116
|
+
startX += colWidths.get(c) ?? defaultColWidth;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
startX -= scrollLeft;
|
|
120
|
+
|
|
121
|
+
// Render each column header (skip hidden columns)
|
|
122
|
+
let x = startX;
|
|
123
|
+
for (let col = startCol; col < endCol; col++) {
|
|
124
|
+
// Skip hidden columns
|
|
125
|
+
if (hidCols.has(col)) continue;
|
|
126
|
+
|
|
127
|
+
const width = colWidths.get(col) ?? defaultColWidth;
|
|
128
|
+
const isSelected = this.isColumnSelected(col, selection, activeCell);
|
|
129
|
+
|
|
130
|
+
// Check if there are hidden columns before this one
|
|
131
|
+
const hasHiddenBefore = this.hasHiddenColumnsBefore(col, hidCols);
|
|
132
|
+
|
|
133
|
+
// Background
|
|
134
|
+
ctx.fillStyle = isSelected
|
|
135
|
+
? this.darkenColor(this.theme.headerBackgroundColor, 0.1)
|
|
136
|
+
: this.theme.headerBackgroundColor;
|
|
137
|
+
ctx.fillRect(x, 0, width, headerHeight);
|
|
138
|
+
|
|
139
|
+
// Bottom border
|
|
140
|
+
ctx.strokeStyle = this.theme.headerBorderColor;
|
|
141
|
+
ctx.lineWidth = 1;
|
|
142
|
+
ctx.beginPath();
|
|
143
|
+
ctx.moveTo(x, headerHeight - 0.5);
|
|
144
|
+
ctx.lineTo(x + width, headerHeight - 0.5);
|
|
145
|
+
ctx.stroke();
|
|
146
|
+
|
|
147
|
+
// Right border
|
|
148
|
+
ctx.beginPath();
|
|
149
|
+
ctx.moveTo(x + width - 0.5, 0);
|
|
150
|
+
ctx.lineTo(x + width - 0.5, headerHeight);
|
|
151
|
+
ctx.stroke();
|
|
152
|
+
|
|
153
|
+
// Hidden column indicator (double blue line on left edge)
|
|
154
|
+
if (hasHiddenBefore) {
|
|
155
|
+
ctx.strokeStyle = this.theme.selectionBorderColor;
|
|
156
|
+
ctx.lineWidth = 2;
|
|
157
|
+
// First line
|
|
158
|
+
ctx.beginPath();
|
|
159
|
+
ctx.moveTo(x + 1.5, 2);
|
|
160
|
+
ctx.lineTo(x + 1.5, headerHeight - 2);
|
|
161
|
+
ctx.stroke();
|
|
162
|
+
// Second line (double line effect)
|
|
163
|
+
ctx.beginPath();
|
|
164
|
+
ctx.moveTo(x + 4.5, 2);
|
|
165
|
+
ctx.lineTo(x + 4.5, headerHeight - 2);
|
|
166
|
+
ctx.stroke();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Label
|
|
170
|
+
const label = this.columnIndexToLabel(col);
|
|
171
|
+
ctx.font = `${this.theme.headerFontSize}px ${this.theme.headerFont}`;
|
|
172
|
+
ctx.fillStyle = isSelected
|
|
173
|
+
? this.darkenColor(this.theme.headerTextColor, 0.2)
|
|
174
|
+
: this.theme.headerTextColor;
|
|
175
|
+
ctx.textAlign = 'center';
|
|
176
|
+
ctx.textBaseline = 'middle';
|
|
177
|
+
ctx.fillText(label, x + width / 2, headerHeight / 2);
|
|
178
|
+
|
|
179
|
+
// Filter icon (if column has active filter)
|
|
180
|
+
if (filters?.has(col)) {
|
|
181
|
+
this.renderFilterIcon(ctx, x, 0, width);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
x += width;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if there are hidden rows immediately before the given row
|
|
190
|
+
*/
|
|
191
|
+
private hasHiddenRowsBefore(row: number, hiddenRows: Set<number>): boolean {
|
|
192
|
+
// Check if row-1 is hidden
|
|
193
|
+
return row > 0 && hiddenRows.has(row - 1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Render row headers
|
|
198
|
+
*/
|
|
199
|
+
renderRowHeaders(
|
|
200
|
+
ctx: CanvasRenderingContext2D,
|
|
201
|
+
startRow: number,
|
|
202
|
+
endRow: number,
|
|
203
|
+
scrollTop: number,
|
|
204
|
+
rowHeights: Map<number, number>,
|
|
205
|
+
defaultRowHeight: number,
|
|
206
|
+
headerWidth: number,
|
|
207
|
+
headerHeight: number,
|
|
208
|
+
selection: Selection | null,
|
|
209
|
+
activeCell: CellPosition | null,
|
|
210
|
+
hiddenRows?: Set<number>,
|
|
211
|
+
skipAccumulationBefore?: number
|
|
212
|
+
): void {
|
|
213
|
+
const hidRows = hiddenRows ?? new Set<number>();
|
|
214
|
+
|
|
215
|
+
// Calculate starting y position (skip hidden rows)
|
|
216
|
+
// If skipAccumulationBefore is set, don't accumulate heights before that row
|
|
217
|
+
// (used when rendering scrollable rows after frozen rows)
|
|
218
|
+
let startY = headerHeight;
|
|
219
|
+
const accumulateFrom = skipAccumulationBefore ?? 0;
|
|
220
|
+
for (let r = accumulateFrom; r < startRow; r++) {
|
|
221
|
+
if (!hidRows.has(r)) {
|
|
222
|
+
startY += rowHeights.get(r) ?? defaultRowHeight;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
startY -= scrollTop;
|
|
226
|
+
|
|
227
|
+
// Render each row header (skip hidden rows)
|
|
228
|
+
let y = startY;
|
|
229
|
+
for (let row = startRow; row < endRow; row++) {
|
|
230
|
+
// Skip hidden rows
|
|
231
|
+
if (hidRows.has(row)) continue;
|
|
232
|
+
|
|
233
|
+
const height = rowHeights.get(row) ?? defaultRowHeight;
|
|
234
|
+
const isSelected = this.isRowSelected(row, selection, activeCell);
|
|
235
|
+
|
|
236
|
+
// Check if there are hidden rows before this one
|
|
237
|
+
const hasHiddenBefore = this.hasHiddenRowsBefore(row, hidRows);
|
|
238
|
+
|
|
239
|
+
// Background
|
|
240
|
+
ctx.fillStyle = isSelected
|
|
241
|
+
? this.darkenColor(this.theme.headerBackgroundColor, 0.1)
|
|
242
|
+
: this.theme.headerBackgroundColor;
|
|
243
|
+
ctx.fillRect(0, y, headerWidth, height);
|
|
244
|
+
|
|
245
|
+
// Bottom border
|
|
246
|
+
ctx.strokeStyle = this.theme.headerBorderColor;
|
|
247
|
+
ctx.lineWidth = 1;
|
|
248
|
+
ctx.beginPath();
|
|
249
|
+
ctx.moveTo(0, y + height - 0.5);
|
|
250
|
+
ctx.lineTo(headerWidth, y + height - 0.5);
|
|
251
|
+
ctx.stroke();
|
|
252
|
+
|
|
253
|
+
// Right border
|
|
254
|
+
ctx.beginPath();
|
|
255
|
+
ctx.moveTo(headerWidth - 0.5, y);
|
|
256
|
+
ctx.lineTo(headerWidth - 0.5, y + height);
|
|
257
|
+
ctx.stroke();
|
|
258
|
+
|
|
259
|
+
// Hidden row indicator (double blue line on top edge)
|
|
260
|
+
if (hasHiddenBefore) {
|
|
261
|
+
ctx.strokeStyle = this.theme.selectionBorderColor;
|
|
262
|
+
ctx.lineWidth = 2;
|
|
263
|
+
// First line
|
|
264
|
+
ctx.beginPath();
|
|
265
|
+
ctx.moveTo(2, y + 1.5);
|
|
266
|
+
ctx.lineTo(headerWidth - 2, y + 1.5);
|
|
267
|
+
ctx.stroke();
|
|
268
|
+
// Second line (double line effect)
|
|
269
|
+
ctx.beginPath();
|
|
270
|
+
ctx.moveTo(2, y + 4.5);
|
|
271
|
+
ctx.lineTo(headerWidth - 2, y + 4.5);
|
|
272
|
+
ctx.stroke();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Label (1-indexed for display)
|
|
276
|
+
const label = String(row + 1);
|
|
277
|
+
ctx.font = `${this.theme.headerFontSize}px ${this.theme.headerFont}`;
|
|
278
|
+
ctx.fillStyle = isSelected
|
|
279
|
+
? this.darkenColor(this.theme.headerTextColor, 0.2)
|
|
280
|
+
: this.theme.headerTextColor;
|
|
281
|
+
ctx.textAlign = 'center';
|
|
282
|
+
ctx.textBaseline = 'middle';
|
|
283
|
+
ctx.fillText(label, headerWidth / 2, y + height / 2);
|
|
284
|
+
|
|
285
|
+
y += height;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Render the corner cell (intersection of row and column headers)
|
|
291
|
+
*/
|
|
292
|
+
renderCornerCell(
|
|
293
|
+
ctx: CanvasRenderingContext2D,
|
|
294
|
+
headerWidth: number,
|
|
295
|
+
headerHeight: number
|
|
296
|
+
): void {
|
|
297
|
+
// Background
|
|
298
|
+
ctx.fillStyle = this.theme.headerBackgroundColor;
|
|
299
|
+
ctx.fillRect(0, 0, headerWidth, headerHeight);
|
|
300
|
+
|
|
301
|
+
// Right border
|
|
302
|
+
ctx.strokeStyle = this.theme.headerBorderColor;
|
|
303
|
+
ctx.lineWidth = 1;
|
|
304
|
+
ctx.beginPath();
|
|
305
|
+
ctx.moveTo(headerWidth - 0.5, 0);
|
|
306
|
+
ctx.lineTo(headerWidth - 0.5, headerHeight);
|
|
307
|
+
ctx.stroke();
|
|
308
|
+
|
|
309
|
+
// Bottom border
|
|
310
|
+
ctx.beginPath();
|
|
311
|
+
ctx.moveTo(0, headerHeight - 0.5);
|
|
312
|
+
ctx.lineTo(headerWidth, headerHeight - 0.5);
|
|
313
|
+
ctx.stroke();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Render resize handles for columns
|
|
318
|
+
*/
|
|
319
|
+
renderColumnResizeHandle(
|
|
320
|
+
ctx: CanvasRenderingContext2D,
|
|
321
|
+
x: number,
|
|
322
|
+
headerHeight: number
|
|
323
|
+
): void {
|
|
324
|
+
ctx.strokeStyle = this.theme.selectionBorderColor;
|
|
325
|
+
ctx.lineWidth = 2;
|
|
326
|
+
ctx.beginPath();
|
|
327
|
+
ctx.moveTo(x, 0);
|
|
328
|
+
ctx.lineTo(x, headerHeight);
|
|
329
|
+
ctx.stroke();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Render resize handles for rows
|
|
334
|
+
*/
|
|
335
|
+
renderRowResizeHandle(
|
|
336
|
+
ctx: CanvasRenderingContext2D,
|
|
337
|
+
y: number,
|
|
338
|
+
headerWidth: number
|
|
339
|
+
): void {
|
|
340
|
+
ctx.strokeStyle = this.theme.selectionBorderColor;
|
|
341
|
+
ctx.lineWidth = 2;
|
|
342
|
+
ctx.beginPath();
|
|
343
|
+
ctx.moveTo(0, y);
|
|
344
|
+
ctx.lineTo(headerWidth, y);
|
|
345
|
+
ctx.stroke();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Render filter icon in column header
|
|
350
|
+
*/
|
|
351
|
+
private renderFilterIcon(
|
|
352
|
+
ctx: CanvasRenderingContext2D,
|
|
353
|
+
x: number,
|
|
354
|
+
y: number,
|
|
355
|
+
width: number
|
|
356
|
+
): void {
|
|
357
|
+
const iconSize = 8;
|
|
358
|
+
const iconX = x + width - iconSize - 3; // 3px padding from right edge
|
|
359
|
+
const iconY = y + 3; // 3px padding from top
|
|
360
|
+
|
|
361
|
+
// Draw a small funnel/filter icon
|
|
362
|
+
ctx.fillStyle = this.theme.headerTextColor;
|
|
363
|
+
ctx.beginPath();
|
|
364
|
+
|
|
365
|
+
// Top triangle (wide part of funnel)
|
|
366
|
+
ctx.moveTo(iconX, iconY);
|
|
367
|
+
ctx.lineTo(iconX + iconSize, iconY);
|
|
368
|
+
ctx.lineTo(iconX + iconSize / 2, iconY + iconSize / 2);
|
|
369
|
+
ctx.closePath();
|
|
370
|
+
ctx.fill();
|
|
371
|
+
|
|
372
|
+
// Bottom triangle (narrow part of funnel)
|
|
373
|
+
ctx.beginPath();
|
|
374
|
+
ctx.moveTo(iconX + iconSize / 2 - 1, iconY + iconSize / 2);
|
|
375
|
+
ctx.lineTo(iconX + iconSize / 2 + 1, iconY + iconSize / 2);
|
|
376
|
+
ctx.lineTo(iconX + iconSize / 2, iconY + iconSize);
|
|
377
|
+
ctx.closePath();
|
|
378
|
+
ctx.fill();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Darken a hex color by a percentage
|
|
383
|
+
*/
|
|
384
|
+
private darkenColor(hex: string, percent: number): string {
|
|
385
|
+
// Remove # if present
|
|
386
|
+
hex = hex.replace('#', '');
|
|
387
|
+
|
|
388
|
+
// Parse RGB
|
|
389
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
390
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
391
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
392
|
+
|
|
393
|
+
// Darken
|
|
394
|
+
const newR = Math.max(0, Math.floor(r * (1 - percent)));
|
|
395
|
+
const newG = Math.max(0, Math.floor(g * (1 - percent)));
|
|
396
|
+
const newB = Math.max(0, Math.floor(b * (1 - percent)));
|
|
397
|
+
|
|
398
|
+
// Convert back to hex
|
|
399
|
+
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|