juxscript 1.1.179 → 1.1.181

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.
@@ -14,6 +14,8 @@ export interface DataFrameOptions {
14
14
  rowsPerPage?: number;
15
15
  showStatus?: boolean;
16
16
  icon?: string;
17
+ maxSheetSize?: number;
18
+ sheetChunkSize?: number;
17
19
  style?: string;
18
20
  class?: string;
19
21
  }
@@ -36,6 +38,8 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
36
38
  private _inlineUpload;
37
39
  private _showStatus;
38
40
  private _icon;
41
+ private _maxSheetSize;
42
+ private _sheetChunkSize;
39
43
  constructor(id: string, options?: DataFrameOptions);
40
44
  protected getTriggerEvents(): readonly string[];
41
45
  protected getCallbackEvents(): readonly string[];
@@ -68,6 +72,14 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
68
72
  filterable(v: boolean): this;
69
73
  paginated(v: boolean): this;
70
74
  rowsPerPage(v: number): this;
75
+ /**
76
+ * ✅ NEW: Set max rows per sheet (prevents memory issues with huge Excel files)
77
+ */
78
+ maxSheetSize(v: number): this;
79
+ /**
80
+ * ✅ NEW: Set chunk size for processing large sheets
81
+ */
82
+ sheetChunkSize(v: number): this;
71
83
  /**
72
84
  * ✅ FIXED: Render multiple Excel sheets as tabs
73
85
  */
@@ -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":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAOnC,MAAM,WAAW,gBAAgB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,cAAc,GAAG,SAAS,GAAG;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,qBAAa,kBAAmB,SAAQ,aAAa,CAAC,cAAc,CAAC;IACjE,OAAO,CAAC,GAAG,CAA0B;IACrC,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,aAAa,CAOnB;IACF,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,aAAa,CAAgE;IACrF,OAAO,CAAC,WAAW,CAAiB;IACpC,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,aAAa,CAAkB;IACvC,OAAO,CAAC,eAAe,CAAiB;gBAE5B,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB;IAiCtD,SAAS,CAAC,gBAAgB,IAAI,SAAS,MAAM,EAAE;IAC/C,SAAS,CAAC,iBAAiB,IAAI,SAAS,MAAM,EAAE;IAMhD,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAwB9B,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAiDpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI;IAiBnE,UAAU,CAAC,KAAK,GAAE,MAAsB,EAAE,MAAM,GAAE,MAAoC,EAAE,IAAI,GAAE,MAAiB,GAAG,IAAI;IAStH,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAC5B,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAM3B,KAAK,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,SAAS,GAAG,IAAI;IAQ7C,MAAM,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI;IAI7E,MAAM,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAI/B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,OAAO,GAAG,IAAI;IAI7C,IAAI,CAAC,CAAC,GAAE,MAAU,GAAG,IAAI;IAIzB,IAAI,CAAC,CAAC,GAAE,MAAU,GAAG,IAAI;IAIzB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,GAAG,GAAG,IAAI;IAIpF,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAQxH,IAAI,EAAE,IAAI,SAAS,GAAG,IAAI,CAAqB;IAC/C,IAAI,MAAM,IAAI,aAAa,CAAyB;IACpD,IAAI,KAAK,IAAI,KAAK,GAAG,IAAI,CAAwB;IACjD,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IACtC,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM;IACjC,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE;IAC/B,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAsC;IACnE,IAAI,OAAO,IAAI,MAAM,EAAE,CAAoC;IAErD,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUhD,OAAO,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IACzB,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAC3B,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAC1B,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAC5B,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAC3B,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAE5B;;OAEG;IACH,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAK7B;;OAEG;IACH,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAS/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkIzB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,aAAa;IA0BrB,OAAO,CAAC,YAAY;IAwCpB,OAAO,CAAC,gBAAgB;IA+CxB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAMtC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,IAAI;CAyGrE;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,kBAAkB,CAExF"}
@@ -30,6 +30,8 @@ export class DataFrameComponent extends BaseComponent {
30
30
  this._inlineUpload = null;
31
31
  this._showStatus = true;
32
32
  this._icon = '';
33
+ this._maxSheetSize = 100000; // ✅ Default 100k rows
34
+ this._sheetChunkSize = 10000; // ✅ Default 10k chunk
33
35
  this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
34
36
  this._showStatus = options.showStatus ?? true;
35
37
  this._icon = options.icon ?? '';
@@ -41,6 +43,8 @@ export class DataFrameComponent extends BaseComponent {
41
43
  paginated: options.paginated ?? true,
42
44
  rowsPerPage: options.rowsPerPage ?? 25
43
45
  };
46
+ this._maxSheetSize = options.maxSheetSize ?? 100000;
47
+ this._sheetChunkSize = options.sheetChunkSize ?? 10000;
44
48
  }
45
49
  getTriggerEvents() { return TRIGGER_EVENTS; }
46
50
  getCallbackEvents() { return CALLBACK_EVENTS; }
