@superleapai/flow-ui 1.0.0

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 (40) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +451 -0
  4. package/components/alert.js +282 -0
  5. package/components/avatar.js +195 -0
  6. package/components/badge.js +135 -0
  7. package/components/button.js +201 -0
  8. package/components/checkbox.js +254 -0
  9. package/components/currency.js +227 -0
  10. package/components/date-time-picker/date-time-picker-utils.js +253 -0
  11. package/components/date-time-picker/date-time-picker.js +532 -0
  12. package/components/duration/duration-constants.js +46 -0
  13. package/components/duration/duration-utils.js +164 -0
  14. package/components/duration/duration.js +448 -0
  15. package/components/enum-multiselect.js +869 -0
  16. package/components/enum-select.js +831 -0
  17. package/components/enumeration.js +213 -0
  18. package/components/file-input.js +533 -0
  19. package/components/icon.js +200 -0
  20. package/components/input.js +259 -0
  21. package/components/label.js +111 -0
  22. package/components/multiselect.js +351 -0
  23. package/components/phone-input/phone-input.js +392 -0
  24. package/components/phone-input/phone-utils.js +157 -0
  25. package/components/popover.js +240 -0
  26. package/components/radio-group.js +435 -0
  27. package/components/record-multiselect.js +956 -0
  28. package/components/record-select.js +930 -0
  29. package/components/select.js +544 -0
  30. package/components/spinner.js +136 -0
  31. package/components/table.js +335 -0
  32. package/components/textarea.js +114 -0
  33. package/components/time-picker.js +357 -0
  34. package/components/toast.js +343 -0
  35. package/core/flow.js +1729 -0
  36. package/core/superleapClient.js +146 -0
  37. package/dist/output.css +2 -0
  38. package/index.d.ts +458 -0
  39. package/index.js +253 -0
  40. package/package.json +70 -0
