juxscript 1.1.205 → 1.1.207

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.
@@ -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,103 @@ 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
+ // Use the SAME cell-reading method as the parser
621
+ const rawRows = await this._driver.readRawExcelRows(this._rawFileData.file, 15);
622
+
623
+ if (rawRows.length === 0) return;
624
+
625
+ // Log what we got so we can verify alignment
626
+ console.log('[DataFrame Preview] Raw rows from readRawExcelRows:');
627
+ rawRows.forEach(r => console.log(` sheetRow ${r.sheetRow}:`, r.values.slice(0, 5)));
628
+
629
+ // Auto-detect best header row
630
+ let selectedSheetRow = rawRows[0].sheetRow;
631
+ for (const { sheetRow, values } of rawRows) {
632
+ const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
633
+ if (nonEmpty.length < values.length * 0.5) continue;
634
+ const nonNumeric = nonEmpty.filter(v => {
635
+ const str = String(v).trim();
636
+ return isNaN(Number(str)) && str !== '';
637
+ }).length;
638
+ if (nonNumeric >= nonEmpty.length * 0.7) {
639
+ selectedSheetRow = sheetRow;
640
+ break;
590
641
  }
591
- } catch {
592
- suggestedRow = 0;
593
642
  }
594
643
 
595
644
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
@@ -601,14 +650,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
601
650
 
602
651
  const modalContentHTML = `
603
652
  <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>
653
+ <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>
654
+ <input type="hidden" id="${this._id}-header-row" value="${selectedSheetRow}" />
609
655
  </div>
610
- <div class="jux-reshape-preview-container">
611
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
656
+ <div>
657
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
612
658
  <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
659
  </div>
614
660
  `;
@@ -628,6 +674,8 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
628
674
  const input = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
629
675
  const headerRow = parseInt(input.value) || 0;
630
676
 
677
+ console.log(`[DataFrame] Apply clicked: headerRow=${headerRow}`);
678
+
631
679
  this.state.loading = true;
632
680
  this._updateStatus('Re-parsing with new settings...', 'loading');
633
681
 
@@ -668,134 +716,79 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
668
716
 
669
717
  await new Promise(resolve => requestAnimationFrame(resolve));
670
718
 
671
- const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
672
719
  const previewDiv = document.getElementById(`${this._id}-preview`)!;
673
720
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
721
+ const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
674
722
 
675
- const updateHint = (headerRow: number) => {
723
+ const updateHint = (row: number) => {
676
724
  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.`;
725
+ const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
726
+ const headerNames = vals.filter((v: any) => v != null && String(v).trim() !== '').map((v: any) => String(v).trim());
727
+ const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
728
+ if (row > rawRows[0].sheetRow) {
729
+ hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
679
730
  } else {
680
- hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
731
+ hintDiv.innerHTML = `Sheet row <strong>${row}</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
681
732
  }
682
733
  };
683
734
 
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
735
+ const renderPreview = (selected: number) => {
736
+ if (!previewDiv) return;
737
+ previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
738
+
739
+ previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
740
+ tr.addEventListener('click', () => {
741
+ const rowIdx = parseInt((tr as HTMLElement).dataset.sheetRow!);
742
+ hiddenInput.value = String(rowIdx);
743
+ console.log(`[DataFrame Preview] Clicked sheetRow=${rowIdx}`);
744
+ updateHint(rowIdx);
745
+ renderPreview(rowIdx);
696
746
  });
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
- }
747
+ });
781
748
  };
782
749
 
783
- if (headerRowInput) headerRowInput.addEventListener('input', updatePreview);
784
- updatePreview();
750
+ updateHint(selectedSheetRow);
751
+ renderPreview(selectedSheetRow);
785
752
  this._reshapeModal.open();
786
753
  }
787
754
 
