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
@@ -7,7 +7,6 @@ export interface CheckboxOptions {
7
7
  disabled?: boolean;
8
8
  name?: string;
9
9
  value?: string;
10
- onChange?: (checked: boolean) => void;
11
10
  style?: string;
12
11
  class?: string;
13
12
  }
@@ -22,18 +21,42 @@ type CheckboxState = {
22
21
  class: string;
23
22
  };
24
23
 
24
+ /**
25
+ * Checkbox component
26
+ *
27
+ * Usage:
28
+ * jux.checkbox('terms', {
29
+ * label: 'I agree to terms',
30
+ * checked: false
31
+ * })
32
+ * .bind('change', (e) => console.log(e.target.checked))
33
+ * .render('#form');
34
+ *
35
+ * // Two-way binding with state
36
+ * const termsState = state(false);
37
+ * jux.checkbox('terms')
38
+ * .label('I agree to terms')
39
+ * .sync('checked', termsState)
40
+ * .render('#form');
41
+ */
25
42
  export class Checkbox {
26
43
  state: CheckboxState;
27
44
  container: HTMLElement | null = null;
28
45
  _id: string;
29
46
  id: string;
30
- private _onChange?: (checked: boolean) => void;
31
- private _boundState?: State<boolean>;
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
+ }> = [];
32
56
 