package/core/flow.js ADDED
@@ -0,0 +1,1729 @@
1
+ /**
2
+ * Flow UI Components Library
3
+ *
4
+ * A reusable design system for building multi-step forms.
5
+ * Contains only UI components and state management utilities.
6
+ * Business logic should be implemented separately.
7
+ */
8
+ (function (global) {
9
+ "use strict";
10
+
11
+ // ============================================================================
12
+ // STATE MANAGEMENT
13
+ // ============================================================================
14
+
15
+ let _state = {};
16
+ let _onStateChange = null;
17
+
18
+ /**
19
+ * Initialize state with default values
20
+ * @param {Object} initialState - Initial state object
21
+ * @param {Function} onChangeCallback - Optional callback when state changes
22
+ */
23
+ function initState(initialState, onChangeCallback) {
24
+ _state = { ...initialState };
25
+ _onStateChange = onChangeCallback;
26
+ }
27
+
28
+ /**
29
+ * Get current state
30
+ * @returns {Object} Current state
31
+ */
32
+ function getState() {
33
+ return _state;
34
+ }
35
+
36
+ /**
37
+ * Update state with partial values
38
+ * @param {Object} partial - Partial state to merge
39
+ */
40
+ function setState(partial) {
41
+ _state = { ..._state, ...partial };
42
+ if (_onStateChange) {
43
+ _onStateChange(_state);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Get a specific state value
49
+ * @param {string} key - State key
50
+ * @returns {*} State value
51
+ */
52
+ function get(key) {
53
+ return _state[key];
54
+ }
55
+
56
+ /**
57
+ * Set a specific state value
58
+ * @param {string} key - State key
59
+ * @param {*} value - Value to set
60
+ */
61
+ function set(key, value) {
62
+ setState({ [key]: value });
63
+ }
64
+
65
+ /**
66
+ * Resolve a component by name. Uses FlowUI._getComponent (set by index.js after
67
+ * capturing components) when available, so components work after globals are cleaned.
68
+ * Falls back to global[name] when flow.js is used without the single-file entry.
69
+ */
70
+ function getComponent(name) {
71
+ if (typeof global.FlowUI !== "undefined" && typeof global.FlowUI._getComponent === "function") {
72
+ const c = global.FlowUI._getComponent(name);
73
+ if (c) return c;
74
+ }
75
+ return global[name];
76
+ }
77
+
78
+ // ============================================================================
79
+ // SCREEN UTILITIES
80
+ // ============================================================================
81
+
82
+ /**
83
+ * Create a screen wrapper with title and description
84
+ * @param {string} title - Screen title
85
+ * @param {string} description - Screen description
86
+ * @returns {HTMLElement} Screen wrapper element
87
+ */
88
+ function createScreen(title, description) {
89
+ const wrapper = document.createElement("div");
90
+ wrapper.className = "screen";
91
+
92
+ const h = document.createElement("h2");
93
+ h.className = "screen-title";
94
+ h.textContent = title;
95
+
96
+ const p = document.createElement("p");
97
+ p.className = "screen-description";
98
+ p.textContent = description;
99
+
100
+ wrapper.appendChild(h);
101
+ wrapper.appendChild(p);
102
+ return wrapper;
103
+ }
104
+
105
+ /**
106
+ * Create a form grid container
107
+ * @returns {HTMLElement} Grid element
108
+ */
109
+ function createGrid() {
110
+ const grid = document.createElement("div");
111
+ grid.className = "form-grid";
112
+ return grid;
113
+ }
114
+
115
+ /**
116
+ * Create a field wrapper with label (uses Label component when available)
117
+ * @param {string} label - Field label
118
+ * @param {boolean} required - Whether field is required
119
+ * @param {string} helpText - Optional help text for tooltip
120
+ * @returns {HTMLElement} Field wrapper element
121
+ */
122
+ function createFieldWrapper(label, required = false, helpText = null) {
123
+ const field = document.createElement("div");
124
+ field.className = "field";
125
+
126
+ let labelEl;
127
+ const LabelComponent = getComponent("Label");
128
+ if (LabelComponent && typeof LabelComponent.create === "function") {
129
+ const suffix = helpText && getComponent("Tooltip") ? getComponent("Tooltip").create(helpText) : null;
130
+ labelEl = LabelComponent.create({
131
+ label: label,
132
+ required: required,
133
+ requiredPosition: "right",
134
+ optional: false,
135
+ size: "default",
136
+ suffix: suffix || undefined,
137
+ });
138
+ } else {
139
+ // Fallback: inline label (no Label component)
140
+ labelEl = document.createElement("label");
141
+ labelEl.className = "field-label";
142
+ const labelContentWrapper = document.createElement("span");
143
+ labelContentWrapper.style.display = "inline-flex";
144
+ labelContentWrapper.style.alignItems = "center";
145
+ labelContentWrapper.style.gap = "0.25rem";
146
+ if (required) {
147
+ labelContentWrapper.appendChild(document.createTextNode(label + " "));
148
+ const asterisk = document.createElement("span");
149
+ asterisk.className = "required-asterisk";
150
+ asterisk.textContent = "*";
151
+ labelContentWrapper.appendChild(asterisk);
152
+ } else {
153
+ labelContentWrapper.appendChild(document.createTextNode(label));
154
+ }
155
+ if (helpText && getComponent("Tooltip")) {
156
+ const tooltip = getComponent("Tooltip").create(helpText);
157
+ if (tooltip) labelContentWrapper.appendChild(tooltip);
158
+ }
159
+ labelEl.appendChild(labelContentWrapper);
160
+ }
161
+
162
+ field.appendChild(labelEl);
163
+ return field;
164
+ }
165
+
166
+ // ============================================================================
167
+ // FORM COMPONENTS
168
+ // ============================================================================
169
+
170
+ /**
171
+ * Create a text input field
172
+ * @param {Object} config - Configuration object
173
+ * @param {string} config.label - Field label
174
+ * @param {string} config.fieldId - State key for this field
175
+ * @param {string} config.placeholder - Placeholder text
176
+ * @param {boolean} config.required - Whether field is required
177
+ * @param {string} config.type - Input type (text, email, number, etc.)
178
+ * @returns {HTMLElement} Field element
179
+ */
180
+ function createInput(config) {
181
+ const { label, fieldId, placeholder, required = false, type = "text", helpText = null, variant, inputSize, disabled = false, showReadOnlyIcon } = config;
182
+
183
+ const field = createFieldWrapper(label, required, helpText);
184
+
185
+ if (getComponent("InputComponent") && getComponent("InputComponent").create) {
186
+ const currentValue = get(fieldId) || "";
187
+ const inputEl = getComponent("InputComponent").create({
188
+ variant: variant || "default",
189
+ inputSize: inputSize || "default",
190
+ type,
191
+ placeholder: placeholder || `Enter ${label.toLowerCase()}`,
192
+ value: currentValue,
193
+ disabled,
194
+ showReadOnlyIcon,
195
+ onChange: (e) => set(fieldId, e.target.value),
196
+ onInput: (e) => set(fieldId, e.target.value),
197
+ });
198
+ inputEl._fieldId = fieldId;
199
+ field.appendChild(inputEl);
200
+ }
201
+ return field;
202
+ }
203
+
204
+ /**
205
+ * Create a textarea field
206
+ * @param {Object} config - Configuration object
207
+ * @param {string} config.label - Field label
208
+ * @param {string} config.fieldId - State key for this field
209
+ * @param {string} config.placeholder - Placeholder text
210
+ * @param {boolean} config.required - Whether field is required
211
+ * @param {string} [config.helpText] - Optional help text for tooltip
212
+ * @param {string} [config.variant] - 'default' | 'borderless' | 'inline' | 'error' | 'warning'
213
+ * @param {number} [config.rows] - Number of rows
214
+ * @param {boolean} [config.disabled] - Whether textarea is disabled
215
+ * @returns {HTMLElement} Field element
216
+ */
217
+ function createTextarea(config) {
218
+ const { label, fieldId, placeholder, required = false, helpText = null, variant, rows, disabled = false } = config;
219
+
220
+ const field = createFieldWrapper(label, required, helpText);
221
+ field.setAttribute("data-field-id", fieldId);
222
+
223
+ if (getComponent("TextareaComponent") && getComponent("TextareaComponent").create) {
224
+ const currentValue = get(fieldId) || "";
225
+ const textareaEl = getComponent("TextareaComponent").create({
226
+ variant: variant || "default",
227
+ placeholder: placeholder || `Enter ${label.toLowerCase()}`,
228
+ value: currentValue,
229
+ rows,
230
+ disabled,
231
+ onChange: (e) => set(fieldId, e.target.value),
232
+ onInput: (e) => set(fieldId, e.target.value),
233
+ });
234
+ textareaEl._fieldId = fieldId;
235
+ field.appendChild(textareaEl);
236
+ return field;
237
+ }
238
+
239
+ const textarea = document.createElement("textarea");
240
+ textarea.className = "textarea";
241
+ textarea.placeholder = placeholder || `Enter ${label.toLowerCase()}`;
242
+ textarea.value = get(fieldId) || "";
243
+ textarea.disabled = disabled;
244
+ if (rows != null) textarea.rows = rows;
245
+ textarea.addEventListener("change", (e) => set(fieldId, e.target.value));
246
+
247
+ field.appendChild(textarea);
248
+ return field;
249
+ }
250
+
251
+ /**
252
+ * Create a select dropdown field (using custom select component)
253
+ * @param {Object} config - Configuration object
254
+ * @param {string} config.label - Field label
255
+ * @param {string} config.fieldId - State key for this field
256
+ * @param {Array} config.options - Array of { value, label } objects
257
+ * @param {boolean} config.required - Whether field is required
258
+ * @param {Function} config.onChange - Optional change handler
259
+ * @param {boolean} config.disabled - Whether select is disabled
260
+ * @returns {HTMLElement} Field element
261
+ */
262
+ function createSelect(config) {
263
+ const { label, fieldId, options = [], required = false, onChange, disabled = false, helpText = null } = config;
264
+
265
+ const field = createFieldWrapper(label, required, helpText);
266
+
267
+ // Use custom select component if available, fallback to native select
268
+ if (getComponent("Select") && getComponent("Select").create) {
269
+ const currentValue = get(fieldId) || "";
270
+ const placeholder = `Select ${label}`;
271
+
272
+ const customSelect = getComponent("Select").create({
273
+ fieldId,
274
+ options,
275
+ placeholder,
276
+ value: currentValue,
277
+ disabled,
278
+ onChange: (value) => {
279
+ set(fieldId, value);
280
+ if (onChange) {onChange(value);}
281
+ },
282
+ });
283
+
284
+ // Store reference for dynamic updates
285
+ customSelect._fieldId = fieldId;
286
+ customSelect._onChange = onChange;
287
+
288
+ field.appendChild(customSelect);
289
+ return field;
290
+ }
291
+
292
+ // Fallback to native select
293
+ const select = document.createElement("select");
294
+ select.className = "select";
295
+ select.disabled = disabled;
296
+ const placeholderOption = document.createElement("option");
297
+ placeholderOption.value = "";
298
+ placeholderOption.textContent = `Select ${label}`;
299
+ placeholderOption.className = "select-placeholder";
300
+ placeholderOption.disabled = true;
301
+ placeholderOption.selected = true;
302
+ select.appendChild(placeholderOption);
303
+
304
+ options.forEach((opt) => {
305
+ const o = document.createElement("option");
306
+ o.value = opt.value || opt.slug || opt.id;
307
+ o.textContent = opt.label || opt.name || opt.value;
308
+ select.appendChild(o);
309
+ });
310
+
311
+ select.value = get(fieldId) || "";
312
+
313
+ // Set placeholder attribute for styling
314
+ if (!select.value) {
315
+ select.setAttribute("data-placeholder", "true");
316
+ }
317
+
318
+ select.addEventListener("change", (e) => {
319
+ const value = e.target.value;
320
+ set(fieldId, value);
321
+
322
+ // Update placeholder attribute for styling
323
+ if (value === "") {
324
+ select.setAttribute("data-placeholder", "true");
325
+ } else {
326
+ select.removeAttribute("data-placeholder");
327
+ }
328
+
329
+ if (onChange) {onChange(value);}
330
+ });
331
+
332
+ field.appendChild(select);
333
+ return field;
334
+ }
335
+
336
+ /**
337
+ * Create a time picker field (using TimePicker component)
338
+ * @param {Object} config - Configuration object
339
+ * @param {string} config.label - Field label
340
+ * @param {string} config.fieldId - State key for this field
341
+ * @param {string} config.value - Initial value "HH:mm"
342
+ * @param {string} config.placeholder - Placeholder text
343
+ * @param {boolean} config.required - Whether field is required
344
+ * @param {Function} config.onChange - Optional change handler
345
+ * @param {boolean} config.disabled - Whether time picker is disabled
346
+ * @param {boolean} config.use24Hour - Use 24-hour format (default: false, i.e. 12-hour AM/PM)
347
+ * @returns {HTMLElement} Field element
348
+ */
349
+ function createTimePicker(config) {
350
+ const { label, fieldId, value: initialValue, placeholder, required = false, onChange, disabled = false, use24Hour = false, helpText = null } = config;
351
+
352
+ const field = createFieldWrapper(label, required, helpText);
353
+
354
+ if (getComponent("TimePicker") && getComponent("TimePicker").create) {
355
+ const currentValue = get(fieldId) || initialValue || "";
356
+
357
+ const timePicker = getComponent("TimePicker").create({
358
+ fieldId,
359
+ value: currentValue,
360
+ placeholder: placeholder || `Select ${label}`,
361
+ disabled,
362
+ use24Hour,
363
+ onChange: (value) => {
364
+ set(fieldId, value);
365
+ if (onChange) { onChange(value); }
366
+ },
367
+ });
368
+
369
+ timePicker._fieldId = fieldId;
370
+ field.appendChild(timePicker);
371
+ return field;
372
+ }
373
+
374
+ // Fallback: native time input
375
+ const input = document.createElement("input");
376
+ input.type = "time";
377
+ input.className = "input";
378
+ input.value = get(fieldId) || "";
379
+ input.disabled = disabled;
380
+ input.addEventListener("change", (e) => {
381
+ set(fieldId, e.target.value);
382
+ if (onChange) { onChange(e.target.value); }
383
+ });
384
+ field.appendChild(input);
385
+ return field;
386
+ }
387
+
388
+ /**
389
+ * Create a date-time picker field (uses DateTimePicker component when available)
390
+ * State stores value as ISO date string or null.
391
+ * @param {Object} config - Configuration object
392
+ * @param {string} config.label - Field label
393
+ * @param {string} config.fieldId - State key for this field
394
+ * @param {Date|string|null} config.value - Initial value (Date or ISO string)
395
+ * @param {string} config.placeholder - Placeholder text
396
+ * @param {boolean} config.required - Whether field is required
397
+ * @param {Function} config.onChange - Optional change handler
398
+ * @param {boolean} config.disabled - Whether picker is disabled
399
+ * @param {12|24} config.hourCycle - 12 or 24 hour format
400
+ * @param {string} config.granularity - 'day' | 'hour' | 'minute' | 'second'
401
+ * @param {string} config.size - 'small' | 'default' | 'large'
402
+ * @param {Date} config.fromDate - Min selectable date
403
+ * @param {Date} config.toDate - Max selectable date
404
+ * @param {string} config.helpText - Optional help text for tooltip
405
+ * @returns {HTMLElement} Field element
406
+ */
407
+ function createDateTimePicker(config) {
408
+ const {
409
+ label,
410
+ fieldId,
411
+ value: initialValue,
412
+ placeholder,
413
+ required = false,
414
+ onChange,
415
+ disabled = false,
416
+ hourCycle = 12,
417
+ granularity = "minute",
418
+ size = "default",
419
+ fromDate,
420
+ toDate,
421
+ helpText = null,
422
+ } = config;
423
+
424
+ const field = createFieldWrapper(label, required, helpText);
425
+
426
+ if (getComponent("DateTimePicker") && getComponent("DateTimePicker").create) {
427
+ const raw = get(fieldId);
428
+ let currentValue;
429
+ if (initialValue instanceof Date) {
430
+ currentValue = initialValue;
431
+ } else if (raw === null || raw === undefined || raw === "") {
432
+ currentValue = undefined;
433
+ } else {
434
+ try {
435
+ currentValue = typeof raw === "string" ? new Date(raw) : raw;
436
+ if (Number.isNaN(currentValue.getTime())) currentValue = undefined;
437
+ } catch (e) {
438
+ currentValue = undefined;
439
+ }
440
+ }
441
+
442
+ const picker = getComponent("DateTimePicker").create({
443
+ value: currentValue,
444
+ placeholder: placeholder || `Pick ${label.toLowerCase()}`,
445
+ disabled,
446
+ hourCycle,
447
+ granularity,
448
+ size,
449
+ fromDate,
450
+ toDate,
451
+ onChange: (date) => {
452
+ set(fieldId, date ? date.toISOString() : null);
453
+ if (onChange) onChange(date);
454
+ },
455
+ });
456
+
457
+ picker._fieldId = fieldId;
458
+ field.appendChild(picker);
459
+ return field;
460
+ }
461
+
462
+ // Fallback: native date and time inputs
463
+ const dateInput = document.createElement("input");
464
+ dateInput.type = "date";
465
+ dateInput.className = "input";
466
+ const raw = get(fieldId);
467
+ if (raw) {
468
+ try {
469
+ const d = typeof raw === "string" ? new Date(raw) : raw;
470
+ if (!Number.isNaN(d.getTime())) {
471
+ dateInput.value = d.toISOString().slice(0, 10);
472
+ }
473
+ } catch (e) {}
474
+ }
475
+ dateInput.disabled = disabled;
476
+ dateInput.addEventListener("change", (e) => {
477
+ const val = e.target.value ? new Date(e.target.value + "T12:00:00Z").toISOString() : null;
478
+ set(fieldId, val);
479
+ if (onChange) onChange(val ? new Date(val) : undefined);
480
+ });
481
+ field.appendChild(dateInput);
482
+ return field;
483
+ }
484
+
485
+ /**
486
+ * Create a radio button group
487
+ * @param {Object} config - Configuration object
488
+ * @param {string} config.label - Field label
489
+ * @param {string} config.fieldId - State key for this field
490
+ * @param {Array} config.options - Array of { value, label, disabled? } objects
491
+ * @param {boolean} config.required - Whether field is required
492
+ * @param {Function} config.onChange - Optional change handler
493
+ * @param {string} [config.orientation] - 'horizontal' | 'vertical'
494
+ * @param {boolean} [config.disabled] - Disable all radios
495
+ * @returns {HTMLElement} Field element
496
+ */
497
+ function createRadioGroup(config) {
498
+ const { label, fieldId, options = [], required = false, onChange, helpText = null, orientation = "horizontal", disabled = false } = config;
499
+
500
+ const field = createFieldWrapper(label, required, helpText);
501
+
502
+ if (getComponent("RadioGroup") && getComponent("RadioGroup").create) {
503
+ const currentValue = get(fieldId);
504
+ const radioGroupEl = getComponent("RadioGroup").create({
505
+ name: fieldId,
506
+ options: options.map((opt) => ({ value: opt.value, label: opt.label || opt.value, disabled: opt.disabled })),
507
+ value: currentValue,
508
+ disabled,
509
+ orientation,
510
+ onChange: (value) => {
511
+ set(fieldId, value);
512
+ if (onChange) onChange(value);
513
+ },
514
+ });
515
+ radioGroupEl._fieldId = fieldId;
516
+ field.appendChild(radioGroupEl);
517
+ return field;
518
+ }
519
+
520
+ const radioGroup = document.createElement("div");
521
+ radioGroup.className = "radio-group";
522
+
523
+ options.forEach((opt) => {
524
+ const wrapper = document.createElement("div");
525
+ wrapper.className = "radio-option";
526
+
527
+ const radio = document.createElement("input");
528
+ radio.type = "radio";
529
+ radio.name = fieldId;
530
+ radio.value = opt.value;
531
+ radio.id = `${fieldId}-${opt.value}`;
532
+ radio.checked = get(fieldId) === opt.value;
533
+ radio.disabled = disabled || !!opt.disabled;
534
+ radio.addEventListener("change", () => {
535
+ set(fieldId, opt.value);
536
+ if (onChange) {onChange(opt.value);}
537
+ });
538
+
539
+ const radioLabel = document.createElement("label");
540
+ radioLabel.htmlFor = `${fieldId}-${opt.value}`;
541
+ radioLabel.textContent = opt.label || opt.value;
542
+
543
+ wrapper.appendChild(radio);
544
+ wrapper.appendChild(radioLabel);
545
+ radioGroup.appendChild(wrapper);
546
+ });
547
+
548
+ field.appendChild(radioGroup);
549
+ return field;
550
+ }
551
+
552
+ /**
553
+ * Create a multi-select field (uses MultiSelect component when available, else checkbox group)
554
+ * @param {Object} config - Configuration object
555
+ * @param {string} config.label - Field label
556
+ * @param {string} config.fieldId - State key for this field (stores array)
557
+ * @param {Array} config.options - Array of { value, label } or { slug, display_name } objects
558
+ * @param {boolean} config.required - Whether field is required
559
+ * @param {Function} config.onChange - Optional change handler (receives array of selected values)
560
+ * @param {string} config.placeholder - Placeholder when none selected
561
+ * @param {string} config.helpText - Optional help text for tooltip
562
+ * @param {string} config.variant - 'default' | 'error' | 'warning' | 'borderless' | 'inline'
563
+ * @param {string} config.size - 'default' | 'large' | 'small'
564
+ * @param {string} config.type - 'default' (count) | 'tags' (show tags in trigger)
565
+ * @param {boolean} config.disabled - Whether multiselect is disabled
566
+ * @returns {HTMLElement} Field element
567
+ */
568
+ function createMultiSelect(config) {
569
+ const { label, fieldId, options = [], required = false, onChange, placeholder, helpText = null, variant, size, type, disabled = false } = config;
570
+
571
+ const field = createFieldWrapper(label, required, helpText);
572
+
573
+ if (getComponent("MultiSelect") && getComponent("MultiSelect").create) {
574
+ const currentValues = get(fieldId) || [];
575
+ const multiEl = getComponent("MultiSelect").create({
576
+ fieldId,
577
+ options,
578
+ placeholder: placeholder || `Select ${label}`,
579
+ value: currentValues,
580
+ label: "selected",
581
+ variant: variant || "default",
582
+ size: size || "default",
583
+ type: type || "default",
584
+ disabled,
585
+ onValuesChange: (values) => {
586
+ set(fieldId, values);
587
+ if (onChange) onChange(values);
588
+ },
589
+ });
590
+ multiEl._fieldId = fieldId;
591
+ field.appendChild(multiEl);
592
+ return field;
593
+ }
594
+
595
+ // Fallback: checkbox group
596
+ const checkboxContainer = document.createElement("div");
597
+ checkboxContainer.className = "checkbox-group";
598
+
599
+ const currentValues = get(fieldId) || [];
600
+
601
+ options.forEach((opt) => {
602
+ const checkboxWrapper = document.createElement("label");
603
+ checkboxWrapper.className = "checkbox-label";
604
+
605
+ const checkbox = document.createElement("input");
606
+ checkbox.type = "checkbox";
607
+ const optValue = opt.value || opt.slug || opt.id;
608
+ checkbox.value = optValue;
609
+ checkbox.checked = currentValues.includes(optValue);
610
+ checkbox.addEventListener("change", () => {
611
+ const current = get(fieldId) || [];
612
+ const updated = checkbox.checked
613
+ ? [...current, optValue]
614
+ : current.filter((v) => v !== optValue);
615
+ set(fieldId, updated);
616
+ if (onChange) {onChange(updated);}
617
+ });
618
+
619
+ checkboxWrapper.appendChild(checkbox);
620
+ checkboxWrapper.appendChild(document.createTextNode(" " + (opt.label || opt.name || opt.display_name || opt.value)));
621
+ checkboxContainer.appendChild(checkboxWrapper);
622
+ });
623
+
624
+ field.appendChild(checkboxContainer);
625
+ return field;
626
+ }
627
+
628
+ /**
629
+ * Create a record select field (single record from an object, with search)
630
+ * Uses RecordSelect component when available and superleapClient is set.
631
+ * @param {Object} config - Configuration object
632
+ * @param {string} config.label - Field label
633
+ * @param {string} config.fieldId - State key for this field
634
+ * @param {string} config.objectSlug - Object slug (e.g. "account", "opportunity")
635
+ * @param {string} [config.placeholder] - Placeholder text
636
+ * @param {string} [config.searchPlaceholder] - Search input placeholder
637
+ * @param {boolean} [config.required] - Whether field is required
638
+ * @param {Function} [config.onChange] - Optional change handler (value, record?) => void
639
+ * @param {boolean} [config.disabled] - Whether record select is disabled
640
+ * @param {string} [config.variant] - 'default' | 'error' | 'warning' | 'borderless' | 'inline'
641
+ * @param {string} [config.size] - 'default' | 'large' | 'small'
642
+ * @param {boolean} [config.canClear] - Show clear button when value is set
643
+ * @param {number} [config.initialLimit] - Initial fetch limit
644
+ * @param {string} [config.helpText] - Optional help text for tooltip
645
+ * @returns {HTMLElement} Field element
646
+ */
647
+ function createRecordSelect(config) {
648
+ const {
649
+ label,
650
+ fieldId,
651
+ objectSlug,
652
+ placeholder,
653
+ searchPlaceholder,
654
+ required = false,
655
+ onChange,
656
+ disabled = false,
657
+ variant,
658
+ size,
659
+ canClear,
660
+ initialLimit,
661
+ helpText = null,
662
+ } = config;
663
+
664
+ const field = createFieldWrapper(label, required, helpText);
665
+
666
+ if (getComponent("RecordSelect") && getComponent("RecordSelect").create) {
667
+ const currentValue = get(fieldId) || "";
668
+
669
+ const recordSelectEl = getComponent("RecordSelect").create({
670
+ fieldId,
671
+ objectSlug,
672
+ placeholder: placeholder || `Select ${label}`,
673
+ searchPlaceholder,
674
+ value: currentValue,
675
+ disabled,
676
+ variant: variant || "default",
677
+ size: size || "default",
678
+ canClear: !!canClear,
679
+ initialLimit,
680
+ onChange: (value, record) => {
681
+ set(fieldId, value);
682
+ if (onChange) onChange(value, record);
683
+ },
684
+ });
685
+
686
+ recordSelectEl._fieldId = fieldId;
687
+ field.appendChild(recordSelectEl);
688
+ return field;
689
+ }
690
+
691
+ const fallback = document.createElement("div");
692
+ fallback.className = "text-reg-13 text-typography-quaternary-text";
693
+ fallback.textContent = "Record select requires RecordSelect component and SuperLeap client.";
694
+ field.appendChild(fallback);
695
+ return field;
696
+ }
697
+
698
+ /**
699
+ * Create a record multi-select field (multiple records from an object, with search)
700
+ * @param {Object} config - Configuration object
701
+ * @param {string} config.label - Field label
702
+ * @param {string} config.fieldId - State key (stores array of record ids)
703
+ * @param {string} config.objectSlug - Object slug (e.g. "account", "opportunity")
704
+ * @param {string} [config.placeholder] - Placeholder text
705
+ * @param {string} [config.searchPlaceholder] - Search input placeholder
706
+ * @param {boolean} [config.required=false]
707
+ * @param {Function} [config.onChange] - (values: string[], records?) => void
708
+ * @param {boolean} [config.disabled=false]
709
+ * @param {string} [config.variant] - 'default' | 'error' | 'warning' | 'borderless' | 'inline'
710
+ * @param {string} [config.size] - 'default' | 'large' | 'small'
711
+ * @param {number} [config.initialLimit] - Initial fetch limit
712
+ * @param {Array<string>} [config.displayFields] - Fields to show as secondary info
713
+ * @param {string} [config.helpText] - Optional help text for tooltip
714
+ * @returns {HTMLElement} Field element
715
+ */
716
+ function createRecordMultiSelect(config) {
717
+ const {
718
+ label,
719
+ fieldId,
720
+ objectSlug,
721
+ placeholder,
722
+ searchPlaceholder,
723
+ required = false,
724
+ onChange,
725
+ disabled = false,
726
+ variant,
727
+ size,
728
+ initialLimit,
729
+ displayFields,
730
+ helpText = null,
731
+ } = config;
732
+
733
+ const field = createFieldWrapper(label, required, helpText);
734
+
735
+ if (getComponent("RecordMultiSelect") && getComponent("RecordMultiSelect").create) {
736
+ const currentValues = get(fieldId) || [];
737
+ const recordMultiEl = getComponent("RecordMultiSelect").create({
738
+ fieldId,
739
+ objectSlug,
740
+ placeholder: placeholder || `Select ${label}`,
741
+ searchPlaceholder,
742
+ value: currentValues,
743
+ disabled,
744
+ variant: variant || "default",
745
+ size: size || "default",
746
+ initialLimit,
747
+ displayFields: displayFields || [],
748
+ onValuesChange: (values, records) => {
749
+ set(fieldId, values);
750
+ if (onChange) onChange(values, records);
751
+ },
752
+ });
753
+ recordMultiEl._fieldId = fieldId;
754
+ field.appendChild(recordMultiEl);
755
+ return field;
756
+ }
757
+
758
+ const fallback = document.createElement("div");
759
+ fallback.className = "text-reg-13 text-typography-quaternary-text";
760
+ fallback.textContent = "Record multi-select requires RecordMultiSelect component and SuperLeap client.";
761
+ field.appendChild(fallback);
762
+ return field;
763
+ }
764
+
765
+ /**
766
+ * Create an enum select field (options from SuperLeap object column)
767
+ * @param {Object} config - Configuration object
768
+ * @param {string} config.label - Field label
769
+ * @param {string} config.fieldId - State key for this field
770
+ * @param {string} config.objectSlug - Object slug (e.g. "accounts")
771
+ * @param {string} config.columnSlug - Column slug (e.g. "status")
772
+ * @param {string} [config.placeholder] - Placeholder text
773
+ * @param {boolean} [config.required=false]
774
+ * @param {Function} [config.onChange] - (value: string) => void
775
+ * @param {boolean} [config.disabled=false]
776
+ * @param {string} [config.variant] - 'default' | 'error' | 'warning' | 'borderless' | 'inline'
777
+ * @param {string} [config.size] - 'default' | 'large' | 'small'
778
+ * @param {boolean} [config.canClear] - Show clear button
779
+ * @param {Object} [config.currentRecordData] - For dependent fields
780
+ * @param {string} [config.helpText] - Optional help text
781
+ * @returns {HTMLElement} Field element
782
+ */
783
+ function createEnumSelect(config) {
784
+ const {
785
+ label,
786
+ fieldId,
787
+ objectSlug,
788
+ columnSlug,
789
+ placeholder,
790
+ required = false,
791
+ onChange,
792
+ disabled = false,
793
+ variant,
794
+ size,
795
+ canClear,
796
+ currentRecordData,
797
+ helpText = null,
798
+ } = config;
799
+
800
+ const field = createFieldWrapper(label, required, helpText);
801
+
802
+ if (getComponent("EnumSelect") && getComponent("EnumSelect").create) {
803
+ const currentValue = get(fieldId) || "";
804
+ const enumSelectEl = getComponent("EnumSelect").create({
805
+ fieldId,
806
+ objectSlug,
807
+ columnSlug,
808
+ placeholder: placeholder || `Select ${label}`,
809
+ value: currentValue,
810
+ disabled,
811
+ variant: variant || "default",
812
+ size: size || "default",
813
+ canClear: !!canClear,
814
+ currentRecordData: currentRecordData || {},
815
+ onChange: (value) => {
816
+ set(fieldId, value);
817
+ if (onChange) onChange(value);
818
+ },
819
+ });
820
+ enumSelectEl._fieldId = fieldId;
821
+ field.appendChild(enumSelectEl);
822
+ return field;
823
+ }
824
+
825
+ const fallback = document.createElement("div");
826
+ fallback.className = "text-reg-13 text-typography-quaternary-text";
827
+ fallback.textContent = "Enum select requires EnumSelect component and SuperLeap client.";
828
+ field.appendChild(fallback);
829
+ return field;
830
+ }
831
+
832
+ /**
833
+ * Create an enum multi-select field (options from SuperLeap object column)
834
+ * @param {Object} config - Configuration object
835
+ * @param {string} config.label - Field label
836
+ * @param {string} config.fieldId - State key (stores array of values)
837
+ * @param {string} config.objectSlug - Object slug (e.g. "accounts")
838
+ * @param {string} config.columnSlug - Column slug (e.g. "tags")
839
+ * @param {string} [config.placeholder] - Placeholder text
840
+ * @param {boolean} [config.required=false]
841
+ * @param {Function} [config.onChange] - (values: string[]) => void
842
+ * @param {boolean} [config.disabled=false]
843
+ * @param {string} [config.variant] - 'default' | 'error' | 'warning' | 'borderless' | 'inline'
844
+ * @param {string} [config.size] - 'default' | 'large' | 'small'
845
+ * @param {boolean} [config.canClear] - Show clear button
846
+ * @param {Object} [config.currentRecordData] - For dependent fields
847
+ * @param {string} [config.helpText] - Optional help text
848
+ * @returns {HTMLElement} Field element
849
+ */
850
+ function createEnumMultiSelect(config) {
851
+ const {
852
+ label,
853
+ fieldId,
854
+ objectSlug,
855
+ columnSlug,
856
+ placeholder,
857
+ required = false,
858
+ onChange,
859
+ disabled = false,
860
+ variant,
861
+ size,
862
+ canClear,
863
+ currentRecordData,
864
+ helpText = null,
865
+ } = config;
866
+
867
+ const field = createFieldWrapper(label, required, helpText);
868
+
869
+ if (getComponent("EnumMultiSelect") && getComponent("EnumMultiSelect").create) {
870
+ const currentValues = get(fieldId) || [];
871
+ const enumMultiEl = getComponent("EnumMultiSelect").create({
872
+ fieldId,
873
+ objectSlug,
874
+ columnSlug,
875
+ placeholder: placeholder || `Select ${label}`,
876
+ value: currentValues,
877
+ disabled,
878
+ variant: variant || "default",
879
+ size: size || "default",
880
+ canClear: !!canClear,
881
+ currentRecordData: currentRecordData || {},
882
+ onChange: (values) => {
883
+ set(fieldId, values);
884
+ if (onChange) onChange(values);
885
+ },
886
+ });
887
+ enumMultiEl._fieldId = fieldId;
888
+ field.appendChild(enumMultiEl);
889
+ return field;
890
+ }
891
+
892
+ const fallback = document.createElement("div");
893
+ fallback.className = "text-reg-13 text-typography-quaternary-text";
894
+ fallback.textContent = "Enum multi-select requires EnumMultiSelect component and SuperLeap client.";
895
+ field.appendChild(fallback);
896
+ return field;
897
+ }
898
+
899
+ /**
900
+ * Create a duration picker field (stores value in seconds or milliseconds)
901
+ * @param {Object} config - Configuration object
902
+ * @param {string} config.label - Field label
903
+ * @param {string} config.fieldId - State key for this field
904
+ * @param {number|null} [config.value] - Initial value (seconds or ms per formatType)
905
+ * @param {string} [config.formatType='seconds'] - 'seconds' | 'milliseconds'
906
+ * @param {string} [config.placeholder='hh:mm:ss']
907
+ * @param {boolean} [config.required=false]
908
+ * @param {Function} [config.onChange] - (value: number|null) => void
909
+ * @param {boolean} [config.disabled=false]
910
+ * @param {string} [config.variant] - 'default' | 'error' | 'warning' | 'success' | 'borderless' | 'inline'
911
+ * @param {string} [config.size] - 'small' | 'default' | 'large'
912
+ * @param {string} [config.helpText] - Optional help text
913
+ * @returns {HTMLElement} Field element
914
+ */
915
+ function createDuration(config) {
916
+ const {
917
+ label,
918
+ fieldId,
919
+ value: initialValue,
920
+ formatType = "seconds",
921
+ placeholder = "hh:mm:ss",
922
+ required = false,
923
+ onChange,
924
+ disabled = false,
925
+ variant = "default",
926
+ size = "default",
927
+ helpText = null,
928
+ } = config;
929
+
930
+ const field = createFieldWrapper(label, required, helpText);
931
+ field.setAttribute("data-field-id", fieldId);
932
+
933
+ if (getComponent("Duration") && getComponent("Duration").create) {
934
+ const currentValue = get(fieldId);
935
+ const valueToUse = currentValue !== undefined && currentValue !== null ? currentValue : initialValue;
936
+ const durationEl = getComponent("Duration").create({
937
+ value: valueToUse,
938
+ formatType,
939
+ placeholder,
940
+ disabled,
941
+ variant,
942
+ size,
943
+ onChange: (value) => {
944
+ set(fieldId, value);
945
+ if (onChange) onChange(value);
946
+ },
947
+ });
948
+ durationEl._fieldId = fieldId;
949
+ field.appendChild(durationEl);
950
+ return field;
951
+ }
952
+
953
+ const fallback = document.createElement("div");
954
+ fallback.className = "text-reg-13 text-typography-quaternary-text";
955
+ fallback.textContent = "Duration picker requires Duration component.";
956
+ field.appendChild(fallback);
957
+ return field;
958
+ }
959
+
960
+ /**
961
+ * Create an enumeration field (N items, count 0..totalElements, e.g. star rating)
962
+ * @param {Object} config - Configuration object
963
+ * @param {string} config.label - Field label
964
+ * @param {string} config.fieldId - State key (stores number 0..totalElements)
965
+ * @param {number} config.totalElements - Number of items (e.g. 5 for 5 stars)
966
+ * @param {string|HTMLElement} config.enabledIcon - Icon when item is selected
967
+ * @param {string|HTMLElement} config.disabledIcon - Icon when item is not selected
968
+ * @param {number} [config.defaultValue=0] - Initial count
969
+ * @param {boolean} [config.required=false]
970
+ * @param {Function} [config.onChange] - (count: number) => void
971
+ * @param {boolean} [config.disabled=false]
972
+ * @param {boolean} [config.readOnly=false]
973
+ * @param {string} [config.variant] - 'default' | 'error' | 'warning' | 'success' | 'borderless' | 'inline'
974
+ * @param {string} [config.size] - 'default' | 'large' | 'small'
975
+ * @param {string} [config.helpText] - Optional help text
976
+ * @returns {HTMLElement} Field element
977
+ */
978
+ function createEnumeration(config) {
979
+ const {
980
+ label,
981
+ fieldId,
982
+ totalElements,
983
+ enabledIcon,
984
+ disabledIcon,
985
+ defaultValue = 0,
986
+ required = false,
987
+ onChange,
988
+ disabled = false,
989
+ readOnly = false,
990
+ variant = "default",
991
+ size = "default",
992
+ helpText = null,
993
+ } = config;
994
+
995
+ const field = createFieldWrapper(label, required, helpText);
996
+ field.setAttribute("data-field-id", fieldId);
997
+
998
+ if (getComponent("Enumeration") && getComponent("Enumeration").create) {
999
+ const currentValue = get(fieldId);
1000
+ const valueToUse = currentValue !== undefined && currentValue !== null ? currentValue : defaultValue;
1001
+ const enumEl = getComponent("Enumeration").create({
1002
+ totalElements,
1003
+ enabledIcon,
1004
+ disabledIcon,
1005
+ defaultValue: valueToUse,
1006
+ disabled,
1007
+ readOnly,
1008
+ variant,
1009
+ size,
1010
+ onValueChange: (count) => {
1011
+ set(fieldId, count);
1012
+ if (onChange) onChange(count);
1013
+ },
1014
+ });
1015
+ enumEl._fieldId = fieldId;
1016
+ field.appendChild(enumEl);
1017
+ return field;
1018
+ }
1019
+
1020
+ const fallback = document.createElement("div");
1021
+ fallback.className = "text-reg-13 text-typography-quaternary-text";
1022
+ fallback.textContent = "Enumeration requires Enumeration component.";
1023
+ field.appendChild(fallback);
1024
+ return field;
1025
+ }
1026
+
1027
+ /**
1028
+ * Create a file upload field
1029
+ * When FileInput component is available, uses S3 upload and stores array of URLs in state.
1030
+ * Otherwise stores array of File objects (client-side only).
1031
+ * @param {Object} config - Configuration object
1032
+ * @param {string} config.label - Field label
1033
+ * @param {string} config.fieldId - State key for this field (stores array of files or URLs)
1034
+ * @param {boolean} config.multiple - Allow multiple files
1035
+ * @param {string} config.accept - Accepted file types
1036
+ * @param {boolean} config.required - Whether field is required
1037
+ * @param {string} [config.helpText] - Optional help text
1038
+ * @param {boolean} [config.isPrivate] - Whether files are private (FileInput only)
1039
+ * @param {number} [config.maxFiles] - Max files when multiple (FileInput only)
1040
+ * @param {number} [config.maxFileSize] - Max file size in bytes (FileInput only)
1041
+ * @returns {HTMLElement} Field element
1042
+ */
1043
+ function createFileUpload(config) {
1044
+ const { label, fieldId, multiple = true, accept, required = false, helpText = null, isPrivate, maxFiles, maxFileSize } = config;
1045
+
1046
+ if (getComponent("FileInput") && getComponent("FileInput").create) {
1047
+ return getComponent("FileInput").create({
1048
+ label,
1049
+ fieldId,
1050
+ multiple,
1051
+ accept: accept || "*",
1052
+ required,
1053
+ helpText,
1054
+ isPrivate: !!isPrivate,
1055
+ maxFiles: maxFiles != null ? maxFiles : null,
1056
+ maxFileSize: maxFileSize != null ? maxFileSize : 10 * 1024 * 1024,
1057
+ });
1058
+ }
1059
+
1060
+ const field = createFieldWrapper(label, required, helpText);
1061
+
1062
+ const uploadWrapper = document.createElement("div");
1063
+ uploadWrapper.className = "file-upload-wrapper";
1064
+
1065
+ const btn = document.createElement("div");
1066
+ btn.className = "file-upload-button";
1067
+ btn.textContent = "Choose a file";
1068
+
1069
+ const currentFiles = get(fieldId) || [];
1070
+ const statusText = document.createElement("div");
1071
+ statusText.className = "file-upload-text";
1072
+ statusText.textContent = currentFiles.length > 0
1073
+ ? `${currentFiles.length} file(s) selected`
1074
+ : "No files chosen";
1075
+
1076
+ const input = document.createElement("input");
1077
+ input.type = "file";
1078
+ input.multiple = multiple;
1079
+ input.className = "file-upload-input";
1080
+ if (accept) {input.accept = accept;}
1081
+
1082
+ input.addEventListener("change", function () {
1083
+ const files = Array.from(this.files || []);
1084
+ set(fieldId, files);
1085
+ statusText.textContent = files.length > 0
1086
+ ? `${files.length} file(s) selected`
1087
+ : "No files chosen";
1088
+ });
1089
+
1090
+ uploadWrapper.appendChild(btn);
1091
+ uploadWrapper.appendChild(statusText);
1092
+ uploadWrapper.appendChild(input);
1093
+ field.appendChild(uploadWrapper);
1094
+
1095
+ const hint = document.createElement("div");
1096
+ hint.className = "field-hint";
1097
+ hint.textContent = multiple ? "Multiple files allowed." : "Single file only.";
1098
+ field.appendChild(hint);
1099
+
1100
+ return field;
1101
+ }
1102
+
1103
+ /**
1104
+ * Create a currency field (decimal input with 2 decimal places, SubOption-style layout).
1105
+ * State stores number | null.
1106
+ * @param {Object} config - Configuration object
1107
+ * @param {string} config.label - Field label (optional; column label/currency used when available)
1108
+ * @param {string} config.fieldId - State key for this field
1109
+ * @param {Object} [config.column] - { properties?: { currency?: { currency?: string } }, placeholder?: string }
1110
+ * @param {string} [config.placeholder]
1111
+ * @param {boolean} [config.required=false]
1112
+ * @param {string} [config.helpText]
1113
+ * @param {string} [config.variant] - 'inline' | 'default' | 'borderless' | 'error' | 'warning'
1114
+ * @param {string} [config.size] - 'small' | 'default' | 'large'
1115
+ * @param {boolean} [config.disabled=false]
1116
+ * @param {Function} [config.onChange]
1117
+ * @returns {HTMLElement} Field element
1118
+ */
1119
+ function createCurrency(config) {
1120
+ const {
1121
+ label,
1122
+ fieldId,
1123
+ column = {},
1124
+ placeholder,
1125
+ required = false,
1126
+ helpText = null,
1127
+ variant = "default",
1128
+ size = "default",
1129
+ disabled = false,
1130
+ onChange,
1131
+ } = config;
1132
+
1133
+ const field = createFieldWrapper(label || "Amount", required, helpText);
1134
+ field.setAttribute("data-field-id", fieldId);
1135
+
1136
+ if (getComponent("CurrencyComponent") && getComponent("CurrencyComponent").create) {
1137
+ const currentValue = get(fieldId);
1138
+ const currencyEl = getComponent("CurrencyComponent").create({
1139
+ variant,
1140
+ size,
1141
+ placeholder: placeholder || column.placeholder || "Enter value",
1142
+ disabled,
1143
+ column,
1144
+ value: currentValue != null ? currentValue : null,
1145
+ onChange: (value) => {
1146
+ set(fieldId, value);
1147
+ if (onChange) onChange(value);
1148
+ },
1149
+ });
1150
+ currencyEl._fieldId = fieldId;
1151
+ field.appendChild(currencyEl);
1152
+ return field;
1153
+ }
1154
+
1155
+ const input = document.createElement("input");
1156
+ input.type = "number";
1157
+ input.step = "0.01";
1158
+ input.placeholder = placeholder || "Enter amount";
1159
+ input.value = get(fieldId) != null ? get(fieldId) : "";
1160
+ input.disabled = disabled;
1161
+ input.className = "input w-full";
1162
+ input.addEventListener("input", (e) => {
1163
+ const raw = e.target.value;
1164
+ const num = raw === "" ? null : parseFloat(raw);
1165
+ set(fieldId, isNaN(num) ? null : num);
1166
+ if (onChange) onChange(isNaN(num) ? null : num);
1167
+ });
1168
+ field.appendChild(input);
1169
+ return field;
1170
+ }
1171
+
1172
+ /**
1173
+ * Create a phone input field (international phone number with country selector)
1174
+ * State stores string in format "callingCode-nationalNumber" (e.g., "91-9876543210")
1175
+ * @param {Object} config - Configuration object
1176
+ * @param {string} config.label - Field label
1177
+ * @param {string} config.fieldId - State key for this field
1178
+ * @param {string} [config.defaultCountryCode] - Default country code (e.g., 'US', 'IN')
1179
+ * @param {string} [config.placeholder] - Custom placeholder
1180
+ * @param {boolean} [config.required=false]
1181
+ * @param {string} [config.helpText]
1182
+ * @param {string} [config.variant] - 'default' | 'error' | 'warning' | 'borderless' | 'inline'
1183
+ * @param {string} [config.inputSize] - 'default' | 'large' | 'small'
1184
+ * @param {boolean} [config.disabled=false]
1185
+ * @param {boolean} [config.disableCountrySelect=false] - Disable country selector
1186
+ * @param {boolean} [config.hideCountrySelect=false] - Hide country selector completely
1187
+ * @param {Function} [config.onChange] - Optional change handler (fullValue, country)
1188
+ * @returns {HTMLElement} Field element
1189
+ */
1190
+ function createPhoneInput(config) {
1191
+ const {
1192
+ label,
1193
+ fieldId,
1194
+ defaultCountryCode,
1195
+ placeholder,
1196
+ required = false,
1197
+ helpText = null,
1198
+ variant = "default",
1199
+ inputSize = "default",
1200
+ disabled = false,
1201
+ disableCountrySelect = false,
1202
+ hideCountrySelect = false,
1203
+ onChange,
1204
+ } = config;
1205
+
1206
+ const field = createFieldWrapper(label, required, helpText);
1207
+ field.setAttribute("data-field-id", fieldId);
1208
+
1209
+ if (getComponent("PhoneInput") && getComponent("PhoneInput").create) {
1210
+ const currentValue = get(fieldId) || "";
1211
+ const phoneInputEl = getComponent("PhoneInput").create({
1212
+ variant,
1213
+ inputSize,
1214
+ defaultCountryCode: defaultCountryCode || "IN",
1215
+ defaultPhoneNumber: currentValue,
1216
+ placeholder,
1217
+ disabled,
1218
+ disableCountrySelect,
1219
+ hideCountrySelect,
1220
+ onChange: (fullValue, country) => {
1221
+ set(fieldId, fullValue);
1222
+ if (onChange) onChange(fullValue, country);
1223
+ },
1224
+ });
1225
+ phoneInputEl._fieldId = fieldId;
1226
+ field.appendChild(phoneInputEl);
1227
+ return field;
1228
+ }
1229
+
1230
+ // Fallback: simple tel input
1231
+ const input = document.createElement("input");
1232
+ input.type = "tel";
1233
+ input.className = "input";
1234
+ input.placeholder = placeholder || "Enter phone number";
1235
+ input.value = get(fieldId) || "";
1236
+ input.disabled = disabled;
1237
+ input.addEventListener("input", (e) => {
1238
+ set(fieldId, e.target.value);
1239
+ if (onChange) onChange(e.target.value, null);
1240
+ });
1241
+ field.appendChild(input);
1242
+ return field;
1243
+ }
1244
+
1245
+ /**
1246
+ * Create a checkbox field with optional label and help text
1247
+ * @param {Object} config - { label, fieldId, checked, indeterminate, disabled, helpText, size, align, isLabelCaps, onChange }
1248
+ * @returns {HTMLElement} Field wrapper containing checkbox
1249
+ */
1250
+ function createCheckbox(config) {
1251
+ const {
1252
+ label,
1253
+ fieldId,
1254
+ checked = false,
1255
+ indeterminate = false,
1256
+ disabled = false,
1257
+ helpText = null,
1258
+ size = "default",
1259
+ align = "left",
1260
+ isLabelCaps = false,
1261
+ onChange,
1262
+ } = config;
1263
+
1264
+ const field = createFieldWrapper(label, false, helpText);
1265
+ field.setAttribute("data-field-id", fieldId);
1266
+
1267
+ if (getComponent("Checkbox") && getComponent("Checkbox").create) {
1268
+ const currentValue = get(fieldId);
1269
+ const checkboxEl = getComponent("Checkbox").create({
1270
+ id: fieldId,
1271
+ name: fieldId,
1272
+ checked: currentValue !== undefined ? currentValue : checked,
1273
+ indeterminate,
1274
+ disabled,
1275
+ size,
1276
+ align,
1277
+ isLabelCaps,
1278
+ onChange: (isChecked) => {
1279
+ set(fieldId, isChecked);
1280
+ if (onChange) onChange(isChecked);
1281
+ },
1282
+ });
1283
+ checkboxEl._fieldId = fieldId;
1284
+ field.appendChild(checkboxEl);
1285
+ return field;
1286
+ }
1287
+
1288
+ // Fallback: simple checkbox input
1289
+ const input = document.createElement("input");
1290
+ input.type = "checkbox";
1291
+ input.className = "checkbox";
1292
+ input.id = fieldId;
1293
+ input.name = fieldId;
1294
+ input.checked = get(fieldId) || checked;
1295
+ input.disabled = disabled;
1296
+ input.addEventListener("change", (e) => {
1297
+ set(fieldId, e.target.checked);
1298
+ if (onChange) onChange(e.target.checked);
1299
+ });
1300
+ field.appendChild(input);
1301
+ return field;
1302
+ }
1303
+
1304
+ // ============================================================================
1305
+ // STEPPER COMPONENT
1306
+ // ============================================================================
1307
+
1308
+ /**
1309
+ * Create/update a stepper component
1310
+ * @param {HTMLElement} container - Container element for stepper
1311
+ * @param {Array} steps - Array of { id, label } objects
1312
+ * @param {string} currentStepId - Currently active step ID
1313
+ */
1314
+ function renderStepper(container, steps, currentStepId) {
1315
+ if (!container) {return;}
1316
+ container.innerHTML = "";
1317
+
1318
+ steps.forEach((step, idx) => {
1319
+ const pill = document.createElement("div");
1320
+ pill.className = "step-pill" + (step.id === currentStepId ? " step-pill--active" : "");
1321
+
1322
+ const indexSpan = document.createElement("span");
1323
+ indexSpan.className = "step-pill-index";
1324
+ indexSpan.textContent = String(idx + 1);
1325
+
1326
+ const labelSpan = document.createElement("span");
1327
+ labelSpan.textContent = step.label;
1328
+
1329
+ pill.appendChild(indexSpan);
1330
+ pill.appendChild(labelSpan);
1331
+ container.appendChild(pill);
1332
+ });
1333
+ }
1334
+
1335
+ // ============================================================================
1336
+ // ALERT COMPONENT
1337
+ // ============================================================================
1338
+
1339
+ /**
1340
+ * Render alerts/errors
1341
+ * Uses Alert component when available for design-system styling.
1342
+ * @param {HTMLElement} container - Container element for alerts
1343
+ * @param {Array} messages - Array of error/info messages (strings) or { title?, description } objects
1344
+ * @param {string} type - Alert type: "error" | "info" | "success" | "warning" | "destructive" | "default"
1345
+ */
1346
+ function renderAlerts(container, messages = [], type = "error") {
1347
+ if (!container) {return;}
1348
+ container.innerHTML = "";
1349
+
1350
+ const Alert = getComponent("Alert");
1351
+ const useAlertComponent = Alert && typeof Alert.create === "function";
1352
+
1353
+ messages.forEach((msg) => {
1354
+ const description = typeof msg === "string" ? msg : (msg.description || msg.title || "");
1355
+ const title = typeof msg === "object" && msg.title ? msg.title : "";
1356
+
1357
+ if (useAlertComponent && (description || title)) {
1358
+ const variantMap = { error: "error", info: "info", success: "success" };
1359
+ const variant = variantMap[type] || type;
1360
+ const alertEl = Alert.create({ title, description, variant });
1361
+ if (alertEl) container.appendChild(alertEl);
1362
+ } else {
1363
+ const div = document.createElement("div");
1364
+ div.className = `alert alert--${type}`;
1365
+ div.textContent = description || title;
1366
+ container.appendChild(div);
1367
+ }
1368
+ });
1369
+ }
1370
+
1371
+ // ============================================================================
1372
+ // TABLE COMPONENT
1373
+ // ============================================================================
1374
+
1375
+ /**
1376
+ * Create a data table with radio selection
1377
+ * @param {Object} config - Configuration object
1378
+ * @param {Array} config.columns - Array of { key, label } for columns
1379
+ * @param {Array} config.data - Array of data objects
1380
+ * @param {string} config.fieldId - State key for selected row
1381
+ * @param {string} config.idKey - Key to use as unique identifier (default: "id")
1382
+ * @param {Function} config.onSelect - Optional callback when row is selected
1383
+ * @returns {HTMLElement} Table container element
1384
+ */
1385
+ function createDataTable(config) {
1386
+ const { columns, data = [], fieldId, idKey = "id", onSelect } = config;
1387
+
1388
+ const tableContainer = document.createElement("div");
1389
+ tableContainer.className = "table-container";
1390
+
1391
+ const tableScroll = document.createElement("div");
1392
+ tableScroll.className = "table-scroll";
1393
+
1394
+ const table = document.createElement("table");
1395
+ table.className = "table";
1396
+
1397
+ // Header
1398
+ const thead = document.createElement("thead");
1399
+ const headerRow = document.createElement("tr");
1400
+ headerRow.innerHTML = "<th></th>"; // Radio column
1401
+ columns.forEach((col) => {
1402
+ const th = document.createElement("th");
1403
+ th.textContent = col.label;
1404
+ headerRow.appendChild(th);
1405
+ });
1406
+ thead.appendChild(headerRow);
1407
+ table.appendChild(thead);
1408
+
1409
+ // Body
1410
+ const tbody = document.createElement("tbody");
1411
+
1412
+ if (data.length === 0) {
1413
+ const tr = document.createElement("tr");
1414
+ tr.innerHTML = `<td colspan="${columns.length + 1}" style="text-align: center; padding: 2rem; color: var(--text-muted);">No results found.</td>`;
1415
+ tbody.appendChild(tr);
1416
+ } else {
1417
+ data.forEach((row) => {
1418
+ const tr = document.createElement("tr");
1419
+
1420
+ // Radio cell
1421
+ const tdRadio = document.createElement("td");
1422
+ const radio = document.createElement("input");
1423
+ radio.type = "radio";
1424
+ radio.name = fieldId;
1425
+ radio.value = row[idKey];
1426
+ radio.checked = get(fieldId) === row[idKey];
1427
+ radio.addEventListener("change", () => {
1428
+ set(fieldId, row[idKey]);
1429
+ if (onSelect) {onSelect(row);}
1430
+ });
1431
+ tdRadio.appendChild(radio);
1432
+ tr.appendChild(tdRadio);
1433
+
1434
+ // Data cells
1435
+ columns.forEach((col) => {
1436
+ const td = document.createElement("td");
1437
+ td.textContent = row[col.key] || "";
1438
+ tr.appendChild(td);
1439
+ });
1440
+
1441
+ tbody.appendChild(tr);
1442
+ });
1443
+ }
1444
+
1445
+ table.appendChild(tbody);
1446
+ tableScroll.appendChild(table);
1447
+ tableContainer.appendChild(tableScroll);
1448
+
1449
+ return tableContainer;
1450
+ }
1451
+
1452
+ /**
1453
+ * Create a design-system table (SuperleapTable component)
1454
+ * For radio-selection table use createDataTable instead.
1455
+ * @param {Object} config - Configuration object
1456
+ * @param {Array} config.data - Array of row objects
1457
+ * @param {Array} config.columns - Column definitions { header, accessor?, cell?(row) }
1458
+ * @param {boolean} [config.showHeader=true]
1459
+ * @param {string} [config.headerSize='small'] - 'small' | 'default' | 'large'
1460
+ * @param {boolean} [config.hasBorder=true]
1461
+ * @param {Function} [config.onRowClick] - (rowId) => void
1462
+ * @param {Function} [config.onFetch] - For infinite scroll
1463
+ * @param {boolean} [config.hasMore=false]
1464
+ * @param {boolean} [config.isLoading=false]
1465
+ * @param {string} [config.emptyMessage='No data available']
1466
+ * @returns {HTMLElement|Object} Table container or table instance
1467
+ */
1468
+ function createTable(config) {
1469
+ const SuperleapTable = getComponent("SuperleapTable");
1470
+ if (SuperleapTable && typeof SuperleapTable.createTable === "function") {
1471
+ return SuperleapTable.createTable(config);
1472
+ }
1473
+ const tableContainer = document.createElement("div");
1474
+ tableContainer.className = "table-container";
1475
+ tableContainer.textContent = "Table component (SuperleapTable) not loaded.";
1476
+ return tableContainer;
1477
+ }
1478
+
1479
+ /**
1480
+ * Create a search input for filtering
1481
+ * @param {Object} config - Configuration object
1482
+ * @param {string} config.placeholder - Placeholder text
1483
+ * @param {string} config.fieldId - State key for search query
1484
+ * @param {Function} config.onSearch - Callback when search value changes
1485
+ * @returns {HTMLElement} Search container element
1486
+ */
1487
+ function createSearchInput(config) {
1488
+ const { placeholder = "Search...", fieldId, onSearch } = config;
1489
+
1490
+ const searchContainer = document.createElement("div");
1491
+ searchContainer.className = "search-container";
1492
+ searchContainer.innerHTML = `
1493
+ <svg class="search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
1494
+ <input type="text" class="search-input" placeholder="${placeholder}" value="${get(fieldId) || ""}">
1495
+ `;
1496
+
1497
+ const searchInput = searchContainer.querySelector(".search-input");
1498
+ searchInput.addEventListener("input", (e) => {
1499
+ set(fieldId, e.target.value.toLowerCase());
1500
+ if (onSearch) {onSearch(e.target.value.toLowerCase());}
1501
+ });
1502
+
1503
+ return searchContainer;
1504
+ }
1505
+
1506
+ // ============================================================================
1507
+ // SUMMARY/REVIEW COMPONENT
1508
+ // ============================================================================
1509
+
1510
+ /**
1511
+ * Create a summary row for review screens
1512
+ * @param {string} label - Row label
1513
+ * @param {string} value - Row value
1514
+ * @returns {HTMLElement} Field element
1515
+ */
1516
+ function createSummaryRow(label, value) {
1517
+ const field = document.createElement("div");
1518
+ field.className = "field";
1519
+
1520
+ const l = document.createElement("div");
1521
+ l.className = "field-label";
1522
+ l.textContent = label;
1523
+
1524
+ const v = document.createElement("div");
1525
+ v.className = "field-hint";
1526
+ v.textContent = value || "-";
1527
+
1528
+ field.appendChild(l);
1529
+ field.appendChild(v);
1530
+ return field;
1531
+ }
1532
+
1533
+ // ============================================================================
1534
+ // TOAST NOTIFICATIONS
1535
+ // ============================================================================
1536
+
1537
+ /**
1538
+ * Create a badge element (Badge component)
1539
+ * @param {Object} config - { variant, color, size, startIcon, endIcon, icon, content, className }
1540
+ * @returns {HTMLElement} Badge element
1541
+ */
1542
+ function createBadge(config) {
1543
+ const Badge = getComponent("Badge");
1544
+ if (Badge && typeof Badge.create === "function") {
1545
+ return Badge.create(config);
1546
+ }
1547
+ const span = document.createElement("span");
1548
+ span.className = "badge";
1549
+ span.textContent = config && (config.content || "");
1550
+ return span;
1551
+ }
1552
+
1553
+ /**
1554
+ * Create an avatar element (Avatar component)
1555
+ * @param {Object} config - { name, image?, size?, shape?, className? }
1556
+ * @returns {HTMLElement} Avatar element
1557
+ */
1558
+ function createAvatar(config) {
1559
+ const Avatar = getComponent("Avatar");
1560
+ if (Avatar && typeof Avatar.create === "function") {
1561
+ return Avatar.create(config);
1562
+ }
1563
+ const div = document.createElement("div");
1564
+ div.className = "flex shrink-0 size-32 items-center justify-center rounded-full bg-primary-base";
1565
+ div.textContent = (config && config.name && config.name.charAt(0)) || "?";
1566
+ return div;
1567
+ }
1568
+
1569
+ /**
1570
+ * Create a vivid avatar (name-based background and text color)
1571
+ * @param {Object} config - { name, image?, size?, shape?, className? }
1572
+ * @returns {HTMLElement} VividAvatar element
1573
+ */
1574
+ function createVividAvatar(config) {
1575
+ const Avatar = getComponent("Avatar");
1576
+ if (Avatar && typeof Avatar.createVivid === "function") {
1577
+ return Avatar.createVivid(config);
1578
+ }
1579
+ return createAvatar(config);
1580
+ }
1581
+
1582
+ /**
1583
+ * Create an avatar group (overlapping avatars, max 3 + remainder)
1584
+ * @param {Object} config - { users: [{ id, name, image? }], size?, className? }
1585
+ * @returns {HTMLElement} AvatarGroup wrapper element
1586
+ */
1587
+ function createAvatarGroup(config) {
1588
+ const Avatar = getComponent("Avatar");
1589
+ if (Avatar && typeof Avatar.createGroup === "function") {
1590
+ return Avatar.createGroup(config);
1591
+ }
1592
+ const div = document.createElement("div");
1593
+ div.className = "flex -space-x-4";
1594
+ div.textContent = "";
1595
+ return div;
1596
+ }
1597
+
1598
+ /**
1599
+ * Create a vivid avatar group (each avatar uses name-based color)
1600
+ * @param {Object} config - { users: [{ id, name, image? }], size?, className? }
1601
+ * @returns {HTMLElement} VividAvatarGroup wrapper element
1602
+ */
1603
+ function createVividAvatarGroup(config) {
1604
+ const Avatar = getComponent("Avatar");
1605
+ if (Avatar && typeof Avatar.createVividGroup === "function") {
1606
+ return Avatar.createVividGroup(config);
1607
+ }
1608
+ return createAvatarGroup(config);
1609
+ }
1610
+
1611
+ /**
1612
+ * Create a loader/spinner element (Loader/Spinner component)
1613
+ * @param {Object} config - { size: 'small'|'medium'|'large', color, text }
1614
+ * @returns {HTMLElement} Spinner container element
1615
+ */
1616
+ function createLoader(config) {
1617
+ const Loader = getComponent("Loader");
1618
+ const Spinner = getComponent("Spinner");
1619
+ const comp = Loader || Spinner;
1620
+ if (comp && typeof comp.create === "function") {
1621
+ return comp.create(config || {});
1622
+ }
1623
+ const div = document.createElement("div");
1624
+ div.className = "spinner";
1625
+ div.textContent = config && config.text ? config.text : "Loading...";
1626
+ return div;
1627
+ }
1628
+
1629
+ /**
1630
+ * Show a toast notification
1631
+ * Wrapper for the Toast component (components/toast.js)
1632
+ * @param {string} message - Toast message
1633
+ * @param {string} type - Toast type: "success", "error", "warning", "info", "loading"
1634
+ * @param {number} duration - Duration in ms (default 4000, 0 for persistent)
1635
+ * @returns {Object} Toast API {close, element}
1636
+ */
1637
+ function showToast(message, type = "info", duration = 4000) {
1638
+ // Check if Toast component is available
1639
+ const Toast = getComponent("Toast");
1640
+ if (Toast && typeof Toast.show === "function") {
1641
+ return Toast.show(message, type, duration);
1642
+ }
1643
+
1644
+ // Fallback: log to console if Toast component not loaded
1645
+ console.warn("[FlowUI] Toast component not loaded. Message:", message, "Type:", type);
1646
+ return { close: function() {}, element: null };
1647
+ }
1648
+
1649
+ // ============================================================================
1650
+ // EXPORT PUBLIC API
1651
+ // ============================================================================
1652
+
1653
+ global.FlowUI = {
1654
+ // State management
1655
+ initState,
1656
+ getState,
1657
+ setState,
1658
+ get,
1659
+ set,
1660
+
1661
+ // Screen utilities
1662
+ createScreen,
1663
+ createGrid,
1664
+ createFieldWrapper,
1665
+
1666
+ // Form components
1667
+ createInput,
1668
+ createTextarea,
1669
+ createSelect,
1670
+ createTimePicker,
1671
+ createDateTimePicker,
1672
+ createRadioGroup,
1673
+ createMultiSelect,
1674
+ createRecordSelect,
1675
+ createRecordMultiSelect,
1676
+ createEnumSelect,
1677
+ createEnumMultiSelect,
1678
+ createDuration,
1679
+ createEnumeration,
1680
+ createFileUpload,
1681
+ createCurrency,
1682
+ createPhoneInput,
1683
+ createCheckbox,
1684
+
1685
+ // Button (delegates to Button component when available; resolved at call time via getComponent)
1686
+ createButton: function (config) {
1687
+ const Button = getComponent("Button");
1688
+ if (Button && typeof Button.create === "function") {
1689
+ return Button.create(config);
1690
+ }
1691
+ const btn = document.createElement("button");
1692
+ btn.type = (config && config.type) || "button";
1693
+ btn.className = "btn " + (config && config.variant === "primary" ? "btn-primary" : "btn-ghost");
1694
+ btn.textContent = (config && (config.text || config.label)) || "Button";
1695
+ if (config && config.disabled) btn.disabled = true;
1696
+ if (config && typeof config.onClick === "function") {
1697
+ btn.addEventListener("click", config.onClick);
1698
+ }
1699
+ return btn;
1700
+ },
1701
+
1702
+ // Stepper
1703
+ renderStepper,
1704
+
1705
+ // Alerts (now shows toast notifications)
1706
+ showToast,
1707
+ renderAlerts, // Legacy support for static alerts
1708
+
1709
+ // Table
1710
+ createDataTable,
1711
+ createTable,
1712
+ createSearchInput,
1713
+
1714
+ // Summary
1715
+ createSummaryRow,
1716
+
1717
+ // Badge & Loader
1718
+ createBadge,
1719
+ createLoader,
1720
+
1721
+ // Avatar
1722
+ createAvatar,
1723
+ createVividAvatar,
1724
+ createAvatarGroup,
1725
+ createVividAvatarGroup,
1726
+ };
1727
+
1728
+ console.log("[FlowUI] Module loaded successfully");
1729
+ })(typeof window !== "undefined" ? window : this);