juxscript 1.0.19 → 1.0.21

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 (77) hide show
  1. package/bin/cli.js +121 -72
  2. package/lib/components/alert.ts +212 -165
  3. package/lib/components/badge.ts +93 -103
  4. package/lib/components/base/BaseComponent.ts +397 -0
  5. package/lib/components/base/FormInput.ts +322 -0
  6. package/lib/components/button.ts +63 -122
  7. package/lib/components/card.ts +109 -155
  8. package/lib/components/charts/areachart.ts +315 -0
  9. package/lib/components/charts/barchart.ts +421 -0
  10. package/lib/components/charts/doughnutchart.ts +263 -0
  11. package/lib/components/charts/lib/BaseChart.ts +402 -0
  12. package/lib/components/charts/lib/chart-types.ts +159 -0
  13. package/lib/components/charts/lib/chart-utils.ts +160 -0
  14. package/lib/components/charts/lib/chart.ts +707 -0
  15. package/lib/components/checkbox.ts +264 -127
  16. package/lib/components/code.ts +75 -108
  17. package/lib/components/container.ts +113 -130
  18. package/lib/components/data.ts +37 -5
  19. package/lib/components/datepicker.ts +195 -147
  20. package/lib/components/dialog.ts +187 -157
  21. package/lib/components/divider.ts +85 -191
  22. package/lib/components/docs-data.json +544 -2027
  23. package/lib/components/dropdown.ts +178 -136
  24. package/lib/components/element.ts +227 -171
  25. package/lib/components/fileupload.ts +285 -228
  26. package/lib/components/guard.ts +92 -0
  27. package/lib/components/heading.ts +46 -69
  28. package/lib/components/helpers.ts +13 -6
  29. package/lib/components/hero.ts +107 -95
  30. package/lib/components/icon.ts +160 -0
  31. package/lib/components/icons.ts +175 -0
  32. package/lib/components/include.ts +153 -5
  33. package/lib/components/input.ts +174 -374
  34. package/lib/components/kpicard.ts +16 -16
  35. package/lib/components/list.ts +378 -240
  36. package/lib/components/loading.ts +142 -211
  37. package/lib/components/menu.ts +103 -97
  38. package/lib/components/modal.ts +138 -144
  39. package/lib/components/nav.ts +169 -90
  40. package/lib/components/paragraph.ts +49 -150
  41. package/lib/components/progress.ts +118 -200
  42. package/lib/components/radio.ts +297 -149
  43. package/lib/components/script.ts +19 -87
  44. package/lib/components/select.ts +184 -186
  45. package/lib/components/sidebar.ts +152 -140
  46. package/lib/components/style.ts +19 -82
  47. package/lib/components/switch.ts +258 -188
  48. package/lib/components/table.ts +1117 -170
  49. package/lib/components/tabs.ts +162 -145
  50. package/lib/components/theme-toggle.ts +108 -169
  51. package/lib/components/tooltip.ts +86 -157
  52. package/lib/components/write.ts +108 -127
  53. package/lib/jux.ts +86 -41
  54. package/machinery/build.js +466 -0
  55. package/machinery/compiler.js +354 -105
  56. package/machinery/server.js +23 -100
  57. package/machinery/watcher.js +153 -130
  58. package/package.json +1 -2
  59. package/presets/base.css +1166 -0
  60. package/presets/notion.css +2 -1975
  61. package/lib/adapters/base-adapter.js +0 -35
  62. package/lib/adapters/index.js +0 -33
  63. package/lib/adapters/mysql-adapter.js +0 -65
  64. package/lib/adapters/postgres-adapter.js +0 -70
  65. package/lib/adapters/sqlite-adapter.js +0 -56
  66. package/lib/components/areachart.ts +0 -1246
  67. package/lib/components/areachartsmooth.ts +0 -1380
  68. package/lib/components/barchart.ts +0 -1250
  69. package/lib/components/chart.ts +0 -127
  70. package/lib/components/doughnutchart.ts +0 -1191
  71. package/lib/components/footer.ts +0 -165
  72. package/lib/components/header.ts +0 -187
  73. package/lib/components/layout.ts +0 -239
  74. package/lib/components/main.ts +0 -137
  75. package/lib/layouts/default.jux +0 -8
  76. package/lib/layouts/figma.jux +0 -0
  77. /package/lib/{themes → components/charts/lib}/charts.js +0 -0
