juxscript 1.1.187 → 1.1.189

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.
@@ -4,7 +4,7 @@ import { TabularDriver } from '../storage/TabularDriver.js';
4
4
  import { FileUpload } from './fileupload.js';
5
5
  import { Table } from './table.js';
6
6
  import { Tabs } from './tabs.js';
7
- import { Modal } from './modal.js'; // ✅ Import Modal
7
+ import { Modal } from './modal.js';
8
8
  import { renderIcon } from './icons.js';
9
9
  const TRIGGER_EVENTS = [];
10
10
  const CALLBACK_EVENTS = ['load', 'error', 'transform'];
@@ -23,21 +23,21 @@ export class DataFrameComponent extends BaseComponent {
23
23
  });
24
24
  this._df = null;
25
25
  this._table = null;
26
- this._tabs = null; // ✅ NEW: Tabs for multi-sheet Excel // @ts-ignore used for multi-sheet
27
- this._sheets = new Map(); // ✅ NEW: Store all sheets
28
- this._uploadRef = null; // @ts-ignore used for reference tracking
26
+ this._tabs = null;
27
+ this._sheets = new Map();
28
+ this._uploadRef = null;
29
29
  this._storageKey = null;
30
30
  this._pendingSource = null;
31
31
  this._inlineUpload = null;
32
32
  this._showStatus = true;
33
33
  this._icon = '';
34
- this._maxSheetSize = 100000; // ✅ Default 100k rows
35
- this._sheetChunkSize = 10000; // ✅ Default 10k chunk
36
- this._maxFileSize = 50; // ✅ Default 50MB
34
+ this._maxSheetSize = 100000;
35
+ this._sheetChunkSize = 10000;
36
+ this._maxFileSize = 50;
37
37
  this._showReshapeWarning = true;
38
38
  this._rawFileData = null;
39
- this._reshapeModal = null; // ✅ ADD THIS LINE
40
- this._reshapeModalRendered = false; // Track if modal already rendered
39
+ this._reshapeModal = null;
40
+ this._reshapeModalRendered = false;
41
41
  this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
42
42
  this._showStatus = options.showStatus ?? true;
43
43
  this._icon = options.icon ?? '';
