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,5 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
2
3
 
3
4
  /**
4
5
  * Heading options
@@ -33,6 +34,15 @@ export class Heading {
33
34
  _id: string;
34
35
  id: string;
35
36
 
37
+ // CRITICAL: Store bind/sync instructions for deferred wiring
38
+ private _bindings: Array<{ event: string, handler: Function }> = [];
39
+ private _syncBindings: Array<{
40
+ property: string,
41
+ stateObj: State<any>,
42
+ toState?: Function,
43
+ toComponent?: Function
44
+ }> = [];
45
+
36
46
  constructor(id: string, options: HeadingOptions = {}) {
37
47
  this._id = id;
38
48
  this.id = id;
@@ -69,46 +79,79 @@ export class Heading {
69
79
  return this;
70
80
  }
71
81
 
82
+ bind(event: string, handler: Function): this {
83
+ this._bindings.push({ event, handler });
84
+ return this;
85
+ }
86
+
87
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
88
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
89
+ throw new Error(`Heading.sync: Expected a State object for property "${property}"`);
90
+ }
91
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
92
+ return this;
93
+ }
94
+
72
95
  /* -------------------------
73
96
  * Render
74
97
  * ------------------------- */
75
98
 
76
- render(targetId?: string | HTMLElement): this {
99
+ render(targetId?: string): this {
100
+ // === 1. SETUP: Get or create container ===
77
101
  let container: HTMLElement;
78
-
79
102
  if (targetId) {
80
- if (targetId instanceof HTMLElement) {
81
- container = targetId;
82
- } else {
83
- const target = document.querySelector(targetId);
84
- if (!target || !(target instanceof HTMLElement)) {
85
- throw new Error(`Heading: Target element "${targetId}" not found`);
86
- }
87
- container = target;
103
+ const target = document.querySelector(targetId);
104
+ if (!target || !(target instanceof HTMLElement)) {
105
+ throw new Error(`Heading: Target "${targetId}" not found`);
88
106
  }
107
+ container = target;
89
108
  } else {
90
109
  container = getOrCreateContainer(this._id);
91
110
  }
92
-
93
111
  this.container = container;
94
- const { level, text, class: className, style } = this.state;
95
112
 
113
+ // === 2. PREPARE: Destructure state ===
114
+ const { text, level, style, class: className } = this.state;
115
+
116
+ // === 3. BUILD: Create DOM elements ===
96
117
  const heading = document.createElement(`h${level}`) as HTMLHeadingElement;
118
+ heading.className = `jux-heading jux-heading-${level}`;
97
119
  heading.id = this._id;
98
120
  heading.textContent = text;
121
+ if (className) heading.className += ` ${className}`;
122
+ if (style) heading.setAttribute('style', style);
123
+
124
+ // === 4. WIRE: Attach event listeners and sync bindings ===
125
+
126
+ // Wire custom bindings from .bind() calls
127
+ this._bindings.forEach(({ event, handler }) => {
128
+ heading.addEventListener(event, handler as EventListener);
129
+ });
130
+
131
+ // Wire sync bindings from .sync() calls
132
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
133
+ if (property === 'text') {
134
+ const transformToComponent = toComponent || ((v: any) => String(v));
135
+
136
+ stateObj.subscribe((val: any) => {
137
+ const transformed = transformToComponent(val);
138
+ heading.textContent = transformed;
139
+ this.state.text = transformed;
140
+ });
141
+ }
142
+ });
99
143
 
100
- if (className) {
101
- heading.className = className;
102
- }
103
-
104
- if (style) {
105
- heading.setAttribute('style', style);
106
- }
107
-
144
+ // === 5. RENDER: Append to DOM and finalize ===
108
145
  container.appendChild(heading);
109
-
110
146
  return this;
111
147
  }
148
+
149
+ renderTo(juxComponent: any): this {
150
+ if (!juxComponent?._id) {
151
+ throw new Error('Heading.renderTo: Invalid component');
152
+ }
153
+ return this.render(`#${juxComponent._id}`);
154
+ }
112
155
  }
