juxscript 1.0.20 → 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 (76) hide show
  1. package/bin/cli.js +121 -72
  2. package/lib/components/alert.ts +143 -92
  3. package/lib/components/badge.ts +93 -94
  4. package/lib/components/base/BaseComponent.ts +397 -0
  5. package/lib/components/base/FormInput.ts +322 -0
  6. package/lib/components/button.ts +40 -131
  7. package/lib/components/card.ts +57 -79
  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/{chart-types.ts → charts/lib/chart-types.ts} +1 -1
  13. package/lib/components/{chart-utils.ts → charts/lib/chart-utils.ts} +1 -1
  14. package/lib/components/{chart.ts → charts/lib/chart.ts} +3 -3
  15. package/lib/components/checkbox.ts +255 -204
  16. package/lib/components/code.ts +31 -78
  17. package/lib/components/container.ts +113 -130
  18. package/lib/components/data.ts +37 -5
  19. package/lib/components/datepicker.ts +180 -147
  20. package/lib/components/dialog.ts +218 -221
  21. package/lib/components/divider.ts +63 -87
  22. package/lib/components/docs-data.json +498 -2404
  23. package/lib/components/dropdown.ts +191 -236
  24. package/lib/components/element.ts +196 -145
  25. package/lib/components/fileupload.ts +253 -167
  26. package/lib/components/guard.ts +92 -0
  27. package/lib/components/heading.ts +31 -97
  28. package/lib/components/helpers.ts +13 -6
  29. package/lib/components/hero.ts +51 -114
  30. package/lib/components/icon.ts +33 -120
  31. package/lib/components/icons.ts +2 -1
  32. package/lib/components/include.ts +76 -3
  33. package/lib/components/input.ts +155 -407
  34. package/lib/components/kpicard.ts +16 -16
  35. package/lib/components/list.ts +358 -261
  36. package/lib/components/loading.ts +142 -211
  37. package/lib/components/menu.ts +63 -152
  38. package/lib/components/modal.ts +42 -129
  39. package/lib/components/nav.ts +79 -101
  40. package/lib/components/paragraph.ts +38 -102
  41. package/lib/components/progress.ts +108 -166
  42. package/lib/components/radio.ts +283 -234
  43. package/lib/components/script.ts +19 -87
  44. package/lib/components/select.ts +189 -199
  45. package/lib/components/sidebar.ts +110 -141
  46. package/lib/components/style.ts +19 -82
  47. package/lib/components/switch.ts +254 -183
  48. package/lib/components/table.ts +1078 -208
  49. package/lib/components/tabs.ts +42 -106
  50. package/lib/components/theme-toggle.ts +73 -165
  51. package/lib/components/tooltip.ts +85 -316
  52. package/lib/components/write.ts +108 -127
  53. package/lib/jux.ts +67 -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 -1
  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 -1128
  67. package/lib/components/areachartsmooth.ts +0 -1380
  68. package/lib/components/barchart.ts +0 -1322
  69. package/lib/components/doughnutchart.ts +0 -1259
  70. package/lib/components/footer.ts +0 -165
  71. package/lib/components/header.ts +0 -187
  72. package/lib/components/layout.ts +0 -239
  73. package/lib/components/main.ts +0 -137
  74. package/lib/layouts/default.jux +0 -8
  75. package/lib/layouts/figma.jux +0 -0
  76. /package/lib/{themes → components/charts/lib}/charts.js +0 -0
