juxscript 1.1.205 → 1.1.206

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.
@@ -87,11 +87,16 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
87
87
  private _updateStatus;
88
88
  private _setDataFrame;
89
89
  private _detectMalformedData;
90
- private _detectLikelyHeaderRow;
91
90
  private _showReshapeModal;
92
91
  private _cleanupReshapeModal;
93
- private _showExcelReshapeModal;
94
92
  private _escapeHtml;
93
+ /**
94
+ * Build a clickable preview table from raw row data.
95
+ * Each row stores its actual sheet row index via data-sheet-row attribute.
96
+ * Returns the table HTML string.
97
+ */
98
+ private _buildClickablePreviewHTML;
99
+ private _showExcelReshapeModal;
95
100
  private _showCSVReshapeModal;
96
101
  update(_prop: string, _value: any): void;
97
102
  render(targetId?: string | HTMLElement | BaseComponent<any>): this;
@@ -1 +1 @@
1
- {"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
@@ -416,29 +416,6 @@ export class DataFrameComponent extends BaseComponent {
416
416
  }
417
417
  return false;
418
418
  }
419
- _detectLikelyHeaderRow(df) {
420
- const rows = df.toRows();
421
- const cols = df.columns;
422
- const colsAreGeneric = cols.some(c => c.startsWith('__EMPTY') || c.match(/^_\d+$/) || c.match(/^col_\d+$/));
423
- if (!colsAreGeneric)
424
- return 0;
425
- for (let i = 0; i < Math.min(rows.length, 10); i++) {
426
- const row = rows[i];
427
- const values = Object.values(row);
428
- const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
429
- if (nonEmpty.length < values.length * 0.5)
430
- continue;
431
- const nonNumericCount = nonEmpty.filter(v => {
432
- const str = String(v).trim();
433
- return isNaN(Number(str)) && str !== '';
434
- }).length;
435
- if (nonNumericCount >= nonEmpty.length * 0.7) {
436
- // toRows index i = file row (i + 1) since row 0 was used as headers
437
- return i + 1;
438
- }
439
- }
440
- return 0;
441
- }
442
419
  /* ═══════════════════════════════════════════════════
443
420
  * RESHAPE MODAL
444
421
  * ═══════════════════════════════════════════════════ */
@@ -461,23 +438,121 @@ export class DataFrameComponent extends BaseComponent {
461
438
  this._reshapeModalRendered = false;
462
439
  }
463
440
  }
