juxscript 1.1.204 → 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.
@@ -217,6 +217,11 @@ export class DataFrameComponent extends BaseComponent {
217
217
  }
218
218
  });
219
219
  const sheetNames = Object.keys(sheets);
220
+ if (sheetNames.length === 0) {
221
+ this._updateStatus('No data found in file', 'error');
222
+ this.state.loading = false;
223
+ return;
224
+ }
220
225
  await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
221
226
  if (sheetNames.length > 1) {
222
227
  this._renderMultiSheet(sheets, file.name);
@@ -251,7 +256,6 @@ export class DataFrameComponent extends BaseComponent {
251
256
  const wrapper = document.getElementById(this._id);
252
257
  if (!wrapper)
253
258
  return;
254
- // Clean up existing content
255
259
  const existingTable = wrapper.querySelector('.jux-table-wrapper');
256
260
  if (existingTable)
257
261
  existingTable.remove();
@@ -262,11 +266,10 @@ export class DataFrameComponent extends BaseComponent {
262
266
  this._sheets.set(name, df);
263
267
  });
264
268
  const sheetNames = Object.keys(sheets);
265
- // Build tabs using the Tabs component
266
269
  const tabDefs = sheetNames.map(name => ({
267
270
  id: name,
268
271
  label: name,
269
- content: '' // Content will be added after render
272
+ content: ''
270
273
  }));
271
274
  this._tabs = new Tabs(`${this._id}-tabs`, {
272
275
  tabs: tabDefs,
@@ -275,15 +278,12 @@ export class DataFrameComponent extends BaseComponent {
275
278
  this._tabs.bind('tabChange', (tabId) => {
276
279
  this._df = this._sheets.get(tabId) || null;
277
280
  });
278
- // Create container for tabs
279
281
  const tabsContainer = document.createElement('div');
280
282
  tabsContainer.className = 'jux-dataframe-tabs';
281
283
  wrapper.appendChild(tabsContainer);
282
284
  this._tabs.render(tabsContainer);
283
- // Now render tables into each tab panel
284
- sheetNames.forEach((sheetName, idx) => {
285
+ sheetNames.forEach((sheetName) => {
285
286
  const df = sheets[sheetName];
286
- const panelId = `${this._id}-tabs-${sheetName}-panel`;
287
287
  const table = new Table(`${this._id}-table-${sheetName}`, {
288
288
  striped: this._tableOptions.striped,
289
289
  hoverable: this._tableOptions.hoverable,
@@ -294,48 +294,13 @@ export class DataFrameComponent extends BaseComponent {
294
294
  });
295
295
  const columnDefs = df.columns.map(col => ({ key: col, label: col }));
296
296
  table.columns(columnDefs).rows(df.toRows());
297
- // Add settings button to tab panel
298
297
  const settingsBtn = new Button(`${this._id}-settings-${sheetName}`, {
299
298
  label: '⚙️ Import Settings',
300
299
  variant: 'ghost',
301
300
  size: 'small'
302
301
  });
303
302
  settingsBtn.bind('click', () => this._showReshapeModal());
304
- // Use addTabContent to add components
305
303
  this._tabs.addTabContent(sheetName, [settingsBtn, table]);
306
- if (this._tableOptions.filterable) {
307
- // Add filter input above table
308
- const panel = document.getElementById(panelId);
309
- if (panel) {
310
- const filterContainer = document.createElement('div');
311
- filterContainer.className = 'jux-dataframe-filter';
312
- const input = document.createElement('input');
313
- input.type = 'text';
314
- input.placeholder = `Filter ${sheetName}...`;
315
- input.className = 'jux-input-element jux-dataframe-filter-input';
316
- const iconEl = renderIcon('search');
317
- iconEl.style.width = '16px';
318
- iconEl.style.height = '16px';
319
- const iconWrap = document.createElement('span');
320
- iconWrap.className = 'jux-dataframe-filter-icon';
321
- iconWrap.appendChild(iconEl);
322
- filterContainer.appendChild(iconWrap);
323
- filterContainer.appendChild(input);
324
- input.addEventListener('input', () => {
325
- const text = input.value.toLowerCase();
326
- if (!text) {
327
- table.rows(df.toRows());
328
- return;
329
- }
330
- const filtered = df.filter((row) => Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text)));
331
- table.rows(filtered.toRows());
332
- });
333
- const tableWrapper = panel.querySelector('.jux-table-wrapper');
334
- if (tableWrapper) {
335
- panel.insertBefore(filterContainer, tableWrapper);
336
- }
337
- }
338
- }
339
304
  });
340
305
  const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
341
306
  this._updateStatus(`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`, 'success');
@@ -378,7 +343,6 @@ export class DataFrameComponent extends BaseComponent {
378
343
  const emptyColumns = cols.filter(c => {
379
344
  if (!c.startsWith('__EMPTY'))
380
345
  return false;
381
- // Check if ALL values in this column are null/empty
382
346
  return rows.every(row => {
383
347
  const val = row[c];
384
348
  return val === null || val === undefined || String(val).trim() === '';
@@ -411,7 +375,6 @@ export class DataFrameComponent extends BaseComponent {
411
375
  }
412
376
  else {
413
377
  this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols`, 'success');
414
- // Always show settings button if we have raw file data
415
378
  if (this._rawFileData) {
416
379
  requestAnimationFrame(() => {
417
380
  const statusEl = document.getElementById(`${this._id}-status`);
@@ -453,27 +416,6 @@ export class DataFrameComponent extends BaseComponent {
453
416
  }
454
417
  return false;
455
418
  }
456
- _detectLikelyHeaderRow(df) {
457
- const rows = df.toRows();
458
- for (let i = 0; i < Math.min(rows.length, 10); i++) {
459
- const row = rows[i];
460
- const values = Object.values(row);
461
- const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
462
- if (nonEmpty.length < values.length * 0.5)
463
- continue;
464
- const nonNumericCount = nonEmpty.filter(v => {
465
- const str = String(v).trim();
466
- return isNaN(Number(str)) && str !== '';
467
- }).length;
468
- if (nonNumericCount >= nonEmpty.length * 0.7 && i > 0) {
469
- // i is the index in toRows(). Row 0 of the file was already consumed
470
- // as the header during the initial parse, so the actual 0-based file
471
- // row index is i + 1.
472
- return i + 1;
473
- }
474
- }
475
- return 0;
476
- }
477
419
  /* ═══════════════════════════════════════════════════
478
420
  * RESHAPE MODAL
479
421
  * ═══════════════════════════════════════════════════ */
@@ -496,24 +438,121 @@ export class DataFrameComponent extends BaseComponent {
496
438
  this._reshapeModalRendered = false;
497
439
  }
498
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
+ }
499
501
  async _showExcelReshapeModal() {
500
502
  if (!this._rawFileData?.file)
501
503
  return;
502
504
  this._cleanupReshapeModal();
503
- // Always detect from a fresh raw parse, not from current _df
504
- let suggestedRow = 0;
505
- try {
506
- const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
507
- headerRow: 0,
508
- maxSheetSize: 20
509
- });
510
- const rawSheet = Object.values(rawSheets)[0];
511
- if (rawSheet) {
512
- 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));
513
539
  }
540
+ rawRows.push({ sheetRow: r, values });
514
541
  }
515
- catch {
516
- 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
+ }
517
556
  }
518
557
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
519
558
  title: 'Excel Import Settings',
@@ -523,25 +562,11 @@ export class DataFrameComponent extends BaseComponent {
523
562
  });
524
563
  const modalContentHTML = `
525
564
  <div style="margin-bottom: 1rem;">
526
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
527
- Header Row (0-based index)
528
- </label>
529
- <input
530
- type="number"
531
- id="${this._id}-header-row"
532
- class="jux-input-element"
533
- value="${suggestedRow}"
534
- min="0"
535
- max="50"
536
- style="width: 100%;"
537
- />
538
- <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;">
539
- </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}" />
540
567
  </div>
541
- <div class="jux-reshape-preview-container">
542
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
543
- Preview
544
- </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>
545
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>
546
571
  </div>
547
572
  `;
@@ -592,122 +617,73 @@ export class DataFrameComponent extends BaseComponent {
592
617
  this._reshapeModal.render(document.body);
593
618
  this._reshapeModalRendered = true;
594
619
  await new Promise(resolve => requestAnimationFrame(resolve));
595
- const headerRowInput = document.getElementById(`${this._id}-header-row`);
596
620
  const previewDiv = document.getElementById(`${this._id}-preview`);
597
621
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
598
- const updateHint = (headerRow) => {
622
+ const hiddenInput = document.getElementById(`${this._id}-header-row`);
623
+ const updateHint = (row) => {
599
624
  if (!hintDiv)
600
625
  return;
601
- if (headerRow > 0) {
602
- hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
603
- `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.`;
604
631
  }
605
632
  else {
606
- 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>`;
607
634
  }
608
635
  };
609
- const updatePreview = async () => {
610
- const headerRow = parseInt(headerRowInput?.value) || 0;
611
- updateHint(headerRow);
612
- try {
613
- // ALWAYS parse with headerRow=0 to get raw file structure
614
- const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
615
- headerRow: 0,
616
- maxSheetSize: Math.max(headerRow + 12, 15)
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);
617
647
  });
618
- const rawSheet = Object.values(rawSheets)[0];
619
- if (!rawSheet) {
620
- if (previewDiv)
621
- previewDiv.textContent = 'No data found';
622
- return;
623
- }
624
- // rawSheet.columns = row 0 values (when parsed with headerRow=0)
625
- // rawSheet.toRows() = rows 1+ (data rows when parsed with headerRow=0)
626
- const rawCols = rawSheet.columns;
627
- const rawRows = rawSheet.toRows();
628
- // Build HTML table showing raw file structure
629
- let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
630
- // We need to show rows 0 through headerRow+7 (or so)
631
- // Row 0 = rawCols, Row 1+ = rawRows[i-1]
632
- const totalRowsToShow = Math.min(headerRow + 8, rawRows.length + 1);
633
- for (let fileRow = 0; fileRow < totalRowsToShow; fileRow++) {
634
- const isHeader = (fileRow === headerRow);
635
- const isSkipped = (fileRow < headerRow);
636
- let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
637
- if (isHeader) {
638
- rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
639
- }
640
- else if (isSkipped) {
641
- rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
642
- }
643
- html += `<tr style="${rowStyle}">`;
644
- // Row index cell
645
- 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;">`;
646
- if (isHeader) {
647
- html += `<span style="color: hsl(142 71% 45%);">▶ ${fileRow}</span>`;
648
- }
649
- else {
650
- html += `${fileRow}`;
651
- }
652
- html += '</td>';
653
- // Get values for this file row
654
- let values;
655
- if (fileRow === 0) {
656
- values = rawCols;
657
- }
658
- else {
659
- values = rawRows[fileRow - 1] ? Object.values(rawRows[fileRow - 1]) : [];
660
- }
661
- // Show first 6 columns
662
- const displayCols = values.slice(0, 6);
663
- displayCols.forEach(val => {
664
- const displayVal = val != null ? String(val).substring(0, 20) : '';
665
- const cellStyle = isHeader
666
- ? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
667
- : 'padding: 8px 12px;';
668
- html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
669
- });
670
- if (values.length > 6) {
671
- html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
672
- }
673
- // Status badge cell
674
- html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap;">`;
675
- if (isHeader) {
676
- html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
677
- }
678
- else if (isSkipped) {
679
- html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
680
- }
681
- else {
682
- html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
683
- }
684
- html += '</td>';
685
- html += '</tr>';
686
- }
687
- html += '</table>';
688
- if (previewDiv) {
689
- previewDiv.innerHTML = html;
690
- }
691
- }
692
- catch (err) {
693
- if (previewDiv)
694
- previewDiv.textContent = `Error: ${err.message}`;
695
- }
648
+ });
696
649
  };
697
- if (headerRowInput)
698
- headerRowInput.addEventListener('input', updatePreview);
699
- updatePreview();
650
+ updateHint(selectedSheetRow);
651
+ renderPreview(selectedSheetRow);
700
652
  this._reshapeModal.open();
701
653
  }
702
- _escapeHtml(text) {
703
- const div = document.createElement('div');
704
- div.textContent = text;
705
- return div.innerHTML;
706
- }
707
654
  _showCSVReshapeModal() {
708
- if (!this._rawFileData)
655
+ if (!this._rawFileData?.text)
709
656
  return;
710
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
+ }
711
687
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
712
688
  title: 'CSV Import Settings',
713
689
  size: 'large',
@@ -725,12 +701,11 @@ export class DataFrameComponent extends BaseComponent {
725
701
  </select>
726
702
  </div>
727
703
  <div style="margin-bottom: 1rem;">
728
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based index)</label>
729
- <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}" />
730
706
  </div>
731
- <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>
732
- <div class="jux-reshape-preview-container">
733
- <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>
734
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>
735
710
  </div>
736
711
  `;
@@ -746,12 +721,10 @@ export class DataFrameComponent extends BaseComponent {
746
721
  label: 'Apply & Re-import',
747
722
  variant: 'primary',
748
723
  click: async () => {
749
- if (!this._rawFileData?.text)
750
- return;
751
724
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
752
- const headerRowInput = document.getElementById(`${this._id}-header-row`);
725
+ const hiddenInput = document.getElementById(`${this._id}-header-row`);
753
726
  const delim = delimiterSelect.value;
754
- const headerRow = parseInt(headerRowInput.value) || 0;
727
+ const headerRow = parseInt(hiddenInput.value) || 0;
755
728
  this.state.loading = true;
756
729
  this._updateStatus('Re-parsing with new settings...', 'loading');
757
730
  try {
@@ -775,109 +748,59 @@ export class DataFrameComponent extends BaseComponent {
775
748
  this._reshapeModalRendered = true;
776
749
  requestAnimationFrame(() => {
777
750
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
778
- const headerRowInput = document.getElementById(`${this._id}-header-row`);
779
751
  const previewDiv = document.getElementById(`${this._id}-preview`);
780
752
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
781
- if (this._rawFileData?.text) {
782
- const detected = this._driver._detectDelimiter(this._rawFileData.text);
783
- if (delimiterSelect)
784
- delimiterSelect.value = detected;
785
- const detectedHeaderRow = this._driver._detectHeaderRow(this._rawFileData.text, detected);
786
- if (headerRowInput)
787
- headerRowInput.value = String(detectedHeaderRow);
788
- }
789
- const updateHint = () => {
753
+ const hiddenInput = document.getElementById(`${this._id}-header-row`);
754
+ if (delimiterSelect)
755
+ delimiterSelect.value = detected;
756
+ const updateHint = (row) => {
790
757
  if (!hintDiv)
791
758
  return;
792
- const headerRow = parseInt(headerRowInput?.value) || 0;
793
- if (headerRow > 0) {
794
- hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
795
- `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.`;
796
764
  }
797
765
  else {
798
- 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>`;
799
767
  }
