juxscript 1.1.196 → 1.1.197

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]];
@@ -515,7 +496,7 @@ export class DataFrameComponent extends BaseComponent {
515
496
  const modalContentHTML = `
516
497
  <div style="margin-bottom: 1rem;">
517
498
  <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
518
- Header Row
499
+ Header Row (0-based index)
519
500
  </label>
520
501
  <input
521
502
  type="number"
@@ -533,7 +514,7 @@ export class DataFrameComponent extends BaseComponent {
533
514
  <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
534
515
  Preview
535
516
  </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>
517
+ <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>
537
518
  </div>
538
519
  `;
539
520
  this._reshapeModal
@@ -585,17 +566,17 @@ export class DataFrameComponent extends BaseComponent {
585
566
  return;
586
567
  if (headerRow > 0) {
587
568
  hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
588
- `Rows <strong>0–${headerRow - 1}</strong> will be skipped (not imported).`;
569
+ `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
589
570
  }
590
571
  else {
591
- hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers. No rows will be skipped.`;
572
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
592
573
  }
593
574
  };
594
575
  const updatePreview = async () => {
595
576
  const headerRow = parseInt(headerRowInput?.value) || 0;
596
577
  updateHint(headerRow);
597
578
  try {
598
- // Get raw data (headerRow=0) to show ALL rows including skipped ones
579
+ // Get raw data to show all rows
599
580
  const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
600
581
  headerRow: 0,
601
582
  maxSheetSize: headerRow + 12
@@ -608,63 +589,68 @@ export class DataFrameComponent extends BaseComponent {
608
589
  }
609
590
  const rawCols = rawSheet.columns;
610
591
  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 ? ' ...' : ''}`);
592
+ // Build a simple HTML table for clarity
593
+ let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
594
+ // Render each row
595
+ const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
596
+ for (let i = 0; i < totalRows; i++) {
597
+ const isHeader = (i === headerRow);
598
+ const isSkipped = (i < headerRow);
599
+ let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
600
+ let cellStyle = 'padding: 6px 8px; text-align: left;';
601
+ if (isHeader) {
602
+ rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
627
603
  }
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 ? ' ...' : ''}`);
604
+ else if (isSkipped) {
605
+ rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
606
+ }
607
+ html += `<tr style="${rowStyle}">`;
608
+ // Row index cell
609
+ html += `<td style="${cellStyle} width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
610
+ if (isHeader) {
611
+ html += `<strong>→ ${i}</strong>`;
612
+ }
613
+ else {
614
+ html += `${i}`;
615
+ }
616
+ html += '</td>';
617
+ // Data cells
618
+ let values;
619
+ if (i === 0) {
620
+ values = rawCols;
621
+ }
622
+ else if (i - 1 < rawRows.length) {
623
+ values = Object.values(rawRows[i - 1]);
624
+ }
625
+ else {
626
+ values = [];
627
+ }
628
+ // Show first 6 columns
629
+ const displayCols = values.slice(0, 6);
630
+ displayCols.forEach(val => {
631
+ const displayVal = val != null ? String(val).substring(0, 25) : '';
632
+ html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
664
633
  });
634
+ if (values.length > 6) {
635
+ html += `<td style="${cellStyle} color: hsl(var(--muted-foreground));">...</td>`;
636
+ }
637
+ // Status cell
638
+ html += `<td style="${cellStyle} text-align: right; font-size: 10px;">`;
639
+ if (isHeader) {
640
+ html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
641
+ }
642
+ else if (isSkipped) {
643
+ html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
644
+ }
645
+ else {
646
+ html += '<span style="color: hsl(var(--success));">data</span>';
647
+ }
648
+ html += '</td>';
649
+ html += '</tr>';
665
650
  }
651
+ html += '</table>';
666
652
  if (previewDiv) {
667
- previewDiv.textContent = lines.join('\n');
653
+ previewDiv.innerHTML = html;
668
654
  }
669
655
  }
670
656
  catch (err) {
@@ -677,6 +663,11 @@ export class DataFrameComponent extends BaseComponent {
677
663
  updatePreview();
678
664
  this._reshapeModal.open();
679
665
  }
666
+ _escapeHtml(text) {
667
+ const div = document.createElement('div');
668
+ div.textContent = text;
669
+ return div.innerHTML;
670
+ }
680
671
  _showCSVReshapeModal() {
681
672
  if (!this._rawFileData)
682
673
  return;
@@ -698,17 +689,13 @@ export class DataFrameComponent extends BaseComponent {
698
689
  </select>
699
690
  </div>
700
691
  <div style="margin-bottom: 1rem;">
701
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row</label>
692
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based index)</label>
702
693
  <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
703
694
  </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;">
695
+ <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>
696
+ <div class="jux-reshape-preview-container">
710
697
  <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>
698
+ <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
699
  </div>
713
700
  `;
714
701
  this._reshapeModal
@@ -727,17 +714,14 @@ export class DataFrameComponent extends BaseComponent {
727
714
  return;
728
715
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
729
716
  const headerRowInput = document.getElementById(`${this._id}-header-row`);
730
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
731
717
  const delim = delimiterSelect.value;
732
718
  const headerRow = parseInt(headerRowInput.value) || 0;
733
- const skipRows = parseInt(skipRowsInput.value) || 0;
734
719
  this.state.loading = true;
735
720
  this._updateStatus('Re-parsing with new settings...', 'loading');
736
721
  try {
737
722
  const df = this._driver.parseCSV(this._rawFileData.text, {
738
723
  delimiter: delim,
739
724
  headerRow,
740
- skipRows,
741
725
  hasHeader: true
742
726
  });
743
727
  await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
@@ -755,7 +739,6 @@ export class DataFrameComponent extends BaseComponent {
755
739
  requestAnimationFrame(() => {
756
740
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
757
741
  const headerRowInput = document.getElementById(`${this._id}-header-row`);
758
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
759
742
  const previewDiv = document.getElementById(`${this._id}-preview`);
760
743
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
761
744
  if (this._rawFileData?.text) {
@@ -770,75 +753,83 @@ export class DataFrameComponent extends BaseComponent {
770
753
  if (!hintDiv)
771
754
  return;
772
755
  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.`;
756
+ if (headerRow > 0) {
757
+ hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
758
+ `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
759
+ }
760
+ else {
761
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
762
+ }
779
763
  };
780
764
  const updatePreview = () => {
781
765
  if (!this._rawFileData?.text)
782
766
  return;
783
767
  const delim = delimiterSelect?.value || ',';
784
768
  const headerRow = parseInt(headerRowInput?.value) || 0;
785
- const skipRows = parseInt(skipRowsInput?.value) || 0;
786
769
  updateHint();
787
770
  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, {
771
+ // Parse raw to show all rows
772
+ const rawDf = this._driver.parseCSV(this._rawFileData.text, {
824
773
  delimiter: delim,
825
- headerRow,
826
- skipRows,
774
+ headerRow: 0,
827
775
  hasHeader: true,
828
- maxRows: 8
829
- });
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}`);
776
+ maxRows: headerRow + 10
838
777
  });
839
- if (previewDiv) {
840
- previewDiv.textContent = lines.join('\n');
778
+ const rawCols = rawDf.columns;
779
+ const rawRows = rawDf.toRows();
780
+ // Build HTML table
781
+ let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
782
+ const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
783
+ for (let i = 0; i < totalRows; i++) {
784
+ const isHeader = (i === headerRow);
785
+ const isSkipped = (i < headerRow);
786
+ let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
787
+ if (isHeader) {
788
+ rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
789
+ }
790
+ else if (isSkipped) {
791
+ rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
792
+ }
793
+ html += `<tr style="${rowStyle}">`;
794
+ // Row index
795
+ html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
796
+ html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
797
+ html += '</td>';
798
+ // Data
799
+ let values;
800
+ if (i === 0) {
801
+ values = rawCols;
802
+ }
803
+ else if (i - 1 < rawRows.length) {
804
+ values = Object.values(rawRows[i - 1]);
805
+ }
806
+ else {
807
+ values = [];
808
+ }
809
+ values.slice(0, 6).forEach(val => {
810
+ const displayVal = val != null ? String(val).substring(0, 25) : '';
811
+ html += `<td style="padding: 6px 8px;">${this._escapeHtml(displayVal)}</td>`;
812
+ });
813
+ if (values.length > 6) {
814
+ html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
815
+ }
816
+ // Status
817
+ html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
818
+ if (isHeader) {
819
+ html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
820
+ }
821
+ else if (isSkipped) {
822
+ html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
823
+ }
824
+ else {
825
+ html += '<span style="color: hsl(var(--success));">data</span>';
826
+ }
827
+ html += '</td>';
828
+ html += '</tr>';
841
829
  }
830
+ html += '</table>';
831
+ if (previewDiv)
832
+ previewDiv.innerHTML = html;
842
833
  }
843
834
  catch (err) {
844
835
  if (previewDiv)
@@ -849,8 +840,6 @@ export class DataFrameComponent extends BaseComponent {
849
840
  delimiterSelect.addEventListener('change', updatePreview);
850
841
  if (headerRowInput)
851
842
  headerRowInput.addEventListener('input', updatePreview);
852
- if (skipRowsInput)
853
- skipRowsInput.addEventListener('input', updatePreview);
854
843
  updatePreview();
855
844
  this._reshapeModal.open();
856
845
  });
@@ -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`,
@@ -641,7 +616,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
641
616
  const modalContentHTML = `
642
617
  <div style="margin-bottom: 1rem;">
643
618
  <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
644
- Header Row
619
+ Header Row (0-based index)
645
620
  </label>
646
621
  <input
647
622
  type="number"
@@ -659,7 +634,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
659
634
  <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
660
635
  Preview
661
636
  </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>
637
+ <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>
663
638
  </div>
664
639
  `;
665
640
 
@@ -718,9 +693,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
718
693
  if (!hintDiv) return;
719
694
  if (headerRow > 0) {
720
695
  hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
721
- `Rows <strong>0–${headerRow - 1}</strong> will be skipped (not imported).`;
696
+ `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
722
697
  } else {
723
- hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers. No rows will be skipped.`;
698
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
724
699
  }
725
700
  };
726
701
 
@@ -729,7 +704,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
729
704
  updateHint(headerRow);
730
705
 
731
706
  try {
732
- // Get raw data (headerRow=0) to show ALL rows including skipped ones
707
+ // Get raw data to show all rows
733
708
  const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
734
709
  headerRow: 0,
735
710
  maxSheetSize: headerRow + 12
@@ -743,71 +718,76 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
743
718
 
744
719
  const rawCols = rawSheet.columns;
745
720
  const rawRows = rawSheet.toRows();
746
- const colWidth = 20;
747
- const lines: string[] = [];
748
721
 
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
- }
722
+ // Build a simple HTML table for clarity
723
+ let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
766
724
 
767
- lines.push('');
768
- }
725
+ // Render each row
726
+ const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
769
727
 
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
- }
728
+ for (let i = 0; i < totalRows; i++) {
729
+ const isHeader = (i === headerRow);
730
+ const isSkipped = (i < headerRow);
731
+
732
+ let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
733
+ let cellStyle = 'padding: 6px 8px; text-align: left;';
785
734
 
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('');
735
+ if (isHeader) {
736
+ rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
737
+ } else if (isSkipped) {
738
+ rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
739
+ }
789
740
 
790
- // === DATA ROWS ===
791
- lines.push('────────────────────────────────────────────────────────────────────────────────────');
792
- lines.push(' DATA ROWS (will be imported):');
793
- lines.push('');
741
+ html += `<tr style="${rowStyle}">`;
794
742
 
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);
743
+ // Row index cell
744
+ html += `<td style="${cellStyle} width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
745
+ if (isHeader) {
746
+ html += `<strong>→ ${i}</strong>`;
747
+ } else {
748
+ html += `${i}`;
749
+ }
750
+ html += '</td>';
751
+
752
+ // Data cells
753
+ let values: any[];
754
+ if (i === 0) {
755
+ values = rawCols;
756
+ } else if (i - 1 < rawRows.length) {
757
+ values = Object.values(rawRows[i - 1]);
758
+ } else {
759
+ values = [];
760
+ }
798
761
 
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 ? ' ...' : ''}`);
762
+ // Show first 6 columns
763
+ const displayCols = values.slice(0, 6);
764
+ displayCols.forEach(val => {
765
+ const displayVal = val != null ? String(val).substring(0, 25) : '';
766
+ html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
806
767
  });
768
+
769
+ if (values.length > 6) {
770
+ html += `<td style="${cellStyle} color: hsl(var(--muted-foreground));">...</td>`;
771
+ }
772
+
773
+ // Status cell
774
+ html += `<td style="${cellStyle} text-align: right; font-size: 10px;">`;
775
+ if (isHeader) {
776
+ html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
777
+ } else if (isSkipped) {
778
+ html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
779
+ } else {
780
+ html += '<span style="color: hsl(var(--success));">data</span>';
781
+ }
782
+ html += '</td>';
783
+
784
+ html += '</tr>';
807
785
  }
808
786
 
787
+ html += '</table>';
788
+
809
789
  if (previewDiv) {
810
- previewDiv.textContent = lines.join('\n');
790
+ previewDiv.innerHTML = html;
811
791
  }
812
792
  } catch (err: any) {
813
793
  if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
@@ -820,6 +800,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
820
800
  this._reshapeModal.open();
821
801
  }
822
802
 
803
+ private _escapeHtml(text: string): string {
804
+ const div = document.createElement('div');
805
+ div.textContent = text;
806
+ return div.innerHTML;
807
+ }
808
+
823
809
  private _showCSVReshapeModal(): void {
824
810
  if (!this._rawFileData) return;
825
811
 
@@ -843,17 +829,13 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
843
829
  </select>
844
830
  </div>
845
831
  <div style="margin-bottom: 1rem;">
846
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row</label>
832
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based index)</label>
847
833
  <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
848
834
  </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;">
835
+ <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>
836
+ <div class="jux-reshape-preview-container">
855
837
  <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>
838
+ <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
839
  </div>
858
840
  `;
859
841
 
@@ -873,11 +855,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
873
855
 
874
856
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
875
857
  const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
876
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
877
858
 
878
859
  const delim = delimiterSelect.value;
879
860
  const headerRow = parseInt(headerRowInput.value) || 0;
880
- const skipRows = parseInt(skipRowsInput.value) || 0;
881
861
 
882
862
  this.state.loading = true;
883
863
  this._updateStatus('Re-parsing with new settings...', 'loading');
@@ -886,7 +866,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
886
866
  const df = this._driver.parseCSV(this._rawFileData.text, {
887
867
  delimiter: delim,
888
868
  headerRow,
889
- skipRows,
890
869
  hasHeader: true
891
870
  });
892
871
 
@@ -907,7 +886,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
907
886
  requestAnimationFrame(() => {
908
887
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
909
888
  const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
910
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
911
889
  const previewDiv = document.getElementById(`${this._id}-preview`)!;
912
890
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
913
891
 
@@ -922,12 +900,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
922
900
  const updateHint = () => {
923
901
  if (!hintDiv) return;
924
902
  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.`;
903
+ if (headerRow > 0) {
904
+ hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
905
+ `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
906
+ } else {
907
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
908
+ }
931
909
  };
932
910
 
933
911
  const updatePreview = () => {
@@ -935,74 +913,81 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
935
913
 
936
914
  const delim = delimiterSelect?.value || ',';
937
915
  const headerRow = parseInt(headerRowInput?.value) || 0;
938
- const skipRows = parseInt(skipRowsInput?.value) || 0;
939
916
 
940
917
  updateHint();
941
918
 
942
919
  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
- });
920
+ // Parse raw to show all rows
921
+ const rawDf = this._driver.parseCSV(this._rawFileData.text, {
922
+ delimiter: delim,
923
+ headerRow: 0,
924
+ hasHeader: true,
925
+ maxRows: headerRow + 10
926
+ });
927
+
928
+ const rawCols = rawDf.columns;
929
+ const rawRows = rawDf.toRows();
930
+
931
+ // Build HTML table
932
+ let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
933
+
934
+ const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
935
+
936
+ for (let i = 0; i < totalRows; i++) {
937
+ const isHeader = (i === headerRow);
938
+ const isSkipped = (i < headerRow);
957
939
 
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`);
940
+ let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
941
+
942
+ if (isHeader) {
943
+ rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
944
+ } else if (isSkipped) {
945
+ rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
974
946
  }
975
947
 
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
- }
948
+ html += `<tr style="${rowStyle}">`;
949
+
950
+ // Row index
951
+ html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
952
+ html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
953
+ html += '</td>';
954
+
955
+ // Data
956
+ let values: any[];
957
+ if (i === 0) {
958
+ values = rawCols;
959
+ } else if (i - 1 < rawRows.length) {
960
+ values = Object.values(rawRows[i - 1]);
961
+ } else {
962
+ values = [];
963
+ }
982
964
 
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
- });
965
+ values.slice(0, 6).forEach(val => {
966
+ const displayVal = val != null ? String(val).substring(0, 25) : '';
967
+ html += `<td style="padding: 6px 8px;">${this._escapeHtml(displayVal)}</td>`;
968
+ });
991
969
 
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))}`);
970
+ if (values.length > 6) {
971
+ html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
972
+ }
995
973
 
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
- });
974
+ // Status
975
+ html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
976
+ if (isHeader) {
977
+ html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
978
+ } else if (isSkipped) {
979
+ html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
980
+ } else {
981
+ html += '<span style="color: hsl(var(--success));">data</span>';
982
+ }
983
+ html += '</td>';
1002
984
 
1003
- if (previewDiv) {
1004
- previewDiv.textContent = lines.join('\n');
985
+ html += '</tr>';
1005
986
  }
987
+
988
+ html += '</table>';
989
+
990
+ if (previewDiv) previewDiv.innerHTML = html;
1006
991
  } catch (err: any) {
1007
992
  if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
1008
993
  }
@@ -1010,7 +995,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
1010
995
 
1011
996
  if (delimiterSelect) delimiterSelect.addEventListener('change', updatePreview);
1012
997
  if (headerRowInput) headerRowInput.addEventListener('input', updatePreview);
1013
- if (skipRowsInput) skipRowsInput.addEventListener('input', updatePreview);
1014
998
 
1015
999
  updatePreview();
1016
1000
  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.197",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",