juxscript 1.1.195 → 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"
@@ -526,14 +507,14 @@ export class DataFrameComponent extends BaseComponent {
526
507
  max="50"
527
508
  style="width: 100%;"
528
509
  />
529
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint">
510
+ <div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;">
530
511
  </div>
531
512
  </div>
532
513
  <div class="jux-reshape-preview-container">
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" class="jux-reshape-preview"></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
@@ -583,73 +564,93 @@ export class DataFrameComponent extends BaseComponent {
583
564
  const updateHint = (headerRow) => {
584
565
  if (!hintDiv)
585
566
  return;
586
- hintDiv.innerHTML = headerRow > 0
587
- ? `Row <strong>${headerRow}</strong> will be used as column headers. ` +
588
- `The <strong>${headerRow}</strong> row${headerRow > 1 ? 's' : ''} above will be skipped.`
589
- : `Row <strong>0</strong> (first row) will be used as column headers.`;
567
+ if (headerRow > 0) {
568
+ hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
569
+ `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
570
+ }
571
+ else {
572
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
573
+ }
590
574
  };
591
575
  const updatePreview = async () => {
592
576
  const headerRow = parseInt(headerRowInput?.value) || 0;
593
577
  updateHint(headerRow);
594
578
  try {
595
- // First, get raw data (headerRow=0) to show skipped rows
579
+ // Get raw data to show all rows
596
580
  const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
597
581
  headerRow: 0,
598
- maxSheetSize: headerRow + 15
582
+ maxSheetSize: headerRow + 12
599
583
  });
600
584
  const rawSheet = Object.values(rawSheets)[0];
601
- // Then, get parsed data with the chosen header row
602
- const parsedSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
603
- headerRow,
604
- maxSheetSize: headerRow + 15
605
- });
606
- const parsedSheet = Object.values(parsedSheets)[0];
607
- if (!rawSheet && !parsedSheet) {
585
+ if (!rawSheet) {
608
586
  if (previewDiv)
609
587
  previewDiv.textContent = 'No data found';
610
588
  return;
611
589
  }
612
- const colWidth = 22;
613
- const idxWidth = 6;
614
- const lines = [];
615
- // Show skipped rows (rows before the header)
616
- if (rawSheet && headerRow > 0) {
617
- lines.push('');
618
- lines.push(` ┌${'─'.repeat(100)}┐`);
619
- lines.push(` │ ROWS 0-${headerRow - 1} WILL BE SKIPPED (not imported)`.padEnd(101) + '│');
620
- lines.push(` └${'─'.repeat(100)}┘`);
621
- lines.push('');
622
- const rawRows = rawSheet.toRows();
623
- const rawCols = rawSheet.columns;
624
- // Show raw header (row 0) - dimmed
625
- lines.push(`${'[0]'.padEnd(idxWidth)}${rawCols.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ← skipped`);
626
- const skippedCount = Math.min(headerRow - 1, rawRows.length);
627
- for (let i = 0; i < skippedCount; i++) {
628
- const row = rawRows[i];
629
- const rowIdx = `[${i + 1}]`.padEnd(idxWidth);
630
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
631
- lines.push(`${rowIdx}${cols} ← skipped`);
590
+ const rawCols = rawSheet.columns;
591
+ const rawRows = rawSheet.toRows();
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;';
632
603
  }
633
- lines.push('');
634
- lines.push(` ╔${'═'.repeat(100)}╗`);
635
- lines.push(` ║ ▼ DATA STARTS HERE (Row ${headerRow} = Column Headers)`.padEnd(101) + '║');
636
- lines.push(` ╚${'═'.repeat(100)}╝`);
637
- lines.push('');
638
- }
639
- // Show header row and data rows from parsed result
640
- if (parsedSheet) {
641
- const headerLine = `${'HDR'.padEnd(idxWidth)}${parsedSheet.columns.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ◀ HEADERS`;
642
- lines.push(headerLine);
643
- lines.push(`${'───'.padEnd(idxWidth)}${'─'.repeat(Math.min(colWidth * parsedSheet.columns.length, 120))}`);
644
- const dataRows = parsedSheet.toRows().slice(0, 8);
645
- dataRows.forEach((row, i) => {
646
- const rowIdx = `[${headerRow + 1 + i}]`.padEnd(idxWidth);
647
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
648
- lines.push(`${rowIdx}${cols}`);
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>`;
649
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>';
650
650
  }
651
+ html += '</table>';
651
652
  if (previewDiv) {
652
- previewDiv.textContent = lines.join('\n');
653
+ previewDiv.innerHTML = html;
653
654
  }
654
655
  }
655
656
  catch (err) {
@@ -662,6 +663,11 @@ export class DataFrameComponent extends BaseComponent {
662
663
  updatePreview();
663
664
  this._reshapeModal.open();
664
665
  }
666
+ _escapeHtml(text) {
667
+ const div = document.createElement('div');
668
+ div.textContent = text;
669
+ return div.innerHTML;
670
+ }
665
671
  _showCSVReshapeModal() {
666
672
  if (!this._rawFileData)
667
673
  return;
@@ -683,17 +689,13 @@ export class DataFrameComponent extends BaseComponent {
683
689
  </select>
684
690
  </div>
685
691
  <div style="margin-bottom: 1rem;">
686
- <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>
687
693
  <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
688
694
  </div>
689
- <div style="margin-bottom: 1rem;">
690
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
691
- <input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
692
- </div>
693
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint"></div>
694
- <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">
695
697
  <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
696
- <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>
697
699
  </div>
698
700
  `;
699
701
  this._reshapeModal
@@ -712,17 +714,14 @@ export class DataFrameComponent extends BaseComponent {
712
714
  return;
713
715
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
714
716
  const headerRowInput = document.getElementById(`${this._id}-header-row`);
715
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
716
717
  const delim = delimiterSelect.value;
717
718
  const headerRow = parseInt(headerRowInput.value) || 0;
718
- const skipRows = parseInt(skipRowsInput.value) || 0;
719
719
  this.state.loading = true;
720
720
  this._updateStatus('Re-parsing with new settings...', 'loading');
721
721
  try {
722
722
  const df = this._driver.parseCSV(this._rawFileData.text, {
723
723
  delimiter: delim,
724
724
  headerRow,
725
- skipRows,
726
725
  hasHeader: true
727
726
  });
728
727
  await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
@@ -740,7 +739,6 @@ export class DataFrameComponent extends BaseComponent {
740
739
  requestAnimationFrame(() => {
741
740
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
742
741
  const headerRowInput = document.getElementById(`${this._id}-header-row`);
743
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
744
742
  const previewDiv = document.getElementById(`${this._id}-preview`);
745
743
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`);
746
744
  if (this._rawFileData?.text) {
@@ -755,75 +753,83 @@ export class DataFrameComponent extends BaseComponent {
755
753
  if (!hintDiv)
756
754
  return;
757
755
  const headerRow = parseInt(headerRowInput?.value) || 0;
758
- const skipRows = parseInt(skipRowsInput?.value) || 0;
759
- const totalSkipped = headerRow + skipRows;
760
- hintDiv.innerHTML = totalSkipped > 0
761
- ? `Row <strong>${headerRow + skipRows}</strong> will be used as column headers. ` +
762
- `<strong>${totalSkipped}</strong> row${totalSkipped > 1 ? 's' : ''} above will be skipped.`
763
- : `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
+ }
764
763
  };
765
764
  const updatePreview = () => {
766
765
  if (!this._rawFileData?.text)
767
766
  return;
768
767
  const delim = delimiterSelect?.value || ',';
769
768
  const headerRow = parseInt(headerRowInput?.value) || 0;
770
- const skipRows = parseInt(skipRowsInput?.value) || 0;
771
769
  updateHint();
772
770
  try {
773
- const colWidth = 22;
774
- const idxWidth = 6;
775
- const lines = [];
776
- const totalOffset = headerRow + skipRows;
777
- // Parse raw (no header offset) to show skipped rows
778
- if (totalOffset > 0) {
779
- const rawDf = this._driver.parseCSV(this._rawFileData.text, {
780
- delimiter: delim,
781
- headerRow: 0,
782
- skipRows: 0,
783
- hasHeader: true,
784
- maxRows: totalOffset + 1
785
- });
786
- lines.push('');
787
- lines.push(` ┌${'─'.repeat(100)}┐`);
788
- lines.push(` │ ROWS 0-${totalOffset - 1} WILL BE SKIPPED (not imported)`.padEnd(101) + '│');
789
- lines.push(` └${'─'.repeat(100)}┘`);
790
- lines.push('');
791
- const rawCols = rawDf.columns;
792
- lines.push(`${'[0]'.padEnd(idxWidth)}${rawCols.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ← skipped`);
793
- const rawRows = rawDf.toRows();
794
- const skippedCount = Math.min(totalOffset - 1, rawRows.length);
795
- for (let i = 0; i < skippedCount; i++) {
796
- const row = rawRows[i];
797
- const rowIdx = `[${i + 1}]`.padEnd(idxWidth);
798
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
799
- lines.push(`${rowIdx}${cols} ← skipped`);
800
- }
801
- lines.push('');
802
- lines.push(` ╔${'═'.repeat(100)}╗`);
803
- lines.push(` ║ ▼ DATA STARTS HERE (Row ${totalOffset} = Column Headers)`.padEnd(101) + '║');
804
- lines.push(` ╚${'═'.repeat(100)}╝`);
805
- lines.push('');
806
- }
807
- // Parse with actual settings for header + data rows
808
- const df = this._driver.parseCSV(this._rawFileData.text, {
771
+ // Parse raw to show all rows
772
+ const rawDf = this._driver.parseCSV(this._rawFileData.text, {
809
773
  delimiter: delim,
810
- headerRow,
811
- skipRows,
774
+ headerRow: 0,
812
775
  hasHeader: true,
813
- maxRows: 8
776
+ maxRows: headerRow + 10
814
777
  });
815
- const headerLine = `${'HDR'.padEnd(idxWidth)}${df.columns.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ◀ HEADERS`;
816
- lines.push(headerLine);
817
- lines.push(`${'───'.padEnd(idxWidth)}${'─'.repeat(Math.min(colWidth * df.columns.length, 120))}`);
818
- const dataRows = df.toRows();
819
- dataRows.forEach((row, i) => {
820
- const rowIdx = `[${totalOffset + 1 + i}]`.padEnd(idxWidth);
821
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
822
- lines.push(`${rowIdx}${cols}`);
823
- });
824
- if (previewDiv) {
825
- 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>';
826
829
  }
830
+ html += '</table>';
831
+ if (previewDiv)
832
+ previewDiv.innerHTML = html;
827
833
  }
828
834
  catch (err) {
829
835
  if (previewDiv)
@@ -834,8 +840,6 @@ export class DataFrameComponent extends BaseComponent {
834
840
  delimiterSelect.addEventListener('change', updatePreview);
835
841
  if (headerRowInput)
836
842
  headerRowInput.addEventListener('input', updatePreview);
837
- if (skipRowsInput)
838
- skipRowsInput.addEventListener('input', updatePreview);
839
843
  updatePreview();
840
844
  this._reshapeModal.open();
841
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"
@@ -652,14 +627,14 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
652
627
  max="50"
653
628
  style="width: 100%;"
654
629
  />
655
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint">
630
+ <div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;">
656
631
  </div>
657
632
  </div>
658
633
  <div class="jux-reshape-preview-container">
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" class="jux-reshape-preview"></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
 
@@ -716,10 +691,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
716
691
 
717
692
  const updateHint = (headerRow: number) => {
718
693
  if (!hintDiv) return;
719
- hintDiv.innerHTML = headerRow > 0
720
- ? `Row <strong>${headerRow}</strong> will be used as column headers. ` +
721
- `The <strong>${headerRow}</strong> row${headerRow > 1 ? 's' : ''} above will be skipped.`
722
- : `Row <strong>0</strong> (first row) will be used as column headers.`;
694
+ if (headerRow > 0) {
695
+ hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
696
+ `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
697
+ } else {
698
+ hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
699
+ }
723
700
  };
724
701
 
725
702
  const updatePreview = async () => {
@@ -727,74 +704,90 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
727
704
  updateHint(headerRow);
728
705
 
729
706
  try {
730
- // First, get raw data (headerRow=0) to show skipped rows
707
+ // Get raw data to show all rows
731
708
  const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
732
709
  headerRow: 0,
733
- maxSheetSize: headerRow + 15
710
+ maxSheetSize: headerRow + 12
734
711
  });
735
712
  const rawSheet = Object.values(rawSheets)[0];
736
713
 
737
- // Then, get parsed data with the chosen header row
738
- const parsedSheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
739
- headerRow,
740
- maxSheetSize: headerRow + 15
741
- });
742
- const parsedSheet = Object.values(parsedSheets)[0];
743
-
744
- if (!rawSheet && !parsedSheet) {
714
+ if (!rawSheet) {
745
715
  if (previewDiv) previewDiv.textContent = 'No data found';
746
716
  return;
747
717
  }
748
718
 
749
- const colWidth = 22;
750
- const idxWidth = 6;
751
- const lines: string[] = [];
752
-
753
- // Show skipped rows (rows before the header)
754
- if (rawSheet && headerRow > 0) {
755
- lines.push('');
756
- lines.push(` ┌${'─'.repeat(100)}┐`);
757
- lines.push(` │ ROWS 0-${headerRow - 1} WILL BE SKIPPED (not imported)`.padEnd(101) + '│');
758
- lines.push(` └${'─'.repeat(100)}┘`);
759
- lines.push('');
760
-
761
- const rawRows = rawSheet.toRows();
762
- const rawCols = rawSheet.columns;
763
-
764
- // Show raw header (row 0) - dimmed
765
- lines.push(`${'[0]'.padEnd(idxWidth)}${rawCols.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ← skipped`);
766
-
767
- const skippedCount = Math.min(headerRow - 1, rawRows.length);
768
- for (let i = 0; i < skippedCount; i++) {
769
- const row = rawRows[i];
770
- const rowIdx = `[${i + 1}]`.padEnd(idxWidth);
771
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
772
- lines.push(`${rowIdx}${cols} ← skipped`);
719
+ const rawCols = rawSheet.columns;
720
+ const rawRows = rawSheet.toRows();
721
+
722
+ // Build a simple HTML table for clarity
723
+ let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
724
+
725
+ // Render each row
726
+ const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
727
+
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;';
734
+
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;';
773
739
  }
774
740
 
775
- lines.push('');
776
- lines.push(` ╔${'═'.repeat(100)}╗`);
777
- lines.push(` ║ ▼ DATA STARTS HERE (Row ${headerRow} = Column Headers)`.padEnd(101) + '║');
778
- lines.push(` ╚${'═'.repeat(100)}╝`);
779
- lines.push('');
780
- }
741
+ html += `<tr style="${rowStyle}">`;
781
742
 
782
- // Show header row and data rows from parsed result
783
- if (parsedSheet) {
784
- const headerLine = `${'HDR'.padEnd(idxWidth)}${parsedSheet.columns.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ◀ HEADERS`;
785
- lines.push(headerLine);
786
- lines.push(`${'───'.padEnd(idxWidth)}${'─'.repeat(Math.min(colWidth * parsedSheet.columns.length, 120))}`);
787
-
788
- const dataRows = parsedSheet.toRows().slice(0, 8);
789
- dataRows.forEach((row, i) => {
790
- const rowIdx = `[${headerRow + 1 + i}]`.padEnd(idxWidth);
791
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
792
- lines.push(`${rowIdx}${cols}`);
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
+ }
761
+
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>`;
793
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>';
794
785
  }
795
786
 
787
+ html += '</table>';
788
+
796
789
  if (previewDiv) {
797
- previewDiv.textContent = lines.join('\n');
790
+ previewDiv.innerHTML = html;
798
791
  }
799
792
  } catch (err: any) {
800
793
  if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
@@ -807,6 +800,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
807
800
  this._reshapeModal.open();
808
801
  }
809
802
 
803
+ private _escapeHtml(text: string): string {
804
+ const div = document.createElement('div');
805
+ div.textContent = text;
806
+ return div.innerHTML;
807
+ }
808
+
810
809
  private _showCSVReshapeModal(): void {
811
810
  if (!this._rawFileData) return;
812
811
 
@@ -830,17 +829,13 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
830
829
  </select>
831
830
  </div>
832
831
  <div style="margin-bottom: 1rem;">
833
- <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>
834
833
  <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
835
834
  </div>
836
- <div style="margin-bottom: 1rem;">
837
- <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
838
- <input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
839
- </div>
840
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint"></div>
841
- <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">
842
837
  <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
843
- <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>
844
839
  </div>
845
840
  `;
846
841
 
@@ -860,11 +855,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
860
855
 
861
856
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
862
857
  const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
863
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
864
858
 
865
859
  const delim = delimiterSelect.value;
866
860
  const headerRow = parseInt(headerRowInput.value) || 0;
867
- const skipRows = parseInt(skipRowsInput.value) || 0;
868
861
 
869
862
  this.state.loading = true;
870
863
  this._updateStatus('Re-parsing with new settings...', 'loading');
@@ -873,7 +866,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
873
866
  const df = this._driver.parseCSV(this._rawFileData.text, {
874
867
  delimiter: delim,
875
868
  headerRow,
876
- skipRows,
877
869
  hasHeader: true
878
870
  });
879
871
 
@@ -894,7 +886,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
894
886
  requestAnimationFrame(() => {
895
887
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
896
888
  const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
897
- const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
898
889
  const previewDiv = document.getElementById(`${this._id}-preview`)!;
899
890
  const hintDiv = document.getElementById(`${this._id}-reshape-hint`)!;
900
891
 
@@ -909,12 +900,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
909
900
  const updateHint = () => {
910
901
  if (!hintDiv) return;
911
902
  const headerRow = parseInt(headerRowInput?.value) || 0;
912
- const skipRows = parseInt(skipRowsInput?.value) || 0;
913
- const totalSkipped = headerRow + skipRows;
914
- hintDiv.innerHTML = totalSkipped > 0
915
- ? `Row <strong>${headerRow + skipRows}</strong> will be used as column headers. ` +
916
- `<strong>${totalSkipped}</strong> row${totalSkipped > 1 ? 's' : ''} above will be skipped.`
917
- : `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
+ }
918
909
  };
919
910
 
920
911
  const updatePreview = () => {
@@ -922,74 +913,81 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
922
913
 
923
914
  const delim = delimiterSelect?.value || ',';
924
915
  const headerRow = parseInt(headerRowInput?.value) || 0;
925
- const skipRows = parseInt(skipRowsInput?.value) || 0;
926
916
 
927
917
  updateHint();
928
918
 
929
919
  try {
930
- const colWidth = 22;
931
- const idxWidth = 6;
932
- const lines: string[] = [];
933
- const totalOffset = headerRow + skipRows;
934
-
935
- // Parse raw (no header offset) to show skipped rows
936
- if (totalOffset > 0) {
937
- const rawDf = this._driver.parseCSV(this._rawFileData.text, {
938
- delimiter: delim,
939
- headerRow: 0,
940
- skipRows: 0,
941
- hasHeader: true,
942
- maxRows: totalOffset + 1
943
- });
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);
944
939
 
945
- lines.push('');
946
- lines.push(` ┌${'─'.repeat(100)}┐`);
947
- lines.push(` │ ROWS 0-${totalOffset - 1} WILL BE SKIPPED (not imported)`.padEnd(101) + '│');
948
- lines.push(` └${'─'.repeat(100)}┘`);
949
- lines.push('');
950
-
951
- const rawCols = rawDf.columns;
952
- lines.push(`${'[0]'.padEnd(idxWidth)}${rawCols.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ← skipped`);
953
-
954
- const rawRows = rawDf.toRows();
955
- const skippedCount = Math.min(totalOffset - 1, rawRows.length);
956
- for (let i = 0; i < skippedCount; i++) {
957
- const row = rawRows[i];
958
- const rowIdx = `[${i + 1}]`.padEnd(idxWidth);
959
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
960
- 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;';
961
946
  }
962
947
 
963
- lines.push('');
964
- lines.push(` ╔${'═'.repeat(100)}╗`);
965
- lines.push(` ║ ▼ DATA STARTS HERE (Row ${totalOffset} = Column Headers)`.padEnd(101) + '║');
966
- lines.push(` ╚${'═'.repeat(100)}╝`);
967
- lines.push('');
968
- }
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
+ }
969
964
 
970
- // Parse with actual settings for header + data rows
971
- const df = this._driver.parseCSV(this._rawFileData.text, {
972
- delimiter: delim,
973
- headerRow,
974
- skipRows,
975
- hasHeader: true,
976
- maxRows: 8
977
- });
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
+ });
978
969
 
979
- const headerLine = `${'HDR'.padEnd(idxWidth)}${df.columns.map(c => String(c).substring(0, colWidth - 2).padEnd(colWidth)).join('│ ')} ◀ HEADERS`;
980
- lines.push(headerLine);
981
- 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
+ }
982
973
 
983
- const dataRows = df.toRows();
984
- dataRows.forEach((row, i) => {
985
- const rowIdx = `[${totalOffset + 1 + i}]`.padEnd(idxWidth);
986
- const cols = Object.values(row).map(v => String(v ?? '').substring(0, colWidth - 2).padEnd(colWidth)).join('│ ');
987
- lines.push(`${rowIdx}${cols}`);
988
- });
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>';
989
984
 
990
- if (previewDiv) {
991
- previewDiv.textContent = lines.join('\n');
985
+ html += '</tr>';
992
986
  }
987
+
988
+ html += '</table>';
989
+
990
+ if (previewDiv) previewDiv.innerHTML = html;
993
991
  } catch (err: any) {
994
992
  if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
995
993
  }
@@ -997,7 +995,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
997
995
 
998
996
  if (delimiterSelect) delimiterSelect.addEventListener('change', updatePreview);
999
997
  if (headerRowInput) headerRowInput.addEventListener('input', updatePreview);
1000
- if (skipRowsInput) skipRowsInput.addEventListener('input', updatePreview);
1001
998
 
1002
999
  updatePreview();
1003
1000
  this._reshapeModal!.open();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.195",
3
+ "version": "1.1.197",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",