788
- private _escapeHtml(text: string): string {
789
- const div = document.createElement('div');
790
- div.textContent = text;
791
- return div.innerHTML;
792
- }
793
-
794
755
  private _showCSVReshapeModal(): void {
795
- if (!this._rawFileData) return;
756
+ if (!this._rawFileData?.text) return;
796
757
 
797
758
  this._cleanupReshapeModal();
798
759
 
760
+ const text = this._rawFileData.text;
761
+ const detected = (this._driver as any)._detectDelimiter(text);
762
+
763
+ // Parse raw lines
764
+ const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
765
+ const rawRows: { sheetRow: number; values: any[] }[] = [];
766
+ const maxPreviewRows = Math.min(lines.length, 15);
767
+
768
+ for (let i = 0; i < maxPreviewRows; i++) {
769
+ if (!lines[i]) {
770
+ rawRows.push({ sheetRow: i, values: [''] });
771
+ continue;
772
+ }
773
+ const values = (this._driver as any)._parseLine(lines[i], detected);
774
+ rawRows.push({ sheetRow: i, values });
775
+ }
776
+
777
+ // Auto-detect header row
778
+ let selectedRow = 0;
779
+ for (const { sheetRow, values } of rawRows) {
780
+ const nonEmpty = values.filter((v: string) => v.trim() !== '');
781
+ if (nonEmpty.length < values.length * 0.5) continue;
782
+ const nonNumeric = nonEmpty.filter((v: string) => {
783
+ const trimmed = v.trim();
784
+ return isNaN(Number(trimmed)) && trimmed !== '';
785
+ }).length;
786
+ if (nonNumeric >= nonEmpty.length * 0.7) {
787
+ selectedRow = sheetRow;
788
+ break;
789
+ }
790
+ }
791
+
799
792
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
800
793
  title: 'CSV Import Settings',
801
794
  size: 'large',
@@ -814,12 +807,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
814
807
  </select>
815
808
  </div>
816
809
  <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%;" />
810
+ <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>
811
+ <input type="hidden" id="${this._id}-header-row" value="${selectedRow}" />
819
812
  </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>
813
+ <div>
814
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Click a row to select it as the header:</div>
823
815
  <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
816
  </div>
825
817
  `;
@@ -836,26 +828,24 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
836
828
  label: 'Apply & Re-import',
837
829
  variant: 'primary',
838
830
  click: async () => {
839
- if (!this._rawFileData?.text) return;
840
-
841
831
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
842
- const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
832
+ const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
843
833
 
844
834
  const delim = delimiterSelect.value;
845
- const headerRow = parseInt(headerRowInput.value) || 0;
835
+ const headerRow = parseInt(hiddenInput.value) || 0;
846
836
 
847
837
  this.state.loading = true;
848
838
  this._updateStatus('Re-parsing with new settings...', 'loading');
849
839
 
850
840
  try {
851
- const df = this._driver.parseCSV(this._rawFileData.text, {
841
+ const df = this._driver.parseCSV(this._rawFileData!.text!, {
852
842
  delimiter: delim,
853
843
  headerRow,
854
844
  hasHeader: true
855
845
  });
856
846
 
857
- await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
858
- this._setDataFrame(df, this._rawFileData.file.name);
847
+ await this._driver.store(this._rawFileData!.file.name, df, { source: this._rawFileData!.file.name });
848
+ this._setDataFrame(df, this._rawFileData!.file.name);
859
849
  this._reshapeModal!.closeModal();
860
850
  } catch (err: any) {
861
851
  this._updateStatus(`Error: ${err.message}`, 'error');
@@ -870,105 +860,63 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
870
860
 
871
861
  requestAnimationFrame(() => {
872
862
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
873
- const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
874
863
  const previewDiv = document.getElementById(`${this._id}-preview`)!;
875
864
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
865
+ const hiddenInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
876
866
 
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
- }
867
+ if (delimiterSelect) delimiterSelect.value = detected;
884
868
 
885
- const updateHint = () => {
869
+ const updateHint = (row: number) => {
886
870
  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.`;
871
+ const vals = rawRows.find(r => r.sheetRow === row)?.values ?? [];
872
+ const headerNames = vals.filter((v: any) => v != null && String(v).trim() !== '').map((v: any) => String(v).trim());
873
+ const preview = headerNames.slice(0, 4).join(', ') + (headerNames.length > 4 ? '…' : '');
874
+ if (row > rawRows[0].sheetRow) {
875
+ hintDiv.innerHTML = `Sheet row <strong>${row}</strong> selected as header. Columns: <code>${this._escapeHtml(preview)}</code>. Rows before it will be skipped.`;
890
876
  } else {
891
- hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
877
+ hintDiv.innerHTML = `Sheet row <strong>${row}</strong> (first row) selected as header. Columns: <code>${this._escapeHtml(preview)}</code>`;
892
878
  }
893
879
  };
894
880
 