@@ -86,25 +90,29 @@ export class DataFrameComponent extends BaseComponent {
86
90
  this.state.loading = true;
87
91
  this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
88
92
  try {
89
- // ✅ Check if multi-sheet Excel
90
93
  const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
91
94
  file.name.toLowerCase().endsWith('.xls');
92
95
  if (isExcel) {
93
- const sheets = await this._driver.streamFileMultiSheet(file);
96
+ // Pass chunking options for large files
97
+ const sheets = await this._driver.streamFileMultiSheet(file, {
98
+ maxSheetSize: this._maxSheetSize,
99
+ sheetChunkSize: this._sheetChunkSize,
100
+ onProgress: (loaded, total) => {
101
+ const pct = total ? Math.round((loaded / total) * 100) : 0;
102
+ this._updateStatus(`⏳ Parsing ${file.name}... ${pct}%`, 'loading');
103
+ }
104
+ });
94
105
  const sheetNames = Object.keys(sheets);
95
- // Store first sheet to IndexedDB
96
106
  await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
97
107
  if (sheetNames.length > 1) {
98
- // ✅ Multi-sheet: render tabs
99
108
  this._renderMultiSheet(sheets, file.name);
100
109
  }
101
110
  else {
102
- // Single sheet: render normally
103
111
  this._setDataFrame(sheets[sheetNames[0]], file.name);
104
112
  }
105
113
  }
106
114
  else {
107
- // CSV/TSV: single sheet
115
+ // CSV/TSV: use existing streaming
108
116
  const df = await this._driver.streamFile(file);
109
117
  await this._driver.store(file.name, df, { source: file.name });
110
118
  this._setDataFrame(df, file.name);
@@ -208,6 +216,20 @@ export class DataFrameComponent extends BaseComponent {
208
216
  filterable(v) { this._tableOptions.filterable = v; return this; }
209
217
  paginated(v) { this._tableOptions.paginated = v; return this; }
210
218
  rowsPerPage(v) { this._tableOptions.rowsPerPage = v; return this; }
219
+ /**
220
+ * ✅ NEW: Set max rows per sheet (prevents memory issues with huge Excel files)
221
+ */
222
+ maxSheetSize(v) {
223
+ this._maxSheetSize = v;
224
+ return this;
225
+ }
226
+ /**
227
+ * ✅ NEW: Set chunk size for processing large sheets
228
+ */
229
+ sheetChunkSize(v) {
230
+ this._sheetChunkSize = v;
231
+ return this;
232
+ }
211
233
  /* ═══════════════════════════════════════════════════
212
234
  * MULTI-SHEET RENDERING
213
235
  * ═══════════════════════════════════════════════════ */
@@ -460,25 +482,27 @@ export class DataFrameComponent extends BaseComponent {
460
482
  this.state.loading = true;
461
483
  this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
462
484
  try {
463
- // ✅ Check if multi-sheet Excel
464
485
  const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
465
486
  file.name.toLowerCase().endsWith('.xls');
466
487
  if (isExcel) {
467
- const sheets = await this._driver.streamFileMultiSheet(file);
488
+ const sheets = await this._driver.streamFileMultiSheet(file, {
489
+ maxSheetSize: this._maxSheetSize,
490
+ sheetChunkSize: this._sheetChunkSize,
491
+ onProgress: (loaded, total) => {
492
+ const pct = total ? Math.round((loaded / total) * 100) : 0;
493
+ this._updateStatus(`⏳ Parsing ${file.name}... ${pct}%`, 'loading');
494
+ }
495
+ });
468
496
  const sheetNames = Object.keys(sheets);
469
- // Store first sheet to IndexedDB
470
497
  await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
471
498
  if (sheetNames.length > 1) {
472
- // ✅ Multi-sheet: render tabs
473
499
  this._renderMultiSheet(sheets, file.name);
474
500
  }
475
501
  else {
476
- // Single sheet: render normally
477
502
  this._setDataFrame(sheets[sheetNames[0]], file.name);
478
503
  }
479
504
  }
480
505
  else {
481
- // CSV/TSV: single sheet
482
506
  const df = await this._driver.streamFile(file);
483
507
  await this._driver.store(file.name, df, { source: file.name });
484
508
  this._setDataFrame(df, file.name);
@@ -20,6 +20,8 @@ export interface DataFrameOptions {
20
20
  rowsPerPage?: number;
21
21
  showStatus?: boolean;
22
22
  icon?: string;
23
+ maxSheetSize?: number; // ✅ NEW: Max rows per sheet (default: 100k)
24
+ sheetChunkSize?: number; // ✅ NEW: Chunk size for large sheets (default: 10k)
23
25
  style?: string;
24
26
  class?: string;
25
27
  }
@@ -51,6 +53,8 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
51
53
  private _inlineUpload: { label: string; accept: string; icon: string } | null = null;
52
54
  private _showStatus: boolean = true;
53
55
  private _icon: string = '';
56
+ private _maxSheetSize: number = 100000; // ✅ Default 100k rows
57
+ private _sheetChunkSize: number = 10000; // ✅ Default 10k chunk
54
58
 
55
59
  constructor(id: string, options: DataFrameOptions = {}) {
56
60
  super(id, {
@@ -81,6 +85,8 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
81
85
  paginated: options.paginated ?? true,
82
86
  rowsPerPage: options.rowsPerPage ?? 25
83
87
  };
88
+ this._maxSheetSize = options.maxSheetSize ?? 100000;
89
+ this._sheetChunkSize = options.sheetChunkSize ?? 10000;
84
90
  }
85
91
 
86
92
  protected getTriggerEvents(): readonly string[] { return TRIGGER_EVENTS; }
@@ -124,26 +130,31 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
124
130
  this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
125
131
 
126
132
  try {
127
- // ✅ Check if multi-sheet Excel
128
133
  const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
129
134
  file.name.toLowerCase().endsWith('.xls');
130
135
 
131
136
  if (isExcel) {
132
- const sheets = await this._driver.streamFileMultiSheet(file);
137
+ // Pass chunking options for large files
138
+ const sheets = await this._driver.streamFileMultiSheet(file, {
139
+ maxSheetSize: this._maxSheetSize,
140
+ sheetChunkSize: this._sheetChunkSize,
141
+ onProgress: (loaded, total) => {
142
+ const pct = total ? Math.round((loaded / total) * 100) : 0;
143
+ this._updateStatus(`⏳ Parsing ${file.name}... ${pct}%`, 'loading');
144
+ }
145
+ });
146
+
133
147
  const sheetNames = Object.keys(sheets);
134
148
 
135
- // Store first sheet to IndexedDB
136
149
  await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
137
150
 
138
151
  if (sheetNames.length > 1) {
139
- // ✅ Multi-sheet: render tabs
140
152
  this._renderMultiSheet(sheets, file.name);
141
153
  } else {
142
- // Single sheet: render normally
143
154
  this._setDataFrame(sheets[sheetNames[0]], file.name);
144
155
  }
145
156
  } else {
146
- // CSV/TSV: single sheet
157
+ // CSV/TSV: use existing streaming
147
158
  const df = await this._driver.streamFile(file);
148
159
  await this._driver.store(file.name, df, { source: file.name });
149
160
  this._setDataFrame(df, file.name);
@@ -257,6 +268,22 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
257
268
  paginated(v: boolean): this { this._tableOptions.paginated = v; return this; }
258
269
  rowsPerPage(v: number): this { this._tableOptions.rowsPerPage = v; return this; }
259
270
 
271
+ /**
272
+ * ✅ NEW: Set max rows per sheet (prevents memory issues with huge Excel files)
273
+ */
274
+ maxSheetSize(v: number): this {
275
+ this._maxSheetSize = v;
276
+ return this;
277
+ }
278
+
279
+ /**
280
+ * ✅ NEW: Set chunk size for processing large sheets
281
+ */
282
+ sheetChunkSize(v: number): this {
283
+ this._sheetChunkSize = v;
284
+ return this;
285
+ }
286
+
260
287
  /* ═══════════════════════════════════════════════════
261
288
  * MULTI-SHEET RENDERING
262
289
  * ═══════════════════════════════════════════════════ */
@@ -567,26 +594,29 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
567
594
  this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
568
595
 
569
596
  try {
570
- // ✅ Check if multi-sheet Excel
571
597
  const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
572
598
  file.name.toLowerCase().endsWith('.xls');
573
599
 
574
600
  if (isExcel) {
575
- const sheets = await this._driver.streamFileMultiSheet(file);
601
+ const sheets = await this._driver.streamFileMultiSheet(file, {
602
+ maxSheetSize: this._maxSheetSize,
603
+ sheetChunkSize: this._sheetChunkSize,
604
+ onProgress: (loaded, total) => {
605
+ const pct = total ? Math.round((loaded / total) * 100) : 0;
606
+ this._updateStatus(`⏳ Parsing ${file.name}... ${pct}%`, 'loading');
607
+ }
608
+ });
609
+
576
610
  const sheetNames = Object.keys(sheets);
577
611
 
578
- // Store first sheet to IndexedDB
579
612
  await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
580
613
 
581
614
  if (sheetNames.length > 1) {
582
- // ✅ Multi-sheet: render tabs
583
615
  this._renderMultiSheet(sheets, file.name);
584
616
  } else {
585
- // Single sheet: render normally
586
617
  this._setDataFrame(sheets[sheetNames[0]], file.name);
587
618
  }
588
619
  } else {
589
- // CSV/TSV: single sheet
590
620
  const df = await this._driver.streamFile(file);
591
621
  await this._driver.store(file.name, df, { source: file.name });
592
622
  this._setDataFrame(df, file.name);
@@ -18,6 +18,8 @@ export interface ParseOptions {
18
18
  skipRows?: number;
19
19
  columns?: string[];
20
20
  sheet?: string | number;
21
+ maxSheetSize?: number;
22
+ sheetChunkSize?: number;
21
23
  }
22
24
  export declare class TabularDriver {
23
25
  private _dbName;
@@ -74,11 +76,10 @@ export declare class TabularDriver {
74
76
  */
75
77
  fetch(url: string, options?: ParseOptions): Promise<DataFrame>;
76
78
  /**
77
- * ✅ NEW: Stream Excel file and return all sheets
78
- * @param file - Excel file (.xlsx or .xls)
79
- * @returns Record<sheetName, DataFrame>
79
+ * ✅ OPTIMIZED: Stream Excel file with chunked row processing
80
+ * Handles large files by processing rows in batches
80
81
  */
81
- streamFileMultiSheet(file: File): Promise<Record<string, DataFrame>>;
82
+ streamFileMultiSheet(file: File, options?: ParseOptions): Promise<Record<string, DataFrame>>;
82
83
  private _splitLines;
83
84
  private _parseLine;
84
85
  private _autoType;
@@ -1 +1 @@
1
- {"version":3,"file":"TabularDriver.d.ts","sourceRoot":"","sources":["TabularDriver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TabularDriver.d.ts","sourceRoot":"","sources":["TabularDriver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,aAAa;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,GAAG,CAA4B;gBAE3B,MAAM,GAAE,MAAsB,EAAE,SAAS,GAAE,MAAiB;IAKlE,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC;IA4BlC;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,SAAS;IA6C7D;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,SAAS,CAAC;IAoG5E;;OAEG;YACW,UAAU;IAuExB;;OAEG;IACG,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAiBlD;;OAEG;IACG,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAuBzF;;OAEG;IACG,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAwBjD;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA4BzD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAqBlH;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB5B;;OAEG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,SAAS,CAAC;IA0ExE;;;OAGG;IACG,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAyGtG,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,UAAU;IAmClB,OAAO,CAAC,SAAS;IAYjB,KAAK,IAAI,IAAI;CAMhB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,aAAa,CAEhF"}
@@ -419,12 +419,11 @@ export class TabularDriver {
419
419
  return df;
420
420
  }
421
421
  /**
422
- * ✅ NEW: Stream Excel file and return all sheets
423
- * @param file - Excel file (.xlsx or .xls)
424
- * @returns Record<sheetName, DataFrame>
422
+ * ✅ OPTIMIZED: Stream Excel file with chunked row processing
423
+ * Handles large files by processing rows in batches
425
424
  */
426
- async streamFileMultiSheet(file) {
427
- // FIX: Dynamic import XLSX here
425
+ async streamFileMultiSheet(file, options = {}) {
426
+ const { maxSheetSize = 100000, sheetChunkSize = 10000, onProgress } = options;
428
427
  let XLSX;
429
428
  try {
430
429
  XLSX = await import('xlsx');
@@ -432,16 +431,79 @@ export class TabularDriver {
432
431
  catch {
433
432
  throw new Error('XLSX support requires the "xlsx" package. Install it with: npm install xlsx');
434
433
  }
434
+ if (onProgress)
435
+ onProgress(0, file.size);
435
436
  const buffer = await file.arrayBuffer();
436
- const workbook = XLSX.read(buffer, { type: 'array' });
437
+ if (onProgress)
438
+ onProgress(file.size * 0.3, file.size);
439
+ const workbook = XLSX.read(buffer, {
440
+ type: 'array',
441
+ sheetRows: maxSheetSize, // ✅ Limit rows read per sheet
442
+ dense: false // ✅ Use sparse format for memory efficiency
443
+ });
437
444
  const sheets = {};
438
- workbook.SheetNames.forEach((sheetName) => {
445
+ const totalSheets = workbook.SheetNames.length;
446
+ let processedSheets = 0;
447
+ for (const sheetName of workbook.SheetNames) {
439
448
  const worksheet = workbook.Sheets[sheetName];
440
- const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: null });
441
- if (jsonData.length > 0) {
442
- sheets[sheetName] = new DataFrame(jsonData);
449
+ // Get sheet range to determine size
450
+ const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
451
+ const totalRows = range.e.r - range.s.r + 1;
452
+ if (totalRows === 0) {
453
+ processedSheets++;
454
+ continue;
443
455
  }
444
- });
456
+ // ✅ Process in chunks if sheet is large
457
+ if (totalRows > sheetChunkSize) {
458
+ const rows = [];
459
+ let headers = [];
460
+ for (let startRow = 0; startRow < totalRows; startRow += sheetChunkSize) {
461
+ const endRow = Math.min(startRow + sheetChunkSize, totalRows);
462
+ // ✅ Read chunk with limited row range
463
+ const chunkData = XLSX.utils.sheet_to_json(worksheet, {
464
+ range: startRow,
465
+ header: startRow === 0 ? undefined : headers,
466
+ defval: null,
467
+ raw: false, // ✅ Convert dates/numbers to strings to reduce memory
468
+ blankrows: false
469
+ });
470
+ if (startRow === 0 && chunkData.length > 0) {
471
+ headers = Object.keys(chunkData[0]);
472
+ }
473
+ rows.push(...chunkData);
474
+ if (onProgress) {
475
+ const progress = 0.3 + (0.6 * (processedSheets + (startRow / totalRows)) / totalSheets);
476
+ onProgress(file.size * progress, file.size);
477
+ }
478
+ // ✅ Stop if we hit max rows
479
+ if (rows.length >= maxSheetSize) {
480
+ console.warn(`⚠️ Sheet "${sheetName}" truncated to ${maxSheetSize} rows`);
481
+ break;
482
+ }
483
+ }
484
+ if (rows.length > 0) {
485
+ sheets[sheetName] = new DataFrame(rows);
486
+ }
487
+ }
488
+ else {
489
+ // ✅ Small sheet: process normally
490
+ const jsonData = XLSX.utils.sheet_to_json(worksheet, {
491
+ defval: null,
492
+ raw: false,
493
+ blankrows: false
494
+ });
495
+ if (jsonData.length > 0) {
496
+ sheets[sheetName] = new DataFrame(jsonData);
497
+ }
498
+ }
499
+ processedSheets++;
500
+ if (onProgress) {
501
+ const progress = 0.3 + (0.6 * (processedSheets / totalSheets));
502
+ onProgress(file.size * progress, file.size);
503
+ }
504
+ }
505
+ if (onProgress)
506
+ onProgress(file.size, file.size);
445
507
  return sheets;
446
508
  }
447
509
  /* ═══════════════════════════════════════════════════
@@ -19,7 +19,9 @@ export interface ParseOptions {
19
19
  maxRows?: number;
20
20
  skipRows?: number;
21
21
  columns?: string[];
22
- sheet?: string | number; // ✅ For XLSX: sheet name or index
22
+ sheet?: string | number;
23
+ maxSheetSize?: number; // ✅ NEW: Max rows per sheet to prevent memory issues
24
+ sheetChunkSize?: number; // ✅ NEW: Process Excel in chunks
23
25
  }
24
26
 
25
27
  export class TabularDriver {
@@ -523,12 +525,12 @@ export class TabularDriver {
523
525
  }
524
526
 
525
527
  /**
526
- * ✅ NEW: Stream Excel file and return all sheets
527
- * @param file - Excel file (.xlsx or .xls)
528
- * @returns Record<sheetName, DataFrame>
528
+ * ✅ OPTIMIZED: Stream Excel file with chunked row processing
529
+ * Handles large files by processing rows in batches
529
530
  */
530
- async streamFileMultiSheet(file: File): Promise<Record<string, DataFrame>> {
531
- // FIX: Dynamic import XLSX here
531
+ async streamFileMultiSheet(file: File, options: ParseOptions = {}): Promise<Record<string, DataFrame>> {
532
+ const { maxSheetSize = 100000, sheetChunkSize = 10000, onProgress } = options;
533
+
532
534
  let XLSX: any;
533
535
  try {
534
536
  XLSX = await import('xlsx');
@@ -536,19 +538,93 @@ export class TabularDriver {
536
538
  throw new Error('XLSX support requires the "xlsx" package. Install it with: npm install xlsx');
537
539
  }
538
540
 
541
+ if (onProgress) onProgress(0, file.size);
542
+
539
543
  const buffer = await file.arrayBuffer();
540
- const workbook = XLSX.read(buffer, { type: 'array' });
544
+
545
+ if (onProgress) onProgress(file.size * 0.3, file.size);
546
+
547
+ const workbook = XLSX.read(buffer, {
548
+ type: 'array',
549
+ sheetRows: maxSheetSize, // ✅ Limit rows read per sheet
550
+ dense: false // ✅ Use sparse format for memory efficiency
551
+ });
541
552
 
542
553
  const sheets: Record<string, DataFrame> = {};
554
+ const totalSheets = workbook.SheetNames.length;
555
+ let processedSheets = 0;
543
556
 
544
- workbook.SheetNames.forEach((sheetName: string) => {
557
+ for (const sheetName of workbook.SheetNames) {
545
558
  const worksheet = workbook.Sheets[sheetName];
546
- const jsonData: Record<string, any>[] = XLSX.utils.sheet_to_json(worksheet, { defval: null });
547
559
 
548
- if (jsonData.length > 0) {
549
- sheets[sheetName] = new DataFrame(jsonData);
560
+ // Get sheet range to determine size
561
+ const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
562
+ const totalRows = range.e.r - range.s.r + 1;
563
+
564
+ if (totalRows === 0) {
565
+ processedSheets++;
566
+ continue;
550
567
  }
551
- });
568
+
569
+ // ✅ Process in chunks if sheet is large
570
+ if (totalRows > sheetChunkSize) {
571
+ const rows: Record<string, any>[] = [];
572
+ let headers: string[] = [];
573
+
574
+ for (let startRow = 0; startRow < totalRows; startRow += sheetChunkSize) {
575
+ const endRow = Math.min(startRow + sheetChunkSize, totalRows);
576
+
577
+ // ✅ Read chunk with limited row range
578
+ const chunkData: Record<string, any>[] = XLSX.utils.sheet_to_json(worksheet, {
579
+ range: startRow,
580
+ header: startRow === 0 ? undefined : headers,
581
+ defval: null,
582
+ raw: false, // ✅ Convert dates/numbers to strings to reduce memory
583
+ blankrows: false
584
+ });
585
+
586
+ if (startRow === 0 && chunkData.length > 0) {
587
+ headers = Object.keys(chunkData[0]);
588
+ }
589
+
590
+ rows.push(...chunkData);
591
+
592
+ if (onProgress) {
593
+ const progress = 0.3 + (0.6 * (processedSheets + (startRow / totalRows)) / totalSheets);
594
+ onProgress(file.size * progress, file.size);
595
+ }
596
+
597
+ // ✅ Stop if we hit max rows
598
+ if (rows.length >= maxSheetSize) {
599
+ console.warn(`⚠️ Sheet "${sheetName}" truncated to ${maxSheetSize} rows`);
600
+ break;
601
+ }
602
+ }
603
+
604
+ if (rows.length > 0) {
605
+ sheets[sheetName] = new DataFrame(rows);
606
+ }
607
+ } else {
608
+ // ✅ Small sheet: process normally
609
+ const jsonData: Record<string, any>[] = XLSX.utils.sheet_to_json(worksheet, {
610
+ defval: null,
611
+ raw: false,
612
+ blankrows: false
613
+ });
614
+
615
+ if (jsonData.length > 0) {
616
+ sheets[sheetName] = new DataFrame(jsonData);
617
+ }
618
+ }
619
+
620
+ processedSheets++;
621
+ if (onProgress) {
622
+ const progress = 0.3 + (0.6 * (processedSheets / totalSheets));
623
+ onProgress(file.size * progress, file.size);
624
+ }
625
+ }
626
+
627
+ if (onProgress) onProgress(file.size, file.size);
552
628
 
553
629
  return sheets;
554
630
  }
@@ -1,11 +1,10 @@
1
1
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
2
2
 
3
3
  /* ═══════════════════════════════════════════════════════════════════
4
- * DESIGN TOKENS (Shadcn Variables)
4
+ * DESIGN TOKENS
5
5
  * ═══════════════════════════════════════════════════════════════════ */
6
6
 
7
7
  :root {
8
- /* Base colors */
9
8
  --background: 0 0% 100%;
10
9
  --foreground: 0 0% 3.9%;
11
10
  --muted: 0 0% 96.1%;
@@ -13,27 +12,19 @@
13
12
  --border: 0 0% 89.8%;
14
13
  --input: 0 0% 89.8%;
15
14
  --ring: 0 0% 3.9%;
16
-
17
- /* Accent colors */
18
15
  --primary: 0 0% 9%;
19
16
  --primary-foreground: 0 0% 98%;
20
17
  --secondary: 0 0% 96.1%;
21
18
  --secondary-foreground: 0 0% 9%;
22
19
  --accent: 0 0% 96.1%;
23
20
  --accent-foreground: 0 0% 9%;
24
-
25
- /* Semantic colors */
26
21
  --destructive: 0 84.2% 60.2%;
27
22
  --destructive-foreground: 0 0% 98%;
28
23
  --success: 142 71% 45%;
29
24
  --success-foreground: 0 0% 98%;
30
25
  --warning: 48 96% 53%;
31
26
  --warning-foreground: 0 0% 9%;
32
-
33
- /* Sizes */
34
- --radius: 0.375rem;
35
-
36
- /* Font */
27
+ --radius: 0.5rem;
37
28
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
38
29
  }
39
30
 
@@ -54,13 +45,12 @@
54
45
  }
55
46
 
56
47
  /* ═══════════════════════════════════════════════════════════════════
57
- * BASE INPUT STYLING (Shared across all inputs)
48
+ * BASE INPUTS
58
49
  * ═══════════════════════════════════════════════════════════════════ */
59
50
 
60
51
  .jux-input-element,
61
52
  .jux-table-filter,
62
53
  .jux-dataframe-filter-input {
63
- display: flex;
64
54
  height: 2.5rem;
65
55
  width: 100%;
66
56
  border-radius: var(--radius);
@@ -70,8 +60,7 @@
70
60
  font-size: 0.875rem;
71
61
  color: hsl(var(--foreground));
72
62
  outline: none !important;
73
- transition: all 0.15s ease;
74
- box-sizing: border-box;
63
+ transition: border-color 0.2s, box-shadow 0.2s;
75
64
  }
76
65
 
77
66
  .jux-input-element::placeholder,
@@ -83,25 +72,18 @@
83
72
  .jux-input-element:focus,
84
73
  .jux-table-filter:focus,
85
74
  .jux-dataframe-filter-input:focus {
86
-
87
- box-shadow: 0 0 0 1px hsl(var(--input)) !important;
88
- }
89
-
90
- .jux-input-element:disabled {
91
- cursor: not-allowed;
92
- opacity: 0.5;
75
+ border-color: hsl(var(--ring));
76
+ box-shadow: 0 0 0 3px hsl(var(--ring) / 0.1);
93
77
  }
94
78
 
95
79
  /* ═══════════════════════════════════════════════════════════════════
96
- * BUTTON BASE (Single source of truth)
80
+ * BUTTONS
97
81
  * ═══════════════════════════════════════════════════════════════════ */
98
82
 
99
83
  .jux-button,
100
84
  .jux-modal-action,
101
85
  .jux-table-pagination button,
102
- .jux-fileupload-button,
103
- .jux-dropdown-trigger,
104
- .jux-dmenu-trigger {
86
+ .jux-fileupload-button {
105
87
  display: inline-flex;
106
88
  align-items: center;
107
89
  justify-content: center;
@@ -112,37 +94,24 @@
112
94
  height: 2.5rem;
113
95
  padding: 0 1rem;
114
96
  cursor: pointer;
115
- transition: all 0.15s ease;
116
- border: 1px solid transparent;
97
+ transition: all 0.2s;
98
+ border: none;
117
99
  background: hsl(var(--primary));
118
100
  color: hsl(var(--primary-foreground));
119
101
  }
120
102
 
121
103
  .jux-button:hover,
122
- .jux-modal-action:hover,
123
- .jux-table-pagination button:hover:not(:disabled),
124
- .jux-fileupload-button:hover,
125
- .jux-dropdown-trigger:hover,
126
- .jux-dmenu-trigger:hover {
104
+ .jux-table-pagination button:hover:not(:disabled) {
127
105
  opacity: 0.9;
128
106
  }
129
107
 
130
- .jux-button:focus-visible,
131
- .jux-modal-action:focus-visible,
132
- .jux-fileupload-button:focus-visible {
133
- outline: 2px solid hsl(var(--ring));
134
- outline-offset: 2px;
135
- }
136
-
137
108
  .jux-button:disabled,
138
109
  .jux-table-pagination button:disabled {
139
110
  pointer-events: none;
140
111
  opacity: 0.5;
141
112
  }
142
113
 
143
- /* Button variants */
144
114
  .jux-button-outline,
145
- .jux-modal-action-secondary,
146
115
  .jux-fileupload-button {
147
116
  border: 1px solid hsl(var(--input));
148
117
  background: hsl(var(--background));
@@ -150,101 +119,75 @@
150
119
  }
151
120
 
152
121
  .jux-button-outline:hover,
153
- .jux-modal-action-secondary:hover,
154
122
  .jux-fileupload-button:hover {
155
123
  background: hsl(var(--accent));
156
124
  }
157
125
 
158
- .jux-button-ghost,
159
- .jux-modal-action-ghost {
126
+ .jux-button-ghost {
160
127
  background: transparent;
161
128
  color: hsl(var(--foreground));
162
- border: none;
163
129
  }
164
130
 
165
- .jux-button-ghost:hover,
166
- .jux-modal-action-ghost:hover {
131
+ .jux-button-ghost:hover {
167
132
  background: hsl(var(--accent));
168
133
  }
169
134
 
170
- .jux-button-destructive,
171
- .jux-modal-action-destructive {
172
- background: hsl(var(--destructive));
173
- color: hsl(var(--destructive-foreground));
174
- }
175
-
176
- .jux-button-destructive:hover,
177
- .jux-modal-action-destructive:hover {
178
- opacity: 0.9;
179
- }
180
-
181
- /* Button sizes */
182
- .jux-button-sm { height: 2.25rem; padding: 0 0.875rem; font-size: 0.8125rem; }
183
- .jux-button-lg { height: 2.75rem; padding: 0 1.25rem; font-size: 0.9375rem; }
184
- .jux-button-xl { height: 3rem; padding: 0 1.5rem; font-size: 1rem; font-weight: 600; }
185
-
186
135
  /* ═══════════════════════════════════════════════════════════════════
187
- * TYPOGRAPHY (DRY headings)
136
+ * TYPOGRAPHY
188
137
  * ═══════════════════════════════════════════════════════════════════ */
189
138
 
190
- h1, h2, h3, h4, h5, h6,
191
- .jux-heading-1, .jux-heading-2, .jux-heading-3, .jux-heading-4, .jux-heading-5, .jux-heading-6 {
192
- scroll-margin: 5rem;
139
+ h1, h2, h3, h4, h5, h6 {
193
140
  font-weight: 600;
194
141
  letter-spacing: -0.025em;
195
142
  color: hsl(var(--foreground));
196
143
  }
197
144
 
198
- h1, .jux-heading-1 { font-size: 2.25rem; font-weight: 800; }
199
- h2, .jux-heading-2 { font-size: 1.875rem; border-bottom: 1px solid hsl(var(--border)); padding-bottom: 0.5rem; }
200
- h3, .jux-heading-3 { font-size: 1.5rem; }
201
- h4, .jux-heading-4 { font-size: 1.25rem; }
202
- h5, .jux-heading-5 { font-size: 1.125rem; }
203
- h6, .jux-heading-6 { font-size: 1rem; }
145
+ h1 { font-size: 2.25rem; font-weight: 800; }
146
+ h2 { font-size: 1.875rem; padding-bottom: 0.5rem; border-bottom: 1px solid hsl(var(--border)); }
147
+ h3 { font-size: 1.5rem; }
148
+ h4 { font-size: 1.25rem; }
204
149
 
205
- p, .jux-paragraph { line-height: 1.75; }
206
- p:not(:first-child) { margin-top: 1.5rem; }
207
-
208
- .jux-muted { font-size: 0.875rem; color: hsl(var(--muted-foreground)); }
150
+ p { line-height: 1.75; color: hsl(var(--foreground)); }
209
151
 
210
152
  /* ═══════════════════════════════════════════════════════════════════
211
- * DATAFRAME COMPONENT
153
+ * DATAFRAME
212
154
  * ═══════════════════════════════════════════════════════════════════ */
213
155
 
214
156
  .jux-dataframe {
215
157
  display: flex;
216
158
  flex-direction: column;
217
- gap: 0.75rem;
159
+ gap: 1rem;
218
160
  width: 100%;
219
161
  }
220
162
 
221
163
  .jux-dataframe-status {
222
164
  display: flex;
223
165
  align-items: center;
224
- padding: 0.5rem 0.75rem;
225
- font-size: 0.8125rem;
166
+ gap: 0.5rem;
167
+ padding: 0.75rem 1rem;
168
+ font-size: 0.875rem;
226
169
  font-weight: 500;
227
170
  border-radius: var(--radius);
228
171
  border: 1px solid hsl(var(--border));
229
- background: hsl(var(--muted));
172
+ background: hsl(var(--muted) / 0.5);
230
173
  color: hsl(var(--muted-foreground));
231
174
  }
232
175
 
233
176
  .jux-dataframe-status-success {
234
177
  background: hsl(var(--success) / 0.1);
235
- border-color: hsl(var(--success) / 0.3);
178
+ border-color: hsl(var(--success) / 0.2);
236
179
  color: hsl(var(--success));
237
180
  }
238
181
 
239
182
  .jux-dataframe-status-loading {
240
183
  background: hsl(var(--warning) / 0.1);
241
- border-color: hsl(var(--warning) / 0.3);
184
+ border-color: hsl(var(--warning) / 0.2);
242
185
  color: hsl(var(--warning));
243
186
  }
244
187
 
245
188
  .jux-dataframe-status-error {
246
189
  background: hsl(var(--destructive) / 0.1);
247
- border-color: hsl(var(--destructive) / 0.3);
190
+ border-color: hsl(var(--destructive) / 0.2);
248
191
  color: hsl(var(--destructive));
249
192
  }
250
193
 
@@ -252,7 +195,6 @@ p:not(:first-child) { margin-top: 1.5rem; }
252
195
  position: relative;
253
196
  display: flex;
254
197
  align-items: center;
255
- width: 100%;
256
198
  margin-bottom: 0.75rem;
257
199
  }
258
200
 
@@ -263,316 +205,220 @@ p:not(:first-child) { margin-top: 1.5rem; }
263
205
  transform: translateY(-50%);
264
206
  color: hsl(var(--muted-foreground));
265
207
  pointer-events: none;
266
- display: flex;
267
- align-items: center;
268
208
  z-index: 1;
269
209
  }
270
210
 
271
211
  .jux-dataframe-filter-input {
272
- padding-left: 2.25rem !important;
212
+ padding-left: 2.5rem !important;
273
213
  }
274
214
 
275
215
  /* ═══════════════════════════════════════════════════════════════════
276
- * FILE UPLOAD
216
+ * TABS
277
217
  * ═══════════════════════════════════════════════════════════════════ */
278
218
 
279
- .jux-fileupload { gap: 0.5rem; }
219
+ .jux-tabs {
220
+ width: 100%;
221
+ }
280
222
 
281
- .jux-fileupload-item {
282
- display: flex;
223
+ .jux-tabs-list {
224
+ display: inline-flex;
225
+ height: 2.5rem;
283
226
  align-items: center;
284
- gap: 0.5rem;
285
- padding: 0.375rem 0.75rem;
286
- background: hsl(var(--muted));
287
- border: 1px solid hsl(var(--border));
227
+ justify-content: flex-start;
228
+ gap: 0.25rem;
288
229
  border-radius: var(--radius);
289
- font-size: 0.8125rem;
290
- color: hsl(var(--foreground));
230
+ background: hsl(var(--muted) / 0.3);
231
+ padding: 0.25rem;
232
+ border: 1px solid hsl(var(--border));
291
233
  }
292
234
 
293
- .jux-fileupload-filename {
294
- flex: 1;
295
- font-weight: 500;
296
- overflow: hidden;
297
- text-overflow: ellipsis;
235
+ .jux-tabs-button {
236
+ display: inline-flex;
237
+ align-items: center;
238
+ justify-content: center;
298
239
  white-space: nowrap;
240
+ border-radius: calc(var(--radius) - 0.125rem);
241
+ padding: 0.5rem 1rem;
242
+ font-size: 0.875rem;
243
+ font-weight: 500;
244
+ color: hsl(var(--muted-foreground));
245
+ cursor: pointer;
246
+ border: none;
247
+ background: transparent;
248
+ transition: all 0.2s;
299
249
  }
300
250
 
301
- .jux-fileupload-filesize {
302
- color: hsl(var(--muted-foreground));
303
- font-size: 0.75rem;
251
+ .jux-tabs-button:hover {
252
+ background: hsl(var(--muted) / 0.5);
253
+ color: hsl(var(--foreground));
304
254
  }
305
255
 
306
- .jux-fileupload-remove {
307
- background: transparent;
256
+ .jux-tabs-button-active {
257
+ background: hsl(var(--background));
258
+ color: hsl(var(--foreground));
259
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
260
+ font-weight: 600;
261
+ }
262
+
263
+ .jux-tabs-panel {
264
+ margin-top: 1rem;
265
+ border-radius: var(--radius);
266
+ border: 1px solid hsl(var(--border));
267
+ background: hsl(var(--background));
268
+ padding: 0;
269
+ overflow: hidden;
270
+ }
271
+
272
+ /* ✅ Remove inner padding/border from tables inside tabs */
273
+ .jux-tabs-panel .jux-table-wrapper {
308
274
  border: none;
309
- color: hsl(var(--muted-foreground));
310
- cursor: pointer;
311
- padding: 0 0.25rem;
312
- border-radius: 0.25rem;
313
- transition: all 0.15s;
275
+ border-radius: 0;
314
276
  }
315
277
 
316
- .jux-fileupload-remove:hover {
317
- color: hsl(var(--destructive));
318
- background: hsl(var(--destructive) / 0.1);
278
+ .jux-tabs-panel .jux-dataframe-filter {
279
+ padding: 1rem 1rem 0;
280
+ margin-bottom: 0.75rem;
319
281
  }
320
282
 
321
283
  /* ═══════════════════════════════════════════════════════════════════
322
284
  * TABLE
323
285
  * ═══════════════════════════════════════════════════════════════════ */
324
286
 
325
- .jux-table-wrapper { width: 100%; overflow-x: auto; }
287
+ .jux-table-wrapper {
288
+ width: 100%;
289
+ overflow-x: auto;
290
+ border-radius: var(--radius);
291
+ border: 1px solid hsl(var(--border));
292
+ background: hsl(var(--background));
293
+ }
326
294
 
327
295
  .jux-table {
328
296
  width: 100%;
329
297
  border-collapse: collapse;
330
298
  }
331
299
 
332
- .jux-table thead tr { border-top: 1px solid hsl(var(--border)); }
333
- .jux-table tbody tr { border-top: 1px solid hsl(var(--border)); }
334
- .jux-table-striped tbody tr:nth-child(even) { background: hsl(var(--muted)); }
300
+ .jux-table thead tr {
301
+ border-bottom: 1px solid hsl(var(--border));
302
+ background: hsl(var(--muted) / 0.3);
303
+ }
304
+
305
+ .jux-table tbody tr {
306
+ border-bottom: 1px solid hsl(var(--border));
307
+ transition: background-color 0.15s;
308
+ }
309
+
310
+ .jux-table tbody tr:last-child {
311
+ border-bottom: none;
312
+ }
335
313
 
336
314
  .jux-table th,
337
315
  .jux-table td {
338
- border: 1px solid hsl(var(--border));
339
- padding: 0.5rem 1rem;
316
+ padding: 0.75rem 1rem;
340
317
  text-align: left;
318
+ border: none;
319
+ }
320
+
321
+ .jux-table th {
322
+ font-weight: 600;
323
+ font-size: 0.875rem;
324
+ color: hsl(var(--foreground));
341
325
  }
342
326
 
343
- .jux-table th { font-weight: 700; }
327
+ .jux-table td {
328
+ font-size: 0.875rem;
329
+ color: hsl(var(--foreground));
330
+ }
344
331
 
345
- .jux-table-hoverable tbody tr {
346
- transition: background-color 0.1s ease;
332
+ .jux-table-striped tbody tr:nth-child(even) {
333
+ background: hsl(var(--muted) / 0.2);
347
334
  }
348
335
 
349
336
  .jux-table-hoverable tbody tr:hover {
350
- background: hsl(var(--accent)) !important;
337
+ background: hsl(var(--muted) / 0.5);
351
338
  }
352
339
 
353
340
  .jux-table-pagination {
354
- margin-top: 0.75rem;
341
+ margin-top: 1rem;
342
+ padding: 0.75rem 1rem;
355
343
  display: flex;
356
- gap: 0.375rem;
344
+ gap: 0.5rem;
357
345
  justify-content: center;
358
346
  align-items: center;
359
- padding: 0.5rem 0;
347
+ border-top: 1px solid hsl(var(--border));
348
+ background: hsl(var(--muted) / 0.2);
360
349
  }
361
350
 
362
351
  .jux-table-pagination span {
363
352
  font-size: 0.875rem;
364
- color: hsl(var(--muted-foreground));
365
- padding: 0 0.75rem;
353
+ color: hsl(var(--foreground));
354
+ font-weight: 500;
366
355
  }
367
356
 
368
357
  /* ═══════════════════════════════════════════════════════════════════
369
- * MODAL
358
+ * FILE UPLOAD
370
359
  * ═══════════════════════════════════════════════════════════════════ */
371
360
 
372
- .jux-modal-overlay {
373
- position: fixed;
374
- inset: 0;
375
- z-index: 50;
376
- background: rgba(0, 0, 0, 0.8);
377
- backdrop-filter: blur(2px);
361
+ .jux-fileupload-item {
378
362
  display: flex;
379
363
  align-items: center;
380
- justify-content: center;
381
- }
382
-
383
- .jux-modal {
384
- position: relative;
385
- background: hsl(var(--background));
386
- border: 1px solid hsl(var(--border));
387
- border-radius: 0.5rem;
388
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
389
- width: 100%;
390
- max-width: 32rem;
391
- z-index: 51;
392
- display: flex;
393
- flex-direction: column;
394
- gap: 1rem;
395
- padding: 1.5rem;
396
- }
397
-
398
- .jux-modal-header {
399
- display: flex;
400
- align-items: flex-start;
401
364
  gap: 0.75rem;
365
+ padding: 0.75rem 1rem;
366
+ background: hsl(var(--muted) / 0.3);
367
+ border: 1px solid hsl(var(--border));
368
+ border-radius: var(--radius);
369
+ font-size: 0.875rem;
402
370
  }
403
371
 
404
- .jux-modal-header-title {
372
+ .jux-fileupload-filename {
405
373
  flex: 1;
406
- font-size: 1.125rem;
407
- font-weight: 600;
374
+ font-weight: 500;
408
375
  color: hsl(var(--foreground));
376
+ overflow: hidden;
377
+ text-overflow: ellipsis;
378
+ white-space: nowrap;
409
379
  }
410
380
 
411
- .jux-modal-content {
412
- font-size: 0.875rem;
381
+ .jux-fileupload-filesize {
413
382
  color: hsl(var(--muted-foreground));
414
- line-height: 1.5;
415
- }
416
-
417
- .jux-modal-footer {
418
- display: flex;
419
- justify-content: flex-end;
420
- gap: 0.5rem;
383
+ font-size: 0.8125rem;
421
384
  }
422
385
 
423
- .jux-modal-close {
424
- position: absolute;
425
- top: 1rem;
426
- right: 1rem;
386
+ .jux-fileupload-remove {
427
387
  background: transparent;
428
388
  border: none;
429
- cursor: pointer;
430
- color: hsl(var(--muted-foreground));
431
- padding: 0.25rem;
432
- }
433
-
434
- .jux-modal-close:hover {
435
- color: hsl(var(--foreground));
436
- }
437
-
438
- /* ═══════════════════════════════════════════════════════════════════
439
- * TABS
440
- * ═══════════════════════════════════════════════════════════════════ */
441
-
442
- .jux-tabs-list {
443
- display: inline-flex;
444
- height: 40px;
445
- align-items: center;
446
- justify-content: center;
447
- border-radius: var(--radius);
448
- background: hsl(var(--muted));
449
- padding: 0.25rem;
450
- }
451
-
452
- .jux-tabs-button {
453
- display: inline-flex;
454
- align-items: center;
455
- justify-content: center;
456
- white-space: nowrap;
457
- border-radius: calc(var(--radius) - 2px);
458
- padding: 0.375rem 0.75rem;
459
- font-size: 0.875rem;
460
- font-weight: 500;
461
389
  color: hsl(var(--muted-foreground));
462
390
  cursor: pointer;
463
- border: none;
464
- background: transparent;
465
- transition: all 0.15s;
466
- }
467
-
468
- .jux-tabs-button-active {
469
- background: hsl(var(--background));
470
- color: hsl(var(--foreground));
471
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
391
+ padding: 0.25rem;
392
+ border-radius: 0.25rem;
393
+ transition: all 0.2s;
472
394
  }
473
395
 
474
- .jux-tabs-panel {
475
- margin-top: 0.5rem;
476
- border-radius: var(--radius);
477
- border: 1px solid hsl(var(--border));
478
- padding: 1.5rem;
396
+ .jux-fileupload-remove:hover {
397
+ color: hsl(var(--destructive));
398
+ background: hsl(var(--destructive) / 0.1);
479
399
  }
480
400
 
481
401
  /* ═══════════════════════════════════════════════════════════════════
482
- * CARD
402
+ * UTILITIES
483
403
  * ═══════════════════════════════════════════════════════════════════ */
484
404
 
485
- .jux-card {
486
- border-radius: 0.5rem;
487
- border: 1px solid hsl(var(--border));
488
- background: hsl(var(--background));
489
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
490
- overflow: hidden;
491
- }
492
-
493
- .jux-card-header {
494
- display: flex;
495
- align-items: center;
496
- gap: 0.75rem;
497
- padding: 1.5rem;
498
- background: hsl(var(--muted) / 0.5);
499
- border-bottom: 1px solid hsl(var(--border));
500
- }
501
-
502
- .jux-card-title {
503
- flex: 1;
504
- font-size: 1.125rem;
505
- font-weight: 600;
506
- color: hsl(var(--foreground));
507
- }
508
-
509
- .jux-card-body {
510
- padding: 1.5rem;
405
+ .jux-muted {
511
406
  font-size: 0.875rem;
512
407
  color: hsl(var(--muted-foreground));
513
408
  }
514
409
 
515
- /* ═══════════════════════════════════════════════════════════════════
516
- * DROPDOWN MENU
517
- * ═══════════════════════════════════════════════════════════════════ */
518
-
519
- .jux-dmenu-wrapper { position: relative; display: inline-block; }
520
-
521
- .jux-dmenu-menu {
522
- position: absolute;
523
- top: calc(100% + 4px);
524
- left: 0;
525
- min-width: 220px;
526
- padding: 4px;
527
- border-radius: 0.5rem;
528
- border: 1px solid hsl(var(--border));
529
- background: hsl(var(--background));
530
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
531
- z-index: 100;
532
- display: none;
533
- flex-direction: column;
534
- }
535
-
536
- .jux-dmenu-item {
537
- display: flex;
538
- align-items: center;
539
- justify-content: space-between;
540
- width: 100%;
541
- padding: 6px 8px;
542
- font-size: 0.875rem;
543
- border-radius: 4px;
544
- border: none;
545
- background: transparent;
546
- color: hsl(var(--foreground));
547
- cursor: pointer;
548
- transition: background 0.1s;
549
- }
550
-
551
- .jux-dmenu-item:hover {
552
- background: hsl(var(--accent));
553
- }
554
-
555
- .jux-dmenu-sep {
556
- height: 1px;
557
- margin: 4px -4px;
558
- background: hsl(var(--border));
559
- }
560
-
561
- /* ═══════════════════════════════════════════════════════════════════
562
- * UTILITIES
563
- * ═══════════════════════════════════════════════════════════════════ */
564
-
565
- hr, .jux-hr {
410
+ hr {
566
411
  margin: 1.5rem 0;
567
412
  border: none;
568
413
  border-top: 1px solid hsl(var(--border));
569
414
  }
570
415
 
571
- code, .jux-inline-code {
416
+ code {
572
417
  border-radius: 0.25rem;
573
418
  background: hsl(var(--muted));
574
- padding: 0.15rem 0.3rem;
419
+ padding: 0.2rem 0.4rem;
575
420
  font-family: ui-monospace, monospace;
576
421
  font-size: 0.875rem;
577
422
  font-weight: 600;
423
+ color: hsl(var(--foreground));
578
424
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.179",
3
+ "version": "1.1.181",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",