juxscript 1.1.166 → 1.1.167

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,41 @@ 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
43
  /**
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')
44
+ * Add an inline file upload control.
45
+ * @param label - Button label (default: 'Upload File')
53
46
  * @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
47
+ * @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
54
48
  */
55
- withUpload(label?: string, accept?: string): this;
56
- /**
57
- * Apply a transform to the current DataFrame and update the table
58
- */
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;
59
54
  apply(fn: (df: DataFrame) => DataFrame): this;
60
- /**
61
- * Filter rows
62
- */
63
55
  filter(predicate: (row: Record<string, any>, index: number) => boolean): this;
64
- /**
65
- * Select columns
66
- */
67
56
  select(...cols: string[]): this;
68
- /**
69
- * Sort by column
70
- */
71
57
  sort(col: string, descending?: boolean): this;
72
- /**
73
- * Show first N rows
74
- */
75
58
  head(n?: number): this;
76
- /**
77
- * Show last N rows
78
- */
79
59
  tail(n?: number): this;
80
- /**
81
- * Add a computed column
82
- */
83
60
  withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this;
84
- /**
85
- * Where clause
86
- */
87
61
  where(col: string, op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', value: any): this;
88
- /** Get the underlying DataFrame */
89
62
  get df(): DataFrame | null;
90
- /** Get the underlying TabularDriver */
91
63
  get driver(): TabularDriver;
92
- /** Get the internal Table component */
93
64
  get table(): Table | null;
94
- /** Get describe() stats */
95
65
  describe(): Record<string, any> | null;
96
- /** Export to CSV string */
97
66
  toCSV(delimiter?: string): string;
98
- /** Export to row objects */
99
67
  toRows(): Record<string, any>[];
100
- /** Get shape */
101
68
  get shape(): [number, number];
102
- /** Get column names */
103
69
  get columns(): string[];
104
- /** Save current DataFrame to IndexedDB */
105
70
  save(key?: string): Promise<string | null>;
106
71
  striped(v: boolean): this;
107
72
  hoverable(v: boolean): this;
@@ -112,6 +77,7 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
112
77
  private _updateStatus;
113
78
  private _setDataFrame;
114
79
  private _updateTable;
80
+ private _showFilterInput;
115
81
  update(prop: string, value: any): void;
116
82
  render(targetId?: string | HTMLElement | BaseComponent<any>): this;
117
83
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@ import { DataFrame } from '../storage/DataFrame.js';
3
3
  import { TabularDriver } from '../storage/TabularDriver.js';
4
4
  import { FileUpload } from './fileupload.js';
5
5
  import { Table } from './table.js';
6
+ import { renderIcon } from './icons.js';
6
7
  const TRIGGER_EVENTS = [];
7
8
  const CALLBACK_EVENTS = ['load', 'error', 'transform'];
8
9
  export class DataFrameComponent extends BaseComponent {
@@ -24,12 +25,16 @@ export class DataFrameComponent extends BaseComponent {
24
25
  this._storageKey = null;
25
26
  this._pendingSource = null;
26
27
  this._inlineUpload = null;
28
+ this._showStatus = true;
29
+ this._icon = '';
27
30
  this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
31
+ this._showStatus = options.showStatus ?? true;
32
+ this._icon = options.icon ?? '';
28
33
  this._tableOptions = {
29
34
  striped: options.striped ?? true,
30
35
  hoverable: options.hoverable ?? true,
31
36
  sortable: options.sortable ?? true,
32
- filterable: options.filterable ?? true,
37
+ filterable: options.filterable ?? false, // defer until data loaded
33
38
  paginated: options.paginated ?? true,
34
39
  rowsPerPage: options.rowsPerPage ?? 25
35
40
  };
@@ -39,18 +44,17 @@ export class DataFrameComponent extends BaseComponent {
39
44
  /* ═══════════════════════════════════════════════════
40
45
  * DATA SOURCES
41
46
  * ═══════════════════════════════════════════════════ */
42
- /**
43
- * Load from IndexedDB by storage key
44
- */
45
47
  fromStorage(key) {
46
48
  this._storageKey = key;
47
- this._pendingSource = async () => {
49
+ const loadFn = async () => {
48
50
  this.state.loading = true;
51
+ this._updateStatus('⏳ Loading...', 'loading');
49
52
  try {
50
- let df = await this._driver.loadByName(key);
53
+ const df = await this._driver.loadByName(key);
51
54
  if (!df) {
52
55
  this._triggerCallback('error', 'No table found with key: ' + key, null, this);
53
56
  this.state.loading = false;
57
+ this._updateStatus('No table found: ' + key, 'empty');
54
58
  return;
55
59
  }
56
60
  this._setDataFrame(df, 'storage: ' + key);
@@ -58,42 +62,44 @@ export class DataFrameComponent extends BaseComponent {
58
62
  catch (err) {
59
63
  this._triggerCallback('error', err.message, null, this);
60
64
  this.state.loading = false;
65
+ this._updateStatus('❌ ' + err.message, 'error');
61
66
  }
62
67
  };
68
+ if (this._table) {
69
+ loadFn();
70
+ }
71
+ else {
72
+ this._pendingSource = loadFn;
73
+ }
63
74
  return this;
64
75
  }
65
- /**
66
- * Load from a FileUpload component — auto-wires change event
67
- */
68
76
  fromUpload(upload) {
69
77
  this._uploadRef = upload;
70
78
  this._pendingSource = async () => {
71
- // Wire upload's change event to parse incoming files
72
79
  upload.bind('change', async (files) => {
73
80
  if (!files || files.length === 0)
74
81
  return;
75
82
  const file = files[0];
76
83
  this.state.loading = true;
84
+ this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
77
85
  try {
78
86
  const df = await this._driver.streamFile(file);
79
- // Auto-persist to IndexedDB
80
87
  await this._driver.store(file.name, df, { source: file.name });
81
- this._setDataFrame(df, 'upload: ' + file.name);
88
+ this._setDataFrame(df, file.name);
82
89
  }
83
90
  catch (err) {
84
91
  this._triggerCallback('error', err.message, null, this);
85
92
  this.state.loading = false;
93
+ this._updateStatus('❌ ' + err.message, 'error');
86
94
  }
87
95
  });
88
96
  };
89
97
  return this;
90
98
  }
91
- /**
92
- * Load from raw data — array of objects or Record<string, any[]>
93
- */
94
99
  fromData(data) {
95
- this._pendingSource = async () => {
100
+ const loadFn = async () => {
96
101
  this.state.loading = true;
102
+ this._updateStatus('⏳ Loading data...', 'loading');
97
103
  try {
98
104
  const df = new DataFrame(data);
99
105
  this._setDataFrame(df, 'inline data');
@@ -101,27 +107,37 @@ export class DataFrameComponent extends BaseComponent {
101
107
  catch (err) {
102
108
  this._triggerCallback('error', err.message, null, this);
103
109
  this.state.loading = false;
110
+ this._updateStatus('❌ ' + err.message, 'error');
104
111
  }
105
112
  };
113
+ if (this._table) {
114
+ loadFn();
115
+ }
116
+ else {
117
+ this._pendingSource = loadFn;
118
+ }
106
119
  return this;
107
120
  }
108
121
  /**
109
- * Add an inline file upload control above the table.
110
- * Auto-wires parsing, storage, and display zero callbacks needed.
111
- *
112
- * @param label - Button/label text (default: 'Upload File')
122
+ * Add an inline file upload control.
123
+ * @param label - Button label (default: 'Upload File')
113
124
  * @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
125
+ * @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
114
126
  */
115
- withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls') {
116
- this._inlineUpload = { label, accept };
127
+ withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls', icon = 'upload') {
128
+ this._inlineUpload = { label, accept, icon };
117
129
  return this;
118
130
  }
119
131
  /* ═══════════════════════════════════════════════════
120
- * TRANSFORM API (returns new DataFrameComponent view)
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
140
  * ═══════════════════════════════════════════════════ */
122
- /**
123
- * Apply a transform to the current DataFrame and update the table
124
- */
125
141
  apply(fn) {
126
142
  if (!this._df)
127
143
  return this;
@@ -130,78 +146,38 @@ export class DataFrameComponent extends BaseComponent {
130
146
  this._triggerCallback('transform', result, null, this);
131
147
  return this;
132
148
  }
133
- /**
134
- * Filter rows
135
- */
136
149
  filter(predicate) {
137
150
  return this.apply(df => df.filter(predicate));
138
151
  }
139
- /**
140
- * Select columns
141
- */
142
152
  select(...cols) {
143
153
  return this.apply(df => df.select(...cols));
144
154
  }
145
- /**
146
- * Sort by column
147
- */
148
155
  sort(col, descending) {
149
156
  return this.apply(df => df.sort(col, descending));
150
157
  }
151
- /**
152
- * Show first N rows
153
- */
154
158
  head(n = 5) {
155
159
  return this.apply(df => df.head(n));
156
160
  }
157
- /**
158
- * Show last N rows
159
- */
160
161
  tail(n = 5) {
161
162
  return this.apply(df => df.tail(n));
162
163
  }
163
- /**
164
- * Add a computed column
165
- */
166
164
  withColumn(name, fn) {
167
165
  return this.apply(df => df.withColumn(name, fn));
168
166
  }
169
- /**
170
- * Where clause
171
- */
172
167
  where(col, op, value) {
173
168
  return this.apply(df => df.where(col, op, value));
174
169
  }
175
170
  /* ═══════════════════════════════════════════════════
176
171
  * ACCESSORS
177
172
  * ═══════════════════════════════════════════════════ */
178
- /** Get the underlying DataFrame */
179
173
  get df() { return this._df; }
180
- /** Get the underlying TabularDriver */
181
174
  get driver() { return this._driver; }
182
- /** Get the internal Table component */
183
175
  get table() { return this._table; }
184
- /** Get describe() stats */
185
- describe() {
186
- return this._df?.describe() ?? null;
187
- }
188
- /** Export to CSV string */
189
- toCSV(delimiter) {
190
- return this._df?.toCSV(delimiter) ?? '';
191
- }
192
- /** Export to row objects */
193
- toRows() {
194
- return this._df?.toRows() ?? [];
195
- }
196
- /** Get shape */
197
- get shape() {
198
- return this._df?.shape ?? [0, 0];
199
- }
200
- /** Get column names */
201
- get columns() {
202
- return this._df?.columns ?? [];
203
- }
204
- /** Save current DataFrame to IndexedDB */
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 ?? []; }
205
181
  async save(key) {
206
182
  if (!this._df)
207
183
  return null;
@@ -220,10 +196,26 @@ export class DataFrameComponent extends BaseComponent {
220
196
  /* ═══════════════════════════════════════════════════
221
197
  * INTERNAL
222
198
  * ═══════════════════════════════════════════════════ */
223
- _updateStatus(text) {
199
+ _updateStatus(text, type = 'empty') {
224
200
  const el = document.getElementById(`${this._id}-status`);
225
- if (el)
226
- el.textContent = text;
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);
227
219
  }
228
220
  _setDataFrame(df, sourceName) {
229
221
  this._df = df;
@@ -237,7 +229,11 @@ export class DataFrameComponent extends BaseComponent {
237
229
  this._df = df.select(...cleanCols);
238
230
  }
239
231
  this._updateTable();
240
- this._updateStatus(`✅ ${sourceName} — ${this._df.height} rows × ${this._df.width} cols`);
232
+ this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols`, 'success');
233
+ // Enable filter now that data exists
234
+ if (this._tableOptions.filterable) {
235
+ this._showFilterInput();
236
+ }
241
237
  this._triggerCallback('load', this._df, null, this);
242
238
  }
243
239
  _updateTable() {
@@ -245,6 +241,49 @@ export class DataFrameComponent extends BaseComponent {
245
241
  return;
246
242
  this._table.columns(this._df.columns).rows(this._df.toRows());
247
243
  }
244
+ _showFilterInput() {
245
+ const wrapper = document.getElementById(this._id);
246
+ if (!wrapper)
247
+ return;
248
+ // Only add once
249
+ if (wrapper.querySelector('.jux-dataframe-filter'))
250
+ return;
251
+ const filterContainer = document.createElement('div');
252
+ filterContainer.className = 'jux-dataframe-filter';
253
+ const input = document.createElement('input');
254
+ input.type = 'text';
255
+ input.placeholder = 'Filter rows...';
256
+ input.className = 'jux-input-element jux-dataframe-filter-input';
257
+ const iconEl = renderIcon('search');
258
+ iconEl.style.width = '16px';
259
+ iconEl.style.height = '16px';
260
+ const iconWrap = document.createElement('span');
261
+ iconWrap.className = 'jux-dataframe-filter-icon';
262
+ iconWrap.appendChild(iconEl);
263
+ filterContainer.appendChild(iconWrap);
264
+ filterContainer.appendChild(input);
265
+ // Insert before the table
266
+ const tableWrapper = wrapper.querySelector('.jux-table-wrapper');
267
+ if (tableWrapper) {
268
+ wrapper.insertBefore(filterContainer, tableWrapper);
269
+ }
270
+ else {
271
+ wrapper.appendChild(filterContainer);
272
+ }
273
+ input.addEventListener('input', () => {
274
+ if (!this._df)
275
+ return;
276
+ const text = input.value.toLowerCase();
277
+ if (!text) {
278
+ this._table?.rows(this._df.toRows());
279
+ return;
280
+ }
281
+ const filtered = this._df.filter((row) => {
282
+ return Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text));
283
+ });
284
+ this._table?.rows(filtered.toRows());
285
+ });
286
+ }
248
287
  update(prop, value) { }
249
288
  /* ═══════════════════════════════════════════════════
250
289
  * RENDER
@@ -259,13 +298,16 @@ export class DataFrameComponent extends BaseComponent {
259
298
  wrapper.className += ` ${className}`;
260
299
  if (style)
261
300
  wrapper.setAttribute('style', style);
262
- // Inline upload (if withUpload was called)
301
+ // Inline upload
263
302
  if (this._inlineUpload) {
264
- const upload = new FileUpload(`${this._id}-upload`, {
303
+ const uploadOpts = {
265
304
  label: this._inlineUpload.label,
266
- accept: this._inlineUpload.accept
267
- });
268
- // Wire it as a fromUpload source
305
+ accept: this._inlineUpload.accept,
306
+ };
307
+ if (this._inlineUpload.icon) {
308
+ uploadOpts.icon = this._inlineUpload.icon;
309
+ }
310
+ const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
269
311
  this._uploadRef = upload;
270
312
  this._pendingSource = async () => {
271
313
  upload.bind('change', async (files) => {
@@ -273,7 +315,7 @@ export class DataFrameComponent extends BaseComponent {
273
315
  return;
274
316
  const file = files[0];
275
317
  this.state.loading = true;
276
- this._updateStatus('⏳ Parsing ' + file.name + '...');
318
+ this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
277
319
  try {
278
320
  const df = await this._driver.streamFile(file);
279
321
  await this._driver.store(file.name, df, { source: file.name });
@@ -282,11 +324,10 @@ export class DataFrameComponent extends BaseComponent {
282
324
  catch (err) {
283
325
  this._triggerCallback('error', err.message, null, this);
284
326
  this.state.loading = false;
285
- this._updateStatus('❌ ' + err.message);
327
+ this._updateStatus('❌ ' + err.message, 'error');
286
328
  }
287
329
  });
288
330
  };
289
- // Render upload into wrapper before status/table
290
331
  const uploadContainer = document.createElement('div');
291
332
  uploadContainer.className = 'jux-dataframe-upload';
292
333
  uploadContainer.id = `${this._id}-upload-container`;
@@ -297,19 +338,20 @@ export class DataFrameComponent extends BaseComponent {
297
338
  else {
298
339
  container.appendChild(wrapper);
299
340
  }
300
- // Status bar
301
- const statusBar = document.createElement('div');
302
- statusBar.className = 'jux-dataframe-status';
303
- statusBar.id = `${this._id}-status`;
304
- statusBar.style.cssText = 'font-size:13px;color:#888;margin-bottom:8px;';
305
- statusBar.textContent = 'No data loaded.';
306
- wrapper.appendChild(statusBar);
307
- // Create internal table
341
+ // Status bar (conditional)
342
+ if (this._showStatus) {
343
+ const statusBar = document.createElement('div');
344
+ statusBar.className = 'jux-dataframe-status jux-dataframe-status-empty';
345
+ statusBar.id = `${this._id}-status`;
346
+ statusBar.textContent = 'No data loaded.';
347
+ wrapper.appendChild(statusBar);
348
+ }
349
+ // Table — filterable is false initially; we enable it after data loads
308
350
  const tbl = new Table(`${this._id}-table`, {
309
351
  striped: this._tableOptions.striped,
310
352
  hoverable: this._tableOptions.hoverable,
311
353
  sortable: this._tableOptions.sortable,
312
- filterable: this._tableOptions.filterable,
354
+ filterable: false, // we handle filtering ourselves
313
355
  paginated: this._tableOptions.paginated,
314
356
  rowsPerPage: this._tableOptions.rowsPerPage
315
357
  });
@@ -3,6 +3,7 @@ import { DataFrame } from '../storage/DataFrame.js';
3
3
  import { TabularDriver } from '../storage/TabularDriver.js';
4
4
  import { FileUpload } from './fileupload.js';
5
5
  import { Table } from './table.js';
6
+ import { renderIcon } from './icons.js';
6
7
 
7
8
  const TRIGGER_EVENTS = [] as const;
8
9
  const CALLBACK_EVENTS = ['load', 'error', 'transform'] as const;
@@ -16,6 +17,8 @@ export interface DataFrameOptions {
16
17
  filterable?: boolean;
17
18
  paginated?: boolean;
18
19
  rowsPerPage?: number;
20
+ showStatus?: boolean;
21
+ icon?: string;
19
22
  style?: string;
20
23
  class?: string;
21
24
  }
@@ -42,7 +45,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
42
45
  private _uploadRef: FileUpload | null = null;
43
46
  private _storageKey: string | null = null;
44
47
  private _pendingSource: (() => Promise<void>) | null = null;
45
- 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 = '';
46
51
 
47
52
  constructor(id: string, options: DataFrameOptions = {}) {
48
53
  super(id, {
@@ -62,11 +67,14 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
62
67
  options.storeName ?? 'frames'
63
68
  );
64
69
 
70
+ this._showStatus = options.showStatus ?? true;
71
+ this._icon = options.icon ?? '';
72
+
65
73
  this._tableOptions = {
66
74
  striped: options.striped ?? true,
67
75
  hoverable: options.hoverable ?? true,
68
76
  sortable: options.sortable ?? true,
69
- filterable: options.filterable ?? true,
77
+ filterable: options.filterable ?? false, // defer until data loaded
70
78
  paginated: options.paginated ?? true,
71
79
  rowsPerPage: options.rowsPerPage ?? 25
72
80
  };
@@ -79,91 +87,98 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
79
87
  * DATA SOURCES
80
88
  * ═══════════════════════════════════════════════════ */
81
89
 
82
- /**
83
- * Load from IndexedDB by storage key
84
- */
85
90
  fromStorage(key: string): this {
86
91
  this._storageKey = key;
87
- this._pendingSource = async () => {
92
+ const loadFn = async () => {
88
93
  this.state.loading = true;
94
+ this._updateStatus('⏳ Loading...', 'loading');
89
95
  try {
90
- let df = await this._driver.loadByName(key);
96
+ const df = await this._driver.loadByName(key);
91
97
  if (!df) {
92
98
  this._triggerCallback('error', 'No table found with key: ' + key, null, this);
93
99
  this.state.loading = false;
100
+ this._updateStatus('No table found: ' + key, 'empty');
94
101
  return;
95
102
  }
96
103
  this._setDataFrame(df, 'storage: ' + key);
97
104
  } catch (err: any) {
98
105
  this._triggerCallback('error', err.message, null, this);
99
106
  this.state.loading = false;
107
+ this._updateStatus('❌ ' + err.message, 'error');
100
108
  }
101
109
  };
110
+ if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
102
111
  return this;
103
112
  }
104
113
 
105
- /**
106
- * Load from a FileUpload component — auto-wires change event
107
- */
108
114
  fromUpload(upload: FileUpload): this {
109
115
  this._uploadRef = upload;
110
116
  this._pendingSource = async () => {
111
- // Wire upload's change event to parse incoming files
112
117
  upload.bind('change', async (files: File[]) => {
113
118
  if (!files || files.length === 0) return;
114
119
  const file = files[0];
115
120
  this.state.loading = true;
116
-
121
+ this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
117
122
  try {
118
123
  const df = await this._driver.streamFile(file);
119
- // Auto-persist to IndexedDB
120
124
  await this._driver.store(file.name, df, { source: file.name });
121
- this._setDataFrame(df, 'upload: ' + file.name);
125
+ this._setDataFrame(df, file.name);
122
126
  } catch (err: any) {
123
127
  this._triggerCallback('error', err.message, null, this);
124
128
  this.state.loading = false;
129
+ this._updateStatus('❌ ' + err.message, 'error');
125
130
  }
126
131
  });
127
132
  };
128
133
  return this;
129
134
  }
130
135
 
131
- /**
132
- * Load from raw data — array of objects or Record<string, any[]>
133
- */
134
136
  fromData(data: Record<string, any>[] | Record<string, any[]>): this {
135
- this._pendingSource = async () => {
137
+ const loadFn = async () => {
136
138
  this.state.loading = true;
139
+ this._updateStatus('⏳ Loading data...', 'loading');
137
140
  try {
138
141
  const df = new DataFrame(data);
139
142
  this._setDataFrame(df, 'inline data');
140
143
  } catch (err: any) {
141
144
  this._triggerCallback('error', err.message, null, this);
142
145
  this.state.loading = false;
146
+ this._updateStatus('❌ ' + err.message, 'error');
143
147
  }
144
148
  };
149
+ if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
145
150
  return this;
146
151
  }
147
152
 
148
153
  /**
149
- * Add an inline file upload control above the table.
150
- * Auto-wires parsing, storage, and display zero callbacks needed.
151
- *
152
- * @param label - Button/label text (default: 'Upload File')
154
+ * Add an inline file upload control.
155
+ * @param label - Button label (default: 'Upload File')
153
156
  * @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
157
+ * @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
154
158
  */
155
- withUpload(label: string = 'Upload File', accept: string = '.csv,.tsv,.txt,.xlsx,.xls'): this {
156
- this._inlineUpload = { label, accept };
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 };
157
165
  return this;
158
166
  }
159
167
 
160
168
  /* ═══════════════════════════════════════════════════
161
- * TRANSFORM API (returns new DataFrameComponent view)
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
180
  * ═══════════════════════════════════════════════════ */
163
181
 
164
- /**
165
- * Apply a transform to the current DataFrame and update the table
166
- */
167
182
  apply(fn: (df: DataFrame) => DataFrame): this {
168
183
  if (!this._df) return this;
169
184
  const result = fn(this._df);
@@ -172,51 +187,30 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
172
187
  return this;
173
188
  }
174
189
 
175
- /**
176
- * Filter rows
177
- */
178
190
  filter(predicate: (row: Record<string, any>, index: number) => boolean): this {
179
191
  return this.apply(df => df.filter(predicate));
180
192
  }
181
193
 
182
- /**
183
- * Select columns
184
- */
185
194
  select(...cols: string[]): this {
186
195
  return this.apply(df => df.select(...cols));
187
196
  }
188
197
 
189
- /**
190
- * Sort by column
191
- */
192
198
  sort(col: string, descending?: boolean): this {
193
199
  return this.apply(df => df.sort(col, descending));
194
200
  }
195
201
 
196
- /**
197
- * Show first N rows
198
- */
199
202
  head(n: number = 5): this {
200
203
  return this.apply(df => df.head(n));
201
204
  }
202
205
 
203
- /**
204
- * Show last N rows
205
- */
206
206
  tail(n: number = 5): this {
207
207
  return this.apply(df => df.tail(n));
208
208
  }
209
209
 
210
- /**
211
- * Add a computed column
212
- */
213
210
  withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this {
214
211
  return this.apply(df => df.withColumn(name, fn));
215
212
  }
216
213
 
217
- /**
218
- * Where clause
219
- */
220
214
  where(col: string, op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', value: any): this {
221
215
  return this.apply(df => df.where(col, op, value));
222
216
  }
@@ -225,41 +219,15 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
225
219
  * ACCESSORS
226
220
  * ═══════════════════════════════════════════════════ */
227
221
 
228
- /** Get the underlying DataFrame */
229
222
  get df(): DataFrame | null { return this._df; }
230
-
231
- /** Get the underlying TabularDriver */
232
223
  get driver(): TabularDriver { return this._driver; }
233
-
234
- /** Get the internal Table component */
235
224
  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 ?? []; }
236
230
 
237
- /** Get describe() stats */
238
- describe(): Record<string, any> | null {
239
- return this._df?.describe() ?? null;
240
- }
241
-
242
- /** Export to CSV string */
243
- toCSV(delimiter?: string): string {
244
- return this._df?.toCSV(delimiter) ?? '';
245
- }
246
-
247
- /** Export to row objects */
248
- toRows(): Record<string, any>[] {
249
- return this._df?.toRows() ?? [];
250
- }
251
-
252
- /** Get shape */
253
- get shape(): [number, number] {
254
- return this._df?.shape ?? [0, 0];
255
- }
256
-
257
- /** Get column names */
258
- get columns(): string[] {
259
- return this._df?.columns ?? [];
260
- }
261
-
262
- /** Save current DataFrame to IndexedDB */
263
231
  async save(key?: string): Promise<string | null> {
264
232
  if (!this._df) return null;
265
233
  const name = key ?? this._storageKey ?? this._id;
@@ -281,9 +249,28 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
281
249
  * INTERNAL
282
250
  * ═══════════════════════════════════════════════════ */
283
251
 
284
- private _updateStatus(text: string): void {
252
+ private _updateStatus(text: string, type: 'loading' | 'success' | 'error' | 'empty' = 'empty'): void {
285
253
  const el = document.getElementById(`${this._id}-status`);
286
- if (el) el.textContent = text;
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
274
  }
288
275
 
289
276
  private _setDataFrame(df: DataFrame, sourceName: string): void {
@@ -300,7 +287,16 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
300
287
  }
301
288
 
302
289
  this._updateTable();
303
- this._updateStatus(`✅ ${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols`);
290
+ this._updateStatus(
291
+ `${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols`,
292
+ 'success'
293
+ );
294
+
295
+ // Enable filter now that data exists
296
+ if (this._tableOptions.filterable) {
297
+ this._showFilterInput();
298
+ }
299
+
304
300
  this._triggerCallback('load', this._df, null, this);
305
301
  }
306
302
 
@@ -309,6 +305,55 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
309
305
  this._table.columns(this._df.columns).rows(this._df.toRows());
310
306
  }
311
307
 
308
+ private _showFilterInput(): void {
309
+ const wrapper = document.getElementById(this._id);
310
+ if (!wrapper) return;
311
+ // Only add once
312
+ if (wrapper.querySelector('.jux-dataframe-filter')) return;
313
+
314
+ const filterContainer = document.createElement('div');
315
+ filterContainer.className = 'jux-dataframe-filter';
316
+
317
+ const input = document.createElement('input');
318
+ input.type = 'text';
319
+ input.placeholder = 'Filter rows...';
320
+ input.className = 'jux-input-element jux-dataframe-filter-input';
321
+
322
+ const iconEl = renderIcon('search');
323
+ iconEl.style.width = '16px';
324
+ iconEl.style.height = '16px';
325
+
326
+ const iconWrap = document.createElement('span');
327
+ iconWrap.className = 'jux-dataframe-filter-icon';
328
+ iconWrap.appendChild(iconEl);
329
+
330
+ filterContainer.appendChild(iconWrap);
331
+ filterContainer.appendChild(input);
332
+
333
+ // Insert before the table
334
+ const tableWrapper = wrapper.querySelector('.jux-table-wrapper');
335
+ if (tableWrapper) {
336
+ wrapper.insertBefore(filterContainer, tableWrapper);
337
+ } else {
338
+ wrapper.appendChild(filterContainer);
339
+ }
340
+
341
+ input.addEventListener('input', () => {
342
+ if (!this._df) return;
343
+ const text = input.value.toLowerCase();
344
+ if (!text) {
345
+ this._table?.rows(this._df.toRows());
346
+ return;
347
+ }
348
+ const filtered = this._df.filter((row) => {
349
+ return Object.values(row).some(v =>
350
+ v !== null && v !== undefined && String(v).toLowerCase().includes(text)
351
+ );
352
+ });
353
+ this._table?.rows(filtered.toRows());
354
+ });
355
+ }
356
+
312
357
  update(prop: string, value: any): void { }
313
358
 
314
359
  /* ═══════════════════════════════════════════════════
@@ -325,22 +370,25 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
325
370
  if (className) wrapper.className += ` ${className}`;
326
371
  if (style) wrapper.setAttribute('style', style);
327
372
 
328
- // Inline upload (if withUpload was called)
373
+ // Inline upload
329
374
  if (this._inlineUpload) {
330
- const upload = new FileUpload(`${this._id}-upload`, {
375
+ const uploadOpts: any = {
331
376
  label: this._inlineUpload.label,
332
- accept: this._inlineUpload.accept
333
- });
377
+ accept: this._inlineUpload.accept,
378
+ };
379
+ if (this._inlineUpload.icon) {
380
+ uploadOpts.icon = this._inlineUpload.icon;
381
+ }
382
+
383
+ const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
334
384
 
335
- // Wire it as a fromUpload source
336
385
  this._uploadRef = upload;
337
386
  this._pendingSource = async () => {
338
387
  upload.bind('change', async (files: File[]) => {
339
388
  if (!files || files.length === 0) return;
340
389
  const file = files[0];
341
390
  this.state.loading = true;
342
- this._updateStatus('⏳ Parsing ' + file.name + '...');
343
-
391
+ this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
344
392
  try {
345
393
  const df = await this._driver.streamFile(file);
346
394
  await this._driver.store(file.name, df, { source: file.name });
@@ -348,12 +396,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
348
396
  } catch (err: any) {
349
397
  this._triggerCallback('error', err.message, null, this);
350
398
  this.state.loading = false;
351
- this._updateStatus('❌ ' + err.message);
399
+ this._updateStatus('❌ ' + err.message, 'error');
352
400
  }
353
401
  });
354
402
  };
355
403
 
356
- // Render upload into wrapper before status/table
357
404
  const uploadContainer = document.createElement('div');
358
405
  uploadContainer.className = 'jux-dataframe-upload';
359
406
  uploadContainer.id = `${this._id}-upload-container`;
@@ -364,20 +411,21 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
364
411
  container.appendChild(wrapper);
365
412
  }
366
413
 
367
- // Status bar
368
- const statusBar = document.createElement('div');
369
- statusBar.className = 'jux-dataframe-status';
370
- statusBar.id = `${this._id}-status`;
371
- statusBar.style.cssText = 'font-size:13px;color:#888;margin-bottom:8px;';
372
- statusBar.textContent = 'No data loaded.';
373
- wrapper.appendChild(statusBar);
414
+ // Status bar (conditional)
415
+ if (this._showStatus) {
416
+ const statusBar = document.createElement('div');
417
+ statusBar.className = 'jux-dataframe-status jux-dataframe-status-empty';
418
+ statusBar.id = `${this._id}-status`;
419
+ statusBar.textContent = 'No data loaded.';
420
+ wrapper.appendChild(statusBar);
421
+ }
374
422
 
375
- // Create internal table
423
+ // Table filterable is false initially; we enable it after data loads
376
424
  const tbl = new Table(`${this._id}-table`, {
377
425
  striped: this._tableOptions.striped,
378
426
  hoverable: this._tableOptions.hoverable,
379
427
  sortable: this._tableOptions.sortable,
380
- filterable: this._tableOptions.filterable,
428
+ filterable: false, // we handle filtering ourselves
381
429
  paginated: this._tableOptions.paginated,
382
430
  rowsPerPage: this._tableOptions.rowsPerPage
383
431
  });
@@ -392,7 +440,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
392
440
  }
393
441
 
394
442
  this._wireStandardEvents(wrapper);
395
-
396
443
  return this;
397
444
  }
398
445
  }
@@ -1103,4 +1103,358 @@ button.jux-button:active,
1103
1103
 
1104
1104
  .dark .jux-menu-chevron {
1105
1105
  color: #a1a1aa;
1106
+ }
1107
+
1108
+ /* ═══════════════════════════════════════════════════════════════════
1109
+ * DATAFRAME COMPONENT
1110
+ * ═══════════════════════════════════════════════════════════════════ */
1111
+
1112
+ .jux-dataframe {
1113
+ display: flex;
1114
+ flex-direction: column;
1115
+ gap: 0.75rem;
1116
+ width: 100%;
1117
+ }
1118
+
1119
+ .jux-dataframe-upload {
1120
+ margin-bottom: 0.25rem;
1121
+ }
1122
+
1123
+ .jux-dataframe-status {
1124
+ display: flex;
1125
+ align-items: center;
1126
+ padding: 0.5rem 0.75rem;
1127
+ font-size: 0.8125rem;
1128
+ font-weight: 500;
1129
+ border-radius: 0.375rem;
1130
+ border: 1px solid #e4e4e7;
1131
+ background: #fafafa;
1132
+ color: #71717a;
1133
+ transition: all 0.2s ease;
1134
+ }
1135
+
1136
+ .jux-dataframe-status-success {
1137
+ background: #f0fdf4;
1138
+ border-color: #bbf7d0;
1139
+ color: #15803d;
1140
+ }
1141
+
1142
+ .jux-dataframe-status-loading {
1143
+ background: #fefce8;
1144
+ border-color: #fde68a;
1145
+ color: #a16207;
1146
+ }
1147
+
1148
+ .jux-dataframe-status-error {
1149
+ background: #fef2f2;
1150
+ border-color: #fecaca;
1151
+ color: #dc2626;
1152
+ }
1153
+
1154
+ .jux-dataframe-status-empty {
1155
+ background: #fafafa;
1156
+ border-color: #e4e4e7;
1157
+ color: #a1a1aa;
1158
+ }
1159
+
1160
+ /* DataFrame Filter */
1161
+ .jux-dataframe-filter {
1162
+ position: relative;
1163
+ display: flex;
1164
+ align-items: center;
1165
+ width: 100%;
1166
+ }
1167
+
1168
+ .jux-dataframe-filter-icon {
1169
+ position: absolute;
1170
+ left: 0.75rem;
1171
+ top: 50%;
1172
+ transform: translateY(-50%);
1173
+ color: #a1a1aa;
1174
+ pointer-events: none;
1175
+ display: flex;
1176
+ align-items: center;
1177
+ }
1178
+
1179
+ .jux-dataframe-filter-input {
1180
+ padding-left: 2.25rem !important;
1181
+ }
1182
+
1183
+ /* ═══════════════════════════════════════════════════════════════════
1184
+ * FILE UPLOAD (Shadcn styled)
1185
+ * ═══════════════════════════════════════════════════════════════════ */
1186
+
1187
+ .jux-fileupload {
1188
+ gap: 0.5rem;
1189
+ }
1190
+
1191
+ .jux-fileupload-button-container {
1192
+ display: flex;
1193
+ align-items: center;
1194
+ gap: 0.5rem;
1195
+ }
1196
+
1197
+ .jux-fileupload-button {
1198
+ display: inline-flex;
1199
+ align-items: center;
1200
+ justify-content: center;
1201
+ white-space: nowrap;
1202
+ border-radius: 0.375rem;
1203
+ font-size: 0.875rem;
1204
+ font-weight: 500;
1205
+ height: 2.5rem;
1206
+ padding: 0 1rem;
1207
+ cursor: pointer;
1208
+ transition: all 0.15s ease;
1209
+ border: 1px solid #e4e4e7;
1210
+ background: #ffffff;
1211
+ color: #09090b;
1212
+ }
1213
+
1214
+ .jux-fileupload-button:hover {
1215
+ background: #f4f4f5;
1216
+ color: #18181b;
1217
+ }
1218
+
1219
+ .jux-fileupload-button:disabled {
1220
+ pointer-events: none;
1221
+ opacity: 0.5;
1222
+ }
1223
+
1224
+ .jux-fileupload-icon {
1225
+ display: flex;
1226
+ align-items: center;
1227
+ color: #71717a;
1228
+ }
1229
+
1230
+ .jux-fileupload-list {
1231
+ display: flex;
1232
+ flex-direction: column;
1233
+ gap: 0.25rem;
1234
+ }
1235
+
1236
+ .jux-fileupload-item {
1237
+ display: flex;
1238
+ align-items: center;
1239
+ gap: 0.5rem;
1240
+ padding: 0.375rem 0.75rem;
1241
+ background: #f4f4f5;
1242
+ border: 1px solid #e4e4e7;
1243
+ border-radius: 0.375rem;
1244
+ font-size: 0.8125rem;
1245
+ color: #09090b;
1246
+ }
1247
+
1248
+ .jux-fileupload-filename {
1249
+ flex: 1;
1250
+ font-weight: 500;
1251
+ overflow: hidden;
1252
+ text-overflow: ellipsis;
1253
+ white-space: nowrap;
1254
+ }
1255
+
1256
+ .jux-fileupload-filesize {
1257
+ color: #a1a1aa;
1258
+ font-size: 0.75rem;
1259
+ flex-shrink: 0;
1260
+ }
1261
+
1262
+ .jux-fileupload-remove {
1263
+ background: transparent;
1264
+ border: none;
1265
+ color: #a1a1aa;
1266
+ cursor: pointer;
1267
+ font-size: 1rem;
1268
+ padding: 0 0.25rem;
1269
+ line-height: 1;
1270
+ border-radius: 0.25rem;
1271
+ transition: all 0.15s;
1272
+ }
1273
+
1274
+ .jux-fileupload-remove:hover {
1275
+ color: #ef4444;
1276
+ background: #fef2f2;
1277
+ }
1278
+
1279
+ /* ═══════════════════════════════════════════════════════════════════
1280
+ * TABLE ENHANCEMENTS
1281
+ * ═══════════════════════════════════════════════════════════════════ */
1282
+
1283
+ .jux-table-wrapper {
1284
+ width: 100%;
1285
+ overflow-x: auto;
1286
+ }
1287
+
1288
+ .jux-table-filter {
1289
+ display: flex;
1290
+ height: 2.5rem;
1291
+ width: 100%;
1292
+ border-radius: 0.375rem;
1293
+ border: 1px solid #e4e4e7;
1294
+ background: #ffffff;
1295
+ padding: 0.5rem 0.75rem;
1296
+ font-size: 0.875rem;
1297
+ color: #09090b;
1298
+ outline: none;
1299
+ transition: all 0.15s ease;
1300
+ box-sizing: border-box;
1301
+ margin-bottom: 0.75rem;
1302
+ }
1303
+
1304
+ .jux-table-filter::placeholder {
1305
+ color: #a1a1aa;
1306
+ }
1307
+
1308
+ .jux-table-filter:focus {
1309
+ border-color: #18181b;
1310
+ box-shadow: 0 0 0 2px #18181b, 0 0 0 4px rgba(24, 24, 27, 0.1);
1311
+ }
1312
+
1313
+ .jux-table-pagination {
1314
+ margin-top: 0.75rem;
1315
+ display: flex;
1316
+ gap: 0.375rem;
1317
+ justify-content: center;
1318
+ align-items: center;
1319
+ padding: 0.5rem 0;
1320
+ }
1321
+
1322
+ .jux-table-pagination button {
1323
+ display: inline-flex;
1324
+ align-items: center;
1325
+ justify-content: center;
1326
+ white-space: nowrap;
1327
+ border-radius: 0.375rem;
1328
+ font-size: 0.875rem;
1329
+ font-weight: 500;
1330
+ height: 2.25rem;
1331
+ padding: 0 0.875rem;
1332
+ cursor: pointer;
1333
+ transition: all 0.15s ease;
1334
+ border: 1px solid #e4e4e7;
1335
+ background: #ffffff;
1336
+ color: #09090b;
1337
+ }
1338
+
1339
+ .jux-table-pagination button:hover:not(:disabled) {
1340
+ background: #f4f4f5;
1341
+ color: #18181b;
1342
+ }
1343
+
1344
+ .jux-table-pagination button:disabled {
1345
+ pointer-events: none;
1346
+ opacity: 0.5;
1347
+ }
1348
+
1349
+ .jux-table-pagination span {
1350
+ font-size: 0.875rem;
1351
+ color: #71717a;
1352
+ padding: 0 0.75rem;
1353
+ }
1354
+
1355
+ /* Table hoverable rows */
1356
+ .jux-table-hoverable tbody tr {
1357
+ transition: background-color 0.1s ease;
1358
+ }
1359
+
1360
+ .jux-table-hoverable tbody tr:hover {
1361
+ background-color: #f4f4f5 !important;
1362
+ }
1363
+
1364
+ /* Selection highlight override for shadcn */
1365
+ .jux-table-row-selected {
1366
+ background-color: #eff6ff !important;
1367
+ }
1368
+
1369
+ .jux-table-row-selected:hover {
1370
+ background-color: #dbeafe !important;
1371
+ }
1372
+
1373
+ /* ═══════════════════════════════════════════════════════════════════
1374
+ * DARK MODE - DATAFRAME / FILEUPLOAD / TABLE
1375
+ * ═══════════════════════════════════════════════════════════════════ */
1376
+
1377
+ .dark .jux-dataframe-status {
1378
+ background: #18181b;
1379
+ border-color: #27272a;
1380
+ color: #a1a1aa;
1381
+ }
1382
+
1383
+ .dark .jux-dataframe-status-success {
1384
+ background: #052e16;
1385
+ border-color: #166534;
1386
+ color: #4ade80;
1387
+ }
1388
+
1389
+ .dark .jux-dataframe-status-loading {
1390
+ background: #422006;
1391
+ border-color: #a16207;
1392
+ color: #facc15;
1393
+ }
1394
+
1395
+ .dark .jux-dataframe-status-error {
1396
+ background: #450a0a;
1397
+ border-color: #991b1b;
1398
+ color: #f87171;
1399
+ }
1400
+
1401
+ .dark .jux-fileupload-button {
1402
+ border-color: #27272a;
1403
+ background: #09090b;
1404
+ color: #fafafa;
1405
+ }
1406
+
1407
+ .dark .jux-fileupload-button:hover {
1408
+ background: #27272a;
1409
+ }
1410
+
1411
+ .dark .jux-fileupload-item {
1412
+ background: #18181b;
1413
+ border-color: #27272a;
1414
+ color: #fafafa;
1415
+ }
1416
+
1417
+ .dark .jux-fileupload-filesize {
1418
+ color: #71717a;
1419
+ }
1420
+
1421
+ .dark .jux-table-filter {
1422
+ border-color: #27272a;
1423
+ background: #09090b;
1424
+ color: #fafafa;
1425
+ }
1426
+
1427
+ .dark .jux-table-filter::placeholder {
1428
+ color: #52525b;
1429
+ }
1430
+
1431
+ .dark .jux-table-filter:focus {
1432
+ border-color: #fafafa;
1433
+ box-shadow: 0 0 0 2px #fafafa, 0 0 0 4px rgba(250, 250, 250, 0.1);
1434
+ }
1435
+
1436
+ .dark .jux-table-pagination button {
1437
+ border-color: #27272a;
1438
+ background: #09090b;
1439
+ color: #fafafa;
1440
+ }
1441
+
1442
+ .dark .jux-table-pagination button:hover:not(:disabled) {
1443
+ background: #27272a;
1444
+ }
1445
+
1446
+ .dark .jux-table-pagination span {
1447
+ color: #71717a;
1448
+ }
1449
+
1450
+ .dark .jux-table-hoverable tbody tr:hover {
1451
+ background-color: #27272a !important;
1452
+ }
1453
+
1454
+ .dark .jux-table-row-selected {
1455
+ background-color: #172554 !important;
1456
+ }
1457
+
1458
+ .dark .jux-table-row-selected:hover {
1459
+ background-color: #1e3a5f !important;
1106
1460
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.166",
3
+ "version": "1.1.167",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",