113
156
 
114
157
  /**
@@ -1,4 +1,5 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
2
3
 
3
4
  /**
4
5
  * Hero component options
@@ -26,6 +27,9 @@ type HeroState = {
26
27
  variant: string;
27
28
  style: string;
28
29
  class: string;
30
+ content: string;
31
+ backgroundOverlay: boolean;
32
+ centered: boolean;
29
33
  };
30
34
 
31
35
  /**
@@ -45,6 +49,15 @@ export class Hero {
45
49
  _id: string;
46
50
  id: string;
47
51
 
52
+ // CRITICAL: Store bind/sync instructions for deferred wiring
53
+ private _bindings: Array<{ event: string, handler: Function }> = [];
54
+ private _syncBindings: Array<{
55
+ property: string,
56
+ stateObj: State<any>,
57
+ toState?: Function,
58
+ toComponent?: Function
59
+ }> = [];
60
+
48
61
  constructor(id: string, options: HeroOptions = {}) {
49
62
  this._id = id;
50
63
  this.id = id;
@@ -57,7 +70,10 @@ export class Hero {
57
70
  backgroundImage: options.backgroundImage ?? '',
58
71
  variant: options.variant ?? 'default',
59
72
  style: options.style ?? '',
60
- class: options.class ?? ''
73
+ class: options.class ?? '',
74
+ content: '',
75
+ backgroundOverlay: false,
76
+ centered: false
61
77
  };
62
78
  }
63
79
 
@@ -105,70 +121,134 @@ export class Hero {
105
121
  return this;
106
122
  }
107
123
 
124
+ bind(event: string, handler: Function): this {
125
+ this._bindings.push({ event, handler });
126
+ return this;
127
+ }
128
+
129
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
130
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
131
+ throw new Error(`Hero.sync: Expected a State object for property "${property}"`);
132
+ }
133
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
134
+ return this;
135
+ }
136
+
108
137
  /* -------------------------
109
138
  * Render
110
139
  * ------------------------- */
111
140
 
