@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustquery/browser",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Turn any textarea into an interactive trigger-based editor with inline styling",
5
5
  "type": "module",
6
6
  "main": "dist/trustquery.js",
@@ -0,0 +1,516 @@
1
+ // AttachmentManager - Handles CSV file attachments with drag & drop
2
+ // Single responsibility: manage attachment state and file operations
3
+
4
+ export default class AttachmentManager {
5
+ /**
6
+ * Create attachment manager
7
+ * @param {Object} options - Configuration options
8
+ */
9
+ constructor(options = {}) {
10
+ this.options = {
11
+ container: options.container || null,
12
+ dropZone: options.dropZone || null,
13
+ styleManager: options.styleManager || null,
14
+ commandScanner: options.commandScanner || null, // For scanning CSV columns
15
+ dropdownManager: options.dropdownManager || null, // For showing dropdown on warning click
16
+ csvModalManager: options.csvModalManager || null, // For displaying CSV content in modal
17
+ onAttachmentAdd: options.onAttachmentAdd || null,
18
+ onAttachmentRemove: options.onAttachmentRemove || null,
19
+ debug: options.debug || false,
20
+ ...options
21
+ };
22
+
23
+ this.attachedFiles = new Map(); // Store attached files
24
+
25
+ if (this.options.debug) {
26
+ console.log('[AttachmentManager] Initialized');
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Initialize drag & drop handlers
32
+ */
33
+ init() {
34
+ if (!this.options.container || !this.options.dropZone) {
35
+ console.warn('[AttachmentManager] Missing container or dropZone');
36
+ return;
37
+ }
38
+
39
+ this.setupDragAndDrop();
40
+
41
+ if (this.options.debug) {
42
+ console.log('[AttachmentManager] Drag & drop initialized');
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Setup drag & drop event handlers
48
+ */
49
+ setupDragAndDrop() {
50
+ const dropZone = this.options.dropZone;
51
+
52
+ dropZone.addEventListener('dragover', this.handleDragOver);
53
+ dropZone.addEventListener('dragleave', this.handleDragLeave);
54
+ dropZone.addEventListener('drop', this.handleDrop);
55
+ }
56
+
57
+ /**
58
+ * Handle drag over event
59
+ */
60
+ handleDragOver = (e) => {
61
+ e.preventDefault();
62
+ e.stopPropagation();
63
+ this.options.dropZone.style.background = '#f0f9ff';
64
+ this.options.dropZone.style.borderColor = '#3b82f6';
65
+ };
66
+
67
+ /**
68
+ * Handle drag leave event
69
+ */
70
+ handleDragLeave = (e) => {
71
+ e.preventDefault();
72
+ e.stopPropagation();
73
+ this.options.dropZone.style.background = '';
74
+ this.options.dropZone.style.borderColor = '';
75
+ };
76
+
77
+ /**
78
+ * Handle drop event
79
+ */
80
+ handleDrop = async (e) => {
81
+ e.preventDefault();
82
+ e.stopPropagation();
83
+ this.options.dropZone.style.background = '';
84
+ this.options.dropZone.style.borderColor = '';
85
+
86
+ const files = Array.from(e.dataTransfer.files);
87
+ for (const file of files) {
88
+ await this.addAttachment(file);
89
+ }
90
+ };
91
+
92
+ /**
93
+ * Parse CSV and extract metadata
94
+ * @param {string} csvText - CSV file content
95
+ * @returns {Object} - { rows, columns, headers }
96
+ */
97
+ parseCSVMetadata(csvText) {
98
+ const lines = csvText.trim().split('\n');
99
+ const rows = lines.length;
100
+
101
+ // Parse first row as headers
102
+ const firstLine = lines[0] || '';
103
+ const headers = firstLine.split(',').map(h => h.trim());
104
+ const columns = headers.length;
105
+
106
+ return { rows, columns, headers };
107
+ }
108
+
109
+ /**
110
+ * Scan CSV headers for trigger matches
111
+ * @param {Array} headers - CSV column headers
112
+ * @returns {Array} - Array of matches
113
+ */
114
+ scanCSVHeaders(headers) {
115
+ if (!this.options.commandScanner) {
116
+ return [];
117
+ }
118
+
119
+ const matches = [];
120
+
121
+ // Get CSV-specific triggers from command map
122
+ const commandMap = this.options.commandScanner.commandMap;
123
+ if (!commandMap || !commandMap['tql-triggers']) {
124
+ return [];
125
+ }
126
+
127
+ // Check warning triggers for csv-match-column type
128
+ const warnings = commandMap['tql-triggers'].warning || [];
129
+ const csvTriggers = warnings.filter(t => t.type === 'csv-match-column');
130
+
131
+ // Check each header against CSV triggers
132
+ headers.forEach((header, index) => {
133
+ csvTriggers.forEach(trigger => {
134
+ if (trigger.match && trigger.match.includes(header)) {
135
+ matches.push({
136
+ header,
137
+ columnIndex: index,
138
+ trigger,
139
+ intent: {
140
+ category: trigger.category,
141
+ handler: trigger.handler
142
+ }
143
+ });
144
+ }
145
+ });
146
+ });
147
+
148
+ return matches;
149
+ }
150
+
151
+ /**
152
+ * Format file size in KB
153
+ * @param {number} bytes - File size in bytes
154
+ * @returns {string} - Formatted size
155
+ */
156
+ formatFileSize(bytes) {
157
+ return (bytes / 1024).toFixed(1) + ' KB';
158
+ }
159
+
160
+ /**
161
+ * Create attachment wrapper with icon placeholder and card
162
+ * @param {File} file - File object
163
+ * @param {string} csvText - CSV file content
164
+ * @param {Object} metadata - { rows, columns, headers }
165
+ * @param {Array} matches - CSV header matches
166
+ * @returns {HTMLElement} - Wrapper element
167
+ */
168
+ createAttachmentCard(file, csvText, metadata, matches = []) {
169
+ // Create wrapper container
170
+ const wrapper = document.createElement('div');
171
+ wrapper.className = 'tq-attachment-wrapper';
172
+
173
+ // Create icon placeholder
174
+ const iconPlaceholder = document.createElement('div');
175
+ iconPlaceholder.className = 'tq-attachment-icon-placeholder';
176
+
177
+ // Add warning/error/info icon if matches found
178
+ let icon = null;
179
+ if (matches.length > 0) {
180
+ const match = matches[0];
181
+ const messageState = match.intent?.handler?.['message-state'] || 'warning';
182
+
183
+ // Map message state to icon filename
184
+ const iconMap = {
185
+ 'error': 'trustquery-error.svg',
186
+ 'warning': 'trustquery-warning.svg',
187
+ 'info': 'trustquery-info.svg'
188
+ };
189
+
190
+ icon = document.createElement('img');
191
+ icon.className = 'tq-attachment-icon';
192
+ icon.src = `./assets/${iconMap[messageState] || iconMap.warning}`;
193
+ icon.title = `CSV column ${messageState} - click to review`;
194
+
195
+ // Handle icon click - show dropdown (using mousedown to prevent double-firing)
196
+ icon.addEventListener('mousedown', (e) => {
197
+ e.preventDefault();
198
+ e.stopPropagation();
199
+ this.handleWarningClick(icon, matches, wrapper, file.name);
200
+ });
201
+
202
+ iconPlaceholder.appendChild(icon);
203
+ }
204
+
205
+ // Create card
206
+ const card = document.createElement('div');
207
+ card.className = 'tq-attachment-card';
208
+
209
+ // Remove button
210
+ const removeBtn = document.createElement('button');
211
+ removeBtn.className = 'tq-attachment-remove';
212
+ removeBtn.innerHTML = '×';
213
+ removeBtn.onclick = () => this.removeAttachment(file.name, wrapper);
214
+
215
+ // File name header
216
+ const fileNameHeader = document.createElement('div');
217
+ fileNameHeader.className = 'tq-attachment-header';
218
+ fileNameHeader.textContent = file.name;
219
+
220
+ // Metadata row
221
+ const metaRow = document.createElement('div');
222
+ metaRow.className = 'tq-attachment-meta';
223
+
224
+ const sizeSpan = document.createElement('span');
225
+ sizeSpan.textContent = this.formatFileSize(file.size);
226
+
227
+ const rowsSpan = document.createElement('span');
228
+ rowsSpan.textContent = `${metadata.rows} rows`;
229
+
230
+ const colsSpan = document.createElement('span');
231
+ colsSpan.textContent = `${metadata.columns} cols`;
232
+
233
+ metaRow.appendChild(sizeSpan);
234
+ metaRow.appendChild(rowsSpan);
235
+ metaRow.appendChild(colsSpan);
236
+
237
+ card.appendChild(removeBtn);
238
+ card.appendChild(fileNameHeader);
239
+ card.appendChild(metaRow);
240
+
241
+ // Apply styles via StyleManager
242
+ if (this.options.styleManager) {
243
+ this.options.styleManager.applyWrapperStyles(wrapper);
244
+ this.options.styleManager.applyIconPlaceholderStyles(iconPlaceholder, matches.length > 0);
245
+ this.options.styleManager.applyIconStyles(icon);
246
+ this.options.styleManager.applyCardStyles(card);
247
+ this.options.styleManager.applyRemoveButtonStyles(removeBtn);
248
+ this.options.styleManager.applyHeaderStyles(fileNameHeader);
249
+ this.options.styleManager.applyMetaStyles(metaRow);
250
+ }
251
+
252
+ // Add click handler to card to open CSV modal
253
+ card.style.cursor = 'pointer';
254
+ card.addEventListener('click', (e) => {
255
+ // Don't open modal if clicking remove button
256
+ if (e.target === removeBtn || removeBtn.contains(e.target)) {
257
+ return;
258
+ }
259
+
260
+ if (this.options.csvModalManager) {
261
+ // Get the latest CSV text from attachedFiles (in case it was updated)
262
+ const attachment = this.attachedFiles.get(file.name);
263
+ const currentText = attachment ? attachment.text : csvText;
264
+ const currentMetadata = attachment ? attachment.metadata : metadata;
265
+
266
+ this.options.csvModalManager.show({
267
+ file,
268
+ text: currentText,
269
+ metadata: currentMetadata
270
+ });
271
+ }
272
+ });
273
+
274
+ // Assemble structure
275
+ wrapper.appendChild(iconPlaceholder);
276
+ wrapper.appendChild(card);
277
+
278
+ return wrapper;
279
+ }
280
+
281
+ /**
282
+ * Update CSV column header in stored text
283
+ * @param {string} fileName - File name
284
+ * @param {number} colIndex - Column index
285
+ * @param {string} appendText - Text to append (e.g., "/PST")
286
+ */
287
+ updateCSVColumnHeader(fileName, colIndex, appendText) {
288
+ const attachment = this.attachedFiles.get(fileName);
289
+ if (!attachment) {
290
+ console.warn('[AttachmentManager] File not found:', fileName);
291
+ return;
292
+ }
293
+
294
+ // Parse CSV and update header
295
+ const lines = attachment.text.split('\n');
296
+ if (lines.length === 0) {
297
+ return;
298
+ }
299
+
300
+ const headers = lines[0].split(',').map(h => h.trim());
301
+ if (colIndex >= headers.length) {
302
+ return;
303
+ }
304
+
305
+ // Remove any existing suffix (text after /)
306
+ const baseHeader = headers[colIndex].split('/')[0];
307
+ headers[colIndex] = baseHeader + appendText;
308
+
309
+ // Update first line
310
+ lines[0] = headers.join(', ');
311
+
312
+ // Update stored text
313
+ attachment.text = lines.join('\n');
314
+
315
+ // Update metadata
316
+ attachment.metadata.headers = headers;
317
+
318
+ // Re-scan headers for matches
319
+ const newMatches = this.scanCSVHeaders(headers);
320
+ attachment.matches = newMatches;
321
+
322
+ // Update icon visibility based on matches
323
+ if (attachment.wrapper) {
324
+ const iconPlaceholder = attachment.wrapper.querySelector('.tq-attachment-icon-placeholder');
325
+ const existingIcon = iconPlaceholder?.querySelector('.tq-attachment-icon');
326
+
327
+ if (newMatches.length === 0 && existingIcon) {
328
+ // No more matches - remove the icon
329
+ existingIcon.remove();
330
+ if (this.options.debug) {
331
+ console.log('[AttachmentManager] Removed warning icon - all warnings resolved');
332
+ }
333
+ } else if (newMatches.length > 0 && !existingIcon) {
334
+ // New matches appeared - add icon (shouldn't happen in normal flow, but handle it)
335
+ const match = newMatches[0];
336
+ const messageState = match.intent?.handler?.['message-state'] || 'warning';
337
+ const iconMap = {
338
+ 'error': 'trustquery-error.svg',
339
+ 'warning': 'trustquery-warning.svg',
340
+ 'info': 'trustquery-info.svg'
341
+ };
342
+
343
+ const icon = document.createElement('img');
344
+ icon.className = 'tq-attachment-icon';
345
+ icon.src = `./assets/${iconMap[messageState] || iconMap.warning}`;
346
+ icon.title = `CSV column ${messageState} - click to review`;
347
+
348
+ // Handle icon click
349
+ icon.addEventListener('mousedown', (e) => {
350
+ e.preventDefault();
351
+ e.stopPropagation();
352
+ this.handleWarningClick(icon, newMatches, attachment.wrapper, fileName);
353
+ });
354
+
355
+ // Apply styles if styleManager exists
356
+ if (this.options.styleManager) {
357
+ this.options.styleManager.applyIconStyles(icon);
358
+ }
359
+
360
+ iconPlaceholder?.appendChild(icon);
361
+
362
+ if (this.options.debug) {
363
+ console.log('[AttachmentManager] Added warning icon - new warnings detected');
364
+ }
365
+ }
366
+ }
367
+
368
+ if (this.options.debug) {
369
+ console.log('[AttachmentManager] Updated CSV column', colIndex, 'to', headers[colIndex]);
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Handle warning icon click - show dropdown
375
+ * @param {HTMLElement} iconEl - Warning icon element
376
+ * @param {Array} matches - CSV header matches
377
+ * @param {HTMLElement} wrapper - Wrapper element
378
+ * @param {string} fileName - File name for tracking
379
+ */
380
+ handleWarningClick(iconEl, matches, wrapper, fileName) {
381
+ if (!this.options.dropdownManager || matches.length === 0) {
382
+ return;
383
+ }
384
+
385
+ // Use the first match (could be enhanced to handle multiple)
386
+ const match = matches[0];
387
+
388
+ // Create match data similar to text matches
389
+ const matchData = {
390
+ text: match.header,
391
+ command: {
392
+ id: `csv-column-${match.columnIndex}`,
393
+ match: match.header,
394
+ matchType: 'csv-column',
395
+ messageState: match.intent.handler['message-state'] || 'warning',
396
+ category: match.intent.category,
397
+ intent: match.intent,
398
+ handler: match.intent.handler
399
+ },
400
+ intent: match.intent,
401
+ // Add context for attachment icon click
402
+ isAttachmentIcon: true,
403
+ fileName: fileName,
404
+ csvColumnIndex: match.columnIndex
405
+ };
406
+
407
+ // Show dropdown using DropdownManager
408
+ this.options.dropdownManager.showDropdown(iconEl, matchData);
409
+
410
+ if (this.options.debug) {
411
+ console.log('[AttachmentManager] Showing dropdown for CSV column:', match.header);
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Add attachment to container
417
+ * @param {File} file - File to attach
418
+ */
419
+ async addAttachment(file) {
420
+ if (!file.name.endsWith('.csv')) {
421
+ alert('Only CSV files are supported');
422
+ return;
423
+ }
424
+
425
+ // Check if already attached
426
+ if (this.attachedFiles.has(file.name)) {
427
+ console.warn('[AttachmentManager] File already attached:', file.name);
428
+ return;
429
+ }
430
+
431
+ // Read file to get metadata
432
+ const text = await file.text();
433
+ const metadata = this.parseCSVMetadata(text);
434
+
435
+ // Scan CSV headers for trigger matches
436
+ const matches = this.scanCSVHeaders(metadata.headers || []);
437
+
438
+ // Create wrapper with card and icon
439
+ const wrapper = this.createAttachmentCard(file, text, metadata, matches);
440
+
441
+ // Store file reference with matches and text
442
+ this.attachedFiles.set(file.name, { file, text, wrapper, metadata, matches });
443
+
444
+ // Add to container
445
+ this.options.container.appendChild(wrapper);
446
+ this.options.container.classList.add('has-attachments');
447
+
448
+ // Trigger callback
449
+ if (this.options.onAttachmentAdd) {
450
+ this.options.onAttachmentAdd({ file, metadata, matches });
451
+ }
452
+
453
+ if (this.options.debug) {
454
+ console.log('[AttachmentManager] Added:', file.name, metadata, 'matches:', matches);
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Remove attachment
460
+ * @param {string} fileName - File name to remove
461
+ * @param {HTMLElement} wrapper - Wrapper element
462
+ */
463
+ removeAttachment(fileName, wrapper) {
464
+ wrapper.remove();
465
+ const attachment = this.attachedFiles.get(fileName);
466
+ this.attachedFiles.delete(fileName);
467
+
468
+ // Remove has-attachments class if no attachments left
469
+ if (this.attachedFiles.size === 0) {
470
+ this.options.container.classList.remove('has-attachments');
471
+ }
472
+
473
+ // Trigger callback
474
+ if (this.options.onAttachmentRemove && attachment) {
475
+ this.options.onAttachmentRemove({ file: attachment.file, metadata: attachment.metadata });
476
+ }
477
+
478
+ if (this.options.debug) {
479
+ console.log('[AttachmentManager] Removed:', fileName);
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Get all attached files
485
+ * @returns {Array} - Array of { file, metadata }
486
+ */
487
+ getAttachments() {
488
+ return Array.from(this.attachedFiles.values());
489
+ }
490
+
491
+ /**
492
+ * Clear all attachments
493
+ */
494
+ clearAll() {
495
+ this.attachedFiles.forEach((attachment, fileName) => {
496
+ this.removeAttachment(fileName, attachment.wrapper);
497
+ });
498
+ }
499
+
500
+ /**
501
+ * Cleanup
502
+ */
503
+ destroy() {
504
+ if (this.options.dropZone) {
505
+ this.options.dropZone.removeEventListener('dragover', this.handleDragOver);
506
+ this.options.dropZone.removeEventListener('dragleave', this.handleDragLeave);
507
+ this.options.dropZone.removeEventListener('drop', this.handleDrop);
508
+ }
509
+
510
+ this.clearAll();
511
+
512
+ if (this.options.debug) {
513
+ console.log('[AttachmentManager] Destroyed');
514
+ }
515
+ }
516
+ }
@@ -0,0 +1,174 @@
1
+ // AttachmentStyleManager - Handles all styling for attachment cards
2
+ // Single responsibility: apply consistent inline styles to attachment UI elements
3
+
4
+ export default class AttachmentStyleManager {
5
+ /**
6
+ * Create attachment style manager
7
+ * @param {Object} options - Style options
8
+ */
9
+ constructor(options = {}) {
10
+ this.options = {
11
+ // Card colors
12
+ cardBackground: options.cardBackground || '#f0f9ff', // Light blue background
13
+ cardBorder: options.cardBorder || '#e2e8f0',
14
+
15
+ // Text colors
16
+ headerColor: options.headerColor || '#1e293b',
17
+ metaColor: options.metaColor || '#64748b',
18
+
19
+ // Remove button colors
20
+ removeBackground: options.removeBackground || '#ef4444',
21
+ removeBackgroundHover: options.removeBackgroundHover || '#dc2626',
22
+
23
+ ...options
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Apply wrapper styles (container for icon + card)
29
+ * @param {HTMLElement} wrapper - Wrapper element
30
+ */
31
+ applyWrapperStyles(wrapper) {
32
+ Object.assign(wrapper.style, {
33
+ display: 'flex',
34
+ alignItems: 'center',
35
+ gap: '8px',
36
+ marginBottom: '6px'
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Apply icon placeholder styles
42
+ * @param {HTMLElement} placeholder - Icon placeholder element
43
+ * @param {boolean} hasIcon - Whether icon is present
44
+ */
45
+ applyIconPlaceholderStyles(placeholder, hasIcon) {
46
+ Object.assign(placeholder.style, {
47
+ width: '24px',
48
+ minWidth: '24px',
49
+ height: '48px',
50
+ display: 'flex',
51
+ alignItems: 'center',
52
+ justifyContent: 'center',
53
+ flexShrink: '0'
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Apply icon styles
59
+ * @param {HTMLElement} icon - Icon element
60
+ */
61
+ applyIconStyles(icon) {
62
+ if (!icon) return;
63
+
64
+ Object.assign(icon.style, {
65
+ width: '20px',
66
+ height: '20px',
67
+ cursor: 'pointer',
68
+ transition: 'transform 0.2s'
69
+ });
70
+
71
+ // Hover effect - slight scale
72
+ icon.addEventListener('mouseenter', () => {
73
+ icon.style.transform = 'scale(1.1)';
74
+ });
75
+
76
+ icon.addEventListener('mouseleave', () => {
77
+ icon.style.transform = 'scale(1)';
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Apply card styles (reduced padding to prevent cutoff)
83
+ * @param {HTMLElement} card - Card element
84
+ */
85
+ applyCardStyles(card) {
86
+ Object.assign(card.style, {
87
+ position: 'relative',
88
+ background: this.options.cardBackground,
89
+ border: `1px solid ${this.options.cardBorder}`,
90
+ borderRadius: '6px',
91
+ padding: '6px 10px',
92
+ display: 'flex',
93
+ flexDirection: 'column',
94
+ gap: '2px',
95
+ boxSizing: 'border-box',
96
+ maxHeight: '48px',
97
+ overflow: 'hidden',
98
+ flex: '1' // Take remaining space in wrapper
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Apply remove button styles (dark icon, no background)
104
+ * @param {HTMLElement} button - Remove button element
105
+ */
106
+ applyRemoveButtonStyles(button) {
107
+ Object.assign(button.style, {
108
+ position: 'absolute',
109
+ top: '4px', // Adjusted for reduced padding
110
+ right: '6px',
111
+ background: 'transparent',
112
+ color: '#64748b',
113
+ border: 'none',
114
+ borderRadius: '3px',
115
+ width: '18px',
116
+ height: '18px',
117
+ fontSize: '18px',
118
+ lineHeight: '1',
119
+ cursor: 'pointer',
120
+ display: 'flex',
121
+ alignItems: 'center',
122
+ justifyContent: 'center',
123
+ transition: 'color 0.2s',
124
+ padding: '0',
125
+ fontWeight: '400'
126
+ });
127
+
128
+ // Hover effect - darker color
129
+ button.addEventListener('mouseenter', () => {
130
+ button.style.color = '#1e293b'; // Darker on hover
131
+ });
132
+
133
+ button.addEventListener('mouseleave', () => {
134
+ button.style.color = '#64748b'; // Back to gray
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Apply header (file name) styles - smaller size
140
+ * @param {HTMLElement} header - Header element
141
+ */
142
+ applyHeaderStyles(header) {
143
+ Object.assign(header.style, {
144
+ fontWeight: '600',
145
+ fontSize: '11px', // Further reduced from 12px to 11px
146
+ color: this.options.headerColor,
147
+ paddingRight: '22px',
148
+ wordBreak: 'break-word',
149
+ lineHeight: '1.2' // Tighter line height
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Apply metadata row styles
155
+ * @param {HTMLElement} metaRow - Metadata row element
156
+ */
157
+ applyMetaStyles(metaRow) {
158
+ Object.assign(metaRow.style, {
159
+ display: 'flex',
160
+ gap: '10px', // Slightly reduced from 12px
161
+ fontSize: '10px', // Reduced from 11px
162
+ color: this.options.metaColor,
163
+ lineHeight: '1.2'
164
+ });
165
+ }
166
+
167
+ /**
168
+ * Update theme colors dynamically
169
+ * @param {Object} newColors - New color options
170
+ */
171
+ updateTheme(newColors) {
172
+ Object.assign(this.options, newColors);
173
+ }
174
+ }