441
+ _escapeHtml(text) {
442
+ const div = document.createElement('div');
443
+ div.textContent = text;
444
+ return div.innerHTML;
445
+ }
446
+ /**
447
+ * Build a clickable preview table from raw row data.
448
+ * Each row stores its actual sheet row index via data-sheet-row attribute.
449
+ * Returns the table HTML string.
450
+ */
451
+ _buildClickablePreviewHTML(rawRows, selectedSheetRow) {
452
+ let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
453
+ for (const { sheetRow, values } of rawRows) {
454
+ const isHeader = (sheetRow === selectedSheetRow);
455
+ const isSkipped = (sheetRow < selectedSheetRow);
456
+ let rowStyle = 'border-bottom: 1px solid hsl(var(--border)); cursor: pointer; transition: background 0.1s;';
457
+ if (isHeader) {
458
+ rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
459
+ }
460
+ else if (isSkipped) {
461
+ rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
462
+ }
463
+ html += `<tr data-sheet-row="${sheetRow}" style="${rowStyle}" onmouseover="this.style.outline='2px solid hsl(142 71% 45% / 0.5)'" onmouseout="this.style.outline=''">`;
464
+ // Row index cell
465
+ html += `<td style="padding: 8px 12px; width: 60px; font-weight: 600; color: hsl(var(--muted-foreground)); border-right: 1px solid hsl(var(--border)); text-align: center; user-select: none;">`;
466
+ if (isHeader) {
467
+ html += `<span style="color: hsl(142 71% 45%);">▶ ${sheetRow}</span>`;
468
+ }
469
+ else {
470
+ html += `${sheetRow}`;
471
+ }
472
+ html += '</td>';
473
+ // Show first 6 columns
474
+ const displayCols = values.slice(0, 6);
475
+ displayCols.forEach(val => {
476
+ const displayVal = val != null ? String(val).substring(0, 20) : '';
477
+ const cellStyle = isHeader
478
+ ? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
479
+ : 'padding: 8px 12px;';
480
+ html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
481
+ });
482
+ if (values.length > 6) {
483
+ html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
484
+ }
485
+ // Status badge
486
+ html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap; user-select: none;">`;
487
+ if (isHeader) {
488
+ html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
489
+ }
490
+ else if (isSkipped) {
491
+ html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
492
+ }
493
+ else {
494
+ html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
495
+ }
496
+ html += '</td></tr>';
497
+ }
498
+ html += '</table>';
499
+ return html;
500
+ }
464
501
  async _showExcelReshapeModal() {
465
502
  if (!this._rawFileData?.file)
466
503
  return;
467
504
  this._cleanupReshapeModal();
468
- let suggestedRow = 0;
469
- try {
470
- const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
471
- headerRow: 0,
472
- maxSheetSize: 20
473
- });
474
- const rawSheet = Object.values(rawSheets)[0];
475
- if (rawSheet) {
476
- suggestedRow = this._detectLikelyHeaderRow(rawSheet);
505
+ // Read raw cells from the file
506
+ const XLSX = await import('xlsx');
507
+ const buffer = await this._rawFileData.file.arrayBuffer();
508
+ const workbook = XLSX.read(buffer, {
509
+ type: 'array',
510
+ sheetRows: 20,
511
+ dense: false
512
+ });
513
+ const sheetName = workbook.SheetNames[0];
514
+ const worksheet = workbook.Sheets[sheetName];
515
+ const ref = worksheet['!ref'];
516
+ if (!ref)
517
+ return;
518
+ const range = XLSX.utils.decode_range(ref);
519
+ const endRow = Math.min(range.e.r, 14); // Show up to 15 rows
520
+ const startCol = range.s.c;
521
+ const endCol = range.e.c;
522
+ const readCellValue = (r, c) => {
523
+ const addr = XLSX.utils.encode_cell({ r, c });
524
+ const cell = worksheet[addr];
525
+ if (!cell)
526
+ return null;
527
+ if (cell.w !== undefined)
528
+ return cell.w;
529
+ if (cell.v !== undefined)
530
+ return cell.v;
531
+ return null;
532
+ };
533
+ // Build raw row data with actual sheet row indices
534
+ const rawRows = [];
535
+ for (let r = 0; r <= endRow; r++) {
536
+ const values = [];
537
+ for (let c = startCol; c <= endCol; c++) {
538
+ values.push(readCellValue(r, c));
477
539
  }
540
+ rawRows.push({ sheetRow: r, values });
478
541
  }
479
- catch {
480
- suggestedRow = 0;
542
+ // Auto-detect best header row
543
+ let selectedSheetRow = 0;
544
+ for (const { sheetRow, values } of rawRows) {
545
+ const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
546
+ if (nonEmpty.length < values.length * 0.5)
547
+ continue;
548
+ const nonNumeric = nonEmpty.filter(v => {
549
+ const str = String(v).trim();
550
+ return isNaN(Number(str)) && str !== '';
551
+ }).length;
552
+ if (nonNumeric >= nonEmpty.length * 0.7) {
553
+ selectedSheetRow = sheetRow;
554
+ break;
555
+ }
481
556
  }
482
557
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
483
558
  title: 'Excel Import Settings',
@@ -487,14 +562,11 @@ export class DataFrameComponent extends BaseComponent {
487
562
  });
488
563
  const modalContentHTML = `
489
564
  <div style="margin-bottom: 1rem;">
490
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
491
- Header Row (0-based index)
492
- </label>
493
- <input type="number" id="${this._id}-header-row" class="jux-input-element" value="${suggestedRow}" min="0" max="50" style="width: 100%;" />
494
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
565
+ <div id="${this._id}-reshape-hint" style="padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
566
+ <input type="hidden" id="${this._id}-header-row" value="${selectedSheetRow}" />
495
567
  </div>
496
- <div class="jux-reshape-preview-container">
497
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
568
+ <div>
569
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
498
570
  <div id="${this._id}-preview" style="font-family: ui-monospace, monospace; font-size: 12px; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 0; overflow: hidden; max-height: 400px; overflow-y: auto;"></div>
499
571
  </div>
500
572
  `;
@@ -545,129 +617,73 @@ export class DataFrameComponent extends BaseComponent {
545
617
  this._reshapeModal.render(document.body);
546
618
  this._reshapeModalRendered = true;
547
619
  await new Promise(resolve => requestAnimationFrame(resolve));
548
- const headerRowInput = document.getElementById(`${this._id}-header-row`);
549
620
  const previewDiv = document.getElementById(`${this._id}-preview`);
550
621
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
551
- const updateHint = (headerRow) => {
622
+ const hiddenInput = document.getElementById(`${this._id}-header-row`);
623
+ const updateHint = (row) => {
552
624
  if (!hintDiv)
553
625
  return;
554
- if (headerRow > 0) {
555
- hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
626
+ const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
627
+ const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
628
+ const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
629
+ if (row > 0) {
630
+ hintDiv.innerHTML = `Row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows 0–${row - 1} will be skipped.`;
556
631
  }
557
632
  else {
558
- hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
633
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
559
634
  }
560
635
  };
561
- const updatePreview = async () => {
562
- const headerRow = parseInt(headerRowInput?.value) || 0;
563
- updateHint(headerRow);
564
- try {
565
- // Read cells directly from XLSX to match parser behavior exactly
566
- const XLSX = await import('xlsx');
567
- const buffer = await this._rawFileData.file.arrayBuffer();
568
- const workbook = XLSX.read(buffer, {
569
- type: 'array',
570
- sheetRows: Math.max(headerRow + 12, 15),
571
- dense: false
636
+ const renderPreview = (selected) => {
637
+ if (!previewDiv)
638
+ return;
639
+ previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
640
+ // Wire click handlers on each row
641
+ previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
642
+ tr.addEventListener('click', () => {
643
+ const rowIdx = parseInt(tr.dataset.sheetRow);
644
+ hiddenInput.value = String(rowIdx);
645
+ updateHint(rowIdx);
646
+ renderPreview(rowIdx);
572
647
  });
573
- const sheetName = workbook.SheetNames[0];
574
- const worksheet = workbook.Sheets[sheetName];
575
- const ref = worksheet['!ref'];
576
- if (!ref) {
577
- if (previewDiv)
578
- previewDiv.textContent = 'No data found';
579
- return;
580
- }
581
- const range = XLSX.utils.decode_range(ref);
582
- const endRow = range.e.r;
583
- const startCol = range.s.c;
584
- const endCol = range.e.c;
585
- const readCellValue = (r, c) => {
586
- const addr = XLSX.utils.encode_cell({ r, c });
587
- const cell = worksheet[addr];
588
- if (!cell)
589
- return null;
590
- if (cell.w !== undefined)
591
- return cell.w;
592
- if (cell.v !== undefined)
593
- return cell.v;
594
- return null;
595
- };
596
- const readRow = (r) => {
597
- const vals = [];
598
- for (let c = startCol; c <= endCol; c++) {
599
- vals.push(readCellValue(r, c));
600
- }
601
- return vals;
602
- };
603
- let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
604
- const totalRowsToShow = Math.min(headerRow + 8, endRow + 1);
605
- for (let fileRow = 0; fileRow < totalRowsToShow; fileRow++) {
606
- const isHeader = (fileRow === headerRow);
607
- const isSkipped = (fileRow < headerRow);
608
- let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
609
- if (isHeader) {
610
- rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
611
- }
612
- else if (isSkipped) {
613
- rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
614
- }
615
- html += `<tr style="${rowStyle}">`;
616
- html += `<td style="padding: 8px 12px; width: 60px; font-weight: 600; color: hsl(var(--muted-foreground)); border-right: 1px solid hsl(var(--border)); text-align: center;">`;
617
- if (isHeader) {
618
- html += `<span style="color: hsl(142 71% 45%);">▶ ${fileRow}</span>`;
619
- }
620
- else {
621
- html += `${fileRow}`;
622
- }
623
- html += '</td>';
624
- const values = readRow(fileRow);
625
- const displayCols = values.slice(0, 6);
626
- displayCols.forEach(val => {
627
- const displayVal = val != null ? String(val).substring(0, 20) : '';
628
- const cellStyle = isHeader
629
- ? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
630
- : 'padding: 8px 12px;';
631
- html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
632
- });
633
- if (values.length > 6) {
634
- html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">...</td>`;
635
- }
636
- html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap;">`;
637
- if (isHeader) {
638
- html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
639
- }
640
- else if (isSkipped) {
641
- html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
642
- }
643
- else {
644
- html += '<span style="color: hsl(var(--success));">data</span>';
645
- }
646
- html += '</td></tr>';
647
- }
648
- html += '</table>';
649
- if (previewDiv)
650
- previewDiv.innerHTML = html;
651
- }
652
- catch (err) {
653
- if (previewDiv)
654
- previewDiv.textContent = `Error: ${err.message}`;
655
- }
648
+ });
656
649
  };
657
- if (headerRowInput)
658
- headerRowInput.addEventListener('input', updatePreview);
659
- updatePreview();
650
+ updateHint(selectedSheetRow);
651
+ renderPreview(selectedSheetRow);
660
652
  this._reshapeModal.open();
661
653
  }
662
- _escapeHtml(text) {
663
- const div = document.createElement('div');
664
- div.textContent = text;
665
- return div.innerHTML;
666
- }
667
654
  _showCSVReshapeModal() {
668
- if (!this._rawFileData)
655
+ if (!this._rawFileData?.text)
669
656
  return;
670
657
  this._cleanupReshapeModal();
658
+ const text = this._rawFileData.text;
659
+ const detected = this._driver._detectDelimiter(text);
660
+ // Parse raw lines
661
+ const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
662
+ const rawRows = [];
663
+ const maxPreviewRows = Math.min(lines.length, 15);
664
+ for (let i = 0; i < maxPreviewRows; i++) {
665
+ if (!lines[i]) {
666
+ rawRows.push({ sheetRow: i, values: [''] });
667
+ continue;
668
+ }
669
+ const values = this._driver._parseLine(lines[i], detected);
670
+ rawRows.push({ sheetRow: i, values });
671
+ }
672
+ // Auto-detect header row
673
+ let selectedRow = 0;
674
+ for (const { sheetRow, values } of rawRows) {
675
+ const nonEmpty = values.filter((v) => v.trim() !== '');
676
+ if (nonEmpty.length < values.length * 0.5)
677
+ continue;
678
+ const nonNumeric = nonEmpty.filter((v) => {
679
+ const trimmed = v.trim();
680
+ return isNaN(Number(trimmed)) && trimmed !== '';
681
+ }).length;
682
+ if (nonNumeric >= nonEmpty.length * 0.7) {
683
+ selectedRow = sheetRow;
684
+ break;
685
+ }
686
+ }
671
687
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
672
688
  title: 'CSV Import Settings',
673
689
  size: 'large',
@@ -685,12 +701,11 @@ export class DataFrameComponent extends BaseComponent {
685
701
  </select>
686
702
  </div>
687
703
  <div style="margin-bottom: 1rem;">
688
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based index)</label>
689
- <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
704
+ <div id="${this._id}-reshape-hint" style="padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
705
+ <input type="hidden" id="${this._id}-header-row" value="${selectedRow}" />
690
706
  </div>
691
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; margin-bottom: 1rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
692
- <div class="jux-reshape-preview-container">
693
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
707
+ <div>
708
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
694
709
  <div id="${this._id}-preview" style="font-family: monospace; font-size: 12px; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 0; overflow: hidden; max-height: 400px; overflow-y: auto;"></div>
695
710
  </div>
696
711
  `;
@@ -706,12 +721,10 @@ export class DataFrameComponent extends BaseComponent {
706
721
  label: 'Apply & Re-import',
707
722
  variant: 'primary',
708
723
  click: async () => {
709
- if (!this._rawFileData?.text)
710
- return;
711
724
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
712
- const headerRowInput = document.getElementById(`${this._id}-header-row`);
725
+ const hiddenInput = document.getElementById(`${this._id}-header-row`);
713
726
  const delim = delimiterSelect.value;
714
- const headerRow = parseInt(headerRowInput.value) || 0;
727
+ const headerRow = parseInt(hiddenInput.value) || 0;
715
728
  this.state.loading = true;
716
729
  this._updateStatus('Re-parsing with new settings...', 'loading');
717
730
  try {
@@ -735,102 +748,59 @@ export class DataFrameComponent extends BaseComponent {
735
748
  this._reshapeModalRendered = true;
736
749
  requestAnimationFrame(() => {
737
750
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
738
- const headerRowInput = document.getElementById(`${this._id}-header-row`);
739
751
  const previewDiv = document.getElementById(`${this._id}-preview`);
740
752
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
741
- if (this._rawFileData?.text) {
742
- const detected = this._driver._detectDelimiter(this._rawFileData.text);
743
- if (delimiterSelect)
744
- delimiterSelect.value = detected;
745
- const detectedHeaderRow = this._driver._detectHeaderRow(this._rawFileData.text, detected);
746
- if (headerRowInput)
747
- headerRowInput.value = String(detectedHeaderRow);
748
- }
749
- const updateHint = () => {
753
+ const hiddenInput = document.getElementById(`${this._id}-header-row`);
754
+ if (delimiterSelect)
755
+ delimiterSelect.value = detected;
756
+ const updateHint = (row) => {
750
757
  if (!hintDiv)
751
758
  return;
752
- const headerRow = parseInt(headerRowInput?.value) || 0;
753
- if (headerRow > 0) {
754
- hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
759
+ const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
760
+ const headerNames = vals.filter((v) => v != null && String(v).trim() !== '').map((v) => String(v).trim());
761
+ const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
762
+ if (row > 0) {
763
+ hintDiv.innerHTML = `Row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows 0–${row - 1} will be skipped.`;
755
764
  }
756
765
  else {
757
- hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
766
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
758
767
  }
759
768
  };
760
- const updatePreview = () => {
761
- if (!this._rawFileData?.text)
762
- return;
769
+ const reparse = () => {
763
770
  const delim = delimiterSelect?.value || ',';
764
- const headerRow = parseInt(headerRowInput?.value) || 0;
765
- updateHint();
766
- try {
767
- const rawDf = this._driver.parseCSV(this._rawFileData.text, {
768
- delimiter: delim,
769
- headerRow: 0,
770
- hasHeader: true,
771
- maxRows: headerRow + 10
772
- });
773
- const rawCols = rawDf.columns;
774
- const rawRows = rawDf.toRows();
775
- let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
776
- const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
777
- for (let i = 0; i < totalRows; i++) {
778
- const isHeader = (i === headerRow);
779
- const isSkipped = (i < headerRow);
780
- let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
781
- if (isHeader) {
782
- rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
783
- }
784
- else if (isSkipped) {
785
- rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
786
- }
787
- html += `<tr style="${rowStyle}">`;
788
- html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
789
- html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
790
- html += '</td>';
791
- let values;
792
- if (i === 0) {
793
- values = rawCols;
794
- }
795
- else if (i - 1 < rawRows.length) {
796
- values = Object.values(rawRows[i - 1]);
797
- }
798
- else {
799
- values = [];
800
- }
801
- values.slice(0, 6).forEach(val => {
802
- const displayVal = val != null ? String(val).substring(0, 25) : '';
803
- html += `<td style="padding: 6px 8px;">${this._escapeHtml(displayVal)}</td>`;
804
- });
805
- if (values.length > 6) {
806
- html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
807
- }
808
- html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
809
- if (isHeader) {
810
- html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
811
- }
812
- else if (isSkipped) {
813
- html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
814
- }
815
- else {
816
- html += '<span style="color: hsl(var(--success));">data</span>';
817
- }
818
- html += '</td></tr>';
771
+ rawRows.length = 0;
772
+ for (let i = 0; i < maxPreviewRows; i++) {
773
+ if (!lines[i]) {
774
+ rawRows.push({ sheetRow: i, values: [''] });
775
+ continue;
819
776
  }
820
- html += '</table>';
821
- if (previewDiv)
822
- previewDiv.innerHTML = html;
823
- }
824
- catch (err) {
825
- if (previewDiv)
826
- previewDiv.textContent = `Error: ${err.message}`;
777
+ const values = this._driver._parseLine(lines[i], delim);
778
+ rawRows.push({ sheetRow: i, values });
827
779
  }
828
780
  };
829
- if (delimiterSelect)
830
- delimiterSelect.addEventListener('change', updatePreview);
831
- if (headerRowInput)
832
- headerRowInput.addEventListener('input', updatePreview);
833
- updatePreview();
781
+ const renderPreview = (selected) => {
782
+ if (!previewDiv)
783
+ return;
784
+ previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
785
+ previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
786
+ tr.addEventListener('click', () => {
787
+ const rowIdx = parseInt(tr.dataset.sheetRow);
788
+ hiddenInput.value = String(rowIdx);
789
+ updateHint(rowIdx);
790
+ renderPreview(rowIdx);
791
+ });
792
+ });
793
+ };
794
+ if (delimiterSelect) {
795
+ delimiterSelect.addEventListener('change', () => {
796
+ reparse();
797
+ const current = parseInt(hiddenInput.value) || 0;
798
+ updateHint(current);
799
+ renderPreview(current);
800
+ });
801
+ }
802
+ updateHint(selectedRow);
803
+ renderPreview(selectedRow);
834
804
  this._reshapeModal.open();
835
805
  });
836
806
  }
@@ -519,37 +519,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
519
519
  return false;
520
520
  }
521
521
 
522
- private _detectLikelyHeaderRow(df: DataFrame): number {
523
- const rows = df.toRows();
524
- const cols = df.columns;
525
-
526
- const colsAreGeneric = cols.some(c =>
527
- c.startsWith('__EMPTY') || c.match(/^_\d+$/) || c.match(/^col_\d+$/)
528
- );
529
-
530
- if (!colsAreGeneric) return 0;
531
-
532
- for (let i = 0; i < Math.min(rows.length, 10); i++) {
533
- const row = rows[i];
534
- const values = Object.values(row);
535
- const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
536
-
537
- if (nonEmpty.length < values.length * 0.5) continue;
538
-
539
- const nonNumericCount = nonEmpty.filter(v => {
540
- const str = String(v).trim();
541
- return isNaN(Number(str)) && str !== '';
542
- }).length;
543
-
544
- if (nonNumericCount >= nonEmpty.length * 0.7) {
545
- // toRows index i = file row (i + 1) since row 0 was used as headers
546
- return i + 1;
547
- }
548
- }
549
-
550
- return 0;
551
- }
552
-
553
522
  /* ═══════════════════════════════════════════════════
554
523
  * RESHAPE MODAL
555
524
  * ═══════════════════════════════════════════════════ */
@@ -573,23 +542,132 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
573
542
  }
574
543
  }
575
544
 
545
+ private _escapeHtml(text: string): string {
546
+ const div = document.createElement('div');
547
+ div.textContent = text;
548
+ return div.innerHTML;
549
+ }
550
+
551
+ /**
552
+ * Build a clickable preview table from raw row data.
553
+ * Each row stores its actual sheet row index via data-sheet-row attribute.
554
+ * Returns the table HTML string.
555
+ */
556
+ private _buildClickablePreviewHTML(
557
+ rawRows: { sheetRow: number; values: any[] }[],
558
+ selectedSheetRow: number
559
+ ): string {
560
+ let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
561
+
562
+ for (const { sheetRow, values } of rawRows) {
563
+ const isHeader = (sheetRow === selectedSheetRow);
564
+ const isSkipped = (sheetRow < selectedSheetRow);
565
+
566
+ let rowStyle = 'border-bottom: 1px solid hsl(var(--border)); cursor: pointer; transition: background 0.1s;';
567
+
568
+ if (isHeader) {
569
+ rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
570
+ } else if (isSkipped) {
571
+ rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
572
+ }
573
+
574
+ html += `<tr data-sheet-row="${sheetRow}" style="${rowStyle}" onmouseover="this.style.outline='2px solid hsl(142 71% 45% / 0.5)'" onmouseout="this.style.outline=''">`;
575
+
576
+ // Row index cell
577
+ html += `<td style="padding: 8px 12px; width: 60px; font-weight: 600; color: hsl(var(--muted-foreground)); border-right: 1px solid hsl(var(--border)); text-align: center; user-select: none;">`;
578
+ if (isHeader) {
579
+ html += `<span style="color: hsl(142 71% 45%);">▶ ${sheetRow}</span>`;
580
+ } else {
581
+ html += `${sheetRow}`;
582
+ }
583
+ html += '</td>';
584
+
585
+ // Show first 6 columns
586
+ const displayCols = values.slice(0, 6);
587
+ displayCols.forEach(val => {
588
+ const displayVal = val != null ? String(val).substring(0, 20) : '';
589
+ const cellStyle = isHeader
590
+ ? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
591
+ : 'padding: 8px 12px;';
592
+ html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
593
+ });
594
+
595
+ if (values.length > 6) {
596
+ html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
597
+ }
598
+
599
+ // Status badge
600
+ html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap; user-select: none;">`;
601
+ if (isHeader) {
602
+ html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
603
+ } else if (isSkipped) {
604
+ html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
605
+ } else {
606
+ html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
607
+ }
608
+ html += '</td></tr>';
609
+ }
610
+
611
+ html += '</table>';
612
+ return html;
613
+ }
614
+
576
615
  private async _showExcelReshapeModal(): Promise<void> {
577
616
  if (!this._rawFileData?.file) return;
578
617
 
579
618
  this._cleanupReshapeModal();
580
619
 
581
- let suggestedRow = 0;
582
- try {
583
- const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
584
- headerRow: 0,
585
- maxSheetSize: 20
586
- });
587
- const rawSheet = Object.values(rawSheets)[0];
588
- if (rawSheet) {
589
- suggestedRow = this._detectLikelyHeaderRow(rawSheet);
620
+ // Read raw cells from the file
621
+ const XLSX = await import('xlsx');
622
+ const buffer = await this._rawFileData.file.arrayBuffer();
623
+ const workbook = XLSX.read(buffer, {
624
+ type: 'array',
625
+ sheetRows: 20,
626
+ dense: false
627
+ });
628
+
629
+ const sheetName = workbook.SheetNames[0];
630
+ const worksheet = workbook.Sheets[sheetName];
631
+ const ref = worksheet['!ref'];
632
+ if (!ref) return;
633
+
634
+ const range = XLSX.utils.decode_range(ref);
635
+ const endRow = Math.min(range.e.r, 14); // Show up to 15 rows
636
+ const startCol = range.s.c;
637
+ const endCol = range.e.c;
638
+
639
+ const readCellValue = (r: number, c: number): any => {
640
+ const addr = XLSX.utils.encode_cell({ r, c });
641
+ const cell = worksheet[addr];
642
+ if (!cell) return null;
643
+ if (cell.w !== undefined) return cell.w;
644
+ if (cell.v !== undefined) return cell.v;
645
+ return null;
646
+ };
647
+
648
+ // Build raw row data with actual sheet row indices
649
+ const rawRows: { sheetRow: number; values: any[] }[] = [];
650
+ for (let r = 0; r <= endRow; r++) {
651
+ const values: any[] = [];
652
+ for (let c = startCol; c <= endCol; c++) {
653
+ values.push(readCellValue(r, c));
654
+ }
655
+ rawRows.push({ sheetRow: r, values });
656
+ }
657
+
658
+ // Auto-detect best header row
659
+ let selectedSheetRow = 0;
660
+ for (const { sheetRow, values } of rawRows) {
661
+ const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
662
+ if (nonEmpty.length < values.length * 0.5) continue;
663
+ const nonNumeric = nonEmpty.filter(v => {
664
+ const str = String(v).trim();
665
+ return isNaN(Number(str)) && str !== '';
666
+ }).length;
667
+ if (nonNumeric >= nonEmpty.length * 0.7) {
668
+ selectedSheetRow = sheetRow;
669
+ break;
590
670
  }
591
- } catch {
592
- suggestedRow = 0;
593
671
  }
594
672
 
595
673
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
@@ -601,14 +679,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
601
679
 
602
680
  const modalContentHTML = `
603
681
  <div style="margin-bottom: 1rem;">
604
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
605
- Header Row (0-based index)
606
- </label>
607
- <input type="number" id="${this._id}-header-row" class="jux-input-element" value="${suggestedRow}" min="0" max="50" style="width: 100%;" />
608
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
682
+ <div id="${this._id}-reshape-hint" style="padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
683
+ <input type="hidden" id="${this._id}-header-row" value="${selectedSheetRow}" />
609
684
  </div>
610
- <div class="jux-reshape-preview-container">
611
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
685
+ <div>
686
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
612
687
  <div id="${this._id}-preview" style="font-family: ui-monospace, monospace; font-size: 12px; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 0; overflow: hidden; max-height: 400px; overflow-y: auto;"></div>
613
688
  </div>
614
689
  `;
@@ -668,134 +743,79 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
668
743
 
669
744
  await new Promise(resolve => requestAnimationFrame(resolve));
670
745
 
671
- const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
672
746
  const previewDiv = document.getElementById(`${this._id}-preview`)!;
673
747
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
748
+ const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
674
749
 
675
- const updateHint = (headerRow: number) => {
750
+ const updateHint = (row: number) => {
676
751
  if (!hintDiv) return;
677
- if (headerRow > 0) {
678
- hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
752
+ const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
753
+ const headerNames = vals.filter((v: any) => v != null && String(v).trim() !== '').map((v: any) => String(v).trim());
754
+ const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
755
+ if (row > 0) {
756
+ hintDiv.innerHTML = `Row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows 0–${row - 1} will be skipped.`;
679
757
  } else {
680
- hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
758
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
681
759
  }
682
760
  };
683
761
 
684
- const updatePreview = async () => {
685
- const headerRow = parseInt(headerRowInput?.value) || 0;
686
- updateHint(headerRow);
687
-
688
- try {
689
- // Read cells directly from XLSX to match parser behavior exactly
690
- const XLSX = await import('xlsx');
691
- const buffer = await this._rawFileData!.file.arrayBuffer();
692
- const workbook = XLSX.read(buffer, {
693
- type: 'array',
694
- sheetRows: Math.max(headerRow + 12, 15),
695
- dense: false
762
+ const renderPreview = (selected: number) => {
763
+ if (!previewDiv) return;
764
+ previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
765
+
766
+ // Wire click handlers on each row
767
+ previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
768
+ tr.addEventListener('click', () => {
769
+ const rowIdx = parseInt((tr as HTMLElement).dataset.sheetRow!);
770
+ hiddenInput.value = String(rowIdx);
771
+ updateHint(rowIdx);
772
+ renderPreview(rowIdx);
696
773
  });
697
-
698
- const sheetName = workbook.SheetNames[0];
699
- const worksheet = workbook.Sheets[sheetName];
700
- const ref = worksheet['!ref'];
701
- if (!ref) {
702
- if (previewDiv) previewDiv.textContent = 'No data found';
703
- return;
704
- }
705
-
706
- const range = XLSX.utils.decode_range(ref);
707
- const endRow = range.e.r;
708
- const startCol = range.s.c;
709
- const endCol = range.e.c;
710
-
711
- const readCellValue = (r: number, c: number): any => {
712
- const addr = XLSX.utils.encode_cell({ r, c });
713
- const cell = worksheet[addr];
714
- if (!cell) return null;
715
- if (cell.w !== undefined) return cell.w;
716
- if (cell.v !== undefined) return cell.v;
717
- return null;
718
- };
719
-
720
- const readRow = (r: number): any[] => {
721
- const vals: any[] = [];
722
- for (let c = startCol; c <= endCol; c++) {
723
- vals.push(readCellValue(r, c));
724
- }
725
- return vals;
726
- };
727
-
728
- let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
729
- const totalRowsToShow = Math.min(headerRow + 8, endRow + 1);
730
-
731
- for (let fileRow = 0; fileRow < totalRowsToShow; fileRow++) {
732
- const isHeader = (fileRow === headerRow);
733
- const isSkipped = (fileRow < headerRow);
734
-
735
- let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
736
- if (isHeader) {
737
- rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
738
- } else if (isSkipped) {
739
- rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
740
- }
741
-
742
- html += `<tr style="${rowStyle}">`;
743
- html += `<td style="padding: 8px 12px; width: 60px; font-weight: 600; color: hsl(var(--muted-foreground)); border-right: 1px solid hsl(var(--border)); text-align: center;">`;
744
- if (isHeader) {
745
- html += `<span style="color: hsl(142 71% 45%);">▶ ${fileRow}</span>`;
746
- } else {
747
- html += `${fileRow}`;
748
- }
749
- html += '</td>';
750
-
751
- const values = readRow(fileRow);
752
- const displayCols = values.slice(0, 6);
753
- displayCols.forEach(val => {
754
- const displayVal = val != null ? String(val).substring(0, 20) : '';
755
- const cellStyle = isHeader
756
- ? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
757
- : 'padding: 8px 12px;';
758
- html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
759
- });
760
-
761
- if (values.length > 6) {
762
- html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">...</td>`;
763
- }
764
-
765
- html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap;">`;
766
- if (isHeader) {
767
- html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
768
- } else if (isSkipped) {
769
- html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
770
- } else {
771
- html += '<span style="color: hsl(var(--success));">data</span>';
772
- }
773
- html += '</td></tr>';
774
- }
775
-
776
- html += '</table>';
777
- if (previewDiv) previewDiv.innerHTML = html;
778
- } catch (err: any) {
779
- if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
780
- }
774
+ });
781
775
  };
782
776
 
783
- if (headerRowInput) headerRowInput.addEventListener('input', updatePreview);
784
- updatePreview();
777
+ updateHint(selectedSheetRow);
778
+ renderPreview(selectedSheetRow);
785
779
  this._reshapeModal.open();
786
780
  }
787
781
 
788
- private _escapeHtml(text: string): string {
789
- const div = document.createElement('div');
790
- div.textContent = text;
791
- return div.innerHTML;
792
- }
793
-
794
782
  private _showCSVReshapeModal(): void {
795
- if (!this._rawFileData) return;
783
+ if (!this._rawFileData?.text) return;
796
784
 
797
785
  this._cleanupReshapeModal();
798
786
 
787
+ const text = this._rawFileData.text;
788
+ const detected = (this._driver as any)._detectDelimiter(text);
789
+
790
+ // Parse raw lines
791
+ const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
792
+ const rawRows: { sheetRow: number; values: any[] }[] = [];
793
+ const maxPreviewRows = Math.min(lines.length, 15);
794
+
795
+ for (let i = 0; i < maxPreviewRows; i++) {
796
+ if (!lines[i]) {
797
+ rawRows.push({ sheetRow: i, values: [''] });
798
+ continue;
799
+ }
800
+ const values = (this._driver as any)._parseLine(lines[i], detected);
801
+ rawRows.push({ sheetRow: i, values });
802
+ }
803
+
804
+ // Auto-detect header row
805
+ let selectedRow = 0;
806
+ for (const { sheetRow, values } of rawRows) {
807
+ const nonEmpty = values.filter((v: string) => v.trim() !== '');
808
+ if (nonEmpty.length < values.length * 0.5) continue;
809
+ const nonNumeric = nonEmpty.filter((v: string) => {
810
+ const trimmed = v.trim();
811
+ return isNaN(Number(trimmed)) && trimmed !== '';
812
+ }).length;
813
+ if (nonNumeric >= nonEmpty.length * 0.7) {
814
+ selectedRow = sheetRow;
815
+ break;
816
+ }
817
+ }
818
+
799
819
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
800
820
  title: 'CSV Import Settings',
801
821
  size: 'large',
@@ -814,12 +834,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
814
834
  </select>
815
835
  </div>
816
836
  <div style="margin-bottom: 1rem;">
817
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based index)</label>
818
- <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
837
+ <div id="${this._id}-reshape-hint" style="padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
838
+ <input type="hidden" id="${this._id}-header-row" value="${selectedRow}" />
819
839
  </div>
820
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; margin-bottom: 1rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
821
- <div class="jux-reshape-preview-container">
822
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
840
+ <div>
841
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
823
842
  <div id="${this._id}-preview" style="font-family: monospace; font-size: 12px; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 0; overflow: hidden; max-height: 400px; overflow-y: auto;"></div>
824
843
  </div>
825
844
  `;
@@ -836,26 +855,24 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
836
855
  label: 'Apply & Re-import',
837
856
  variant: 'primary',
838
857
  click: async () => {
839
- if (!this._rawFileData?.text) return;
840
-
841
858
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
842
- const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
859
+ const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
843
860
 
844
861
  const delim = delimiterSelect.value;
845
- const headerRow = parseInt(headerRowInput.value) || 0;
862
+ const headerRow = parseInt(hiddenInput.value) || 0;
846
863
 
847
864
  this.state.loading = true;
848
865
  this._updateStatus('Re-parsing with new settings...', 'loading');
849
866
 
850
867
  try {
851
- const df = this._driver.parseCSV(this._rawFileData.text, {
868
+ const df = this._driver.parseCSV(this._rawFileData!.text!, {
852
869
  delimiter: delim,
853
870
  headerRow,
854
871
  hasHeader: true
855
872
  });
856
873
 
857
- await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
858
- this._setDataFrame(df, this._rawFileData.file.name);
874
+ await this._driver.store(this._rawFileData!.file.name, df, { source: this._rawFileData!.file.name });
875
+ this._setDataFrame(df, this._rawFileData!.file.name);
859
876
  this._reshapeModal!.closeModal();
860
877
  } catch (err: any) {
861
878
  this._updateStatus(`Error: ${err.message}`, 'error');
@@ -870,105 +887,62 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
870
887
 
871
888
  requestAnimationFrame(() => {
872
889
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
873
- const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
874
890
  const previewDiv = document.getElementById(`${this._id}-preview`)!;
875
891
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
892
+ const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
876
893
 
877
- if (this._rawFileData?.text) {
878
- const detected = (this._driver as any)._detectDelimiter(this._rawFileData.text);
879
- if (delimiterSelect) delimiterSelect.value = detected;
880
-
881
- const detectedHeaderRow = (this._driver as any)._detectHeaderRow(this._rawFileData.text, detected);
882
- if (headerRowInput) headerRowInput.value = String(detectedHeaderRow);
883
- }
894
+ if (delimiterSelect) delimiterSelect.value = detected;
884
895
 
885
- const updateHint = () => {
896
+ const updateHint = (row: number) => {
886
897
  if (!hintDiv) return;
887
- const headerRow = parseInt(headerRowInput?.value) || 0;
888
- if (headerRow > 0) {
889
- hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
898
+ const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
899
+ const headerNames = vals.filter((v: any) => v != null && String(v).trim() !== '').map((v: any) => String(v).trim());
900
+ const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
901
+ if (row > 0) {
902
+ hintDiv.innerHTML = `Row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows 0–${row - 1} will be skipped.`;
890
903
  } else {
891
- hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
904
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
892
905
  }
893
906
  };
894
907
 
895
- const updatePreview = () => {
896
- if (!this._rawFileData?.text) return;
897
-
908
+ const reparse = () => {
898
909
  const delim = delimiterSelect?.value || ',';
899
- const headerRow = parseInt(headerRowInput?.value) || 0;
900
- updateHint();
901
-
902
- try {
903
- const rawDf = this._driver.parseCSV(this._rawFileData.text, {
904
- delimiter: delim,
905
- headerRow: 0,
906
- hasHeader: true,
907
- maxRows: headerRow + 10
908
- });
909
-
910
- const rawCols = rawDf.columns;
911
- const rawRows = rawDf.toRows();
912
-
913
- let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
914
- const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
915
-
916
- for (let i = 0; i < totalRows; i++) {
917
- const isHeader = (i === headerRow);
918
- const isSkipped = (i < headerRow);
919
-
920
- let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
921
- if (isHeader) {
922
- rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
923
- } else if (isSkipped) {
924
- rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
925
- }
926
-
927
- html += `<tr style="${rowStyle}">`;
928
- html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
929
- html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
930
- html += '</td>';
931
-
932
- let values: any[];
933
- if (i === 0) {
934
- values = rawCols;
935
- } else if (i - 1 < rawRows.length) {
936
- values = Object.values(rawRows[i - 1]);
937
- } else {
938
- values = [];
939
- }
940
-
941
- values.slice(0, 6).forEach(val => {
942
- const displayVal = val != null ? String(val).substring(0, 25) : '';
943
- html += `<td style="padding: 6px 8px;">${this._escapeHtml(displayVal)}</td>`;
944
- });
945
-
946
- if (values.length > 6) {
947
- html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
948
- }
949
-
950
- html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
951
- if (isHeader) {
952
- html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
953
- } else if (isSkipped) {
954
- html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
955
- } else {
956
- html += '<span style="color: hsl(var(--success));">data</span>';
957
- }
958
- html += '</td></tr>';
910
+ rawRows.length = 0;
911
+ for (let i = 0; i < maxPreviewRows; i++) {
912
+ if (!lines[i]) {
913
+ rawRows.push({ sheetRow: i, values: [''] });
914
+ continue;
959
915
  }
960
-
961
- html += '</table>';
962
- if (previewDiv) previewDiv.innerHTML = html;
963
- } catch (err: any) {
964
- if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
916
+ const values = (this._driver as any)._parseLine(lines[i], delim);
917
+ rawRows.push({ sheetRow: i, values });
965
918
  }
966
919
  };
967
920
 
968
- if (delimiterSelect) delimiterSelect.addEventListener('change', updatePreview);
969
- if (headerRowInput) headerRowInput.addEventListener('input', updatePreview);
921
+ const renderPreview = (selected: number) => {
922
+ if (!previewDiv) return;
923
+ previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
924
+
925
+ previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
926
+ tr.addEventListener('click', () => {
927
+ const rowIdx = parseInt((tr as HTMLElement).dataset.sheetRow!);
928
+ hiddenInput.value = String(rowIdx);
929
+ updateHint(rowIdx);
930
+ renderPreview(rowIdx);
931
+ });
932
+ });
933
+ };
934
+
935
+ if (delimiterSelect) {
936
+ delimiterSelect.addEventListener('change', () => {
937
+ reparse();
938
+ const current = parseInt(hiddenInput.value) || 0;
939
+ updateHint(current);
940
+ renderPreview(current);
941
+ });
942
+ }
970
943
 
971
- updatePreview();
944
+ updateHint(selectedRow);
945
+ renderPreview(selectedRow);
972
946
  this._reshapeModal!.open();
973
947
  });
974
948
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.205",
3
+ "version": "1.1.206",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",