112
141
  render(targetId?: string): this {
142
+ // === 1. SETUP: Get or create container ===
113
143
  let container: HTMLElement;
114
-
115
144
  if (targetId) {
116
145
  const target = document.querySelector(targetId);
117
146
  if (!target || !(target instanceof HTMLElement)) {
118
- throw new Error(`Hero: Target element "${targetId}" not found`);
147
+ throw new Error(`Hero: Target "${targetId}" not found`);
119
148
  }
120
149
  container = target;
121
150
  } else {
122
151
  container = getOrCreateContainer(this._id);
123
152
  }
124
-
125
153
  this.container = container;
126
- const { title, subtitle, cta, ctaLink, backgroundImage, variant, style, class: className } = this.state;
127
154
 
128
- const hero = document.createElement('div');
129
- hero.className = `jux-hero jux-hero-${variant}`;
130
- hero.id = this._id;
155
+ // === 2. PREPARE: Destructure state ===
156
+ const { title, subtitle, content, backgroundImage, backgroundOverlay, centered, style, class: className } = this.state;
131
157
 
132
- if (className) {
133
- hero.className += ` ${className}`;
134
- }
135
-
136
- if (style) {
137
- hero.setAttribute('style', style);
138
- }
158
+ // === 3. BUILD: Create DOM elements ===
159
+ const hero = document.createElement('section');
160
+ hero.className = 'jux-hero';
161
+ hero.id = this._id;
162
+ if (centered) hero.classList.add('jux-hero-centered');
163
+ if (className) hero.className += ` ${className}`;
164
+ if (style) hero.setAttribute('style', style);
139
165
 
140
166
  if (backgroundImage) {
141
167
  hero.style.backgroundImage = `url(${backgroundImage})`;
168
+ if (backgroundOverlay) {
169
+ const overlay = document.createElement('div');
170
+ overlay.className = 'jux-hero-overlay';
171
+ hero.appendChild(overlay);
172
+ }
142
173
  }
143
174
 
144
- const content = document.createElement('div');
145
- content.className = 'jux-hero-content';
175
+ const contentContainer = document.createElement('div');
176
+ contentContainer.className = 'jux-hero-content';
146
177
 
147
178
  if (title) {
148
179
  const titleEl = document.createElement('h1');
149
180
  titleEl.className = 'jux-hero-title';
181
+ titleEl.id = `${this._id}-title`;
150
182
  titleEl.textContent = title;
151
- content.appendChild(titleEl);
183
+ contentContainer.appendChild(titleEl);
152
184
  }
153
185
 
154
186
  if (subtitle) {
155
187
  const subtitleEl = document.createElement('p');
156
188
  subtitleEl.className = 'jux-hero-subtitle';
189
+ subtitleEl.id = `${this._id}-subtitle`;
157
190
  subtitleEl.textContent = subtitle;
158
- content.appendChild(subtitleEl);
191
+ contentContainer.appendChild(subtitleEl);
159
192
  }
160
193
 
161
- if (cta) {
162
- const ctaEl = document.createElement('a');
163
- ctaEl.className = 'jux-hero-cta jux-button jux-button-primary';
164
- ctaEl.href = ctaLink;
165
- ctaEl.textContent = cta;
166
- content.appendChild(ctaEl);
194
+ if (content) {
195
+ const contentEl = document.createElement('div');
196
+ contentEl.className = 'jux-hero-body';
197
+ contentEl.innerHTML = content;
198
+ contentContainer.appendChild(contentEl);
167
199
  }
168
200
 
169
- hero.appendChild(content);
170
- container.appendChild(hero);
201
+ hero.appendChild(contentContainer);
202
+
203
+ // === 4. WIRE: Attach event listeners and sync bindings ===
204
+
205
+ // Wire custom bindings from .bind() calls
206
+ this._bindings.forEach(({ event, handler }) => {
207
+ hero.addEventListener(event, handler as EventListener);
208
+ });
209
+
210
+ // Wire sync bindings from .sync() calls
211
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
212
+ if (property === 'title') {
213
+ const transformToComponent = toComponent || ((v: any) => String(v));
214
+
215
+ stateObj.subscribe((val: any) => {
216
+ const transformed = transformToComponent(val);
217
+ const titleEl = document.getElementById(`${this._id}-title`);
218
+ if (titleEl) {
219
+ titleEl.textContent = transformed;
220
+ }
221
+ this.state.title = transformed;
222
+ });
223
+ }
224
+ else if (property === 'subtitle') {
225
+ const transformToComponent = toComponent || ((v: any) => String(v));
226
+
227
+ stateObj.subscribe((val: any) => {
228
+ const transformed = transformToComponent(val);
229
+ const subtitleEl = document.getElementById(`${this._id}-subtitle`);
230
+ if (subtitleEl) {
231
+ subtitleEl.textContent = transformed;
232
+ }
233
+ this.state.subtitle = transformed;
234
+ });
235
+ }
236
+ else if (property === 'content') {
237
+ const transformToComponent = toComponent || ((v: any) => String(v));
238
+
239
+ stateObj.subscribe((val: any) => {
240
+ const transformed = transformToComponent(val);
241
+ const contentEl = hero.querySelector('.jux-hero-body');
242
+ if (contentEl) {
243
+ contentEl.innerHTML = transformed;
244
+ }
245
+ this.state.content = transformed;
246
+ });
247
+ }
248
+ });
171
249
 
250
+ // === 5. RENDER: Append to DOM and finalize ===
251
+ container.appendChild(hero);
172
252
  return this;
173
253
  }
174
254
 
@@ -176,14 +256,9 @@ export class Hero {
176
256
  * Render to another Jux component's container
177
257
  */
178
258
  renderTo(juxComponent: any): this {
179
- if (!juxComponent || typeof juxComponent !== 'object') {
180
- throw new Error('Hero.renderTo: Invalid component - not an object');
259
+ if (!juxComponent?._id) {
260
+ throw new Error('Hero.renderTo: Invalid component');
181
261
  }
182
-
183
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
184
- throw new Error('Hero.renderTo: Invalid component - missing _id (not a Jux component)');
185
- }
186
-
187
262
  return this.render(`#${juxComponent._id}`);
188
263
  }
189
264
  }
