juxscript 1.1.169 → 1.1.170

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.
@@ -12,8 +12,6 @@ export interface DataFrameOptions {
12
12
  filterable?: boolean;
13
13
  paginated?: boolean;
14
14
  rowsPerPage?: number;
15
- showStatus?: boolean;
16
- icon?: string;
17
15
  style?: string;
18
16
  class?: string;
19
17
  }
@@ -32,41 +30,78 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
32
30
  private _storageKey;
33
31
  private _pendingSource;
34
32
  private _inlineUpload;
35
- private _showStatus;
36
- private _icon;
37
33
  constructor(id: string, options?: DataFrameOptions);
38
34
  protected getTriggerEvents(): readonly string[];
39
35
  protected getCallbackEvents(): readonly string[];
36
+ /**
37
+ * Load from IndexedDB by storage key
38
+ */
40
39
  fromStorage(key: string): this;
40
+ /**
41
+ * Load from a FileUpload component — auto-wires change event
42
+ */
41
43
  fromUpload(upload: FileUpload): this;
44
+ /**
45
+ * Load from raw data — array of objects or Record<string, any[]>
46
+ */
42
47
  fromData(data: Record<string, any>[] | Record<string, any[]>): this;
43
48
  /**
44
- * Add an inline file upload control.
45
- * @param label - Button label (default: 'Upload File')
49
+ * Add an inline file upload control above the table.
50
+ * Auto-wires parsing, storage, and display zero callbacks needed.
51
+ *
52
+ * @param label - Button/label text (default: 'Upload File')
46
53
  * @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
47
- * @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
48
54
  */
49
- withUpload(label?: string, accept?: string, icon?: string): this;
50
- /** Show/hide the status bar */
51
- showStatus(v: boolean): this;
52
- /** Set a custom icon for the status bar */
53
- statusIcon(v: string): this;
55
+ withUpload(label?: string, accept?: string): this;
56
+ /**
57
+ * Apply a transform to the current DataFrame and update the table
58
+ */
54
59
  apply(fn: (df: DataFrame) => DataFrame): this;
60
+ /**
61
+ * Filter rows
62
+ */
55
63
  filter(predicate: (row: Record<string, any>, index: number) => boolean): this;
64
+ /**
65
+ * Select columns
66
+ */
56
67
  select(...cols: string[]): this;
68
+ /**
69
+ * Sort by column
70
+ */
57
71
  sort(col: string, descending?: boolean): this;
72
+ /**
73
+ * Show first N rows
74
+ */
58
75
  head(n?: number): this;
76
+ /**
77
+ * Show last N rows
78
+ */
59
79
  tail(n?: number): this;
80
+ /**
81
+ * Add a computed column
82
+ */
60
83
  withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this;
84
+ /**
85
+ * Where clause
86
+ */
61
87
  where(col: string, op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', value: any): this;
88
+ /** Get the underlying DataFrame */
62
89
  get df(): DataFrame | null;
90
+ /** Get the underlying TabularDriver */
63
91
  get driver(): TabularDriver;
92
+ /** Get the internal Table component */
64
93
  get table(): Table | null;
94
+ /** Get describe() stats */
65
95
  describe(): Record<string, any> | null;
96
+ /** Export to CSV string */
66
97
  toCSV(delimiter?: string): string;
98
+ /** Export to row objects */
67
99
  toRows(): Record<string, any>[];
100
+ /** Get shape */
68
101
  get shape(): [number, number];
102
+ /** Get column names */
69
103
  get columns(): string[];
104
+ /** Save current DataFrame to IndexedDB */
70
105
  save(key?: string): Promise<string | null>;
71
106
  striped(v: boolean): this;
72
107
  hoverable(v: boolean): this;
@@ -77,8 +112,8 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
77
112
  private _updateStatus;
78
113
  private _setDataFrame;
79
114
  private _updateTable;
80
- private _showFilterInput;
81
115
  update(prop: string, value: any): void;
116
+ private _showFilterInput;
82
117
  render(targetId?: string | HTMLElement | BaseComponent<any>): this;
83
118
  }
84
119
  export declare function dataframe(id: string, options?: DataFrameOptions): DataFrameComponent;