@@ -0,0 +1,322 @@
1
+ import { BaseComponent } from './BaseComponent.js';
2
+
3
+ /**
4
+ * Base state interface for all form inputs
5
+ */
6
+ export interface FormInputState extends Record<string, any> {
7
+ label: string;
8
+ required: boolean;
9
+ disabled: boolean;
10
+ name: string;
11
+ style: string;
12
+ class: string;
13
+ errorMessage?: string;
14
+ }
15
+
16
+ /**
17
+ * Abstract base class for all form input components
18
+ * Extends BaseComponent with form-specific functionality
19
+ */
20
+ export abstract class FormInput<TState extends FormInputState> extends BaseComponent<TState> {
21
+ protected _inputElement: HTMLElement | null = null;
22
+ protected _labelElement: HTMLLabelElement | null = null;
23
+ protected _errorElement: HTMLElement | null = null;
24
+ protected _onValidate?: (value: any) => boolean | string;
25
+ protected _hasBeenValidated: boolean = false; // NEW: Track if user has submitted/validated
26
+
27
+ /* ═════════════════════════════════════════════════════════════════
28
+ * ABSTRACT METHODS (Child must implement)
29
+ * ═════════════════════════════════════════════════════════════════ */
30
+
31
+ /**
32
+ * Get the current value of the input
33
+ */
34
+ abstract getValue(): any;
35
+
36
+ /**
37
+ * Set the value of the input
38
+ */
39
+ abstract setValue(value: any): this;
40
+
41
+ /**
42
+ * Build the actual input element (input, select, textarea, etc.)
43
+ */
44
+ protected abstract _buildInputElement(): HTMLElement;
45
+
46
+ /**
47
+ * Validate the current value
48
+ */
49
+ protected abstract _validateValue(value: any): boolean | string;
50
+
51
+ /* ═════════════════════════════════════════════════════════════════
52
+ * COMMON FORM INPUT API
53
+ * ═════════════════════════════════════════════════════════════════ */
54
+
55
+ label(value: string): this {
56
+ this.state.label = value;
57
+ if (this._labelElement) {
58
+ this._labelElement.textContent = value;
59
+ }
60
+ return this;
61
+ }
62
+
63
+ required(value: boolean): this {
64
+ this.state.required = value;
65
+ return this;
66
+ }
67
+
68
+ name(value: string): this {
69
+ this.state.name = value;
70
+ return this;
71
+ }
72
+
73
+ onValidate(handler: (value: any) => boolean | string): this {
74
+ this._onValidate = handler;
75
+ return this;
76
+ }
77
+
78
+ /* ═════════════════════════════════════════════════════════════════
79
+ * VALIDATION
80
+ * ═════════════════════════════════════════════════════════════════ */
81
+
82
+ /**
83
+ * Validate the current value and show/hide errors
84
+ */
85
+ validate(): boolean {
86
+ this._hasBeenValidated = true; // Mark as validated
87
+ const value = this.getValue();
88
+ const result = this._validateValue(value);
89
+
90
+ if (result === true) {
91
+ this._clearError();
92
+ return true;
93
+ } else {
94
+ this._showError(result as string);
95
+ return false;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Check if current value is valid without showing errors
101
+ */
102
+ isValid(): boolean {
103
+ const value = this.getValue();
104
+ return this._validateValue(value) === true;
105
+ }
106
+
107
+ /**
108
+ * Show error message
109
+ */
110
+ protected _showError(message: string): void {
111
+ if (this._errorElement) {
112
+ this._errorElement.textContent = message;
113
+ this._errorElement.style.display = 'block';
114
+ }
115
+
116
+ if (this._inputElement) {
117
+ this._inputElement.classList.add('jux-input-invalid');
118
+ }
119
+
120
+ this.state.errorMessage = message;
121
+ }
122
+
123
+ /**
124
+ * Clear error message
125
+ */
126
+ protected _clearError(): void {
127
+ if (this._errorElement) {
128
+ this._errorElement.textContent = '';
129
+ this._errorElement.style.display = 'none';
130
+ }
131
+
132
+ if (this._inputElement) {
133
+ this._inputElement.classList.remove('jux-input-invalid');
134
+ }
135
+
136
+ this.state.errorMessage = undefined;
137
+ }
138
+
139
+ /* ═════════════════════════════════════════════════════════════════
140
+ * COMMON RENDER HELPERS
141
+ * ═════════════════════════════════════════════════════════════════ */
142
+
143
+ /**
144
+ * Build label element
145
+ */
146
+ protected _renderLabel(): HTMLLabelElement {
147
+ const { label, required } = this.state;
148
+
149
+ const labelEl = document.createElement('label');
150
+ labelEl.className = 'jux-input-label';
151
+ labelEl.htmlFor = `${this._id}-input`;
152
+ labelEl.textContent = label;
153
+
154
+ if (required) {
155
+ const requiredSpan = document.createElement('span');
156
+ requiredSpan.className = 'jux-input-required';
157
+ requiredSpan.textContent = ' *';
158
+ labelEl.appendChild(requiredSpan);
159
+ }
160
+
161
+ this._labelElement = labelEl;
162
+ return labelEl;
163
+ }
164
+
165
+ /**
166
+ * Build error element
167
+ */
168
+ protected _renderError(): HTMLElement {
169
+ const errorEl = document.createElement('div');
170
+ errorEl.className = 'jux-input-error';
171
+ errorEl.id = `${this._id}-error`;
172
+ errorEl.style.display = 'none';
173
+
174
+ this._errorElement = errorEl;
175
+ return errorEl;
176
+ }
177
+
178
+ /**
179
+ * Wire up two-way sync for value property
180
+ */
181
+ protected _wireFormSync(inputElement: HTMLElement, eventName: string = 'input'): void {
182
+ const valueSync = this._syncBindings.find(b => b.property === 'value');
183
+
184
+ if (valueSync) {
185
+ const { stateObj, toState, toComponent } = valueSync;
186
+
187
+ // Default transforms
188
+ const transformToState = toState || ((v: any) => v);
189
+ const transformToComponent = toComponent || ((v: any) => v);
190
+
191
+ let isUpdating = false;
192
+
193
+ // State → Component
194
+ stateObj.subscribe((val: any) => {
195
+ if (isUpdating) return;
196
+ const transformed = transformToComponent(val);
197
+ this.setValue(transformed);
198
+ });
199
+
200
+ // Component → State
201
+ inputElement.addEventListener(eventName, () => {
202
+ if (isUpdating) return;
203
+ isUpdating = true;
204
+
205
+ const value = this.getValue();
206
+ const transformed = transformToState(value);
207
+ this._clearError();
208
+
209
+ stateObj.set(transformed);
210
+
211
+ setTimeout(() => { isUpdating = false; }, 0);
212
+ });
213
+ } else {
214
+ // Default behavior without sync
215
+ inputElement.addEventListener(eventName, () => {
216
+ this._clearError();
217
+ });
218
+ }
219
+
220
+ // Only validate on blur IF the field has been validated before (e.g., after submit)
221
+ inputElement.addEventListener('blur', () => {
222
+ if (this._hasBeenValidated) {
223
+ this.validate();
224
+ }
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Inject default form input styles
230
+ */
231
+ protected _injectFormStyles(): void {
232
+ const styleId = 'jux-form-input-styles';
233
+ if (document.getElementById(styleId)) return;
234
+
235
+ const style = document.createElement('style');
236
+ style.id = styleId;
237
+ style.textContent = `
238
+ .jux-input {
239
+ margin-bottom: 16px;
240
+ }
241
+
242
+ .jux-input-container {
243
+ position: relative;
244
+ }
245
+
246
+ .jux-input-with-icon .jux-input-element {
247
+ padding-left: 40px;
248
+ }
249
+
250
+ .jux-input-icon {
251
+ position: absolute;
252
+ left: 12px;
253
+ top: 50%;
254
+ transform: translateY(-50%);
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ color: #6b7280;
259
+ pointer-events: none;
260
+ }
261
+
262
+ .jux-input-icon svg {
263
+ width: 18px;
264
+ height: 18px;
265
+ }
266
+
267
+ .jux-input-label {
268
+ display: block;
269
+ margin-bottom: 6px;
270
+ font-weight: 500;
271
+ color: #374151;
272
+ }
273
+
274
+ .jux-input-required {
275
+ color: #ef4444;
276
+ }
277
+
278
+ .jux-input-element {
279
+ width: 100%;
280
+ padding: 8px 12px;
281
+ border: 1px solid #d1d5db;
282
+ border-radius: 6px;
283
+ font-size: 14px;
284
+ transition: border-color 0.2s;
285
+ box-sizing: border-box;
286
+ }
287
+
288
+ .jux-input-element:focus {
289
+ outline: none;
290
+ border-color: #3b82f6;
291
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
292
+ }
293
+
294
+ .jux-input-element:disabled {
295
+ background-color: #f3f4f6;
296
+ cursor: not-allowed;
297
+ }
298
+
299
+ .jux-input-element.jux-input-invalid {
300
+ border-color: #ef4444;
301
+ }
302
+
303
+ .jux-input-element.jux-input-invalid:focus {
304
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
305
+ }
306
+
307
+ .jux-input-error {
308
+ color: #ef4444;
309
+ font-size: 12px;
310
+ margin-top: 4px;
311
+ }
312
+
313
+ .jux-input-counter {
314
+ text-align: right;
315
+ font-size: 12px;
316
+ color: #6b7280;
317
+ margin-top: 4px;
318
+ }
319
+ `;
320
+ document.head.appendChild(style);
321
+ }
322
+ }
@@ -1,10 +1,10 @@
1
- import { getOrCreateContainer } from './helpers.js';
2
- import { State } from '../reactivity/state.js';
1
+ import { BaseComponent } from './base/BaseComponent.js';
3
2
  import { renderIcon } from './icons.js';
4
3
 
5
- /**
6
- * Button component options
7
- */
4
+ // Event definitions
5
+ const TRIGGER_EVENTS = [] as const;
6
+ const CALLBACK_EVENTS = ['click'] as const; // ✅ Button fires click events
7
+
8
8
  export interface ButtonOptions {
9
9
  label?: string;
10
10
  icon?: string;
@@ -19,9 +19,6 @@ export interface ButtonOptions {
19
19
  class?: string;
20
20
  }
21
21
 
22
- /**
23
- * Button component state
24
- */
25
22
  type ButtonState = {
26
23
  label: string;
27
24
  icon: string;
@@ -36,31 +33,13 @@ type ButtonState = {
36
33
  class: string;
37
34
  };
38
35
 
39
- /**
40
- * Button component
41
- */
42
- export class Button {
43
- state: ButtonState;
44
- container: HTMLElement | null = null;
45
- _id: string;
46
- id: string;
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
- }> = [];
36
+ export class Button extends BaseComponent<ButtonState> {
37
+ private _buttonElement: HTMLButtonElement | null = null; // ✅ Store button reference
56
38
 
57
39
  constructor(id: string, options?: ButtonOptions) {
58
- this._id = id;
59
- this.id = id;
60
-
61
40
  const opts = options || {};
62
41
 
63
- this.state = {
42
+ super(id, {
64
43
  label: opts.label ?? 'Button',
65
44
  icon: opts.icon ?? '',
66
45
  variant: opts.variant ?? 'primary',
@@ -72,12 +51,22 @@ export class Button {
72
51
  type: opts.type ?? 'button',
73
52
  style: opts.style ?? '',
74
53
  class: opts.class ?? ''
75
- };
54
+ });
55
+ }
56
+
57
+ protected getTriggerEvents(): readonly string[] {
58
+ return TRIGGER_EVENTS;
76
59
  }
77
60
 
78
- /* -------------------------
79
- * Fluent API
80
- * ------------------------- */
61
+ protected getCallbackEvents(): readonly string[] {
62
+ return CALLBACK_EVENTS;
63
+ }
64
+
65
+ /* ═════════════════════════════════════════════════════════════════
66
+ * FLUENT API
67
+ * ═════════════════════════════════════════════════════════════════ */
68
+
69
+ // ✅ Inherited from BaseComponent
81
70
 
82
71
  label(value: string): this {
83
72
  this.state.label = value;
@@ -99,16 +88,6 @@ export class Button {
99
88
  return this;
100
89
  }
101
90
 
102
- disabled(value: boolean): this {
103
- this.state.disabled = value;
104
- return this;
105
- }
106
-
107
- loading(value: boolean): this {
108
- this.state.loading = value;
109
- return this;
110
- }
111
-
112
91
  iconPosition(value: 'left' | 'right'): this {
113
92
  this.state.iconPosition = value;
114
93
  return this;
@@ -124,62 +103,30 @@ export class Button {
124
103
  return this;
125
104
  }
126
105
 
127
- style(value: string): this {
128
- this.state.style = value;
129
- return this;
130
- }
131
-
132
- class(value: string): this {
133
- this.state.class = value;
134
- return this;
135
- }
106
+ /* ═════════════════════════════════════════════════════════════════
107
+ * RENDER
108
+ * ═════════════════════════════════════════════════════════════════ */
136
109
 
137
110
  /**
138
- * Bind DOM events (click, hover, etc.)
139
- * Stores handlers for wiring in render()
111
+ * Override visible() to update the actual button element
140
112
  */
141
- bind(event: string, handler: Function): this {
142
- this._bindings.push({ event, handler });
143
- return this;
144
- }
113
+ visible(value: boolean): this {
114
+ (this.state as any).visible = value;
145
115
 
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}"`);
116
+ // Update the button element directly, not the container
117
+ if (this._buttonElement) {
118
+ this._buttonElement.style.display = value ? '' : 'none';
153
119
  }
154
- this._syncBindings.push({ property, stateObj, toState, toComponent });
120
+
155
121
  return this;
156
122
  }
157
123
 
158
- /* -------------------------
159
- * Render
160
- * ------------------------- */
161
-
162
124
  render(targetId?: string): this {
163
- // === 1. SETUP: Get or create container ===
164
- let container: HTMLElement;
165
- if (targetId) {
166
- const target = document.querySelector(targetId);
167
- if (!target || !(target instanceof HTMLElement)) {
168
- throw new Error(`Button: Target "${targetId}" not found`);
169
- }
170
- container = target;
171
- } else {
172
- container = getOrCreateContainer(this._id);
173
- }
174
- this.container = container;
175
-
176
- // === 2. PREPARE: Destructure state ===
125
+ const container = this._setupContainer(targetId);
177
126
  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
127
 
181
- // === 3. BUILD: Create DOM elements ===
182
128
  const button = document.createElement('button');
129
+ this._buttonElement = button; // ✅ Store reference
183
130
  button.className = `jux-button jux-button-${variant} jux-button-${size}`;
184
131
  button.id = this._id;
185
132
  button.disabled = disabled || loading;
@@ -205,46 +152,15 @@ export class Button {
205
152
  button.appendChild(iconEl);
206
153
  }
207
154
 
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));
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
- });
155
+ button.addEventListener('click', (e) => {
156
+ if (!disabled && !loading) {
157
+ this._triggerCallback('click', e);
244
158
  }
245
159
  });
246
160
 
247
- // === 5. RENDER: Append to DOM and finalize ===
161
+ this._wireStandardEvents(button);
162
+ this._wireAllSyncs();
163
+
248
164
  container.appendChild(button);
249
165
 
250
166
  requestAnimationFrame(() => {
@@ -255,13 +171,6 @@ export class Button {
255
171
 
256
172
  return this;
257
173
  }
258
-
259
- renderTo(juxComponent: any): this {
260
- if (!juxComponent?._id) {
261
- throw new Error('Button.renderTo: Invalid component');
262
- }
263
- return this.render(`#${juxComponent._id}`);
264
- }
265
174
  }
266
175
 
267
176
  export function button(id: string, options?: ButtonOptions): Button {