juxscript 1.0.18 → 1.0.20

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 (44) hide show
  1. package/lib/components/alert.ts +124 -128
  2. package/lib/components/areachart.ts +169 -287
  3. package/lib/components/areachartsmooth.ts +2 -2
  4. package/lib/components/badge.ts +63 -72
  5. package/lib/components/barchart.ts +120 -48
  6. package/lib/components/button.ts +99 -101
  7. package/lib/components/card.ts +97 -121
  8. package/lib/components/chart-types.ts +159 -0
  9. package/lib/components/chart-utils.ts +160 -0
  10. package/lib/components/chart.ts +628 -48
  11. package/lib/components/checkbox.ts +137 -51
  12. package/lib/components/code.ts +89 -75
  13. package/lib/components/container.ts +1 -1
  14. package/lib/components/datepicker.ts +93 -78
  15. package/lib/components/dialog.ts +163 -130
  16. package/lib/components/divider.ts +111 -193
  17. package/lib/components/docs-data.json +711 -264
  18. package/lib/components/doughnutchart.ts +125 -57
  19. package/lib/components/dropdown.ts +172 -85
  20. package/lib/components/element.ts +66 -61
  21. package/lib/components/fileupload.ts +142 -171
  22. package/lib/components/heading.ts +64 -21
  23. package/lib/components/hero.ts +109 -34
  24. package/lib/components/icon.ts +247 -0
  25. package/lib/components/icons.ts +174 -0
  26. package/lib/components/include.ts +77 -2
  27. package/lib/components/input.ts +174 -125
  28. package/lib/components/list.ts +120 -79
  29. package/lib/components/menu.ts +97 -2
  30. package/lib/components/modal.ts +144 -63
  31. package/lib/components/nav.ts +153 -52
  32. package/lib/components/paragraph.ts +78 -28
  33. package/lib/components/progress.ts +83 -107
  34. package/lib/components/radio.ts +151 -52
  35. package/lib/components/select.ts +110 -102
  36. package/lib/components/sidebar.ts +148 -105
  37. package/lib/components/switch.ts +124 -125
  38. package/lib/components/table.ts +214 -137
  39. package/lib/components/tabs.ts +194 -113
  40. package/lib/components/theme-toggle.ts +38 -7
  41. package/lib/components/tooltip.ts +207 -47
  42. package/lib/jux.ts +24 -5
  43. package/lib/reactivity/state.ts +13 -299
  44. package/package.json +1 -2
@@ -1,71 +1,63 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
2
3
 
3
4
  /**
4
- * Table column configuration
5
+ * Column definition
5
6
  */
