juxscript 1.0.19 → 1.0.21

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.
Files changed (77) hide show
  1. package/bin/cli.js +121 -72
  2. package/lib/components/alert.ts +212 -165
  3. package/lib/components/badge.ts +93 -103
  4. package/lib/components/base/BaseComponent.ts +397 -0
  5. package/lib/components/base/FormInput.ts +322 -0
  6. package/lib/components/button.ts +63 -122
  7. package/lib/components/card.ts +109 -155
  8. package/lib/components/charts/areachart.ts +315 -0
  9. package/lib/components/charts/barchart.ts +421 -0
  10. package/lib/components/charts/doughnutchart.ts +263 -0
  11. package/lib/components/charts/lib/BaseChart.ts +402 -0
  12. package/lib/components/charts/lib/chart-types.ts +159 -0
  13. package/lib/components/charts/lib/chart-utils.ts +160 -0
  14. package/lib/components/charts/lib/chart.ts +707 -0
  15. package/lib/components/checkbox.ts +264 -127
  16. package/lib/components/code.ts +75 -108
  17. package/lib/components/container.ts +113 -130
  18. package/lib/components/data.ts +37 -5
  19. package/lib/components/datepicker.ts +195 -147
  20. package/lib/components/dialog.ts +187 -157
  21. package/lib/components/divider.ts +85 -191
  22. package/lib/components/docs-data.json +544 -2027
  23. package/lib/components/dropdown.ts +178 -136
  24. package/lib/components/element.ts +227 -171
  25. package/lib/components/fileupload.ts +285 -228
  26. package/lib/components/guard.ts +92 -0
  27. package/lib/components/heading.ts +46 -69
  28. package/lib/components/helpers.ts +13 -6
  29. package/lib/components/hero.ts +107 -95
  30. package/lib/components/icon.ts +160 -0
  31. package/lib/components/icons.ts +175 -0
  32. package/lib/components/include.ts +153 -5
  33. package/lib/components/input.ts +174 -374
  34. package/lib/components/kpicard.ts +16 -16
  35. package/lib/components/list.ts +378 -240
  36. package/lib/components/loading.ts +142 -211
  37. package/lib/components/menu.ts +103 -97
  38. package/lib/components/modal.ts +138 -144
  39. package/lib/components/nav.ts +169 -90
  40. package/lib/components/paragraph.ts +49 -150
  41. package/lib/components/progress.ts +118 -200
  42. package/lib/components/radio.ts +297 -149
  43. package/lib/components/script.ts +19 -87
  44. package/lib/components/select.ts +184 -186
  45. package/lib/components/sidebar.ts +152 -140
  46. package/lib/components/style.ts +19 -82
  47. package/lib/components/switch.ts +258 -188
  48. package/lib/components/table.ts +1117 -170
  49. package/lib/components/tabs.ts +162 -145
  50. package/lib/components/theme-toggle.ts +108 -169
  51. package/lib/components/tooltip.ts +86 -157
  52. package/lib/components/write.ts +108 -127
  53. package/lib/jux.ts +86 -41
  54. package/machinery/build.js +466 -0
  55. package/machinery/compiler.js +354 -105
  56. package/machinery/server.js +23 -100
  57. package/machinery/watcher.js +153 -130
  58. package/package.json +1 -2
  59. package/presets/base.css +1166 -0
  60. package/presets/notion.css +2 -1975
  61. package/lib/adapters/base-adapter.js +0 -35
  62. package/lib/adapters/index.js +0 -33
  63. package/lib/adapters/mysql-adapter.js +0 -65
  64. package/lib/adapters/postgres-adapter.js +0 -70
  65. package/lib/adapters/sqlite-adapter.js +0 -56
  66. package/lib/components/areachart.ts +0 -1246
  67. package/lib/components/areachartsmooth.ts +0 -1380
  68. package/lib/components/barchart.ts +0 -1250
  69. package/lib/components/chart.ts +0 -127
  70. package/lib/components/doughnutchart.ts +0 -1191
  71. package/lib/components/footer.ts +0 -165
  72. package/lib/components/header.ts +0 -187
  73. package/lib/components/layout.ts +0 -239
  74. package/lib/components/main.ts +0 -137
  75. package/lib/layouts/default.jux +0 -8
  76. package/lib/layouts/figma.jux +0 -0
  77. /package/lib/{themes → components/charts/lib}/charts.js +0 -0
@@ -1,302 +1,1249 @@
1
- import { getOrCreateContainer } from './helpers.js';
1
+ /* ═════════════════════════════════════════════════════════════════
2
+ * SECTION 1: DEFINITIONS
3
+ * Type definitions, constants, interfaces
4
+ * ═════════════════════════════════════════════════════════════════ */
2
5
 
