juxscript 1.1.196 → 1.1.198

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.
@@ -91,6 +91,7 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
91
91
  private _showReshapeModal;
92
92
  private _cleanupReshapeModal;
93
93
  private _showExcelReshapeModal;
94
+ private _escapeHtml;
94
95
  private _showCSVReshapeModal;
95
96
  update(_prop: string, _value: any): void;
96
97
  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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"}
@@ -3,7 +3,9 @@ import { DataFrame } from '../storage/DataFrame.js';
3
3
  import { TabularDriver } from '../storage/TabularDriver.js';
4
4
  import { FileUpload } from './fileupload.js';
5
5
  import { Table } from './table.js';
6
+ import { Tabs } from './tabs.js';
6
7
  import { Modal } from './modal.js';
8
+ import { Button } from './button.js';
7
9
  import { renderIcon } from './icons.js';
8
10
  const TRIGGER_EVENTS = [];
9
11
  const CALLBACK_EVENTS = ['load', 'error', 'transform'];
@@ -249,65 +251,39 @@ export class DataFrameComponent extends BaseComponent {
249
251
  const wrapper = document.getElementById(this._id);
250
252
  if (!wrapper)
251
253
  return;
254
+ // Clean up existing content
252
255
  const existingTable = wrapper.querySelector('.jux-table-wrapper');
253
256
  if (existingTable)
254
257
  existingTable.remove();
255
- const existingTabs = wrapper.querySelector('.jux-dataframe-tabs');
258
+ const existingTabs = wrapper.querySelector('.jux-tabs');
256
259
  if (existingTabs)
257
260
  existingTabs.remove();
258
261
  Object.entries(sheets).forEach(([name, df]) => {
259
262
  this._sheets.set(name, df);
260
263
  });
261
264
  const sheetNames = Object.keys(sheets);
262
- // Build custom tab bar with settings cogs
265
+ // Build tabs using the Tabs component
266
+ const tabDefs = sheetNames.map(name => ({
267
+ id: name,
268
+ label: name,
269
+ content: '' // Content will be added after render
270
+ }));
271
+ this._tabs = new Tabs(`${this._id}-tabs`, {
272
+ tabs: tabDefs,
273
+ activeTab: sheetNames[0]
274
+ });
275
+ this._tabs.bind('tabChange', (tabId) => {
276
+ this._df = this._sheets.get(tabId) || null;
277
+ });
278
+ // Create container for tabs
263
279
  const tabsContainer = document.createElement('div');
264
280
  tabsContainer.className = 'jux-dataframe-tabs';
265
- const tabList = document.createElement('div');
266
- tabList.className = 'jux-tabs-list';
267
- sheetNames.forEach((sheetName, idx) => {
268
- const tabBtn = document.createElement('button');
269
- tabBtn.className = 'jux-tabs-button' + (idx === 0 ? ' jux-tabs-button-active' : '');
270
- tabBtn.setAttribute('data-sheet', sheetName);
271
- const labelSpan = document.createElement('span');
272
- labelSpan.textContent = sheetName;
273
- tabBtn.appendChild(labelSpan);
274
- // Add settings cog icon
275
- const cogBtn = document.createElement('button');
276
- cogBtn.className = 'jux-dataframe-tab-settings';
277
- cogBtn.title = 'Import settings for ' + sheetName;
278
- cogBtn.innerHTML = `<span class="iconify" data-icon="lucide:settings" style="width:14px;height:14px;"></span>`;
279
- cogBtn.addEventListener('click', (e) => {
280
- e.stopPropagation();
281
- this._showReshapeModal();
282
- });
283
- tabBtn.appendChild(cogBtn);
284
- tabBtn.addEventListener('click', () => {
285
- // Switch active tab
286
- tabList.querySelectorAll('.jux-tabs-button').forEach(btn => {
287
- btn.classList.remove('jux-tabs-button-active');
288
- });
289
- tabBtn.classList.add('jux-tabs-button-active');
290
- // Show/hide panels
291
- wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
292
- panel.style.display = 'none';
293
- });
294
- const panel = document.getElementById(`${this._id}-panel-${sheetName}`);
295
- if (panel)
296
- panel.style.display = 'block';
297
- // Update current df reference
298
- this._df = this._sheets.get(sheetName) || null;
299
- });
300
- tabList.appendChild(tabBtn);
301
- });
302
- tabsContainer.appendChild(tabList);
303
281
  wrapper.appendChild(tabsContainer);
304
- // Create panels for each sheet
282
+ this._tabs.render(tabsContainer);
283
+ // Now render tables into each tab panel
305
284
  sheetNames.forEach((sheetName, idx) => {
306
285
  const df = sheets[sheetName];
307
- const panel = document.createElement('div');
308
- panel.className = 'jux-tabs-panel';
309
- panel.id = `${this._id}-panel-${sheetName}`;
310
- panel.style.display = idx === 0 ? 'block' : 'none';
286
+ const panelId = `${this._id}-tabs-${sheetName}-panel`;
311
287
  const table = new Table(`${this._id}-table-${sheetName}`, {
312
288
  striped: this._tableOptions.striped,
313
289
  hoverable: this._tableOptions.hoverable,
@@ -318,44 +294,49 @@ export class DataFrameComponent extends BaseComponent {
318
294
  });
319
295
  const columnDefs = df.columns.map(col => ({ key: col, label: col }));
320
296
  table.columns(columnDefs).rows(df.toRows());
321
- wrapper.appendChild(panel);
322
- table.render(panel);
297
+ // Add settings button to tab panel
298
+ const settingsBtn = new Button(`${this._id}-settings-${sheetName}`, {
299
+ label: '⚙️ Import Settings',
300
+ variant: 'ghost',
301
+ size: 'small'
302
+ });
303
+ settingsBtn.bind('click', () => this._showReshapeModal());
304
+ // Use addTabContent to add components
305
+ this._tabs.addTabContent(sheetName, [settingsBtn, table]);
323
306
  if (this._tableOptions.filterable) {
324
- const filterContainer = document.createElement('div');
325
- filterContainer.className = 'jux-dataframe-filter';
326
- const input = document.createElement('input');
327
- input.type = 'text';
328
- input.placeholder = `Filter ${sheetName}...`;
329
- input.className = 'jux-input-element jux-dataframe-filter-input';
330
- const iconEl = renderIcon('search');
331
- iconEl.style.width = '16px';
332
- iconEl.style.height = '16px';
333
- const iconWrap = document.createElement('span');
334
- iconWrap.className = 'jux-dataframe-filter-icon';
335
- iconWrap.appendChild(iconEl);
336
- filterContainer.appendChild(iconWrap);
337
- filterContainer.appendChild(input);
338
- input.addEventListener('input', () => {
339
- const text = input.value.toLowerCase();
340
- if (!text) {
341
- table.rows(df.toRows());
342
- return;
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);
343
336
  }
344
- const filtered = df.filter((row) => Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text)));
345
- table.rows(filtered.toRows());
346
- });
347
- const tableWrapper = panel.querySelector('.jux-table-wrapper');
348
- if (tableWrapper) {
349
- panel.insertBefore(filterContainer, tableWrapper);
350
337
  }
351
338
  }