6
- export interface TableColumn {
7
+ export interface ColumnDef {
7
8
  key: string;
8
9
  label: string;
9
10
  width?: string;
10
- align?: 'left' | 'center' | 'right';
11
- renderCell?: (value: any, row: any) => string | HTMLElement;
11
+ render?: (value: any, row: any) => string | HTMLElement;
12
12
  }
13
13
 
14
14
  /**
15
- * Table component options
15
+ * Table options
16
16
  */
17
17
  export interface TableOptions {
18
- columns?: TableColumn[];
19
- data?: any[];
18
+ columns?: (string | ColumnDef)[];
19
+ rows?: any[][];
20
+ headers?: boolean;
20
21
  striped?: boolean;
21
22
  hoverable?: boolean;
22
23
  bordered?: boolean;
23
- allowHtml?: boolean;
24
+ compact?: boolean;
24
25
  style?: string;
25
26
  class?: string;
26
27
  }
27
28
 
28
29
  /**
29
- * Table component state
30
+ * Table state
30
31
  */
31
32
  type TableState = {
32
- columns: TableColumn[];
33
- data: any[];
33
+ columns: ColumnDef[];
34
+ rows: any[][];
35
+ headers: boolean;
34
36
  striped: boolean;
35
37
  hoverable: boolean;
36
38
  bordered: boolean;
37
- allowHtml: boolean;
39
+ compact: boolean;
38
40
  style: string;
39
41
  class: string;
40
42
  };
41
43
 
42
44
  /**
43
45
  * Table component
44
- *
46
+ *
45
47
  * 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();
48
+ * jux.table('users', {
49
+ * columns: ['Name', 'Email', 'Role'],
50
+ * rows: [['John', 'john@example.com', 'Admin']]
51
+ * }).render('#app');
52
+ *
53
+ * // With state binding
54
+ * const todos = state([]);
55
+ * jux.table('todos-table')
56
+ * .columns(['ID', 'Text', 'Done'])
57
+ * .sync('rows', todos, (items) =>
58
+ * items.map(item => [item.id, item.text, item.done])
59
+ * )
60
+ * .render('#app');
69
61
  */
70
62
  export class Table {
71
63
  state: TableState;
@@ -73,17 +65,35 @@ export class Table {
73
65
  _id: string;
74
66
  id: string;
75
67
 
68
+ // CRITICAL: Store bind/sync instructions for deferred wiring
69
+ private _bindings: Array<{ event: string, handler: Function }> = [];
70
+ private _syncBindings: Array<{
71
+ property: string,
72
+ stateObj: State<any>,
73
+ toState?: Function,
74
+ toComponent?: Function
75
+ }> = [];
76
+
77
+ // Store table element reference
78
+ private _tableElement: HTMLTableElement | null = null;
79
+
76
80
  constructor(id: string, options: TableOptions = {}) {
77
81
  this._id = id;
78
82
  this.id = id;
79
83
 
84
+ // Normalize columns to ColumnDef format
85
+ const normalizedColumns = (options.columns ?? []).map(col =>
86
+ typeof col === 'string' ? { key: col, label: col } : col
87
+ );
88
+
80
89
  this.state = {
81
- columns: options.columns ?? [],
82
- data: options.data ?? [],
90
+ columns: normalizedColumns,
91
+ rows: options.rows ?? [],
92
+ headers: options.headers ?? true,
83
93
  striped: options.striped ?? false,
84
- hoverable: options.hoverable ?? true,
94
+ hoverable: options.hoverable ?? false,
85
95
  bordered: options.bordered ?? false,
86
- allowHtml: options.allowHtml ?? true,
96
+ compact: options.compact ?? false,
87
97
  style: options.style ?? '',
88
98
  class: options.class ?? ''
89
99
  };
@@ -93,13 +103,21 @@ export class Table {
93
103
  * Fluent API
94
104
  * ------------------------- */
95
105
 
96
- columns(value: TableColumn[]): this {
97
- this.state.columns = value;
106
+ columns(value: (string | ColumnDef)[]): this {
107
+ this.state.columns = value.map(col =>
108
+ typeof col === 'string' ? { key: col, label: col } : col
109
+ );
110
+ return this;
111
+ }
112
+
113
+ rows(value: any[][]): this {
114
+ this.state.rows = value;
115
+ this._updateTable();
98
116
  return this;
99
117
  }
100
118
 
101
- data(value: any[]): this {
102
- this.state.data = value;
119
+ headers(value: boolean): this {
120
+ this.state.headers = value;
103
121
  return this;
104
122
  }
105
123
 
@@ -118,8 +136,8 @@ export class Table {
118
136
  return this;
119
137
  }
120
138
 
121
- allowHtml(value: boolean): this {
122
- this.state.allowHtml = value;
139
+ compact(value: boolean): this {
140
+ this.state.compact = value;
123
141
  return this;
124
142
  }
125
143
 
@@ -133,44 +151,53 @@ export class Table {
133
151
  return this;
134
152
  }
135
153
 
136
- /* -------------------------
137
- * Helpers
138
- * ------------------------- */
154
+ /**
155
+ * Bind event handler (stores for wiring in render)
156
+ * DOM events only: click, mouseenter, etc.
157
+ */
158
+ bind(event: string, handler: Function): this {
159
+ this._bindings.push({ event, handler });
160
+ return this;
161
+ }
139
162
 
140
163
  /**
141
- * Auto-generate columns from data
164
+ * Sync with state (one-way: State Component)
165
+ *
166
+ * @param property - Component property to sync ('rows', 'columns')
167
+ * @param stateObj - State object to sync with
168
+ * @param transform - Optional transform function from state to component
142
169
  */
143
- private _autoGenerateColumns(data: any[]): TableColumn[] {
144
- if (!data || data.length === 0) {
145
- return [];
170
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
171
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
172
+ throw new Error(`Table.sync: Expected a State object for property "${property}"`);
146
173
  }
174
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
175
+ return this;
176
+ }
177
+
178
+ /* -------------------------
179
+ * Helpers
180
+ * ------------------------- */
147
181
 
148
- const firstRow = data[0];
149
- const keys = Object.keys(firstRow);
182
+ private _updateTable(): void {
183
+ if (!this._tableElement) return;
150
184
 
151
- return keys.map(key => ({
152
- key,
153
- label: this._formatLabel(key)
154
- }));
155
- }
185
+ const tbody = this._tableElement.querySelector('tbody');
186
+ if (!tbody) return;
156
187
 
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+/);
170
-
171
- return words
172
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
173
- .join(' ');
188
+ // Clear existing rows
189
+ tbody.innerHTML = '';
190
+
191
+ // Add new rows
192
+ this.state.rows.forEach(rowData => {
193
+ const tr = document.createElement('tr');
194
+ rowData.forEach(cellData => {
195
+ const td = document.createElement('td');
196
+ td.textContent = String(cellData);
197
+ tr.appendChild(td);
198
+ });
199
+ tbody.appendChild(tr);
200
+ });
174
201
  }
175
202
 
176
203
  /* -------------------------
@@ -178,125 +205,175 @@ export class Table {
178
205
  * ------------------------- */
179
206
 
180
207
  render(targetId?: string): this {
208
+ // === 1. SETUP: Get or create container ===
181
209
  let container: HTMLElement;
182
-
183
210
  if (targetId) {
184
211
  const target = document.querySelector(targetId);
185
212
  if (!target || !(target instanceof HTMLElement)) {
186
- throw new Error(`Table: Target element "${targetId}" not found`);
213
+ throw new Error(`Table: Target "${targetId}" not found`);
187
214
  }
188
215
  container = target;
189
216
  } else {
190
217
  container = getOrCreateContainer(this._id);
191
218
  }
192
-
193
219
  this.container = container;
194
- let { columns, data, striped, hoverable, bordered, allowHtml, style, class: className } = this.state;
195
220
 
196
- // Auto-generate columns if not provided
197
- if (columns.length === 0 && data.length > 0) {
198
- columns = this._autoGenerateColumns(data);
199
- }
221
+ // === 2. PREPARE: Destructure state and check sync flags ===
222
+ const { columns, rows, striped, hoverable, bordered, style, class: className } = this.state;
200
223
 
224
+ // === 3. BUILD: Create DOM elements ===
201
225
  const wrapper = document.createElement('div');
202
226
  wrapper.className = 'jux-table-wrapper';
203
227
  wrapper.id = this._id;
204
-
205
- if (className) {
206
- wrapper.className += ` ${className}`;
207
- }
208
-
209
- if (style) {
210
- wrapper.setAttribute('style', style);
211
- }
228
+ if (className) wrapper.className += ` ${className}`;
229
+ if (style) wrapper.setAttribute('style', style);
212
230
 
213
231
  const table = document.createElement('table');
214
232
  table.className = 'jux-table';
215
-
216
233
  if (striped) table.classList.add('jux-table-striped');
217
234
  if (hoverable) table.classList.add('jux-table-hoverable');
218
235
  if (bordered) table.classList.add('jux-table-bordered');
219
236
 
220
- // Table header
237
+ // Header
221
238
  const thead = document.createElement('thead');
222
239
  const headerRow = document.createElement('tr');
223
-
224
240
  columns.forEach(col => {
225
241
  const th = document.createElement('th');
226
242
  th.textContent = col.label;
227
243
  if (col.width) th.style.width = col.width;
228
- if (col.align) th.style.textAlign = col.align;
229
244
  headerRow.appendChild(th);
230
245
  });
231
-
232
246
  thead.appendChild(headerRow);
233
247
  table.appendChild(thead);
234
248
 
235
- // Table body
249
+ // Body
236
250
  const tbody = document.createElement('tbody');
237
-
238
- data.forEach(row => {
251
+ rows.forEach(row => {
239
252
  const tr = document.createElement('tr');
240
-
241
253
  columns.forEach(col => {
242
254
  const td = document.createElement('td');
243
- if (col.align) td.style.textAlign = col.align;
244
-
245
- const cellValue = row[col.key] ?? '';
255
+ const cellValue = row[col.key];
246
256
 
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) {
257
+ if (col.render) {
258
+ const rendered = col.render(cellValue, row);
259
+ if (typeof rendered === 'string') {
260
+ td.innerHTML = rendered;
261
+ } else {
251
262
  td.appendChild(rendered);
252
- } else if (typeof rendered === 'string') {
253
- if (allowHtml) {
254
- td.innerHTML = rendered;
255
- } else {
256
- td.textContent = rendered;
257
- }
258
263
  }
259
264
  } else {
260
- // Default rendering
261
- if (allowHtml && typeof cellValue === 'string' && cellValue.includes('<')) {
262
- td.innerHTML = cellValue;
263
- } else {
264
- td.textContent = String(cellValue);
265
- }
265
+ td.textContent = cellValue != null ? String(cellValue) : '';
266
266
  }
267
267
 
268
268
  tr.appendChild(td);
269
269
  });
270
-
271
270
  tbody.appendChild(tr);
272
271
  });
273
-
274
272
  table.appendChild(tbody);
275
273
  wrapper.appendChild(table);
276
- container.appendChild(wrapper);
277
274
 
275
+ // === 4. WIRE: Attach event listeners and sync bindings ===
276
+
277
+ // Wire custom bindings from .bind() calls
278
+ this._bindings.forEach(({ event, handler }) => {
279
+ wrapper.addEventListener(event, handler as EventListener);
280
+ });
281
+
282
+ // Wire sync bindings from .sync() calls
283
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
284
+ if (property === 'rows' || property === 'data') {
285
+ const transformToComponent = toComponent || ((v: any) => v);
286
+
287
+ stateObj.subscribe((val: any) => {
288
+ const transformed = transformToComponent(val);
289
+ this.state.rows = transformed;
290
+
291
+ // Re-render tbody
292
+ tbody.innerHTML = '';
293
+ transformed.forEach((row: any) => {
294
+ const tr = document.createElement('tr');
295
+ this.state.columns.forEach(col => {
296
+ const td = document.createElement('td');
297
+ const cellValue = row[col.key];
298
+
299
+ if (col.render) {
300
+ const rendered = col.render(cellValue, row);
301
+ if (typeof rendered === 'string') {
302
+ td.innerHTML = rendered;
303
+ } else {
304
+ td.appendChild(rendered);
305
+ }
306
+ } else {
307
+ td.textContent = cellValue != null ? String(cellValue) : '';
308
+ }
309
+
310
+ tr.appendChild(td);
311
+ });
312
+ tbody.appendChild(tr);
313
+ });
314
+ });
315
+ }
316
+ else if (property === 'columns') {
317
+ const transformToComponent = toComponent || ((v: any) => v);
318
+
319
+ stateObj.subscribe((val: any) => {
320
+ const transformed = transformToComponent(val);
321
+ this.state.columns = transformed;
322
+
323
+ // Re-render entire table
324
+ table.innerHTML = '';
325
+
326
+ const thead = document.createElement('thead');
327
+ const headerRow = document.createElement('tr');
328
+ transformed.forEach((col: any) => {
329
+ const th = document.createElement('th');
330
+ th.textContent = col.label;
331
+ if (col.width) th.style.width = col.width;
332
+ headerRow.appendChild(th);
333
+ });
334
+ thead.appendChild(headerRow);
335
+ table.appendChild(thead);
336
+
337
+ const tbody = document.createElement('tbody');
338
+ this.state.rows.forEach(row => {
339
+ const tr = document.createElement('tr');
340
+ transformed.forEach((col: any) => {
341
+ const td = document.createElement('td');
342
+ const cellValue = row[col.key];
343
+
344
+ if (col.render) {
345
+ const rendered = col.render(cellValue, row);
346
+ if (typeof rendered === 'string') {
347
+ td.innerHTML = rendered;
348
+ } else {
349
+ td.appendChild(rendered);
350
+ }
351
+ } else {
352
+ td.textContent = cellValue != null ? String(cellValue) : '';
353
+ }
354
+
355
+ tr.appendChild(td);
356
+ });
357
+ tbody.appendChild(tr);
358
+ });
359
+ table.appendChild(tbody);
360
+ });
361
+ }
362
+ });
363
+
364
+ // === 5. RENDER: Append to DOM and finalize ===
365
+ container.appendChild(wrapper);
278
366
  return this;
279
367
  }
280
368
 
281
- /**
282
- * Render to another Jux component's container
283
- */
284
369
  renderTo(juxComponent: any): this {
285
- if (!juxComponent || typeof juxComponent !== 'object') {
286
- throw new Error('Table.renderTo: Invalid component - not an object');
287
- }
288
-
289
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
290
- throw new Error('Table.renderTo: Invalid component - missing _id (not a Jux component)');
370
+ if (!juxComponent?._id) {
371
+ throw new Error('Table.renderTo: Invalid component');
291
372
  }
292
-
293
373
  return this.render(`#${juxComponent._id}`);
294
374
  }
295
375
  }
296
376
 
297
- /**
298
- * Factory helper
299
- */
300
377
  export function table(id: string, options: TableOptions = {}): Table {
301
378
  return new Table(id, options);
302
379
  }