@@ -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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"}
@@ -25,16 +25,12 @@ export class DataFrameComponent extends BaseComponent {
25
25
  this._storageKey = null;
26
26
  this._pendingSource = null;
27
27
  this._inlineUpload = null;
28
- this._showStatus = true;
29
- this._icon = '';
30
28
  this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
31
- this._showStatus = options.showStatus ?? true;
32
- this._icon = options.icon ?? '';
33
29
  this._tableOptions = {
34
30
  striped: options.striped ?? true,
35
31
  hoverable: options.hoverable ?? true,
36
32
  sortable: options.sortable ?? true,
37
- filterable: options.filterable ?? false, // defer until data loaded
33
+ filterable: options.filterable ?? true,
38
34
  paginated: options.paginated ?? true,
39
35
  rowsPerPage: options.rowsPerPage ?? 25
40
36
  };
@@ -44,17 +40,18 @@ export class DataFrameComponent extends BaseComponent {
44
40
  /* ═══════════════════════════════════════════════════
45
41
  * DATA SOURCES
46
42
  * ═══════════════════════════════════════════════════ */
43
+ /**
44
+ * Load from IndexedDB by storage key
45
+ */
47
46
  fromStorage(key) {
48
47
  this._storageKey = key;
49
- const loadFn = async () => {
48
+ this._pendingSource = async () => {
50
49
  this.state.loading = true;
51
- this._updateStatus('⏳ Loading...', 'loading');
52
50
  try {
53
- const df = await this._driver.loadByName(key);
51
+ let df = await this._driver.loadByName(key);
54
52
  if (!df) {
55
53
  this._triggerCallback('error', 'No table found with key: ' + key, null, this);
56
54
  this.state.loading = false;
57
- this._updateStatus('No table found: ' + key, 'empty');
58
55
  return;
59
56
  }
60
57
  this._setDataFrame(df, 'storage: ' + key);
@@ -62,44 +59,42 @@ export class DataFrameComponent extends BaseComponent {
62
59
  catch (err) {
63
60
  this._triggerCallback('error', err.message, null, this);
64
61
  this.state.loading = false;
65
- this._updateStatus('❌ ' + err.message, 'error');
66
62
  }
67
63
  };
68
- if (this._table) {
69
- loadFn();
70
- }
71
- else {
72
- this._pendingSource = loadFn;
73
- }
74
64
  return this;
75
65
  }
66
+ /**
67
+ * Load from a FileUpload component — auto-wires change event
68
+ */
76
69
  fromUpload(upload) {
77
70
  this._uploadRef = upload;
78
71
  this._pendingSource = async () => {
72
+ // Wire upload's change event to parse incoming files
79
73
  upload.bind('change', async (files) => {
80
74
  if (!files || files.length === 0)
81
75
  return;
82
76
  const file = files[0];
83
77
  this.state.loading = true;
84
- this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
85
78
  try {
86
79
  const df = await this._driver.streamFile(file);
80
+ // Auto-persist to IndexedDB
87
81
  await this._driver.store(file.name, df, { source: file.name });
88
- this._setDataFrame(df, file.name);
82
+ this._setDataFrame(df, 'upload: ' + file.name);
89
83
  }
90
84
  catch (err) {
91
85
  this._triggerCallback('error', err.message, null, this);
92
86
  this.state.loading = false;
93
- this._updateStatus('❌ ' + err.message, 'error');
94
87
  }
95
88
  });
96
89
  };
97
90
  return this;
98
91
  }
92
+ /**
93
+ * Load from raw data — array of objects or Record<string, any[]>
94
+ */
99
95
  fromData(data) {
100
- const loadFn = async () => {
96
+ this._pendingSource = async () => {
101
97
  this.state.loading = true;
102
- this._updateStatus('⏳ Loading data...', 'loading');
103
98
  try {
104
99
  const df = new DataFrame(data);
105
100
  this._setDataFrame(df, 'inline data');
@@ -107,37 +102,27 @@ export class DataFrameComponent extends BaseComponent {
107
102
  catch (err) {
108
103
  this._triggerCallback('error', err.message, null, this);
109
104
  this.state.loading = false;
110
- this._updateStatus('❌ ' + err.message, 'error');
111
105
  }
112
106
  };
113
- if (this._table) {
114
- loadFn();
115
- }
116
- else {
117
- this._pendingSource = loadFn;
118
- }
119
107
  return this;
120
108
  }
121
109
  /**
122
- * Add an inline file upload control.
123
- * @param label - Button label (default: 'Upload File')
110
+ * Add an inline file upload control above the table.
111
+ * Auto-wires parsing, storage, and display zero callbacks needed.
112
+ *
113
+ * @param label - Button/label text (default: 'Upload File')
124
114
  * @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
125
- * @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
126
115
  */
127
- withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls', icon = 'upload') {
128
- this._inlineUpload = { label, accept, icon };
116
+ withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls') {
117
+ this._inlineUpload = { label, accept };
129
118
  return this;
130
119
  }
131
120
  /* ═══════════════════════════════════════════════════
132
- * UI TOGGLES
133
- * ═══════════════════════════════════════════════════ */
134
- /** Show/hide the status bar */
135
- showStatus(v) { this._showStatus = v; return this; }
136
- /** Set a custom icon for the status bar */
137
- statusIcon(v) { this._icon = v; return this; }
138
- /* ═══════════════════════════════════════════════════
139
- * TRANSFORM API
121
+ * TRANSFORM API (returns new DataFrameComponent view)
140
122
  * ═══════════════════════════════════════════════════ */
123
+ /**
124
+ * Apply a transform to the current DataFrame and update the table
125
+ */
141
126
  apply(fn) {
142
127
  if (!this._df)
143
128
  return this;
@@ -146,38 +131,78 @@ export class DataFrameComponent extends BaseComponent {
146
131
  this._triggerCallback('transform', result, null, this);
147
132
  return this;
148
133
  }
134
+ /**
135
+ * Filter rows
136
+ */
149
137
  filter(predicate) {
150
138
  return this.apply(df => df.filter(predicate));
151
139
  }
140
+ /**
141
+ * Select columns
142
+ */
152
143
  select(...cols) {
153
144
  return this.apply(df => df.select(...cols));
154
145
  }
146
+ /**
147
+ * Sort by column
148
+ */
155
149
  sort(col, descending) {
156
150
  return this.apply(df => df.sort(col, descending));
157
151
  }
152
+ /**
153
+ * Show first N rows
154
+ */
158
155
  head(n = 5) {
159
156
  return this.apply(df => df.head(n));
160
157
  }
158
+ /**
159
+ * Show last N rows
160
+ */
161
161
  tail(n = 5) {
162
162
  return this.apply(df => df.tail(n));
163
163
  }
164
+ /**
165
+ * Add a computed column
166
+ */
164
167
  withColumn(name, fn) {
165
168
  return this.apply(df => df.withColumn(name, fn));
166
169
  }
170
+ /**
171
+ * Where clause
172
+ */
167
173
  where(col, op, value) {
168
174
  return this.apply(df => df.where(col, op, value));
169
175
  }
170
176
  /* ═══════════════════════════════════════════════════
171
177
  * ACCESSORS
172
178
  * ═══════════════════════════════════════════════════ */
179
+ /** Get the underlying DataFrame */
173
180
  get df() { return this._df; }
181
+ /** Get the underlying TabularDriver */
174
182
  get driver() { return this._driver; }
183
+ /** Get the internal Table component */
175
184
  get table() { return this._table; }
176
- describe() { return this._df?.describe() ?? null; }
177
- toCSV(delimiter) { return this._df?.toCSV(delimiter) ?? ''; }
178
- toRows() { return this._df?.toRows() ?? []; }
179
- get shape() { return this._df?.shape ?? [0, 0]; }
180
- get columns() { return this._df?.columns ?? []; }
185
+ /** Get describe() stats */
186
+ describe() {
187
+ return this._df?.describe() ?? null;
188
+ }
189
+ /** Export to CSV string */
190
+ toCSV(delimiter) {
191
+ return this._df?.toCSV(delimiter) ?? '';
192
+ }
193
+ /** Export to row objects */
194
+ toRows() {
195
+ return this._df?.toRows() ?? [];
196
+ }
197
+ /** Get shape */
198
+ get shape() {
199
+ return this._df?.shape ?? [0, 0];
200
+ }
201
+ /** Get column names */
202
+ get columns() {
203
+ return this._df?.columns ?? [];
204
+ }
205
+ /** Save current DataFrame to IndexedDB */
181
206
  async save(key) {
182
207
  if (!this._df)
183
208
  return null;
@@ -196,26 +221,10 @@ export class DataFrameComponent extends BaseComponent {
196
221
  /* ═══════════════════════════════════════════════════
197
222
  * INTERNAL
198
223
  * ═══════════════════════════════════════════════════ */
199
- _updateStatus(text, type = 'empty') {
224
+ _updateStatus(text) {
200
225
  const el = document.getElementById(`${this._id}-status`);
201
- if (!el)
202
- return;
203
- el.className = 'jux-dataframe-status';
204
- if (type)
205
- el.classList.add(`jux-dataframe-status-${type}`);
206
- // Clear and rebuild
207
- el.innerHTML = '';
208
- if (this._icon && type === 'success') {
209
- const iconEl = renderIcon(this._icon);
210
- iconEl.style.width = '16px';
211
- iconEl.style.height = '16px';
212
- iconEl.style.marginRight = '6px';
213
- iconEl.style.verticalAlign = 'middle';
214
- el.appendChild(iconEl);
215
- }
216
- const span = document.createElement('span');
217
- span.textContent = text;
218
- el.appendChild(span);
226
+ if (el)
227
+ el.textContent = text;
219
228
  }
220
229
  _setDataFrame(df, sourceName) {
221
230
  this._df = df;
@@ -229,18 +238,22 @@ export class DataFrameComponent extends BaseComponent {
229
238
  this._df = df.select(...cleanCols);
230
239
  }
231
240
  this._updateTable();
232
- this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols`, 'success');
233
- // Enable filter now that data exists
241
+ this._updateStatus(`✅ ${sourceName} — ${this._df.height} rows × ${this._df.width} cols`);
242
+ this._triggerCallback('load', this._df, null, this);
243
+ // ✅ Enable filter now that data exists (render it into DOM)
234
244
  if (this._tableOptions.filterable) {
235
245
  this._showFilterInput();
236
246
  }
237
- this._triggerCallback('load', this._df, null, this);
238
247
  }
239
248
  _updateTable() {
240
249
  if (!this._table || !this._df)
241
250
  return;
242
251
  this._table.columns(this._df.columns).rows(this._df.toRows());
243
252
  }
253
+ update(prop, value) { }
254
+ /* ═══════════════════════════════════════════════════
255
+ * RENDER
256
+ * ═══════════════════════════════════════════════════ */
244
257
  _showFilterInput() {
245
258
  const wrapper = document.getElementById(this._id);
246
259
  if (!wrapper)
@@ -262,14 +275,14 @@ export class DataFrameComponent extends BaseComponent {
262
275
  iconWrap.appendChild(iconEl);
263
276
  filterContainer.appendChild(iconWrap);
264
277
  filterContainer.appendChild(input);
265
- // ✅ FIX: Find the table wrapper that's a direct child of wrapper
266
- const tableWrapper = wrapper.querySelector('.jux-table-wrapper');
267
- if (tableWrapper && tableWrapper.parentElement === wrapper) {
268
- // Insert before table wrapper (safe because we verified it's a child)
269
- wrapper.insertBefore(filterContainer, tableWrapper);
278
+ // ✅ FIX: Find the table element (we know it exists because render() created it)
279
+ const tableElement = wrapper.querySelector('.jux-table-wrapper table');
280
+ if (tableElement && tableElement.parentElement) {
281
+ // Insert filter BEFORE the table element
282
+ tableElement.parentElement.insertBefore(filterContainer, tableElement);
270
283
  }
271
284
  else {
272
- // Fallback: just append to wrapper
285
+ // Fallback: append to wrapper (shouldn't happen, but safe)
273
286
  wrapper.appendChild(filterContainer);
274
287
  }
275
288
  input.addEventListener('input', () => {
@@ -286,10 +299,6 @@ export class DataFrameComponent extends BaseComponent {
286
299
  this._table?.rows(filtered.toRows());
287
300
  });
288
301
  }
289
- update(prop, value) { }
290
- /* ═══════════════════════════════════════════════════
291
- * RENDER
292
- * ═══════════════════════════════════════════════════ */
293
302
  render(targetId) {
294
303
  const container = this._setupContainer(targetId);
295
304
  const { style, class: className } = this.state;
@@ -300,16 +309,13 @@ export class DataFrameComponent extends BaseComponent {
300
309
  wrapper.className += ` ${className}`;
301
310
  if (style)
302
311
  wrapper.setAttribute('style', style);
303
- // Inline upload
312
+ // Inline upload (if withUpload was called)
304
313
  if (this._inlineUpload) {
305
- const uploadOpts = {
314
+ const upload = new FileUpload(`${this._id}-upload`, {
306
315
  label: this._inlineUpload.label,
307
- accept: this._inlineUpload.accept,
308
- };
309
- if (this._inlineUpload.icon) {
310
- uploadOpts.icon = this._inlineUpload.icon;
311
- }
312
- const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
316
+ accept: this._inlineUpload.accept
317
+ });
318
+ // Wire it as a fromUpload source
313
319
  this._uploadRef = upload;
314
320
  this._pendingSource = async () => {
315
321
  upload.bind('change', async (files) => {
@@ -317,7 +323,7 @@ export class DataFrameComponent extends BaseComponent {
317
323
  return;
318
324
  const file = files[0];
319
325
  this.state.loading = true;
320
- this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
326
+ this._updateStatus('⏳ Parsing ' + file.name + '...');
321
327
  try {
322
328
  const df = await this._driver.streamFile(file);
323
329
  await this._driver.store(file.name, df, { source: file.name });
@@ -326,10 +332,11 @@ export class DataFrameComponent extends BaseComponent {
326
332
  catch (err) {
327
333
  this._triggerCallback('error', err.message, null, this);
328
334
  this.state.loading = false;
329
- this._updateStatus('❌ ' + err.message, 'error');
335
+ this._updateStatus('❌ ' + err.message);
330
336
  }
331
337
  });
332
338
  };
339
+ // Render upload into wrapper before status/table
333
340
  const uploadContainer = document.createElement('div');
334
341
  uploadContainer.className = 'jux-dataframe-upload';
335
342
  uploadContainer.id = `${this._id}-upload-container`;
@@ -340,15 +347,14 @@ export class DataFrameComponent extends BaseComponent {
340
347
  else {
341
348
  container.appendChild(wrapper);
342
349
  }
343
- // Status bar (conditional)
344
- if (this._showStatus) {
345
- const statusBar = document.createElement('div');
346
- statusBar.className = 'jux-dataframe-status jux-dataframe-status-empty';
347
- statusBar.id = `${this._id}-status`;
348
- statusBar.textContent = 'No data loaded.';
349
- wrapper.appendChild(statusBar);
350
- }
351
- // Table — filterable is false initially; we enable it after data loads
350
+ // Status bar
351
+ const statusBar = document.createElement('div');
352
+ statusBar.className = 'jux-dataframe-status';
353
+ statusBar.id = `${this._id}-status`;
354
+ statusBar.style.cssText = 'font-size:13px;color:#888;margin-bottom:8px;';
355
+ statusBar.textContent = 'No data loaded.';
356
+ wrapper.appendChild(statusBar);
357
+ // ✅ Create internal table — this establishes the DOM structure
352
358
  const tbl = new Table(`${this._id}-table`, {
353
359
  striped: this._tableOptions.striped,
354
360
  hoverable: this._tableOptions.hoverable,
@@ -357,7 +363,7 @@ export class DataFrameComponent extends BaseComponent {
357
363
  paginated: this._tableOptions.paginated,
358
364
  rowsPerPage: this._tableOptions.rowsPerPage
359
365
  });
360
- tbl.render(wrapper);
366
+ tbl.render(wrapper); // ✅ Table now exists in wrapper
361
367
  this._table = tbl;
362
368
  // Execute pending data source
363
369
  if (this._pendingSource) {
@@ -17,8 +17,6 @@ export interface DataFrameOptions {
17
17
  filterable?: boolean;
18
18
  paginated?: boolean;
19
19
  rowsPerPage?: number;
20
- showStatus?: boolean;
21
- icon?: string;
22
20
  style?: string;
23
21
  class?: string;
24
22
  }
@@ -45,9 +43,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
45
43
  private _uploadRef: FileUpload | null = null;
46
44
  private _storageKey: string | null = null;
47
45
  private _pendingSource: (() => Promise<void>) | null = null;
48
- private _inlineUpload: { label: string; accept: string; icon: string } | null = null;
49
- private _showStatus: boolean = true;
50
- private _icon: string = '';
46
+ private _inlineUpload: { label: string; accept: string } | null = null;
51
47
 
52
48
  constructor(id: string, options: DataFrameOptions = {}) {
53
49
  super(id, {
@@ -67,14 +63,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
67
63
  options.storeName ?? 'frames'
68
64
  );
69
65
 
70
- this._showStatus = options.showStatus ?? true;
71
- this._icon = options.icon ?? '';
72
-
73
66
  this._tableOptions = {
74
67
  striped: options.striped ?? true,
75
68
  hoverable: options.hoverable ?? true,
76
69
  sortable: options.sortable ?? true,
77
- filterable: options.filterable ?? false, // defer until data loaded
70
+ filterable: options.filterable ?? true,
78
71
  paginated: options.paginated ?? true,
79
72
  rowsPerPage: options.rowsPerPage ?? 25
80
73
  };
@@ -87,98 +80,91 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
87
80
  * DATA SOURCES
88
81
  * ═══════════════════════════════════════════════════ */
89
82
 
83
+ /**
84
+ * Load from IndexedDB by storage key
85
+ */
90
86
  fromStorage(key: string): this {
91
87
  this._storageKey = key;
92
- const loadFn = async () => {
88
+ this._pendingSource = async () => {
93
89
  this.state.loading = true;
94
- this._updateStatus('⏳ Loading...', 'loading');
95
90
  try {
96
- const df = await this._driver.loadByName(key);
91
+ let df = await this._driver.loadByName(key);
97
92
  if (!df) {
98
93
  this._triggerCallback('error', 'No table found with key: ' + key, null, this);
99
94
  this.state.loading = false;
100
- this._updateStatus('No table found: ' + key, 'empty');
101
95
  return;
102
96
  }
103
97
  this._setDataFrame(df, 'storage: ' + key);
104
98
  } catch (err: any) {
105
99
  this._triggerCallback('error', err.message, null, this);
106
100
  this.state.loading = false;
107
- this._updateStatus('❌ ' + err.message, 'error');
108
101
  }
109
102
  };
110
- if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
111
103
  return this;
112
104
  }
113
105
 
106
+ /**
107
+ * Load from a FileUpload component — auto-wires change event
108
+ */
114
109
  fromUpload(upload: FileUpload): this {
115
110
  this._uploadRef = upload;
116
111
  this._pendingSource = async () => {
112
+ // Wire upload's change event to parse incoming files
117
113
  upload.bind('change', async (files: File[]) => {
118
114
  if (!files || files.length === 0) return;
119
115
  const file = files[0];
120
116
  this.state.loading = true;
121
- this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
117
+
122
118
  try {
123
119
  const df = await this._driver.streamFile(file);
120
+ // Auto-persist to IndexedDB
124
121
  await this._driver.store(file.name, df, { source: file.name });
125
- this._setDataFrame(df, file.name);
122
+ this._setDataFrame(df, 'upload: ' + file.name);
126
123
  } catch (err: any) {
127
124
  this._triggerCallback('error', err.message, null, this);
128
125
  this.state.loading = false;
129
- this._updateStatus('❌ ' + err.message, 'error');
130
126
  }
131
127
  });
132
128
  };
133
129
  return this;
134
130
  }
135
131
 
132
+ /**
133
+ * Load from raw data — array of objects or Record<string, any[]>
134
+ */
136
135
  fromData(data: Record<string, any>[] | Record<string, any[]>): this {
137
- const loadFn = async () => {
136
+ this._pendingSource = async () => {
138
137
  this.state.loading = true;
139
- this._updateStatus('⏳ Loading data...', 'loading');
140
138
  try {
141
139
  const df = new DataFrame(data);
142
140
  this._setDataFrame(df, 'inline data');
143
141
  } catch (err: any) {
144
142
  this._triggerCallback('error', err.message, null, this);
145
143
  this.state.loading = false;
146
- this._updateStatus('❌ ' + err.message, 'error');
147
144
  }
148
145
  };
149
- if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
150
146
  return this;
151
147
  }
152
148
 
153
149
  /**
154
- * Add an inline file upload control.
155
- * @param label - Button label (default: 'Upload File')
150
+ * Add an inline file upload control above the table.
151
+ * Auto-wires parsing, storage, and display zero callbacks needed.
152
+ *
153
+ * @param label - Button/label text (default: 'Upload File')
156
154
  * @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
157
- * @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
158
155
  */
159
- withUpload(
160
- label: string = 'Upload File',
161
- accept: string = '.csv,.tsv,.txt,.xlsx,.xls',
162
- icon: string = 'upload'
163
- ): this {
164
- this._inlineUpload = { label, accept, icon };
156
+ withUpload(label: string = 'Upload File', accept: string = '.csv,.tsv,.txt,.xlsx,.xls'): this {
157
+ this._inlineUpload = { label, accept };
165
158
  return this;
166
159
  }
167
160
 
168
161
  /* ═══════════════════════════════════════════════════
169
- * UI TOGGLES
170
- * ═══════════════════════════════════════════════════ */
171
-
172
- /** Show/hide the status bar */
173
- showStatus(v: boolean): this { this._showStatus = v; return this; }
174
-
175
- /** Set a custom icon for the status bar */
176
- statusIcon(v: string): this { this._icon = v; return this; }
177
-
178
- /* ═══════════════════════════════════════════════════
179
- * TRANSFORM API
162
+ * TRANSFORM API (returns new DataFrameComponent view)
180
163
  * ═══════════════════════════════════════════════════ */
181
164
 
165
+ /**
166
+ * Apply a transform to the current DataFrame and update the table
167
+ */
182
168
  apply(fn: (df: DataFrame) => DataFrame): this {
183
169
  if (!this._df) return this;
184
170
  const result = fn(this._df);
@@ -187,30 +173,51 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
187
173
  return this;
188
174
  }
189
175
 
176
+ /**
177
+ * Filter rows
178
+ */
190
179
  filter(predicate: (row: Record<string, any>, index: number) => boolean): this {
191
180
  return this.apply(df => df.filter(predicate));
192
181
  }
193
182
 
183
+ /**
184
+ * Select columns
185
+ */
194
186
  select(...cols: string[]): this {
195
187
  return this.apply(df => df.select(...cols));
196
188
  }
197
189
 
190
+ /**
191
+ * Sort by column
192
+ */
198
193
  sort(col: string, descending?: boolean): this {
199
194
  return this.apply(df => df.sort(col, descending));
200
195
  }
201
196
 
197
+ /**
198
+ * Show first N rows
199
+ */
202
200
  head(n: number = 5): this {
203
201
  return this.apply(df => df.head(n));
204
202
  }
205
203
 
204
+ /**
205
+ * Show last N rows
206
+ */
206
207
  tail(n: number = 5): this {
207
208
  return this.apply(df => df.tail(n));
208
209
  }
209
210
 
211
+ /**
212
+ * Add a computed column
213
+ */
210
214
  withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this {
211
215
  return this.apply(df => df.withColumn(name, fn));
212
216
  }
213
217
 
218
+ /**
219
+ * Where clause
220
+ */
214
221
  where(col: string, op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', value: any): this {
215
222
  return this.apply(df => df.where(col, op, value));
216
223
  }
@@ -219,15 +226,41 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
219
226
  * ACCESSORS
220
227
  * ═══════════════════════════════════════════════════ */
221
228
 
229
+ /** Get the underlying DataFrame */
222
230
  get df(): DataFrame | null { return this._df; }
231
+
232
+ /** Get the underlying TabularDriver */
223
233
  get driver(): TabularDriver { return this._driver; }
234
+
235
+ /** Get the internal Table component */
224
236
  get table(): Table | null { return this._table; }
225
- describe(): Record<string, any> | null { return this._df?.describe() ?? null; }
226
- toCSV(delimiter?: string): string { return this._df?.toCSV(delimiter) ?? ''; }
227
- toRows(): Record<string, any>[] { return this._df?.toRows() ?? []; }
228
- get shape(): [number, number] { return this._df?.shape ?? [0, 0]; }
229
- get columns(): string[] { return this._df?.columns ?? []; }
230
237
 
238
+ /** Get describe() stats */
239
+ describe(): Record<string, any> | null {
240
+ return this._df?.describe() ?? null;
241
+ }
242
+
243
+ /** Export to CSV string */
244
+ toCSV(delimiter?: string): string {
245
+ return this._df?.toCSV(delimiter) ?? '';
246
+ }
247
+
248
+ /** Export to row objects */
249
+ toRows(): Record<string, any>[] {
250
+ return this._df?.toRows() ?? [];
251
+ }
252
+
253
+ /** Get shape */
254
+ get shape(): [number, number] {
255
+ return this._df?.shape ?? [0, 0];
256
+ }
257
+
258
+ /** Get column names */
259
+ get columns(): string[] {
260
+ return this._df?.columns ?? [];
261
+ }
262
+
263
+ /** Save current DataFrame to IndexedDB */
231
264
  async save(key?: string): Promise<string | null> {
232
265
  if (!this._df) return null;
233
266
  const name = key ?? this._storageKey ?? this._id;
@@ -249,28 +282,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
249
282
  * INTERNAL
250
283
  * ═══════════════════════════════════════════════════ */
251
284
 
252
- private _updateStatus(text: string, type: 'loading' | 'success' | 'error' | 'empty' = 'empty'): void {
285
+ private _updateStatus(text: string): void {
253
286
  const el = document.getElementById(`${this._id}-status`);
254
- if (!el) return;
255
-
256
- el.className = 'jux-dataframe-status';
257
- if (type) el.classList.add(`jux-dataframe-status-${type}`);
258
-
259
- // Clear and rebuild
260
- el.innerHTML = '';
261
-
262
- if (this._icon && type === 'success') {
263
- const iconEl = renderIcon(this._icon);
264
- iconEl.style.width = '16px';
265
- iconEl.style.height = '16px';
266
- iconEl.style.marginRight = '6px';
267
- iconEl.style.verticalAlign = 'middle';
268
- el.appendChild(iconEl);
269
- }
270
-
271
- const span = document.createElement('span');
272
- span.textContent = text;
273
- el.appendChild(span);
287
+ if (el) el.textContent = text;
274
288
  }
275
289
 
276
290
  private _setDataFrame(df: DataFrame, sourceName: string): void {
@@ -287,17 +301,13 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
287
301
  }
288
302
 
289
303
  this._updateTable();
290
- this._updateStatus(
291
- `${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols`,
292
- 'success'
293
- );
304
+ this._updateStatus(`✅ ${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols`);
305
+ this._triggerCallback('load', this._df, null, this);
294
306
 
295
- // Enable filter now that data exists
307
+ // Enable filter now that data exists (render it into DOM)
296
308
  if (this._tableOptions.filterable) {
297
309
  this._showFilterInput();
298
310
  }
299
-
300
- this._triggerCallback('load', this._df, null, this);
301
311
  }
302
312
 
303
313
  private _updateTable(): void {
@@ -305,6 +315,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
305
315
  this._table.columns(this._df.columns).rows(this._df.toRows());
306
316
  }
307
317
 
318
+ update(prop: string, value: any): void { }
319
+
320
+ /* ═══════════════════════════════════════════════════
321
+ * RENDER
322
+ * ═══════════════════════════════════════════════════ */
323
+
308
324
  private _showFilterInput(): void {
309
325
  const wrapper = document.getElementById(this._id);
310
326
  if (!wrapper) return;
@@ -331,14 +347,14 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
331
347
  filterContainer.appendChild(iconWrap);
332
348
  filterContainer.appendChild(input);
333
349
 
334
- // ✅ FIX: Find the table wrapper that's a direct child of wrapper
335
- const tableWrapper = wrapper.querySelector('.jux-table-wrapper');
350
+ // ✅ FIX: Find the table element (we know it exists because render() created it)
351
+ const tableElement = wrapper.querySelector('.jux-table-wrapper table');
336
352
 
337
- if (tableWrapper && tableWrapper.parentElement === wrapper) {
338
- // Insert before table wrapper (safe because we verified it's a child)
339
- wrapper.insertBefore(filterContainer, tableWrapper);
353
+ if (tableElement && tableElement.parentElement) {
354
+ // Insert filter BEFORE the table element
355
+ tableElement.parentElement.insertBefore(filterContainer, tableElement);
340
356
  } else {
341
- // Fallback: just append to wrapper
357
+ // Fallback: append to wrapper (shouldn't happen, but safe)
342
358
  wrapper.appendChild(filterContainer);
343
359
  }
344
360
 
@@ -358,12 +374,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
358
374
  });
359
375
  }
360
376
 
361
- update(prop: string, value: any): void { }
362
-
363
- /* ═══════════════════════════════════════════════════
364
- * RENDER
365
- * ═══════════════════════════════════════════════════ */
366
-
367
377
  render(targetId?: string | HTMLElement | BaseComponent<any>): this {
368
378
  const container = this._setupContainer(targetId);
369
379
  const { style, class: className } = this.state;
@@ -374,25 +384,22 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
374
384
  if (className) wrapper.className += ` ${className}`;
375
385
  if (style) wrapper.setAttribute('style', style);
376
386
 
377
- // Inline upload
387
+ // Inline upload (if withUpload was called)
378
388
  if (this._inlineUpload) {
379
- const uploadOpts: any = {
389
+ const upload = new FileUpload(`${this._id}-upload`, {
380
390
  label: this._inlineUpload.label,
381
- accept: this._inlineUpload.accept,
382
- };
383
- if (this._inlineUpload.icon) {
384
- uploadOpts.icon = this._inlineUpload.icon;
385
- }
386
-
387
- const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
391
+ accept: this._inlineUpload.accept
392
+ });
388
393
 
394
+ // Wire it as a fromUpload source
389
395
  this._uploadRef = upload;
390
396
  this._pendingSource = async () => {
391
397
  upload.bind('change', async (files: File[]) => {
392
398
  if (!files || files.length === 0) return;
393
399
  const file = files[0];
394
400
  this.state.loading = true;
395
- this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
401
+ this._updateStatus('⏳ Parsing ' + file.name + '...');
402
+
396
403
  try {
397
404
  const df = await this._driver.streamFile(file);
398
405
  await this._driver.store(file.name, df, { source: file.name });
@@ -400,11 +407,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
400
407
  } catch (err: any) {
401
408
  this._triggerCallback('error', err.message, null, this);
402
409
  this.state.loading = false;
403
- this._updateStatus('❌ ' + err.message, 'error');
410
+ this._updateStatus('❌ ' + err.message);
404
411
  }
405
412
  });
406
413
  };
407
414
 
415
+ // Render upload into wrapper before status/table
408
416
  const uploadContainer = document.createElement('div');
409
417
  uploadContainer.className = 'jux-dataframe-upload';
410
418
  uploadContainer.id = `${this._id}-upload-container`;
@@ -415,16 +423,15 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
415
423
  container.appendChild(wrapper);
416
424
  }
417
425
 
418
- // Status bar (conditional)
419
- if (this._showStatus) {
420
- const statusBar = document.createElement('div');
421
- statusBar.className = 'jux-dataframe-status jux-dataframe-status-empty';
422
- statusBar.id = `${this._id}-status`;
423
- statusBar.textContent = 'No data loaded.';
424
- wrapper.appendChild(statusBar);
425
- }
426
+ // Status bar
427
+ const statusBar = document.createElement('div');
428
+ statusBar.className = 'jux-dataframe-status';
429
+ statusBar.id = `${this._id}-status`;
430
+ statusBar.style.cssText = 'font-size:13px;color:#888;margin-bottom:8px;';
431
+ statusBar.textContent = 'No data loaded.';
432
+ wrapper.appendChild(statusBar);
426
433
 
427
- // Table filterable is false initially; we enable it after data loads
434
+ // Create internal table this establishes the DOM structure
428
435
  const tbl = new Table(`${this._id}-table`, {
429
436
  striped: this._tableOptions.striped,
430
437
  hoverable: this._tableOptions.hoverable,
@@ -433,7 +440,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
433
440
  paginated: this._tableOptions.paginated,
434
441
  rowsPerPage: this._tableOptions.rowsPerPage
435
442
  });
436
- tbl.render(wrapper);
443
+ tbl.render(wrapper); // ✅ Table now exists in wrapper
437
444
  this._table = tbl;
438
445
 
439
446
  // Execute pending data source
@@ -444,6 +451,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
444
451
  }
445
452
 
446
453
  this._wireStandardEvents(wrapper);
454
+
447
455
  return this;
448
456
  }
449
457
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.169",
3
+ "version": "1.1.170",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",