@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.
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +451 -0
- package/components/alert.js +282 -0
- package/components/avatar.js +195 -0
- package/components/badge.js +135 -0
- package/components/button.js +201 -0
- package/components/checkbox.js +254 -0
- package/components/currency.js +227 -0
- package/components/date-time-picker/date-time-picker-utils.js +253 -0
- package/components/date-time-picker/date-time-picker.js +532 -0
- package/components/duration/duration-constants.js +46 -0
- package/components/duration/duration-utils.js +164 -0
- package/components/duration/duration.js +448 -0
- package/components/enum-multiselect.js +869 -0
- package/components/enum-select.js +831 -0
- package/components/enumeration.js +213 -0
- package/components/file-input.js +533 -0
- package/components/icon.js +200 -0
- package/components/input.js +259 -0
- package/components/label.js +111 -0
- package/components/multiselect.js +351 -0
- package/components/phone-input/phone-input.js +392 -0
- package/components/phone-input/phone-utils.js +157 -0
- package/components/popover.js +240 -0
- package/components/radio-group.js +435 -0
- package/components/record-multiselect.js +956 -0
- package/components/record-select.js +930 -0
- package/components/select.js +544 -0
- package/components/spinner.js +136 -0
- package/components/table.js +335 -0
- package/components/textarea.js +114 -0
- package/components/time-picker.js +357 -0
- package/components/toast.js +343 -0
- package/core/flow.js +1729 -0
- package/core/superleapClient.js +146 -0
- package/dist/output.css +2 -0
- package/index.d.ts +458 -0
- package/index.js +253 -0
- 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);
|