895
- const updatePreview = () => {
896
- if (!this._rawFileData?.text) return;
897
-
881
+ const reparse = () => {
898
882
  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>';
883
+ rawRows.length = 0;
884
+ for (let i = 0; i < maxPreviewRows; i++) {
885
+ if (!lines[i]) {
886
+ rawRows.push({ sheetRow: i, values: [''] });
887
+ continue;
959
888
  }
960
-
961
- html += '</table>';
962
- if (previewDiv) previewDiv.innerHTML = html;
963
- } catch (err: any) {
964
- if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
889
+ const values = (this._driver as any)._parseLine(lines[i], delim);
890
+ rawRows.push({ sheetRow: i, values });
965
891
  }
966
892
  };
967
893
 
968
- if (delimiterSelect) delimiterSelect.addEventListener('change', updatePreview);
969
- if (headerRowInput) headerRowInput.addEventListener('input', updatePreview);
894
+ const renderPreview = (selected: number) => {
895
+ if (!previewDiv) return;
896
+ previewDiv.innerHTML = this._buildClickablePreviewHTML(rawRows, selected);
897
+
898
+ previewDiv.querySelectorAll('tr[data-sheet-row]').forEach(tr => {
899
+ tr.addEventListener('click', () => {
900
+ const rowIdx = parseInt((tr as HTMLElement).dataset.sheetRow!);
901
+ hiddenInput.value = String(rowIdx);
902
+ console.log(`[DataFrame Preview] Clicked sheetRow=${rowIdx}`);
903
+ updateHint(rowIdx);
904
+ renderPreview(rowIdx);
905
+ });
906
+ });
907
+ };
908
+
909
+ if (delimiterSelect) {
910
+ delimiterSelect.addEventListener('change', () => {
911
+ reparse();
912
+ const current = parseInt(hiddenInput.value) || 0;
913
+ updateHint(current);
914
+ renderPreview(current);
915
+ });
916
+ }
970
917
 
971
- updatePreview();
918
+ updateHint(selectedRow);
919
+ renderPreview(selectedRow);
972
920
  this._reshapeModal!.open();
973
921
  });
974
922
  }
@@ -87,12 +87,18 @@ export declare class TabularDriver {
87
87
  * Fetch and stream-parse a remote CSV/TSV file
88
88
  */
89
89
  fetch(url: string, options?: ParseOptions): Promise<DataFrame>;
90
+ /**
91
+ * Read raw cell values from first sheet of an Excel file.
92
+ * Returns rows with their actual sheet row indices.
93
+ * Used by both the preview UI and the parser to ensure consistency.
94
+ */
95
+ readRawExcelRows(file: File, maxRows?: number): Promise<{
96
+ sheetRow: number;
97
+ values: any[];
98
+ }[]>;
90
99
  /**
91
100
  * ✅ FIXED: Stream Excel file with optional headerRow override
92
- * headerRow is 0-based: 0 = first row, 1 = second row, etc.
93
- *
94
- * Uses direct cell access instead of sheet_to_json to avoid
95
- * issues with blank row handling and sparse arrays.
101
+ * headerRow is the absolute sheet row index (same as sheetRow from readRawExcelRows).
96
102
  */
97
103
  streamFileMultiSheet(file: File, options?: ParseOptions): Promise<Record<string, DataFrame>>;
98
104
  private _splitLines;
@@ -1 +1 @@
1
- {"version":3,"file":"TabularDriver.d.ts","sourceRoot":"","sources":["TabularDriver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TabularDriver.d.ts","sourceRoot":"","sources":["TabularDriver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,qBAAa,aAAa;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,GAAG,CAA4B;gBAE3B,MAAM,GAAE,MAAsB,EAAE,SAAS,GAAE,MAAiB;IAKlE,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC;IA4BlC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAgCxB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAwBxB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,SAAS;IA6D7D;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,SAAS,CAAC;IAoG5E;;OAEG;YACW,UAAU;IAuExB;;OAEG;IACG,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAiBlD;;OAEG;IACG,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAuBzF;;OAEG;IACG,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAwBjD;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA4BzD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAqBlH;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB5B;;OAEG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,SAAS,CAAC;IA0ExE;;;;OAIG;IACG,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,MAAW,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,GAAG,EAAE,CAAA;KAAE,EAAE,CAAC;IA6CxG;;;OAGG;IACG,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAwItG,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,UAAU;IAmClB,OAAO,CAAC,SAAS;IAYjB,KAAK,IAAI,IAAI;CAMhB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,aAAa,CAEhF"}