3
- /**
4
- * Table column configuration
5
- */
6
- export interface TableColumn {
6
+ import { BaseComponent } from './base/BaseComponent.js';
7
+ import { State } from '../reactivity/state.js';
8
+
9
+ // Event definitions
10
+ const TRIGGER_EVENTS = [
11
+ 'rowClick', 'rowHover', 'cellClick',
12
+ 'selected', 'deselected', 'selectionChange'
13
+ ] as const;
14
+
15
+ const CALLBACK_EVENTS = [
16
+ 'sortChange', 'filterChange', 'pageChange', 'dataChange'
17
+ ] as const;
18
+
19
+ type TriggerEvent = typeof TRIGGER_EVENTS[number];
20
+ type CallbackEvent = typeof CALLBACK_EVENTS[number];
21
+
22
+ // Type definitions
23
+ export type SelectionBehaviorOnDataChange = 'clear' | 'preserve';
24
+ export type SelectionTrigger = 'row' | 'checkbox';
25
+
26
+ export interface ColumnDef {
27
+ key: string;
28
+ label: string;
29
+ width?: string;
30
+ render?: (value: any, row: any) => string | HTMLElement;
31
+ // ✨ NEW: Mark as computed column
32
+ computed?: boolean;
33
+ }
34
+
35
+ // ✨ NEW: Computed column definition
36
+ export interface ComputedColumnDef {
7
37
  key: string;
8
38
  label: string;
39
+ compute: (row: any, rowIndex: number) => any;
40
+ render?: (value: any, row: any, rowIndex: number) => string | HTMLElement;
9
41
  width?: string;
10
- align?: 'left' | 'center' | 'right';
11
- renderCell?: (value: any, row: any) => string | HTMLElement;
12
42
  }
13
43
 
14
- /**
15
- * Table component options
16
- */
17
44
  export interface TableOptions {
18
- columns?: TableColumn[];
19
- data?: any[];
45
+ columns?: (string | ColumnDef)[];
46
+ rows?: any[][];
47
+ headers?: boolean;
20
48
  striped?: boolean;
21
49
  hoverable?: boolean;
22
50
  bordered?: boolean;
23
- allowHtml?: boolean;
51
+ compact?: boolean;
52
+ sortable?: boolean;
53
+ filterable?: boolean;
54
+ paginated?: boolean;
55
+ rowsPerPage?: number;
56
+ selectable?: boolean;
57
+ multiSelect?: boolean;
58
+ showCheckboxes?: boolean;
59
+ showBulkCheckbox?: boolean;
60
+ selectionTrigger?: SelectionTrigger;
24
61
  style?: string;
25
62
  class?: string;
63
+ rowIdField?: string;
64
+ selectionBehavior?: SelectionBehaviorOnDataChange;
26
65
  }
27
66
 
28
- /**
29
- * Table component state
30
- */
31
67
  type TableState = {
32
- columns: TableColumn[];
33
- data: any[];
68
+ columns: ColumnDef[];
69
+ rows: any[][];
70
+ // ✨ NEW: Store computed column definitions
71
+ computedColumns: Map<string, ComputedColumnDef>;
72
+ headers: boolean;
34
73
  striped: boolean;
35
74
  hoverable: boolean;
36
75
  bordered: boolean;
37
- allowHtml: boolean;
76
+ compact: boolean;
77
+ sortable: boolean;
78
+ filterable: boolean;
79
+ paginated: boolean;
80
+ rowsPerPage: number;
81
+ currentPage: number;
82
+ sortColumn: string | null;
83
+ sortDirection: 'asc' | 'desc';
84
+ filterText: string;
85
+ selectable: boolean;
86
+ multiSelect: boolean;
87
+ showCheckboxes: boolean;
88
+ showBulkCheckbox: boolean;
89
+ selectionTrigger: SelectionTrigger;
90
+ selectedIndexes: Set<number>;
38
91
  style: string;
39
92
  class: string;
93
+ rowIdField?: string;
94
+ selectionBehavior: SelectionBehaviorOnDataChange;
40
95
  };
41
96
 
42
- /**
43
- * Table component
44
- *
45
- * Usage:
46
- * // Auto-generate columns from data
47
- * const table = jux.table('myTable', {
48
- * data: [
49
- * { name: 'Alice', age: 30 },
50
- * { name: 'Bob', age: 25 }
51
- * ],
52
- * striped: true,
53
- * allowHtml: true
54
- * });
55
- * table.render();
56
- *
57
- * // Or specify columns explicitly
58
- * const table = jux.table('myTable', {
59
- * columns: [
60
- * { key: 'name', label: 'Name' },
61
- * { key: 'age', label: 'Age', align: 'center' }
62
- * ],
63
- * data: [
64
- * { name: 'Alice', age: 30 },
65
- * { name: 'Bob', age: 25 }
66
- * ]
67
- * });
68
- * table.render();
69
- */
70
- export class Table {
71
- state: TableState;
72
- container: HTMLElement | null = null;
73
- _id: string;
74
- id: string;
97
+ /* ═════════════════════════════════════════════════════════════════
98
+ * SECTION 2: CONSTRUCTOR & STORAGE
99
+ * Class declaration, instance variables, initialization
100
+ * ═════════════════════════════════════════════════════════════════ */
101
+
102
+ export class Table extends BaseComponent<TableState> {
103
+ private _tableElement: HTMLTableElement | null = null;
75
104
 
76
105
  constructor(id: string, options: TableOptions = {}) {
77
- this._id = id;
78
- this.id = id;
106
+ const normalizedColumns = (options.columns ?? []).map(col =>
107
+ typeof col === 'string' ? { key: col, label: col } : col
108
+ );
79
109
 
80
- this.state = {
81
- columns: options.columns ?? [],
82
- data: options.data ?? [],
110
+ // Initialize base with state
111
+ super(id, {
112
+ columns: normalizedColumns,
113
+ rows: options.rows ?? [],
114
+ computedColumns: new Map(), // ✨ NEW: Initialize empty Map
115
+ headers: options.headers ?? true,
83
116
  striped: options.striped ?? false,
84
- hoverable: options.hoverable ?? true,
117
+ hoverable: options.hoverable ?? false,
85
118
  bordered: options.bordered ?? false,
86
- allowHtml: options.allowHtml ?? true,
119
+ compact: options.compact ?? false,
120
+ sortable: options.sortable ?? false,
121
+ filterable: options.filterable ?? false,
122
+ paginated: options.paginated ?? false,
123
+ rowsPerPage: options.rowsPerPage ?? 10,
124
+ currentPage: 1,
125
+ sortColumn: null,
126
+ sortDirection: 'asc',
127
+ filterText: '',
128
+ selectable: options.selectable ?? false,
129
+ multiSelect: options.multiSelect ?? false,
130
+ showCheckboxes: options.showCheckboxes ?? false,
131
+ showBulkCheckbox: options.showBulkCheckbox ?? false,
132
+ selectionTrigger: options.selectionTrigger ?? 'row',
133
+ selectedIndexes: new Set<number>(),
87
134
  style: options.style ?? '',
88
- class: options.class ?? ''
89
- };
135
+ class: options.class ?? '',
136
+ rowIdField: options.rowIdField,
137
+ selectionBehavior: options.selectionBehavior ?? 'clear'
138
+ });
139
+ }
140
+
141
+ /* ═════════════════════════════════════════════════════════════════
142
+ * ABSTRACT METHOD IMPLEMENTATIONS
143
+ * ═════════════════════════════════════════════════════════════════ */
144
+
145
+ protected getTriggerEvents(): readonly string[] {
146
+ return TRIGGER_EVENTS;
147
+ }
148
+
149
+ protected getCallbackEvents(): readonly string[] {
150
+ return CALLBACK_EVENTS;
151
+ }
152
+
153
+ /* ═════════════════════════════════════════════════════════════════
154
+ * SECTION 3: FLUENT API
155
+ * ═════════════════════════════════════════════════════════════════ */
156
+
157
+ // ✅ Inherited from BaseComponent: style(), class(), bind(), sync(), renderTo()
158
+
159
+ // Configuration methods
160
+ columns(value: (string | ColumnDef)[]): this {
161
+ this.state.columns = value.map(col =>
162
+ typeof col === 'string' ? { key: col, label: col } : col
163
+ );
164
+ return this;
165
+ }
166
+
167
+ rows(value: any[][]): this {
168
+ const previousRows = this.state.rows;
169
+ const hadSelections = this.state.selectedIndexes.size > 0;
170
+
171
+ // Handle selections based on behavior
172
+ if (this.state.selectionBehavior === 'preserve' && this.state.rowIdField) {
173
+ this._preserveSelections(previousRows, value);
174
+ } else {
175
+ this.state.selectedIndexes.clear();
176
+ }
177
+
178
+ this.state.rows = value;
179
+
180
+ // Auto-reset pagination if current page is now invalid
181
+ if (this.state.paginated) {
182
+ const totalPages = Math.ceil(value.length / this.state.rowsPerPage);
183
+ if (this.state.currentPage > totalPages && totalPages > 0) {
184
+ this.state.currentPage = totalPages;
185
+ }
186
+ if (totalPages === 0) {
187
+ this.state.currentPage = 1;
188
+ }
189
+ }
190
+
191
+ this._updateTable();
192
+
193
+ // Fire callbacks
194
+ this._triggerCallback('dataChange', value, previousRows);
195
+
196
+ if (hadSelections && this._triggerHandlers.has('selectionChange')) {
197
+ this._triggerHandlers.get('selectionChange')!(
198
+ this.getSelectedRows(),
199
+ this.getSelectedIndexes(),
200
+ new CustomEvent('dataChange')
201
+ );
202
+ }
203
+
204
+ return this;
90
205
  }
91
206
 
92
- /* -------------------------
93
- * Fluent API
94
- * ------------------------- */
207
+ /**
208
+ * Add a computed column that evaluates dynamically at render time
209
+ *
210
+ * @param key - Unique key for the column
211
+ * @param label - Display label in header
212
+ * @param compute - Function to compute value from row data
213
+ * @param render - Optional custom renderer for the computed value
214
+ * @param width - Optional column width
215
+ */
216
+ computedColumn(
217
+ key: string,
218
+ label: string,
219
+ compute: (row: any, rowIndex: number) => any,
220
+ render?: (value: any, row: any, rowIndex: number) => string | HTMLElement,
221
+ width?: string
222
+ ): this {
223
+ // Store computed column definition
224
+ this.state.computedColumns.set(key, {
225
+ key,
226
+ label,
227
+ compute,
228
+ render,
229
+ width
230
+ });
231
+
232
+ // Add to columns list if not already present
233
+ const existingColumn = this.state.columns.find(col => col.key === key);
234
+ if (!existingColumn) {
235
+ this.state.columns.push({
236
+ key,
237
+ label,
238
+ width,
239
+ computed: true,
240
+ render: render as any
241
+ });
242
+ }
243
+
244
+ // If already rendered, update table
245
+ if (this._tableElement) {
246
+ const tbody = this._tableElement.querySelector('tbody');
247
+ const thead = this._tableElement.querySelector('thead');
248
+
249
+ if (tbody && thead) {
250
+ // Rebuild header with new column
251
+ const table = this._tableElement;
252
+ table.innerHTML = '';
253
+ const newThead = this._buildTableHeader();
254
+ table.appendChild(newThead);
255
+
256
+ const newTbody = document.createElement('tbody');
257
+ this._renderTableBody(newTbody);
258
+ table.appendChild(newTbody);
259
+
260
+ // Re-wire events
261
+ this._wireTriggerEvents(newTbody);
262
+ }
263
+ }
95
264
 
96
- columns(value: TableColumn[]): this {
97
- this.state.columns = value;
98
265
  return this;
99
266
  }
100
267
 
101
- data(value: any[]): this {
102
- this.state.data = value;
268
+ /**
269
+ * Remove a computed column
270
+ */
271
+ removeComputedColumn(key: string): this {
272
+ this.state.computedColumns.delete(key);
273
+ this.state.columns = this.state.columns.filter(col => col.key !== key);
274
+
275
+ if (this._tableElement) {
276
+ const tbody = this._tableElement.querySelector('tbody');
277
+ const thead = this._tableElement.querySelector('thead');
278
+
279
+ if (tbody && thead) {
280
+ const table = this._tableElement;
281
+ table.innerHTML = '';
282
+ const newThead = this._buildTableHeader();
283
+ table.appendChild(newThead);
284
+
285
+ const newTbody = document.createElement('tbody');
286
+ this._renderTableBody(newTbody);
287
+ table.appendChild(newTbody);
288
+
289
+ this._wireTriggerEvents(newTbody);
290
+ }
291
+ }
292
+
103
293
  return this;
104
294
  }
105
295
 
296
+ // Visual options
297
+ headers(value: boolean): this {
298
+ this.state.headers = value;
299
+ return this;
300
+ }
106
301
  striped(value: boolean): this {
107
302
  this.state.striped = value;
108
303
  return this;
109
304
  }
110
-
111
305
  hoverable(value: boolean): this {
112
306
  this.state.hoverable = value;
113
307
  return this;
114
308
  }
115
-
116
309
  bordered(value: boolean): this {
117
310
  this.state.bordered = value;
118
311
  return this;
119
312
  }
313
+ compact(value: boolean): this {
314
+ this.state.compact = value;
315
+ return this;
316
+ }
120
317
 
121
- allowHtml(value: boolean): this {
122
- this.state.allowHtml = value;
318
+ // Feature toggles
319
+ sortable(value: boolean): this {
320
+ this.state.sortable = value;
321
+ return this;
322
+ }
323
+ filterable(value: boolean): this {
324
+ this.state.filterable = value;
325
+ return this;
326
+ }
327
+ paginated(value: boolean): this {
328
+ this.state.paginated = value;
329
+ return this;
330
+ }
331
+ rowsPerPage(value: number): this {
332
+ this.state.rowsPerPage = value;
123
333
  return this;
124
334
  }
125
335
 
126
- style(value: string): this {
127
- this.state.style = value;
336
+ // Selection configuration
337
+ selectable(value: boolean): this {
338
+ this.state.selectable = value;
128
339
  return this;
129
340
  }
341
+ multiSelect(value: boolean): this {
342
+ this.state.multiSelect = value;
343
+ if (value) {
344
+ this.state.selectable = true; // multi-select implies selectable
345
+ }
346
+ return this;
347
+ }
348
+ showCheckboxes(value: boolean): this {
349
+ this.state.showCheckboxes = value;
350
+
351
+ // If already rendered, update immediately
352
+ if (this._tableElement) {
353
+ const tbody = this._tableElement.querySelector('tbody');
354
+ const thead = this._tableElement.querySelector('thead');
355
+
356
+ if (tbody && thead) {
357
+ // Update header
358
+ this._updateHeaderCheckbox(thead);
359
+ // Re-render body with/without checkboxes
360
+ this._renderTableBody(tbody);
361
+ // Update bulk checkbox state
362
+ this._updateBulkCheckboxState();
363
+ }
364
+ }
130
365
 
131
- class(value: string): this {
132
- this.state.class = value;
133
366
  return this;
134
367
  }
368
+ showBulkCheckbox(value: boolean): this {
369
+ this.state.showBulkCheckbox = value;
370
+ if (value) {
371
+ this.state.multiSelect = true;
372
+ this.state.selectable = true;
373
+ this.state.showCheckboxes = true;
374
+ }
135
375
 
136
- /* -------------------------
137
- * Helpers
138
- * ------------------------- */
376
+ // If already rendered, update immediately
377
+ if (this._tableElement) {
378
+ const thead = this._tableElement.querySelector('thead');
379
+ const tbody = this._tableElement.querySelector('tbody');
139
380
 
140
- /**
141
- * Auto-generate columns from data
142
- */
143
- private _autoGenerateColumns(data: any[]): TableColumn[] {
144
- if (!data || data.length === 0) {
145
- return [];
381
+ if (thead && tbody) {
382
+ this._updateHeaderCheckbox(thead);
383
+ this._renderTableBody(tbody);
384
+ this._updateBulkCheckboxState();
385
+ }
146
386
  }
147
387
 
148
- const firstRow = data[0];
149
- const keys = Object.keys(firstRow);
388
+ return this;
389
+ }
390
+ selectionTrigger(value: SelectionTrigger): this {
391
+ this.state.selectionTrigger = value;
150
392
 
151
- return keys.map(key => ({
152
- key,
153
- label: this._formatLabel(key)
154
- }));
393
+ // If already rendered, update cursor style
394
+ if (this._tableElement) {
395
+ const tbody = this._tableElement.querySelector('tbody');
396
+ if (tbody) {
397
+ if (this.state.selectionTrigger === 'row' && this.state.selectable) {
398
+ tbody.style.cursor = 'pointer';
399
+ } else {
400
+ tbody.style.cursor = '';
401
+ }
402
+ }
403
+ }
404
+
405
+ return this;
155
406
  }
156
407
 
157
- /**
158
- * Format column label from key
159
- * Examples:
160
- * 'name' -> 'Name'
161
- * 'firstName' -> 'First Name'
162
- * 'user_id' -> 'User Id'
163
- */
164
- private _formatLabel(key: string): string {
165
- const words = key
166
- .replace(/([A-Z])/g, ' $1')
167
- .replace(/_/g, ' ')
168
- .trim()
169
- .split(/\s+/);
408
+ // Selection actions
409
+ selectAll(): this {
410
+ this.state.rows.forEach((_, index) => {
411
+ this.state.selectedIndexes.add(index);
412
+ });
413
+ this._updateRowSelectionUI();
170
414
 
171
- return words
172
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
173
- .join(' ');
415
+ // Fire selectionChange
416
+ if (this._triggerHandlers.has('selectionChange')) {
417
+ this._triggerHandlers.get('selectionChange')!(
418
+ this.getSelectedRows(),
419
+ this.getSelectedIndexes(),
420
+ new CustomEvent('bulkSelect')
421
+ );
422
+ }
423
+
424
+ return this;
174
425
  }
426
+ deselectAll(): this {
427
+ this.state.selectedIndexes.clear();
428
+ this._updateRowSelectionUI();
429
+
430
+ // Fire selectionChange
431
+ if (this._triggerHandlers.has('selectionChange')) {
432
+ this._triggerHandlers.get('selectionChange')!(
433
+ [],
434
+ [],
435
+ new CustomEvent('bulkDeselect')
436
+ );
437
+ }
175
438
 
176
- /* -------------------------
177
- * Render
178
- * ------------------------- */
439
+ return this;
440
+ }
441
+ clearSelection(): this {
442
+ this.state.selectedIndexes.clear();
443
+ this._updateRowSelectionUI();
444
+ return this;
445
+ }
446
+ selectRows(indexes: number[]): this {
447
+ if (!this.state.multiSelect) {
448
+ this.state.selectedIndexes.clear();
449
+ }
450
+ indexes.forEach(index => {
451
+ if (index >= 0 && index < this.state.rows.length) {
452
+ this.state.selectedIndexes.add(index);
453
+ }
454
+ });
455
+ this._updateRowSelectionUI();
456
+ return this;
457
+ }
458
+ deselectRows(indexes: number[]): this {
459
+ indexes.forEach(index => this.state.selectedIndexes.delete(index));
460
+ this._updateRowSelectionUI();
461
+ return this;
462
+ }
463
+
464
+ // Public utilities
465
+ getSelectedIndexes(): number[] {
466
+ return Array.from(this.state.selectedIndexes);
467
+ }
468
+ getSelectedRows(): any[] {
469
+ return Array.from(this.state.selectedIndexes).map(index => this.state.rows[index]);
470
+ }
471
+
472
+ /* ═════════════════════════════════════════════════════════════════
473
+ * SECTION 5: RENDER LIFECYCLE
474
+ * Main render method and BUILD phase helpers
475
+ * ═════════════════════════════════════════════════════════════════ */
179
476
 
180
477
  render(targetId?: string): this {
181
- let container: HTMLElement;
478
+ const container = this._setupContainer(targetId);
479
+ const wrapper = this._buildWrapper();
480
+ const table = this._buildTable(wrapper);
481
+ const tbody = table.querySelector('tbody')!;
482
+ this._wireAllEvents(wrapper, tbody);
483
+ container.appendChild(wrapper);
484
+ this._tableElement = table;
485
+ return this;
486
+ }
182
487
 
488
+ // Step 1: SETUP
489
+ protected _setupContainer(targetId?: string): HTMLElement {
490
+ let container: HTMLElement;
183
491
  if (targetId) {
184
492
  const target = document.querySelector(targetId);
185
493
  if (!target || !(target instanceof HTMLElement)) {
186
- throw new Error(`Table: Target element "${targetId}" not found`);
494
+ throw new Error(`Table: Target "${targetId}" not found`);
187
495
  }
188
496
  container = target;
189
497
  } else {
190
- container = getOrCreateContainer(this._id);
498
+ // Inline getOrCreateContainer functionality
499
+ let element = document.getElementById(this._id);
500
+ if (!element) {
501
+ element = document.createElement('div');
502
+ element.id = this._id;
503
+ document.body.appendChild(element);
504
+ }
505
+ container = element;
191
506
  }
192
-
193
507
  this.container = container;
194
- let { columns, data, striped, hoverable, bordered, allowHtml, style, class: className } = this.state;
508
+ return container;
509
+ }
195
510
 
196
- // Auto-generate columns if not provided
197
- if (columns.length === 0 && data.length > 0) {
198
- columns = this._autoGenerateColumns(data);
199
- }
511
+ // Step 2: BUILD wrapper
512
+ private _buildWrapper(): HTMLElement {
513
+ const { style, class: className } = this.state;
200
514
 
201
515
  const wrapper = document.createElement('div');
202
516
  wrapper.className = 'jux-table-wrapper';
203
517
  wrapper.id = this._id;
518
+ if (className) wrapper.className += ` ${className}`;
519
+ if (style) wrapper.setAttribute('style', style);
204
520
 
205
- if (className) {
206
- wrapper.className += ` ${className}`;
521
+ if (this.state.selectable) {
522
+ const selectionStyles = document.createElement('style');
523
+ selectionStyles.textContent = `
524
+ .jux-table-row-selected { background-color: #e3f2fd !important; }
525
+ .jux-table-row-selected:hover { background-color: #bbdefb !important; }
526
+ `;
527
+ wrapper.appendChild(selectionStyles);
207
528
  }
208
529
 
209
- if (style) {
210
- wrapper.setAttribute('style', style);
530
+ if (this.state.filterable) {
531
+ wrapper.appendChild(this._buildFilterInput());
211
532
  }
212
533
 
534
+ return wrapper;
535
+ }
536
+
537
+ private _buildFilterInput(): HTMLInputElement {
538
+ const input = document.createElement('input');
539
+ input.type = 'text';
540
+ input.placeholder = 'Filter...';
541
+
542
+ input.className = 'jux-table-filter';
543
+ input.style.cssText = 'margin-bottom: 10px; padding: 5px; width: 100%;';
544
+
545
+ // Add event listener to handle filtering
546
+ input.addEventListener('input', (e) => {
547
+ const target = e.target as HTMLInputElement;
548
+ this.state.filterText = target.value;
549
+
550
+ // Re-render table body
551
+ const tbody = this._tableElement?.querySelector('tbody');
552
+ if (tbody) {
553
+ this._renderTableBody(tbody);
554
+
555
+ // Update pagination if enabled
556
+ const wrapper = this._tableElement?.closest('.jux-table-wrapper') as HTMLElement;
557
+ if (wrapper && this.state.paginated) {
558
+ // Reset to page 1 when filtering
559
+ this.state.currentPage = 1;
560
+ this._updatePagination(wrapper, tbody);
561
+ }
562
+ }
563
+
564
+ // Fire callback
565
+ this._triggerCallback('filterChange', target.value, e);
566
+ });
567
+
568
+ return input;
569
+ }
570
+
571
+ // Step 3: BUILD table
572
+ private _buildTable(wrapper: HTMLElement): HTMLTableElement {
573
+ const { striped, hoverable, bordered } = this.state;
213
574
  const table = document.createElement('table');
214
575
  table.className = 'jux-table';
215
-
216
576
  if (striped) table.classList.add('jux-table-striped');
217
577
  if (hoverable) table.classList.add('jux-table-hoverable');
218
578
  if (bordered) table.classList.add('jux-table-bordered');
579
+ // Build and append header
580
+ if (this.state.headers) {
581
+ const thead = this._buildTableHeader();
582
+ table.appendChild(thead);
583
+ }
584
+ // Build and append body
585
+ const tbody = document.createElement('tbody');
586
+ this._renderTableBody(tbody);
587
+ table.appendChild(tbody);
588
+ wrapper.appendChild(table);
589
+ return table;
590
+ }
219
591
 
220
- // Table header
592
+ private _buildTableHeader(): HTMLTableSectionElement {
221
593
  const thead = document.createElement('thead');
222
594
  const headerRow = document.createElement('tr');
223
595
 
224
- columns.forEach(col => {
225
- const th = document.createElement('th');
226
- th.textContent = col.label;
227
- if (col.width) th.style.width = col.width;
228
- if (col.align) th.style.textAlign = col.align;
596
+ // Add bulk checkbox or empty checkbox column
597
+ if (this.state.showBulkCheckbox) {
598
+ headerRow.appendChild(this._buildBulkCheckboxCell());
599
+ } else if (this.state.showCheckboxes) {
600
+ const emptyTh = document.createElement('th');
601
+ emptyTh.style.width = '40px';
602
+ headerRow.appendChild(emptyTh);
603
+ }
604
+
605
+ // Add column headers with optional sort
606
+ this.state.columns.forEach(col => {
607
+ const th = this._buildColumnHeader(col);
229
608
  headerRow.appendChild(th);
230
609
  });
231
610
 
232
611
  thead.appendChild(headerRow);
233
- table.appendChild(thead);
612
+ return thead;
613
+ }
234
614
 
235
- // Table body
236
- const tbody = document.createElement('tbody');
615
+ private _buildBulkCheckboxCell(): HTMLTableCellElement {
616
+ const bulkTh = document.createElement('th');
617
+ bulkTh.style.width = '40px';
618
+ bulkTh.style.textAlign = 'center';
237
619
 
238
- data.forEach(row => {
239
- const tr = document.createElement('tr');
620
+ const bulkCheckbox = document.createElement('input');
621
+ bulkCheckbox.type = 'checkbox';
622
+ bulkCheckbox.className = 'jux-bulk-checkbox';
623
+ bulkCheckbox.style.cursor = 'pointer';
624
+ bulkCheckbox.title = 'Select all';
240
625
 
241
- columns.forEach(col => {
242
- const td = document.createElement('td');
243
- if (col.align) td.style.textAlign = col.align;
244
-
245
- const cellValue = row[col.key] ?? '';
246
-
247
- // Custom render function takes precedence
248
- if (col.renderCell && typeof col.renderCell === 'function') {
249
- const rendered = col.renderCell(cellValue, row);
250
- if (rendered instanceof HTMLElement) {
251
- td.appendChild(rendered);
252
- } else if (typeof rendered === 'string') {
253
- if (allowHtml) {
254
- td.innerHTML = rendered;
626
+ bulkCheckbox.addEventListener('change', (e) => {
627
+ if (bulkCheckbox.checked) {
628
+ this.selectAll();
629
+ } else {
630
+ this.deselectAll();
631
+ }
632
+ });
633
+
634
+ bulkTh.appendChild(bulkCheckbox);
635
+ return bulkTh;
636
+ }
637
+
638
+ private _buildColumnHeader(col: ColumnDef): HTMLTableCellElement {
639
+ const th = document.createElement('th');
640
+ th.textContent = col.label;
641
+ th.setAttribute('data-column-key', col.key);
642
+ if (col.width) th.style.width = col.width;
643
+
644
+ if (this.state.sortable) {
645
+ th.style.cursor = 'pointer';
646
+ th.style.userSelect = 'none';
647
+
648
+ th.addEventListener('click', (e) => {
649
+ // Update state
650
+ if (this.state.sortColumn === col.key) {
651
+ this.state.sortDirection = this.state.sortDirection === 'asc' ? 'desc' : 'asc';
652
+ } else {
653
+ this.state.sortColumn = col.key;
654
+ this.state.sortDirection = 'asc';
655
+ }
656
+
657
+ // Re-render
658
+ const tbody = this._tableElement!.querySelector('tbody')!;
659
+ this._renderTableBody(tbody);
660
+
661
+ // Update UI indicators
662
+ const headerRow = th.parentElement!;
663
+ headerRow.querySelectorAll('th[data-column-key]').forEach(h => {
664
+ h.textContent = h.textContent?.replace(' ▲', '').replace(' ▼', '') || '';
665
+ });
666
+ th.textContent = col.label + (this.state.sortDirection === 'asc' ? ' ▲' : ' ▼');
667
+
668
+ // Fire callback
669
+ this._triggerCallback('sortChange', col.key, this.state.sortDirection, e);
670
+ });
671
+ }
672
+
673
+ return th;
674
+ }
675
+
676
+ // Step 4: WIRE orchestrator
677
+ private _wireAllEvents(wrapper: HTMLElement, tbody: HTMLTableSectionElement): void {
678
+ this._wireTriggerEvents(tbody);
679
+
680
+ if (this.state.paginated) {
681
+ this._updatePagination(wrapper, tbody);
682
+ }
683
+
684
+ this._wireStandardEvents(wrapper); // ✅ Use inherited method
685
+ this._wireSyncBindings(wrapper, tbody);
686
+ }
687
+
688
+ /* ═════════════════════════════════════════════════════════════════
689
+ * SECTION 6: EVENT WIRING
690
+ * WIRE phase - connects storage to actual DOM listeners/subscriptions
691
+ * ═════════════════════════════════════════════════════════════════ */
692
+
693
+ private _wireTriggerEvents(tbody: HTMLTableSectionElement): void {
694
+ // === rowClick: Fire immediately when row is clicked ===
695
+ if (this._triggerHandlers.has('rowClick')) {
696
+ const handler = this._triggerHandlers.get('rowClick')!;
697
+ tbody.addEventListener('click', (e) => {
698
+ const tr = (e.target as HTMLElement).closest('tr');
699
+ if (tr && tbody.contains(tr)) {
700
+ const rowIndex = Array.from(tbody.children).indexOf(tr);
701
+ let rows = this._getFilteredRows();
702
+ rows = this._getSortedRows(rows);
703
+ rows = this._getPaginatedRows(rows);
704
+ const rowData = rows[rowIndex];
705
+ if (rowData) {
706
+ handler(rowData, rowIndex, e);
707
+ }
708
+ }
709
+ });
710
+ tbody.style.cursor = 'pointer';
711
+ }
712
+ // === cellClick: Fire immediately when cell is clicked ===
713
+ if (this._triggerHandlers.has('cellClick')) {
714
+ const handler = this._triggerHandlers.get('cellClick')!;
715
+ tbody.addEventListener('click', (e) => {
716
+ const td = (e.target as HTMLElement).closest('td');
717
+ if (td) {
718
+ const tr = td.closest('tr');
719
+ if (tr && tbody.contains(tr)) {
720
+ const rowIndex = Array.from(tbody.children).indexOf(tr);
721
+ const cellIndex = Array.from(tr.children).indexOf(td);
722
+ let rows = this._getFilteredRows();
723
+ rows = this._getSortedRows(rows);
724
+ rows = this._getPaginatedRows(rows);
725
+ const rowData = rows[rowIndex];
726
+ const columnKey = this.state.columns[cellIndex]?.key;
727
+ const cellValue = rowData?.[columnKey];
728
+ if (rowData && columnKey) {
729
+ handler(cellValue, rowData, columnKey, rowIndex, cellIndex, e);
730
+ }
731
+ }
732
+ }
733
+ });
734
+ }
735
+ // === Selection events: Fire with internal logic ===
736
+ if (this.state.selectable) {
737
+ tbody.addEventListener('click', (e) => {
738
+ const target = e.target as HTMLElement;
739
+
740
+ // Check if click was on checkbox
741
+ const isCheckboxClick = target.tagName === 'INPUT' && target.getAttribute('type') === 'checkbox';
742
+
743
+ // If trigger is 'checkbox' and click wasn't on checkbox, ignore
744
+ if (this.state.selectionTrigger === 'checkbox' && !isCheckboxClick) {
745
+ return;
746
+ }
747
+
748
+ // ✨ FIX: If trigger is 'row' and click WAS on checkbox, toggle it manually
749
+ if (this.state.selectionTrigger === 'row' && isCheckboxClick) {
750
+ e.preventDefault(); // Prevent default checkbox behavior
751
+ const checkbox = target as HTMLInputElement;
752
+ checkbox.checked = !checkbox.checked; // ✨ Toggle the checkbox
753
+ }
754
+
755
+ const tr = target.closest('tr');
756
+ if (tr && tbody.contains(tr)) {
757
+ const visualRowIndex = Array.from(tbody.children).indexOf(tr);
758
+ let rows = this._getFilteredRows();
759
+ rows = this._getSortedRows(rows);
760
+ const actualRowIndex = this.state.rows.indexOf(rows[
761
+ (this.state.currentPage - 1) * this.state.rowsPerPage + visualRowIndex
762
+ ]);
763
+
764
+ if (actualRowIndex === -1) return;
765
+ const wasSelected = this.state.selectedIndexes.has(actualRowIndex);
766
+
767
+ if (this.state.multiSelect) {
768
+ // Toggle selection
769
+ if (wasSelected) {
770
+ this.state.selectedIndexes.delete(actualRowIndex);
771
+ tr.classList.remove('jux-table-row-selected');
772
+
773
+ // Update checkbox if present
774
+ const checkbox = tr.querySelector('input[type="checkbox"]') as HTMLInputElement;
775
+ if (checkbox) checkbox.checked = false;
776
+
777
+ if (this._triggerHandlers.has('deselected')) {
778
+ this._triggerHandlers.get('deselected')!(this.state.rows[actualRowIndex], actualRowIndex, e);
779
+ }
255
780
  } else {
256
- td.textContent = rendered;
781
+ this.state.selectedIndexes.add(actualRowIndex);
782
+ tr.classList.add('jux-table-row-selected');
783
+
784
+ // Update checkbox if present
785
+ const checkbox = tr.querySelector('input[type="checkbox"]') as HTMLInputElement;
786
+ if (checkbox) checkbox.checked = true;
787
+
788
+ if (this._triggerHandlers.has('selected')) {
789
+ this._triggerHandlers.get('selected')!(this.state.rows[actualRowIndex], actualRowIndex, e);
790
+ }
791
+ }
792
+ } else {
793
+ // Single select
794
+ const previousSelection = Array.from(this.state.selectedIndexes);
795
+ this.state.selectedIndexes.clear();
796
+ tbody.querySelectorAll('tr').forEach(row => {
797
+ row.classList.remove('jux-table-row-selected');
798
+ const checkbox = row.querySelector('input[type="checkbox"]') as HTMLInputElement;
799
+ if (checkbox) checkbox.checked = false;
800
+ });
801
+
802
+ if (!wasSelected) {
803
+ this.state.selectedIndexes.add(actualRowIndex);
804
+ tr.classList.add('jux-table-row-selected');
805
+
806
+ // Update checkbox if present
807
+ const checkbox = tr.querySelector('input[type="checkbox"]') as HTMLInputElement;
808
+ if (checkbox) checkbox.checked = true;
809
+
810
+ if (this._triggerHandlers.has('selected')) {
811
+ this._triggerHandlers.get('selected')!(this.state.rows[actualRowIndex], actualRowIndex, e);
812
+ }
813
+ }
814
+
815
+ if (this._triggerHandlers.has('deselected') && previousSelection.length > 0) {
816
+ previousSelection.forEach(idx => {
817
+ if (idx !== actualRowIndex) {
818
+ this._triggerHandlers.get('deselected')!(this.state.rows[idx], idx, e);
819
+ }
820
+ });
257
821
  }
258
822
  }
823
+
824
+ // Update bulk checkbox state if present
825
+ this._updateBulkCheckboxState();
826
+
827
+ // Fire selectionChange
828
+ if (this._triggerHandlers.has('selectionChange')) {
829
+ this._triggerHandlers.get('selectionChange')!(this.getSelectedRows(), this.getSelectedIndexes(), e);
830
+ }
831
+ }
832
+ });
833
+
834
+ // Only set pointer cursor if row is trigger
835
+ if (this.state.selectionTrigger === 'row') {
836
+ tbody.style.cursor = 'pointer';
837
+ }
838
+ }
839
+ }
840
+
841
+ private _wireSyncBindings(wrapper: HTMLElement, tbody: HTMLTableSectionElement): void {
842
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
843
+ if (property === 'rows' || property === 'data') {
844
+ this._wireSyncRows(stateObj, toComponent, tbody, wrapper);
845
+ } else if (property === 'columns') {
846
+ this._wireSyncColumns(stateObj, toComponent, wrapper, tbody);
847
+ }
848
+ });
849
+ }
850
+
851
+ private _wireSyncRows(
852
+ stateObj: State<any>,
853
+ toComponent: Function | undefined,
854
+ tbody: HTMLTableSectionElement,
855
+ wrapper: HTMLElement
856
+ ): void {
857
+ const transform = toComponent || ((v: any) => v);
858
+ stateObj.subscribe((val: any) => {
859
+ const previousRows = this.state.rows;
860
+ const transformed = transform(val);
861
+ // Handle selections
862
+ const hadSelections = this.state.selectedIndexes.size > 0;
863
+ if (this.state.selectionBehavior === 'preserve' && this.state.rowIdField) {
864
+ this._preserveSelections(previousRows, transformed);
865
+ } else {
866
+ const isAppend = transformed.length > previousRows.length;
867
+ if (isAppend) {
868
+ const maxValidIndex = Math.min(previousRows.length, transformed.length) - 1;
869
+ const validSelections = new Set<number>();
870
+ this.state.selectedIndexes.forEach(idx => {
871
+ if (idx <= maxValidIndex) validSelections.add(idx);
872
+ });
873
+ this.state.selectedIndexes = validSelections;
259
874
  } else {
260
- // Default rendering
261
- if (allowHtml && typeof cellValue === 'string' && cellValue.includes('<')) {
262
- td.innerHTML = cellValue;
875
+ this.state.selectedIndexes.clear();
876
+ }
877
+ }
878
+ this.state.rows = transformed;
879
+ // Auto-reset pagination
880
+ if (this.state.paginated) {
881
+ const totalPages = Math.ceil(transformed.length / this.state.rowsPerPage);
882
+ if (this.state.currentPage > totalPages && totalPages > 0) {
883
+ this.state.currentPage = totalPages;
884
+ }
885
+ if (totalPages === 0) {
886
+ this.state.currentPage = 1;
887
+ }
888
+ }
889
+ // Re-render
890
+ this._renderTableBody(tbody);
891
+ if (this.state.paginated) {
892
+ this._updatePagination(wrapper, tbody);
893
+ }
894
+ // Fire callbacks
895
+ this._triggerCallback('dataChange', transformed, previousRows);
896
+ if (hadSelections || this.state.selectedIndexes.size > 0) {
897
+ this._triggerHandlers.get('selectionChange')?.(this.getSelectedRows(), this.getSelectedIndexes(), new CustomEvent('dataChange'));
898
+ }
899
+ });
900
+ }
901
+
902
+ private _wireSyncColumns(
903
+ stateObj: State<any>,
904
+ toComponent: Function | undefined,
905
+ wrapper: HTMLElement,
906
+ tbody: HTMLTableSectionElement
907
+ ): void {
908
+ const transform = toComponent || ((v: any) => v);
909
+ stateObj.subscribe((val: any) => {
910
+ const transformed = transform(val);
911
+ this.state.columns = transformed;
912
+ // Full re-render needed for columns
913
+ const table = this._tableElement!;
914
+ table.innerHTML = '';
915
+ const thead = this._buildTableHeader();
916
+ table.appendChild(thead);
917
+ const newTbody = document.createElement('tbody');
918
+ this._renderTableBody(newTbody);
919
+ table.appendChild(newTbody);
920
+ // Re-wire events
921
+ this._wireTriggerEvents(newTbody);
922
+ if (this.state.paginated) {
923
+ this._updatePagination(wrapper, newTbody);
924
+ }
925
+ });
926
+ }
927
+
928
+ /* ═════════════════════════════════════════════════════════════════
929
+ * SECTION 7: INTERNAL HELPERS
930
+ * Data processing, DOM updates, selection management
931
+ * ═════════════════════════════════════════════════════════════════ */
932
+
933
+ // Data processing
934
+ private _getFilteredRows(): any[][] {
935
+ if (!this.state.filterText) return this.state.rows;
936
+ const searchText = this.state.filterText.toLowerCase();
937
+ return this.state.rows.filter(row => {
938
+ return this.state.columns.some(col => {
939
+ const value = row[col.key];
940
+ return String(value).toLowerCase().includes(searchText);
941
+ });
942
+ });
943
+ }
944
+
945
+ private _getSortedRows(rows: any[][]): any[][] {
946
+ if (!this.state.sortColumn) return rows;
947
+
948
+ // ✨ Check if sorting by computed column
949
+ const computedDef = this.state.computedColumns.get(this.state.sortColumn);
950
+
951
+ const sorted = [...rows].sort((a, b) => {
952
+ let aVal: any;
953
+ let bVal: any;
954
+
955
+ if (computedDef) {
956
+ // ✨ Compute values on the fly for computed columns
957
+ const aIndex = this.state.rows.indexOf(a);
958
+ const bIndex = this.state.rows.indexOf(b);
959
+ aVal = computedDef.compute(a, aIndex);
960
+ bVal = computedDef.compute(b, bIndex);
961
+ } else {
962
+ // Normal column: get value from row data
963
+ aVal = a[this.state.sortColumn!];
964
+ bVal = b[this.state.sortColumn!];
965
+ }
966
+
967
+ // Standard comparison logic
968
+ if (aVal === bVal) return 0;
969
+ if (aVal == null) return 1;
970
+ if (bVal == null) return -1;
971
+
972
+ const comparison = aVal < bVal ? -1 : 1;
973
+ return this.state.sortDirection === 'asc' ? comparison : -comparison;
974
+ });
975
+
976
+ return sorted;
977
+ }
978
+
979
+ private _getPaginatedRows(rows: any[][]): any[][] {
980
+ if (!this.state.paginated) return rows;
981
+ const start = (this.state.currentPage - 1) * this.state.rowsPerPage;
982
+ const end = start + this.state.rowsPerPage;
983
+ return rows.slice(start, end);
984
+ }
985
+
986
+ // DOM updates
987
+ private _updateTable(): void {
988
+ if (!this._tableElement) return;
989
+ const tbody = this._tableElement.querySelector('tbody');
990
+ if (!tbody) return;
991
+ // Re-render using the same logic as initial render
992
+ this._renderTableBody(tbody);
993
+ // Update pagination if enabled
994
+ const wrapper = this._tableElement.closest('.jux-table-wrapper');
995
+ if (wrapper && this.state.paginated) {
996
+ this._updatePagination(wrapper as HTMLElement, tbody);
997
+ }
998
+ }
999
+
1000
+ private _renderTableBody(tbody: HTMLTableSectionElement): void {
1001
+ tbody.innerHTML = '';
1002
+ let rows = this._getFilteredRows();
1003
+ rows = this._getSortedRows(rows);
1004
+ const totalPages = Math.ceil(rows.length / this.state.rowsPerPage);
1005
+ rows = this._getPaginatedRows(rows);
1006
+
1007
+ rows.forEach((row, visualIndex) => {
1008
+ const tr = document.createElement('tr');
1009
+
1010
+ // Check if this row is selected
1011
+ const actualRowIndex = this.state.rows.indexOf(row);
1012
+ const isSelected = this.state.selectedIndexes.has(actualRowIndex);
1013
+ if (isSelected) {
1014
+ tr.classList.add('jux-table-row-selected');
1015
+ }
1016
+
1017
+ // Add checkbox column if enabled
1018
+ if (this.state.showCheckboxes) {
1019
+ const checkboxTd = document.createElement('td');
1020
+ checkboxTd.style.width = '40px';
1021
+ checkboxTd.style.textAlign = 'center';
1022
+ const checkbox = document.createElement('input');
1023
+ checkbox.type = 'checkbox';
1024
+ checkbox.checked = isSelected;
1025
+ checkbox.style.cursor = 'pointer';
1026
+ checkboxTd.appendChild(checkbox);
1027
+ tr.appendChild(checkboxTd);
1028
+ }
1029
+ // Add data columns (including computed columns)
1030
+ this.state.columns.forEach(col => {
1031
+ const td = document.createElement('td');
1032
+
1033
+ // ✨ NEW: Check if this is a computed column
1034
+ const computedDef = this.state.computedColumns.get(col.key);
1035
+
1036
+ let cellValue: any;
1037
+ let rendered: string | HTMLElement;
1038
+
1039
+ if (computedDef) {
1040
+ // ✨ Computed column: Evaluate compute function
1041
+ cellValue = computedDef.compute(row, actualRowIndex);
1042
+
1043
+ // Use computed column's custom renderer if provided
1044
+ if (computedDef.render) {
1045
+ rendered = computedDef.render(cellValue, row, actualRowIndex);
263
1046
  } else {
264
- td.textContent = String(cellValue);
1047
+ // Default: stringify the computed value
1048
+ rendered = cellValue != null ? String(cellValue) : '';
1049
+ }
1050
+ } else {
1051
+ // Normal column: Get value from row data
1052
+ cellValue = row[col.key];
1053
+
1054
+ // Use column's render function if provided
1055
+ if (col.render) {
1056
+ rendered = col.render(cellValue, row);
1057
+ } else {
1058
+ rendered = cellValue != null ? String(cellValue) : '';
265
1059
  }
266
1060
  }
267
1061
 
1062
+ // Insert rendered content
1063
+ if (typeof rendered === 'string') {
1064
+ td.innerHTML = rendered;
1065
+ } else {
1066
+ td.appendChild(rendered);
1067
+ }
1068
+
268
1069
  tr.appendChild(td);
269
1070
  });
270
1071
 
271
1072
  tbody.appendChild(tr);
272
1073
  });
1074
+ }
273
1075
 
274
- table.appendChild(tbody);
275
- wrapper.appendChild(table);
276
- container.appendChild(wrapper);
1076
+ private _updateHeaderCheckbox(thead: HTMLTableSectionElement): void {
1077
+ const headerRow = thead.querySelector('tr');
1078
+ if (!headerRow) return;
1079
+ // Remove existing checkbox column(s)
1080
+ const existingCheckboxThs = headerRow.querySelectorAll('th:first-child');
1081
+ existingCheckboxThs.forEach(th => {
1082
+ if (th.querySelector('.jux-bulk-checkbox') || th.textContent === '') {
1083
+ th.remove();
1084
+ }
1085
+ });
1086
+ // Add bulk checkbox or empty checkbox column
1087
+ if (this.state.showBulkCheckbox) {
1088
+ const bulkTh = document.createElement('th');
1089
+ bulkTh.style.width = '40px';
1090
+ bulkTh.style.textAlign = 'center';
1091
+ const bulkCheckbox = document.createElement('input');
1092
+ bulkCheckbox.type = 'checkbox';
1093
+ bulkCheckbox.className = 'jux-bulk-checkbox';
1094
+ bulkCheckbox.style.cursor = 'pointer';
1095
+ bulkCheckbox.title = 'Select all';
1096
+ bulkCheckbox.addEventListener('change', (e) => {
1097
+ if (bulkCheckbox.checked) {
1098
+ this.selectAll();
1099
+ } else {
1100
+ this.deselectAll();
1101
+ }
1102
+ });
1103
+ bulkTh.appendChild(bulkCheckbox);
1104
+ headerRow.insertBefore(bulkTh, headerRow.firstChild);
1105
+ } else if (this.state.showCheckboxes) {
1106
+ // Add empty header cell for checkbox column (no bulk select)
1107
+ const checkboxTh = document.createElement('th');
1108
+ checkboxTh.style.width = '40px';
1109
+ headerRow.insertBefore(checkboxTh, headerRow.firstChild);
1110
+ }
1111
+ }
277
1112
 
278
- return this;
1113
+ private _updateBulkCheckboxState(): void {
1114
+ if (!this.state.showBulkCheckbox || !this._tableElement) return;
1115
+ const bulkCheckbox = this._tableElement.querySelector('.jux-bulk-checkbox') as HTMLInputElement;
1116
+ if (!bulkCheckbox) return;
1117
+ const totalRows = this.state.rows.length;
1118
+ const selectedRows = this.state.selectedIndexes.size;
1119
+ if (selectedRows === 0) {
1120
+ bulkCheckbox.checked = false;
1121
+ bulkCheckbox.indeterminate = false;
1122
+ } else if (selectedRows === totalRows) {
1123
+ bulkCheckbox.checked = true;
1124
+ bulkCheckbox.indeterminate = false;
1125
+ } else {
1126
+ bulkCheckbox.checked = false;
1127
+ bulkCheckbox.indeterminate = true;
1128
+ }
279
1129
  }
280
1130
 
281
- /**
282
- * Render to another Jux component's container
283
- */
284
- renderTo(juxComponent: any): this {
285
- if (!juxComponent || typeof juxComponent !== 'object') {
286
- throw new Error('Table.renderTo: Invalid component - not an object');
1131
+ private _updateRowSelectionUI(): void {
1132
+ if (!this._tableElement) return;
1133
+ const tbody = this._tableElement.querySelector('tbody');
1134
+ if (!tbody) return;
1135
+ // Get current page rows
1136
+ let rows = this._getFilteredRows();
1137
+ rows = this._getSortedRows(rows);
1138
+ const pageRows = this._getPaginatedRows(rows);
1139
+ // Update each visible row's selection state
1140
+ Array.from(tbody.children).forEach((tr, visualIndex) => {
1141
+ const pageRowData = pageRows[visualIndex];
1142
+ const actualRowIndex = this.state.rows.indexOf(pageRowData);
1143
+ const isSelected = this.state.selectedIndexes.has(actualRowIndex);
1144
+ // Update row highlight
1145
+ if (isSelected) {
1146
+ tr.classList.add('jux-table-row-selected');
1147
+ } else {
1148
+ tr.classList.remove('jux-table-row-selected');
1149
+ }
1150
+ // Update checkbox if present
1151
+ const checkbox = tr.querySelector('input[type="checkbox"]') as HTMLInputElement;
1152
+ if (checkbox) {
1153
+ checkbox.checked = isSelected;
1154
+ }
1155
+ });
1156
+ // Update bulk checkbox state
1157
+ this._updateBulkCheckboxState();
1158
+ }
1159
+
1160
+ // Selection management
1161
+ private _preserveSelections(previousRows: any[][], newRows: any[][]): void {
1162
+ if (!this.state.rowIdField || this.state.selectionBehavior === 'clear') {
1163
+ this.state.selectedIndexes.clear();
1164
+ return;
287
1165
  }
1166
+ // Build map of old selections by ID
1167
+ const selectedIds = new Set<any>();
1168
+ Array.from(this.state.selectedIndexes).forEach(index => {
1169
+ const row = previousRows[index];
1170
+ if (row && row[this.state.rowIdField!] != null) {
1171
+ selectedIds.add(row[this.state.rowIdField!]);
1172
+ }
1173
+ });
1174
+ // Find new indexes for selected IDs
1175
+ this.state.selectedIndexes.clear();
1176
+ newRows.forEach((row, index) => {
1177
+ const rowId = row[this.state.rowIdField!];
1178
+ if (rowId != null && selectedIds.has(rowId)) {
1179
+ this.state.selectedIndexes.add(index);
1180
+ }
1181
+ });
1182
+ }
288
1183
 
289
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
290
- throw new Error('Table.renderTo: Invalid component - missing _id (not a Jux component)');
1184
+ // Pagination
1185
+ private _updatePagination(wrapper: HTMLElement, tbody: HTMLTableSectionElement): void {
1186
+ // Remove existing pagination
1187
+ const existingPagination = wrapper.querySelector('.jux-table-pagination');
1188
+ if (existingPagination) {
1189
+ existingPagination.remove();
291
1190
  }
1191
+ let rows = this._getFilteredRows();
1192
+ rows = this._getSortedRows(rows);
1193
+ const totalPages = Math.ceil(rows.length / this.state.rowsPerPage);
1194
+ if (totalPages <= 1) return;
1195
+ const pagination = document.createElement('div');
1196
+ pagination.className = 'jux-table-pagination';
1197
+ pagination.style.cssText = 'margin-top: 10px; display: flex; gap: 5px; justify-content: center;';
1198
+ // Previous button
1199
+ const prevBtn = document.createElement('button');
1200
+ prevBtn.textContent = 'Previous';
1201
+ prevBtn.disabled = this.state.currentPage === 1;
1202
+ prevBtn.addEventListener('click', (e) => {
1203
+ if (this.state.currentPage > 1) {
1204
+ const previousPage = this.state.currentPage;
1205
+ this.state.currentPage--;
1206
+ this._renderTableBody(tbody);
1207
+ this._updatePagination(wrapper, tbody);
1208
+ this._triggerCallback('pageChange', this.state.currentPage, previousPage, e);
1209
+ }
1210
+ });
1211
+ pagination.appendChild(prevBtn);
1212
+ // Page info
1213
+ const pageInfo = document.createElement('span');
1214
+ pageInfo.textContent = `Page ${this.state.currentPage} of ${totalPages}`;
1215
+ pageInfo.style.cssText = 'display: flex; align-items: center; padding: 0 10px;';
1216
+ pagination.appendChild(pageInfo);
1217
+ // Next button
1218
+ const nextBtn = document.createElement('button');
1219
+ nextBtn.textContent = 'Next';
1220
+ nextBtn.disabled = this.state.currentPage === totalPages;
1221
+ nextBtn.addEventListener('click', (e) => {
1222
+ if (this.state.currentPage < totalPages) {
1223
+ const previousPage = this.state.currentPage;
1224
+ this.state.currentPage++;
1225
+ this._renderTableBody(tbody);
1226
+ this._updatePagination(wrapper, tbody);
1227
+ this._triggerCallback('pageChange', this.state.currentPage, previousPage, e);
1228
+ }
1229
+ });
1230
+ pagination.appendChild(nextBtn);
1231
+ wrapper.appendChild(pagination);
1232
+ }
292
1233
 
1234
+ /* ═════════════════════════════════════════════════════════════════
1235
+ * SECTION 8: EXPORTS
1236
+ * Convenience wrappers and factory function
1237
+ * ═════════════════════════════════════════════════════════════════ */
1238
+
1239
+ renderTo(juxComponent: any): this {
1240
+ if (!juxComponent?._id) {
1241
+ throw new Error('Table.renderTo: Invalid component');
1242
+ }
293
1243
  return this.render(`#${juxComponent._id}`);
294
1244
  }
295
1245
  }
296
1246
 
297
- /**
298
- * Factory helper
299
- */
300
1247
  export function table(id: string, options: TableOptions = {}): Table {
301
1248
  return new Table(id, options);
302
- }
1249
+ }