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,4 +1,6 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
3
+ import { renderIcon } from './icons.js';
2
4
 
3
5
  /**
4
6
  * Modal component options
@@ -23,6 +25,7 @@ type ModalState = {
23
25
  closeOnBackdropClick: boolean;
24
26
  size: string;
25
27
  isOpen: boolean;
28
+ open: boolean;
26
29
  style: string;
27
30
  class: string;
28
31
  };
@@ -45,6 +48,15 @@ export class Modal {
45
48
  _id: string;
46
49
  id: string;
47
50
 
51
+ // CRITICAL: Store bind/sync instructions for deferred wiring
52
+ private _bindings: Array<{ event: string, handler: Function }> = [];
53
+ private _syncBindings: Array<{
54
+ property: string,
55
+ stateObj: State<any>,
56
+ toState?: Function,
57
+ toComponent?: Function
58
+ }> = [];
59
+
48
60
  constructor(id: string, options: ModalOptions = {}) {
49
61
  this._id = id;
50
62
  this.id = id;
@@ -56,6 +68,7 @@ export class Modal {
56
68
  closeOnBackdropClick: options.closeOnBackdropClick ?? true,
57
69
  size: options.size ?? 'medium',
58
70
  isOpen: false,
71
+ open: false,
59
72
  style: options.style ?? '',
60
73
  class: options.class ?? ''
61
74
  };
@@ -130,94 +143,167 @@ export class Modal {
130
143
  * Render
131
144
  * ------------------------- */
132
145
 