@@ -63,7 +63,7 @@ export class DataFrameComponent extends BaseComponent {
63
63
  this._storageKey = key;
64
64
  const loadFn = async () => {
65
65
  this.state.loading = true;
66
- this._updateStatus('Loading...', 'loading');
66
+ this._updateStatus('Loading...', 'loading');
67
67
  try {
68
68
  const df = await this._driver.loadByName(key);
69
69
  if (!df) {
@@ -77,7 +77,7 @@ export class DataFrameComponent extends BaseComponent {
77
77
  catch (err) {
78
78
  this._triggerCallback('error', err.message, null, this);
79
79
  this.state.loading = false;
80
- this._updateStatus('❌ ' + err.message, 'error');
80
+ this._updateStatus(err.message, 'error');
81
81
  }
82
82
  };
83
83
  if (this._table) {
@@ -94,56 +94,7 @@ export class DataFrameComponent extends BaseComponent {
94
94
  upload.bind('change', async (files) => {
95
95
  if (!files || files.length === 0)
96
96
  return;
97
- const file = files[0];
98
- // ✅ Check file size
99
- const fileSizeMB = file.size / (1024 * 1024);
100
- if (fileSizeMB > this._maxFileSize) {
101
- this._updateStatus(`❌ File too large (${fileSizeMB.toFixed(1)}MB). Max: ${this._maxFileSize}MB`, 'error');
102
- return;
103
- }
104
- this.state.loading = true;
105
- this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
106
- try {
107
- const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
108
- file.name.toLowerCase().endsWith('.xls');
109
- if (isExcel) {
110
- // ✅ Store raw file for reshape
111
- this._rawFileData = { file, isExcel: true };
112
- // ✅ Pass chunking options for large files
113
- const sheets = await this._driver.streamFileMultiSheet(file, {
114
- maxSheetSize: this._maxSheetSize,
115
- sheetChunkSize: this._sheetChunkSize,
116
- onProgress: (loaded, total) => {
117
- const pct = total ? Math.round((loaded / total) * 100) : 0;
118
- this._updateStatus(`⏳ Parsing ${file.name}... ${pct}%`, 'loading');
119
- }
120
- });
121
- const sheetNames = Object.keys(sheets);
122
- await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
123
- if (sheetNames.length > 1) {
124
- this._renderMultiSheet(sheets, file.name);
125
- }
126
- else {
127
- this._setDataFrame(sheets[sheetNames[0]], file.name);
128
- }
129
- }
130
- else {
131
- // ✅ CSV/TSV: Store raw text for reshaping
132
- const text = await file.text();
133
- this._rawFileData = { file, text, isExcel: false };
134
- const df = this._driver.parseCSV(text, {
135
- autoDetectDelimiter: true,
136
- hasHeader: true
137
- });
138
- await this._driver.store(file.name, df, { source: file.name });
139
- this._setDataFrame(df, file.name);
140
- }
141
- }
142
- catch (err) {
143
- this._triggerCallback('error', err.message, null, this);
144
- this.state.loading = false;
145
- this._updateStatus('❌ ' + err.message, 'error');
146
- }
97
+ await this._handleFile(files[0]);
147
98
  });
148
99
  };
149
100
  return this;
@@ -151,7 +102,7 @@ export class DataFrameComponent extends BaseComponent {
151
102
  fromData(data) {
152
103
  const loadFn = async () => {
153
104
  this.state.loading = true;
154
- this._updateStatus('Loading data...', 'loading');
105
+ this._updateStatus('Loading data...', 'loading');
155
106
  try {
156
107
  const df = new DataFrame(data);
157
108
  this._setDataFrame(df, 'inline data');
@@ -159,7 +110,7 @@ export class DataFrameComponent extends BaseComponent {
159
110
  catch (err) {
160
111
  this._triggerCallback('error', err.message, null, this);
161
112
  this.state.loading = false;
162
- this._updateStatus('❌ ' + err.message, 'error');
113
+ this._updateStatus(err.message, 'error');
163
114
  }
164
115
  };
165
116
  if (this._table) {
@@ -237,68 +188,90 @@ export class DataFrameComponent extends BaseComponent {
237
188
  filterable(v) { this._tableOptions.filterable = v; return this; }
238
189
  paginated(v) { this._tableOptions.paginated = v; return this; }
239
190
  rowsPerPage(v) { this._tableOptions.rowsPerPage = v; return this; }
240
- /**
241
- * NEW: Set max rows per sheet (prevents memory issues with huge Excel files)
242
- */
243
- maxSheetSize(v) {
244
- this._maxSheetSize = v;
245
- return this;
246
- }
247
- /**
248
- * NEW: Set chunk size for processing large sheets
249
- */
250
- sheetChunkSize(v) {
251
- this._sheetChunkSize = v;
252
- return this;
253
- }
254
- /**
255
- * NEW: Set max file size in MB
256
- */
257
- maxFileSize(mb) {
258
- this._maxFileSize = mb;
259
- return this;
191
+ maxSheetSize(v) { this._maxSheetSize = v; return this; }
192
+ sheetChunkSize(v) { this._sheetChunkSize = v; return this; }
193
+ maxFileSize(mb) { this._maxFileSize = mb; return this; }
194
+ /* ═══════════════════════════════════════════════════
195
+ * FILE HANDLING
196
+ * ═══════════════════════════════════════════════════ */
197
+ async _handleFile(file) {
198
+ const fileSizeMB = file.size / (1024 * 1024);
199
+ if (fileSizeMB > this._maxFileSize) {
200
+ this._updateStatus(`File too large (${fileSizeMB.toFixed(1)}MB). Max: ${this._maxFileSize}MB`, 'error');
201
+ return;
202
+ }
203
+ this.state.loading = true;
204
+ this._updateStatus('Parsing ' + file.name + '...', 'loading');
205
+ try {
206
+ const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
207
+ file.name.toLowerCase().endsWith('.xls');
208
+ if (isExcel) {
209
+ this._rawFileData = { file, isExcel: true };
210
+ const sheets = await this._driver.streamFileMultiSheet(file, {
211
+ maxSheetSize: this._maxSheetSize,
212
+ sheetChunkSize: this._sheetChunkSize,
213
+ onProgress: (loaded, total) => {
214
+ const pct = total ? Math.round((loaded / total) * 100) : 0;
215
+ this._updateStatus(`Parsing ${file.name}... ${pct}%`, 'loading');
216
+ }
217
+ });
218
+ const sheetNames = Object.keys(sheets);
219
+ await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
220
+ if (sheetNames.length > 1) {
221
+ this._renderMultiSheet(sheets, file.name);
222
+ }
223
+ else {
224
+ this._setDataFrame(sheets[sheetNames[0]], file.name);
225
+ }
226
+ }
227
+ else {
228
+ const text = await file.text();
229
+ this._rawFileData = { file, text, isExcel: false };
230
+ const df = this._driver.parseCSV(text, {
231
+ autoDetectDelimiter: true,
232
+ hasHeader: true
233
+ });
234
+ await this._driver.store(file.name, df, { source: file.name });
235
+ this._setDataFrame(df, file.name);
236
+ }
237
+ }
238
+ catch (err) {
239
+ this._triggerCallback('error', err.message, null, this);
240
+ this.state.loading = false;
241
+ this._updateStatus('Error: ' + err.message, 'error');
242
+ }
260
243
  }
261
244
  /* ═══════════════════════════════════════════════════
262
245
  * MULTI-SHEET RENDERING
263
246
  * ═══════════════════════════════════════════════════ */
264
- /**
265
- * ✅ FIXED: Render multiple Excel sheets as tabs
266
- */
267
247
  _renderMultiSheet(sheets, sourceName) {
268
248
  this.state.loading = false;
269
249
  this._sheets.clear();
270
250
  const wrapper = document.getElementById(this._id);
271
251
  if (!wrapper)
272
252
  return;
273
- // Clear existing table if any
274
253
  const existingTable = wrapper.querySelector('.jux-table-wrapper');
275
254
  if (existingTable)
276
255
  existingTable.remove();
277
- // Store all sheets
278
256
  Object.entries(sheets).forEach(([name, df]) => {
279
257
  this._sheets.set(name, df);
280
258
  });
281
259
  const sheetNames = Object.keys(sheets);
282
- // ✅ FIX: Create tabs with EMPTY content first (just placeholders)
283
260
  const tabs = new Tabs(`${this._id}-tabs`, {
284
261
  tabs: sheetNames.map(name => ({
285
262
  id: name,
286
263
  label: name,
287
- content: '' // Empty - we'll fill after render
264
+ content: ''
288
265
  })),
289
266
  activeTab: sheetNames[0]
290
267
  });
291
268
  this._tabs = tabs;
292
- // Render tabs container
293
269
  const tabsContainer = document.createElement('div');
294
270
  tabsContainer.className = 'jux-dataframe-tabs';
295
271
  wrapper.appendChild(tabsContainer);
296
- // ✅ Render tabs NOW (creates all tab panels in DOM)
297
272
  tabs.render(tabsContainer);
298
- // ✅ NOW populate each tab with its DataFrame table (panels exist now)
299
273
  sheetNames.forEach(sheetName => {
300
274
  const df = sheets[sheetName];
301
- // Create table
302
275
  const table = new Table(`${this._id}-table-${sheetName}`, {
303
276
  striped: this._tableOptions.striped,
304
277
  hoverable: this._tableOptions.hoverable,
@@ -307,21 +280,12 @@ export class DataFrameComponent extends BaseComponent {
307
280
  paginated: this._tableOptions.paginated,
308
281
  rowsPerPage: this._tableOptions.rowsPerPage
309
282
  });
310
- // Convert columns to ColumnDef[]
311
- const columnDefs = df.columns.map(col => ({
312
- key: col,
313
- label: col
314
- }));
283
+ const columnDefs = df.columns.map(col => ({ key: col, label: col }));
315
284
  table.columns(columnDefs).rows(df.toRows());
316
- // ✅ Find the tab panel (it exists now because tabs.render() was called)
317
285
  const tabPanel = document.getElementById(`${this._id}-tabs-${sheetName}-panel`);
318
- if (!tabPanel) {
319
- console.error(`Tab panel not found: ${this._id}-tabs-${sheetName}-panel`);
286
+ if (!tabPanel)
320
287
  return;
321
- }
322
- // ✅ Render table directly into tab panel
323
288
  table.render(tabPanel);
324
- // Add filter if enabled
325
289
  if (this._tableOptions.filterable) {
326
290
  const filterContainer = document.createElement('div');
327
291
  filterContainer.className = 'jux-dataframe-filter';
@@ -343,22 +307,17 @@ export class DataFrameComponent extends BaseComponent {
343
307
  table.rows(df.toRows());
344
308
  return;
345
309
  }
346
- const filtered = df.filter((row) => {
347
- return Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text));
348
- });
310
+ const filtered = df.filter((row) => Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text)));
349
311
  table.rows(filtered.toRows());
350
312
  });
351
- // ✅ Insert filter BEFORE the table wrapper (which exists now)
352
313
  const tableWrapper = tabPanel.querySelector('.jux-table-wrapper');
353
314
  if (tableWrapper) {
354
315
  tabPanel.insertBefore(filterContainer, tableWrapper);
355
316
  }
356
317
  }
357
318
  });
358
- // Update status
359
319
  const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
360
320
  this._updateStatus(`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`, 'success');
361
- // Set first sheet as active DataFrame
362
321
  this._df = sheets[sheetNames[0]];
363
322
  this._triggerCallback('load', this._df, null, this);
364
323
  }
@@ -396,25 +355,18 @@ export class DataFrameComponent extends BaseComponent {
396
355
  if (cleanCols.length < df.columns.length) {
397
356
  this._df = df.select(...cleanCols);
398
357
  }
399
- // Update the table with new data
400
358
  if (this._table && this._df) {
401
- const columnDefs = this._df.columns.map(col => ({
402
- key: col,
403
- label: col
404
- }));
359
+ const columnDefs = this._df.columns.map(col => ({ key: col, label: col }));
405
360
  this._table.columns(columnDefs).rows(this._df.toRows());
406
361
  }
407
- // ✅ Detect malformed data
408
362
  const isMalformed = this._detectMalformedData(this._df);
409
- // ✅ Show warning if malformed
410
363
  if (isMalformed && this._showReshapeWarning && this._rawFileData) {
411
- this._updateStatus(`⚠️ ${sourceName} — ${this._df.height} rows × ${this._df.width} cols (Data may be malformed — headers may be on wrong row)`, 'warning');
412
- // Add Fix Import Settings button after a tick to ensure status DOM is ready
364
+ this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols (Data may be malformed — headers may be on wrong row)`, 'warning');
413
365
  requestAnimationFrame(() => {
414
366
  const statusEl = document.getElementById(`${this._id}-status`);
415
367
  if (statusEl) {
416
368
  const settingsBtn = document.createElement('button');
417
- settingsBtn.textContent = '⚙️ Fix Import Settings';
369
+ settingsBtn.textContent = 'Fix Import Settings';
418
370
  settingsBtn.className = 'jux-button jux-button-sm jux-button-warning';
419
371
  settingsBtn.style.marginLeft = '0.5rem';
420
372
  settingsBtn.addEventListener('click', () => this._showReshapeModal());
@@ -424,13 +376,12 @@ export class DataFrameComponent extends BaseComponent {
424
376
  }
425
377
  else {
426
378
  this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols`, 'success');
427
- // ✅ Still add Settings button for manual adjustment
428
379
  if (this._showReshapeWarning && this._rawFileData) {
429
380
  requestAnimationFrame(() => {
430
381
  const statusEl = document.getElementById(`${this._id}-status`);
431
382
  if (statusEl) {
432
383
  const settingsBtn = document.createElement('button');
433
- settingsBtn.textContent = '⚙️ Settings';
384
+ settingsBtn.textContent = 'Settings';
434
385
  settingsBtn.className = 'jux-button jux-button-sm jux-button-ghost';
435
386
  settingsBtn.style.marginLeft = '0.5rem';
436
387
  settingsBtn.addEventListener('click', () => this._showReshapeModal());
@@ -441,67 +392,65 @@ export class DataFrameComponent extends BaseComponent {
441
392
  }
442
393
  this._triggerCallback('load', this._df, null, this);
443
394
  }
444
- /**
445
- * NEW: Detect if data looks malformed
446
- */
395
+ /* ═══════════════════════════════════════════════════
396
+ * MALFORMED DATA DETECTION
397
+ * ═══════════════════════════════════════════════════ */
447
398
  _detectMalformedData(df) {
448
399
  const columns = df.columns;
449
400
  const rows = df.toRows();
450
- // Check 1: Columns have generic names like "__EMPTY", "_1", "col_0"
451
401
  const hasGenericColumns = columns.some(col => col.startsWith('__EMPTY') ||
452
- col.startsWith('_') ||
402
+ col.match(/^_\d+$/) ||
453
403
  col.match(/^col_\d+$/));
454
404
  if (hasGenericColumns)
455
405
  return true;
456
- // Check 2: First row values look like metadata (e.g., "Exported On:", "12/4/2025")
457
406
  if (rows.length > 0) {
458
407
  const firstRow = rows[0];
459
408
  const values = Object.values(firstRow);
409
+ const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
410
+ if (nonEmpty.length < columns.length * 0.5)
411
+ return true;
460
412
  const hasMetadata = values.some(v => String(v).includes('Exported') ||
461
413
  String(v).includes('Generated') ||
462
414
  String(v).includes('Report'));
463
415
  if (hasMetadata)
464
416
  return true;
465
417
  }
466
- // Check 3: Row 2 or 3 has values that look like headers (mostly strings, no numbers)
467
- if (rows.length >= 3) {
468
- const secondRow = rows[1];
469
- const thirdRow = rows[2];
470
- const checkRow = (row) => {
471
- const values = Object.values(row);
472
- const nonNumeric = values.filter(v => {
473
- const str = String(v).trim();
474
- return isNaN(Number(str)) && str !== '';
475
- }).length;
476
- return nonNumeric >= values.length * 0.7; // 70% non-numeric
477
- };
478
- if (checkRow(secondRow) || checkRow(thirdRow)) {
479
- return true;
418
+ return false;
419
+ }
420
+ _detectLikelyHeaderRow(df) {
421
+ const rows = df.toRows();
422
+ for (let i = 0; i < Math.min(rows.length, 10); i++) {
423
+ const row = rows[i];
424
+ const values = Object.values(row);
425
+ const nonEmpty = values.filter(v => v !== null && v !== undefined && String(v).trim() !== '');
426
+ if (nonEmpty.length < values.length * 0.5)
427
+ continue;
428
+ const nonNumericCount = nonEmpty.filter(v => {
429
+ const str = String(v).trim();
430
+ return isNaN(Number(str)) && str !== '';
431
+ }).length;
432
+ if (nonNumericCount >= nonEmpty.length * 0.7 && i > 0) {
433
+ // i is index in toRows() but row 0 of the file was consumed as header,
434
+ // so the actual file row index is i + 1
435
+ return i + 1;
480
436
  }
481
437
  }
482
- return false;
438
+ return 0;
483
439
  }
484
- /**
485
- * UPDATED: Show settings modal using Modal component
486
- */
440
+ /* ═══════════════════════════════════════════════════
441
+ * RESHAPE MODAL
442
+ * ═══════════════════════════════════════════════════ */
487
443
  _showReshapeModal() {
488
444
  if (!this._rawFileData)
489
445
  return;
490
- const isExcel = this._rawFileData.isExcel;
491
- if (isExcel) {
446
+ if (this._rawFileData.isExcel) {
492
447
  this._showExcelReshapeModal();
493
448
  }
494
449
  else {
495
450
  this._showCSVReshapeModal();
496
451
  }
497
452
  }
498
- /**
499
- * ✅ UPDATED: Excel reshape modal using Modal component
500
- */
501
- async _showExcelReshapeModal() {
502
- if (!this._rawFileData?.file)
503
- return;
504
- // Remove old modal from DOM if it was previously rendered
453
+ _cleanupReshapeModal() {
505
454
  if (this._reshapeModal && this._reshapeModalRendered) {
506
455
  const oldEl = document.getElementById(`${this._id}-reshape-modal`);
507
456
  if (oldEl)
@@ -509,43 +458,47 @@ export class DataFrameComponent extends BaseComponent {
509
458
  this._reshapeModal = null;
510
459
  this._reshapeModalRendered = false;
511
460
  }
512
- // Create fresh modal
461
+ }
462
+ async _showExcelReshapeModal() {
463
+ if (!this._rawFileData?.file)
464
+ return;
465
+ this._cleanupReshapeModal();
466
+ const suggestedRow = this._df ? this._detectLikelyHeaderRow(this._df) : 0;
513
467
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
514
468
  title: 'Excel Import Settings',
515
469
  size: 'large',
516
470
  close: true,
517
471
  backdropClose: false
518
472
  });
519
- // Build modal content
520
- const modalContent = document.createElement('div');
521
- modalContent.innerHTML = `
473
+ const modalContentHTML = `
522
474
  <div style="margin-bottom: 1rem;">
523
475
  <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
524
476
  Header Row (0-based)
525
477
  </label>
526
- <input
527
- type="number"
528
- id="${this._id}-header-row"
529
- class="jux-input-element"
530
- value="0"
531
- min="0"
532
- max="50"
533
- style="width: 100%;"
478
+ <input
479
+ type="number"
480
+ id="${this._id}-header-row"
481
+ class="jux-input-element"
482
+ value="${suggestedRow}"
483
+ min="0"
484
+ max="50"
485
+ style="width: 100%;"
534
486
  />
535
- <div style="font-size: 0.875rem; color: hsl(var(--muted-foreground)); margin-top: 0.5rem; padding: 0.5rem; background: hsl(var(--warning) / 0.1); border-radius: var(--radius); border: 1px solid hsl(var(--warning) / 0.3);">
536
- ⚠️ <strong>Detected issue:</strong> Row 0 contains metadata ("Exported On:"), row 2 contains actual headers ("340B ID"). Try setting this to <strong>2</strong>.
487
+ <div class="jux-reshape-hint">
488
+ <strong>Detected issue:</strong> The current header row appears to contain metadata or empty values.
489
+ Row ${suggestedRow} looks like it contains the actual column headers.
490
+ Adjust the value above and check the preview below.
537
491
  </div>
538
492
  </div>
539
-
540
- <div style="border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 1rem; background: hsl(var(--muted) / 0.3); max-height: 400px; overflow-y: auto;">
493
+ <div class="jux-reshape-preview-container">
541
494
  <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
542
495
  Preview (first 10 rows)
543
496
  </div>
544
- <div id="${this._id}-preview" style="font-family: 'JetBrains Mono', 'Courier New', monospace; font-size: 0.75rem; white-space: pre; color: hsl(var(--foreground)); line-height: 1.5;"></div>
497
+ <div id="${this._id}-preview" class="jux-reshape-preview"></div>
545
498
  </div>
546
499
  `;
547
500
  this._reshapeModal
548
- .content(modalContent.innerHTML)
501
+ .content(modalContentHTML)
549
502
  .actions([
550
503
  {
551
504
  label: 'Cancel',
@@ -556,10 +509,10 @@ export class DataFrameComponent extends BaseComponent {
556
509
  label: 'Apply & Re-import',
557
510
  variant: 'primary',
558
511
  click: async () => {
559
- const headerRowInput = document.getElementById(`${this._id}-header-row`);
560
- const headerRow = parseInt(headerRowInput.value) || 0;
512
+ const input = document.getElementById(`${this._id}-header-row`);
513
+ const headerRow = parseInt(input.value) || 0;
561
514
  this.state.loading = true;
562
- this._updateStatus('Re-parsing with new settings...', 'loading');
515
+ this._updateStatus('Re-parsing with new settings...', 'loading');
563
516
  try {
564
517
  const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
565
518
  headerRow,
@@ -572,48 +525,46 @@ export class DataFrameComponent extends BaseComponent {
572
525
  this._renderMultiSheet(sheets, this._rawFileData.file.name);
573
526
  }
574
527
  else {
575
- this._showReshapeWarning = false; // Prevent recursive warning after manual fix
528
+ this._showReshapeWarning = false;
576
529
  this._setDataFrame(sheets[sheetNames[0]], this._rawFileData.file.name);
577
530
  }
578
531
  this._reshapeModal.closeModal();
579
532
  }
580
533
  catch (err) {
581
- this._updateStatus(`❌ ${err.message}`, 'error');
534
+ this._updateStatus(`Error: ${err.message}`, 'error');
582
535
  }
583
536
  }
584
537
  }
585
538
  ]);
586
- // Render modal to document.body and open it
587
539
  this._reshapeModal.render(document.body);
588
540
  this._reshapeModalRendered = true;
589
- // Wait a tick for DOM to update after render
590
541
  await new Promise(resolve => requestAnimationFrame(resolve));
591
542
  const headerRowInput = document.getElementById(`${this._id}-header-row`);
592
543
  const previewDiv = document.getElementById(`${this._id}-preview`);
593
- // Update preview on header row change
594
544
  const updatePreview = async () => {
595
545
  const headerRow = parseInt(headerRowInput?.value) || 0;
596
546
  try {
597
547
  const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
598
548
  headerRow,
599
- maxSheetSize: 10
549
+ maxSheetSize: headerRow + 20
600
550
  });
601
551
  const firstSheet = Object.values(sheets)[0];
602
552
  if (!firstSheet) {
603
553
  if (previewDiv)
604
- previewDiv.textContent = '⚠️ No data found';
554
+ previewDiv.textContent = 'No data found';
605
555
  return;
606
556
  }
607
557
  const preview = firstSheet.toRows().slice(0, 10).map((row, i) => {
608
- const cols = Object.values(row).map(v => String(v).padEnd(20)).join(' ');
609
- return `${i === 0 ? '📌 ' : ' '}${cols}`;
558
+ const cols = Object.values(row).map(v => String(v ?? '').padEnd(20)).join(' | ');
559
+ return `${i === 0 ? '>> ' : ' '}${cols}`;
610
560
  }).join('\n');
611
- if (previewDiv)
612
- previewDiv.textContent = `Columns: ${firstSheet.columns.join(' ')}\n${'─'.repeat(80)}\n${preview}`;
561
+ if (previewDiv) {
562
+ previewDiv.textContent = `Columns: ${firstSheet.columns.join(' | ')}\n${'─'.repeat(80)}\n${preview}`;
563
+ }
613
564
  }
614
565
  catch (err) {
615
566
  if (previewDiv)
616
- previewDiv.textContent = `⚠️ Error: ${err.message}`;
567
+ previewDiv.textContent = `Error: ${err.message}`;
617
568
  }
618
569
  };
619
570
  if (headerRowInput)
@@ -621,57 +572,41 @@ export class DataFrameComponent extends BaseComponent {
621
572
  updatePreview();
622
573
  this._reshapeModal.open();
623
574
  }
624
- /**
625
- * ✅ UPDATED: CSV reshape modal using Modal component
626
- */
627
575
  _showCSVReshapeModal() {
628
576
  if (!this._rawFileData)
629
577
  return;
630
- // Remove old modal from DOM if it was previously rendered
631
- if (this._reshapeModal && this._reshapeModalRendered) {
632
- const oldEl = document.getElementById(`${this._id}-reshape-modal`);
633
- if (oldEl)
634
- oldEl.remove();
635
- this._reshapeModal = null;
636
- this._reshapeModalRendered = false;
637
- }
638
- // Create fresh modal
578
+ this._cleanupReshapeModal();
639
579
  this._reshapeModal = new Modal(`${this._id}-reshape-modal`, {
640
580
  title: 'CSV Import Settings',
641
581
  size: 'large',
642
582
  close: true,
643
583
  backdropClose: false
644
584
  });
645
- // Build modal content
646
- const modalContent = document.createElement('div');
647
- modalContent.innerHTML = `
585
+ const modalContentHTML = `
648
586
  <div style="margin-bottom: 1rem;">
649
587
  <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
650
588
  <select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
651
589
  <option value=",">Comma (,)</option>
652
590
  <option value="|">Pipe (|)</option>
653
- <option value="\t">Tab (\\t)</option>
591
+ <option value="&#9;">Tab (\\t)</option>
654
592
  <option value=";">Semicolon (;)</option>
655
593
  </select>
656
594
  </div>
657
-
658
595
  <div style="margin-bottom: 1rem;">
659
596
  <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based)</label>
660
597
  <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
661
598
  </div>
662
-
663
599
  <div style="margin-bottom: 1rem;">
664
600
  <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
665
601
  <input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
666
602
  </div>
667
-
668
- <div style="border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 1rem; background: hsl(var(--muted) / 0.3); max-height: 400px; overflow-y: auto;">
603
+ <div class="jux-reshape-preview-container">
669
604
  <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
670
- <div id="${this._id}-preview" style="font-family: 'JetBrains Mono', 'Courier New', monospace; font-size: 0.75rem; white-space: pre; color: hsl(var(--foreground)); line-height: 1.5;"></div>
605
+ <div id="${this._id}-preview" class="jux-reshape-preview"></div>
671
606
  </div>
672
607
  `;
673
608
  this._reshapeModal
674
- .content(modalContent.innerHTML)
609
+ .content(modalContentHTML)
675
610
  .actions([
676
611
  {
677
612
  label: 'Cancel',
@@ -687,11 +622,11 @@ export class DataFrameComponent extends BaseComponent {
687
622
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
688
623
  const headerRowInput = document.getElementById(`${this._id}-header-row`);
689
624
  const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
690
- const delim = delimiterSelect.value === '\\t' ? '\t' : delimiterSelect.value;
625
+ const delim = delimiterSelect.value;
691
626
  const headerRow = parseInt(headerRowInput.value) || 0;
692
627
  const skipRows = parseInt(skipRowsInput.value) || 0;
693
628
  this.state.loading = true;
694
- this._updateStatus('Re-parsing with new settings...', 'loading');
629
+ this._updateStatus('Re-parsing with new settings...', 'loading');
695
630
  try {
696
631
  const df = this._driver.parseCSV(this._rawFileData.text, {
697
632
  delimiter: delim,
@@ -700,39 +635,35 @@ export class DataFrameComponent extends BaseComponent {
700
635
  hasHeader: true
701
636
  });
702
637
  await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
703
- this._showReshapeWarning = false; // Prevent recursive warning after manual fix
638
+ this._showReshapeWarning = false;
704
639
  this._setDataFrame(df, this._rawFileData.file.name);
705
640
  this._reshapeModal.closeModal();
706
641
  }
707
642
  catch (err) {
708
- this._updateStatus(`❌ ${err.message}`, 'error');
643
+ this._updateStatus(`Error: ${err.message}`, 'error');
709
644
  }
710
645
  }
711
646
  }
712
647
  ]);
713
- // Render modal to document.body and open it
714
648
  this._reshapeModal.render(document.body);
715
649
  this._reshapeModalRendered = true;
716
- // Use requestAnimationFrame to ensure DOM is ready
717
650
  requestAnimationFrame(() => {
718
651
  const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
719
652
  const headerRowInput = document.getElementById(`${this._id}-header-row`);
720
653
  const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
721
654
  const previewDiv = document.getElementById(`${this._id}-preview`);
722
- // Auto-detect initial values
723
655
  if (this._rawFileData?.text) {
724
656
  const detected = this._driver._detectDelimiter(this._rawFileData.text);
725
657
  if (delimiterSelect)
726
- delimiterSelect.value = detected === '\t' ? '\\t' : detected;
727
- const headerRow = this._driver._detectHeaderRow(this._rawFileData.text, detected);
658
+ delimiterSelect.value = detected;
659
+ const detectedHeaderRow = this._driver._detectHeaderRow(this._rawFileData.text, detected);
728
660
  if (headerRowInput)
729
- headerRowInput.value = String(headerRow);
661
+ headerRowInput.value = String(detectedHeaderRow);
730
662
  }
731
- // Update preview on changes
732
- const updatePreview = async () => {
663
+ const updatePreview = () => {
733
664
  if (!this._rawFileData?.text)
734
665
  return;
735
- const delim = delimiterSelect?.value === '\\t' ? '\t' : (delimiterSelect?.value || ',');
666
+ const delim = delimiterSelect?.value || ',';
736
667
  const headerRow = parseInt(headerRowInput?.value) || 0;
737
668
  const skipRows = parseInt(skipRowsInput?.value) || 0;
738
669
  try {
@@ -744,15 +675,16 @@ export class DataFrameComponent extends BaseComponent {
744
675
  maxRows: 10
745
676
  });
746
677
  const preview = df.toRows().map((row, i) => {
747
- const cols = Object.values(row).map(v => String(v).padEnd(20)).join(' ');
748
- return `${i === 0 ? '📌 ' : ' '}${cols}`;
678
+ const cols = Object.values(row).map(v => String(v ?? '').padEnd(20)).join(' | ');
679
+ return `${i === 0 ? '>> ' : ' '}${cols}`;
749
680
  }).join('\n');
750
- if (previewDiv)
751
- previewDiv.textContent = `Columns: ${df.columns.join(' ')}\n${'─'.repeat(80)}\n${preview}`;
681
+ if (previewDiv) {
682
+ previewDiv.textContent = `Columns: ${df.columns.join(' | ')}\n${'─'.repeat(80)}\n${preview}`;
683
+ }
752
684
  }
753
685
  catch (err) {
754
686
  if (previewDiv)
755
- previewDiv.textContent = `⚠️ Error: ${err.message}`;
687
+ previewDiv.textContent = `Error: ${err.message}`;
756
688
  }
757
689
  };
758
690
  if (delimiterSelect)
@@ -765,10 +697,10 @@ export class DataFrameComponent extends BaseComponent {
765
697
  this._reshapeModal.open();
766
698
  });
767
699
  }
768
- update(_prop, _value) { }
769
700
  /* ═══════════════════════════════════════════════════
770
- * RENDER
701
+ * UPDATE & RENDER
771
702
  * ═══════════════════════════════════════════════════ */
703
+ update(_prop, _value) { }
772
704
  render(targetId) {
773
705
  const container = this._setupContainer(targetId);
774
706
  const { style, class: className } = this.state;
@@ -789,48 +721,11 @@ export class DataFrameComponent extends BaseComponent {
789
721
  }
790
722
  const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
791
723
  this._uploadRef = upload;
792
- // ✅ FIX: Use the SAME logic as fromUpload() to handle multi-sheet
793
724
  this._pendingSource = async () => {
794
725
  upload.bind('change', async (files) => {
795
726
  if (!files || files.length === 0)
796
727
  return;
797
- const file = files[0];
798
- this.state.loading = true;
799
- this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
800
- try {
801
- const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
802
- file.name.toLowerCase().endsWith('.xls');
803
- if (isExcel) {
804
- // ✅ Store raw file for reshape
805
- this._rawFileData = { file, isExcel: true };
806
- const sheets = await this._driver.streamFileMultiSheet(file, {
807
- maxSheetSize: this._maxSheetSize,
808
- sheetChunkSize: this._sheetChunkSize,
809
- onProgress: (loaded, total) => {
810
- const pct = total ? Math.round((loaded / total) * 100) : 0;
811
- this._updateStatus(`⏳ Parsing ${file.name}... ${pct}%`, 'loading');
812
- }
813
- });
814
- const sheetNames = Object.keys(sheets);
815
- await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
816
- if (sheetNames.length > 1) {
817
- this._renderMultiSheet(sheets, file.name);
818
- }
819
- else {
820
- this._setDataFrame(sheets[sheetNames[0]], file.name);
821
- }
822
- }
823
- else {
824
- const df = await this._driver.streamFile(file);
825
- await this._driver.store(file.name, df, { source: file.name });
826
- this._setDataFrame(df, file.name);
827
- }
828
- }
829
- catch (err) {
830
- this._triggerCallback('error', err.message, null, this);
831
- this.state.loading = false;
832
- this._updateStatus('❌ ' + err.message, 'error');
833
- }
728
+ await this._handleFile(files[0]);
834
729
  });
835
730
  };
836
731
  const uploadContainer = document.createElement('div');