800
768
  };
801
- const updatePreview = () => {
802
- if (!this._rawFileData?.text)
803
- return;
769
+ const reparse = () => {
804
770
  const delim = delimiterSelect?.value || ',';
805
- const headerRow = parseInt(headerRowInput?.value) || 0;
806
- updateHint();
807
- try {
808
- // Parse raw to show all rows
809
- const rawDf = this._driver.parseCSV(this._rawFileData.text, {
810
- delimiter: delim,
811
- headerRow: 0,
812
- hasHeader: true,
813
- maxRows: headerRow + 10
814
- });
815
- const rawCols = rawDf.columns;
816
- const rawRows = rawDf.toRows();
817
- // Build HTML table
818
- let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
819
- const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
820
- for (let i = 0; i < totalRows; i++) {
821
- const isHeader = (i === headerRow);
822
- const isSkipped = (i < headerRow);
823
- let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
824
- if (isHeader) {
825
- rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
826
- }
827
- else if (isSkipped) {
828
- rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
829
- }
830
- html += `<tr style="${rowStyle}">`;
831
- // Row index
832
- html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
833
- html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
834
- html += '</td>';
835
- // Data
836
- let values;
837
- if (i === 0) {
838
- values = rawCols;
839
- }
840
- else if (i - 1 < rawRows.length) {
841
- values = Object.values(rawRows[i - 1]);
842
- }
843
- else {
844
- values = [];
845
- }
846
- values.slice(0, 6).forEach(val => {
847
- const displayVal = val != null ? String(val).substring(0, 25) : '';
848
- html += `<td style="padding: 6px 8px;">${this._escapeHtml(displayVal)}</td>`;
849
- });
850
- if (values.length > 6) {
851
- html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
852
- }
853
- // Status
854
- html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
855
- if (isHeader) {
856
- html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
857
- }
858
- else if (isSkipped) {
859
- html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
860
- }
861
- else {
862
- html += '<span style="color: hsl(var(--success));">data</span>';
863
- }
864
- html += '</td>';
865
- html += '</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;
866
776
  }
867
- html += '</table>';
868
- if (previewDiv)
869
- previewDiv.innerHTML = html;
870
- }
871
- catch (err) {
872
- if (previewDiv)
873
- previewDiv.textContent = `Error: ${err.message}`;
777
+ const values = this._driver._parseLine(lines[i], delim);
778
+ rawRows.push({ sheetRow: i, values });
874
779
  }
875
780
  };
876
- if (delimiterSelect)
877
- delimiterSelect.addEventListener('change', updatePreview);
878
- if (headerRowInput)
879
- headerRowInput.addEventListener('input', updatePreview);
880
- 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);
881
804
  this._reshapeModal.open();
882
805
  });
883
806
  }