146
+ bind(event: string, handler: Function): this {
147
+ this._bindings.push({ event, handler });
148
+ return this;
149
+ }
150
+
151
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
152
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
153
+ throw new Error(`Modal.sync: Expected a State object for property "${property}"`);
154
+ }
155
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
156
+ return this;
157
+ }
158
+
133
159
  render(targetId?: string): this {
160
+ // === 1. SETUP: Get or create container ===
134
161
  let container: HTMLElement;
135
-
136
162
  if (targetId) {
137
163
  const target = document.querySelector(targetId);
138
164
  if (!target || !(target instanceof HTMLElement)) {
139
- throw new Error(`Modal: Target element "${targetId}" not found`);
165
+ throw new Error(`Modal: Target "${targetId}" not found`);
140
166
  }
141
167
  container = target;
142
168
  } else {
143
169
  container = getOrCreateContainer(this._id);
144
170
  }
145
-
146
171
  this.container = container;
147
- const { title, content, showCloseButton, closeOnBackdropClick, size, isOpen, style, class: className } = this.state;
148
172
 
149
- // Modal backdrop
173
+ // === 2. PREPARE: Destructure state and check sync flags ===
174
+ const { open, title, content, size, closeOnBackdropClick: closeOnBackdrop, showCloseButton: showClose, style, class: className } = this.state;
175
+ const hasOpenSync = this._syncBindings.some(b => b.property === 'open');
176
+
177
+ // === 3. BUILD: Create DOM elements ===
178
+ const overlay = document.createElement('div');
179
+ overlay.className = 'jux-modal-overlay';
180
+ overlay.id = this._id;
181
+ overlay.style.display = open ? 'flex' : 'none';
182
+ if (className) overlay.className += ` ${className}`;
183
+ if (style) overlay.setAttribute('style', style);
184
+
150
185
  const modal = document.createElement('div');
151
186
  modal.className = `jux-modal jux-modal-${size}`;
152
- modal.id = this._id;
153
187
 
154
- if (className) {
155
- modal.className += ` ${className}`;
188
+ if (showClose) {
189
+ const closeButton = document.createElement('button');
190
+ closeButton.className = 'jux-modal-close';
191
+ closeButton.innerHTML = '×';
192
+ modal.appendChild(closeButton);
156
193
  }
157
194
 
158
- if (style) {
159
- modal.setAttribute('style', style);
195
+ if (title) {
196
+ const header = document.createElement('div');
197
+ header.className = 'jux-modal-header';
198
+ header.textContent = title;
199
+ modal.appendChild(header);
160
200
  }
161
201
 
162
- if (isOpen) {
163
- modal.classList.add('jux-modal-open');
164
- }
202
+ const body = document.createElement('div');
203
+ body.className = 'jux-modal-body';
204
+ body.innerHTML = content;
205
+ modal.appendChild(body);
165
206
 
166
- // Modal dialog
167
- const dialog = document.createElement('div');
168
- dialog.className = 'jux-modal-dialog';
207
+ overlay.appendChild(modal);
169
208
 
170
- // Modal header
171
- if (title || showCloseButton) {
172
- const header = document.createElement('div');
173
- header.className = 'jux-modal-header';
209
+ // === 4. WIRE: Attach event listeners and sync bindings ===
174
210
 
175
- if (title) {
176
- const titleEl = document.createElement('h3');
177
- titleEl.className = 'jux-modal-title';
178
- titleEl.textContent = title;
179
- header.appendChild(titleEl);
211
+ // Default close behavior (only if NOT using sync)
212
+ if (!hasOpenSync) {
213
+ if (showClose) {
214
+ const closeButton = modal.querySelector('.jux-modal-close');
215
+ closeButton?.addEventListener('click', () => {
216
+ this.state.open = false;
217
+ overlay.style.display = 'none';
218
+ });
180
219
  }
181
220
 
182
- if (showCloseButton) {
183
- const closeBtn = document.createElement('button');
184
- closeBtn.className = 'jux-modal-close';
185
- closeBtn.innerHTML = '&times;';
186
- header.appendChild(closeBtn);
187
-
188
- // Event binding - close button
189
- closeBtn.addEventListener('click', () => this.close());
221
+ if (closeOnBackdrop) {
222
+ overlay.addEventListener('click', (e) => {
223
+ if (e.target === overlay) {
224
+ this.state.open = false;
225
+ overlay.style.display = 'none';
226
+ }
227
+ });
190
228
  }
191
-
192
- dialog.appendChild(header);
193
229
  }
194
230
 
195
- // Modal body
196
- const body = document.createElement('div');
197
- body.className = 'jux-modal-body';
198
- body.innerHTML = content;
199
- dialog.appendChild(body);
231
+ // Wire custom bindings from .bind() calls
232
+ this._bindings.forEach(({ event, handler }) => {
233
+ overlay.addEventListener(event, handler as EventListener);
234
+ });
235
+
236
+ // Wire sync bindings from .sync() calls
237
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
238
+ if (property === 'open') {
239
+ const transformToState = toState || ((v: any) => Boolean(v));
240
+ const transformToComponent = toComponent || ((v: any) => Boolean(v));
241
+
242
+ let isUpdating = false;
243
+
244
+ // State → Component
245
+ stateObj.subscribe((val: any) => {
246
+ if (isUpdating) return;
247
+ const transformed = transformToComponent(val);
248
+ this.state.open = transformed;
249
+ overlay.style.display = transformed ? 'flex' : 'none';
250
+ });
251
+
252
+ // Component → State (close button, backdrop)
253
+ if (showClose) {
254
+ const closeButton = modal.querySelector('.jux-modal-close');
255
+ closeButton?.addEventListener('click', () => {
256
+ if (isUpdating) return;
257
+ isUpdating = true;
258
+
259
+ this.state.open = false;
260
+ overlay.style.display = 'none';
261
+ stateObj.set(transformToState(false));
262
+
263
+ setTimeout(() => { isUpdating = false; }, 0);
264
+ });
265
+ }
200
266
 
201
- modal.appendChild(dialog);
202
- container.appendChild(modal);
267
+ if (closeOnBackdrop) {
268
+ overlay.addEventListener('click', (e) => {
269
+ if (e.target === overlay) {
270
+ if (isUpdating) return;
271
+ isUpdating = true;
203
272
 
204
- // Event binding - backdrop click
205
- if (closeOnBackdropClick) {
206
- modal.addEventListener('click', (e) => {
207
- if (e.target === modal) {
208
- this.close();
209
- }
210
- });
211
- }
273
+ this.state.open = false;
274
+ overlay.style.display = 'none';
275
+ stateObj.set(transformToState(false));
212
276
 
213
- // Event binding - escape key
214
- const escapeHandler = (e: KeyboardEvent) => {
215
- if (e.key === 'Escape' && this.state.isOpen) {
216
- this.close();
277
+ setTimeout(() => { isUpdating = false; }, 0);
278
+ }
279
+ });
280
+ }
217
281
  }
218
- };
219
- document.addEventListener('keydown', escapeHandler);
282
+ else if (property === 'content') {
283
+ const transformToComponent = toComponent || ((v: any) => String(v));
284
+
285
+ stateObj.subscribe((val: any) => {
286
+ const transformed = transformToComponent(val);
287
+ body.innerHTML = transformed;
288
+ this.state.content = transformed;
289
+ });
290
+ }
291
+ else if (property === 'title') {
292
+ const transformToComponent = toComponent || ((v: any) => String(v));
293
+
294
+ stateObj.subscribe((val: any) => {
295
+ const transformed = transformToComponent(val);
296
+ const header = modal.querySelector('.jux-modal-header');
297
+ if (header) {
298
+ header.textContent = transformed;
299
+ }
300
+ this.state.title = transformed;
301
+ });
302
+ }
303
+ });
220
304
 
305
+ // === 5. RENDER: Append to DOM and finalize ===
306
+ container.appendChild(overlay);
221
307
  return this;
222
308
  }
223
309
 
@@ -225,14 +311,9 @@ export class Modal {
225
311
  * Render to another Jux component's container
226
312
  */
227
313
  renderTo(juxComponent: any): this {
228
- if (!juxComponent || typeof juxComponent !== 'object') {
229
- throw new Error('Modal.renderTo: Invalid component - not an object');
230
- }
231
-
232
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
233
- throw new Error('Modal.renderTo: Invalid component - missing _id (not a Jux component)');
314
+ if (!juxComponent?._id) {
315
+ throw new Error('Modal.renderTo: Invalid component');
234
316
  }
235
-
236
317
  return this.render(`#${juxComponent._id}`);
237
318
  }
238
319
  }
@@ -1,61 +1,62 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
3
+ import { renderIcon } from './icons.js';
2
4
  import { req } from './req.js';
3
5
 
4
- /**
5
- * Nav item configuration
6
- */
7
6
  export interface NavItem {
8
7
  label: string;
9
8
  href: string;
10
9
  active?: boolean;
11
10
  }
12
11
 
13
- /**
14
- * Nav component options
15
- */
12
+ export interface NavBrand {
13
+ text?: string;
14
+ href?: string;
15
+ icon?: string;
16
+ }
17
+
16
18
  export interface NavOptions {
17
19
  items?: NavItem[];
20
+ brand?: NavBrand;
18
21
  variant?: 'default' | 'pills' | 'tabs';
22
+ sticky?: boolean;
19
23
  style?: string;
20
24
  class?: string;
21
25
  }
22
26
 
23
- /**
24
- * Nav component state
25
- */
26
27
  type NavState = {
27
28
  items: NavItem[];
29
+ brand?: NavBrand;
28
30
  variant: string;
31
+ sticky: boolean;
29
32
  style: string;
30
33
  class: string;
31
34
  };
32
35
 
33
- /**
34
- * Nav component
35
- *
36
- * Usage:
37
- * const nav = jux.nav('myNav', {
38
- * variant: 'pills',
39
- * items: [
40
- * { label: 'Home', href: '/', active: true },
41
- * { label: 'About', href: '/about' }
42
- * ]
43
- * });
44
- * nav.render();
45
- */
46
36
  export class Nav {
47
37
  state: NavState;
48
38
  container: HTMLElement | null = null;
49
39
  _id: string;
50
40
  id: string;
51
41
 
42
+ // CRITICAL: Store bind/sync instructions for deferred wiring
43
+ private _bindings: Array<{ event: string, handler: Function }> = [];
44
+ private _syncBindings: Array<{
45
+ property: string,
46
+ stateObj: State<any>,
47
+ toState?: Function,
48
+ toComponent?: Function
49
+ }> = [];
50
+
52
51
  constructor(id: string, options: NavOptions = {}) {
53
52
  this._id = id;
54
53
  this.id = id;
55
54
 
56
55
  this.state = {
57
56
  items: options.items ?? [],
57
+ brand: options.brand,
58
58
  variant: options.variant ?? 'default',
59
+ sticky: options.sticky ?? false,
59
60
  style: options.style ?? '',
60
61
  class: options.class ?? ''
61
62
  };
@@ -90,11 +91,21 @@ export class Nav {
90
91
  return this;
91
92
  }
92
93
 
94
+ brand(value: NavBrand): this {
95
+ this.state.brand = value;
96
+ return this;
97
+ }
98
+
93
99
  variant(value: string): this {
94
100
  this.state.variant = value;
95
101
  return this;
96
102
  }
97
103
 
104
+ sticky(value: boolean): this {
105
+ this.state.sticky = value;
106
+ return this;
107
+ }
108
+
98
109
  style(value: string): this {
99
110
  this.state.style = value;
100
111
  return this;
@@ -105,74 +116,164 @@ export class Nav {
105
116
  return this;
106
117
  }
107
118
 
119
+ bind(event: string, handler: Function): this {
120
+ this._bindings.push({ event, handler });
121
+ return this;
122
+ }
123
+
124
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
125
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
126
+ throw new Error(`Nav.sync: Expected a State object for property "${property}"`);
127
+ }
128
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
129
+ return this;
130
+ }
131
+
108
132
  /* -------------------------
109
- * Render
133
+ * Render (5-Step Pattern)
110
134
  * ------------------------- */
111
135
 
112
136
  render(targetId?: string): this {
137
+ // === 1. SETUP: Get or create container ===
113
138
  let container: HTMLElement;
114
-
115
139
  if (targetId) {
116
140
  const target = document.querySelector(targetId);
117
141
  if (!target || !(target instanceof HTMLElement)) {
118
- throw new Error(`Nav: Target element "${targetId}" not found`);
142
+ throw new Error(`Nav: Target "${targetId}" not found`);
119
143
  }
120
144
  container = target;
121
145
  } else {
122
146
  container = getOrCreateContainer(this._id);
123
147
  }
124
-
125
148
  this.container = container;
126
- const { items, variant, style, class: className } = this.state;
127
149
 
150
+ // === 2. PREPARE: Destructure state ===
151
+ const { brand, items, variant, sticky, style, class: className } = this.state;
152
+
153
+ // === 3. BUILD: Create DOM elements ===
128
154
  const nav = document.createElement('nav');
129
155
  nav.className = `jux-nav jux-nav-${variant}`;
130
156
  nav.id = this._id;
157
+ if (sticky) nav.classList.add('jux-nav-sticky');
158
+ if (className) nav.className += ` ${className}`;
159
+ if (style) nav.setAttribute('style', style);
131
160
 
132
- if (className) {
133
- nav.className += ` ${className}`;
134
- }
161
+ // Brand/Logo
162
+ if (brand) {
163
+ const brandEl = document.createElement('div');
164
+ brandEl.className = 'jux-nav-brand';
135
165
 
136
- if (style) {
137
- nav.setAttribute('style', style);
166
+ if (brand.href) {
167
+ const link = document.createElement('a');
168
+ link.href = brand.href;
169
+ if (brand.icon) {
170
+ const icon = document.createElement('span');
171
+ icon.appendChild(renderIcon(brand.icon));
172
+ link.appendChild(icon);
173
+ }
174
+ if (brand.text) {
175
+ const text = document.createElement('span');
176
+ text.textContent = brand.text;
177
+ link.appendChild(text);
178
+ }
179
+ brandEl.appendChild(link);
180
+ } else {
181
+ if (brand.icon) {
182
+ const icon = document.createElement('span');
183
+ icon.appendChild(renderIcon(brand.icon));
184
+ brandEl.appendChild(icon);
185
+ }
186
+ if (brand.text) {
187
+ const text = document.createElement('span');
188
+ text.textContent = brand.text;
189
+ brandEl.appendChild(text);
190
+ }
191
+ }
192
+
193
+ nav.appendChild(brandEl);
138
194
  }
139
195
 
196
+ // Nav items
197
+ const itemsContainer = document.createElement('div');
198
+ itemsContainer.className = 'jux-nav-items';
199
+
140
200
  items.forEach(item => {
141
- const link = document.createElement('a');
142
- link.className = 'jux-nav-item';
143
- link.href = item.href;
144
- link.textContent = item.label;
201
+ const navItem = document.createElement('a');
202
+ navItem.className = 'jux-nav-item';
203
+ navItem.href = item.href;
204
+ navItem.textContent = item.label;
205
+ if (item.active) navItem.classList.add('jux-nav-item-active');
206
+ itemsContainer.appendChild(navItem);
207
+ });
208
+
209
+ nav.appendChild(itemsContainer);
210
+
211
+ // === 4. WIRE: Attach event listeners and sync bindings ===
212
+
213
+ // Wire custom bindings from .bind() calls
214
+ this._bindings.forEach(({ event, handler }) => {
215
+ nav.addEventListener(event, handler as EventListener);
216
+ });
217
+
218
+ // Wire sync bindings from .sync() calls
219
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
220
+ if (property === 'items') {
221
+ const transformToComponent = toComponent || ((v: any) => v);
222
+
223
+ stateObj.subscribe((val: any) => {
224
+ const transformed = transformToComponent(val);
225
+ this.state.items = transformed;
226
+ this._setActiveStates();
145
227
 
146
- if (item.active) {
147
- link.classList.add('jux-nav-item-active');
228
+ // Re-render items
229
+ itemsContainer.innerHTML = '';
230
+ this.state.items.forEach((item: any) => {
231
+ const navItem = document.createElement('a');
232
+ navItem.className = 'jux-nav-item';
233
+ navItem.href = item.href;
234
+ navItem.textContent = item.label;
235
+ if (item.active) navItem.classList.add('jux-nav-item-active');
236
+ itemsContainer.appendChild(navItem);
237
+ });
238
+ });
148
239
  }
240
+ else if (property === 'brand') {
241
+ const transformToComponent = toComponent || ((v: any) => v);
149
242
 
150
- nav.appendChild(link);
243
+ stateObj.subscribe((val: any) => {
244
+ const transformed = transformToComponent(val);
245
+ const brandEl = nav.querySelector('.jux-nav-brand');
246
+ if (brandEl && transformed.text) {
247
+ const textEl = brandEl.querySelector('span:last-child');
248
+ if (textEl) {
249
+ textEl.textContent = transformed.text;
250
+ }
251
+ }
252
+ this.state.brand = transformed;
253
+ });
254
+ }
151
255
  });
152
256
 
257
+ // === 5. RENDER: Append to DOM and finalize ===
153
258
  container.appendChild(nav);
259
+
260
+ requestAnimationFrame(() => {
261
+ if ((window as any).lucide) {
262
+ (window as any).lucide.createIcons();
263
+ }
264
+ });
265
+
154
266
  return this;
155
267
  }
156
268
 
157
- /**
158
- * Render to another Jux component's container
159
- */
160
269
  renderTo(juxComponent: any): this {
161
- if (!juxComponent || typeof juxComponent !== 'object') {
162
- throw new Error('Nav.renderTo: Invalid component - not an object');
270
+ if (!juxComponent?._id) {
271
+ throw new Error('Nav.renderTo: Invalid component');
163
272
  }
164
-
165
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
166
- throw new Error('Nav.renderTo: Invalid component - missing _id (not a Jux component)');
167
- }
168
-
169
273
  return this.render(`#${juxComponent._id}`);
170
274
  }
171
275
  }
172
276
 
173
- /**
174
- * Factory helper
175
- */
176
277
  export function nav(id: string, options: NavOptions = {}): Nav {
177
278
  return new Nav(id, options);
178
279
  }