@@ -0,0 +1,247 @@
1
+ import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
3
+ import { renderIcon, renderEmoji } from './icons.js';
4
+
5
+ /**
6
+ * Icon component options
7
+ */
8
+ export interface IconOptions {
9
+ value: string;
10
+ size?: string;
11
+ color?: string;
12
+ useEmoji?: boolean;
13
+ style?: string;
14
+ class?: string;
15
+ }
16
+
17
+ /**
18
+ * Icon component state
19
+ */
20
+ type IconState = {
21
+ value: string;
22
+ size: string;
23
+ color: string;
24
+ useEmoji: boolean;
25
+ style: string;
26
+ class: string;
27
+ };
28
+
29
+ /**
30
+ * Icon component - Render an icon from emoji, icon name, or image path
31
+ *
32
+ * Usage:
33
+ * // Render Lucide icon from emoji
34
+ * jux.icon('myIcon', { value: '🚀' }).render('#container');
35
+ *
36
+ * // Render Lucide icon from name
37
+ * jux.icon('myIcon', { value: 'rocket', size: '32px', color: 'red' }).render('#container');
38
+ *
39
+ * // Render image icon
40
+ * jux.icon('myIcon', { value: '/path/icon.png' }).render('#container');
41
+ *
42
+ * // Force emoji rendering (skip conversion)
43
+ * jux.icon('myIcon', { value: '🚀', useEmoji: true }).render('#container');
44
+ */
45
+ export class Icon {
46
+ state: IconState;
47
+ container: HTMLElement | null = null;
48
+ _id: string;
49
+ id: string;
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
+
60
+ constructor(id: string, options: IconOptions) {
61
+ this._id = id;
62
+ this.id = id;
63
+
64
+ this.state = {
65
+ value: options.value,
66
+ size: options.size ?? '24px',
67
+ color: options.color ?? '',
68
+ useEmoji: options.useEmoji ?? false,
69
+ style: options.style ?? '',
70
+ class: options.class ?? ''
71
+ };
72
+ }
73
+
74
+ /* -------------------------
75
+ * Fluent API
76
+ * ------------------------- */
77
+
78
+ value(value: string): this {
79
+ this.state.value = value;
80
+ return this;
81
+ }
82
+
83
+ size(value: string): this {
84
+ this.state.size = value;
85
+ return this;
86
+ }
87
+
88
+ color(value: string): this {
89
+ this.state.color = value;
90
+ return this;
91
+ }
92
+
93
+ useEmoji(value: boolean): this {
94
+ this.state.useEmoji = value;
95
+ return this;
96
+ }
97
+
98
+ style(value: string): this {
99
+ this.state.style = value;
100
+ return this;
101
+ }
102
+
103
+ class(value: string): this {
104
+ this.state.class = value;
105
+ return this;
106
+ }
107
+
108
+ bind(event: string, handler: Function): this {
109
+ this._bindings.push({ event, handler });
110
+ return this;
111
+ }
112
+
113
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
114
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
115
+ throw new Error(`Icon.sync: Expected a State object for property "${property}"`);
116
+ }
117
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
118
+ return this;
119
+ }
120
+
121
+ /* -------------------------
122
+ * Render (5-Step Pattern)
123
+ * ------------------------- */
124
+
125
+ render(targetId?: string): this {
126
+ // === 1. SETUP: Get or create container ===
127
+ let container: HTMLElement;
128
+ if (targetId) {
129
+ const target = document.querySelector(targetId);
130
+ if (!target || !(target instanceof HTMLElement)) {
131
+ throw new Error(`Icon: Target element "${targetId}" not found`);
132
+ }
133
+ container = target;
134
+ } else {
135
+ container = getOrCreateContainer(this._id);
136
+ }
137
+ this.container = container;
138
+
139
+ // === 2. PREPARE: Destructure state ===
140
+ const { value, size, color, useEmoji, style, class: className } = this.state;
141
+
142
+ // === 3. BUILD: Create DOM elements ===
143
+ const wrapper = document.createElement('span');
144
+ wrapper.className = 'jux-icon';
145
+ wrapper.id = this._id;
146
+ if (className) wrapper.className += ` ${className}`;
147
+ if (style) wrapper.setAttribute('style', style);
148
+
149
+ const iconElement = useEmoji ? renderEmoji(value) : renderIcon(value);
150
+ iconElement.style.width = size;
151
+ iconElement.style.height = size;
152
+ if (color) iconElement.style.color = color;
153
+
154
+ wrapper.appendChild(iconElement);
155
+
156
+ // === 4. WIRE: Attach event listeners and sync bindings ===
157
+
158
+ // Wire custom bindings from .bind() calls
159
+ this._bindings.forEach(({ event, handler }) => {
160
+ wrapper.addEventListener(event, handler as EventListener);
161
+ });
162
+
163
+ // Wire sync bindings from .sync() calls
164
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
165
+ if (property === 'value') {
166
+ const transformToComponent = toComponent || ((v: any) => String(v));
167
+
168
+ stateObj.subscribe((val: any) => {
169
+ const transformed = transformToComponent(val);
170
+ this.state.value = transformed;
171
+
172
+ // Re-render icon
173
+ wrapper.innerHTML = '';
174
+ const newIcon = this.state.useEmoji ? renderEmoji(transformed) : renderIcon(transformed);
175
+ newIcon.style.width = this.state.size;
176
+ newIcon.style.height = this.state.size;
177
+ if (this.state.color) newIcon.style.color = this.state.color;
178
+ wrapper.appendChild(newIcon);
179
+
180
+ requestAnimationFrame(() => {
181
+ if ((window as any).lucide) {
182
+ (window as any).lucide.createIcons();
183
+ }
184
+ });
185
+ });
186
+ }
187
+ else if (property === 'size') {
188
+ const transformToComponent = toComponent || ((v: any) => String(v));
189
+
190
+ stateObj.subscribe((val: any) => {
191
+ const transformed = transformToComponent(val);
192
+ const icon = wrapper.querySelector('img, svg, span');
193
+ if (icon instanceof HTMLElement) {
194
+ icon.style.width = transformed;
195
+ icon.style.height = transformed;
196
+ }
197
+ this.state.size = transformed;
198
+ });
199
+ }
200
+ else if (property === 'color') {
201
+ const transformToComponent = toComponent || ((v: any) => String(v));
202
+
203
+ stateObj.subscribe((val: any) => {
204
+ const transformed = transformToComponent(val);
205
+ const icon = wrapper.querySelector('img, svg, span');
206
+ if (icon instanceof HTMLElement) {
207
+ icon.style.color = transformed;
208
+ }
209
+ this.state.color = transformed;
210
+ });
211
+ }
212
+ });
213
+
214
+ // === 5. RENDER: Append to DOM and finalize ===
215
+ container.appendChild(wrapper);
216
+
217
+ requestAnimationFrame(() => {
218
+ if ((window as any).lucide) {
219
+ (window as any).lucide.createIcons();
220
+ }
221
+ });
222
+
223
+ return this;
224
+ }
225
+
226
+ /**
227
+ * Render to another Jux component's container
228
+ */
229
+ renderTo(juxComponent: any): this {
230
+ if (!juxComponent || typeof juxComponent !== 'object') {
231
+ throw new Error('Icon.renderTo: Invalid component - not an object');
232
+ }
233
+
234
+ if (!juxComponent._id || typeof juxComponent._id !== 'string') {
235
+ throw new Error('Icon.renderTo: Invalid component - missing _id (not a Jux component)');
236
+ }
237
+
238
+ return this.render(`#${juxComponent._id}`);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Factory helper
244
+ */
245
+ export function icon(id: string, options: IconOptions): Icon {
246
+ return new Icon(id, options);
247
+ }