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
  * Button component options
@@ -6,7 +8,6 @@ import { getOrCreateContainer } from './helpers.js';
6
8
  export interface ButtonOptions {
7
9
  label?: string;
8
10
  icon?: string;
9
- click?: (e: Event) => void;
10
11
  variant?: 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'info' | 'link' | string;
11
12
  size?: 'small' | 'medium' | 'large';
12
13
  disabled?: boolean;
@@ -24,7 +25,6 @@ export interface ButtonOptions {
24
25
  type ButtonState = {
25
26
  label: string;
26
27
  icon: string;
27
- click: ((e: Event) => void) | null;
28
28
  variant: string;
29
29
  size: string;
30
30
  disabled: boolean;
@@ -38,12 +38,6 @@ type ButtonState = {
38
38
 
39
39
  /**
40
40
  * Button component
41
- *
42
- * Usage:
43
- * jux.button('myButton').label('Click Me').click(() => console.log('hi')).render();
44
- *
45
- * // Or with options
46
- * jux.button('myButton', { label: 'Click Me', click: () => console.log('hi') }).render();
47
41
  */
48
42
  export class Button {
49
43
  state: ButtonState;
@@ -51,6 +45,15 @@ export class Button {
51
45
  _id: string;
52
46
  id: string;
53
47
 
48
+ // CRITICAL: Store bind/sync instructions for deferred wiring
49
+ private _bindings: Array<{ event: string, handler: Function }> = [];
50
+ private _syncBindings: Array<{
51
+ property: string,
52
+ stateObj: State<any>,
53
+ toState?: Function,
54
+ toComponent?: Function
55
+ }> = [];
56
+
54
57
  constructor(id: string, options?: ButtonOptions) {
55
58
  this._id = id;
56
59
  this.id = id;
@@ -60,7 +63,6 @@ export class Button {
60
63
  this.state = {
61
64
  label: opts.label ?? 'Button',
62
65
  icon: opts.icon ?? '',
63
- click: opts.click ?? null,
64
66
  variant: opts.variant ?? 'primary',
65
67
  size: opts.size ?? 'medium',
66
68
  disabled: opts.disabled ?? false,
@@ -77,195 +79,191 @@ export class Button {
77
79
  * Fluent API
78
80
  * ------------------------- */
79
81
 
80
- /**
81
- * Set button label text
82
- */
83
82
  label(value: string): this {
84
83
  this.state.label = value;
85
84
  return this;
86
85
  }
87
86
 
88
- /**
89
- * Set button icon (emoji or text)
90
- */
91
87
  icon(value: string): this {
92
88
  this.state.icon = value;
93
89
  return this;
94
90
  }
95
91
 
96
- /**
97
- * Set click handler
98
- */
99
- click(callback: (e: Event) => void): this {
100
- this.state.click = callback;
101
- return this;
102
- }
103
-
104
- /**
105
- * Set button variant/style
106
- */
107
- variant(value: string): this {
92
+ variant(value: 'primary' | 'secondary' | 'danger' | 'success' | 'warning' | 'info' | 'link' | string): this {
108
93
  this.state.variant = value;
109
94
  return this;
110
95
  }
111
96
 
112
- /**
113
- * Set button size
114
- */
115
97
  size(value: 'small' | 'medium' | 'large'): this {
116
98
  this.state.size = value;
117
99
  return this;
118
100
  }
119
101
 
120
- /**
121
- * Set disabled state
122
- */
123
102
  disabled(value: boolean): this {
124
103
  this.state.disabled = value;
125
104
  return this;
126
105
  }
127
106
 
128
- /**
129
- * Set loading state
130
- */
131
107
  loading(value: boolean): this {
132
108
  this.state.loading = value;
133
109
  return this;
134
110
  }
135
111
 
136
- /**
137
- * Set icon position (left or right)
138
- */
139
112
  iconPosition(value: 'left' | 'right'): this {
140
113
  this.state.iconPosition = value;
141
114
  return this;
142
115
  }
143
116
 
144
- /**
145
- * Set full width
146
- */
147
117
  fullWidth(value: boolean): this {
148
118
  this.state.fullWidth = value;
149
119
  return this;
150
120
  }
151
121
 
152
- /**
153
- * Set button type attribute
154
- */
155
122
  type(value: 'button' | 'submit' | 'reset'): this {
156
123
  this.state.type = value;
157
124
  return this;
158
125
  }
159
126
 
160
- /**
161
- * Set inline style
162
- */
163
127
  style(value: string): this {
164
128
  this.state.style = value;
165
129
  return this;
166
130
  }
167
131
 
168
- /**
169
- * Set additional CSS classes
170
- */
171
132
  class(value: string): this {
172
133
  this.state.class = value;
173
134
  return this;
174
135
  }
175
136
 
137
+ /**
138
+ * Bind DOM events (click, hover, etc.)
139
+ * Stores handlers for wiring in render()
140
+ */
141
+ bind(event: string, handler: Function): this {
142
+ this._bindings.push({ event, handler });
143
+ return this;
144
+ }
145
+
146
+ /**
147
+ * Two-way state synchronization
148
+ * Stores sync instructions for wiring in render()
149
+ */
150
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
151
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
152
+ throw new Error(`Button.sync: Expected a State object for property "${property}"`);
153
+ }
154
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
155
+ return this;
156
+ }
157
+
176
158
  /* -------------------------
177
159
  * Render
178
160
  * ------------------------- */
179
161
 
180
- /**
181
- * Render button to target element
182
- */
183
162
  render(targetId?: string): this {
163
+ // === 1. SETUP: Get or create container ===
184
164
  let container: HTMLElement;
185
-
186
165
  if (targetId) {
187
166
  const target = document.querySelector(targetId);
188
167
  if (!target || !(target instanceof HTMLElement)) {
189
- throw new Error(`Button: Target element "${targetId}" not found`);
168
+ throw new Error(`Button: Target "${targetId}" not found`);
190
169
  }
191
170
  container = target;
192
171
  } else {
193
172
  container = getOrCreateContainer(this._id);
194
173
  }
195
-
196
174
  this.container = container;
197
- const { label, icon, click, variant, size, disabled, loading, iconPosition, fullWidth, type, style, class: className } = this.state;
198
175
 
176
+ // === 2. PREPARE: Destructure state ===
177
+ const { label: text, variant, size, disabled, icon, iconPosition, loading, style, class: className } = this.state;
178
+ const hasTextSync = this._syncBindings.some(b => b.property === 'text');
179
+ const hasDisabledSync = this._syncBindings.some(b => b.property === 'disabled');
180
+
181
+ // === 3. BUILD: Create DOM elements ===
199
182
  const button = document.createElement('button');
200
183
  button.className = `jux-button jux-button-${variant} jux-button-${size}`;
201
184
  button.id = this._id;
202
- button.type = type;
203
185
  button.disabled = disabled || loading;
204
186
 
205
- if (fullWidth) {
206
- button.classList.add('jux-button-full-width');
207
- }
208
-
209
- if (loading) {
210
- button.classList.add('jux-button-loading');
211
- }
212
-
213
- if (className) {
214
- button.className += ` ${className}`;
215
- }
216
-
217
- if (style) {
218
- button.setAttribute('style', style);
219
- }
187
+ if (className) button.className += ` ${className}`;
188
+ if (style) button.setAttribute('style', style);
220
189
 
221
- // Build button content
222
190
  if (icon && iconPosition === 'left') {
223
191
  const iconEl = document.createElement('span');
224
- iconEl.className = 'jux-button-icon jux-button-icon-left';
225
- iconEl.textContent = icon;
192
+ iconEl.className = 'jux-button-icon';
193
+ iconEl.appendChild(renderIcon(icon));
226
194
  button.appendChild(iconEl);
227
195
  }
228
196
 
229
- const labelEl = document.createElement('span');
230
- labelEl.className = 'jux-button-label';
231
- labelEl.textContent = loading ? 'Loading...' : label;
232
- button.appendChild(labelEl);
197
+ const textEl = document.createElement('span');
198
+ textEl.textContent = loading ? 'Loading...' : text;
199
+ button.appendChild(textEl);
233
200
 
234
201
  if (icon && iconPosition === 'right') {
235
202
  const iconEl = document.createElement('span');
236
- iconEl.className = 'jux-button-icon jux-button-icon-right';
237
- iconEl.textContent = icon;
203
+ iconEl.className = 'jux-button-icon';
204
+ iconEl.appendChild(renderIcon(icon));
238
205
  button.appendChild(iconEl);
239
206
  }
240
207
 
241
- // Event binding - click handler
242
- if (click) {
243
- button.addEventListener('click', click);
244
- }
208
+ // === 4. WIRE: Attach event listeners and sync bindings ===
209
+
210
+ // Wire custom bindings from .bind() calls
211
+ this._bindings.forEach(({ event, handler }) => {
212
+ button.addEventListener(event, handler as EventListener);
213
+ });
214
+
215
+ // Wire sync bindings from .sync() calls
216
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
217
+ if (property === 'text') {
218
+ const transformToComponent = toComponent || ((v: any) => String(v));
245
219
 
220
+ stateObj.subscribe((val: any) => {
221
+ const transformed = transformToComponent(val);
222
+ textEl.textContent = this.state.loading ? 'Loading...' : transformed;
223
+ this.state.label = transformed;
224
+ });
225
+ }
226
+ else if (property === 'disabled') {
227
+ const transformToComponent = toComponent || ((v: any) => Boolean(v));
228
+
229
+ stateObj.subscribe((val: any) => {
230
+ const transformed = transformToComponent(val);
231
+ button.disabled = transformed || this.state.loading;
232
+ this.state.disabled = transformed;
233
+ });
234
+ }
235
+ else if (property === 'loading') {
236
+ const transformToComponent = toComponent || ((v: any) => Boolean(v));
237
+
238
+ stateObj.subscribe((val: any) => {
239
+ const transformed = transformToComponent(val);
240
+ button.disabled = this.state.disabled || transformed;
241
+ textEl.textContent = transformed ? 'Loading...' : this.state.label;
242
+ this.state.loading = transformed;
243
+ });
244
+ }
245
+ });
246
+
247
+ // === 5. RENDER: Append to DOM and finalize ===
246
248
  container.appendChild(button);
249
+
250
+ requestAnimationFrame(() => {
251
+ if ((window as any).lucide) {
252
+ (window as any).lucide.createIcons();
253
+ }
254
+ });
255
+
247
256
  return this;
248
257
  }
249
258
 
250
- /**
251
- * Render to another Jux component's container
252
- */
253
- renderTo(juxComponent: this): this {
254
- if (!juxComponent || typeof juxComponent !== 'object') {
255
- throw new Error('Button.renderTo: Invalid component - not an object');
256
- }
257
-
258
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
259
- throw new Error('Button.renderTo: Invalid component - missing _id (not a Jux component)');
259
+ renderTo(juxComponent: any): this {
260
+ if (!juxComponent?._id) {
261
+ throw new Error('Button.renderTo: Invalid component');
260
262
  }
261
-
262
263
  return this.render(`#${juxComponent._id}`);
263
264
  }
264
265
  }
265
266
 
266
- /**
267
- * Factory helper
268
- */
269
267
  export function button(id: string, options?: ButtonOptions): Button {
270
268
  return new Button(id, options);
271
269
  }
@@ -1,94 +1,68 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
2
3
 
3
- /**
4
- * Card component options
5
- */
6
4
  export interface CardOptions {
7
5
  title?: string;
8
- subtitle?: string;
9
6
  content?: string;
10
- image?: string;
11
- variant?: 'default' | 'elevated' | 'outlined';
7
+ footer?: string;
8
+ variant?: string;
9
+ hoverable?: boolean;
12
10
  style?: string;
13
11
  class?: string;
14
12
  }
15
13
 
16
- /**
17
- * Card component state
18
- */
19
14
  type CardState = {
20
15
  title: string;
21
- subtitle: string;
22
16
  content: string;
23
- image: string;
17
+ footer: string;
24
18
  variant: string;
19
+ hoverable: boolean;
25
20
  style: string;
26
21
  class: string;
27
- hasActions: boolean;
28
22
  };
29
23
 
30
- /**
31
- * Card component
32
- *
33
- * Usage:
34
- * const card = jux.card('myCard', {
35
- * title: 'Card Title',
36
- * content: 'Card content here'
37
- * });
38
- * card.render();
39
- *
40
- * // Add actions (any components)
41
- * jux.button('view-btn').label('View').render('#myCard-actions');
42
- * jux.button('delete-btn').label('Delete').render('#myCard-actions');
43
- */
44
24
  export class Card {
45
25
  state: CardState;
46
26
  container: HTMLElement | null = null;
47
27
  _id: string;
48
28
  id: string;
49
29
 
30
+ // CRITICAL: Store bind/sync instructions for deferred wiring
31
+ private _bindings: Array<{ event: string, handler: Function }> = [];
32
+ private _syncBindings: Array<{
33
+ property: string,
34
+ stateObj: State<any>,
35
+ toState?: Function,
36
+ toComponent?: Function
37
+ }> = [];
38
+
50
39
  constructor(id: string, options: CardOptions = {}) {
51
40
  this._id = id;
52
41
  this.id = id;
53
42
 
54
43
  this.state = {
55
44
  title: options.title ?? '',
56
- subtitle: options.subtitle ?? '',
57
45
  content: options.content ?? '',
58
- image: options.image ?? '',
46
+ footer: options.footer ?? '',
59
47
  variant: options.variant ?? 'default',
48
+ hoverable: options.hoverable ?? false,
60
49
  style: options.style ?? '',
61
- class: options.class ?? '',
62
- hasActions: false
50
+ class: options.class ?? ''
63
51
  };
64
52
  }
65
53
 
66
- /* -------------------------
67
- * Fluent API
68
- * ------------------------- */
69
-
70
54
  title(value: string): this {
71
55
  this.state.title = value;
72
56
  return this;
73
57
  }
74
58
 
75
- subtitle(value: string): this {
76
- this.state.subtitle = value;
77
- return this;
78
- }
79
-
80
59
  content(value: string): this {
81
60
  this.state.content = value;
82
61
  return this;
83
62
  }
84
63
 
85
- image(value: string): this {
86
- this.state.image = value;
87
- return this;
88
- }
89
-
90
- variant(value: string): this {
91
- this.state.variant = value;
64
+ footer(value: string): this {
65
+ this.state.footer = value;
92
66
  return this;
93
67
  }
94
68
 
@@ -102,118 +76,120 @@ export class Card {
102
76
  return this;
103
77
  }
104
78
 
105
- /**
106
- * Enable actions region
107
- * Creates an empty actions container that components can render into
108
- */
109
- withActions(): this {
110
- this.state.hasActions = true;
79
+ bind(event: string, handler: Function): this {
80
+ this._bindings.push({ event, handler });
111
81
  return this;
112
82
  }
113
83
 
114
- /* -------------------------
115
- * Render
116
- * ------------------------- */
84
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
85
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
86
+ throw new Error(`Card.sync: Expected a State object for property "${property}"`);
87
+ }
88
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
89
+ return this;
90
+ }
117
91
 
118
92
  render(targetId?: string): this {
93
+ // === 1. SETUP: Get or create container ===
119
94
  let container: HTMLElement;
120
-
121
95
  if (targetId) {
122
96
  const target = document.querySelector(targetId);
123
97
  if (!target || !(target instanceof HTMLElement)) {
124
- throw new Error(`Card: Target element "${targetId}" not found`);
98
+ throw new Error(`Card: Target "${targetId}" not found`);
125
99
  }
126
100
  container = target;
127
101
  } else {
128
102
  container = getOrCreateContainer(this._id);
129
103
  }
130
-
131
104
  this.container = container;
132
- const { title, subtitle, content, image, variant, style, class: className, hasActions } = this.state;
133
105
 
134
- // Create card element
106
+ // === 2. PREPARE: Destructure state ===
107
+ const { title, content, footer, variant, hoverable, style, class: className } = this.state;
108
+
109
+ // === 3. BUILD: Create DOM elements ===
135
110
  const card = document.createElement('div');
136
111
  card.className = `jux-card jux-card-${variant}`;
137
112
  card.id = this._id;
113
+ if (hoverable) card.classList.add('jux-card-hoverable');
114
+ if (className) card.className += ` ${className}`;
115
+ if (style) card.setAttribute('style', style);
138
116
 
139
- if (className) {
140
- card.className += ` ${className}`;
141
- }
142
-
143
- if (style) {
144
- card.setAttribute('style', style);
145
- }
146
-
147
- // Image
148
- if (image) {
149
- const img = document.createElement('img');
150
- img.className = 'jux-card-image';
151
- img.src = image;
152
- img.alt = title || 'Card image';
153
- card.appendChild(img);
154
- }
155
-
156
- // Content wrapper
157
- const cardBody = document.createElement('div');
158
- cardBody.className = 'jux-card-body';
159
-
160
- // Title
161
117
  if (title) {
162
- const titleEl = document.createElement('h3');
163
- titleEl.className = 'jux-card-title';
164
- titleEl.textContent = title;
165
- cardBody.appendChild(titleEl);
118
+ const header = document.createElement('div');
119
+ header.className = 'jux-card-header';
120
+ header.textContent = title;
121
+ card.appendChild(header);
166
122
  }
167
123
 
168
- // Subtitle
169
- if (subtitle) {
170
- const subtitleEl = document.createElement('p');
171
- subtitleEl.className = 'jux-card-subtitle';
172
- subtitleEl.textContent = subtitle;
173
- cardBody.appendChild(subtitleEl);
174
- }
124
+ const body = document.createElement('div');
125
+ body.className = 'jux-card-body';
126
+ body.innerHTML = content;
127
+ card.appendChild(body);
175
128
 
176
- // Content
177
- if (content) {
178
- const contentEl = document.createElement('p');
179
- contentEl.className = 'jux-card-content';
180
- contentEl.textContent = content;
181
- cardBody.appendChild(contentEl);
129
+ if (footer) {
130
+ const footerEl = document.createElement('div');
131
+ footerEl.className = 'jux-card-footer';
132
+ footerEl.innerHTML = footer;
133
+ card.appendChild(footerEl);
182
134
  }
183
135
 
184
- card.appendChild(cardBody);
185
-
186
- // Actions region (empty container for user to populate)
187
- if (hasActions) {
188
- const actionsEl = document.createElement('div');
189
- actionsEl.className = 'jux-card-actions';
190
- actionsEl.id = `${this._id}-actions`;
191
- card.appendChild(actionsEl);
192
- }
136
+ // === 4. WIRE: Attach event listeners and sync bindings ===
137
+
138
+ // Wire custom bindings from .bind() calls
139
+ this._bindings.forEach(({ event, handler }) => {
140
+ card.addEventListener(event, handler as EventListener);
141
+ });
142
+
143
+ // Wire sync bindings from .sync() calls
144
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
145
+ if (property === 'title') {
146
+ const transformToComponent = toComponent || ((v: any) => String(v));
147
+
148
+ stateObj.subscribe((val: any) => {
149
+ const transformed = transformToComponent(val);
150
+ const header = card.querySelector('.jux-card-header');
151
+ if (header) {
152
+ header.textContent = transformed;
153
+ }
154
+ this.state.title = transformed;
155
+ });
156
+ }
157
+ else if (property === 'content') {
158
+ const transformToComponent = toComponent || ((v: any) => String(v));
159
+
160
+ stateObj.subscribe((val: any) => {
161
+ const transformed = transformToComponent(val);
162
+ body.innerHTML = transformed;
163
+ this.state.content = transformed;
164
+ });
165
+ }
166
+ else if (property === 'footer') {
167
+ const transformToComponent = toComponent || ((v: any) => String(v));
168
+
169
+ stateObj.subscribe((val: any) => {
170
+ const transformed = transformToComponent(val);
171
+ const footerEl = card.querySelector('.jux-card-footer');
172
+ if (footerEl) {
173
+ footerEl.innerHTML = transformed;
174
+ }
175
+ this.state.footer = transformed;
176
+ });
177
+ }
178
+ });
193
179
 
180
+ // === 5. RENDER: Append to DOM and finalize ===
194
181
  container.appendChild(card);
195
182
  return this;
196
183
  }
197
184
 
198
- /**
199
- * Render to another Jux component's container
200
- */
201
185
  renderTo(juxComponent: any): this {
202
- if (!juxComponent || typeof juxComponent !== 'object') {
203
- throw new Error('Card.renderTo: Invalid component - not an object');
186
+ if (!juxComponent?._id) {
187
+ throw new Error('Card.renderTo: Invalid component');
204
188
  }
205
-
206
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
207
- throw new Error('Card.renderTo: Invalid component - missing _id (not a Jux component)');
208
- }
209
-
210
189
  return this.render(`#${juxComponent._id}`);
211
190
  }
212
191
  }
213
192
 
214
- /**
215
- * Factory helper
216
- */
217
193
  export function card(id: string, options: CardOptions = {}): Card {
218
194
  return new Card(id, options);
219
195
  }