@trustquery/browser 0.3.0 → 0.3.2

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
+ }
package/src/TrustQuery.js CHANGED
@@ -10,6 +10,12 @@ import AutoGrow from './AutoGrow.js';
10
10
  import ValidationStateManager from './ValidationStateManager.js';
11
11
  import MobileKeyboardHandler from './MobileKeyboardHandler.js';
12
12
 
13
+ // Import attachment managers for re-export
14
+ import AttachmentManager from './AttachmentManager.js';
15
+ import AttachmentStyleManager from './AttachmentStyleManager.js';
16
+ import CSVModalManager from './CSVModalManager.js';
17
+ import CSVModalStyleManager from './CSVModalStyleManager.js';
18
+
13
19
  export default class TrustQuery {
14
20
  // Store all instances
15
21
  static instances = new Map();
@@ -550,3 +556,11 @@ export default class TrustQuery {
550
556
  this.render();
551
557
  }
552
558
  }
559
+
560
+ // Export attachment managers as named exports
561
+ export {
562
+ AttachmentManager,
563
+ AttachmentStyleManager,
564
+ CSVModalManager,
565
+ CSVModalStyleManager
566
+ };