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,79 +1,71 @@
1
- import { getOrCreateContainer } from './helpers.js';
1
+ import { FormInput, FormInputState } from './base/FormInput.js';
2
+ import { renderIcon } from './icons.js';
3
+
4
+ // Event definitions
5
+ const TRIGGER_EVENTS = [] as const;
6
+ const CALLBACK_EVENTS = ['change', 'filesSelected', 'clear'] as const;
2
7
 
3
- /**
4
- * FileUpload component options
5
- */
6
8
  export interface FileUploadOptions {
9
+ label?: string;
7
10
  accept?: string;
8
11
  multiple?: boolean;
9
- maxSize?: number;
10
12
  disabled?: boolean;
11
- text?: string;
12
- dragText?: string;
13
- onChange?: (files: FileList) => void;
14
- onError?: (error: string) => void;
13
+ name?: string;
14
+ icon?: string;
15
+ required?: boolean;
15
16
  style?: string;
16
17
  class?: string;
18
+ onValidate?: (files: File[]) => boolean | string;
17
19
  }
18
20
 
19
- /**
20
- * FileUpload component state
21
- */
22
- type FileUploadState = {
21
+ interface FileUploadState extends FormInputState {
22
+ files: File[];
23
23
  accept: string;
24
24
  multiple: boolean;
25
- maxSize: number;
26
- disabled: boolean;
27
- text: string;
28
- dragText: string;
29
- style: string;
30
- class: string;
31
- files: File[];
32
- };
33
-
34
- /**
35
- * FileUpload component - Drag & drop file input
36
- *
37
- * Usage:
38
- * jux.fileupload('docs', {
39
- * accept: '.pdf,.doc,.docx',
40
- * multiple: true,
41
- * maxSize: 5242880, // 5MB
42
- * text: 'Click to upload or drag and drop',
43
- * onChange: (files) => console.log(files),
44
- * onError: (err) => console.error(err)
45
- * }).render('#form');
46
- */
47
- export class FileUpload {
48
- state: FileUploadState;
49
- container: HTMLElement | null = null;
50
- _id: string;
51
- id: string;
52
- private _onChange?: (files: FileList) => void;
53
- private _onError?: (error: string) => void;
25
+ icon: string;
26
+ }
54
27
 
55
- constructor(id: string, options: FileUploadOptions = {}) {
56
- this._id = id;
57
- this.id = id;
58
- this._onChange = options.onChange;
59
- this._onError = options.onError;
28
+ export class FileUpload extends FormInput<FileUploadState> {
29
+ private _fileListElement: HTMLElement | null = null;
60
30
 
61
- this.state = {
62
- accept: options.accept ?? '*',
31
+ constructor(id: string, options: FileUploadOptions = {}) {
32
+ super(id, {
33
+ files: [],
34
+ accept: options.accept ?? '',
63
35
  multiple: options.multiple ?? false,
64
- maxSize: options.maxSize ?? 10485760, // 10MB default
36
+ icon: options.icon ?? 'upload',
37
+ label: options.label ?? '',
38
+ required: options.required ?? false,
65
39
  disabled: options.disabled ?? false,
66
- text: options.text ?? 'Click to upload or drag and drop',
67
- dragText: options.dragText ?? 'Drop files here',
40
+ name: options.name ?? id,
68
41
  style: options.style ?? '',
69
42
  class: options.class ?? '',
70
- files: []
71
- };
43
+ errorMessage: undefined
44
+ });
45
+
46
+ if (options.onValidate) {
47
+ this._onValidate = options.onValidate;
48
+ }
49
+ }
50
+
51
+ protected getTriggerEvents(): readonly string[] {
52
+ return TRIGGER_EVENTS;
53
+ }
54
+
55
+ protected getCallbackEvents(): readonly string[] {
56
+ return CALLBACK_EVENTS;
72
57
  }
73
58
 
74
- /* -------------------------
75
- * Fluent API
76
- * ------------------------- */
59
+ /* ═════════════════════════════════════════════════════════════════
60
+ * FLUENT API
61
+ * ═════════════════════════════════════════════════════════════════ */
62
+
63
+ // ✅ Inherited from FormInput/BaseComponent:
64
+ // - label(), required(), name(), onValidate()
65
+ // - validate(), isValid()
66
+ // - style(), class()
67
+ // - bind(), sync(), renderTo()
68
+ // - disabled(), enable(), disable()
77
69
 
78
70
  accept(value: string): this {
79
71
  this.state.accept = value;
@@ -85,232 +77,297 @@ export class FileUpload {
85
77
  return this;
86
78
  }
87
79
 
88
- maxSize(value: number): this {
89
- this.state.maxSize = value;
80
+ icon(value: string): this {
81
+ this.state.icon = value;
90
82
  return this;
91
83
  }
92
84
 
93
- disabled(value: boolean): this {
94
- this.state.disabled = value;
85
+ clear(): this {
86
+ this.state.files = [];
87
+ if (this._inputElement) {
88
+ (this._inputElement as HTMLInputElement).value = '';
89
+ }
90
+ if (this._fileListElement) {
91
+ this._updateFileList([]);
92
+ }
93
+ // 🎯 Fire the clear callback event
94
+ this._triggerCallback('clear');
95
95
  return this;
96
96
  }
97
97
 
98
- text(value: string): this {
99
- this.state.text = value;
100
- return this;
101
- }
98
+ /* ═════════════════════════════════════════════════════════════════
99
+ * FORM INPUT IMPLEMENTATION
100
+ * ═════════════════════════════════════════════════════════════════ */
102
101
 
103
- dragText(value: string): this {
104
- this.state.dragText = value;
105
- return this;
102
+ getValue(): File[] {
103
+ return this.state.files;
106
104
  }
107
105
 
108
- style(value: string): this {
109
- this.state.style = value;
106
+ setValue(files: File[]): this {
107
+ this.state.files = files;
108
+ if (this._fileListElement) {
109
+ this._updateFileList(files);
110
+ }
110
111
  return this;
111
112
  }
112
113
 
113
- class(value: string): this {
114
- this.state.class = value;
115
- return this;
114
+ getFiles(): File[] {
115
+ return this.getValue();
116
116
  }
117
117
 
118
- onChange(handler: (files: FileList) => void): this {
119
- this._onChange = handler;
120
- return this;
121
- }
118
+ protected _validateValue(files: File[]): boolean | string {
119
+ const { required } = this.state;
122
120
 
123
- onError(handler: (error: string) => void): this {
124
- this._onError = handler;
125
- return this;
126
- }
121
+ if (required && files.length === 0) {
122
+ return 'Please select at least one file';
123
+ }
127
124
 
128
- /* -------------------------
129
- * Helpers
130
- * ------------------------- */
131
-
132
- private _validateFiles(files: FileList): boolean {
133
- for (let i = 0; i < files.length; i++) {
134
- const file = files[i];
135
- if (file.size > this.state.maxSize) {
136
- const maxMB = Math.round(this.state.maxSize / 1048576);
137
- const error = `File "${file.name}" exceeds maximum size of ${maxMB}MB`;
138
- if (this._onError) {
139
- this._onError(error);
140
- }
141
- return false;
125
+ if (this._onValidate) {
126
+ const result = this._onValidate(files);
127
+ if (result !== true) {
128
+ return result || 'Invalid files';
142
129
  }
143
130
  }
131
+
144
132
  return true;
145
133
  }
146
134
 
147
- private _handleFiles(files: FileList): void {
148
- if (this._validateFiles(files)) {
149
- this.state.files = Array.from(files);
150
- this._updateFileList();
151
- if (this._onChange) {
152
- this._onChange(files);
153
- }
154
- }
155
- }
135
+ protected _buildInputElement(): HTMLElement {
136
+ const { accept, multiple, required, disabled, name } = this.state;
156
137
 
157
- private _updateFileList(): void {
158
- const fileList = document.getElementById(`${this._id}-list`);
159
- if (fileList) {
160
- fileList.innerHTML = '';
161
- this.state.files.forEach((file, index) => {
162
- const item = document.createElement('div');
163
- item.className = 'jux-fileupload-file';
164
-
165
- const name = document.createElement('span');
166
- name.className = 'jux-fileupload-file-name';
167
- name.textContent = file.name;
168
-
169
- const size = document.createElement('span');
170
- size.className = 'jux-fileupload-file-size';
171
- const sizeKB = Math.round(file.size / 1024);
172
- size.textContent = `${sizeKB} KB`;
173
-
174
- const removeBtn = document.createElement('button');
175
- removeBtn.className = 'jux-fileupload-file-remove';
176
- removeBtn.textContent = '×';
177
- removeBtn.addEventListener('click', () => {
178
- this.state.files.splice(index, 1);
179
- this._updateFileList();
180
- });
181
-
182
- item.appendChild(name);
183
- item.appendChild(size);
184
- item.appendChild(removeBtn);
185
- fileList.appendChild(item);
186
- });
187
- }
188
- }
138
+ const input = document.createElement('input');
139
+ input.type = 'file';
140
+ input.className = 'jux-fileupload-input';
141
+ input.id = `${this._id}-input`;
142
+ input.name = name;
143
+ input.required = required;
144
+ input.disabled = disabled;
189
145
 
190
- getFiles(): File[] {
191
- return this.state.files;
192
- }
146
+ if (accept) input.accept = accept;
147
+ if (multiple) input.multiple = multiple;
193
148
 
194
- clear(): void {
195
- this.state.files = [];
196
- this._updateFileList();
197
- const input = document.getElementById(`${this._id}-input`) as HTMLInputElement;
198
- if (input) {
199
- input.value = '';
200
- }
149
+ return input;
201
150
  }
202
151
 
203
- /* -------------------------
204
- * Render
205
- * ------------------------- */
152
+ /* ═════════════════════════════════════════════════════════════════
153
+ * RENDER
154
+ * ═════════════════════════════════════════════════════════════════ */
206
155
 
207
156
  render(targetId?: string): this {
208
- let container: HTMLElement;
209
-
210
- if (targetId) {
211
- const target = document.querySelector(targetId);
212
- if (!target || !(target instanceof HTMLElement)) {
213
- throw new Error(`FileUpload: Target element "${targetId}" not found`);
214
- }
215
- container = target;
216
- } else {
217
- container = getOrCreateContainer(this._id);
218
- }
157
+ const container = this._setupContainer(targetId);
219
158
 
220
- this.container = container;
221
- const { accept, multiple, disabled, text, dragText, style, class: className } = this.state;
159
+ const { icon, style, class: className } = this.state;
222
160
 
161
+ // Build wrapper
223
162
  const wrapper = document.createElement('div');
224
- wrapper.className = 'jux-fileupload';
163
+ wrapper.className = 'jux-input jux-fileupload';
225
164
  wrapper.id = this._id;
165
+ if (className) wrapper.className += ` ${className}`;
166
+ if (style) wrapper.setAttribute('style', style);
226
167
 
227
- if (className) {
228
- wrapper.className += ` ${className}`;
168
+ // Label
169
+ if (this.state.label) {
170
+ wrapper.appendChild(this._renderLabel());
229
171
  }
230
172
 
231
- if (style) {
232
- wrapper.setAttribute('style', style);
173
+ // Hidden file input
174
+ const inputEl = this._buildInputElement() as HTMLInputElement;
175
+ this._inputElement = inputEl;
176
+ wrapper.appendChild(inputEl);
177
+
178
+ // Button container
179
+ const buttonContainer = document.createElement('div');
180
+ buttonContainer.className = 'jux-fileupload-button-container';
181
+
182
+ if (icon) {
183
+ const iconEl = document.createElement('span');
184
+ iconEl.className = 'jux-fileupload-icon';
185
+ iconEl.appendChild(renderIcon(icon));
186
+ buttonContainer.appendChild(iconEl);
233
187
  }
234
188
 
235
- // Hidden file input
236
- const input = document.createElement('input');
237
- input.type = 'file';
238
- input.className = 'jux-fileupload-input';
239
- input.id = `${this._id}-input`;
240
- input.accept = accept;
241
- input.multiple = multiple;
242
- input.disabled = disabled;
189
+ const button = document.createElement('button');
190
+ button.type = 'button';
191
+ button.className = 'jux-fileupload-button';
192
+ button.textContent = 'Choose File(s)';
193
+ button.disabled = this.state.disabled;
243
194
 
244
- input.addEventListener('change', (e) => {
245
- const target = e.target as HTMLInputElement;
246
- if (target.files && target.files.length > 0) {
247
- this._handleFiles(target.files);
248
- }
249
- });
195
+ buttonContainer.appendChild(button);
196
+ wrapper.appendChild(buttonContainer);
250
197
 
251
- // Drop zone
252
- const dropzone = document.createElement('div');
253
- dropzone.className = 'jux-fileupload-dropzone';
198
+ // File list
199
+ const fileList = document.createElement('div');
200
+ fileList.className = 'jux-fileupload-list';
201
+ this._fileListElement = fileList;
202
+ wrapper.appendChild(fileList);
254
203
 
255
- const icon = document.createElement('div');
256
- icon.className = 'jux-fileupload-icon';
257
- icon.textContent = '📁';
204
+ // Error element
205
+ wrapper.appendChild(this._renderError());
258
206
 
259
- const textEl = document.createElement('div');
260
- textEl.className = 'jux-fileupload-text';
261
- textEl.textContent = text;
207
+ // Button click triggers file input
208
+ button.addEventListener('click', () => inputEl.click());
262
209
 
263
- dropzone.appendChild(icon);
264
- dropzone.appendChild(textEl);
210
+ // Wire events
211
+ this._wireStandardEvents(wrapper);
265
212
 
266
- // Click to open file dialog
267
- dropzone.addEventListener('click', () => {
268
- if (!disabled) {
269
- input.click();
270
- }
271
- });
213
+ // Wire file-specific sync
214
+ const filesSync = this._syncBindings.find(b => b.property === 'files' || b.property === 'value');
272
215
 
273
- // Drag & drop
274
- dropzone.addEventListener('dragover', (e) => {
275
- e.preventDefault();
276
- if (!disabled) {
277
- dropzone.classList.add('jux-fileupload-dropzone-active');
278
- textEl.textContent = dragText;
279
- }
280
- });
216
+ if (filesSync) {
217
+ const { stateObj, toState, toComponent } = filesSync;
281
218
 
282
- dropzone.addEventListener('dragleave', () => {
283
- dropzone.classList.remove('jux-fileupload-dropzone-active');
284
- textEl.textContent = text;
285
- });
219
+ const transformToState = toState || ((v: File[]) => v);
220
+ const transformToComponent = toComponent || ((v: any) => v);
286
221
 
287
- dropzone.addEventListener('drop', (e) => {
288
- e.preventDefault();
289
- dropzone.classList.remove('jux-fileupload-dropzone-active');
290
- textEl.textContent = text;
222
+ let isUpdating = false;
291
223
 
292
- if (!disabled && e.dataTransfer?.files) {
293
- this._handleFiles(e.dataTransfer.files);
294
- }
224
+ // State Component
225
+ stateObj.subscribe((val: any) => {
226
+ if (isUpdating) return;
227
+ const transformed = transformToComponent(val);
228
+ this.setValue(transformed);
229
+ });
230
+
231
+ // Component → State
232
+ inputEl.addEventListener('change', () => {
233
+ if (isUpdating) return;
234
+ isUpdating = true;
235
+
236
+ const files = Array.from(inputEl.files || []);
237
+ this.state.files = files;
238
+ this._updateFileList(files);
239
+ this._clearError();
240
+
241
+ const transformed = transformToState(files);
242
+ stateObj.set(transformed);
243
+
244
+ // 🎯 Fire the callback events
245
+ this._triggerCallback('change', files);
246
+ this._triggerCallback('filesSelected', files);
247
+
248
+ setTimeout(() => { isUpdating = false; }, 0);
249
+ });
250
+ } else {
251
+ // Default behavior without sync
252
+ inputEl.addEventListener('change', () => {
253
+ const files = Array.from(inputEl.files || []);
254
+ this.state.files = files;
255
+ this._updateFileList(files);
256
+ this._clearError();
257
+
258
+ // 🎯 Fire the callback events
259
+ this._triggerCallback('change', files);
260
+ this._triggerCallback('filesSelected', files);
261
+ });
262
+ }
263
+
264
+ // Always add blur validation
265
+ inputEl.addEventListener('blur', () => {
266
+ this.validate();
295
267
  });
296
268
 
297
- // File list
298
- const fileList = document.createElement('div');
299
- fileList.className = 'jux-fileupload-list';
300
- fileList.id = `${this._id}-list`;
269
+ // Sync label changes
270
+ const labelSync = this._syncBindings.find(b => b.property === 'label');
271
+ if (labelSync) {
272
+ const transform = labelSync.toComponent || ((v: any) => String(v));
273
+ labelSync.stateObj.subscribe((val: any) => {
274
+ this.label(transform(val));
275
+ });
276
+ }
301
277
 
302
- wrapper.appendChild(input);
303
- wrapper.appendChild(dropzone);
304
- wrapper.appendChild(fileList);
305
278
  container.appendChild(wrapper);
279
+ this._injectFileUploadStyles();
280
+ this._injectFormStyles();
281
+
282
+ requestAnimationFrame(() => {
283
+ if ((window as any).lucide) {
284
+ (window as any).lucide.createIcons();
285
+ }
286
+ });
287
+
306
288
  return this;
307
289
  }
308
290
 
309
- renderTo(juxComponent: any): this {
310
- if (!juxComponent?._id) {
311
- throw new Error('FileUpload.renderTo: Invalid component');
291
+ private _updateFileList(files: File[]): void {
292
+ if (!this._fileListElement) return;
293
+
294
+ this._fileListElement.innerHTML = '';
295
+
296
+ if (files.length === 0) {
297
+ this._fileListElement.textContent = 'No files selected';
298
+ return;
312
299
  }
313
- return this.render(`#${juxComponent._id}`);
300
+
301
+ files.forEach(file => {
302
+ const fileItem = document.createElement('div');
303
+ fileItem.className = 'jux-fileupload-item';
304
+ fileItem.textContent = `${file.name} (${this._formatFileSize(file.size)})`;
305
+ this._fileListElement!.appendChild(fileItem);
306
+ });
307
+ }
308
+
309
+ private _formatFileSize(bytes: number): string {
310
+ if (bytes < 1024) return `${bytes} B`;
311
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
312
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
313
+ }
314
+
315
+ private _injectFileUploadStyles(): void {
316
+ const styleId = 'jux-fileupload-styles';
317
+ if (document.getElementById(styleId)) return;
318
+
319
+ const style = document.createElement('style');
320
+ style.id = styleId;
321
+ style.textContent = `
322
+ .jux-fileupload-input {
323
+ display: none;
324
+ }
325
+
326
+ .jux-fileupload-button-container {
327
+ display: inline-flex;
328
+ align-items: center;
329
+ gap: 8px;
330
+ }
331
+
332
+ .jux-fileupload-icon svg {
333
+ width: 18px;
334
+ height: 18px;
335
+ }
336
+
337
+ .jux-fileupload-button {
338
+ padding: 8px 16px;
339
+ background: #3b82f6;
340
+ color: white;
341
+ border: none;
342
+ border-radius: 6px;
343
+ font-size: 14px;
344
+ cursor: pointer;
345
+ transition: background 0.2s;
346
+ }
347
+
348
+ .jux-fileupload-button:hover:not(:disabled) {
349
+ background: #2563eb;
350
+ }
351
+
352
+ .jux-fileupload-button:disabled {
353
+ background: #9ca3af;
354
+ cursor: not-allowed;
355
+ }
356
+
357
+ .jux-fileupload-list {
358
+ margin-top: 12px;
359
+ font-size: 14px;
360
+ color: #6b7280;
361
+ }
362
+
363
+ .jux-fileupload-item {
364
+ padding: 8px;
365
+ background: #f3f4f6;
366
+ border-radius: 4px;
367
+ margin-bottom: 4px;
368
+ }
369
+ `;
370
+ document.head.appendChild(style);
314
371
  }
315
372
  }
316
373