@trustquery/browser 0.2.10 → 0.3.1

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.
@@ -0,0 +1,417 @@
1
+ // CSVModalManager - Handles CSV modal display with trigger matching
2
+ // Single responsibility: manage CSV modal lifecycle and rendering
3
+
4
+ export default class CSVModalManager {
5
+ /**
6
+ * Create CSV modal manager
7
+ * @param {Object} options - Configuration options
8
+ */
9
+ constructor(options = {}) {
10
+ this.options = {
11
+ styleManager: options.styleManager || null,
12
+ commandScanner: options.commandScanner || null,
13
+ dropdownManager: options.dropdownManager || null,
14
+ onCellClick: options.onCellClick || null,
15
+ debug: options.debug || false,
16
+ ...options
17
+ };
18
+
19
+ this.modal = null;
20
+ this.currentCSVData = null;
21
+ this.parsedData = null;
22
+
23
+ if (this.options.debug) {
24
+ console.log('[CSVModalManager] Initialized');
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Parse CSV text into 2D array
30
+ * @param {string} csvText - Raw CSV text
31
+ * @returns {Array} - 2D array of cells
32
+ */
33
+ parseCSV(csvText) {
34
+ const lines = csvText.trim().split('\n');
35
+ return lines.map(line => {
36
+ // Simple CSV parsing (handles basic cases)
37
+ return line.split(',').map(cell => cell.trim());
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Check if a cell value matches any triggers
43
+ * @param {string} cellValue - Cell content
44
+ * @returns {Object|null} - Match data or null
45
+ */
46
+ checkCellForTriggers(cellValue) {
47
+ if (!this.options.commandScanner || !cellValue) {
48
+ return null;
49
+ }
50
+
51
+ const commandMap = this.options.commandScanner.commandMap;
52
+ if (!commandMap || !commandMap['tql-triggers']) {
53
+ return null;
54
+ }
55
+
56
+ // Check all trigger states (error, warning, info)
57
+ const triggers = commandMap['tql-triggers'];
58
+ const allTriggers = [
59
+ ...(triggers.error || []),
60
+ ...(triggers.warning || []),
61
+ ...(triggers.info || [])
62
+ ];
63
+
64
+ // Check each trigger
65
+ for (const trigger of allTriggers) {
66
+ // Handle 'match' type triggers
67
+ if (trigger.type === 'match' && trigger.match) {
68
+ for (const matchText of trigger.match) {
69
+ if (cellValue.toLowerCase() === matchText.toLowerCase()) {
70
+ return {
71
+ text: cellValue,
72
+ trigger,
73
+ matchType: 'exact',
74
+ messageState: trigger.handler?.['message-state'] || 'info'
75
+ };
76
+ }
77
+ }
78
+ }
79
+
80
+ // Handle 'csv-match-column' type triggers (for CSV headers)
81
+ if (trigger.type === 'csv-match-column' && trigger.match) {
82
+ for (const matchText of trigger.match) {
83
+ if (cellValue.toLowerCase() === matchText.toLowerCase()) {
84
+ return {
85
+ text: cellValue,
86
+ trigger,
87
+ matchType: 'csv-column',
88
+ messageState: trigger.handler?.['message-state'] || 'info'
89
+ };
90
+ }
91
+ }
92
+ }
93
+
94
+ // Handle 'regex' type triggers
95
+ if (trigger.type === 'regex' && trigger.regex) {
96
+ for (const pattern of trigger.regex) {
97
+ const regex = new RegExp(pattern);
98
+ if (regex.test(cellValue)) {
99
+ return {
100
+ text: cellValue,
101
+ trigger,
102
+ matchType: 'regex',
103
+ messageState: trigger.handler?.['message-state'] || 'info'
104
+ };
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ /**
114
+ * Create table cell with optional trigger highlighting
115
+ * @param {string} cellValue - Cell content
116
+ * @param {boolean} isHeader - Whether this is a header cell
117
+ * @param {number} rowIndex - Row index
118
+ * @param {number} colIndex - Column index
119
+ * @returns {HTMLElement} - Table cell element
120
+ */
121
+ createCell(cellValue, isHeader, rowIndex, colIndex) {
122
+ const cell = document.createElement(isHeader ? 'th' : 'td');
123
+
124
+ // Check for trigger matches
125
+ const match = this.checkCellForTriggers(cellValue);
126
+
127
+ if (match) {
128
+ // Create highlighted span
129
+ const span = document.createElement('span');
130
+ span.className = 'tq-csv-match';
131
+ span.textContent = cellValue;
132
+ span.setAttribute('data-message-state', match.messageState);
133
+ span.setAttribute('data-row', rowIndex);
134
+ span.setAttribute('data-col', colIndex);
135
+
136
+ // Apply highlighting styles
137
+ if (this.options.styleManager) {
138
+ this.options.styleManager.applyCellMatchStyles(span, match.messageState);
139
+ }
140
+
141
+ // Add click handler to show dropdown
142
+ span.addEventListener('click', (e) => {
143
+ e.stopPropagation();
144
+ this.handleCellMatchClick(span, match, rowIndex, colIndex);
145
+ });
146
+
147
+ cell.appendChild(span);
148
+ } else {
149
+ cell.textContent = cellValue;
150
+ }
151
+
152
+ // Apply cell styles
153
+ if (this.options.styleManager) {
154
+ if (isHeader) {
155
+ this.options.styleManager.applyHeaderCellStyles(cell);
156
+ } else {
157
+ this.options.styleManager.applyDataCellStyles(cell);
158
+ }
159
+ }
160
+
161
+ return cell;
162
+ }
163
+
164
+ /**
165
+ * Handle click on matched cell
166
+ * @param {HTMLElement} cellEl - Cell element
167
+ * @param {Object} match - Match data
168
+ * @param {number} rowIndex - Row index
169
+ * @param {number} colIndex - Column index
170
+ */
171
+ handleCellMatchClick(cellEl, match, rowIndex, colIndex) {
172
+ if (!this.options.dropdownManager) {
173
+ return;
174
+ }
175
+
176
+ // Create match data for dropdown
177
+ const matchData = {
178
+ text: match.text,
179
+ command: {
180
+ id: `csv-cell-${rowIndex}-${colIndex}`,
181
+ matchType: match.matchType,
182
+ messageState: match.messageState,
183
+ category: match.trigger.category,
184
+ intent: {
185
+ category: match.trigger.category,
186
+ handler: match.trigger.handler
187
+ },
188
+ handler: match.trigger.handler
189
+ },
190
+ intent: {
191
+ category: match.trigger.category,
192
+ handler: match.trigger.handler
193
+ },
194
+ // Store cell position for updates
195
+ csvCellPosition: {
196
+ rowIndex,
197
+ colIndex,
198
+ isHeader: rowIndex === 0
199
+ }
200
+ };
201
+
202
+ // Show dropdown
203
+ this.options.dropdownManager.showDropdown(cellEl, matchData);
204
+
205
+ if (this.options.debug) {
206
+ console.log('[CSVModalManager] Showing dropdown for cell:', match.text, 'at', rowIndex, colIndex);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Update column header with selected option
212
+ * @param {number} colIndex - Column index
213
+ * @param {string} appendText - Text to append (e.g., "/PST")
214
+ */
215
+ updateColumnHeader(colIndex, appendText) {
216
+ if (!this.parsedData || !this.parsedData[0]) {
217
+ return;
218
+ }
219
+
220
+ // Update parsed data
221
+ const originalHeader = this.parsedData[0][colIndex];
222
+
223
+ // Remove any existing suffix (text after /)
224
+ const baseHeader = originalHeader.split('/')[0];
225
+ this.parsedData[0][colIndex] = baseHeader + appendText;
226
+
227
+ // Update the displayed table
228
+ this.refreshTable();
229
+
230
+ if (this.options.debug) {
231
+ console.log('[CSVModalManager] Updated column', colIndex, 'from', originalHeader, 'to', this.parsedData[0][colIndex]);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Refresh the table display
237
+ */
238
+ refreshTable() {
239
+ if (!this.modal || !this.parsedData) {
240
+ return;
241
+ }
242
+
243
+ // Find table container
244
+ const tableContainer = this.modal.querySelector('.tq-csv-table-container');
245
+ if (!tableContainer) {
246
+ return;
247
+ }
248
+
249
+ // Clear and rebuild table
250
+ tableContainer.innerHTML = '';
251
+ const table = this.createTable(this.parsedData);
252
+ tableContainer.appendChild(table);
253
+ }
254
+
255
+ /**
256
+ * Create HTML table from parsed CSV data
257
+ * @param {Array} data - 2D array of cells
258
+ * @returns {HTMLElement} - Table element
259
+ */
260
+ createTable(data) {
261
+ const table = document.createElement('table');
262
+ table.className = 'tq-csv-table';
263
+
264
+ // Create header row
265
+ if (data.length > 0) {
266
+ const thead = document.createElement('thead');
267
+ const headerRow = document.createElement('tr');
268
+
269
+ data[0].forEach((cellValue, colIndex) => {
270
+ const cell = this.createCell(cellValue, true, 0, colIndex);
271
+ headerRow.appendChild(cell);
272
+ });
273
+
274
+ thead.appendChild(headerRow);
275
+ table.appendChild(thead);
276
+ }
277
+
278
+ // Create data rows
279
+ if (data.length > 1) {
280
+ const tbody = document.createElement('tbody');
281
+
282
+ for (let i = 1; i < data.length; i++) {
283
+ const row = document.createElement('tr');
284
+
285
+ data[i].forEach((cellValue, colIndex) => {
286
+ const cell = this.createCell(cellValue, false, i, colIndex);
287
+ row.appendChild(cell);
288
+ });
289
+
290
+ tbody.appendChild(row);
291
+ }
292
+
293
+ table.appendChild(tbody);
294
+ }
295
+
296
+ // Apply table styles
297
+ if (this.options.styleManager) {
298
+ this.options.styleManager.applyTableStyles(table);
299
+ }
300
+
301
+ return table;
302
+ }
303
+
304
+ /**
305
+ * Show CSV modal
306
+ * @param {Object} csvData - { file, text, metadata }
307
+ */
308
+ show(csvData) {
309
+ // Close any existing modal
310
+ this.hide();
311
+
312
+ this.currentCSVData = csvData;
313
+ this.parsedData = this.parseCSV(csvData.text);
314
+
315
+ // Create modal backdrop
316
+ const backdrop = document.createElement('div');
317
+ backdrop.className = 'tq-csv-modal-backdrop';
318
+
319
+ // Create modal
320
+ const modal = document.createElement('div');
321
+ modal.className = 'tq-csv-modal';
322
+
323
+ // Create header
324
+ const header = document.createElement('div');
325
+ header.className = 'tq-csv-modal-header';
326
+
327
+ const title = document.createElement('div');
328
+ title.className = 'tq-csv-modal-title';
329
+ title.textContent = csvData.file.name;
330
+
331
+ const closeBtn = document.createElement('button');
332
+ closeBtn.className = 'tq-csv-modal-close';
333
+ closeBtn.innerHTML = '×';
334
+ closeBtn.onclick = () => this.hide();
335
+
336
+ header.appendChild(title);
337
+ header.appendChild(closeBtn);
338
+
339
+ // Create table container (scrollable)
340
+ const tableContainer = document.createElement('div');
341
+ tableContainer.className = 'tq-csv-table-container';
342
+
343
+ // Create table
344
+ const table = this.createTable(this.parsedData);
345
+ tableContainer.appendChild(table);
346
+
347
+ // Assemble modal
348
+ modal.appendChild(header);
349
+ modal.appendChild(tableContainer);
350
+
351
+ // Apply styles
352
+ if (this.options.styleManager) {
353
+ this.options.styleManager.applyBackdropStyles(backdrop);
354
+ this.options.styleManager.applyModalStyles(modal);
355
+ this.options.styleManager.applyHeaderStyles(header);
356
+ this.options.styleManager.applyTitleStyles(title);
357
+ this.options.styleManager.applyCloseButtonStyles(closeBtn);
358
+ this.options.styleManager.applyTableContainerStyles(tableContainer);
359
+ }
360
+
361
+ // Add to document
362
+ backdrop.appendChild(modal);
363
+ document.body.appendChild(backdrop);
364
+
365
+ this.modal = backdrop;
366
+
367
+ // Close on backdrop click
368
+ backdrop.addEventListener('click', (e) => {
369
+ if (e.target === backdrop) {
370
+ this.hide();
371
+ }
372
+ });
373
+
374
+ // Close on Escape key
375
+ this.escapeHandler = (e) => {
376
+ if (e.key === 'Escape') {
377
+ this.hide();
378
+ }
379
+ };
380
+ document.addEventListener('keydown', this.escapeHandler);
381
+
382
+ if (this.options.debug) {
383
+ console.log('[CSVModalManager] Modal shown for:', csvData.file.name);
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Hide modal
389
+ */
390
+ hide() {
391
+ if (this.modal) {
392
+ this.modal.remove();
393
+ this.modal = null;
394
+ this.currentCSVData = null;
395
+ this.parsedData = null;
396
+ }
397
+
398
+ if (this.escapeHandler) {
399
+ document.removeEventListener('keydown', this.escapeHandler);
400
+ this.escapeHandler = null;
401
+ }
402
+
403
+ if (this.options.debug) {
404
+ console.log('[CSVModalManager] Modal hidden');
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Cleanup
410
+ */
411
+ destroy() {
412
+ this.hide();
413
+ if (this.options.debug) {
414
+ console.log('[CSVModalManager] Destroyed');
415
+ }
416
+ }
417
+ }
@@ -0,0 +1,259 @@
1
+ // CSVModalStyleManager - Handles styling for CSV modal
2
+ // Single responsibility: apply consistent styles to CSV modal elements
3
+
4
+ export default class CSVModalStyleManager {
5
+ /**
6
+ * Create CSV modal style manager
7
+ * @param {Object} options - Style options
8
+ */
9
+ constructor(options = {}) {
10
+ this.options = {
11
+ // Modal colors
12
+ backdropColor: options.backdropColor || 'rgba(0, 0, 0, 0.5)',
13
+ modalBackground: options.modalBackground || '#ffffff',
14
+
15
+ // Header colors
16
+ headerBackground: options.headerBackground || '#f8fafc',
17
+ headerBorder: options.headerBorder || '#e2e8f0',
18
+
19
+ // Table colors
20
+ tableBorder: options.tableBorder || '#e2e8f0',
21
+ headerCellBackground: options.headerCellBackground || '#f1f5f9',
22
+ cellBorder: options.cellBorder || '#e2e8f0',
23
+
24
+ // Highlight colors (matching textarea triggers)
25
+ errorHighlight: options.errorHighlight || '#fee2e2',
26
+ errorBorder: options.errorBorder || '#ef4444',
27
+ warningHighlight: options.warningHighlight || '#fef3c7',
28
+ warningBorder: options.warningBorder || '#f59e0b',
29
+ infoHighlight: options.infoHighlight || '#dbeafe',
30
+ infoBorder: options.infoBorder || '#3b82f6',
31
+
32
+ ...options
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Apply backdrop styles
38
+ * @param {HTMLElement} backdrop - Backdrop element
39
+ */
40
+ applyBackdropStyles(backdrop) {
41
+ Object.assign(backdrop.style, {
42
+ position: 'fixed',
43
+ top: '0',
44
+ left: '0',
45
+ width: '100%',
46
+ height: '100%',
47
+ backgroundColor: this.options.backdropColor,
48
+ display: 'flex',
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ zIndex: '10000',
52
+ padding: '20px'
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Apply modal styles
58
+ * @param {HTMLElement} modal - Modal element
59
+ */
60
+ applyModalStyles(modal) {
61
+ Object.assign(modal.style, {
62
+ backgroundColor: this.options.modalBackground,
63
+ borderRadius: '8px',
64
+ boxShadow: '0 10px 40px rgba(0, 0, 0, 0.2)',
65
+ maxWidth: '90vw',
66
+ maxHeight: '90vh',
67
+ width: '100%',
68
+ height: '100%',
69
+ display: 'flex',
70
+ flexDirection: 'column',
71
+ overflow: 'hidden'
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Apply header styles
77
+ * @param {HTMLElement} header - Header element
78
+ */
79
+ applyHeaderStyles(header) {
80
+ Object.assign(header.style, {
81
+ display: 'flex',
82
+ alignItems: 'center',
83
+ justifyContent: 'space-between',
84
+ padding: '16px 20px',
85
+ borderBottom: `1px solid ${this.options.headerBorder}`,
86
+ backgroundColor: this.options.headerBackground,
87
+ flexShrink: '0'
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Apply title styles
93
+ * @param {HTMLElement} title - Title element
94
+ */
95
+ applyTitleStyles(title) {
96
+ Object.assign(title.style, {
97
+ fontSize: '18px',
98
+ fontWeight: '600',
99
+ color: '#1e293b'
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Apply close button styles
105
+ * @param {HTMLElement} button - Close button element
106
+ */
107
+ applyCloseButtonStyles(button) {
108
+ Object.assign(button.style, {
109
+ background: 'transparent',
110
+ border: 'none',
111
+ fontSize: '28px',
112
+ color: '#64748b',
113
+ cursor: 'pointer',
114
+ lineHeight: '1',
115
+ padding: '0',
116
+ width: '32px',
117
+ height: '32px',
118
+ display: 'flex',
119
+ alignItems: 'center',
120
+ justifyContent: 'center',
121
+ borderRadius: '4px',
122
+ transition: 'background-color 0.2s, color 0.2s'
123
+ });
124
+
125
+ button.addEventListener('mouseenter', () => {
126
+ button.style.backgroundColor = '#f1f5f9';
127
+ button.style.color = '#1e293b';
128
+ });
129
+
130
+ button.addEventListener('mouseleave', () => {
131
+ button.style.backgroundColor = 'transparent';
132
+ button.style.color = '#64748b';
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Apply table container styles (scrollable)
138
+ * @param {HTMLElement} container - Table container element
139
+ */
140
+ applyTableContainerStyles(container) {
141
+ Object.assign(container.style, {
142
+ flex: '1',
143
+ overflow: 'auto',
144
+ padding: '20px'
145
+ });
146
+ }
147
+
148
+ /**
149
+ * Apply table styles
150
+ * @param {HTMLElement} table - Table element
151
+ */
152
+ applyTableStyles(table) {
153
+ Object.assign(table.style, {
154
+ borderCollapse: 'collapse',
155
+ width: '100%',
156
+ fontSize: '14px',
157
+ backgroundColor: '#ffffff'
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Apply header cell styles
163
+ * @param {HTMLElement} cell - Header cell element
164
+ */
165
+ applyHeaderCellStyles(cell) {
166
+ Object.assign(cell.style, {
167
+ padding: '12px 16px',
168
+ textAlign: 'left',
169
+ fontWeight: '600',
170
+ backgroundColor: this.options.headerCellBackground,
171
+ border: `1px solid ${this.options.tableBorder}`,
172
+ color: '#1e293b',
173
+ position: 'sticky',
174
+ top: '0',
175
+ zIndex: '10',
176
+ whiteSpace: 'nowrap'
177
+ });
178
+ }
179
+
180
+ /**
181
+ * Apply data cell styles
182
+ * @param {HTMLElement} cell - Data cell element
183
+ */
184
+ applyDataCellStyles(cell) {
185
+ Object.assign(cell.style, {
186
+ padding: '10px 16px',
187
+ border: `1px solid ${this.options.cellBorder}`,
188
+ color: '#334155',
189
+ whiteSpace: 'nowrap'
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Apply match highlighting styles to cell content
195
+ * @param {HTMLElement} span - Span element
196
+ * @param {string} messageState - Message state (error, warning, info)
197
+ */
198
+ applyCellMatchStyles(span, messageState) {
199
+ const colorMap = {
200
+ 'error': {
201
+ background: this.options.errorHighlight,
202
+ border: this.options.errorBorder
203
+ },
204
+ 'warning': {
205
+ background: this.options.warningHighlight,
206
+ border: this.options.warningBorder
207
+ },
208
+ 'info': {
209
+ background: this.options.infoHighlight,
210
+ border: this.options.infoBorder
211
+ }
212
+ };
213
+
214
+ const colors = colorMap[messageState] || colorMap.info;
215
+
216
+ Object.assign(span.style, {
217
+ backgroundColor: colors.background,
218
+ borderBottom: `2px solid ${colors.border}`,
219
+ padding: '2px 4px',
220
+ borderRadius: '3px',
221
+ cursor: 'pointer',
222
+ transition: 'background-color 0.2s',
223
+ display: 'inline-block'
224
+ });
225
+
226
+ // Hover effect
227
+ span.addEventListener('mouseenter', () => {
228
+ span.style.backgroundColor = this.adjustBrightness(colors.background, -10);
229
+ });
230
+
231
+ span.addEventListener('mouseleave', () => {
232
+ span.style.backgroundColor = colors.background;
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Adjust color brightness
238
+ * @param {string} color - Hex color
239
+ * @param {number} amount - Amount to adjust (-255 to 255)
240
+ * @returns {string} - Adjusted color
241
+ */
242
+ adjustBrightness(color, amount) {
243
+ // Simple brightness adjustment for hex colors
244
+ const hex = color.replace('#', '');
245
+ const num = parseInt(hex, 16);
246
+ const r = Math.max(0, Math.min(255, (num >> 16) + amount));
247
+ const g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount));
248
+ const b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount));
249
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
250
+ }
251
+
252
+ /**
253
+ * Update theme colors dynamically
254
+ * @param {Object} newColors - New color options
255
+ */
256
+ updateTheme(newColors) {
257
+ Object.assign(this.options, newColors);
258
+ }
259
+ }
@@ -102,8 +102,9 @@ export default class DropdownManager {
102
102
  // Setup keyboard navigation
103
103
  this.setupKeyboardHandlers();
104
104
 
105
- // Close on click outside
105
+ // Close on click outside (using mousedown to match icon behavior)
106
106
  setTimeout(() => {
107
+ document.addEventListener('mousedown', this.closeDropdownHandler);
107
108
  document.addEventListener('click', this.closeDropdownHandler);
108
109
  }, 0);
109
110
 
@@ -428,6 +429,7 @@ export default class DropdownManager {
428
429
  this.dropdownOptions = null;
429
430
  this.dropdownMatchData = null;
430
431
  this.selectedDropdownIndex = 0;
432
+ document.removeEventListener('mousedown', this.closeDropdownHandler);
431
433
  document.removeEventListener('click', this.closeDropdownHandler);
432
434
  document.removeEventListener('keydown', this.keyboardHandler);
433
435
  }
@@ -437,8 +439,12 @@ export default class DropdownManager {
437
439
  * Close dropdown handler (bound to document)
438
440
  */
439
441
  closeDropdownHandler = (e) => {
440
- // Only close if clicking outside the dropdown
442
+ // Only close if clicking outside the dropdown AND not on the trigger element
441
443
  if (this.activeDropdown && !this.activeDropdown.contains(e.target)) {
444
+ // Check if clicking on the trigger element itself - don't close in that case
445
+ if (this.activeDropdownMatch && (this.activeDropdownMatch === e.target || this.activeDropdownMatch.contains(e.target))) {
446
+ return;
447
+ }
442
448
  this.hideDropdown();
443
449
  }
444
450
  }