33
57
  constructor(id: string, options: CheckboxOptions = {}) {
34
58
  this._id = id;
35
59
  this.id = id;
36
- this._onChange = options.onChange;
37
60
 
38
61
  this.state = {
39
62
  label: options.label ?? '',
@@ -46,6 +69,10 @@ export class Checkbox {
46
69
  };
47
70
  }
48
71
 
72
+ /* -------------------------
73
+ * Fluent API
74
+ * ------------------------- */
75
+
49
76
  label(value: string): this {
50
77
  this.state.label = value;
51
78
  return this;
@@ -83,35 +110,35 @@ export class Checkbox {
83
110
  return this;
84
111
  }
85
112
 
86
- onChange(handler: (checked: boolean) => void): this {
87
- this._onChange = handler;
113
+ /**
114
+ * Bind event handler (stores for wiring in render)
115
+ * DOM events only: change, click, focus, blur, etc.
116
+ */
117
+ bind(event: string, handler: Function): this {
118
+ this._bindings.push({ event, handler });
88
119
  return this;
89
120
  }
90
121
 
91
122
  /**
92
- * Two-way binding to state
123
+ * Two-way sync with state (stores for wiring in render)
124
+ *
125
+ * @param property - Component property to sync ('checked', 'label', 'disabled')
126
+ * @param stateObj - State object to sync with
127
+ * @param toState - Optional transform function when going from component to state
128
+ * @param toComponent - Optional transform function when going from state to component
93
129
  */
94
- bind(stateObj: State<boolean>): this {
95
- this._boundState = stateObj;
96
-
97
- // Update checkbox when state changes
98
- stateObj.subscribe((val) => {
99
- this.state.checked = val;
100
- this._updateElement();
101
- });
102
-
103
- // Update state when checkbox changes (already handled in onChange)
104
- const originalOnChange = this._onChange;
105
- this._onChange = (checked) => {
106
- stateObj.set(checked);
107
- if (originalOnChange) {
108
- originalOnChange(checked);
109
- }
110
- };
111
-
130
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
131
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
132
+ throw new Error(`Checkbox.sync: Expected a State object for property "${property}"`);
133
+ }
134
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
112
135
  return this;
113
136
  }
114
137
 
138
+ /* -------------------------
139
+ * Helpers
140
+ * ------------------------- */
141
+
115
142
  private _updateElement(): void {
116
143
  const input = document.getElementById(`${this._id}-input`) as HTMLInputElement;
117
144
  if (input) {
@@ -120,37 +147,47 @@ export class Checkbox {
120
147
  }
121
148
  }
122
149
 
150
+ /**
151
+ * Toggle the checkbox
152
+ */
153
+ toggle(): this {
154
+ this.state.checked = !this.state.checked;
155
+ this._updateElement();
156
+ return this;
157
+ }
158
+
123
159
  isChecked(): boolean {
124
160
  return this.state.checked;
125
161
  }
126
162
 
163
+ /* -------------------------
164
+ * Render
165
+ * ------------------------- */
166
+
127
167
  render(targetId?: string): this {
168
+ // === 1. SETUP: Get or create container ===
128
169
  let container: HTMLElement;
129
-
130
170
  if (targetId) {
131
171
  const target = document.querySelector(targetId);
132
172
  if (!target || !(target instanceof HTMLElement)) {
133
- throw new Error(`Checkbox: Target element "${targetId}" not found`);
173
+ throw new Error(`Checkbox: Target "${targetId}" not found`);
134
174
  }
135
175
  container = target;
136
176
  } else {
137
177
  container = getOrCreateContainer(this._id);
138
178
  }
139
-
140
179
  this.container = container;
141
- const { label, checked, disabled, name, value, style, class: className } = this.state;
142
180
 
181
+ // === 2. PREPARE: Destructure state and check sync flags ===
182
+ const { checked, label, disabled, name, style, class: className } = this.state;
183
+ const hasCheckedSync = this._syncBindings.some(b => b.property === 'checked');
184
+
185
+ // === 3. BUILD: Create DOM elements ===
143
186
  const wrapper = document.createElement('div');
144
187
  wrapper.className = 'jux-checkbox';
145
188
  wrapper.id = this._id;
146
-
147
- if (className) {
148
- wrapper.className += ` ${className}`;
149
- }
150
-
151
- if (style) {
152
- wrapper.setAttribute('style', style);
153
- }
189
+ if (className) wrapper.className += ` ${className}`;
190
+ if (style) wrapper.setAttribute('style', style);
154
191
 
155
192
  const labelEl = document.createElement('label');
156
193
  labelEl.className = 'jux-checkbox-label';
@@ -160,28 +197,77 @@ export class Checkbox {
160
197
  input.className = 'jux-checkbox-input';
161
198
  input.id = `${this._id}-input`;
162
199
  input.name = name;
163
- input.value = value;
164
200
  input.checked = checked;
165
201
  input.disabled = disabled;
166
202
 
167
- input.addEventListener('change', (e) => {
168
- const target = e.target as HTMLInputElement;
169
- this.state.checked = target.checked;
170
- if (this._onChange) {
171
- this._onChange(target.checked);
172
- }
173
- });
203
+ const checkmark = document.createElement('span');
204
+ checkmark.className = 'jux-checkbox-checkmark';
205
+
206
+ const text = document.createElement('span');
207
+ text.className = 'jux-checkbox-text';
208
+ text.textContent = label;
174
209
 
175
210
  labelEl.appendChild(input);
211
+ labelEl.appendChild(checkmark);
212
+ labelEl.appendChild(text);
213
+ wrapper.appendChild(labelEl);
214
+
215
+ // === 4. WIRE: Attach event listeners and sync bindings ===
176
216
 
177
- if (label) {
178
- const span = document.createElement('span');
179
- span.className = 'jux-checkbox-text';
180
- span.textContent = label;
181
- labelEl.appendChild(span);
217
+ // Default behavior (only if NOT using sync)
218
+ if (!hasCheckedSync) {
219
+ input.addEventListener('change', () => {
220
+ this.state.checked = input.checked;
221
+ });
182
222
  }
183
223
 
184
- wrapper.appendChild(labelEl);
224
+ // Wire custom bindings from .bind() calls
225
+ this._bindings.forEach(({ event, handler }) => {
226
+ wrapper.addEventListener(event, handler as EventListener);
227
+ });
228
+
229
+ // Wire sync bindings from .sync() calls
230
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
231
+ if (property === 'checked') {
232
+ const transformToState = toState || ((v: any) => Boolean(v));
233
+ const transformToComponent = toComponent || ((v: any) => Boolean(v));
234
+
235
+ let isUpdating = false;
236
+
237
+ // State → Component
238
+ stateObj.subscribe((val: any) => {
239
+ if (isUpdating) return;
240
+ const transformed = transformToComponent(val);
241
+ if (input.checked !== transformed) {
242
+ input.checked = transformed;
243
+ this.state.checked = transformed;
244
+ }
245
+ });
246
+
247
+ // Component → State
248
+ input.addEventListener('change', () => {
249
+ if (isUpdating) return;
250
+ isUpdating = true;
251
+
252
+ const transformed = transformToState(input.checked);
253
+ this.state.checked = input.checked;
254
+ stateObj.set(transformed);
255
+
256
+ setTimeout(() => { isUpdating = false; }, 0);
257
+ });
258
+ }
259
+ else if (property === 'label') {
260
+ const transformToComponent = toComponent || ((v: any) => String(v));
261
+
262
+ stateObj.subscribe((val: any) => {
263
+ const transformed = transformToComponent(val);
264
+ text.textContent = transformed;
265
+ this.state.label = transformed;
266
+ });
267
+ }
268
+ });
269
+
270
+ // === 5. RENDER: Append to DOM and finalize ===
185
271
  container.appendChild(wrapper);
186
272
  return this;
187
273
  }
@@ -1,65 +1,47 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
2
3
 
3
- /**
4
- * Code component options
5
- */
6
4
  export interface CodeOptions {
7
5
  code?: string;
8
6
  language?: string;
9
- lineNumbers?: boolean;
10
- highlight?: boolean;
11
7
  style?: string;
12
8
  class?: string;
13
9
  }
14
10
 
15
- /**
16
- * Code component state
17
- */
18
11
  type CodeState = {
19
12
  code: string;
20
13
  language: string;
21
- lineNumbers: boolean;
22
- highlight: boolean;
23
14
  style: string;
24
15
  class: string;
25
16
  };
26
17
 
27
- /**
28
- * Code component
29
- *
30
- * Usage:
31
- * const code = jux.code('myCode', 'console.log("hello")', 'javascript');
32
- * code.render();
33
- */
34
18
  export class Code {
35
19
  state: CodeState;
36
20
  container: HTMLElement | null = null;
37
21
  _id: string;
38
22
  id: string;
39
23
 
40
- constructor(
41
- id: string,
42
- content?: string,
43
- language?: string,
44
- options: CodeOptions = {}
45
- ) {
24
+ // CRITICAL: Store bind/sync instructions for deferred wiring
25
+ private _bindings: Array<{ event: string, handler: Function }> = [];
26
+ private _syncBindings: Array<{
27
+ property: string,
28
+ stateObj: State<any>,
29
+ toState?: Function,
30
+ toComponent?: Function
31
+ }> = [];
32
+
33
+ constructor(id: string, options: CodeOptions = {}) {
46
34
  this._id = id;
47
35
  this.id = id;
48
36
 
49
37
  this.state = {
50
- code: content ?? options.code ?? '',
51
- language: language ?? options.language ?? 'javascript',
52
- lineNumbers: options.lineNumbers ?? false,
53
- highlight: options.highlight ?? true,
38
+ code: options.code ?? '',
39
+ language: options.language ?? 'javascript',
54
40
  style: options.style ?? '',
55
41
  class: options.class ?? ''
56
42
  };
57
43
  }
58
44
 
59
- /* -------------------------
60
- * Fluent API
61
- * ------------------------- */
62
-
63
45
  code(value: string): this {
64
46
  this.state.code = value;
65
47
  return this;
@@ -70,87 +52,119 @@ export class Code {
70
52
  return this;
71
53
  }
72
54
 
73
- lineNumbers(value: boolean): this {
74
- this.state.lineNumbers = value;
55
+ style(value: string): this {
56
+ this.state.style = value;
75
57
  return this;
76
58
  }
77
59
 
78
- highlight(value: boolean): this {
79
- this.state.highlight = value;
60
+ class(value: string): this {
61
+ this.state.class = value;
80
62
  return this;
81
63
  }
82
64
 
83
- style(value: string): this {
84
- this.state.style = value;
65
+ bind(event: string, handler: Function): this {
66
+ this._bindings.push({ event, handler });
85
67
  return this;
86
68
  }
87
69
 
88
- class(value: string): this {
89
- this.state.class = value;
70
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
71
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
72
+ throw new Error(`Code.sync: Expected a State object for property "${property}"`);
73
+ }
74
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
90
75
  return this;
91
76
  }
92
77
 
93
- /* -------------------------
94
- * Render
95
- * ------------------------- */
96
-
97
78
  render(targetId?: string): this {
79
+ // === 1. SETUP: Get or create container ===
98
80
  let container: HTMLElement;
99
-
100
81
  if (targetId) {
101
82
  const target = document.querySelector(targetId);
102
83
  if (!target || !(target instanceof HTMLElement)) {
103
- throw new Error(`Code: Target element "${targetId}" not found`);
84
+ throw new Error(`Code: Target "${targetId}" not found`);
104
85
  }
105
86
  container = target;
106
87
  } else {
107
88
  container = getOrCreateContainer(this._id);
108
89
  }
109
-
110
90
  this.container = container;
111
- const { code, language, lineNumbers, highlight, style, class: className } = this.state;
112
91
 
113
- const pre = document.createElement('pre');
114
- pre.className = lineNumbers ? 'jux-code line-numbers' : 'jux-code';
115
- pre.id = this._id;
116
-
117
- if (className) {
118
- pre.className += ` ${className}`;
119
- }
92
+ // === 2. PREPARE: Destructure state ===
93
+ const { code, language, style, class: className } = this.state;
120
94
 
121
- if (style) {
122
- pre.setAttribute('style', style);
123
- }
95
+ // === 3. BUILD: Create DOM elements ===
96
+ const wrapper = document.createElement('div');
97
+ wrapper.className = 'jux-code';
98
+ wrapper.id = this._id;
99
+ if (className) wrapper.className += ` ${className}`;
100
+ if (style) wrapper.setAttribute('style', style);
124
101
 
102
+ const pre = document.createElement('pre');
125
103
  const codeEl = document.createElement('code');
126
- codeEl.className = highlight ? `language-${language}` : '';
104
+ codeEl.className = `language-${language}`;
127
105
  codeEl.textContent = code;
128
-
129
106
  pre.appendChild(codeEl);
130
- container.appendChild(pre);
107
+ wrapper.appendChild(pre);
108
+
109
+ // === 4. WIRE: Attach event listeners and sync bindings ===
110
+
111
+ // Wire custom bindings from .bind() calls
112
+ this._bindings.forEach(({ event, handler }) => {
113
+ wrapper.addEventListener(event, handler as EventListener);
114
+ });
115
+
116
+ // Wire sync bindings from .sync() calls
117
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
118
+ if (property === 'code') {
119
+ const transformToComponent = toComponent || ((v: any) => String(v));
120
+
121
+ stateObj.subscribe((val: any) => {
122
+ const transformed = transformToComponent(val);
123
+ codeEl.textContent = transformed;
124
+ this.state.code = transformed;
125
+
126
+ // Re-trigger syntax highlighting if available
127
+ if ((window as any).Prism) {
128
+ (window as any).Prism.highlightElement(codeEl);
129
+ }
130
+ });
131
+ }
132
+ else if (property === 'language') {
133
+ const transformToComponent = toComponent || ((v: any) => String(v));
134
+
135
+ stateObj.subscribe((val: any) => {
136
+ const transformed = transformToComponent(val);
137
+ codeEl.className = `language-${transformed}`;
138
+ this.state.language = transformed;
139
+
140
+ if ((window as any).Prism) {
141
+ (window as any).Prism.highlightElement(codeEl);
142
+ }
143
+ });
144
+ }
145
+ });
146
+
147
+ // === 5. RENDER: Append to DOM and finalize ===
148
+ container.appendChild(wrapper);
149
+
150
+ // Trigger syntax highlighting
151
+ requestAnimationFrame(() => {
152
+ if ((window as any).Prism) {
153
+ (window as any).Prism.highlightElement(codeEl);
154
+ }
155
+ });
131
156
 
132
157
  return this;
133
158
  }
134
159
 
135
- /**
136
- * Render to another Jux component's container
137
- */
138
160
  renderTo(juxComponent: any): this {
139
- if (!juxComponent || typeof juxComponent !== 'object') {
140
- throw new Error('Code.renderTo: Invalid component - not an object');
161
+ if (!juxComponent?._id) {
162
+ throw new Error('Code.renderTo: Invalid component');
141
163
  }
142
-
143
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
144
- throw new Error('Code.renderTo: Invalid component - missing _id (not a Jux component)');
145
- }
146
-
147
164
  return this.render(`#${juxComponent._id}`);
148
165
  }
149
166
  }
150
167
 
151
- /**
152
- * Factory helper
153
- */
154
- export function code(id: string, content?: string, language?: string): Code {
155
- return new Code(id, content, language);
168
+ export function code(id: string, options: CodeOptions = {}): Code {
169
+ return new Code(id, options);
156
170
  }
@@ -144,7 +144,7 @@ export class Container {
144
144
  let computedStyle = style;
145
145
 
146
146
  if (usesFlexbox) {
147
- const flexStyles: string[] = ['display: flex', 'width: max-content'];
147
+ const flexStyles: string[] = ['display: flex', 'width: auto'];
148
148
 
149
149
  if (direction) flexStyles.push(`flex-direction: ${direction}`);
150
150