@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.
@@ -3249,6 +3249,1376 @@ class MobileKeyboardHandler {
3249
3249
  }
3250
3250
  }
3251
3251
 
3252
+ // AttachmentManager - Handles CSV file attachments with drag & drop
3253
+ // Single responsibility: manage attachment state and file operations
3254
+
3255
+ class AttachmentManager {
3256
+ /**
3257
+ * Create attachment manager
3258
+ * @param {Object} options - Configuration options
3259
+ */
3260
+ constructor(options = {}) {
3261
+ this.options = {
3262
+ container: options.container || null,
3263
+ dropZone: options.dropZone || null,
3264
+ styleManager: options.styleManager || null,
3265
+ commandScanner: options.commandScanner || null, // For scanning CSV columns
3266
+ dropdownManager: options.dropdownManager || null, // For showing dropdown on warning click
3267
+ csvModalManager: options.csvModalManager || null, // For displaying CSV content in modal
3268
+ onAttachmentAdd: options.onAttachmentAdd || null,
3269
+ onAttachmentRemove: options.onAttachmentRemove || null,
3270
+ debug: options.debug || false,
3271
+ ...options
3272
+ };
3273
+
3274
+ this.attachedFiles = new Map(); // Store attached files
3275
+
3276
+ if (this.options.debug) {
3277
+ console.log('[AttachmentManager] Initialized');
3278
+ }
3279
+ }
3280
+
3281
+ /**
3282
+ * Initialize drag & drop handlers
3283
+ */
3284
+ init() {
3285
+ if (!this.options.container || !this.options.dropZone) {
3286
+ console.warn('[AttachmentManager] Missing container or dropZone');
3287
+ return;
3288
+ }
3289
+
3290
+ this.setupDragAndDrop();
3291
+
3292
+ if (this.options.debug) {
3293
+ console.log('[AttachmentManager] Drag & drop initialized');
3294
+ }
3295
+ }
3296
+
3297
+ /**
3298
+ * Setup drag & drop event handlers
3299
+ */
3300
+ setupDragAndDrop() {
3301
+ const dropZone = this.options.dropZone;
3302
+
3303
+ dropZone.addEventListener('dragover', this.handleDragOver);
3304
+ dropZone.addEventListener('dragleave', this.handleDragLeave);
3305
+ dropZone.addEventListener('drop', this.handleDrop);
3306
+ }
3307
+
3308
+ /**
3309
+ * Handle drag over event
3310
+ */
3311
+ handleDragOver = (e) => {
3312
+ e.preventDefault();
3313
+ e.stopPropagation();
3314
+ this.options.dropZone.style.background = '#f0f9ff';
3315
+ this.options.dropZone.style.borderColor = '#3b82f6';
3316
+ };
3317
+
3318
+ /**
3319
+ * Handle drag leave event
3320
+ */
3321
+ handleDragLeave = (e) => {
3322
+ e.preventDefault();
3323
+ e.stopPropagation();
3324
+ this.options.dropZone.style.background = '';
3325
+ this.options.dropZone.style.borderColor = '';
3326
+ };
3327
+
3328
+ /**
3329
+ * Handle drop event
3330
+ */
3331
+ handleDrop = async (e) => {
3332
+ e.preventDefault();
3333
+ e.stopPropagation();
3334
+ this.options.dropZone.style.background = '';
3335
+ this.options.dropZone.style.borderColor = '';
3336
+
3337
+ const files = Array.from(e.dataTransfer.files);
3338
+ for (const file of files) {
3339
+ await this.addAttachment(file);
3340
+ }
3341
+ };
3342
+
3343
+ /**
3344
+ * Parse CSV and extract metadata
3345
+ * @param {string} csvText - CSV file content
3346
+ * @returns {Object} - { rows, columns, headers }
3347
+ */
3348
+ parseCSVMetadata(csvText) {
3349
+ const lines = csvText.trim().split('\n');
3350
+ const rows = lines.length;
3351
+
3352
+ // Parse first row as headers
3353
+ const firstLine = lines[0] || '';
3354
+ const headers = firstLine.split(',').map(h => h.trim());
3355
+ const columns = headers.length;
3356
+
3357
+ return { rows, columns, headers };
3358
+ }
3359
+
3360
+ /**
3361
+ * Scan CSV headers for trigger matches
3362
+ * @param {Array} headers - CSV column headers
3363
+ * @returns {Array} - Array of matches
3364
+ */
3365
+ scanCSVHeaders(headers) {
3366
+ if (!this.options.commandScanner) {
3367
+ return [];
3368
+ }
3369
+
3370
+ const matches = [];
3371
+
3372
+ // Get CSV-specific triggers from command map
3373
+ const commandMap = this.options.commandScanner.commandMap;
3374
+ if (!commandMap || !commandMap['tql-triggers']) {
3375
+ return [];
3376
+ }
3377
+
3378
+ // Check warning triggers for csv-match-column type
3379
+ const warnings = commandMap['tql-triggers'].warning || [];
3380
+ const csvTriggers = warnings.filter(t => t.type === 'csv-match-column');
3381
+
3382
+ // Check each header against CSV triggers
3383
+ headers.forEach((header, index) => {
3384
+ csvTriggers.forEach(trigger => {
3385
+ if (trigger.match && trigger.match.includes(header)) {
3386
+ matches.push({
3387
+ header,
3388
+ columnIndex: index,
3389
+ trigger,
3390
+ intent: {
3391
+ category: trigger.category,
3392
+ handler: trigger.handler
3393
+ }
3394
+ });
3395
+ }
3396
+ });
3397
+ });
3398
+
3399
+ return matches;
3400
+ }
3401
+
3402
+ /**
3403
+ * Format file size in KB
3404
+ * @param {number} bytes - File size in bytes
3405
+ * @returns {string} - Formatted size
3406
+ */
3407
+ formatFileSize(bytes) {
3408
+ return (bytes / 1024).toFixed(1) + ' KB';
3409
+ }
3410
+
3411
+ /**
3412
+ * Create attachment wrapper with icon placeholder and card
3413
+ * @param {File} file - File object
3414
+ * @param {string} csvText - CSV file content
3415
+ * @param {Object} metadata - { rows, columns, headers }
3416
+ * @param {Array} matches - CSV header matches
3417
+ * @returns {HTMLElement} - Wrapper element
3418
+ */
3419
+ createAttachmentCard(file, csvText, metadata, matches = []) {
3420
+ // Create wrapper container
3421
+ const wrapper = document.createElement('div');
3422
+ wrapper.className = 'tq-attachment-wrapper';
3423
+
3424
+ // Create icon placeholder
3425
+ const iconPlaceholder = document.createElement('div');
3426
+ iconPlaceholder.className = 'tq-attachment-icon-placeholder';
3427
+
3428
+ // Add warning/error/info icon if matches found
3429
+ let icon = null;
3430
+ if (matches.length > 0) {
3431
+ const match = matches[0];
3432
+ const messageState = match.intent?.handler?.['message-state'] || 'warning';
3433
+
3434
+ // Map message state to icon filename
3435
+ const iconMap = {
3436
+ 'error': 'trustquery-error.svg',
3437
+ 'warning': 'trustquery-warning.svg',
3438
+ 'info': 'trustquery-info.svg'
3439
+ };
3440
+
3441
+ icon = document.createElement('img');
3442
+ icon.className = 'tq-attachment-icon';
3443
+ icon.src = `./assets/${iconMap[messageState] || iconMap.warning}`;
3444
+ icon.title = `CSV column ${messageState} - click to review`;
3445
+
3446
+ // Handle icon click - show dropdown (using mousedown to prevent double-firing)
3447
+ icon.addEventListener('mousedown', (e) => {
3448
+ e.preventDefault();
3449
+ e.stopPropagation();
3450
+ this.handleWarningClick(icon, matches, wrapper, file.name);
3451
+ });
3452
+
3453
+ iconPlaceholder.appendChild(icon);
3454
+ }
3455
+
3456
+ // Create card
3457
+ const card = document.createElement('div');
3458
+ card.className = 'tq-attachment-card';
3459
+
3460
+ // Remove button
3461
+ const removeBtn = document.createElement('button');
3462
+ removeBtn.className = 'tq-attachment-remove';
3463
+ removeBtn.innerHTML = '×';
3464
+ removeBtn.onclick = () => this.removeAttachment(file.name, wrapper);
3465
+
3466
+ // File name header
3467
+ const fileNameHeader = document.createElement('div');
3468
+ fileNameHeader.className = 'tq-attachment-header';
3469
+ fileNameHeader.textContent = file.name;
3470
+
3471
+ // Metadata row
3472
+ const metaRow = document.createElement('div');
3473
+ metaRow.className = 'tq-attachment-meta';
3474
+
3475
+ const sizeSpan = document.createElement('span');
3476
+ sizeSpan.textContent = this.formatFileSize(file.size);
3477
+
3478
+ const rowsSpan = document.createElement('span');
3479
+ rowsSpan.textContent = `${metadata.rows} rows`;
3480
+
3481
+ const colsSpan = document.createElement('span');
3482
+ colsSpan.textContent = `${metadata.columns} cols`;
3483
+
3484
+ metaRow.appendChild(sizeSpan);
3485
+ metaRow.appendChild(rowsSpan);
3486
+ metaRow.appendChild(colsSpan);
3487
+
3488
+ card.appendChild(removeBtn);
3489
+ card.appendChild(fileNameHeader);
3490
+ card.appendChild(metaRow);
3491
+
3492
+ // Apply styles via StyleManager
3493
+ if (this.options.styleManager) {
3494
+ this.options.styleManager.applyWrapperStyles(wrapper);
3495
+ this.options.styleManager.applyIconPlaceholderStyles(iconPlaceholder, matches.length > 0);
3496
+ this.options.styleManager.applyIconStyles(icon);
3497
+ this.options.styleManager.applyCardStyles(card);
3498
+ this.options.styleManager.applyRemoveButtonStyles(removeBtn);
3499
+ this.options.styleManager.applyHeaderStyles(fileNameHeader);
3500
+ this.options.styleManager.applyMetaStyles(metaRow);
3501
+ }
3502
+
3503
+ // Add click handler to card to open CSV modal
3504
+ card.style.cursor = 'pointer';
3505
+ card.addEventListener('click', (e) => {
3506
+ // Don't open modal if clicking remove button
3507
+ if (e.target === removeBtn || removeBtn.contains(e.target)) {
3508
+ return;
3509
+ }
3510
+
3511
+ if (this.options.csvModalManager) {
3512
+ // Get the latest CSV text from attachedFiles (in case it was updated)
3513
+ const attachment = this.attachedFiles.get(file.name);
3514
+ const currentText = attachment ? attachment.text : csvText;
3515
+ const currentMetadata = attachment ? attachment.metadata : metadata;
3516
+
3517
+ this.options.csvModalManager.show({
3518
+ file,
3519
+ text: currentText,
3520
+ metadata: currentMetadata
3521
+ });
3522
+ }
3523
+ });
3524
+
3525
+ // Assemble structure
3526
+ wrapper.appendChild(iconPlaceholder);
3527
+ wrapper.appendChild(card);
3528
+
3529
+ return wrapper;
3530
+ }
3531
+
3532
+ /**
3533
+ * Update CSV column header in stored text
3534
+ * @param {string} fileName - File name
3535
+ * @param {number} colIndex - Column index
3536
+ * @param {string} appendText - Text to append (e.g., "/PST")
3537
+ */
3538
+ updateCSVColumnHeader(fileName, colIndex, appendText) {
3539
+ const attachment = this.attachedFiles.get(fileName);
3540
+ if (!attachment) {
3541
+ console.warn('[AttachmentManager] File not found:', fileName);
3542
+ return;
3543
+ }
3544
+
3545
+ // Parse CSV and update header
3546
+ const lines = attachment.text.split('\n');
3547
+ if (lines.length === 0) {
3548
+ return;
3549
+ }
3550
+
3551
+ const headers = lines[0].split(',').map(h => h.trim());
3552
+ if (colIndex >= headers.length) {
3553
+ return;
3554
+ }
3555
+
3556
+ // Remove any existing suffix (text after /)
3557
+ const baseHeader = headers[colIndex].split('/')[0];
3558
+ headers[colIndex] = baseHeader + appendText;
3559
+
3560
+ // Update first line
3561
+ lines[0] = headers.join(', ');
3562
+
3563
+ // Update stored text
3564
+ attachment.text = lines.join('\n');
3565
+
3566
+ // Update metadata
3567
+ attachment.metadata.headers = headers;
3568
+
3569
+ // Re-scan headers for matches
3570
+ const newMatches = this.scanCSVHeaders(headers);
3571
+ attachment.matches = newMatches;
3572
+
3573
+ // Update icon visibility based on matches
3574
+ if (attachment.wrapper) {
3575
+ const iconPlaceholder = attachment.wrapper.querySelector('.tq-attachment-icon-placeholder');
3576
+ const existingIcon = iconPlaceholder?.querySelector('.tq-attachment-icon');
3577
+
3578
+ if (newMatches.length === 0 && existingIcon) {
3579
+ // No more matches - remove the icon
3580
+ existingIcon.remove();
3581
+ if (this.options.debug) {
3582
+ console.log('[AttachmentManager] Removed warning icon - all warnings resolved');
3583
+ }
3584
+ } else if (newMatches.length > 0 && !existingIcon) {
3585
+ // New matches appeared - add icon (shouldn't happen in normal flow, but handle it)
3586
+ const match = newMatches[0];
3587
+ const messageState = match.intent?.handler?.['message-state'] || 'warning';
3588
+ const iconMap = {
3589
+ 'error': 'trustquery-error.svg',
3590
+ 'warning': 'trustquery-warning.svg',
3591
+ 'info': 'trustquery-info.svg'
3592
+ };
3593
+
3594
+ const icon = document.createElement('img');
3595
+ icon.className = 'tq-attachment-icon';
3596
+ icon.src = `./assets/${iconMap[messageState] || iconMap.warning}`;
3597
+ icon.title = `CSV column ${messageState} - click to review`;
3598
+
3599
+ // Handle icon click
3600
+ icon.addEventListener('mousedown', (e) => {
3601
+ e.preventDefault();
3602
+ e.stopPropagation();
3603
+ this.handleWarningClick(icon, newMatches, attachment.wrapper, fileName);
3604
+ });
3605
+
3606
+ // Apply styles if styleManager exists
3607
+ if (this.options.styleManager) {
3608
+ this.options.styleManager.applyIconStyles(icon);
3609
+ }
3610
+
3611
+ iconPlaceholder?.appendChild(icon);
3612
+
3613
+ if (this.options.debug) {
3614
+ console.log('[AttachmentManager] Added warning icon - new warnings detected');
3615
+ }
3616
+ }
3617
+ }
3618
+
3619
+ if (this.options.debug) {
3620
+ console.log('[AttachmentManager] Updated CSV column', colIndex, 'to', headers[colIndex]);
3621
+ }
3622
+ }
3623
+
3624
+ /**
3625
+ * Handle warning icon click - show dropdown
3626
+ * @param {HTMLElement} iconEl - Warning icon element
3627
+ * @param {Array} matches - CSV header matches
3628
+ * @param {HTMLElement} wrapper - Wrapper element
3629
+ * @param {string} fileName - File name for tracking
3630
+ */
3631
+ handleWarningClick(iconEl, matches, wrapper, fileName) {
3632
+ if (!this.options.dropdownManager || matches.length === 0) {
3633
+ return;
3634
+ }
3635
+
3636
+ // Use the first match (could be enhanced to handle multiple)
3637
+ const match = matches[0];
3638
+
3639
+ // Create match data similar to text matches
3640
+ const matchData = {
3641
+ text: match.header,
3642
+ command: {
3643
+ id: `csv-column-${match.columnIndex}`,
3644
+ match: match.header,
3645
+ matchType: 'csv-column',
3646
+ messageState: match.intent.handler['message-state'] || 'warning',
3647
+ category: match.intent.category,
3648
+ intent: match.intent,
3649
+ handler: match.intent.handler
3650
+ },
3651
+ intent: match.intent,
3652
+ // Add context for attachment icon click
3653
+ isAttachmentIcon: true,
3654
+ fileName: fileName,
3655
+ csvColumnIndex: match.columnIndex
3656
+ };
3657
+
3658
+ // Show dropdown using DropdownManager
3659
+ this.options.dropdownManager.showDropdown(iconEl, matchData);
3660
+
3661
+ if (this.options.debug) {
3662
+ console.log('[AttachmentManager] Showing dropdown for CSV column:', match.header);
3663
+ }
3664
+ }
3665
+
3666
+ /**
3667
+ * Add attachment to container
3668
+ * @param {File} file - File to attach
3669
+ */
3670
+ async addAttachment(file) {
3671
+ if (!file.name.endsWith('.csv')) {
3672
+ alert('Only CSV files are supported');
3673
+ return;
3674
+ }
3675
+
3676
+ // Check if already attached
3677
+ if (this.attachedFiles.has(file.name)) {
3678
+ console.warn('[AttachmentManager] File already attached:', file.name);
3679
+ return;
3680
+ }
3681
+
3682
+ // Read file to get metadata
3683
+ const text = await file.text();
3684
+ const metadata = this.parseCSVMetadata(text);
3685
+
3686
+ // Scan CSV headers for trigger matches
3687
+ const matches = this.scanCSVHeaders(metadata.headers || []);
3688
+
3689
+ // Create wrapper with card and icon
3690
+ const wrapper = this.createAttachmentCard(file, text, metadata, matches);
3691
+
3692
+ // Store file reference with matches and text
3693
+ this.attachedFiles.set(file.name, { file, text, wrapper, metadata, matches });
3694
+
3695
+ // Add to container
3696
+ this.options.container.appendChild(wrapper);
3697
+ this.options.container.classList.add('has-attachments');
3698
+
3699
+ // Trigger callback
3700
+ if (this.options.onAttachmentAdd) {
3701
+ this.options.onAttachmentAdd({ file, metadata, matches });
3702
+ }
3703
+
3704
+ if (this.options.debug) {
3705
+ console.log('[AttachmentManager] Added:', file.name, metadata, 'matches:', matches);
3706
+ }
3707
+ }
3708
+
3709
+ /**
3710
+ * Remove attachment
3711
+ * @param {string} fileName - File name to remove
3712
+ * @param {HTMLElement} wrapper - Wrapper element
3713
+ */
3714
+ removeAttachment(fileName, wrapper) {
3715
+ wrapper.remove();
3716
+ const attachment = this.attachedFiles.get(fileName);
3717
+ this.attachedFiles.delete(fileName);
3718
+
3719
+ // Remove has-attachments class if no attachments left
3720
+ if (this.attachedFiles.size === 0) {
3721
+ this.options.container.classList.remove('has-attachments');
3722
+ }
3723
+
3724
+ // Trigger callback
3725
+ if (this.options.onAttachmentRemove && attachment) {
3726
+ this.options.onAttachmentRemove({ file: attachment.file, metadata: attachment.metadata });
3727
+ }
3728
+
3729
+ if (this.options.debug) {
3730
+ console.log('[AttachmentManager] Removed:', fileName);
3731
+ }
3732
+ }
3733
+
3734
+ /**
3735
+ * Get all attached files
3736
+ * @returns {Array} - Array of { file, metadata }
3737
+ */
3738
+ getAttachments() {
3739
+ return Array.from(this.attachedFiles.values());
3740
+ }
3741
+
3742
+ /**
3743
+ * Clear all attachments
3744
+ */
3745
+ clearAll() {
3746
+ this.attachedFiles.forEach((attachment, fileName) => {
3747
+ this.removeAttachment(fileName, attachment.wrapper);
3748
+ });
3749
+ }
3750
+
3751
+ /**
3752
+ * Cleanup
3753
+ */
3754
+ destroy() {
3755
+ if (this.options.dropZone) {
3756
+ this.options.dropZone.removeEventListener('dragover', this.handleDragOver);
3757
+ this.options.dropZone.removeEventListener('dragleave', this.handleDragLeave);
3758
+ this.options.dropZone.removeEventListener('drop', this.handleDrop);
3759
+ }
3760
+
3761
+ this.clearAll();
3762
+
3763
+ if (this.options.debug) {
3764
+ console.log('[AttachmentManager] Destroyed');
3765
+ }
3766
+ }
3767
+ }
3768
+
3769
+ // AttachmentStyleManager - Handles all styling for attachment cards
3770
+ // Single responsibility: apply consistent inline styles to attachment UI elements
3771
+
3772
+ class AttachmentStyleManager {
3773
+ /**
3774
+ * Create attachment style manager
3775
+ * @param {Object} options - Style options
3776
+ */
3777
+ constructor(options = {}) {
3778
+ this.options = {
3779
+ // Card colors
3780
+ cardBackground: options.cardBackground || '#f0f9ff', // Light blue background
3781
+ cardBorder: options.cardBorder || '#e2e8f0',
3782
+
3783
+ // Text colors
3784
+ headerColor: options.headerColor || '#1e293b',
3785
+ metaColor: options.metaColor || '#64748b',
3786
+
3787
+ // Remove button colors
3788
+ removeBackground: options.removeBackground || '#ef4444',
3789
+ removeBackgroundHover: options.removeBackgroundHover || '#dc2626',
3790
+
3791
+ ...options
3792
+ };
3793
+ }
3794
+
3795
+ /**
3796
+ * Apply wrapper styles (container for icon + card)
3797
+ * @param {HTMLElement} wrapper - Wrapper element
3798
+ */
3799
+ applyWrapperStyles(wrapper) {
3800
+ Object.assign(wrapper.style, {
3801
+ display: 'flex',
3802
+ alignItems: 'center',
3803
+ gap: '8px',
3804
+ marginBottom: '6px'
3805
+ });
3806
+ }
3807
+
3808
+ /**
3809
+ * Apply icon placeholder styles
3810
+ * @param {HTMLElement} placeholder - Icon placeholder element
3811
+ * @param {boolean} hasIcon - Whether icon is present
3812
+ */
3813
+ applyIconPlaceholderStyles(placeholder, hasIcon) {
3814
+ Object.assign(placeholder.style, {
3815
+ width: '24px',
3816
+ minWidth: '24px',
3817
+ height: '48px',
3818
+ display: 'flex',
3819
+ alignItems: 'center',
3820
+ justifyContent: 'center',
3821
+ flexShrink: '0'
3822
+ });
3823
+ }
3824
+
3825
+ /**
3826
+ * Apply icon styles
3827
+ * @param {HTMLElement} icon - Icon element
3828
+ */
3829
+ applyIconStyles(icon) {
3830
+ if (!icon) return;
3831
+
3832
+ Object.assign(icon.style, {
3833
+ width: '20px',
3834
+ height: '20px',
3835
+ cursor: 'pointer',
3836
+ transition: 'transform 0.2s'
3837
+ });
3838
+
3839
+ // Hover effect - slight scale
3840
+ icon.addEventListener('mouseenter', () => {
3841
+ icon.style.transform = 'scale(1.1)';
3842
+ });
3843
+
3844
+ icon.addEventListener('mouseleave', () => {
3845
+ icon.style.transform = 'scale(1)';
3846
+ });
3847
+ }
3848
+
3849
+ /**
3850
+ * Apply card styles (reduced padding to prevent cutoff)
3851
+ * @param {HTMLElement} card - Card element
3852
+ */
3853
+ applyCardStyles(card) {
3854
+ Object.assign(card.style, {
3855
+ position: 'relative',
3856
+ background: this.options.cardBackground,
3857
+ border: `1px solid ${this.options.cardBorder}`,
3858
+ borderRadius: '6px',
3859
+ padding: '6px 10px',
3860
+ display: 'flex',
3861
+ flexDirection: 'column',
3862
+ gap: '2px',
3863
+ boxSizing: 'border-box',
3864
+ maxHeight: '48px',
3865
+ overflow: 'hidden',
3866
+ flex: '1' // Take remaining space in wrapper
3867
+ });
3868
+ }
3869
+
3870
+ /**
3871
+ * Apply remove button styles (dark icon, no background)
3872
+ * @param {HTMLElement} button - Remove button element
3873
+ */
3874
+ applyRemoveButtonStyles(button) {
3875
+ Object.assign(button.style, {
3876
+ position: 'absolute',
3877
+ top: '4px', // Adjusted for reduced padding
3878
+ right: '6px',
3879
+ background: 'transparent',
3880
+ color: '#64748b',
3881
+ border: 'none',
3882
+ borderRadius: '3px',
3883
+ width: '18px',
3884
+ height: '18px',
3885
+ fontSize: '18px',
3886
+ lineHeight: '1',
3887
+ cursor: 'pointer',
3888
+ display: 'flex',
3889
+ alignItems: 'center',
3890
+ justifyContent: 'center',
3891
+ transition: 'color 0.2s',
3892
+ padding: '0',
3893
+ fontWeight: '400'
3894
+ });
3895
+
3896
+ // Hover effect - darker color
3897
+ button.addEventListener('mouseenter', () => {
3898
+ button.style.color = '#1e293b'; // Darker on hover
3899
+ });
3900
+
3901
+ button.addEventListener('mouseleave', () => {
3902
+ button.style.color = '#64748b'; // Back to gray
3903
+ });
3904
+ }
3905
+
3906
+ /**
3907
+ * Apply header (file name) styles - smaller size
3908
+ * @param {HTMLElement} header - Header element
3909
+ */
3910
+ applyHeaderStyles(header) {
3911
+ Object.assign(header.style, {
3912
+ fontWeight: '600',
3913
+ fontSize: '11px', // Further reduced from 12px to 11px
3914
+ color: this.options.headerColor,
3915
+ paddingRight: '22px',
3916
+ wordBreak: 'break-word',
3917
+ lineHeight: '1.2' // Tighter line height
3918
+ });
3919
+ }
3920
+
3921
+ /**
3922
+ * Apply metadata row styles
3923
+ * @param {HTMLElement} metaRow - Metadata row element
3924
+ */
3925
+ applyMetaStyles(metaRow) {
3926
+ Object.assign(metaRow.style, {
3927
+ display: 'flex',
3928
+ gap: '10px', // Slightly reduced from 12px
3929
+ fontSize: '10px', // Reduced from 11px
3930
+ color: this.options.metaColor,
3931
+ lineHeight: '1.2'
3932
+ });
3933
+ }
3934
+
3935
+ /**
3936
+ * Update theme colors dynamically
3937
+ * @param {Object} newColors - New color options
3938
+ */
3939
+ updateTheme(newColors) {
3940
+ Object.assign(this.options, newColors);
3941
+ }
3942
+ }
3943
+
3944
+ // CSVModalManager - Handles CSV modal display with trigger matching
3945
+ // Single responsibility: manage CSV modal lifecycle and rendering
3946
+
3947
+ class CSVModalManager {
3948
+ /**
3949
+ * Create CSV modal manager
3950
+ * @param {Object} options - Configuration options
3951
+ */
3952
+ constructor(options = {}) {
3953
+ this.options = {
3954
+ styleManager: options.styleManager || null,
3955
+ commandScanner: options.commandScanner || null,
3956
+ dropdownManager: options.dropdownManager || null,
3957
+ onCellClick: options.onCellClick || null,
3958
+ debug: options.debug || false,
3959
+ ...options
3960
+ };
3961
+
3962
+ this.modal = null;
3963
+ this.currentCSVData = null;
3964
+ this.parsedData = null;
3965
+
3966
+ if (this.options.debug) {
3967
+ console.log('[CSVModalManager] Initialized');
3968
+ }
3969
+ }
3970
+
3971
+ /**
3972
+ * Parse CSV text into 2D array
3973
+ * @param {string} csvText - Raw CSV text
3974
+ * @returns {Array} - 2D array of cells
3975
+ */
3976
+ parseCSV(csvText) {
3977
+ const lines = csvText.trim().split('\n');
3978
+ return lines.map(line => {
3979
+ // Simple CSV parsing (handles basic cases)
3980
+ return line.split(',').map(cell => cell.trim());
3981
+ });
3982
+ }
3983
+
3984
+ /**
3985
+ * Check if a cell value matches any triggers
3986
+ * @param {string} cellValue - Cell content
3987
+ * @returns {Object|null} - Match data or null
3988
+ */
3989
+ checkCellForTriggers(cellValue) {
3990
+ if (!this.options.commandScanner || !cellValue) {
3991
+ return null;
3992
+ }
3993
+
3994
+ const commandMap = this.options.commandScanner.commandMap;
3995
+ if (!commandMap || !commandMap['tql-triggers']) {
3996
+ return null;
3997
+ }
3998
+
3999
+ // Check all trigger states (error, warning, info)
4000
+ const triggers = commandMap['tql-triggers'];
4001
+ const allTriggers = [
4002
+ ...(triggers.error || []),
4003
+ ...(triggers.warning || []),
4004
+ ...(triggers.info || [])
4005
+ ];
4006
+
4007
+ // Check each trigger
4008
+ for (const trigger of allTriggers) {
4009
+ // Handle 'match' type triggers
4010
+ if (trigger.type === 'match' && trigger.match) {
4011
+ for (const matchText of trigger.match) {
4012
+ if (cellValue.toLowerCase() === matchText.toLowerCase()) {
4013
+ return {
4014
+ text: cellValue,
4015
+ trigger,
4016
+ matchType: 'exact',
4017
+ messageState: trigger.handler?.['message-state'] || 'info'
4018
+ };
4019
+ }
4020
+ }
4021
+ }
4022
+
4023
+ // Handle 'csv-match-column' type triggers (for CSV headers)
4024
+ if (trigger.type === 'csv-match-column' && trigger.match) {
4025
+ for (const matchText of trigger.match) {
4026
+ if (cellValue.toLowerCase() === matchText.toLowerCase()) {
4027
+ return {
4028
+ text: cellValue,
4029
+ trigger,
4030
+ matchType: 'csv-column',
4031
+ messageState: trigger.handler?.['message-state'] || 'info'
4032
+ };
4033
+ }
4034
+ }
4035
+ }
4036
+
4037
+ // Handle 'regex' type triggers
4038
+ if (trigger.type === 'regex' && trigger.regex) {
4039
+ for (const pattern of trigger.regex) {
4040
+ const regex = new RegExp(pattern);
4041
+ if (regex.test(cellValue)) {
4042
+ return {
4043
+ text: cellValue,
4044
+ trigger,
4045
+ matchType: 'regex',
4046
+ messageState: trigger.handler?.['message-state'] || 'info'
4047
+ };
4048
+ }
4049
+ }
4050
+ }
4051
+ }
4052
+
4053
+ return null;
4054
+ }
4055
+
4056
+ /**
4057
+ * Create table cell with optional trigger highlighting
4058
+ * @param {string} cellValue - Cell content
4059
+ * @param {boolean} isHeader - Whether this is a header cell
4060
+ * @param {number} rowIndex - Row index
4061
+ * @param {number} colIndex - Column index
4062
+ * @returns {HTMLElement} - Table cell element
4063
+ */
4064
+ createCell(cellValue, isHeader, rowIndex, colIndex) {
4065
+ const cell = document.createElement(isHeader ? 'th' : 'td');
4066
+
4067
+ // Check for trigger matches
4068
+ const match = this.checkCellForTriggers(cellValue);
4069
+
4070
+ if (match) {
4071
+ // Create highlighted span
4072
+ const span = document.createElement('span');
4073
+ span.className = 'tq-csv-match';
4074
+ span.textContent = cellValue;
4075
+ span.setAttribute('data-message-state', match.messageState);
4076
+ span.setAttribute('data-row', rowIndex);
4077
+ span.setAttribute('data-col', colIndex);
4078
+
4079
+ // Apply highlighting styles
4080
+ if (this.options.styleManager) {
4081
+ this.options.styleManager.applyCellMatchStyles(span, match.messageState);
4082
+ }
4083
+
4084
+ // Add click handler to show dropdown
4085
+ span.addEventListener('click', (e) => {
4086
+ e.stopPropagation();
4087
+ this.handleCellMatchClick(span, match, rowIndex, colIndex);
4088
+ });
4089
+
4090
+ cell.appendChild(span);
4091
+ } else {
4092
+ cell.textContent = cellValue;
4093
+ }
4094
+
4095
+ // Apply cell styles
4096
+ if (this.options.styleManager) {
4097
+ if (isHeader) {
4098
+ this.options.styleManager.applyHeaderCellStyles(cell);
4099
+ } else {
4100
+ this.options.styleManager.applyDataCellStyles(cell);
4101
+ }
4102
+ }
4103
+
4104
+ return cell;
4105
+ }
4106
+
4107
+ /**
4108
+ * Handle click on matched cell
4109
+ * @param {HTMLElement} cellEl - Cell element
4110
+ * @param {Object} match - Match data
4111
+ * @param {number} rowIndex - Row index
4112
+ * @param {number} colIndex - Column index
4113
+ */
4114
+ handleCellMatchClick(cellEl, match, rowIndex, colIndex) {
4115
+ if (!this.options.dropdownManager) {
4116
+ return;
4117
+ }
4118
+
4119
+ // Create match data for dropdown
4120
+ const matchData = {
4121
+ text: match.text,
4122
+ command: {
4123
+ id: `csv-cell-${rowIndex}-${colIndex}`,
4124
+ matchType: match.matchType,
4125
+ messageState: match.messageState,
4126
+ category: match.trigger.category,
4127
+ intent: {
4128
+ category: match.trigger.category,
4129
+ handler: match.trigger.handler
4130
+ },
4131
+ handler: match.trigger.handler
4132
+ },
4133
+ intent: {
4134
+ category: match.trigger.category,
4135
+ handler: match.trigger.handler
4136
+ },
4137
+ // Store cell position for updates
4138
+ csvCellPosition: {
4139
+ rowIndex,
4140
+ colIndex,
4141
+ isHeader: rowIndex === 0
4142
+ }
4143
+ };
4144
+
4145
+ // Show dropdown
4146
+ this.options.dropdownManager.showDropdown(cellEl, matchData);
4147
+
4148
+ if (this.options.debug) {
4149
+ console.log('[CSVModalManager] Showing dropdown for cell:', match.text, 'at', rowIndex, colIndex);
4150
+ }
4151
+ }
4152
+
4153
+ /**
4154
+ * Update column header with selected option
4155
+ * @param {number} colIndex - Column index
4156
+ * @param {string} appendText - Text to append (e.g., "/PST")
4157
+ */
4158
+ updateColumnHeader(colIndex, appendText) {
4159
+ if (!this.parsedData || !this.parsedData[0]) {
4160
+ return;
4161
+ }
4162
+
4163
+ // Update parsed data
4164
+ const originalHeader = this.parsedData[0][colIndex];
4165
+
4166
+ // Remove any existing suffix (text after /)
4167
+ const baseHeader = originalHeader.split('/')[0];
4168
+ this.parsedData[0][colIndex] = baseHeader + appendText;
4169
+
4170
+ // Update the displayed table
4171
+ this.refreshTable();
4172
+
4173
+ if (this.options.debug) {
4174
+ console.log('[CSVModalManager] Updated column', colIndex, 'from', originalHeader, 'to', this.parsedData[0][colIndex]);
4175
+ }
4176
+ }
4177
+
4178
+ /**
4179
+ * Refresh the table display
4180
+ */
4181
+ refreshTable() {
4182
+ if (!this.modal || !this.parsedData) {
4183
+ return;
4184
+ }
4185
+
4186
+ // Find table container
4187
+ const tableContainer = this.modal.querySelector('.tq-csv-table-container');
4188
+ if (!tableContainer) {
4189
+ return;
4190
+ }
4191
+
4192
+ // Clear and rebuild table
4193
+ tableContainer.innerHTML = '';
4194
+ const table = this.createTable(this.parsedData);
4195
+ tableContainer.appendChild(table);
4196
+ }
4197
+
4198
+ /**
4199
+ * Create HTML table from parsed CSV data
4200
+ * @param {Array} data - 2D array of cells
4201
+ * @returns {HTMLElement} - Table element
4202
+ */
4203
+ createTable(data) {
4204
+ const table = document.createElement('table');
4205
+ table.className = 'tq-csv-table';
4206
+
4207
+ // Create header row
4208
+ if (data.length > 0) {
4209
+ const thead = document.createElement('thead');
4210
+ const headerRow = document.createElement('tr');
4211
+
4212
+ data[0].forEach((cellValue, colIndex) => {
4213
+ const cell = this.createCell(cellValue, true, 0, colIndex);
4214
+ headerRow.appendChild(cell);
4215
+ });
4216
+
4217
+ thead.appendChild(headerRow);
4218
+ table.appendChild(thead);
4219
+ }
4220
+
4221
+ // Create data rows
4222
+ if (data.length > 1) {
4223
+ const tbody = document.createElement('tbody');
4224
+
4225
+ for (let i = 1; i < data.length; i++) {
4226
+ const row = document.createElement('tr');
4227
+
4228
+ data[i].forEach((cellValue, colIndex) => {
4229
+ const cell = this.createCell(cellValue, false, i, colIndex);
4230
+ row.appendChild(cell);
4231
+ });
4232
+
4233
+ tbody.appendChild(row);
4234
+ }
4235
+
4236
+ table.appendChild(tbody);
4237
+ }
4238
+
4239
+ // Apply table styles
4240
+ if (this.options.styleManager) {
4241
+ this.options.styleManager.applyTableStyles(table);
4242
+ }
4243
+
4244
+ return table;
4245
+ }
4246
+
4247
+ /**
4248
+ * Show CSV modal
4249
+ * @param {Object} csvData - { file, text, metadata }
4250
+ */
4251
+ show(csvData) {
4252
+ // Close any existing modal
4253
+ this.hide();
4254
+
4255
+ this.currentCSVData = csvData;
4256
+ this.parsedData = this.parseCSV(csvData.text);
4257
+
4258
+ // Create modal backdrop
4259
+ const backdrop = document.createElement('div');
4260
+ backdrop.className = 'tq-csv-modal-backdrop';
4261
+
4262
+ // Create modal
4263
+ const modal = document.createElement('div');
4264
+ modal.className = 'tq-csv-modal';
4265
+
4266
+ // Create header
4267
+ const header = document.createElement('div');
4268
+ header.className = 'tq-csv-modal-header';
4269
+
4270
+ const title = document.createElement('div');
4271
+ title.className = 'tq-csv-modal-title';
4272
+ title.textContent = csvData.file.name;
4273
+
4274
+ const closeBtn = document.createElement('button');
4275
+ closeBtn.className = 'tq-csv-modal-close';
4276
+ closeBtn.innerHTML = '×';
4277
+ closeBtn.onclick = () => this.hide();
4278
+
4279
+ header.appendChild(title);
4280
+ header.appendChild(closeBtn);
4281
+
4282
+ // Create table container (scrollable)
4283
+ const tableContainer = document.createElement('div');
4284
+ tableContainer.className = 'tq-csv-table-container';
4285
+
4286
+ // Create table
4287
+ const table = this.createTable(this.parsedData);
4288
+ tableContainer.appendChild(table);
4289
+
4290
+ // Assemble modal
4291
+ modal.appendChild(header);
4292
+ modal.appendChild(tableContainer);
4293
+
4294
+ // Apply styles
4295
+ if (this.options.styleManager) {
4296
+ this.options.styleManager.applyBackdropStyles(backdrop);
4297
+ this.options.styleManager.applyModalStyles(modal);
4298
+ this.options.styleManager.applyHeaderStyles(header);
4299
+ this.options.styleManager.applyTitleStyles(title);
4300
+ this.options.styleManager.applyCloseButtonStyles(closeBtn);
4301
+ this.options.styleManager.applyTableContainerStyles(tableContainer);
4302
+ }
4303
+
4304
+ // Add to document
4305
+ backdrop.appendChild(modal);
4306
+ document.body.appendChild(backdrop);
4307
+
4308
+ this.modal = backdrop;
4309
+
4310
+ // Close on backdrop click
4311
+ backdrop.addEventListener('click', (e) => {
4312
+ if (e.target === backdrop) {
4313
+ this.hide();
4314
+ }
4315
+ });
4316
+
4317
+ // Close on Escape key
4318
+ this.escapeHandler = (e) => {
4319
+ if (e.key === 'Escape') {
4320
+ this.hide();
4321
+ }
4322
+ };
4323
+ document.addEventListener('keydown', this.escapeHandler);
4324
+
4325
+ if (this.options.debug) {
4326
+ console.log('[CSVModalManager] Modal shown for:', csvData.file.name);
4327
+ }
4328
+ }
4329
+
4330
+ /**
4331
+ * Hide modal
4332
+ */
4333
+ hide() {
4334
+ if (this.modal) {
4335
+ this.modal.remove();
4336
+ this.modal = null;
4337
+ this.currentCSVData = null;
4338
+ this.parsedData = null;
4339
+ }
4340
+
4341
+ if (this.escapeHandler) {
4342
+ document.removeEventListener('keydown', this.escapeHandler);
4343
+ this.escapeHandler = null;
4344
+ }
4345
+
4346
+ if (this.options.debug) {
4347
+ console.log('[CSVModalManager] Modal hidden');
4348
+ }
4349
+ }
4350
+
4351
+ /**
4352
+ * Cleanup
4353
+ */
4354
+ destroy() {
4355
+ this.hide();
4356
+ if (this.options.debug) {
4357
+ console.log('[CSVModalManager] Destroyed');
4358
+ }
4359
+ }
4360
+ }
4361
+
4362
+ // CSVModalStyleManager - Handles styling for CSV modal
4363
+ // Single responsibility: apply consistent styles to CSV modal elements
4364
+
4365
+ class CSVModalStyleManager {
4366
+ /**
4367
+ * Create CSV modal style manager
4368
+ * @param {Object} options - Style options
4369
+ */
4370
+ constructor(options = {}) {
4371
+ this.options = {
4372
+ // Modal colors
4373
+ backdropColor: options.backdropColor || 'rgba(0, 0, 0, 0.5)',
4374
+ modalBackground: options.modalBackground || '#ffffff',
4375
+
4376
+ // Header colors
4377
+ headerBackground: options.headerBackground || '#f8fafc',
4378
+ headerBorder: options.headerBorder || '#e2e8f0',
4379
+
4380
+ // Table colors
4381
+ tableBorder: options.tableBorder || '#e2e8f0',
4382
+ headerCellBackground: options.headerCellBackground || '#f1f5f9',
4383
+ cellBorder: options.cellBorder || '#e2e8f0',
4384
+
4385
+ // Highlight colors (matching textarea triggers)
4386
+ errorHighlight: options.errorHighlight || '#fee2e2',
4387
+ errorBorder: options.errorBorder || '#ef4444',
4388
+ warningHighlight: options.warningHighlight || '#fef3c7',
4389
+ warningBorder: options.warningBorder || '#f59e0b',
4390
+ infoHighlight: options.infoHighlight || '#dbeafe',
4391
+ infoBorder: options.infoBorder || '#3b82f6',
4392
+
4393
+ ...options
4394
+ };
4395
+ }
4396
+
4397
+ /**
4398
+ * Apply backdrop styles
4399
+ * @param {HTMLElement} backdrop - Backdrop element
4400
+ */
4401
+ applyBackdropStyles(backdrop) {
4402
+ Object.assign(backdrop.style, {
4403
+ position: 'fixed',
4404
+ top: '0',
4405
+ left: '0',
4406
+ width: '100%',
4407
+ height: '100%',
4408
+ backgroundColor: this.options.backdropColor,
4409
+ display: 'flex',
4410
+ alignItems: 'center',
4411
+ justifyContent: 'center',
4412
+ zIndex: '10000',
4413
+ padding: '20px'
4414
+ });
4415
+ }
4416
+
4417
+ /**
4418
+ * Apply modal styles
4419
+ * @param {HTMLElement} modal - Modal element
4420
+ */
4421
+ applyModalStyles(modal) {
4422
+ Object.assign(modal.style, {
4423
+ backgroundColor: this.options.modalBackground,
4424
+ borderRadius: '8px',
4425
+ boxShadow: '0 10px 40px rgba(0, 0, 0, 0.2)',
4426
+ maxWidth: '90vw',
4427
+ maxHeight: '90vh',
4428
+ width: '100%',
4429
+ height: '100%',
4430
+ display: 'flex',
4431
+ flexDirection: 'column',
4432
+ overflow: 'hidden'
4433
+ });
4434
+ }
4435
+
4436
+ /**
4437
+ * Apply header styles
4438
+ * @param {HTMLElement} header - Header element
4439
+ */
4440
+ applyHeaderStyles(header) {
4441
+ Object.assign(header.style, {
4442
+ display: 'flex',
4443
+ alignItems: 'center',
4444
+ justifyContent: 'space-between',
4445
+ padding: '16px 20px',
4446
+ borderBottom: `1px solid ${this.options.headerBorder}`,
4447
+ backgroundColor: this.options.headerBackground,
4448
+ flexShrink: '0'
4449
+ });
4450
+ }
4451
+
4452
+ /**
4453
+ * Apply title styles
4454
+ * @param {HTMLElement} title - Title element
4455
+ */
4456
+ applyTitleStyles(title) {
4457
+ Object.assign(title.style, {
4458
+ fontSize: '18px',
4459
+ fontWeight: '600',
4460
+ color: '#1e293b'
4461
+ });
4462
+ }
4463
+
4464
+ /**
4465
+ * Apply close button styles
4466
+ * @param {HTMLElement} button - Close button element
4467
+ */
4468
+ applyCloseButtonStyles(button) {
4469
+ Object.assign(button.style, {
4470
+ background: 'transparent',
4471
+ border: 'none',
4472
+ fontSize: '28px',
4473
+ color: '#64748b',
4474
+ cursor: 'pointer',
4475
+ lineHeight: '1',
4476
+ padding: '0',
4477
+ width: '32px',
4478
+ height: '32px',
4479
+ display: 'flex',
4480
+ alignItems: 'center',
4481
+ justifyContent: 'center',
4482
+ borderRadius: '4px',
4483
+ transition: 'background-color 0.2s, color 0.2s'
4484
+ });
4485
+
4486
+ button.addEventListener('mouseenter', () => {
4487
+ button.style.backgroundColor = '#f1f5f9';
4488
+ button.style.color = '#1e293b';
4489
+ });
4490
+
4491
+ button.addEventListener('mouseleave', () => {
4492
+ button.style.backgroundColor = 'transparent';
4493
+ button.style.color = '#64748b';
4494
+ });
4495
+ }
4496
+
4497
+ /**
4498
+ * Apply table container styles (scrollable)
4499
+ * @param {HTMLElement} container - Table container element
4500
+ */
4501
+ applyTableContainerStyles(container) {
4502
+ Object.assign(container.style, {
4503
+ flex: '1',
4504
+ overflow: 'auto',
4505
+ padding: '20px'
4506
+ });
4507
+ }
4508
+
4509
+ /**
4510
+ * Apply table styles
4511
+ * @param {HTMLElement} table - Table element
4512
+ */
4513
+ applyTableStyles(table) {
4514
+ Object.assign(table.style, {
4515
+ borderCollapse: 'collapse',
4516
+ width: '100%',
4517
+ fontSize: '14px',
4518
+ backgroundColor: '#ffffff'
4519
+ });
4520
+ }
4521
+
4522
+ /**
4523
+ * Apply header cell styles
4524
+ * @param {HTMLElement} cell - Header cell element
4525
+ */
4526
+ applyHeaderCellStyles(cell) {
4527
+ Object.assign(cell.style, {
4528
+ padding: '12px 16px',
4529
+ textAlign: 'left',
4530
+ fontWeight: '600',
4531
+ backgroundColor: this.options.headerCellBackground,
4532
+ border: `1px solid ${this.options.tableBorder}`,
4533
+ color: '#1e293b',
4534
+ position: 'sticky',
4535
+ top: '0',
4536
+ zIndex: '10',
4537
+ whiteSpace: 'nowrap'
4538
+ });
4539
+ }
4540
+
4541
+ /**
4542
+ * Apply data cell styles
4543
+ * @param {HTMLElement} cell - Data cell element
4544
+ */
4545
+ applyDataCellStyles(cell) {
4546
+ Object.assign(cell.style, {
4547
+ padding: '10px 16px',
4548
+ border: `1px solid ${this.options.cellBorder}`,
4549
+ color: '#334155',
4550
+ whiteSpace: 'nowrap'
4551
+ });
4552
+ }
4553
+
4554
+ /**
4555
+ * Apply match highlighting styles to cell content
4556
+ * @param {HTMLElement} span - Span element
4557
+ * @param {string} messageState - Message state (error, warning, info)
4558
+ */
4559
+ applyCellMatchStyles(span, messageState) {
4560
+ const colorMap = {
4561
+ 'error': {
4562
+ background: this.options.errorHighlight,
4563
+ border: this.options.errorBorder
4564
+ },
4565
+ 'warning': {
4566
+ background: this.options.warningHighlight,
4567
+ border: this.options.warningBorder
4568
+ },
4569
+ 'info': {
4570
+ background: this.options.infoHighlight,
4571
+ border: this.options.infoBorder
4572
+ }
4573
+ };
4574
+
4575
+ const colors = colorMap[messageState] || colorMap.info;
4576
+
4577
+ Object.assign(span.style, {
4578
+ backgroundColor: colors.background,
4579
+ borderBottom: `2px solid ${colors.border}`,
4580
+ padding: '2px 4px',
4581
+ borderRadius: '3px',
4582
+ cursor: 'pointer',
4583
+ transition: 'background-color 0.2s',
4584
+ display: 'inline-block'
4585
+ });
4586
+
4587
+ // Hover effect
4588
+ span.addEventListener('mouseenter', () => {
4589
+ span.style.backgroundColor = this.adjustBrightness(colors.background, -10);
4590
+ });
4591
+
4592
+ span.addEventListener('mouseleave', () => {
4593
+ span.style.backgroundColor = colors.background;
4594
+ });
4595
+ }
4596
+
4597
+ /**
4598
+ * Adjust color brightness
4599
+ * @param {string} color - Hex color
4600
+ * @param {number} amount - Amount to adjust (-255 to 255)
4601
+ * @returns {string} - Adjusted color
4602
+ */
4603
+ adjustBrightness(color, amount) {
4604
+ // Simple brightness adjustment for hex colors
4605
+ const hex = color.replace('#', '');
4606
+ const num = parseInt(hex, 16);
4607
+ const r = Math.max(0, Math.min(255, (num >> 16) + amount));
4608
+ const g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount));
4609
+ const b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount));
4610
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
4611
+ }
4612
+
4613
+ /**
4614
+ * Update theme colors dynamically
4615
+ * @param {Object} newColors - New color options
4616
+ */
4617
+ updateTheme(newColors) {
4618
+ Object.assign(this.options, newColors);
4619
+ }
4620
+ }
4621
+
3252
4622
  // TrustQuery - Lightweight library to make textareas interactive
3253
4623
  // Turns matching words into interactive elements with hover bubbles and click actions
3254
4624
 
@@ -3794,5 +5164,5 @@ class TrustQuery {
3794
5164
  }
3795
5165
  }
3796
5166
 
3797
- export { TrustQuery as default };
5167
+ export { AttachmentManager, AttachmentStyleManager, CSVModalManager, CSVModalStyleManager, TrustQuery as default };
3798
5168
  //# sourceMappingURL=trustquery.js.map