@trustquery/browser 0.3.0 → 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.
@@ -3249,6 +3249,1326 @@ 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
+ if (this.options.debug) {
3570
+ console.log('[AttachmentManager] Updated CSV column', colIndex, 'to', headers[colIndex]);
3571
+ }
3572
+ }
3573
+
3574
+ /**
3575
+ * Handle warning icon click - show dropdown
3576
+ * @param {HTMLElement} iconEl - Warning icon element
3577
+ * @param {Array} matches - CSV header matches
3578
+ * @param {HTMLElement} wrapper - Wrapper element
3579
+ * @param {string} fileName - File name for tracking
3580
+ */
3581
+ handleWarningClick(iconEl, matches, wrapper, fileName) {
3582
+ if (!this.options.dropdownManager || matches.length === 0) {
3583
+ return;
3584
+ }
3585
+
3586
+ // Use the first match (could be enhanced to handle multiple)
3587
+ const match = matches[0];
3588
+
3589
+ // Create match data similar to text matches
3590
+ const matchData = {
3591
+ text: match.header,
3592
+ command: {
3593
+ id: `csv-column-${match.columnIndex}`,
3594
+ match: match.header,
3595
+ matchType: 'csv-column',
3596
+ messageState: match.intent.handler['message-state'] || 'warning',
3597
+ category: match.intent.category,
3598
+ intent: match.intent,
3599
+ handler: match.intent.handler
3600
+ },
3601
+ intent: match.intent,
3602
+ // Add context for attachment icon click
3603
+ isAttachmentIcon: true,
3604
+ fileName: fileName,
3605
+ csvColumnIndex: match.columnIndex
3606
+ };
3607
+
3608
+ // Show dropdown using DropdownManager
3609
+ this.options.dropdownManager.showDropdown(iconEl, matchData);
3610
+
3611
+ if (this.options.debug) {
3612
+ console.log('[AttachmentManager] Showing dropdown for CSV column:', match.header);
3613
+ }
3614
+ }
3615
+
3616
+ /**
3617
+ * Add attachment to container
3618
+ * @param {File} file - File to attach
3619
+ */
3620
+ async addAttachment(file) {
3621
+ if (!file.name.endsWith('.csv')) {
3622
+ alert('Only CSV files are supported');
3623
+ return;
3624
+ }
3625
+
3626
+ // Check if already attached
3627
+ if (this.attachedFiles.has(file.name)) {
3628
+ console.warn('[AttachmentManager] File already attached:', file.name);
3629
+ return;
3630
+ }
3631
+
3632
+ // Read file to get metadata
3633
+ const text = await file.text();
3634
+ const metadata = this.parseCSVMetadata(text);
3635
+
3636
+ // Scan CSV headers for trigger matches
3637
+ const matches = this.scanCSVHeaders(metadata.headers || []);
3638
+
3639
+ // Create wrapper with card and icon
3640
+ const wrapper = this.createAttachmentCard(file, text, metadata, matches);
3641
+
3642
+ // Store file reference with matches and text
3643
+ this.attachedFiles.set(file.name, { file, text, wrapper, metadata, matches });
3644
+
3645
+ // Add to container
3646
+ this.options.container.appendChild(wrapper);
3647
+ this.options.container.classList.add('has-attachments');
3648
+
3649
+ // Trigger callback
3650
+ if (this.options.onAttachmentAdd) {
3651
+ this.options.onAttachmentAdd({ file, metadata, matches });
3652
+ }
3653
+
3654
+ if (this.options.debug) {
3655
+ console.log('[AttachmentManager] Added:', file.name, metadata, 'matches:', matches);
3656
+ }
3657
+ }
3658
+
3659
+ /**
3660
+ * Remove attachment
3661
+ * @param {string} fileName - File name to remove
3662
+ * @param {HTMLElement} wrapper - Wrapper element
3663
+ */
3664
+ removeAttachment(fileName, wrapper) {
3665
+ wrapper.remove();
3666
+ const attachment = this.attachedFiles.get(fileName);
3667
+ this.attachedFiles.delete(fileName);
3668
+
3669
+ // Remove has-attachments class if no attachments left
3670
+ if (this.attachedFiles.size === 0) {
3671
+ this.options.container.classList.remove('has-attachments');
3672
+ }
3673
+
3674
+ // Trigger callback
3675
+ if (this.options.onAttachmentRemove && attachment) {
3676
+ this.options.onAttachmentRemove({ file: attachment.file, metadata: attachment.metadata });
3677
+ }
3678
+
3679
+ if (this.options.debug) {
3680
+ console.log('[AttachmentManager] Removed:', fileName);
3681
+ }
3682
+ }
3683
+
3684
+ /**
3685
+ * Get all attached files
3686
+ * @returns {Array} - Array of { file, metadata }
3687
+ */
3688
+ getAttachments() {
3689
+ return Array.from(this.attachedFiles.values());
3690
+ }
3691
+
3692
+ /**
3693
+ * Clear all attachments
3694
+ */
3695
+ clearAll() {
3696
+ this.attachedFiles.forEach((attachment, fileName) => {
3697
+ this.removeAttachment(fileName, attachment.wrapper);
3698
+ });
3699
+ }
3700
+
3701
+ /**
3702
+ * Cleanup
3703
+ */
3704
+ destroy() {
3705
+ if (this.options.dropZone) {
3706
+ this.options.dropZone.removeEventListener('dragover', this.handleDragOver);
3707
+ this.options.dropZone.removeEventListener('dragleave', this.handleDragLeave);
3708
+ this.options.dropZone.removeEventListener('drop', this.handleDrop);
3709
+ }
3710
+
3711
+ this.clearAll();
3712
+
3713
+ if (this.options.debug) {
3714
+ console.log('[AttachmentManager] Destroyed');
3715
+ }
3716
+ }
3717
+ }
3718
+
3719
+ // AttachmentStyleManager - Handles all styling for attachment cards
3720
+ // Single responsibility: apply consistent inline styles to attachment UI elements
3721
+
3722
+ class AttachmentStyleManager {
3723
+ /**
3724
+ * Create attachment style manager
3725
+ * @param {Object} options - Style options
3726
+ */
3727
+ constructor(options = {}) {
3728
+ this.options = {
3729
+ // Card colors
3730
+ cardBackground: options.cardBackground || '#f0f9ff', // Light blue background
3731
+ cardBorder: options.cardBorder || '#e2e8f0',
3732
+
3733
+ // Text colors
3734
+ headerColor: options.headerColor || '#1e293b',
3735
+ metaColor: options.metaColor || '#64748b',
3736
+
3737
+ // Remove button colors
3738
+ removeBackground: options.removeBackground || '#ef4444',
3739
+ removeBackgroundHover: options.removeBackgroundHover || '#dc2626',
3740
+
3741
+ ...options
3742
+ };
3743
+ }
3744
+
3745
+ /**
3746
+ * Apply wrapper styles (container for icon + card)
3747
+ * @param {HTMLElement} wrapper - Wrapper element
3748
+ */
3749
+ applyWrapperStyles(wrapper) {
3750
+ Object.assign(wrapper.style, {
3751
+ display: 'flex',
3752
+ alignItems: 'center',
3753
+ gap: '8px',
3754
+ marginBottom: '6px'
3755
+ });
3756
+ }
3757
+
3758
+ /**
3759
+ * Apply icon placeholder styles
3760
+ * @param {HTMLElement} placeholder - Icon placeholder element
3761
+ * @param {boolean} hasIcon - Whether icon is present
3762
+ */
3763
+ applyIconPlaceholderStyles(placeholder, hasIcon) {
3764
+ Object.assign(placeholder.style, {
3765
+ width: '24px',
3766
+ minWidth: '24px',
3767
+ height: '48px',
3768
+ display: 'flex',
3769
+ alignItems: 'center',
3770
+ justifyContent: 'center',
3771
+ flexShrink: '0'
3772
+ });
3773
+ }
3774
+
3775
+ /**
3776
+ * Apply icon styles
3777
+ * @param {HTMLElement} icon - Icon element
3778
+ */
3779
+ applyIconStyles(icon) {
3780
+ if (!icon) return;
3781
+
3782
+ Object.assign(icon.style, {
3783
+ width: '20px',
3784
+ height: '20px',
3785
+ cursor: 'pointer',
3786
+ transition: 'transform 0.2s'
3787
+ });
3788
+
3789
+ // Hover effect - slight scale
3790
+ icon.addEventListener('mouseenter', () => {
3791
+ icon.style.transform = 'scale(1.1)';
3792
+ });
3793
+
3794
+ icon.addEventListener('mouseleave', () => {
3795
+ icon.style.transform = 'scale(1)';
3796
+ });
3797
+ }
3798
+
3799
+ /**
3800
+ * Apply card styles (reduced padding to prevent cutoff)
3801
+ * @param {HTMLElement} card - Card element
3802
+ */
3803
+ applyCardStyles(card) {
3804
+ Object.assign(card.style, {
3805
+ position: 'relative',
3806
+ background: this.options.cardBackground,
3807
+ border: `1px solid ${this.options.cardBorder}`,
3808
+ borderRadius: '6px',
3809
+ padding: '6px 10px',
3810
+ display: 'flex',
3811
+ flexDirection: 'column',
3812
+ gap: '2px',
3813
+ boxSizing: 'border-box',
3814
+ maxHeight: '48px',
3815
+ overflow: 'hidden',
3816
+ flex: '1' // Take remaining space in wrapper
3817
+ });
3818
+ }
3819
+
3820
+ /**
3821
+ * Apply remove button styles (dark icon, no background)
3822
+ * @param {HTMLElement} button - Remove button element
3823
+ */
3824
+ applyRemoveButtonStyles(button) {
3825
+ Object.assign(button.style, {
3826
+ position: 'absolute',
3827
+ top: '4px', // Adjusted for reduced padding
3828
+ right: '6px',
3829
+ background: 'transparent',
3830
+ color: '#64748b',
3831
+ border: 'none',
3832
+ borderRadius: '3px',
3833
+ width: '18px',
3834
+ height: '18px',
3835
+ fontSize: '18px',
3836
+ lineHeight: '1',
3837
+ cursor: 'pointer',
3838
+ display: 'flex',
3839
+ alignItems: 'center',
3840
+ justifyContent: 'center',
3841
+ transition: 'color 0.2s',
3842
+ padding: '0',
3843
+ fontWeight: '400'
3844
+ });
3845
+
3846
+ // Hover effect - darker color
3847
+ button.addEventListener('mouseenter', () => {
3848
+ button.style.color = '#1e293b'; // Darker on hover
3849
+ });
3850
+
3851
+ button.addEventListener('mouseleave', () => {
3852
+ button.style.color = '#64748b'; // Back to gray
3853
+ });
3854
+ }
3855
+
3856
+ /**
3857
+ * Apply header (file name) styles - smaller size
3858
+ * @param {HTMLElement} header - Header element
3859
+ */
3860
+ applyHeaderStyles(header) {
3861
+ Object.assign(header.style, {
3862
+ fontWeight: '600',
3863
+ fontSize: '11px', // Further reduced from 12px to 11px
3864
+ color: this.options.headerColor,
3865
+ paddingRight: '22px',
3866
+ wordBreak: 'break-word',
3867
+ lineHeight: '1.2' // Tighter line height
3868
+ });
3869
+ }
3870
+
3871
+ /**
3872
+ * Apply metadata row styles
3873
+ * @param {HTMLElement} metaRow - Metadata row element
3874
+ */
3875
+ applyMetaStyles(metaRow) {
3876
+ Object.assign(metaRow.style, {
3877
+ display: 'flex',
3878
+ gap: '10px', // Slightly reduced from 12px
3879
+ fontSize: '10px', // Reduced from 11px
3880
+ color: this.options.metaColor,
3881
+ lineHeight: '1.2'
3882
+ });
3883
+ }
3884
+
3885
+ /**
3886
+ * Update theme colors dynamically
3887
+ * @param {Object} newColors - New color options
3888
+ */
3889
+ updateTheme(newColors) {
3890
+ Object.assign(this.options, newColors);
3891
+ }
3892
+ }
3893
+
3894
+ // CSVModalManager - Handles CSV modal display with trigger matching
3895
+ // Single responsibility: manage CSV modal lifecycle and rendering
3896
+
3897
+ class CSVModalManager {
3898
+ /**
3899
+ * Create CSV modal manager
3900
+ * @param {Object} options - Configuration options
3901
+ */
3902
+ constructor(options = {}) {
3903
+ this.options = {
3904
+ styleManager: options.styleManager || null,
3905
+ commandScanner: options.commandScanner || null,
3906
+ dropdownManager: options.dropdownManager || null,
3907
+ onCellClick: options.onCellClick || null,
3908
+ debug: options.debug || false,
3909
+ ...options
3910
+ };
3911
+
3912
+ this.modal = null;
3913
+ this.currentCSVData = null;
3914
+ this.parsedData = null;
3915
+
3916
+ if (this.options.debug) {
3917
+ console.log('[CSVModalManager] Initialized');
3918
+ }
3919
+ }
3920
+
3921
+ /**
3922
+ * Parse CSV text into 2D array
3923
+ * @param {string} csvText - Raw CSV text
3924
+ * @returns {Array} - 2D array of cells
3925
+ */
3926
+ parseCSV(csvText) {
3927
+ const lines = csvText.trim().split('\n');
3928
+ return lines.map(line => {
3929
+ // Simple CSV parsing (handles basic cases)
3930
+ return line.split(',').map(cell => cell.trim());
3931
+ });
3932
+ }
3933
+
3934
+ /**
3935
+ * Check if a cell value matches any triggers
3936
+ * @param {string} cellValue - Cell content
3937
+ * @returns {Object|null} - Match data or null
3938
+ */
3939
+ checkCellForTriggers(cellValue) {
3940
+ if (!this.options.commandScanner || !cellValue) {
3941
+ return null;
3942
+ }
3943
+
3944
+ const commandMap = this.options.commandScanner.commandMap;
3945
+ if (!commandMap || !commandMap['tql-triggers']) {
3946
+ return null;
3947
+ }
3948
+
3949
+ // Check all trigger states (error, warning, info)
3950
+ const triggers = commandMap['tql-triggers'];
3951
+ const allTriggers = [
3952
+ ...(triggers.error || []),
3953
+ ...(triggers.warning || []),
3954
+ ...(triggers.info || [])
3955
+ ];
3956
+
3957
+ // Check each trigger
3958
+ for (const trigger of allTriggers) {
3959
+ // Handle 'match' type triggers
3960
+ if (trigger.type === 'match' && trigger.match) {
3961
+ for (const matchText of trigger.match) {
3962
+ if (cellValue.toLowerCase() === matchText.toLowerCase()) {
3963
+ return {
3964
+ text: cellValue,
3965
+ trigger,
3966
+ matchType: 'exact',
3967
+ messageState: trigger.handler?.['message-state'] || 'info'
3968
+ };
3969
+ }
3970
+ }
3971
+ }
3972
+
3973
+ // Handle 'csv-match-column' type triggers (for CSV headers)
3974
+ if (trigger.type === 'csv-match-column' && trigger.match) {
3975
+ for (const matchText of trigger.match) {
3976
+ if (cellValue.toLowerCase() === matchText.toLowerCase()) {
3977
+ return {
3978
+ text: cellValue,
3979
+ trigger,
3980
+ matchType: 'csv-column',
3981
+ messageState: trigger.handler?.['message-state'] || 'info'
3982
+ };
3983
+ }
3984
+ }
3985
+ }
3986
+
3987
+ // Handle 'regex' type triggers
3988
+ if (trigger.type === 'regex' && trigger.regex) {
3989
+ for (const pattern of trigger.regex) {
3990
+ const regex = new RegExp(pattern);
3991
+ if (regex.test(cellValue)) {
3992
+ return {
3993
+ text: cellValue,
3994
+ trigger,
3995
+ matchType: 'regex',
3996
+ messageState: trigger.handler?.['message-state'] || 'info'
3997
+ };
3998
+ }
3999
+ }
4000
+ }
4001
+ }
4002
+
4003
+ return null;
4004
+ }
4005
+
4006
+ /**
4007
+ * Create table cell with optional trigger highlighting
4008
+ * @param {string} cellValue - Cell content
4009
+ * @param {boolean} isHeader - Whether this is a header cell
4010
+ * @param {number} rowIndex - Row index
4011
+ * @param {number} colIndex - Column index
4012
+ * @returns {HTMLElement} - Table cell element
4013
+ */
4014
+ createCell(cellValue, isHeader, rowIndex, colIndex) {
4015
+ const cell = document.createElement(isHeader ? 'th' : 'td');
4016
+
4017
+ // Check for trigger matches
4018
+ const match = this.checkCellForTriggers(cellValue);
4019
+
4020
+ if (match) {
4021
+ // Create highlighted span
4022
+ const span = document.createElement('span');
4023
+ span.className = 'tq-csv-match';
4024
+ span.textContent = cellValue;
4025
+ span.setAttribute('data-message-state', match.messageState);
4026
+ span.setAttribute('data-row', rowIndex);
4027
+ span.setAttribute('data-col', colIndex);
4028
+
4029
+ // Apply highlighting styles
4030
+ if (this.options.styleManager) {
4031
+ this.options.styleManager.applyCellMatchStyles(span, match.messageState);
4032
+ }
4033
+
4034
+ // Add click handler to show dropdown
4035
+ span.addEventListener('click', (e) => {
4036
+ e.stopPropagation();
4037
+ this.handleCellMatchClick(span, match, rowIndex, colIndex);
4038
+ });
4039
+
4040
+ cell.appendChild(span);
4041
+ } else {
4042
+ cell.textContent = cellValue;
4043
+ }
4044
+
4045
+ // Apply cell styles
4046
+ if (this.options.styleManager) {
4047
+ if (isHeader) {
4048
+ this.options.styleManager.applyHeaderCellStyles(cell);
4049
+ } else {
4050
+ this.options.styleManager.applyDataCellStyles(cell);
4051
+ }
4052
+ }
4053
+
4054
+ return cell;
4055
+ }
4056
+
4057
+ /**
4058
+ * Handle click on matched cell
4059
+ * @param {HTMLElement} cellEl - Cell element
4060
+ * @param {Object} match - Match data
4061
+ * @param {number} rowIndex - Row index
4062
+ * @param {number} colIndex - Column index
4063
+ */
4064
+ handleCellMatchClick(cellEl, match, rowIndex, colIndex) {
4065
+ if (!this.options.dropdownManager) {
4066
+ return;
4067
+ }
4068
+
4069
+ // Create match data for dropdown
4070
+ const matchData = {
4071
+ text: match.text,
4072
+ command: {
4073
+ id: `csv-cell-${rowIndex}-${colIndex}`,
4074
+ matchType: match.matchType,
4075
+ messageState: match.messageState,
4076
+ category: match.trigger.category,
4077
+ intent: {
4078
+ category: match.trigger.category,
4079
+ handler: match.trigger.handler
4080
+ },
4081
+ handler: match.trigger.handler
4082
+ },
4083
+ intent: {
4084
+ category: match.trigger.category,
4085
+ handler: match.trigger.handler
4086
+ },
4087
+ // Store cell position for updates
4088
+ csvCellPosition: {
4089
+ rowIndex,
4090
+ colIndex,
4091
+ isHeader: rowIndex === 0
4092
+ }
4093
+ };
4094
+
4095
+ // Show dropdown
4096
+ this.options.dropdownManager.showDropdown(cellEl, matchData);
4097
+
4098
+ if (this.options.debug) {
4099
+ console.log('[CSVModalManager] Showing dropdown for cell:', match.text, 'at', rowIndex, colIndex);
4100
+ }
4101
+ }
4102
+
4103
+ /**
4104
+ * Update column header with selected option
4105
+ * @param {number} colIndex - Column index
4106
+ * @param {string} appendText - Text to append (e.g., "/PST")
4107
+ */
4108
+ updateColumnHeader(colIndex, appendText) {
4109
+ if (!this.parsedData || !this.parsedData[0]) {
4110
+ return;
4111
+ }
4112
+
4113
+ // Update parsed data
4114
+ const originalHeader = this.parsedData[0][colIndex];
4115
+
4116
+ // Remove any existing suffix (text after /)
4117
+ const baseHeader = originalHeader.split('/')[0];
4118
+ this.parsedData[0][colIndex] = baseHeader + appendText;
4119
+
4120
+ // Update the displayed table
4121
+ this.refreshTable();
4122
+
4123
+ if (this.options.debug) {
4124
+ console.log('[CSVModalManager] Updated column', colIndex, 'from', originalHeader, 'to', this.parsedData[0][colIndex]);
4125
+ }
4126
+ }
4127
+
4128
+ /**
4129
+ * Refresh the table display
4130
+ */
4131
+ refreshTable() {
4132
+ if (!this.modal || !this.parsedData) {
4133
+ return;
4134
+ }
4135
+
4136
+ // Find table container
4137
+ const tableContainer = this.modal.querySelector('.tq-csv-table-container');
4138
+ if (!tableContainer) {
4139
+ return;
4140
+ }
4141
+
4142
+ // Clear and rebuild table
4143
+ tableContainer.innerHTML = '';
4144
+ const table = this.createTable(this.parsedData);
4145
+ tableContainer.appendChild(table);
4146
+ }
4147
+
4148
+ /**
4149
+ * Create HTML table from parsed CSV data
4150
+ * @param {Array} data - 2D array of cells
4151
+ * @returns {HTMLElement} - Table element
4152
+ */
4153
+ createTable(data) {
4154
+ const table = document.createElement('table');
4155
+ table.className = 'tq-csv-table';
4156
+
4157
+ // Create header row
4158
+ if (data.length > 0) {
4159
+ const thead = document.createElement('thead');
4160
+ const headerRow = document.createElement('tr');
4161
+
4162
+ data[0].forEach((cellValue, colIndex) => {
4163
+ const cell = this.createCell(cellValue, true, 0, colIndex);
4164
+ headerRow.appendChild(cell);
4165
+ });
4166
+
4167
+ thead.appendChild(headerRow);
4168
+ table.appendChild(thead);
4169
+ }
4170
+
4171
+ // Create data rows
4172
+ if (data.length > 1) {
4173
+ const tbody = document.createElement('tbody');
4174
+
4175
+ for (let i = 1; i < data.length; i++) {
4176
+ const row = document.createElement('tr');
4177
+
4178
+ data[i].forEach((cellValue, colIndex) => {
4179
+ const cell = this.createCell(cellValue, false, i, colIndex);
4180
+ row.appendChild(cell);
4181
+ });
4182
+
4183
+ tbody.appendChild(row);
4184
+ }
4185
+
4186
+ table.appendChild(tbody);
4187
+ }
4188
+
4189
+ // Apply table styles
4190
+ if (this.options.styleManager) {
4191
+ this.options.styleManager.applyTableStyles(table);
4192
+ }
4193
+
4194
+ return table;
4195
+ }
4196
+
4197
+ /**
4198
+ * Show CSV modal
4199
+ * @param {Object} csvData - { file, text, metadata }
4200
+ */
4201
+ show(csvData) {
4202
+ // Close any existing modal
4203
+ this.hide();
4204
+
4205
+ this.currentCSVData = csvData;
4206
+ this.parsedData = this.parseCSV(csvData.text);
4207
+
4208
+ // Create modal backdrop
4209
+ const backdrop = document.createElement('div');
4210
+ backdrop.className = 'tq-csv-modal-backdrop';
4211
+
4212
+ // Create modal
4213
+ const modal = document.createElement('div');
4214
+ modal.className = 'tq-csv-modal';
4215
+
4216
+ // Create header
4217
+ const header = document.createElement('div');
4218
+ header.className = 'tq-csv-modal-header';
4219
+
4220
+ const title = document.createElement('div');
4221
+ title.className = 'tq-csv-modal-title';
4222
+ title.textContent = csvData.file.name;
4223
+
4224
+ const closeBtn = document.createElement('button');
4225
+ closeBtn.className = 'tq-csv-modal-close';
4226
+ closeBtn.innerHTML = '×';
4227
+ closeBtn.onclick = () => this.hide();
4228
+
4229
+ header.appendChild(title);
4230
+ header.appendChild(closeBtn);
4231
+
4232
+ // Create table container (scrollable)
4233
+ const tableContainer = document.createElement('div');
4234
+ tableContainer.className = 'tq-csv-table-container';
4235
+
4236
+ // Create table
4237
+ const table = this.createTable(this.parsedData);
4238
+ tableContainer.appendChild(table);
4239
+
4240
+ // Assemble modal
4241
+ modal.appendChild(header);
4242
+ modal.appendChild(tableContainer);
4243
+
4244
+ // Apply styles
4245
+ if (this.options.styleManager) {
4246
+ this.options.styleManager.applyBackdropStyles(backdrop);
4247
+ this.options.styleManager.applyModalStyles(modal);
4248
+ this.options.styleManager.applyHeaderStyles(header);
4249
+ this.options.styleManager.applyTitleStyles(title);
4250
+ this.options.styleManager.applyCloseButtonStyles(closeBtn);
4251
+ this.options.styleManager.applyTableContainerStyles(tableContainer);
4252
+ }
4253
+
4254
+ // Add to document
4255
+ backdrop.appendChild(modal);
4256
+ document.body.appendChild(backdrop);
4257
+
4258
+ this.modal = backdrop;
4259
+
4260
+ // Close on backdrop click
4261
+ backdrop.addEventListener('click', (e) => {
4262
+ if (e.target === backdrop) {
4263
+ this.hide();
4264
+ }
4265
+ });
4266
+
4267
+ // Close on Escape key
4268
+ this.escapeHandler = (e) => {
4269
+ if (e.key === 'Escape') {
4270
+ this.hide();
4271
+ }
4272
+ };
4273
+ document.addEventListener('keydown', this.escapeHandler);
4274
+
4275
+ if (this.options.debug) {
4276
+ console.log('[CSVModalManager] Modal shown for:', csvData.file.name);
4277
+ }
4278
+ }
4279
+
4280
+ /**
4281
+ * Hide modal
4282
+ */
4283
+ hide() {
4284
+ if (this.modal) {
4285
+ this.modal.remove();
4286
+ this.modal = null;
4287
+ this.currentCSVData = null;
4288
+ this.parsedData = null;
4289
+ }
4290
+
4291
+ if (this.escapeHandler) {
4292
+ document.removeEventListener('keydown', this.escapeHandler);
4293
+ this.escapeHandler = null;
4294
+ }
4295
+
4296
+ if (this.options.debug) {
4297
+ console.log('[CSVModalManager] Modal hidden');
4298
+ }
4299
+ }
4300
+
4301
+ /**
4302
+ * Cleanup
4303
+ */
4304
+ destroy() {
4305
+ this.hide();
4306
+ if (this.options.debug) {
4307
+ console.log('[CSVModalManager] Destroyed');
4308
+ }
4309
+ }
4310
+ }
4311
+
4312
+ // CSVModalStyleManager - Handles styling for CSV modal
4313
+ // Single responsibility: apply consistent styles to CSV modal elements
4314
+
4315
+ class CSVModalStyleManager {
4316
+ /**
4317
+ * Create CSV modal style manager
4318
+ * @param {Object} options - Style options
4319
+ */
4320
+ constructor(options = {}) {
4321
+ this.options = {
4322
+ // Modal colors
4323
+ backdropColor: options.backdropColor || 'rgba(0, 0, 0, 0.5)',
4324
+ modalBackground: options.modalBackground || '#ffffff',
4325
+
4326
+ // Header colors
4327
+ headerBackground: options.headerBackground || '#f8fafc',
4328
+ headerBorder: options.headerBorder || '#e2e8f0',
4329
+
4330
+ // Table colors
4331
+ tableBorder: options.tableBorder || '#e2e8f0',
4332
+ headerCellBackground: options.headerCellBackground || '#f1f5f9',
4333
+ cellBorder: options.cellBorder || '#e2e8f0',
4334
+
4335
+ // Highlight colors (matching textarea triggers)
4336
+ errorHighlight: options.errorHighlight || '#fee2e2',
4337
+ errorBorder: options.errorBorder || '#ef4444',
4338
+ warningHighlight: options.warningHighlight || '#fef3c7',
4339
+ warningBorder: options.warningBorder || '#f59e0b',
4340
+ infoHighlight: options.infoHighlight || '#dbeafe',
4341
+ infoBorder: options.infoBorder || '#3b82f6',
4342
+
4343
+ ...options
4344
+ };
4345
+ }
4346
+
4347
+ /**
4348
+ * Apply backdrop styles
4349
+ * @param {HTMLElement} backdrop - Backdrop element
4350
+ */
4351
+ applyBackdropStyles(backdrop) {
4352
+ Object.assign(backdrop.style, {
4353
+ position: 'fixed',
4354
+ top: '0',
4355
+ left: '0',
4356
+ width: '100%',
4357
+ height: '100%',
4358
+ backgroundColor: this.options.backdropColor,
4359
+ display: 'flex',
4360
+ alignItems: 'center',
4361
+ justifyContent: 'center',
4362
+ zIndex: '10000',
4363
+ padding: '20px'
4364
+ });
4365
+ }
4366
+
4367
+ /**
4368
+ * Apply modal styles
4369
+ * @param {HTMLElement} modal - Modal element
4370
+ */
4371
+ applyModalStyles(modal) {
4372
+ Object.assign(modal.style, {
4373
+ backgroundColor: this.options.modalBackground,
4374
+ borderRadius: '8px',
4375
+ boxShadow: '0 10px 40px rgba(0, 0, 0, 0.2)',
4376
+ maxWidth: '90vw',
4377
+ maxHeight: '90vh',
4378
+ width: '100%',
4379
+ height: '100%',
4380
+ display: 'flex',
4381
+ flexDirection: 'column',
4382
+ overflow: 'hidden'
4383
+ });
4384
+ }
4385
+
4386
+ /**
4387
+ * Apply header styles
4388
+ * @param {HTMLElement} header - Header element
4389
+ */
4390
+ applyHeaderStyles(header) {
4391
+ Object.assign(header.style, {
4392
+ display: 'flex',
4393
+ alignItems: 'center',
4394
+ justifyContent: 'space-between',
4395
+ padding: '16px 20px',
4396
+ borderBottom: `1px solid ${this.options.headerBorder}`,
4397
+ backgroundColor: this.options.headerBackground,
4398
+ flexShrink: '0'
4399
+ });
4400
+ }
4401
+
4402
+ /**
4403
+ * Apply title styles
4404
+ * @param {HTMLElement} title - Title element
4405
+ */
4406
+ applyTitleStyles(title) {
4407
+ Object.assign(title.style, {
4408
+ fontSize: '18px',
4409
+ fontWeight: '600',
4410
+ color: '#1e293b'
4411
+ });
4412
+ }
4413
+
4414
+ /**
4415
+ * Apply close button styles
4416
+ * @param {HTMLElement} button - Close button element
4417
+ */
4418
+ applyCloseButtonStyles(button) {
4419
+ Object.assign(button.style, {
4420
+ background: 'transparent',
4421
+ border: 'none',
4422
+ fontSize: '28px',
4423
+ color: '#64748b',
4424
+ cursor: 'pointer',
4425
+ lineHeight: '1',
4426
+ padding: '0',
4427
+ width: '32px',
4428
+ height: '32px',
4429
+ display: 'flex',
4430
+ alignItems: 'center',
4431
+ justifyContent: 'center',
4432
+ borderRadius: '4px',
4433
+ transition: 'background-color 0.2s, color 0.2s'
4434
+ });
4435
+
4436
+ button.addEventListener('mouseenter', () => {
4437
+ button.style.backgroundColor = '#f1f5f9';
4438
+ button.style.color = '#1e293b';
4439
+ });
4440
+
4441
+ button.addEventListener('mouseleave', () => {
4442
+ button.style.backgroundColor = 'transparent';
4443
+ button.style.color = '#64748b';
4444
+ });
4445
+ }
4446
+
4447
+ /**
4448
+ * Apply table container styles (scrollable)
4449
+ * @param {HTMLElement} container - Table container element
4450
+ */
4451
+ applyTableContainerStyles(container) {
4452
+ Object.assign(container.style, {
4453
+ flex: '1',
4454
+ overflow: 'auto',
4455
+ padding: '20px'
4456
+ });
4457
+ }
4458
+
4459
+ /**
4460
+ * Apply table styles
4461
+ * @param {HTMLElement} table - Table element
4462
+ */
4463
+ applyTableStyles(table) {
4464
+ Object.assign(table.style, {
4465
+ borderCollapse: 'collapse',
4466
+ width: '100%',
4467
+ fontSize: '14px',
4468
+ backgroundColor: '#ffffff'
4469
+ });
4470
+ }
4471
+
4472
+ /**
4473
+ * Apply header cell styles
4474
+ * @param {HTMLElement} cell - Header cell element
4475
+ */
4476
+ applyHeaderCellStyles(cell) {
4477
+ Object.assign(cell.style, {
4478
+ padding: '12px 16px',
4479
+ textAlign: 'left',
4480
+ fontWeight: '600',
4481
+ backgroundColor: this.options.headerCellBackground,
4482
+ border: `1px solid ${this.options.tableBorder}`,
4483
+ color: '#1e293b',
4484
+ position: 'sticky',
4485
+ top: '0',
4486
+ zIndex: '10',
4487
+ whiteSpace: 'nowrap'
4488
+ });
4489
+ }
4490
+
4491
+ /**
4492
+ * Apply data cell styles
4493
+ * @param {HTMLElement} cell - Data cell element
4494
+ */
4495
+ applyDataCellStyles(cell) {
4496
+ Object.assign(cell.style, {
4497
+ padding: '10px 16px',
4498
+ border: `1px solid ${this.options.cellBorder}`,
4499
+ color: '#334155',
4500
+ whiteSpace: 'nowrap'
4501
+ });
4502
+ }
4503
+
4504
+ /**
4505
+ * Apply match highlighting styles to cell content
4506
+ * @param {HTMLElement} span - Span element
4507
+ * @param {string} messageState - Message state (error, warning, info)
4508
+ */
4509
+ applyCellMatchStyles(span, messageState) {
4510
+ const colorMap = {
4511
+ 'error': {
4512
+ background: this.options.errorHighlight,
4513
+ border: this.options.errorBorder
4514
+ },
4515
+ 'warning': {
4516
+ background: this.options.warningHighlight,
4517
+ border: this.options.warningBorder
4518
+ },
4519
+ 'info': {
4520
+ background: this.options.infoHighlight,
4521
+ border: this.options.infoBorder
4522
+ }
4523
+ };
4524
+
4525
+ const colors = colorMap[messageState] || colorMap.info;
4526
+
4527
+ Object.assign(span.style, {
4528
+ backgroundColor: colors.background,
4529
+ borderBottom: `2px solid ${colors.border}`,
4530
+ padding: '2px 4px',
4531
+ borderRadius: '3px',
4532
+ cursor: 'pointer',
4533
+ transition: 'background-color 0.2s',
4534
+ display: 'inline-block'
4535
+ });
4536
+
4537
+ // Hover effect
4538
+ span.addEventListener('mouseenter', () => {
4539
+ span.style.backgroundColor = this.adjustBrightness(colors.background, -10);
4540
+ });
4541
+
4542
+ span.addEventListener('mouseleave', () => {
4543
+ span.style.backgroundColor = colors.background;
4544
+ });
4545
+ }
4546
+
4547
+ /**
4548
+ * Adjust color brightness
4549
+ * @param {string} color - Hex color
4550
+ * @param {number} amount - Amount to adjust (-255 to 255)
4551
+ * @returns {string} - Adjusted color
4552
+ */
4553
+ adjustBrightness(color, amount) {
4554
+ // Simple brightness adjustment for hex colors
4555
+ const hex = color.replace('#', '');
4556
+ const num = parseInt(hex, 16);
4557
+ const r = Math.max(0, Math.min(255, (num >> 16) + amount));
4558
+ const g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount));
4559
+ const b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount));
4560
+ return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
4561
+ }
4562
+
4563
+ /**
4564
+ * Update theme colors dynamically
4565
+ * @param {Object} newColors - New color options
4566
+ */
4567
+ updateTheme(newColors) {
4568
+ Object.assign(this.options, newColors);
4569
+ }
4570
+ }
4571
+
3252
4572
  // TrustQuery - Lightweight library to make textareas interactive
3253
4573
  // Turns matching words into interactive elements with hover bubbles and click actions
3254
4574
 
@@ -3794,5 +5114,5 @@ class TrustQuery {
3794
5114
  }
3795
5115
  }
3796
5116
 
3797
- export { TrustQuery as default };
5117
+ export { AttachmentManager, AttachmentStyleManager, CSVModalManager, CSVModalStyleManager, TrustQuery as default };
3798
5118
  //# sourceMappingURL=trustquery.js.map