352
339
  });
353
- // Ensure Iconify renders the cog icons
354
- requestAnimationFrame(() => {
355
- if (window.Iconify) {
356
- window.Iconify.scan();
357
- }
358
- });
359
340
  const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
360
341
  this._updateStatus(`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`, 'success');
361
342
  this._df = sheets[sheetNames[0]];
@@ -505,7 +486,21 @@ export class DataFrameComponent extends BaseComponent {
505
486
  if (!this._rawFileData?.file)
506
487
  return;
507
488
  this._cleanupReshapeModal();
508
- const suggestedRow = this._df ? this._detectLikelyHeaderRow(this._df) : 0;
489
+ // Always detect from a fresh raw parse, not from current _df
490
+ let suggestedRow = 0;
491
+ try {
492
+ const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
493
+ headerRow: 0,
494
+ maxSheetSize: 20
495
+ });
496
+ const rawSheet = Object.values(rawSheets)[0];
497
+ if (rawSheet) {
498
+ suggestedRow = this._detectLikelyHeaderRow(rawSheet);
499
+ }
500
+ }
501
+ catch {
502
+ suggestedRow = 0;
503
+ }
509
504
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
510
505
  title: 'Excel Import Settings',
511
506
  size: 'large',
@@ -515,7 +510,7 @@ export class DataFrameComponent extends BaseComponent {
515
510
  const modalContentHTML = `
516
511
  <div style="margin-bottom: 1rem;">
517
512
  <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
518
- Header Row
513
+ Header Row (0-based index)
519
514
  </label>
520
515
  <input
521
516
  type="number"
@@ -533,7 +528,7 @@ export class DataFrameComponent extends BaseComponent {
533
528
  <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
534
529
  Preview
535
530
  </div>
536
- <div id="${this._id}-preview" style="font-family: monospace; font-size: 0.75rem; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 1rem; overflow-x: auto; white-space: pre; max-height: 400px; overflow-y: auto;"></div>
531
+ <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>
537
532
  </div>
538
533
  `;
539
534
  this._reshapeModal
@@ -585,20 +580,20 @@ export class DataFrameComponent extends BaseComponent {
585
580
  return;
586
581
  if (headerRow > 0) {
587
582
  hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
588
- `Rows <strong>0–${headerRow - 1}</strong> will be skipped (not imported).`;
583
+ `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
589
584
  }
590
585
  else {
591
- hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers. No rows will be skipped.`;
586
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
592
587
  }
593
588
  };
594
589
  const updatePreview = async () => {
595
590
  const headerRow = parseInt(headerRowInput?.value) || 0;
596
591
  updateHint(headerRow);
597
592
  try {
598
- // Get raw data (headerRow=0) to show ALL rows including skipped ones
593
+ // ALWAYS parse with headerRow=0 to get raw file structure
599
594
  const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
600
595
  headerRow: 0,
601
- maxSheetSize: headerRow + 12
596
+ maxSheetSize: Math.max(headerRow + 12, 15)
602
597
  });
603
598
  const rawSheet = Object.values(rawSheets)[0];
604
599
  if (!rawSheet) {
@@ -606,65 +601,72 @@ export class DataFrameComponent extends BaseComponent {
606
601
  previewDiv.textContent = 'No data found';
607
602
  return;
608
603
  }
604
+ // rawSheet.columns = row 0 values (when parsed with headerRow=0)
605
+ // rawSheet.toRows() = rows 1+ (data rows when parsed with headerRow=0)
609
606
  const rawCols = rawSheet.columns;
610
607
  const rawRows = rawSheet.toRows();
611
- const colWidth = 20;
612
- const lines = [];
613
- // === SKIPPED ROWS SECTION ===
614
- if (headerRow > 0) {
615
- lines.push('┌─────────────────────────────────────────────────────────────────────────────────┐');
616
- lines.push(`│ SKIPPED: Rows 0–${headerRow - 1} will NOT be imported │`);
617
- lines.push('└─────────────────────────────────────────────────────────────────────────────────┘');
618
- lines.push('');
619
- // Row 0 is the raw file header (becomes column names when headerRow=0)
620
- const row0Line = rawCols.slice(0, 5).map(c => String(c ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
621
- lines.push(` [0] ${row0Line}${rawCols.length > 5 ? ' ...' : ''}`);
622
- // Rows 1 to headerRow-1 are skipped data rows
623
- for (let i = 0; i < Math.min(headerRow - 1, rawRows.length); i++) {
624
- const row = rawRows[i];
625
- const vals = Object.values(row).slice(0, 5).map(v => String(v ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
626
- lines.push(` [${i + 1}] ${vals}${Object.values(row).length > 5 ? ' ...' : ''}`);
608
+ // Build HTML table showing raw file structure
609
+ let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
610
+ // We need to show rows 0 through headerRow+7 (or so)
611
+ // Row 0 = rawCols, Row 1+ = rawRows[i-1]
612
+ const totalRowsToShow = Math.min(headerRow + 8, rawRows.length + 1);
613
+ for (let fileRow = 0; fileRow < totalRowsToShow; fileRow++) {
614
+ const isHeader = (fileRow === headerRow);
615
+ const isSkipped = (fileRow < headerRow);
616
+ let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
617
+ if (isHeader) {
618
+ rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
627
619
  }
628
- lines.push('');
629
- }
630
- // === HEADER ROW (will become column names) ===
631
- lines.push('╔═════════════════════════════════════════════════════════════════════════════════╗');
632
- lines.push(`║ HEADER ROW ${headerRow} — These values become your column names ║`);
633
- lines.push('╚═════════════════════════════════════════════════════════════════════════════════╝');
634
- lines.push('');
635
- // The header row content - if headerRow=0, it's rawCols, else it's rawRows[headerRow-1]
636
- let headerValues;
637
- if (headerRow === 0) {
638
- headerValues = rawCols;
639
- }
640
- else if (headerRow - 1 < rawRows.length) {
641
- headerValues = Object.values(rawRows[headerRow - 1]).map(v => String(v ?? ''));
642
- }
643
- else {
644
- headerValues = rawCols; // fallback
645
- }
646
- const headerLine = headerValues.slice(0, 5).map(c => String(c ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
647
- lines.push(`▶ [${headerRow}] ${headerLine}${headerValues.length > 5 ? ' ...' : ''}`);
648
- lines.push('');
649
- // === DATA ROWS ===
650
- lines.push('────────────────────────────────────────────────────────────────────────────────────');
651
- lines.push(' DATA ROWS (will be imported):');
652
- lines.push('');
653
- // Data starts at rawRows[headerRow] (since rawRows is 0-indexed after the header)
654
- const dataStartIdx = headerRow;
655
- const dataRows = rawRows.slice(dataStartIdx, dataStartIdx + 6);
656
- if (dataRows.length === 0) {
657
- lines.push(' (No data rows found after header)');
658
- }
659
- else {
660
- dataRows.forEach((row, i) => {
661
- const rowIdx = headerRow + 1 + i;
662
- const vals = Object.values(row).slice(0, 5).map(v => String(v ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
663
- lines.push(` [${rowIdx}] ${vals}${Object.values(row).length > 5 ? ' ...' : ''}`);
620
+ else if (isSkipped) {
621
+ rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
622
+ }
623
+ html += `<tr style="${rowStyle}">`;
624
+ // Row index cell
625
+ 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;">`;
626
+ if (isHeader) {
627
+ html += `<span style="color: hsl(142 71% 45%);">▶ ${fileRow}</span>`;
628
+ }
629
+ else {
630
+ html += `${fileRow}`;
631
+ }
632
+ html += '</td>';
633
+ // Get values for this file row
634
+ let values;
635
+ if (fileRow === 0) {
636
+ values = rawCols;
637
+ }
638
+ else {
639
+ values = rawRows[fileRow - 1] ? Object.values(rawRows[fileRow - 1]) : [];
640
+ }
641
+ // Show first 6 columns
642
+ const displayCols = values.slice(0, 6);
643
+ displayCols.forEach(val => {
644
+ const displayVal = val != null ? String(val).substring(0, 20) : '';
645
+ const cellStyle = isHeader
646
+ ? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
647
+ : 'padding: 8px 12px;';
648
+ html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
664
649
  });
650
+ if (values.length > 6) {
651
+ html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
652
+ }
653
+ // Status badge cell
654
+ html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap;">`;
655
+ if (isHeader) {
656
+ html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
657
+ }
658
+ else if (isSkipped) {
659
+ html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
660
+ }
661
+ else {
662
+ html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
663
+ }
664
+ html += '</td>';
665
+ html += '</tr>';
665
666
  }
667
+ html += '</table>';
666
668
  if (previewDiv) {
667
- previewDiv.textContent = lines.join('\n');
669
+ previewDiv.innerHTML = html;
668
670
  }
669
671
  }
670
672
  catch (err) {
@@ -677,6 +679,11 @@ export class DataFrameComponent extends BaseComponent {
677
679
  updatePreview();
678
680
  this._reshapeModal.open();
679
681
  }
682
+ _escapeHtml(text) {
683
+ const div = document.createElement('div');
684
+ div.textContent = text;
685
+ return div.innerHTML;
686
+ }
680
687
  _showCSVReshapeModal() {
681
688
  if (!this._rawFileData)
682
689
  return;
@@ -698,17 +705,13 @@ export class DataFrameComponent extends BaseComponent {
698
705
  </select>
699
706
  </div>
700
707
  <div style="margin-bottom: 1rem;">
701
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row</label>
708
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based index)</label>
702
709
  <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
703
710
  </div>
704
- <div style="margin-bottom: 1rem;">
705
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
706
- <input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
707
- </div>
708
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint"></div>
709
- <div class="jux-reshape-preview-container" style="margin-top: 1rem;">
711
+ <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>
712
+ <div class="jux-reshape-preview-container">
710
713
  <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
711
- <div id="${this._id}-preview" class="jux-reshape-preview"></div>
714
+ <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>
712
715
  </div>
713
716
  `;
714
717
  this._reshapeModal
@@ -727,17 +730,14 @@ export class DataFrameComponent extends BaseComponent {
727
730
  return;
728
731
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
729
732
  const headerRowInput = document.getElementById(`${this._id}-header-row`);
730
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
731
733
  const delim = delimiterSelect.value;
732
734
  const headerRow = parseInt(headerRowInput.value) || 0;
733
- const skipRows = parseInt(skipRowsInput.value) || 0;
734
735
  this.state.loading = true;
735
736
  this._updateStatus('Re-parsing with new settings...', 'loading');
736
737
  try {
737
738
  const df = this._driver.parseCSV(this._rawFileData.text, {
738
739
  delimiter: delim,
739
740
  headerRow,
740
- skipRows,
741
741
  hasHeader: true
742
742
  });
743
743
  await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
@@ -755,7 +755,6 @@ export class DataFrameComponent extends BaseComponent {
755
755
  requestAnimationFrame(() => {
756
756
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
757
757
  const headerRowInput = document.getElementById(`${this._id}-header-row`);
758
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
759
758
  const previewDiv = document.getElementById(`${this._id}-preview`);
760
759
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
761
760
  if (this._rawFileData?.text) {
@@ -770,75 +769,83 @@ export class DataFrameComponent extends BaseComponent {
770
769
  if (!hintDiv)
771
770
  return;
772
771
  const headerRow = parseInt(headerRowInput?.value) || 0;
773
- const skipRows = parseInt(skipRowsInput?.value) || 0;
774
- const totalSkipped = headerRow + skipRows;
775
- hintDiv.innerHTML = totalSkipped > 0
776
- ? `Row <strong>${headerRow + skipRows}</strong> will be used as column headers. ` +
777
- `<strong>${totalSkipped}</strong> row${totalSkipped > 1 ? 's' : ''} above will be skipped.`
778
- : `Row <strong>0</strong> (first row) will be used as column headers.`;
772
+ if (headerRow > 0) {
773
+ hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
774
+ `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
775
+ }
776
+ else {
777
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
778
+ }
779
779
  };
780
780
  const updatePreview = () => {
781
781
  if (!this._rawFileData?.text)
782
782
  return;
783
783
  const delim = delimiterSelect?.value || ',';
784
784
  const headerRow = parseInt(headerRowInput?.value) || 0;
785
- const skipRows = parseInt(skipRowsInput?.value) || 0;
786
785
  updateHint();
787
786
  try {
788
- const colWidth = 22;
789
- const idxWidth = 6;
790
- const lines = [];
791
- const totalOffset = headerRow + skipRows;
792
- // Parse raw (no header offset) to show skipped rows
793
- if (totalOffset > 0) {
794
- const rawDf = this._driver.parseCSV(this._rawFileData.text, {
795
- delimiter: delim,
796
- headerRow: 0,
797
- skipRows: 0,
798
- hasHeader: true,
799
- maxRows: totalOffset + 1
800
- });
801
- lines.push('');
802
- lines.push(` ┌${'─'.repeat(100)}┐`);
803
- lines.push(` │ ROWS 0-${totalOffset - 1} WILL BE SKIPPED (not imported)`.padEnd(101) + '│');
804
- lines.push(` └${'─'.repeat(100)}┘`);
805
- lines.push('');
806
- const rawCols = rawDf.columns;
807
- lines.push(`${'[0]'.padEnd(idxWidth)}${rawCols.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ← skipped`);
808
- const rawRows = rawDf.toRows();
809
- const skippedCount = Math.min(totalOffset - 1, rawRows.length);
810
- for (let i = 0; i < skippedCount; i++) {
811
- const row = rawRows[i];
812
- const rowIdx = `[${i + 1}]`.padEnd(idxWidth);
813
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
814
- lines.push(`${rowIdx}${cols} ← skipped`);
815
- }
816
- lines.push('');
817
- lines.push(` ╔${'═'.repeat(100)}╗`);
818
- lines.push(` ║ ▼ DATA STARTS HERE (Row ${totalOffset} = Column Headers)`.padEnd(101) + '║');
819
- lines.push(` ╚${'═'.repeat(100)}╝`);
820
- lines.push('');
821
- }
822
- // Parse with actual settings for header + data rows
823
- const df = this._driver.parseCSV(this._rawFileData.text, {
787
+ // Parse raw to show all rows
788
+ const rawDf = this._driver.parseCSV(this._rawFileData.text, {
824
789
  delimiter: delim,
825
- headerRow,
826
- skipRows,
790
+ headerRow: 0,
827
791
  hasHeader: true,
828
- maxRows: 8
792
+ maxRows: headerRow + 10
829
793
  });
830
- const headerLine = `${'HDR'.padEnd(idxWidth)}${df.columns.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ◀ HEADERS`;
831
- lines.push(headerLine);
832
- lines.push(`${'───'.padEnd(idxWidth)}${'─'.repeat(Math.min(colWidth * df.columns.length, 120))}`);
833
- const dataRows = df.toRows();
834
- dataRows.forEach((row, i) => {
835
- const rowIdx = `[${totalOffset + 1 + i}]`.padEnd(idxWidth);
836
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
837
- lines.push(`${rowIdx}${cols}`);
838
- });
839
- if (previewDiv) {
840
- previewDiv.textContent = lines.join('\n');
794
+ const rawCols = rawDf.columns;
795
+ const rawRows = rawDf.toRows();
796
+ // Build HTML table
797
+ let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
798
+ const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
799
+ for (let i = 0; i < totalRows; i++) {
800
+ const isHeader = (i === headerRow);
801
+ const isSkipped = (i < headerRow);
802
+ let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
803
+ if (isHeader) {
804
+ rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
805
+ }
806
+ else if (isSkipped) {
807
+ rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
808
+ }
809
+ html += `<tr style="${rowStyle}">`;
810
+ // Row index
811
+ html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
812
+ html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
813
+ html += '</td>';
814
+ // Data
815
+ let values;
816
+ if (i === 0) {
817
+ values = rawCols;
818
+ }
819
+ else if (i - 1 < rawRows.length) {
820
+ values = Object.values(rawRows[i - 1]);
821
+ }
822
+ else {
823
+ values = [];
824
+ }
825
+ values.slice(0, 6).forEach(val => {
826
+ const displayVal = val != null ? String(val).substring(0, 25) : '';
827
+ html += `<td style="padding: 6px 8px;">${this._escapeHtml(displayVal)}</td>`;
828
+ });
829
+ if (values.length > 6) {
830
+ html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
831
+ }
832
+ // Status
833
+ html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
834
+ if (isHeader) {
835
+ html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
836
+ }
837
+ else if (isSkipped) {
838
+ html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
839
+ }
840
+ else {
841
+ html += '<span style="color: hsl(var(--success));">data</span>';
842
+ }
843
+ html += '</td>';
844
+ html += '</tr>';
841
845
  }
846
+ html += '</table>';
847
+ if (previewDiv)
848
+ previewDiv.innerHTML = html;
842
849
  }
843
850
  catch (err) {
844
851
  if (previewDiv)
@@ -849,8 +856,6 @@ export class DataFrameComponent extends BaseComponent {
849
856
  delimiterSelect.addEventListener('change', updatePreview);
850
857
  if (headerRowInput)
851
858
  headerRowInput.addEventListener('input', updatePreview);
852
- if (skipRowsInput)
853
- skipRowsInput.addEventListener('input', updatePreview);
854
859
  updatePreview();
855
860
  this._reshapeModal.open();
856
861
  });
@@ -5,6 +5,7 @@ import { FileUpload } from './fileupload.js';
5
5
  import { Table } from './table.js';
6
6
  import { Tabs } from './tabs.js';
7
7
  import { Modal } from './modal.js';
8
+ import { Button } from './button.js';
8
9
  import { renderIcon } from './icons.js';
9
10
 
10
11
  const TRIGGER_EVENTS = [] as const;
@@ -311,10 +312,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
311
312
  const wrapper = document.getElementById(this._id);
312
313
  if (!wrapper) return;
313
314
 
315
+ // Clean up existing content
314
316
  const existingTable = wrapper.querySelector('.jux-table-wrapper');
315
317
  if (existingTable) existingTable.remove();
316
318
 
317
- const existingTabs = wrapper.querySelector('.jux-dataframe-tabs');
319
+ const existingTabs = wrapper.querySelector('.jux-tabs');
318
320
  if (existingTabs) existingTabs.remove();
319
321
 
320
322
  Object.entries(sheets).forEach(([name, df]) => {
@@ -323,65 +325,33 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
323
325
 
324
326
  const sheetNames = Object.keys(sheets);
325
327
 
326
- // Build custom tab bar with settings cogs
327
- const tabsContainer = document.createElement('div');
328
- tabsContainer.className = 'jux-dataframe-tabs';
329
-
330
- const tabList = document.createElement('div');
331
- tabList.className = 'jux-tabs-list';
332
-
333
- sheetNames.forEach((sheetName, idx) => {
334
- const tabBtn = document.createElement('button');
335
- tabBtn.className = 'jux-tabs-button' + (idx === 0 ? ' jux-tabs-button-active' : '');
336
- tabBtn.setAttribute('data-sheet', sheetName);
337
-
338
- const labelSpan = document.createElement('span');
339
- labelSpan.textContent = sheetName;
340
- tabBtn.appendChild(labelSpan);
341
-
342
- // Add settings cog icon
343
- const cogBtn = document.createElement('button');
344
- cogBtn.className = 'jux-dataframe-tab-settings';
345
- cogBtn.title = 'Import settings for ' + sheetName;
346
- cogBtn.innerHTML = `<span class="iconify" data-icon="lucide:settings" style="width:14px;height:14px;"></span>`;
347
- cogBtn.addEventListener('click', (e) => {
348
- e.stopPropagation();
349
- this._showReshapeModal();
350
- });
351
- tabBtn.appendChild(cogBtn);
352
-
353
- tabBtn.addEventListener('click', () => {
354
- // Switch active tab
355
- tabList.querySelectorAll('.jux-tabs-button').forEach(btn => {
356
- btn.classList.remove('jux-tabs-button-active');
357
- });
358
- tabBtn.classList.add('jux-tabs-button-active');
359
-
360
- // Show/hide panels
361
- wrapper.querySelectorAll('.jux-tabs-panel').forEach(panel => {
362
- (panel as HTMLElement).style.display = 'none';
363
- });
364
- const panel = document.getElementById(`${this._id}-panel-${sheetName}`);
365
- if (panel) panel.style.display = 'block';
328
+ // Build tabs using the Tabs component
329
+ const tabDefs = sheetNames.map(name => ({
330
+ id: name,
331
+ label: name,
332
+ content: '' // Content will be added after render
333
+ }));
366
334
 
367
- // Update current df reference
368
- this._df = this._sheets.get(sheetName) || null;
369
- });
335
+ this._tabs = new Tabs(`${this._id}-tabs`, {
336
+ tabs: tabDefs,
337
+ activeTab: sheetNames[0]
338
+ });
370
339
 
371
- tabList.appendChild(tabBtn);
340
+ this._tabs.bind('tabChange', (tabId: string) => {
341
+ this._df = this._sheets.get(tabId) || null;
372
342
  });
373
343
 
374
- tabsContainer.appendChild(tabList);
344
+ // Create container for tabs
345
+ const tabsContainer = document.createElement('div');
346
+ tabsContainer.className = 'jux-dataframe-tabs';
375
347
  wrapper.appendChild(tabsContainer);
376
348
 
377
- // Create panels for each sheet
349
+ this._tabs.render(tabsContainer);
350
+
351
+ // Now render tables into each tab panel
378
352
  sheetNames.forEach((sheetName, idx) => {
379
353
  const df = sheets[sheetName];
380
-
381
- const panel = document.createElement('div');
382
- panel.className = 'jux-tabs-panel';
383
- panel.id = `${this._id}-panel-${sheetName}`;
384
- panel.style.display = idx === 0 ? 'block' : 'none';
354
+ const panelId = `${this._id}-tabs-${sheetName}-panel`;
385
355
 
386
356
  const table = new Table(`${this._id}-table-${sheetName}`, {
387
357
  striped: this._tableOptions.striped,
@@ -395,54 +365,59 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
395
365
  const columnDefs = df.columns.map(col => ({ key: col, label: col }));
396
366
  table.columns(columnDefs).rows(df.toRows());
397
367
 
398
- wrapper.appendChild(panel);
399
- table.render(panel);
368
+ // Add settings button to tab panel
369
+ const settingsBtn = new Button(`${this._id}-settings-${sheetName}`, {
370
+ label: '⚙️ Import Settings',
371
+ variant: 'ghost',
372
+ size: 'small'
373
+ });
374
+ settingsBtn.bind('click', () => this._showReshapeModal());
375
+
376
+ // Use addTabContent to add components
377
+ this._tabs!.addTabContent(sheetName, [settingsBtn, table]);
400
378
 
401
379
  if (this._tableOptions.filterable) {
402
- const filterContainer = document.createElement('div');
403
- filterContainer.className = 'jux-dataframe-filter';
404
-
405
- const input = document.createElement('input');
406
- input.type = 'text';
407
- input.placeholder = `Filter ${sheetName}...`;
408
- input.className = 'jux-input-element jux-dataframe-filter-input';
409
-
410
- const iconEl = renderIcon('search');
411
- iconEl.style.width = '16px';
412
- iconEl.style.height = '16px';
413
-
414
- const iconWrap = document.createElement('span');
415
- iconWrap.className = 'jux-dataframe-filter-icon';
416
- iconWrap.appendChild(iconEl);
417
-
418
- filterContainer.appendChild(iconWrap);
419
- filterContainer.appendChild(input);
420
-
421
- input.addEventListener('input', () => {
422
- const text = input.value.toLowerCase();
423
- if (!text) { table.rows(df.toRows()); return; }
424
- const filtered = df.filter((row) =>
425
- Object.values(row).some(v =>
426
- v !== null && v !== undefined && String(v).toLowerCase().includes(text)
427
- )
428
- );
429
- table.rows(filtered.toRows());
430
- });
380
+ // Add filter input above table
381
+ const panel = document.getElementById(panelId);
382
+ if (panel) {
383
+ const filterContainer = document.createElement('div');
384
+ filterContainer.className = 'jux-dataframe-filter';
385
+
386
+ const input = document.createElement('input');
387
+ input.type = 'text';
388
+ input.placeholder = `Filter ${sheetName}...`;
389
+ input.className = 'jux-input-element jux-dataframe-filter-input';
390
+
391
+ const iconEl = renderIcon('search');
392
+ iconEl.style.width = '16px';
393
+ iconEl.style.height = '16px';
394
+
395
+ const iconWrap = document.createElement('span');
396
+ iconWrap.className = 'jux-dataframe-filter-icon';
397
+ iconWrap.appendChild(iconEl);
398
+
399
+ filterContainer.appendChild(iconWrap);
400
+ filterContainer.appendChild(input);
401
+
402
+ input.addEventListener('input', () => {
403
+ const text = input.value.toLowerCase();
404
+ if (!text) { table.rows(df.toRows()); return; }
405
+ const filtered = df.filter((row) =>
406
+ Object.values(row).some(v =>
407
+ v !== null && v !== undefined && String(v).toLowerCase().includes(text)
408
+ )
409
+ );
410
+ table.rows(filtered.toRows());
411
+ });
431
412
 
432
- const tableWrapper = panel.querySelector('.jux-table-wrapper');
433
- if (tableWrapper) {
434
- panel.insertBefore(filterContainer, tableWrapper);
413
+ const tableWrapper = panel.querySelector('.jux-table-wrapper');
414
+ if (tableWrapper) {
415
+ panel.insertBefore(filterContainer, tableWrapper);
416
+ }
435
417
  }
436
418
  }
437
419
  });
438
420
 
439
- // Ensure Iconify renders the cog icons
440
- requestAnimationFrame(() => {
441
- if ((window as any).Iconify) {
442
- (window as any).Iconify.scan();
443
- }
444
- });
445
-
446
421
  const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
447
422
  this._updateStatus(
448
423
  `${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`,
@@ -629,7 +604,20 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
629
604
 
630
605
  this._cleanupReshapeModal();
631
606
 
632
- const suggestedRow = this._df ? this._detectLikelyHeaderRow(this._df) : 0;
607
+ // Always detect from a fresh raw parse, not from current _df
608
+ let suggestedRow = 0;
609
+ try {
610
+ const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
611
+ headerRow: 0,
612
+ maxSheetSize: 20
613
+ });
614
+ const rawSheet = Object.values(rawSheets)[0];
615
+ if (rawSheet) {
616
+ suggestedRow = this._detectLikelyHeaderRow(rawSheet);
617
+ }
618
+ } catch {
619
+ suggestedRow = 0;
620
+ }
633
621
 
634
622
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
635
623
  title: 'Excel Import Settings',
@@ -641,7 +629,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
641
629
  const modalContentHTML = `
642
630
  <div style="margin-bottom: 1rem;">
643
631
  <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
644
- Header Row
632
+ Header Row (0-based index)
645
633
  </label>
646
634
  <input
647
635
  type="number"
@@ -659,7 +647,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
659
647
  <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
660
648
  Preview
661
649
  </div>
662
- <div id="${this._id}-preview" style="font-family: monospace; font-size: 0.75rem; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 1rem; overflow-x: auto; white-space: pre; max-height: 400px; overflow-y: auto;"></div>
650
+ <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>
663
651
  </div>
664
652
  `;
665
653
 
@@ -718,9 +706,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
718
706
  if (!hintDiv) return;
719
707
  if (headerRow > 0) {
720
708
  hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
721
- `Rows <strong>0–${headerRow - 1}</strong> will be skipped (not imported).`;
709
+ `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
722
710
  } else {
723
- hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers. No rows will be skipped.`;
711
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
724
712
  }
725
713
  };
726
714
 
@@ -729,10 +717,10 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
729
717
  updateHint(headerRow);
730
718
 
731
719
  try {
732
- // Get raw data (headerRow=0) to show ALL rows including skipped ones
720
+ // ALWAYS parse with headerRow=0 to get raw file structure
733
721
  const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
734
722
  headerRow: 0,
735
- maxSheetSize: headerRow + 12
723
+ maxSheetSize: Math.max(headerRow + 12, 15)
736
724
  });
737
725
  const rawSheet = Object.values(rawSheets)[0];
738
726
 
@@ -741,73 +729,81 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
741
729
  return;
742
730
  }
743
731
 
732
+ // rawSheet.columns = row 0 values (when parsed with headerRow=0)
733
+ // rawSheet.toRows() = rows 1+ (data rows when parsed with headerRow=0)
744
734
  const rawCols = rawSheet.columns;
745
735
  const rawRows = rawSheet.toRows();
746
- const colWidth = 20;
747
- const lines: string[] = [];
748
736
 
749
- // === SKIPPED ROWS SECTION ===
750
- if (headerRow > 0) {
751
- lines.push('┌─────────────────────────────────────────────────────────────────────────────────┐');
752
- lines.push(`│ SKIPPED: Rows 0–${headerRow - 1} will NOT be imported │`);
753
- lines.push('└─────────────────────────────────────────────────────────────────────────────────┘');
754
- lines.push('');
755
-
756
- // Row 0 is the raw file header (becomes column names when headerRow=0)
757
- const row0Line = rawCols.slice(0, 5).map(c => String(c ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
758
- lines.push(` [0] ${row0Line}${rawCols.length > 5 ? ' ...' : ''}`);
759
-
760
- // Rows 1 to headerRow-1 are skipped data rows
761
- for (let i = 0; i < Math.min(headerRow - 1, rawRows.length); i++) {
762
- const row = rawRows[i];
763
- const vals = Object.values(row).slice(0, 5).map(v => String(v ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
764
- lines.push(` [${i + 1}] ${vals}${Object.values(row).length > 5 ? ' ...' : ''}`);
765
- }
737
+ // Build HTML table showing raw file structure
738
+ let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
766
739
 
767
- lines.push('');
768
- }
740
+ // We need to show rows 0 through headerRow+7 (or so)
741
+ // Row 0 = rawCols, Row 1+ = rawRows[i-1]
742
+ const totalRowsToShow = Math.min(headerRow + 8, rawRows.length + 1);
769
743
 
770
- // === HEADER ROW (will become column names) ===
771
- lines.push('╔═════════════════════════════════════════════════════════════════════════════════╗');
772
- lines.push(`║ HEADER ROW ${headerRow} These values become your column names ║`);
773
- lines.push('╚═════════════════════════════════════════════════════════════════════════════════╝');
774
- lines.push('');
775
-
776
- // The header row content - if headerRow=0, it's rawCols, else it's rawRows[headerRow-1]
777
- let headerValues: string[];
778
- if (headerRow === 0) {
779
- headerValues = rawCols;
780
- } else if (headerRow - 1 < rawRows.length) {
781
- headerValues = Object.values(rawRows[headerRow - 1]).map(v => String(v ?? ''));
782
- } else {
783
- headerValues = rawCols; // fallback
784
- }
744
+ for (let fileRow = 0; fileRow < totalRowsToShow; fileRow++) {
745
+ const isHeader = (fileRow === headerRow);
746
+ const isSkipped = (fileRow < headerRow);
785
747
 
786
- const headerLine = headerValues.slice(0, 5).map(c => String(c ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
787
- lines.push(`▶ [${headerRow}] ${headerLine}${headerValues.length > 5 ? ' ...' : ''}`);
788
- lines.push('');
748
+ let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
789
749
 
790
- // === DATA ROWS ===
791
- lines.push('────────────────────────────────────────────────────────────────────────────────────');
792
- lines.push(' DATA ROWS (will be imported):');
793
- lines.push('');
750
+ if (isHeader) {
751
+ rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
752
+ } else if (isSkipped) {
753
+ rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
754
+ }
794
755
 
795
- // Data starts at rawRows[headerRow] (since rawRows is 0-indexed after the header)
796
- const dataStartIdx = headerRow;
797
- const dataRows = rawRows.slice(dataStartIdx, dataStartIdx + 6);
756
+ html += `<tr style="${rowStyle}">`;
798
757
 
799
- if (dataRows.length === 0) {
800
- lines.push(' (No data rows found after header)');
801
- } else {
802
- dataRows.forEach((row, i) => {
803
- const rowIdx = headerRow + 1 + i;
804
- const vals = Object.values(row).slice(0, 5).map(v => String(v ?? '').substring(0, colWidth - 1).padEnd(colWidth)).join('│');
805
- lines.push(` [${rowIdx}] ${vals}${Object.values(row).length > 5 ? ' ...' : ''}`);
758
+ // Row index cell
759
+ 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;">`;
760
+ if (isHeader) {
761
+ html += `<span style="color: hsl(142 71% 45%);">▶ ${fileRow}</span>`;
762
+ } else {
763
+ html += `${fileRow}`;
764
+ }
765
+ html += '</td>';
766
+
767
+ // Get values for this file row
768
+ let values: any[];
769
+ if (fileRow === 0) {
770
+ values = rawCols;
771
+ } else {
772
+ values = rawRows[fileRow - 1] ? Object.values(rawRows[fileRow - 1]) : [];
773
+ }
774
+
775
+ // Show first 6 columns
776
+ const displayCols = values.slice(0, 6);
777
+ displayCols.forEach(val => {
778
+ const displayVal = val != null ? String(val).substring(0, 20) : '';
779
+ const cellStyle = isHeader
780
+ ? 'padding: 8px 12px; font-weight: 600; color: hsl(var(--foreground));'
781
+ : 'padding: 8px 12px;';
782
+ html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
806
783
  });
784
+
785
+ if (values.length > 6) {
786
+ html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
787
+ }
788
+
789
+ // Status badge cell
790
+ html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap;">`;
791
+ if (isHeader) {
792
+ html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
793
+ } else if (isSkipped) {
794
+ html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
795
+ } else {
796
+ html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
797
+ }
798
+ html += '</td>';
799
+
800
+ html += '</tr>';
807
801
  }
808
802
 
803
+ html += '</table>';
804
+
809
805
  if (previewDiv) {
810
- previewDiv.textContent = lines.join('\n');
806
+ previewDiv.innerHTML = html;
811
807
  }
812
808
  } catch (err: any) {
813
809
  if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
@@ -820,6 +816,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
820
816
  this._reshapeModal.open();
821
817
  }
822
818
 
819
+ private _escapeHtml(text: string): string {
820
+ const div = document.createElement('div');
821
+ div.textContent = text;
822
+ return div.innerHTML;
823
+ }
824
+
823
825
  private _showCSVReshapeModal(): void {
824
826
  if (!this._rawFileData) return;
825
827
 
@@ -843,17 +845,13 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
843
845
  </select>
844
846
  </div>
845
847
  <div style="margin-bottom: 1rem;">
846
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row</label>
848
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based index)</label>
847
849
  <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
848
850
  </div>
849
- <div style="margin-bottom: 1rem;">
850
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
851
- <input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
852
- </div>
853
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint"></div>
854
- <div class="jux-reshape-preview-container" style="margin-top: 1rem;">
851
+ <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>
852
+ <div class="jux-reshape-preview-container">
855
853
  <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
856
- <div id="${this._id}-preview" class="jux-reshape-preview"></div>
854
+ <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>
857
855
  </div>
858
856
  `;
859
857
 
@@ -873,11 +871,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
873
871
 
874
872
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
875
873
  const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
876
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
877
874
 
878
875
  const delim = delimiterSelect.value;
879
876
  const headerRow = parseInt(headerRowInput.value) || 0;
880
- const skipRows = parseInt(skipRowsInput.value) || 0;
881
877
 
882
878
  this.state.loading = true;
883
879
  this._updateStatus('Re-parsing with new settings...', 'loading');
@@ -886,7 +882,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
886
882
  const df = this._driver.parseCSV(this._rawFileData.text, {
887
883
  delimiter: delim,
888
884
  headerRow,
889
- skipRows,
890
885
  hasHeader: true
891
886
  });
892
887
 
@@ -907,7 +902,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
907
902
  requestAnimationFrame(() => {
908
903
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
909
904
  const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
910
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
911
905
  const previewDiv = document.getElementById(`${this._id}-preview`)!;
912
906
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
913
907
 
@@ -922,12 +916,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
922
916
  const updateHint = () => {
923
917
  if (!hintDiv) return;
924
918
  const headerRow = parseInt(headerRowInput?.value) || 0;
925
- const skipRows = parseInt(skipRowsInput?.value) || 0;
926
- const totalSkipped = headerRow + skipRows;
927
- hintDiv.innerHTML = totalSkipped > 0
928
- ? `Row <strong>${headerRow + skipRows}</strong> will be used as column headers. ` +
929
- `<strong>${totalSkipped}</strong> row${totalSkipped > 1 ? 's' : ''} above will be skipped.`
930
- : `Row <strong>0</strong> (first row) will be used as column headers.`;
919
+ if (headerRow > 0) {
920
+ hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
921
+ `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
922
+ } else {
923
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
924
+ }
931
925
  };
932
926
 
933
927
  const updatePreview = () => {
@@ -935,74 +929,81 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
935
929
 
936
930
  const delim = delimiterSelect?.value || ',';
937
931
  const headerRow = parseInt(headerRowInput?.value) || 0;
938
- const skipRows = parseInt(skipRowsInput?.value) || 0;
939
932
 
940
933
  updateHint();
941
934
 
942
935
  try {
943
- const colWidth = 22;
944
- const idxWidth = 6;
945
- const lines: string[] = [];
946
- const totalOffset = headerRow + skipRows;
947
-
948
- // Parse raw (no header offset) to show skipped rows
949
- if (totalOffset > 0) {
950
- const rawDf = this._driver.parseCSV(this._rawFileData.text, {
951
- delimiter: delim,
952
- headerRow: 0,
953
- skipRows: 0,
954
- hasHeader: true,
955
- maxRows: totalOffset + 1
956
- });
936
+ // Parse raw to show all rows
937
+ const rawDf = this._driver.parseCSV(this._rawFileData.text, {
938
+ delimiter: delim,
939
+ headerRow: 0,
940
+ hasHeader: true,
941
+ maxRows: headerRow + 10
942
+ });
943
+
944
+ const rawCols = rawDf.columns;
945
+ const rawRows = rawDf.toRows();
946
+
947
+ // Build HTML table
948
+ let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
949
+
950
+ const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
957
951
 
958
- lines.push('');
959
- lines.push(` ┌${'─'.repeat(100)}┐`);
960
- lines.push(` │ ROWS 0-${totalOffset - 1} WILL BE SKIPPED (not imported)`.padEnd(101) + '│');
961
- lines.push(` └${'─'.repeat(100)}┘`);
962
- lines.push('');
963
-
964
- const rawCols = rawDf.columns;
965
- lines.push(`${'[0]'.padEnd(idxWidth)}${rawCols.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ← skipped`);
966
-
967
- const rawRows = rawDf.toRows();
968
- const skippedCount = Math.min(totalOffset - 1, rawRows.length);
969
- for (let i = 0; i < skippedCount; i++) {
970
- const row = rawRows[i];
971
- const rowIdx = `[${i + 1}]`.padEnd(idxWidth);
972
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
973
- lines.push(`${rowIdx}${cols} ← skipped`);
952
+ for (let i = 0; i < totalRows; i++) {
953
+ const isHeader = (i === headerRow);
954
+ const isSkipped = (i < headerRow);
955
+
956
+ let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
957
+
958
+ if (isHeader) {
959
+ rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
960
+ } else if (isSkipped) {
961
+ rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
974
962
  }
975
963
 
976
- lines.push('');
977
- lines.push(` ╔${'═'.repeat(100)}╗`);
978
- lines.push(` ║ ▼ DATA STARTS HERE (Row ${totalOffset} = Column Headers)`.padEnd(101) + '║');
979
- lines.push(` ╚${'═'.repeat(100)}╝`);
980
- lines.push('');
981
- }
964
+ html += `<tr style="${rowStyle}">`;
965
+
966
+ // Row index
967
+ html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
968
+ html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
969
+ html += '</td>';
970
+
971
+ // Data
972
+ let values: any[];
973
+ if (i === 0) {
974
+ values = rawCols;
975
+ } else if (i - 1 < rawRows.length) {
976
+ values = Object.values(rawRows[i - 1]);
977
+ } else {
978
+ values = [];
979
+ }
982
980
 
983
- // Parse with actual settings for header + data rows
984
- const df = this._driver.parseCSV(this._rawFileData.text, {
985
- delimiter: delim,
986
- headerRow,
987
- skipRows,
988
- hasHeader: true,
989
- maxRows: 8
990
- });
981
+ values.slice(0, 6).forEach(val => {
982
+ const displayVal = val != null ? String(val).substring(0, 25) : '';
983
+ html += `<td style="padding: 6px 8px;">${this._escapeHtml(displayVal)}</td>`;
984
+ });
991
985
 
992
- const headerLine = `${'HDR'.padEnd(idxWidth)}${df.columns.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ◀ HEADERS`;
993
- lines.push(headerLine);
994
- lines.push(`${'───'.padEnd(idxWidth)}${'─'.repeat(Math.min(colWidth * df.columns.length, 120))}`);
986
+ if (values.length > 6) {
987
+ html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
988
+ }
995
989
 
996
- const dataRows = df.toRows();
997
- dataRows.forEach((row, i) => {
998
- const rowIdx = `[${totalOffset + 1 + i}]`.padEnd(idxWidth);
999
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
1000
- lines.push(`${rowIdx}${cols}`);
1001
- });
990
+ // Status
991
+ html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
992
+ if (isHeader) {
993
+ html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
994
+ } else if (isSkipped) {
995
+ html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
996
+ } else {
997
+ html += '<span style="color: hsl(var(--success));">data</span>';
998
+ }
999
+ html += '</td>';
1002
1000
 
1003
- if (previewDiv) {
1004
- previewDiv.textContent = lines.join('\n');
1001
+ html += '</tr>';
1005
1002
  }
1003
+
1004
+ html += '</table>';
1005
+
1006
+ if (previewDiv) previewDiv.innerHTML = html;
1006
1007
  } catch (err: any) {
1007
1008
  if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
1008
1009
  }
@@ -1010,7 +1011,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
1010
1011
 
1011
1012
  if (delimiterSelect) delimiterSelect.addEventListener('change', updatePreview);
1012
1013
  if (headerRowInput) headerRowInput.addEventListener('input', updatePreview);
1013
- if (skipRowsInput) skipRowsInput.addEventListener('input', updatePreview);
1014
1014
 
1015
1015
  updatePreview();
1016
1016
  this._reshapeModal!.open();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.196",
3
+ "version": "1.1.198",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",