juxscript 1.1.170 → 1.1.171

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