@willwade/aac-processors 0.0.5 → 0.0.7

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.
@@ -32,7 +32,6 @@ const path_1 = __importDefault(require("path"));
32
32
  const ExcelJS = __importStar(require("exceljs"));
33
33
  const baseProcessor_1 = require("../core/baseProcessor");
34
34
  const treeStructure_1 = require("../core/treeStructure");
35
- const treeStructure_2 = require("../core/treeStructure");
36
35
  /**
37
36
  * Excel Processor for converting AAC grids to Excel format
38
37
  * Converts AAC tree structures to Excel workbooks with each page as a worksheet
@@ -44,47 +43,18 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
44
43
  * @param filePathOrBuffer - Path to Excel file or Buffer containing Excel data
45
44
  * @returns Array of all text content found in the Excel file
46
45
  */
47
- extractTexts(filePathOrBuffer) {
48
- const texts = [];
49
- try {
50
- const workbook = new ExcelJS.Workbook();
51
- if (Buffer.isBuffer(filePathOrBuffer)) {
52
- // For buffer input, we need to read it differently
53
- // This is a placeholder - actual implementation would need to handle buffer reading
54
- throw new Error('Buffer input not yet implemented for Excel files');
55
- }
56
- else {
57
- // Read from file path
58
- if (!fs_1.default.existsSync(filePathOrBuffer)) {
59
- return texts;
60
- }
61
- // Note: ExcelJS readFile is async, but we need sync for this interface
62
- // This is a limitation that would need to be addressed in a real implementation
63
- throw new Error('Synchronous Excel reading not yet implemented');
64
- }
65
- }
66
- catch (error) {
67
- console.warn(`Failed to extract texts from Excel file: ${error}`);
68
- return texts;
69
- }
46
+ extractTexts(_filePathOrBuffer) {
47
+ console.warn('ExcelProcessor.extractTexts is not implemented yet.');
48
+ return [];
70
49
  }
71
50
  /**
72
51
  * Load Excel file into AACTree structure
73
52
  * @param filePathOrBuffer - Path to Excel file or Buffer containing Excel data
74
53
  * @returns AACTree representation of the Excel file
75
54
  */
76
- loadIntoTree(filePathOrBuffer) {
77
- const tree = new treeStructure_1.AACTree();
78
- try {
79
- // For now, return empty tree as Excel -> AAC conversion is not the primary use case
80
- // This would be implemented if bidirectional conversion is needed
81
- console.warn('Excel to AAC conversion not implemented - returning empty tree');
82
- return tree;
83
- }
84
- catch (error) {
85
- console.warn(`Failed to load Excel file into tree: ${error}`);
86
- return tree;
87
- }
55
+ loadIntoTree(_filePathOrBuffer) {
56
+ console.warn('ExcelProcessor.loadIntoTree is not implemented yet.');
57
+ return new treeStructure_1.AACTree();
88
58
  }
89
59
  /**
90
60
  * Process texts in Excel file (apply translations)
@@ -93,18 +63,13 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
93
63
  * @param outputPath - Path where translated Excel file should be saved
94
64
  * @returns Buffer containing the translated Excel file
95
65
  */
96
- processTexts(filePathOrBuffer, translations, outputPath) {
97
- try {
98
- // Load the Excel file, apply translations, and save
99
- // This would involve reading the Excel file, finding text cells,
100
- // applying translations, and saving to outputPath
101
- throw new Error('Excel text processing not yet implemented');
102
- }
103
- catch (error) {
104
- console.warn(`Failed to process Excel texts: ${error}`);
105
- // Return empty buffer as fallback
106
- return Buffer.alloc(0);
66
+ processTexts(_filePathOrBuffer, _translations, outputPath) {
67
+ console.warn('ExcelProcessor.processTexts is not implemented yet.');
68
+ const outputDir = path_1.default.dirname(outputPath);
69
+ if (!fs_1.default.existsSync(outputDir)) {
70
+ fs_1.default.mkdirSync(outputDir, { recursive: true });
107
71
  }
72
+ return Buffer.alloc(0);
108
73
  }
109
74
  /**
110
75
  * Convert an AAC page to an Excel worksheet
@@ -113,7 +78,7 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
113
78
  * @param tree - Full AAC tree for navigation context
114
79
  * @param usedNames - Set of already used worksheet names to avoid duplicates
115
80
  */
116
- async convertPageToWorksheet(workbook, page, tree, usedNames = new Set()) {
81
+ convertPageToWorksheet(workbook, page, tree, usedNames = new Set()) {
117
82
  // Create worksheet with page name (sanitized for Excel and unique)
118
83
  const worksheetName = this.getUniqueWorksheetName(page.name || page.id, usedNames);
119
84
  const worksheet = workbook.addWorksheet(worksheetName);
@@ -122,16 +87,16 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
122
87
  // Add navigation row if enabled (optional feature)
123
88
  let startRow = 1;
124
89
  if (this.shouldAddNavigationRow()) {
125
- await this.addNavigationRow(worksheet, page, tree);
90
+ this.addNavigationRow(worksheet, page, tree);
126
91
  startRow = 2; // Start content after navigation row
127
92
  }
128
93
  // Convert grid layout if available
129
94
  if (page.grid && page.grid.length > 0) {
130
- await this.convertGridLayout(worksheet, page.grid, startRow);
95
+ this.convertGridLayout(worksheet, page.grid, startRow);
131
96
  }
132
97
  else {
133
98
  // Convert button list to grid layout
134
- await this.convertButtonsToGrid(worksheet, page.buttons, rows, cols, startRow);
99
+ this.convertButtonsToGrid(worksheet, page.buttons, rows, cols, startRow);
135
100
  }
136
101
  // Apply worksheet formatting
137
102
  this.formatWorksheet(worksheet, rows, cols, startRow);
@@ -165,14 +130,14 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
165
130
  * @param grid - 2D array of AAC buttons
166
131
  * @param startRow - Starting row number
167
132
  */
168
- async convertGridLayout(worksheet, grid, startRow) {
133
+ convertGridLayout(worksheet, grid, startRow) {
169
134
  for (let row = 0; row < grid.length; row++) {
170
135
  for (let col = 0; col < grid[row].length; col++) {
171
136
  const button = grid[row][col];
172
137
  if (button) {
173
138
  const excelRow = startRow + row;
174
139
  const excelCol = col + 1; // Excel columns are 1-based
175
- await this.setButtonCell(worksheet, button, excelRow, excelCol);
140
+ this.setButtonCell(worksheet, button, excelRow, excelCol);
176
141
  }
177
142
  }
178
143
  }
@@ -185,7 +150,7 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
185
150
  * @param cols - Number of columns in grid
186
151
  * @param startRow - Starting row number
187
152
  */
188
- async convertButtonsToGrid(worksheet, buttons, rows, cols, startRow) {
153
+ convertButtonsToGrid(worksheet, buttons, rows, cols, startRow) {
189
154
  for (let i = 0; i < buttons.length; i++) {
190
155
  const button = buttons[i];
191
156
  const row = Math.floor(i / cols);
@@ -193,7 +158,7 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
193
158
  if (row < rows) {
194
159
  const excelRow = startRow + row;
195
160
  const excelCol = col + 1; // Excel columns are 1-based
196
- await this.setButtonCell(worksheet, button, excelRow, excelCol);
161
+ this.setButtonCell(worksheet, button, excelRow, excelCol);
197
162
  }
198
163
  }
199
164
  }
@@ -204,7 +169,7 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
204
169
  * @param row - Excel row number
205
170
  * @param col - Excel column number
206
171
  */
207
- async setButtonCell(worksheet, button, row, col) {
172
+ setButtonCell(worksheet, button, row, col) {
208
173
  const cell = worksheet.getCell(row, col);
209
174
  // Set cell value to button label
210
175
  cell.value = button.label || '';
@@ -217,7 +182,7 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
217
182
  this.applyCellStyling(cell, button.style);
218
183
  }
219
184
  // Add navigation link if this is a navigation button
220
- if (button.semanticAction?.intent === treeStructure_2.AACSemanticIntent.NAVIGATE_TO && button.targetPageId) {
185
+ if (button.semanticAction?.intent === treeStructure_1.AACSemanticIntent.NAVIGATE_TO && button.targetPageId) {
221
186
  this.addNavigationLink(cell, button.targetPageId);
222
187
  }
223
188
  // Set cell size for better visibility
@@ -229,14 +194,16 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
229
194
  * @param style - AAC style object
230
195
  */
231
196
  applyCellStyling(cell, style) {
232
- const fill = {};
197
+ let fill;
233
198
  const font = {};
234
- const border = {};
199
+ let border;
235
200
  // Background color
236
201
  if (style.backgroundColor) {
237
- fill.type = 'pattern';
238
- fill.pattern = 'solid';
239
- fill.fgColor = { argb: this.convertColorToArgb(style.backgroundColor) };
202
+ fill = {
203
+ type: 'pattern',
204
+ pattern: 'solid',
205
+ fgColor: { argb: this.convertColorToArgb(style.backgroundColor) },
206
+ };
240
207
  }
241
208
  // Font color
242
209
  if (style.fontColor) {
@@ -263,24 +230,27 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
263
230
  font.underline = true;
264
231
  }
265
232
  // Border
266
- if (style.borderColor || style.borderWidth) {
267
- const borderStyle = style.borderWidth > 1 ? 'thick' : 'thin';
233
+ if (style.borderColor || typeof style.borderWidth === 'number') {
234
+ const borderWidth = style.borderWidth ?? 1;
235
+ const borderStyle = borderWidth > 1 ? 'thick' : 'thin';
268
236
  const borderColor = style.borderColor
269
237
  ? { argb: this.convertColorToArgb(style.borderColor) }
270
238
  : { argb: 'FF000000' }; // Default black
271
- border.top = { style: borderStyle, color: borderColor };
272
- border.left = { style: borderStyle, color: borderColor };
273
- border.bottom = { style: borderStyle, color: borderColor };
274
- border.right = { style: borderStyle, color: borderColor };
239
+ border = {
240
+ top: { style: borderStyle, color: borderColor },
241
+ left: { style: borderStyle, color: borderColor },
242
+ bottom: { style: borderStyle, color: borderColor },
243
+ right: { style: borderStyle, color: borderColor },
244
+ };
275
245
  }
276
246
  // Apply styling to cell
277
- if (Object.keys(fill).length > 0) {
247
+ if (fill) {
278
248
  cell.fill = fill;
279
249
  }
280
250
  if (Object.keys(font).length > 0) {
281
251
  cell.font = font;
282
252
  }
283
- if (Object.keys(border).length > 0) {
253
+ if (border) {
284
254
  cell.border = border;
285
255
  }
286
256
  // Center align text
@@ -369,7 +339,7 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
369
339
  * @param page - Current AAC page
370
340
  * @param tree - Full AAC tree for navigation context
371
341
  */
372
- async addNavigationRow(worksheet, page, tree) {
342
+ addNavigationRow(worksheet, page, tree) {
373
343
  const navButtons = ExcelProcessor.NAVIGATION_BUTTONS;
374
344
  for (let i = 0; i < navButtons.length; i++) {
375
345
  const cell = worksheet.getCell(1, i + 1);
@@ -440,9 +410,9 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
440
410
  // - Max 31 characters
441
411
  // - Cannot contain: \ / ? * [ ] :
442
412
  // - Cannot be empty
443
- const cleaned = (name || '')
444
- .replace(/[\\\/\?\*\[\]:]/g, '_')
445
- .substring(0, 31);
413
+ let cleaned = (name || '').replace(/[\\/?*:]/g, '_');
414
+ cleaned = cleaned.replace(/\[/g, '_').replace(/\]/g, '_');
415
+ cleaned = cleaned.substring(0, 31);
446
416
  if (cleaned.length === 0) {
447
417
  return 'Sheet1';
448
418
  }
@@ -499,11 +469,12 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
499
469
  await this.saveFromTreeAsync(tree, outputPath);
500
470
  }
501
471
  catch (error) {
502
- console.error('Failed to save Excel file:', error);
472
+ const message = error instanceof Error ? error.message : String(error);
473
+ console.error('Failed to save Excel file:', message);
503
474
  try {
504
475
  const fallbackPath = outputPath.replace(/\.xlsx$/i, '_error.txt');
505
476
  fs_1.default.mkdirSync(path_1.default.dirname(fallbackPath), { recursive: true });
506
- fs_1.default.writeFileSync(fallbackPath, `Error saving Excel file: ${error?.message || String(error)}`);
477
+ fs_1.default.writeFileSync(fallbackPath, `Error saving Excel file: ${message}`);
507
478
  }
508
479
  catch (writeError) {
509
480
  console.error('Failed to write Excel error file:', writeError);
@@ -532,7 +503,7 @@ class ExcelProcessor extends baseProcessor_1.BaseProcessor {
532
503
  // Convert each AAC page to an Excel worksheet
533
504
  for (const pageId in tree.pages) {
534
505
  const page = tree.pages[pageId];
535
- await this.convertPageToWorksheet(workbook, page, tree, usedNames);
506
+ this.convertPageToWorksheet(workbook, page, tree, usedNames);
536
507
  }
537
508
  // Save the workbook
538
509
  await workbook.xlsx.writeFile(outputPath);
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Grid3 Color Utilities
3
+ *
4
+ * Comprehensive color handling for Grid3 format, including:
5
+ * - CSS color name lookup (147 named colors)
6
+ * - Color format conversion (hex, RGB, RGBA, named colors)
7
+ * - Color manipulation (darkening, normalization)
8
+ * - Grid3-specific color formatting (8-digit ARGB hex)
9
+ */
10
+ /**
11
+ * Get RGB values for a CSS color name
12
+ * @param name - CSS color name (case-insensitive)
13
+ * @returns RGB tuple [r, g, b] or undefined if not found
14
+ */
15
+ export declare function getNamedColor(name: string): [number, number, number] | undefined;
16
+ /**
17
+ * Convert RGBA values to hex format
18
+ * @param r - Red channel (0-255)
19
+ * @param g - Green channel (0-255)
20
+ * @param b - Blue channel (0-255)
21
+ * @param a - Alpha channel (0-1)
22
+ * @returns Hex color string in format #RRGGBBAA
23
+ */
24
+ export declare function rgbaToHex(r: number, g: number, b: number, a: number): string;
25
+ /**
26
+ * Convert a single color channel value to hex
27
+ * @param value - Channel value (0-255)
28
+ * @returns Two-digit hex string
29
+ */
30
+ export declare function channelToHex(value: number): string;
31
+ /**
32
+ * Clamp RGB channel value to valid range
33
+ * @param value - Channel value
34
+ * @returns Clamped value (0-255)
35
+ */
36
+ export declare function clampColorChannel(value: number): number;
37
+ /**
38
+ * Clamp alpha value to valid range
39
+ * @param value - Alpha value
40
+ * @returns Clamped value (0-1)
41
+ */
42
+ export declare function clampAlpha(value: number): number;
43
+ /**
44
+ * Convert any color format to hex
45
+ * Supports: hex (#RGB, #RRGGBB, #RRGGBBAA), RGB/RGBA, and CSS color names
46
+ * @param value - Color string in any supported format
47
+ * @returns Hex color string (#RRGGBBAA) or undefined if invalid
48
+ */
49
+ export declare function toHexColor(value: string): string | undefined;
50
+ /**
51
+ * Darken a hex color by a specified amount
52
+ * @param hex - Hex color string
53
+ * @param amount - Amount to darken (0-255)
54
+ * @returns Darkened hex color
55
+ */
56
+ export declare function darkenColor(hex: string, amount: number): string;
57
+ /**
58
+ * Normalize any color format to Grid3's 8-digit hex format
59
+ * @param input - Color string in any supported format
60
+ * @param fallback - Fallback color if input is invalid (default: white)
61
+ * @returns Normalized color in format #AARRGGBBFF
62
+ */
63
+ export declare function normalizeColor(input: string, fallback?: string): string;
64
+ /**
65
+ * Ensure a color has an alpha channel (Grid3 format requires 8-digit ARGB)
66
+ * @param color - Color string (hex format)
67
+ * @returns Color with alpha channel in format #AARRGGBBFF
68
+ */
69
+ export declare function ensureAlphaChannel(color: string | undefined): string;
@@ -0,0 +1,331 @@
1
+ "use strict";
2
+ /**
3
+ * Grid3 Color Utilities
4
+ *
5
+ * Comprehensive color handling for Grid3 format, including:
6
+ * - CSS color name lookup (147 named colors)
7
+ * - Color format conversion (hex, RGB, RGBA, named colors)
8
+ * - Color manipulation (darkening, normalization)
9
+ * - Grid3-specific color formatting (8-digit ARGB hex)
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getNamedColor = getNamedColor;
13
+ exports.rgbaToHex = rgbaToHex;
14
+ exports.channelToHex = channelToHex;
15
+ exports.clampColorChannel = clampColorChannel;
16
+ exports.clampAlpha = clampAlpha;
17
+ exports.toHexColor = toHexColor;
18
+ exports.darkenColor = darkenColor;
19
+ exports.normalizeColor = normalizeColor;
20
+ exports.ensureAlphaChannel = ensureAlphaChannel;
21
+ /**
22
+ * CSS color names to RGB values
23
+ * Supports 147 standard CSS color names
24
+ */
25
+ const CSS_COLORS = {
26
+ aliceblue: [240, 248, 255],
27
+ antiquewhite: [250, 235, 215],
28
+ aqua: [0, 255, 255],
29
+ aquamarine: [127, 255, 212],
30
+ azure: [240, 255, 255],
31
+ beige: [245, 245, 220],
32
+ bisque: [255, 228, 196],
33
+ black: [0, 0, 0],
34
+ blanchedalmond: [255, 235, 205],
35
+ blue: [0, 0, 255],
36
+ blueviolet: [138, 43, 226],
37
+ brown: [165, 42, 42],
38
+ burlywood: [222, 184, 135],
39
+ cadetblue: [95, 158, 160],
40
+ chartreuse: [127, 255, 0],
41
+ chocolate: [210, 105, 30],
42
+ coral: [255, 127, 80],
43
+ cornflowerblue: [100, 149, 237],
44
+ cornsilk: [255, 248, 220],
45
+ crimson: [220, 20, 60],
46
+ cyan: [0, 255, 255],
47
+ darkblue: [0, 0, 139],
48
+ darkcyan: [0, 139, 139],
49
+ darkgoldenrod: [184, 134, 11],
50
+ darkgray: [169, 169, 169],
51
+ darkgreen: [0, 100, 0],
52
+ darkgrey: [169, 169, 169],
53
+ darkkhaki: [189, 183, 107],
54
+ darkmagenta: [139, 0, 139],
55
+ darkolivegreen: [85, 107, 47],
56
+ darkorange: [255, 140, 0],
57
+ darkorchid: [153, 50, 204],
58
+ darkred: [139, 0, 0],
59
+ darksalmon: [233, 150, 122],
60
+ darkseagreen: [143, 188, 143],
61
+ darkslateblue: [72, 61, 139],
62
+ darkslategray: [47, 79, 79],
63
+ darkslategrey: [47, 79, 79],
64
+ darkturquoise: [0, 206, 209],
65
+ darkviolet: [148, 0, 211],
66
+ deeppink: [255, 20, 147],
67
+ deepskyblue: [0, 191, 255],
68
+ dimgray: [105, 105, 105],
69
+ dimgrey: [105, 105, 105],
70
+ dodgerblue: [30, 144, 255],
71
+ firebrick: [178, 34, 34],
72
+ floralwhite: [255, 250, 240],
73
+ forestgreen: [34, 139, 34],
74
+ fuchsia: [255, 0, 255],
75
+ gainsboro: [220, 220, 220],
76
+ ghostwhite: [248, 248, 255],
77
+ gold: [255, 215, 0],
78
+ goldenrod: [218, 165, 32],
79
+ gray: [128, 128, 128],
80
+ grey: [128, 128, 128],
81
+ green: [0, 128, 0],
82
+ greenyellow: [173, 255, 47],
83
+ honeydew: [240, 255, 240],
84
+ hotpink: [255, 105, 180],
85
+ indianred: [205, 92, 92],
86
+ indigo: [75, 0, 130],
87
+ ivory: [255, 255, 240],
88
+ khaki: [240, 230, 140],
89
+ lavender: [230, 230, 250],
90
+ lavenderblush: [255, 240, 245],
91
+ lawngreen: [124, 252, 0],
92
+ lemonchiffon: [255, 250, 205],
93
+ lightblue: [173, 216, 230],
94
+ lightcoral: [240, 128, 128],
95
+ lightcyan: [224, 255, 255],
96
+ lightgoldenrodyellow: [250, 250, 210],
97
+ lightgray: [211, 211, 211],
98
+ lightgreen: [144, 238, 144],
99
+ lightgrey: [211, 211, 211],
100
+ lightpink: [255, 182, 193],
101
+ lightsalmon: [255, 160, 122],
102
+ lightseagreen: [32, 178, 170],
103
+ lightskyblue: [135, 206, 250],
104
+ lightslategray: [119, 136, 153],
105
+ lightslategrey: [119, 136, 153],
106
+ lightsteelblue: [176, 196, 222],
107
+ lightyellow: [255, 255, 224],
108
+ lime: [0, 255, 0],
109
+ limegreen: [50, 205, 50],
110
+ linen: [250, 240, 230],
111
+ magenta: [255, 0, 255],
112
+ maroon: [128, 0, 0],
113
+ mediumaquamarine: [102, 205, 170],
114
+ mediumblue: [0, 0, 205],
115
+ mediumorchid: [186, 85, 211],
116
+ mediumpurple: [147, 112, 219],
117
+ mediumseagreen: [60, 179, 113],
118
+ mediumslateblue: [123, 104, 238],
119
+ mediumspringgreen: [0, 250, 154],
120
+ mediumturquoise: [72, 209, 204],
121
+ mediumvioletred: [199, 21, 133],
122
+ midnightblue: [25, 25, 112],
123
+ mintcream: [245, 255, 250],
124
+ mistyrose: [255, 228, 225],
125
+ moccasin: [255, 228, 181],
126
+ navajowhite: [255, 222, 173],
127
+ navy: [0, 0, 128],
128
+ oldlace: [253, 245, 230],
129
+ olive: [128, 128, 0],
130
+ olivedrab: [107, 142, 35],
131
+ orange: [255, 165, 0],
132
+ orangered: [255, 69, 0],
133
+ orchid: [218, 112, 214],
134
+ palegoldenrod: [238, 232, 170],
135
+ palegreen: [152, 251, 152],
136
+ paleturquoise: [175, 238, 238],
137
+ palevioletred: [219, 112, 147],
138
+ papayawhip: [255, 239, 213],
139
+ peachpuff: [255, 218, 185],
140
+ peru: [205, 133, 63],
141
+ pink: [255, 192, 203],
142
+ plum: [221, 160, 221],
143
+ powderblue: [176, 224, 230],
144
+ purple: [128, 0, 128],
145
+ rebeccapurple: [102, 51, 153],
146
+ red: [255, 0, 0],
147
+ rosybrown: [188, 143, 143],
148
+ royalblue: [65, 105, 225],
149
+ saddlebrown: [139, 69, 19],
150
+ salmon: [250, 128, 114],
151
+ sandybrown: [244, 164, 96],
152
+ seagreen: [46, 139, 87],
153
+ seashell: [255, 245, 238],
154
+ sienna: [160, 82, 45],
155
+ silver: [192, 192, 192],
156
+ skyblue: [135, 206, 235],
157
+ slateblue: [106, 90, 205],
158
+ slategray: [112, 128, 144],
159
+ slategrey: [112, 128, 144],
160
+ snow: [255, 250, 250],
161
+ springgreen: [0, 255, 127],
162
+ steelblue: [70, 130, 180],
163
+ tan: [210, 180, 140],
164
+ teal: [0, 128, 128],
165
+ thistle: [216, 191, 216],
166
+ tomato: [255, 99, 71],
167
+ turquoise: [64, 224, 208],
168
+ violet: [238, 130, 238],
169
+ wheat: [245, 222, 179],
170
+ white: [255, 255, 255],
171
+ whitesmoke: [245, 245, 245],
172
+ yellow: [255, 255, 0],
173
+ yellowgreen: [154, 205, 50],
174
+ };
175
+ /**
176
+ * Get RGB values for a CSS color name
177
+ * @param name - CSS color name (case-insensitive)
178
+ * @returns RGB tuple [r, g, b] or undefined if not found
179
+ */
180
+ function getNamedColor(name) {
181
+ const color = CSS_COLORS[name.toLowerCase()];
182
+ return color;
183
+ }
184
+ /**
185
+ * Convert RGBA values to hex format
186
+ * @param r - Red channel (0-255)
187
+ * @param g - Green channel (0-255)
188
+ * @param b - Blue channel (0-255)
189
+ * @param a - Alpha channel (0-1)
190
+ * @returns Hex color string in format #RRGGBBAA
191
+ */
192
+ function rgbaToHex(r, g, b, a) {
193
+ const red = channelToHex(r);
194
+ const green = channelToHex(g);
195
+ const blue = channelToHex(b);
196
+ const alpha = channelToHex(Math.round(a * 255));
197
+ return `#${red}${green}${blue}${alpha}`;
198
+ }
199
+ /**
200
+ * Convert a single color channel value to hex
201
+ * @param value - Channel value (0-255)
202
+ * @returns Two-digit hex string
203
+ */
204
+ function channelToHex(value) {
205
+ const clamped = Math.max(0, Math.min(255, Math.round(value)));
206
+ return clamped.toString(16).padStart(2, '0').toUpperCase();
207
+ }
208
+ /**
209
+ * Clamp RGB channel value to valid range
210
+ * @param value - Channel value
211
+ * @returns Clamped value (0-255)
212
+ */
213
+ function clampColorChannel(value) {
214
+ if (Number.isNaN(value)) {
215
+ return 0;
216
+ }
217
+ return Math.max(0, Math.min(255, value));
218
+ }
219
+ /**
220
+ * Clamp alpha value to valid range
221
+ * @param value - Alpha value
222
+ * @returns Clamped value (0-1)
223
+ */
224
+ function clampAlpha(value) {
225
+ if (Number.isNaN(value)) {
226
+ return 1;
227
+ }
228
+ return Math.max(0, Math.min(1, value));
229
+ }
230
+ /**
231
+ * Convert any color format to hex
232
+ * Supports: hex (#RGB, #RRGGBB, #RRGGBBAA), RGB/RGBA, and CSS color names
233
+ * @param value - Color string in any supported format
234
+ * @returns Hex color string (#RRGGBBAA) or undefined if invalid
235
+ */
236
+ function toHexColor(value) {
237
+ // Try hex format
238
+ const hexMatch = value.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
239
+ if (hexMatch) {
240
+ const hex = hexMatch[1];
241
+ if (hex.length === 3 || hex.length === 4) {
242
+ return `#${hex
243
+ .split('')
244
+ .map((char) => char + char)
245
+ .join('')}`;
246
+ }
247
+ return `#${hex}`;
248
+ }
249
+ // Try RGB/RGBA format
250
+ const rgbMatch = value.match(/^rgba?\((.+)\)$/i);
251
+ if (rgbMatch) {
252
+ const parts = rgbMatch[1]
253
+ .split(',')
254
+ .map((part) => part.trim())
255
+ .filter(Boolean);
256
+ if (parts.length === 3 || parts.length === 4) {
257
+ const [r, g, b, a] = parts;
258
+ const red = clampColorChannel(parseFloat(r));
259
+ const green = clampColorChannel(parseFloat(g));
260
+ const blue = clampColorChannel(parseFloat(b));
261
+ const alpha = parts.length === 4 ? clampAlpha(parseFloat(a)) : 1;
262
+ return rgbaToHex(red, green, blue, alpha);
263
+ }
264
+ }
265
+ // Try CSS color name
266
+ const rgb = getNamedColor(value);
267
+ if (rgb) {
268
+ return rgbaToHex(rgb[0], rgb[1], rgb[2], 1);
269
+ }
270
+ return undefined;
271
+ }
272
+ /**
273
+ * Darken a hex color by a specified amount
274
+ * @param hex - Hex color string
275
+ * @param amount - Amount to darken (0-255)
276
+ * @returns Darkened hex color
277
+ */
278
+ function darkenColor(hex, amount) {
279
+ const normalized = ensureAlphaChannel(hex).substring(1); // strip #
280
+ const rgb = normalized.substring(0, 6);
281
+ const alpha = normalized.substring(6) || 'FF';
282
+ const r = parseInt(rgb.substring(0, 2), 16);
283
+ const g = parseInt(rgb.substring(2, 4), 16);
284
+ const b = parseInt(rgb.substring(4, 6), 16);
285
+ const clamp = (val) => Math.max(0, Math.min(255, val));
286
+ const newR = clamp(r - amount);
287
+ const newG = clamp(g - amount);
288
+ const newB = clamp(b - amount);
289
+ return `#${channelToHex(newR)}${channelToHex(newG)}${channelToHex(newB)}${alpha.toUpperCase()}`;
290
+ }
291
+ /**
292
+ * Normalize any color format to Grid3's 8-digit hex format
293
+ * @param input - Color string in any supported format
294
+ * @param fallback - Fallback color if input is invalid (default: white)
295
+ * @returns Normalized color in format #AARRGGBBFF
296
+ */
297
+ function normalizeColor(input, fallback = '#FFFFFFFF') {
298
+ const trimmed = input.trim();
299
+ if (!trimmed) {
300
+ return fallback;
301
+ }
302
+ const hex = toHexColor(trimmed);
303
+ if (hex) {
304
+ return ensureAlphaChannel(hex).toUpperCase();
305
+ }
306
+ return fallback;
307
+ }
308
+ /**
309
+ * Ensure a color has an alpha channel (Grid3 format requires 8-digit ARGB)
310
+ * @param color - Color string (hex format)
311
+ * @returns Color with alpha channel in format #AARRGGBBFF
312
+ */
313
+ function ensureAlphaChannel(color) {
314
+ if (!color)
315
+ return '#FFFFFFFF';
316
+ // If already 8 digits (with alpha), return as is
317
+ if (color.match(/^#[0-9A-Fa-f]{8}$/))
318
+ return color;
319
+ // If 6 digits (no alpha), add FF for fully opaque
320
+ if (color.match(/^#[0-9A-Fa-f]{6}$/))
321
+ return color + 'FF';
322
+ // If 3 digits (shorthand), expand to 8
323
+ if (color.match(/^#[0-9A-Fa-f]{3}$/)) {
324
+ const r = color[1];
325
+ const g = color[2];
326
+ const b = color[3];
327
+ return `#${r}${r}${g}${g}${b}${b}FF`;
328
+ }
329
+ // Invalid or unknown format, return white
330
+ return '#FFFFFFFF';
331
+ }