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,8 +1,6 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
2
3
 
3
- /**
4
- * Element component options
5
- */
6
4
  export interface ElementOptions {
7
5
  tagType?: string;
8
6
  className?: string;
@@ -14,9 +12,6 @@ export interface ElementOptions {
14
12
  class?: string;
15
13
  }
16
14
 
17
- /**
18
- * Element component state
19
- */
20
15
  type ElementState = {
21
16
  tagType: string;
22
17
  className: string;
@@ -28,30 +23,15 @@ type ElementState = {
28
23
  class: string;
29
24
  };
30
25
 
31
- /**
32
- * Element component - Create arbitrary HTML elements
33
- *
34
- * Usage:
35
- * const div = jux.element('myDiv', { tagType: 'div', className: 'container' });
36
- * div.render('#target');
37
- *
38
- * const span = jux.element('mySpan')
39
- * .tagType('span')
40
- * .className('highlight')
41
- * .text('Hello World')
42
- * .render();
43
- *
44
- * For reactive content, use State bindings after render:
45
- * const count = state(0);
46
- * jux.element('counter').render('#app');
47
- * count.bindText('counter', (val) => `Count: ${val}`);
48
- */
49
26
  export class Element {
50
27
  state: ElementState;
51
28
  container: HTMLElement | null = null;
52
29
  _id: string;
53
30
  id: string;
54
31
 
32
+ private _bindings: Array<{ event: string, handler: Function }> = [];
33
+ private _syncBindings: Array<{ property: string, stateObj: State<any>, transform?: Function }> = [];
34
+
55
35
  constructor(id: string, options: ElementOptions = {}) {
56
36
  this._id = id;
57
37
  this.id = id;
@@ -178,6 +158,25 @@ export class Element {
178
158
  return this;
179
159
  }
180
160
 
161
+ /**
162
+ * Bind an event to a handler
163
+ */
164
+ bind(event: string, handler: Function): Element {
165
+ this._bindings.push({ event, handler });
166
+ return this;
167
+ }
168
+
169
+ /**
170
+ * Sync a property to a state object
171
+ */
172
+ sync(property: string, stateObj: State<any>, transform?: Function): Element {
173
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
174
+ throw new Error(`Element.sync: Expected a State object for property "${property}"`);
175
+ }
176
+ this._syncBindings.push({ property, stateObj, transform });
177
+ return this;
178
+ }
179
+
181
180
  /* -------------------------
182
181
  * Render
183
182
  * ------------------------- */
@@ -187,8 +186,8 @@ export class Element {
187
186
  * @param targetId - CSS selector for target element (optional, defaults to body)
188
187
  */
189
188
  render(targetId?: string): Element {
189
+ // === 1. SETUP: Get container ===
190
190
  let container: HTMLElement;
191
-
192
191
  if (targetId) {
193
192
  const target = document.querySelector(targetId);
194
193
  if (!target || !(target instanceof HTMLElement)) {
@@ -198,48 +197,62 @@ export class Element {
198
197
  } else {
199
198
  container = getOrCreateContainer(this._id);
200
199
  }
201
-
202
200
  this.container = container;
201
+
202
+ // === 2. PREPARE: Destructure state ===
203
203
  const { tagType, className, text, innerHTML, attributes, styles, style, class: classValue } = this.state;
204
204
 
205
- // Create the element
205
+ // === 3. BUILD: Create DOM element ===
206
206
  const element = document.createElement(tagType);
207
-
208
- // Set ID to component ID
209
207
  element.id = this._id;
210
208
 
211
- // Set className
212
- if (className) {
213
- element.className = className;
214
- }
215
-
216
- if (classValue) {
217
- element.className = element.className ? `${element.className} ${classValue}` : classValue;
218
- }
209
+ if (className) element.className = className;
210
+ if (classValue) element.className = `${element.className} ${classValue}`.trim();
219
211
 
220
- // Set content (innerHTML takes precedence over text)
221
- if (innerHTML) {
222
- element.innerHTML = innerHTML;
223
- } else if (text) {
212
+ if (text) {
224
213
  element.textContent = text;
214
+ } else if (innerHTML) {
215
+ element.innerHTML = innerHTML;
225
216
  }
226
217
 
227
- // Set attributes
228
- Object.entries(attributes).forEach(([name, value]) => {
229
- element.setAttribute(name, value);
218
+ Object.entries(attributes).forEach(([key, value]) => {
219
+ element.setAttribute(key, value);
230
220
  });
231
221
 
232
- // Set styles (object)
233
- Object.entries(styles).forEach(([property, value]) => {
234
- element.style.setProperty(property, value);
235
- });
222
+ const styleString = Object.entries(styles)
223
+ .map(([key, value]) => `${key}: ${value}`)
224
+ .join('; ');
236
225
 
237
- // Set style (string)
238
- if (style) {
239
- element.setAttribute('style', style);
226
+ if (styleString || style) {
227
+ element.setAttribute('style', `${styleString}; ${style}`.trim());
240
228
  }
241
229
 
230
+ // === 4. WIRE: Add event listeners ===
231
+
232
+ this._bindings.forEach(({ event, handler }) => {
233
+ element.addEventListener(event, handler as EventListener);
234
+ });
235
+
236
+ this._syncBindings.forEach(({ property, stateObj, transform }) => {
237
+ stateObj.subscribe((val: any) => {
238
+ const transformed = transform ? transform(val) : val;
239
+
240
+ if (property === 'text') {
241
+ element.textContent = String(transformed);
242
+ this.state.text = String(transformed);
243
+ } else if (property === 'innerHTML') {
244
+ element.innerHTML = String(transformed);
245
+ this.state.innerHTML = String(transformed);
246
+ } else if (property === 'class') {
247
+ element.className = String(transformed);
248
+ this.state.class = String(transformed);
249
+ }
250
+ });
251
+ });
252
+
253
+ // === 5. RENDER: Append to DOM ===
242
254
  container.appendChild(element);
255
+
243
256
  return this;
244
257
  }
245
258
 
@@ -251,21 +264,13 @@ export class Element {
251
264
  * const child = jux.element('myChild').renderTo(container);
252
265
  */
253
266
  renderTo(juxComponent: Element): Element {
254
- if (!juxComponent || typeof juxComponent !== 'object') {
255
- throw new Error('Element.renderTo: Invalid component - not an object');
267
+ if (!juxComponent || !juxComponent._id) {
268
+ throw new Error('Element.renderTo: Invalid component');
256
269
  }
257
-
258
- if (!juxComponent._id || typeof juxComponent._id !== 'string') {
259
- throw new Error('Element.renderTo: Invalid component - missing _id (not a Jux component)');
260
- }
261
-
262
270
  return this.render(`#${juxComponent._id}`);
263
271
  }
264
272
  }
265
273
 
266
- /**
267
- * Factory helper
268
- */
269
274
  export function element(id: string, options: ElementOptions = {}): Element {
270
275
  return new Element(id, options);
271
276
  }
@@ -1,73 +1,55 @@
1
1
  import { getOrCreateContainer } from './helpers.js';
2
+ import { State } from '../reactivity/state.js';
2
3
 
3
- /**
4
- * FileUpload component options
5
- */
6
4
  export interface FileUploadOptions {
5
+ label?: string;
7
6
  accept?: string;
8
7
  multiple?: boolean;
9
- maxSize?: number;
10
8
  disabled?: boolean;
11
- text?: string;
12
- dragText?: string;
13
- onChange?: (files: FileList) => void;
14
- onError?: (error: string) => void;
9
+ name?: string;
15
10
  style?: string;
16
11
  class?: string;
17
12
  }
18
13
 
19
- /**
20
- * FileUpload component state
21
- */
22
14
  type FileUploadState = {
15
+ files: File[];
16
+ label: string;
23
17
  accept: string;
24
18
  multiple: boolean;
25
- maxSize: number;
26
19
  disabled: boolean;
27
- text: string;
28
- dragText: string;
20
+ name: string;
29
21
  style: string;
30
22
  class: string;
31
- files: File[];
32
23
  };
33
24
 
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
25
  export class FileUpload {
48
26
  state: FileUploadState;
49
27
  container: HTMLElement | null = null;
50
28
  _id: string;
51
29
  id: string;
52
- private _onChange?: (files: FileList) => void;
53
- private _onError?: (error: string) => void;
30
+
31
+ // CRITICAL: Store bind/sync instructions for deferred wiring
32
+ private _bindings: Array<{ event: string, handler: Function }> = [];
33
+ private _syncBindings: Array<{
34
+ property: string,
35
+ stateObj: State<any>,
36
+ toState?: Function,
37
+ toComponent?: Function
38
+ }> = [];
54
39
 
55
40
  constructor(id: string, options: FileUploadOptions = {}) {
56
41
  this._id = id;
57
42
  this.id = id;
58
- this._onChange = options.onChange;
59
- this._onError = options.onError;
60
43
 
61
44
  this.state = {
62
- accept: options.accept ?? '*',
45
+ files: [],
46
+ label: options.label ?? '',
47
+ accept: options.accept ?? '',
63
48
  multiple: options.multiple ?? false,
64
- maxSize: options.maxSize ?? 10485760, // 10MB default
65
49
  disabled: options.disabled ?? false,
66
- text: options.text ?? 'Click to upload or drag and drop',
67
- dragText: options.dragText ?? 'Drop files here',
50
+ name: options.name ?? id,
68
51
  style: options.style ?? '',
69
- class: options.class ?? '',
70
- files: []
52
+ class: options.class ?? ''
71
53
  };
72
54
  }
73
55
 
@@ -75,6 +57,11 @@ export class FileUpload {
75
57
  * Fluent API
76
58
  * ------------------------- */
77
59
 
60
+ label(value: string): this {
61
+ this.state.label = value;
62
+ return this;
63
+ }
64
+
78
65
  accept(value: string): this {
79
66
  this.state.accept = value;
80
67
  return this;
@@ -85,23 +72,14 @@ export class FileUpload {
85
72
  return this;
86
73
  }
87
74
 
88
- maxSize(value: number): this {
89
- this.state.maxSize = value;
90
- return this;
91
- }
92
-
93
75
  disabled(value: boolean): this {
94
76
  this.state.disabled = value;
77
+ this._updateElement();
95
78
  return this;
96
79
  }
97
80
 
98
- text(value: string): this {
99
- this.state.text = value;
100
- return this;
101
- }
102
-
103
- dragText(value: string): this {
104
- this.state.dragText = value;
81
+ name(value: string): this {
82
+ this.state.name = value;
105
83
  return this;
106
84
  }
107
85
 
@@ -115,13 +93,16 @@ export class FileUpload {
115
93
  return this;
116
94
  }
117
95
 
118
- onChange(handler: (files: FileList) => void): this {
119
- this._onChange = handler;
96
+ bind(event: string, handler: Function): this {
97
+ this._bindings.push({ event, handler });
120
98
  return this;
121
99
  }
122
100
 
123
- onError(handler: (error: string) => void): this {
124
- this._onError = handler;
101
+ sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
102
+ if (!stateObj || typeof stateObj.subscribe !== 'function') {
103
+ throw new Error(`FileUpload.sync: Expected a State object for property "${property}"`);
104
+ }
105
+ this._syncBindings.push({ property, stateObj, toState, toComponent });
125
106
  return this;
126
107
  }
127
108
 
@@ -129,61 +110,15 @@ export class FileUpload {
129
110
  * Helpers
130
111
  * ------------------------- */
131
112
 
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;
142
- }
143
- }
144
- return true;
145
- }
113
+ private _updateElement(): void {
114
+ const input = document.getElementById(`${this._id}-input`) as HTMLInputElement;
115
+ const button = document.getElementById(`${this._id}-button`) as HTMLButtonElement;
146
116
 
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
- }
117
+ if (input) {
118
+ input.disabled = this.state.disabled;
154
119
  }
155
- }
156
-
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
- });
120
+ if (button) {
121
+ button.disabled = this.state.disabled;
187
122
  }
188
123
  }
189
124
 
@@ -193,119 +128,155 @@ export class FileUpload {
193
128
 
194
129
  clear(): void {
195
130
  this.state.files = [];
196
- this._updateFileList();
197
131
  const input = document.getElementById(`${this._id}-input`) as HTMLInputElement;
132
+ const fileList = document.getElementById(`${this._id}-list`);
133
+
198
134
  if (input) {
199
135
  input.value = '';
200
136
  }
137
+ if (fileList) {
138
+ this._updateFileList(fileList, []);
139
+ }
201
140
  }
202
141
 
203
142
  /* -------------------------
204
- * Render
143
+ * Render (5-Step Pattern)
205
144
  * ------------------------- */
206
145
 
207
146
  render(targetId?: string): this {
147
+ // === 1. SETUP: Get or create container ===
208
148
  let container: HTMLElement;
209
-
210
149
  if (targetId) {
211
150
  const target = document.querySelector(targetId);
212
151
  if (!target || !(target instanceof HTMLElement)) {
213
- throw new Error(`FileUpload: Target element "${targetId}" not found`);
152
+ throw new Error(`FileUpload: Target "${targetId}" not found`);
214
153
  }
215
154
  container = target;
216
155
  } else {
217
156
  container = getOrCreateContainer(this._id);
218
157
  }
219
-
220
158
  this.container = container;
221
- const { accept, multiple, disabled, text, dragText, style, class: className } = this.state;
222
159
 
160
+ // === 2. PREPARE: Destructure state and check sync flags ===
161
+ const { label, accept, multiple, disabled, name, style, class: className } = this.state;
162
+ const hasFilesSync = this._syncBindings.some(b => b.property === 'files');
163
+
164
+ // === 3. BUILD: Create DOM elements ===
223
165
  const wrapper = document.createElement('div');
224
166
  wrapper.className = 'jux-fileupload';
225
167
  wrapper.id = this._id;
226
-
227
- if (className) {
228
- wrapper.className += ` ${className}`;
168
+ if (className) wrapper.className += ` ${className}`;
169
+ if (style) wrapper.setAttribute('style', style);
170
+
171
+ if (label) {
172
+ const labelEl = document.createElement('label');
173
+ labelEl.className = 'jux-fileupload-label';
174
+ labelEl.textContent = label;
175
+ wrapper.appendChild(labelEl);
229
176
  }
230
177
 
231
- if (style) {
232
- wrapper.setAttribute('style', style);
233
- }
234
-
235
- // Hidden file input
236
178
  const input = document.createElement('input');
237
179
  input.type = 'file';
238
180
  input.className = 'jux-fileupload-input';
239
181
  input.id = `${this._id}-input`;
240
- input.accept = accept;
241
- input.multiple = multiple;
182
+ input.name = name;
242
183
  input.disabled = disabled;
184
+ if (accept) input.accept = accept;
185
+ if (multiple) input.multiple = multiple;
243
186
 
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
- });
187
+ const button = document.createElement('button');
188
+ button.type = 'button';
189
+ button.className = 'jux-fileupload-button';
190
+ button.id = `${this._id}-button`;
191
+ button.textContent = 'Choose File(s)';
192
+ button.disabled = disabled;
250
193
 
251
- // Drop zone
252
- const dropzone = document.createElement('div');
253
- dropzone.className = 'jux-fileupload-dropzone';
194
+ const fileList = document.createElement('div');
195
+ fileList.className = 'jux-fileupload-list';
196
+ fileList.id = `${this._id}-list`;
254
197
 
255
- const icon = document.createElement('div');
256
- icon.className = 'jux-fileupload-icon';
257
- icon.textContent = '📁';
198
+ wrapper.appendChild(button);
199
+ wrapper.appendChild(input);
200
+ wrapper.appendChild(fileList);
258
201
 
259
- const textEl = document.createElement('div');
260
- textEl.className = 'jux-fileupload-text';
261
- textEl.textContent = text;
202
+ // === 4. WIRE: Attach event listeners and sync bindings ===
262
203
 
263
- dropzone.appendChild(icon);
264
- dropzone.appendChild(textEl);
204
+ // Button click triggers file input
205
+ button.addEventListener('click', () => input.click());
265
206
 
266
- // Click to open file dialog
267
- dropzone.addEventListener('click', () => {
268
- if (!disabled) {
269
- input.click();
270
- }
271
- });
207
+ // Default file change behavior (only if NOT using sync)
208
+ if (!hasFilesSync) {
209
+ input.addEventListener('change', () => {
210
+ this.state.files = Array.from(input.files || []);
211
+ this._updateFileList(fileList, this.state.files);
212
+ });
213
+ }
272
214
 
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
- }
215
+ // Wire custom bindings from .bind() calls
216
+ this._bindings.forEach(({ event, handler }) => {
217
+ wrapper.addEventListener(event, handler as EventListener);
280
218
  });
281
219
 
282
- dropzone.addEventListener('dragleave', () => {
283
- dropzone.classList.remove('jux-fileupload-dropzone-active');
284
- textEl.textContent = text;
285
- });
220
+ // Wire sync bindings from .sync() calls
221
+ this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
222
+ if (property === 'files') {
223
+ const transformToState = toState || ((v: any) => v);
224
+ const transformToComponent = toComponent || ((v: any) => v);
225
+
226
+ let isUpdating = false;
227
+
228
+ // State → Component
229
+ stateObj.subscribe((val: any) => {
230
+ if (isUpdating) return;
231
+ const transformed = transformToComponent(val);
232
+ this.state.files = transformed;
233
+ this._updateFileList(fileList, transformed);
234
+ });
286
235
 
287
- dropzone.addEventListener('drop', (e) => {
288
- e.preventDefault();
289
- dropzone.classList.remove('jux-fileupload-dropzone-active');
290
- textEl.textContent = text;
236
+ // Component State
237
+ input.addEventListener('change', () => {
238
+ if (isUpdating) return;
239
+ isUpdating = true;
291
240
 
292
- if (!disabled && e.dataTransfer?.files) {
293
- this._handleFiles(e.dataTransfer.files);
241
+ const files = Array.from(input.files || []);
242
+ this.state.files = files;
243
+ this._updateFileList(fileList, files);
244
+
245
+ const transformed = transformToState(files);
246
+ stateObj.set(transformed);
247
+
248
+ setTimeout(() => { isUpdating = false; }, 0);
249
+ });
294
250
  }
295
251
  });
296
252
 
297
- // File list
298
- const fileList = document.createElement('div');
299
- fileList.className = 'jux-fileupload-list';
300
- fileList.id = `${this._id}-list`;
301
-
302
- wrapper.appendChild(input);
303
- wrapper.appendChild(dropzone);
304
- wrapper.appendChild(fileList);
253
+ // === 5. RENDER: Append to DOM and finalize ===
305
254
  container.appendChild(wrapper);
306
255
  return this;
307
256
  }
308
257
 
258
+ private _updateFileList(fileList: HTMLElement, files: File[]): void {
259
+ fileList.innerHTML = '';
260
+
261
+ if (files.length === 0) {
262
+ fileList.textContent = 'No files selected';
263
+ return;
264
+ }
265
+
266
+ files.forEach(file => {
267
+ const fileItem = document.createElement('div');
268
+ fileItem.className = 'jux-fileupload-item';
269
+ fileItem.textContent = `${file.name} (${this._formatFileSize(file.size)})`;
270
+ fileList.appendChild(fileItem);
271
+ });
272
+ }
273
+
274
+ private _formatFileSize(bytes: number): string {
275
+ if (bytes < 1024) return `${bytes} B`;
276
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
277
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
278
+ }
279
+
309
280
  renderTo(juxComponent: any): this {
310
281
  if (!juxComponent?._id) {
311
282
  throw new Error('FileUpload.renderTo: Invalid component');