@@ -1,103 +1,86 @@
1
- import { getOrCreateContainer } from './helpers.js';
2
- import { State } from '../reactivity/state.js';
1
+ import { FormInput, FormInputState } from './base/FormInput.js';
2
+
3
+ // Event definitions
4
+ const TRIGGER_EVENTS = [] as const;
5
+ const CALLBACK_EVENTS = ['change'] as const;
3
6
 
4
- /**
5
- * Radio option
6
- */
7
7
  export interface RadioOption {
8
8
  label: string;
9
9
  value: string;
10
10
  disabled?: boolean;
11
11
  }
12
12
 
13
- /**
14
- * Radio component options
15
- */
16
13
  export interface RadioOptions {
17
14
  options?: RadioOption[];
18
15
  value?: string;
16
+ label?: string;
17
+ required?: boolean;
18
+ disabled?: boolean;
19
19
  name?: string;
20
- onChange?: (value: string) => void;
21
- orientation?: 'vertical' | 'horizontal';
20
+ orientation?: 'vertical' | 'horizontal'; // ✅ Add orientation
22
21
  style?: string;
23
22
  class?: string;
23
+ onValidate?: (value: string) => boolean | string;
24
24
  }
25
25
 
26
- /**
27
- * Radio component state
28
- */
29
- type RadioState = {
26
+ interface RadioState extends FormInputState {
30
27
  options: RadioOption[];
31
28
  value: string;
32
- name: string;
33
- orientation: string;
34
- style: string;
35
- class: string;
36
- };
37
-
38
- /**
39
- * Radio component - Radio button group
40
- *
41
- * Usage:
42
- * jux.radio('size', {
43
- * options: [
44
- * { label: 'Small', value: 's' },
45
- * { label: 'Medium', value: 'm' },
46
- * { label: 'Large', value: 'l' }
47
- * ],
48
- * value: 'm',
49
- * onChange: (val) => console.log(val)
50
- * }).render('#form');
51
- *
52
- * // Two-way binding
53
- * const sizeState = state('m');
54
- * jux.radio('size').bind(sizeState).render('#form');
55
- */
56
- export class Radio {
57
- state: RadioState;
58
- container: HTMLElement | null = null;
59
- _id: string;
60
- id: string;
61
- private _onChange?: (value: string) => void;
62
- private _boundState?: State<string>;
29
+ orientation: 'vertical' | 'horizontal'; // ✅ Add to state
30
+ }
63
31
 
64
- constructor(id: string, options: RadioOptions = {}) {
65
- this._id = id;
66
- this.id = id;
67
- this._onChange = options.onChange;
32
+ export class Radio extends FormInput<RadioState> {
33
+ private _radioInputs: HTMLInputElement[] = [];
68
34
 
69
- this.state = {
35
+ constructor(id: string, options: RadioOptions = {}) {
36
+ super(id, {
70
37
  options: options.options ?? [],
71
38
  value: options.value ?? '',
39
+ label: options.label ?? '',
40
+ required: options.required ?? false,
41
+ disabled: options.disabled ?? false,
72
42
  name: options.name ?? id,
73
- orientation: options.orientation ?? 'vertical',
43
+ orientation: options.orientation ?? 'vertical', // ✅ Default to vertical
74
44
  style: options.style ?? '',
75
- class: options.class ?? ''
76
- };
45
+ class: options.class ?? '',
46
+ errorMessage: undefined
47
+ });
48
+
49
+ if (options.onValidate) {
50
+ this._onValidate = options.onValidate;
51
+ }
77
52
  }
78
53
 
79
- /* -------------------------
80
- * Fluent API
81
- * ------------------------- */
54
+ protected getTriggerEvents(): readonly string[] {
55
+ return TRIGGER_EVENTS;
56
+ }
82
57
 
83
- options(value: RadioOption[]): this {
84
- this.state.options = value;
85
- return this;
58
+ protected getCallbackEvents(): readonly string[] {
59
+ return CALLBACK_EVENTS;
86
60
  }
87
61
 
88
- addOption(option: RadioOption): this {
89
- this.state.options = [...this.state.options, option];
62
+ /* ═════════════════════════════════════════════════════════════════
63
+ * FLUENT API
64
+ * ═════════════════════════════════════════════════════════════════ */
65
+
66
+ // ✅ Inherited from FormInput/BaseComponent:
67
+ // - label(), required(), name(), onValidate()
68
+ // - validate(), isValid()
69
+ // - style(), class()
70
+ // - bind(), sync(), renderTo()
71
+ // - disabled(), enable(), disable()
72
+
73
+ options(value: RadioOption[]): this {
74
+ this.state.options = value;
90
75
  return this;
91
76
  }
92
77
 
93
78
  value(value: string): this {
94
- this.state.value = value;
95
- this._updateElement();
96
- return this;
79
+ return this.setValue(value);
97
80
  }
98
81
 
99
- name(value: string): this {
100
- this.state.name = value;
82
+ addOption(option: RadioOption): this {
83
+ this.state.options = [...this.state.options, option];
101
84
  return this;
102
85
  }
103
86
 
@@ -106,128 +89,293 @@ export class Radio {
106
89
  return this;
107
90
  }
108
91
 
109
- style(value: string): this {
110
- this.state.style = value;
111
- return this;
112
- }
113
-
114
- class(value: string): this {
115
- this.state.class = value;
116
- return this;
117
- }
92
+ /* ═════════════════════════════════════════════════════════════════
93
+ * FORM INPUT IMPLEMENTATION
94
+ * ═════════════════════════════════════════════════════════════════ */
118
95
 
119
- onChange(handler: (value: string) => void): this {
120
- this._onChange = handler;
121
- return this;
96
+ getValue(): string {
97
+ return this.state.value;
122
98
  }
123
99
 
124
- /**
125
- * Two-way binding to state
126
- */
127
- bind(stateObj: State<string>): this {
128
- this._boundState = stateObj;
100
+ setValue(value: string): this {
101
+ this.state.value = value;
129
102
 
130
- // Update radio when state changes
131
- stateObj.subscribe((val) => {
132
- this.state.value = val;
133
- this._updateElement();
103
+ // Update all radio inputs
104
+ this._radioInputs.forEach(input => {
105
+ input.checked = input.value === value;
134
106
  });
135
107
 
136
- // Update state when radio changes
137
- this.onChange((value) => stateObj.set(value));
138
-
139
108
  return this;
140
109
  }
141
110
 
142
- /* -------------------------
143
- * Helpers
144
- * ------------------------- */
111
+ protected _validateValue(value: string): boolean | string {
112
+ const { required, options } = this.state;
145
113
 
146
- private _updateElement(): void {
147
- const inputs = document.querySelectorAll(`input[name="${this.state.name}"]`);
148
- inputs.forEach((input) => {
149
- const radioInput = input as HTMLInputElement;
150
- radioInput.checked = radioInput.value === this.state.value;
151
- });
114
+ if (required && !value) {
115
+ return 'Please select an option';
116
+ }
117
+
118
+ // Validate that value is one of the options
119
+ if (value && !options.some(opt => opt.value === value)) {
120
+ return 'Invalid option selected';
121
+ }
122
+
123
+ if (this._onValidate) {
124
+ const result = this._onValidate(value);
125
+ if (result !== true) {
126
+ return result || 'Invalid value';
127
+ }
128
+ }
129
+
130
+ return true;
152
131
  }
153
132
 
154
- getValue(): string {
155
- return this.state.value;
133
+ protected _buildInputElement(): HTMLElement {
134
+ // Radio uses a container, not a single input
135
+ const container = document.createElement('div');
136
+ container.className = 'jux-radio-options';
137
+ return container;
156
138
  }
157
139
 
158
- /* -------------------------
159
- * Render
160
- * ------------------------- */
140
+ /* ═════════════════════════════════════════════════════════════════
141
+ * RENDER
142
+ * ═════════════════════════════════════════════════════════════════ */
161
143
 
162
144
  render(targetId?: string): this {
163
- let container: HTMLElement;
164
-
165
- if (targetId) {
166
- const target = document.querySelector(targetId);
167
- if (!target || !(target instanceof HTMLElement)) {
168
- throw new Error(`Radio: Target element "${targetId}" not found`);
169
- }
170
- container = target;
171
- } else {
172
- container = getOrCreateContainer(this._id);
173
- }
145
+ const container = this._setupContainer(targetId);
174
146
 
175
- this.container = container;
176
- const { options, value, name, orientation, style, class: className } = this.state;
147
+ const { options, value, name, disabled, orientation, style, class: className } = this.state; // Destructure orientation
177
148
 
149
+ // Build wrapper
178
150
  const wrapper = document.createElement('div');
179
- wrapper.className = `jux-radio jux-radio-${orientation}`;
151
+ wrapper.className = 'jux-radio';
180
152
  wrapper.id = this._id;
181
- wrapper.setAttribute('role', 'radiogroup');
153
+ if (className) wrapper.className += ` ${className}`;
154
+ if (style) wrapper.setAttribute('style', style);
182
155
 
183
- if (className) {
184
- wrapper.className += ` ${className}`;
156
+ // Label
157
+ if (this.state.label) {
158
+ wrapper.appendChild(this._renderLabel());
185
159
  }
186
160
 
187
- if (style) {
188
- wrapper.setAttribute('style', style);
189
- }
161
+ // Radio options container
162
+ const optionsContainer = document.createElement('div');
163
+ optionsContainer.className = `jux-radio-options jux-radio-${orientation}`; // ✅ Add orientation class
164
+ this._inputElement = optionsContainer;
165
+
166
+ this._radioInputs = [];
190
167
 
191
- options.forEach((opt, index) => {
192
- const label = document.createElement('label');
193
- label.className = 'jux-radio-label';
168
+ options.forEach((option, index) => {
169
+ const radioWrapper = document.createElement('label');
170
+ radioWrapper.className = 'jux-radio-option';
194
171
 
195
172
  const input = document.createElement('input');
196
173
  input.type = 'radio';
197
174
  input.className = 'jux-radio-input';
198
- input.id = `${this._id}-${index}`;
175
+ input.id = `${this._id}-option-${index}`;
199
176
  input.name = name;
200
- input.value = opt.value;
201
- input.checked = opt.value === value;
202
- input.disabled = opt.disabled ?? false;
203
-
204
- input.addEventListener('change', (e) => {
205
- const target = e.target as HTMLInputElement;
206
- this.state.value = target.value;
207
- if (this._onChange) {
208
- this._onChange(target.value);
209
- }
177
+ input.value = option.value;
178
+ input.checked = option.value === value;
179
+ input.disabled = disabled || option.disabled || false;
180
+
181
+ this._radioInputs.push(input);
182
+
183
+ const radioMark = document.createElement('span');
184
+ radioMark.className = 'jux-radio-mark';
185
+
186
+ const labelText = document.createElement('span');
187
+ labelText.className = 'jux-radio-label-text';
188
+ labelText.textContent = option.label;
189
+
190
+ radioWrapper.appendChild(input);
191
+ radioWrapper.appendChild(radioMark);
192
+ radioWrapper.appendChild(labelText);
193
+
194
+ optionsContainer.appendChild(radioWrapper);
195
+ });
196
+
197
+ wrapper.appendChild(optionsContainer);
198
+
199
+ // Error element
200
+ wrapper.appendChild(this._renderError());
201
+
202
+ // Wire events
203
+ this._wireStandardEvents(wrapper);
204
+
205
+ // Wire radio-specific sync
206
+ const valueSync = this._syncBindings.find(b => b.property === 'value');
207
+
208
+ if (valueSync) {
209
+ const { stateObj, toState, toComponent } = valueSync;
210
+
211
+ const transformToState = toState || ((v: string) => v);
212
+ const transformToComponent = toComponent || ((v: any) => String(v));
213
+
214
+ let isUpdating = false;
215
+
216
+ // State → Component
217
+ stateObj.subscribe((val: any) => {
218
+ if (isUpdating) return;
219
+ const transformed = transformToComponent(val);
220
+ this.setValue(transformed);
210
221
  });
211
222
 
212
- label.appendChild(input);
223
+ // Component → State
224
+ this._radioInputs.forEach(input => {
225
+ input.addEventListener('change', () => {
226
+ if (isUpdating) return;
227
+ isUpdating = true;
213
228
 
214
- const span = document.createElement('span');
215
- span.className = 'jux-radio-text';
216
- span.textContent = opt.label;
217
- label.appendChild(span);
229
+ const selectedValue = input.value;
230
+ this.state.value = selectedValue;
231
+ this._clearError();
232
+
233
+ const transformed = transformToState(selectedValue);
234
+ stateObj.set(transformed);
235
+
236
+ // 🎯 Fire the change callback event
237
+ this._triggerCallback('change', selectedValue);
238
+
239
+ setTimeout(() => { isUpdating = false; }, 0);
240
+ });
241
+ });
242
+ } else {
243
+ // Default behavior without sync
244
+ this._radioInputs.forEach(input => {
245
+ input.addEventListener('change', () => {
246
+ this.state.value = input.value;
247
+ this._clearError();
248
+
249
+ // 🎯 Fire the change callback event
250
+ this._triggerCallback('change', input.value);
251
+ });
252
+ });
253
+ }
218
254
 
219
- wrapper.appendChild(label);
255
+ // Always add blur validation
256
+ this._radioInputs.forEach(input => {
257
+ input.addEventListener('blur', () => {
258
+ this.validate();
259
+ });
220
260
  });
221
261
 
262
+ // Sync label changes
263
+ const labelSync = this._syncBindings.find(b => b.property === 'label');
264
+ if (labelSync) {
265
+ const transform = labelSync.toComponent || ((v: any) => String(v));
266
+ labelSync.stateObj.subscribe((val: any) => {
267
+ this.label(transform(val));
268
+ });
269
+ }
270
+
222
271
  container.appendChild(wrapper);
272
+ this._injectRadioStyles();
273
+
223
274
  return this;
224
275
  }
225
276
 
226
- renderTo(juxComponent: any): this {
227
- if (!juxComponent?._id) {
228
- throw new Error('Radio.renderTo: Invalid component');
229
- }
230
- return this.render(`#${juxComponent._id}`);
277
+ private _injectRadioStyles(): void {
278
+ const styleId = 'jux-radio-styles';
279
+ if (document.getElementById(styleId)) return;
280
+
281
+ const style = document.createElement('style');
282
+ style.id = styleId;
283
+ style.textContent = `
284
+ .jux-radio {
285
+ margin-bottom: 12px;
286
+ }
287
+
288
+ .jux-radio-options {
289
+ display: flex;
290
+ gap: 8px;
291
+ }
292
+
293
+ /* ✅ Vertical orientation (default) */
294
+ .jux-radio-vertical {
295
+ flex-direction: column;
296
+ }
297
+
298
+ /* ✅ Horizontal orientation */
299
+ .jux-radio-horizontal {
300
+ flex-direction: row;
301
+ flex-wrap: wrap;
302
+ }
303
+
304
+ .jux-radio-option {
305
+ display: inline-flex;
306
+ align-items: center;
307
+ cursor: pointer;
308
+ user-select: none;
309
+ position: relative;
310
+ }
311
+
312
+ .jux-radio-input {
313
+ position: absolute;
314
+ opacity: 0;
315
+ cursor: pointer;
316
+ height: 0;
317
+ width: 0;
318
+ }
319
+
320
+ .jux-radio-mark {
321
+ position: relative;
322
+ height: 20px;
323
+ width: 20px;
324
+ border: 2px solid #d1d5db;
325
+ border-radius: 50%;
326
+ background-color: white;
327
+ transition: all 0.2s;
328
+ display: flex;
329
+ align-items: center;
330
+ justify-content: center;
331
+ margin-right: 8px;
332
+ }
333
+
334
+ .jux-radio-mark::after {
335
+ content: '';
336
+ position: absolute;
337
+ width: 10px;
338
+ height: 10px;
339
+ border-radius: 50%;
340
+ background-color: white;
341
+ opacity: 0;
342
+ transition: opacity 0.2s;
343
+ }
344
+
345
+ .jux-radio-input:checked ~ .jux-radio-mark {
346
+ background-color: #3b82f6;
347
+ border-color: #3b82f6;
348
+ }
349
+
350
+ .jux-radio-input:checked ~ .jux-radio-mark::after {
351
+ opacity: 1;
352
+ }
353
+
354
+ .jux-radio-input:disabled ~ .jux-radio-mark {
355
+ background-color: #f3f4f6;
356
+ border-color: #d1d5db;
357
+ cursor: not-allowed;
358
+ }
359
+
360
+ .jux-radio-input:disabled ~ .jux-radio-label-text {
361
+ color: #9ca3af;
362
+ cursor: not-allowed;
363
+ }
364
+
365
+ .jux-radio-input:focus ~ .jux-radio-mark {
366
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
367
+ }
368
+
369
+ .jux-radio-input.jux-input-invalid ~ .jux-radio-mark {
370
+ border-color: #ef4444;
371
+ }
372
+
373
+ .jux-radio-label-text {
374
+ font-size: 14px;
375
+ color: #374151;
376
+ }
377
+ `;
378
+ document.head.appendChild(style);
231
379
  }
232
380
  }
233
381
 
@@ -6,106 +6,38 @@ import { ErrorHandler } from './error-handler.js';
6
6
  * Auto-renders when created or modified
7
7
  */
8
8
  export class Script {
9
- private _content: string = '';
10
- private _type: string = '';
11
- private _element: HTMLScriptElement | null = null;
9
+ private _id: string;
10
+ private code: string;
12
11
 
13
- constructor(content: string = '') {
14
- this._content = content;
15
-
16
- // Auto-render if content provided
17
- if (content) {
18
- this.render();
19
- }
20
- }
21
-
22
- /**
23
- * Set inline JavaScript content
24
- */
25
- content(js: string): this {
26
- this._content = js;
27
- this.render();
28
- return this;
12
+ constructor(code: string, id?: string) {
13
+ // ID only for deduplication, auto-generate if not provided
14
+ this._id = id || `jux-script-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
15
+ this.code = code;
29
16
  }
30
17
 
31
- /**
32
- * Set script type (e.g., 'module', 'text/javascript')
33
- */
34
- type(value: string): this {
35
- this._type = value;
36
- this.render();
37
- return this;
38
- }
39
-
40
- /**
41
- * Set as module script
42
- */
43
- module(): this {
44
- this._type = 'module';
45
- this.render();
46
- return this;
47
- }
48
-
49
- /**
50
- * Render the inline script element
51
- */
52
18
  render(): this {
53
- if (typeof document === 'undefined') {
19
+ // Check if script with this ID already exists
20
+ if (document.getElementById(this._id)) {
54
21
  return this;
55
22
  }
56
23
 
57
- try {
58
- // Remove existing element if it exists
59
- this.remove();
60
-
61
- const script = document.createElement('script');
62
- script.textContent = this._content;
63
-
64
- if (this._type) {
65
- script.type = this._type;
66
- }
67
-
68
- document.head.appendChild(script);
69
- this._element = script;
70
-
71
- console.log('✓ Inline script rendered');
72
- } catch (error: any) {
73
- ErrorHandler.captureError({
74
- component: 'Script',
75
- method: 'render',
76
- message: error.message,
77
- stack: error.stack,
78
- timestamp: new Date(),
79
- context: {
80
- type: this._type || 'inline',
81
- error: 'runtime_exception'
82
- }
83
- });
84
- }
24
+ const script = document.createElement('script');
25
+ script.id = this._id;
26
+ script.textContent = this.code;
27
+ document.head.appendChild(script);
85
28
 
86
29
  return this;
87
30
  }
88
31
 
89
- /**
90
- * Remove the script from the document
91
- */
92
- remove(): this {
93
- if (this._element && this._element.parentNode) {
94
- this._element.parentNode.removeChild(this._element);
95
- this._element = null;
32
+ remove(): void {
33
+ const existing = document.getElementById(this._id);
34
+ if (existing) {
35
+ existing.remove();
96
36
  }
97
- return this;
98
37
  }
99
38
  }
100
39
 
101
- /**
102
- * Factory function
103
- *
104
- * Usage:
105
- * jux.script('console.log("Hello")');
106
- * jux.script().content('alert("Hi")');
107
- * jux.script('export const x = 1;').module();
108
- */
109
- export function script(content: string = ''): Script {
110
- return new Script(content);
40
+ // ✅ ID is optional
41
+ export function script(code: string, id?: string): Script {
42
+ return new Script(code, id);
111
43
  }