@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
@@ -0,0 +1,930 @@
1
+ /**
2
+ * Record Select Component (superleap-flow)
3
+ * Single-record select with search; same trigger/content styling as Select.
4
+ * Uses: Popover (dropdown), InputComponent (search), Spinner (loading), Avatar (vivid for user).
5
+ * Fetches records via superleapClient.getSdk().model(objectSlug).
6
+ * For objectSlug === 'user' shows Vivid Avatar; otherwise static icon from schema or object map (database fallback).
7
+ * Does not use icon from backend payload.
8
+ */
9
+
10
+ (function (global) {
11
+ "use strict";
12
+
13
+ var Popover = global.Popover;
14
+ var InputComponent = global.InputComponent;
15
+ var Spinner = global.Spinner;
16
+
17
+ /** When objectSlug === USERS, show Vivid Avatar (name-based color) instead of static icon */
18
+ var STANDARD_OBJECT_SLUGS_USERS = "user";
19
+
20
+ /** Object slug -> Icon component iconStr + color (Tabler icon names). Used when objectSchema.properties.icon_data not provided. */
21
+ var OBJECT_SLUG_TO_ICON = {
22
+ team: { iconStr: "IconUsers", color: "primary" },
23
+ role: { iconStr: "IconShield", color: "info" },
24
+ iframe: { iconStr: "IconLayout", color: "neutral" },
25
+ lead: { iconStr: "IconUser", color: "primary" },
26
+ opportunity: { iconStr: "IconCurrencyDollar", color: "success" },
27
+ call_log: { iconStr: "IconPhone", color: "info" },
28
+ communication: { iconStr: "IconMessage", color: "primary" },
29
+ history_field: { iconStr: "IconClock", color: "neutral" },
30
+ email: { iconStr: "IconMail", color: "info" },
31
+ };
32
+
33
+ var CHEVRON_SVG =
34
+ '<svg width="16" height="16" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"/></svg>';
35
+ var X_SVG =
36
+ '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" xmlns="http://www.w3.org/2000/svg"><path d="M18 6L6 18M6 6l12 12"/></svg>';
37
+
38
+ var SEARCH_ICON =
39
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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"/><path d="m21 21-4.3-4.3"/></svg>';
40
+
41
+ function triggerClasses(variant, size, disabled, placeholder, hasClear) {
42
+ var v = variant || "default";
43
+ var base =
44
+ "w-full items-center justify-between rounded-4 border-1/2 bg-fill-quarternary-fill-white text-typography-primary-text !text-reg-13 focus:outline-none flex h-full truncate hover:cursor-pointer ";
45
+ var variantClasses = {
46
+ default:
47
+ "border-border-primary hover:border-primary-border focus:border-1/2 focus:border-primary-border",
48
+ error:
49
+ "border-error-border hover:border-error-border-hover focus:border-1/2 focus:border-error-border-hover",
50
+ warning:
51
+ "border-warning-border hover:border-warning-border-hover focus:border-1/2 focus:border-warning-border-hover",
52
+ borderless: "border-none shadow-none rounded-0 bg-fill-quarternary-fill-white",
53
+ inline:
54
+ "focus:border-transparent border border-transparent shadow-none rounded-0 bg-fill-quarternary-fill-white hover:bg-fill-tertiary-fill-light-gray hover:border-transparent",
55
+ };
56
+ var sizeClasses = {
57
+ default: "px-12 py-6",
58
+ large: "px-12 py-8",
59
+ small: "px-8 py-4",
60
+ };
61
+ var placeholderClass = placeholder ? " text-typography-quaternary-text" : "";
62
+ var disabledClass = disabled
63
+ ? " pointer-events-none cursor-not-allowed bg-fill-tertiary-fill-light-gray text-typography-quaternary-text hover:border-border-primary"
64
+ : "";
65
+ var clearPadding = hasClear ? " pr-24" : "";
66
+ return (
67
+ base +
68
+ (variantClasses[v] || variantClasses.default) +
69
+ " " +
70
+ (sizeClasses[size] || sizeClasses.default) +
71
+ placeholderClass +
72
+ disabledClass +
73
+ clearPadding
74
+ );
75
+ }
76
+
77
+ function join() {
78
+ return Array.prototype.filter.call(arguments, Boolean).join(" ");
79
+ }
80
+
81
+ /** Resolve client: use FlowUI._getComponent when bundle has captured globals, else global.superleapClient */
82
+ function getClient() {
83
+ if (global.FlowUI && typeof global.FlowUI._getComponent === "function") {
84
+ var c = global.FlowUI._getComponent("superleapClient");
85
+ if (c) return c;
86
+ }
87
+ return global.superleapClient;
88
+ }
89
+
90
+ /** Resolve Avatar component for Vivid Avatar when objectSlug === user */
91
+ function getAvatar() {
92
+ if (global.FlowUI && typeof global.FlowUI._getComponent === "function") {
93
+ var c = global.FlowUI._getComponent("Avatar");
94
+ if (c) return c;
95
+ }
96
+ return global.Avatar;
97
+ }
98
+
99
+ /** Resolve Icon component for object/record icons */
100
+ function getIcon() {
101
+ if (global.FlowUI && typeof global.FlowUI._getComponent === "function") {
102
+ var c = global.FlowUI._getComponent("Icon");
103
+ if (c) return c;
104
+ }
105
+ return global.Icon;
106
+ }
107
+
108
+ /**
109
+ * Get iconStr and color for the object: from objectSchema.properties.icon_data first, else OBJECT_SLUG_TO_ICON, else IconDatabase fallback.
110
+ * @param {string} slug - objectSlug
111
+ * @param {{ properties?: { icon_data?: { icon?: string, color?: string } } } | null} objectSchema - optional schema (ObjectType)
112
+ * @returns {{ iconStr: string, color: string }}
113
+ */
114
+ function getObjectIconInfo(slug, objectSchema) {
115
+ var color = "neutral";
116
+ var iconStr = "IconDatabase";
117
+ if (objectSchema && objectSchema.properties && objectSchema.properties.icon_data) {
118
+ var iconData = objectSchema.properties.icon_data;
119
+ if (typeof iconData.color === "string" && iconData.color) color = iconData.color;
120
+ if (typeof iconData.icon === "string" && iconData.icon) iconStr = iconData.icon;
121
+ }
122
+ if (!objectSchema || !objectSchema.properties || !objectSchema.properties.icon_data || !objectSchema.properties.icon_data.icon) {
123
+ if (OBJECT_SLUG_TO_ICON[slug]) {
124
+ iconStr = OBJECT_SLUG_TO_ICON[slug].iconStr;
125
+ if (OBJECT_SLUG_TO_ICON[slug].color) color = OBJECT_SLUG_TO_ICON[slug].color;
126
+ } else {
127
+ iconStr = "IconDatabase";
128
+ color = "neutral";
129
+ }
130
+ }
131
+ return { iconStr: iconStr, color: color || "neutral" };
132
+ }
133
+
134
+ /**
135
+ * Create a record select component
136
+ * @param {Object} config
137
+ * @param {string} config.fieldId - Field ID for state management
138
+ * @param {string} config.objectSlug - Object slug (e.g. "account", "opportunity")
139
+ * @param {string} [config.placeholder] - Placeholder text
140
+ * @param {string} [config.searchPlaceholder] - Search input placeholder
141
+ * @param {string} config.value - Current value (record id)
142
+ * @param {Function} config.onChange - Change handler (value, record?) => void
143
+ * @param {boolean} [config.disabled] - Whether select is disabled
144
+ * @param {string} [config.variant] - 'default' | 'error' | 'warning' | 'borderless' | 'inline'
145
+ * @param {string} [config.size] - 'default' | 'large' | 'small'
146
+ * @param {boolean} [config.canClear] - Show clear button when value is set
147
+ * @param {number} [config.initialLimit] - Initial fetch limit (default 50)
148
+ * @param {Object} [config.objectSchema] - Optional object type/schema; properties.icon_data { svg?, color? } used for static icon (not used for user; user shows Vivid Avatar)
149
+ * @returns {HTMLElement} Record select container element
150
+ */
151
+ function createRecordSelect(config) {
152
+ var fieldId = config.fieldId;
153
+ var objectSlug = config.objectSlug;
154
+ var objectSchema = config.objectSchema || null;
155
+ var placeholder = config.placeholder || "Select a record";
156
+ var searchPlaceholder = config.searchPlaceholder || "Search " + (objectSlug || "") + "...";
157
+ var onChange = config.onChange;
158
+ var variant = config.variant || "default";
159
+ var size = config.size || "default";
160
+ var canClear = !!config.canClear;
161
+ var initialLimit = config.initialLimit != null ? config.initialLimit : 50;
162
+
163
+ var disabled = config.disabled === true;
164
+ var value =
165
+ config.value !== undefined && config.value !== null ? config.value : "";
166
+
167
+ if (!objectSlug) {
168
+ var errEl = document.createElement("div");
169
+ errEl.className = "text-reg-13 text-typography-quaternary-text";
170
+ errEl.textContent = "Record select: objectSlug is required.";
171
+ return errEl;
172
+ }
173
+
174
+ var container = document.createElement("div");
175
+ container.className = "record-select relative w-full group";
176
+ container.setAttribute("data-field-id", fieldId);
177
+ container.setAttribute("data-object-slug", objectSlug);
178
+
179
+ var selectedRecord = null;
180
+ var allRecords = [];
181
+ var filteredRecords = [];
182
+ var isOpen = false;
183
+ var searchTerm = "";
184
+ var searchDebounceTimer = null;
185
+ var usePopover = Popover && typeof Popover.create === "function";
186
+ var popover = null;
187
+ var hasMoreRecords = true;
188
+ var currentPage = 1;
189
+ var isFetchingMore = false;
190
+ var totalFetched = 0;
191
+ var contentBase = "record-select-content min-w-[8rem] ";
192
+ if (!usePopover) {
193
+ contentBase += "absolute left-0 right-0 z-50 max-h-[30vh] overflow-hidden rounded-4 bg-fill-quarternary-fill-white shadow-default-medium opacity-0 invisible transition-all duration-150 ease-out group-[.open]:opacity-100 group-[.open]:visible ";
194
+ }
195
+
196
+ // Trigger wrapper + button (same structure as Select)
197
+ var triggerWrapper = document.createElement("span");
198
+ triggerWrapper.className =
199
+ "select-trigger-wrapper relative flex w-full items-center justify-between gap-8";
200
+
201
+ var trigger = document.createElement("button");
202
+ trigger.type = "button";
203
+ trigger.className = triggerClasses(
204
+ variant,
205
+ size,
206
+ disabled,
207
+ !value,
208
+ canClear && !!value && !disabled
209
+ );
210
+ trigger.disabled = disabled;
211
+ trigger.setAttribute("aria-haspopup", "listbox");
212
+ trigger.setAttribute("aria-expanded", "false");
213
+ trigger.setAttribute("aria-label", placeholder);
214
+ trigger.classList.add("record-select-trigger");
215
+
216
+ var triggerContent = document.createElement("div");
217
+ triggerContent.className = "record-select-trigger-content flex items-center gap-8 flex-1 min-w-0";
218
+
219
+ var triggerIcon = document.createElement("span");
220
+ triggerIcon.className = "record-select-trigger-icon shrink-0 hidden";
221
+ triggerContent.appendChild(triggerIcon);
222
+
223
+ var triggerText = document.createElement("div");
224
+ triggerText.className = "truncate text-inherit record-select-value";
225
+ triggerText.textContent = placeholder;
226
+ triggerContent.appendChild(triggerText);
227
+
228
+ trigger.appendChild(triggerContent);
229
+
230
+ var chevron = document.createElement("span");
231
+ chevron.className =
232
+ "ml-4 box-content flex size-16 items-center justify-center shrink-0 transition-transform duration-200 group-[.open]:rotate-180";
233
+ chevron.innerHTML = CHEVRON_SVG;
234
+ chevron.setAttribute("aria-hidden", "true");
235
+ trigger.appendChild(chevron);
236
+
237
+ triggerWrapper.appendChild(trigger);
238
+
239
+ var clearBtn = null;
240
+ if (canClear) {
241
+ clearBtn = document.createElement("button");
242
+ clearBtn.type = "button";
243
+ clearBtn.className =
244
+ "rounded-full absolute right-12 top-1/2 -translate-y-1/2 bg-transparent p-0 text-typography-tertiary-text hover:text-typography-secondary-text focus:outline-none record-select-clear";
245
+ clearBtn.innerHTML = X_SVG;
246
+ clearBtn.setAttribute("aria-label", "Clear selection");
247
+ clearBtn.addEventListener("click", function (e) {
248
+ e.stopPropagation();
249
+ setValue("");
250
+ if (onChange) onChange("");
251
+ });
252
+ triggerWrapper.appendChild(clearBtn);
253
+ }
254
+
255
+ container.appendChild(triggerWrapper);
256
+
257
+ // Dropdown content: search + list (same content pattern as Select)
258
+ var content = document.createElement("div");
259
+ content.setAttribute("role", "listbox");
260
+ content.className = contentBase + (usePopover ? "max-h-[30vh] overflow-hidden flex flex-col" : "top-full mt-1 -translate-y-1 group-[.open]:translate-y-0");
261
+
262
+ var searchWrap = document.createElement("div");
263
+ searchWrap.className = "py-8 border-b-1/2 border-border-primary";
264
+ var searchInputEl;
265
+ var searchInputWrapper = null;
266
+ if (InputComponent && typeof InputComponent.create === "function") {
267
+ searchInputWrapper = InputComponent.create({
268
+ variant: "borderless",
269
+ inputSize: "small",
270
+ type: "text",
271
+ placeholder: searchPlaceholder,
272
+ value: "",
273
+ startIcon: SEARCH_ICON,
274
+ className: "!border-0 !p-0",
275
+ onInput: function () {
276
+ searchTerm = searchInputWrapper.getValue().trim();
277
+ if (!searchTerm) {
278
+ loadInitialAndRender();
279
+ return;
280
+ }
281
+ debouncedSearch();
282
+ },
283
+ });
284
+ var inputEl = searchInputWrapper.getInput();
285
+ if (inputEl) inputEl.setAttribute("aria-label", "Search records");
286
+ searchInputEl = inputEl;
287
+ searchWrap.appendChild(searchInputWrapper);
288
+ } else {
289
+ var fallbackWrapper = document.createElement("div");
290
+ fallbackWrapper.className = "flex items-center gap-8 px-12";
291
+
292
+ var searchIconSpan = document.createElement("span");
293
+ searchIconSpan.className = "shrink-0 text-typography-tertiary-text";
294
+ searchIconSpan.innerHTML = SEARCH_ICON;
295
+ fallbackWrapper.appendChild(searchIconSpan);
296
+
297
+ var searchInput = document.createElement("input");
298
+ searchInput.type = "text";
299
+ searchInput.className =
300
+ "w-full bg-transparent text-reg-13 text-typography-primary-text placeholder:text-typography-quaternary-text focus:outline-none border-none";
301
+ searchInput.placeholder = searchPlaceholder;
302
+ searchInput.setAttribute("aria-label", "Search records");
303
+ searchInputEl = searchInput;
304
+ fallbackWrapper.appendChild(searchInput);
305
+ searchWrap.appendChild(fallbackWrapper);
306
+ }
307
+ content.appendChild(searchWrap);
308
+
309
+ var optionsList = document.createElement("div");
310
+ optionsList.className = "overflow-y-auto max-h-[200px] p-2 w-full rounded-4 bg-fill-quarternary-fill-white record-select-options";
311
+
312
+ // Add scroll listener for infinite scroll
313
+ optionsList.addEventListener("scroll", function () {
314
+ if (isFetchingMore || !hasMoreRecords) return;
315
+ var scrollHeight = optionsList.scrollHeight;
316
+ var scrollTop = optionsList.scrollTop;
317
+ var clientHeight = optionsList.clientHeight;
318
+
319
+ // Trigger load more when scrolled to bottom (with 50px threshold)
320
+ if (scrollTop + clientHeight >= scrollHeight - 50) {
321
+ loadMoreRecords();
322
+ }
323
+ });
324
+
325
+ content.appendChild(optionsList);
326
+
327
+ if (!usePopover) {
328
+ container.appendChild(content);
329
+ }
330
+
331
+ if (usePopover) {
332
+ popover = Popover.create({
333
+ trigger: trigger,
334
+ content: content,
335
+ placement: "bottom",
336
+ align: "start",
337
+ closeOnClickOutside: true,
338
+ bodyClassName: "p-0 overflow-hidden",
339
+ panelClassName: "max-h-[30vh] overflow-hidden",
340
+ onOpen: function () {
341
+ if (disabled) {
342
+ popover.hide();
343
+ return;
344
+ }
345
+ document.querySelectorAll(".custom-select.open, .record-select.open").forEach(function (other) {
346
+ if (other !== container) {
347
+ other.classList.remove("open");
348
+ var t = other.querySelector("button, .custom-select-trigger, .record-select-trigger");
349
+ if (t) t.setAttribute("aria-expanded", "false");
350
+ }
351
+ });
352
+ isOpen = true;
353
+ container.classList.add("open");
354
+ trigger.setAttribute("aria-expanded", "true");
355
+ searchTerm = "";
356
+ if (searchInputWrapper) searchInputWrapper.setValue("");
357
+ else if (searchInputEl) searchInputEl.value = "";
358
+ content.style.minWidth = trigger.offsetWidth + "px";
359
+ loadInitialAndRender();
360
+ setTimeout(function () {
361
+ if (searchInputEl) searchInputEl.focus();
362
+ }, 0);
363
+ },
364
+ onClose: function () {
365
+ isOpen = false;
366
+ container.classList.remove("open");
367
+ trigger.setAttribute("aria-expanded", "false");
368
+ searchTerm = "";
369
+ if (searchInputWrapper) searchInputWrapper.setValue("");
370
+ else if (searchInputEl) searchInputEl.value = "";
371
+ if (searchDebounceTimer) {
372
+ clearTimeout(searchDebounceTimer);
373
+ searchDebounceTimer = null;
374
+ }
375
+ },
376
+ });
377
+ }
378
+
379
+ if (clearBtn) clearBtn.style.display = canClear && value && !disabled ? "" : "none";
380
+
381
+ function setValue(newVal) {
382
+ value = newVal !== undefined && newVal !== null ? newVal : "";
383
+ selectedRecord = null;
384
+ if (value) {
385
+ loadSelectedRecord();
386
+ } else {
387
+ updateTriggerDisplay();
388
+ }
389
+ }
390
+
391
+ function updateTriggerDisplay() {
392
+ if (selectedRecord) {
393
+ triggerText.textContent = selectedRecord.name || selectedRecord.label || value;
394
+ trigger.classList.remove("placeholder");
395
+ trigger.className = triggerClasses(
396
+ variant,
397
+ size,
398
+ disabled,
399
+ false,
400
+ canClear && !!value && !disabled
401
+ );
402
+ triggerIcon.className = "record-select-trigger-icon shrink-0 flex items-center justify-center size-20 rounded-4 overflow-hidden";
403
+ triggerIcon.innerHTML = "";
404
+ if (objectSlug === STANDARD_OBJECT_SLUGS_USERS) {
405
+ var Avatar = getAvatar();
406
+ if (Avatar && typeof Avatar.createVivid === "function") {
407
+ var vividEl = Avatar.createVivid({
408
+ name: selectedRecord.name || selectedRecord.label || "",
409
+ size: "small",
410
+ shape: "circle",
411
+ });
412
+ triggerIcon.appendChild(vividEl);
413
+ } else {
414
+ renderStaticIconPlaceholder(selectedRecord.name || selectedRecord.label);
415
+ }
416
+ } else {
417
+ renderStaticObjectIcon();
418
+ }
419
+ } else {
420
+ triggerText.textContent = placeholder;
421
+ trigger.classList.add("placeholder");
422
+ trigger.className = triggerClasses(
423
+ variant,
424
+ size,
425
+ disabled,
426
+ true,
427
+ canClear && !!value && !disabled
428
+ );
429
+ triggerIcon.className = "record-select-trigger-icon shrink-0 hidden";
430
+ triggerIcon.innerHTML = "";
431
+ }
432
+ if (clearBtn) clearBtn.style.display = canClear && value && !disabled ? "" : "none";
433
+ }
434
+
435
+ function renderStaticObjectIcon() {
436
+ var IconComponent = getIcon();
437
+ if (!IconComponent || typeof IconComponent.create !== "function") return;
438
+ var info = getObjectIconInfo(objectSlug, objectSchema);
439
+ var el = IconComponent.create({
440
+ iconStr: info.iconStr,
441
+ color: info.color,
442
+ fallbackIconStr: "IconDatabase",
443
+ defaultIcon: true,
444
+ className: "size-20 shrink-0",
445
+ });
446
+ if (el) triggerIcon.appendChild(el);
447
+ }
448
+
449
+ function renderStaticIconPlaceholder(label) {
450
+ var initial = (label && String(label).charAt(0).toUpperCase()) || "?";
451
+ var div = document.createElement("div");
452
+ div.className =
453
+ "size-20 rounded-full bg-primary-surface text-primary-text flex items-center justify-center text-reg-12 font-semibold";
454
+ div.textContent = initial;
455
+ triggerIcon.appendChild(div);
456
+ }
457
+
458
+ function showTriggerPlaceholder(record) {
459
+ if (objectSlug === STANDARD_OBJECT_SLUGS_USERS) {
460
+ var Avatar = getAvatar();
461
+ if (Avatar && typeof Avatar.createVivid === "function") {
462
+ var label = record && (record.name || record.label);
463
+ var vividEl = Avatar.createVivid({
464
+ name: label || "?",
465
+ size: "small",
466
+ shape: "circle",
467
+ });
468
+ triggerIcon.appendChild(vividEl);
469
+ } else {
470
+ renderStaticIconPlaceholder(record && (record.name || record.label));
471
+ }
472
+ } else {
473
+ renderStaticObjectIcon();
474
+ }
475
+ }
476
+
477
+ function fetchRecords(search, limit, page) {
478
+ var client = getClient();
479
+ if (!client || typeof client.getSdk !== "function") {
480
+ return Promise.resolve({ records: [], hasMore: false });
481
+ }
482
+ var sdk = client.getSdk();
483
+ if (!sdk) return Promise.resolve({ records: [], hasMore: false });
484
+ var model = sdk.model(objectSlug);
485
+ var fields = ["id", "name"];
486
+ var actualLimit = limit || initialLimit;
487
+ var offset = page ? (page - 1) * actualLimit : 0;
488
+
489
+ try {
490
+ if (model && typeof model.select === "function") {
491
+ var q = model.select.apply(model, fields);
492
+ if (search && search.trim()) {
493
+ q = q.filterBy({
494
+ or: [
495
+ { field: "name", operator: "contains", value: search.trim() },
496
+ { field: "id", operator: "eq", value: search.trim() },
497
+ ],
498
+ });
499
+ }
500
+ var orderBy = ["name"];
501
+ if (objectSlug === "account") orderBy.push("-ParentId");
502
+ return q
503
+ .limit(actualLimit)
504
+ .offset(offset)
505
+ .orderBy.apply(q, orderBy)
506
+ .cache({ ttl: 2 * 60 * 1000 })
507
+ .then(function (records) {
508
+ var mappedRecords = records.map(function (r) {
509
+ var d = r.toJSON ? r.toJSON() : r;
510
+ return {
511
+ id: d.id,
512
+ value: d.id,
513
+ name: d.name || d.id,
514
+ label: d.name || d.id,
515
+ };
516
+ });
517
+ // If we got fewer records than the limit, there are no more
518
+ var hasMore = mappedRecords.length >= actualLimit;
519
+ return { records: mappedRecords, hasMore: hasMore };
520
+ });
521
+ }
522
+ } catch (e) {
523
+ console.error("[RecordSelect] fetchRecords error:", e);
524
+ }
525
+ return Promise.resolve({ records: [], hasMore: false });
526
+ }
527
+
528
+ function loadSelectedRecord() {
529
+ if (!value) {
530
+ updateTriggerDisplay();
531
+ return;
532
+ }
533
+ var client = getClient();
534
+ if (!client || typeof client.getSdk !== "function") {
535
+ updateTriggerDisplay();
536
+ return;
537
+ }
538
+ var sdk = client.getSdk();
539
+ if (!sdk) {
540
+ updateTriggerDisplay();
541
+ return;
542
+ }
543
+ var model = sdk.model(objectSlug);
544
+ model
545
+ .select("id", "name")
546
+ .where({ id: value })
547
+ .limit(1)
548
+ .first()
549
+ .then(function (record) {
550
+ if (record) {
551
+ var d = record.toJSON ? record.toJSON() : record;
552
+ selectedRecord = {
553
+ id: d.id,
554
+ value: d.id,
555
+ name: d.name || d.id,
556
+ label: d.name || d.id,
557
+ };
558
+ }
559
+ updateTriggerDisplay();
560
+ })
561
+ .catch(function () {
562
+ selectedRecord = { id: value, value: value, name: value, label: value };
563
+ updateTriggerDisplay();
564
+ });
565
+ }
566
+
567
+ function showLoading() {
568
+ optionsList.innerHTML = "";
569
+ var loadWrap = document.createElement("div");
570
+ loadWrap.className =
571
+ "flex flex-row items-center justify-center gap-8 py-12 px-12 w-full text-reg-12 text-typography-quaternary-text record-select-loading";
572
+ if (Spinner && typeof Spinner.create === "function") {
573
+ loadWrap.appendChild(Spinner.create({ size: "small", text: "Loading..." }));
574
+ } else {
575
+ var loadText = document.createElement("span");
576
+ loadText.textContent = "Loading...";
577
+ loadWrap.appendChild(loadText);
578
+ }
579
+ optionsList.appendChild(loadWrap);
580
+ }
581
+
582
+ function showLoadingMore() {
583
+ // Remove existing loading more indicator
584
+ var existing = optionsList.querySelector(".record-select-loading-more");
585
+ if (existing) existing.remove();
586
+
587
+ var loadWrap = document.createElement("div");
588
+ loadWrap.className =
589
+ "flex flex-row items-center justify-center gap-8 py-8 px-12 w-full text-reg-12 text-typography-quaternary-text record-select-loading-more";
590
+ if (Spinner && typeof Spinner.create === "function") {
591
+ loadWrap.appendChild(Spinner.create({ size: "small" }));
592
+ } else {
593
+ var loadText = document.createElement("span");
594
+ loadText.textContent = "Loading more...";
595
+ loadWrap.appendChild(loadText);
596
+ }
597
+ optionsList.appendChild(loadWrap);
598
+ }
599
+
600
+ function removeLoadingMore() {
601
+ var loadingMore = optionsList.querySelector(".record-select-loading-more");
602
+ if (loadingMore) loadingMore.remove();
603
+ }
604
+
605
+ function showEmpty(message) {
606
+ optionsList.innerHTML = "";
607
+ var empty = document.createElement("div");
608
+ empty.className =
609
+ "w-full justify-center px-12 py-6 text-center text-reg-12 text-typography-quaternary-text record-select-empty";
610
+ empty.textContent = message || "No records found";
611
+ optionsList.appendChild(empty);
612
+ }
613
+
614
+ function renderOptions() {
615
+ // Clear only the option items, keep loading indicator if present
616
+ var existingOptions = optionsList.querySelectorAll("[role=option]");
617
+ existingOptions.forEach(function (opt) {
618
+ opt.remove();
619
+ });
620
+
621
+ // Remove old loading/empty states
622
+ var oldStates = optionsList.querySelectorAll(".record-select-loading, .record-select-empty");
623
+ oldStates.forEach(function (el) {
624
+ el.remove();
625
+ });
626
+
627
+ filteredRecords.forEach(function (rec) {
628
+ var optionValue = rec.id || rec.value;
629
+ var optionLabel = rec.name || rec.label || rec.value;
630
+ var isSelected = optionValue === value;
631
+
632
+ var option = document.createElement("div");
633
+ option.setAttribute("role", "option");
634
+ option.setAttribute("data-value", optionValue);
635
+ option.setAttribute("aria-selected", isSelected);
636
+ option.className = join(
637
+ "relative flex w-full cursor-pointer select-none items-center gap-8 rounded-2 px-12 py-6 text-reg-13 outline-none first:rounded-t-4 last:rounded-b-4",
638
+ "hover:bg-fill-tertiary-fill-light-gray focus:bg-fill-tertiary-fill-light-gray",
639
+ isSelected
640
+ ? "bg-primary-surface hover:!bg-primary-surface-hover"
641
+ : ""
642
+ );
643
+
644
+ var optContent = document.createElement("span");
645
+ optContent.className = "flex items-center gap-8 flex-1 truncate";
646
+ if (objectSlug === STANDARD_OBJECT_SLUGS_USERS) {
647
+ var Avatar = getAvatar();
648
+ if (Avatar && typeof Avatar.createVivid === "function") {
649
+ var avatarEl = Avatar.createVivid({
650
+ name: optionLabel || "",
651
+ size: "small",
652
+ shape: "circle",
653
+ });
654
+ optContent.appendChild(avatarEl);
655
+ } else {
656
+ var pl = document.createElement("span");
657
+ pl.className =
658
+ "size-20 rounded-full bg-primary-surface text-primary-text flex items-center justify-center text-reg-12 font-semibold shrink-0";
659
+ pl.textContent = (optionLabel || "?").charAt(0).toUpperCase();
660
+ optContent.appendChild(pl);
661
+ }
662
+ } else {
663
+ var IconComponent = getIcon();
664
+ if (IconComponent && typeof IconComponent.create === "function") {
665
+ var iconInfo = getObjectIconInfo(objectSlug, objectSchema);
666
+ var iconEl = IconComponent.create({
667
+ iconStr: iconInfo.iconStr,
668
+ color: iconInfo.color,
669
+ fallbackIconStr: "IconDatabase",
670
+ defaultIcon: true,
671
+ className: "size-20 shrink-0",
672
+ });
673
+ if (iconEl) optContent.appendChild(iconEl);
674
+ } else {
675
+ var pl = document.createElement("span");
676
+ pl.className =
677
+ "size-20 rounded-4 flex items-center justify-center shrink-0 bg-neutral-surface-hover text-neutral-text-base";
678
+ pl.textContent = "?";
679
+ optContent.appendChild(pl);
680
+ }
681
+ }
682
+ var labelSpan = document.createElement("span");
683
+ labelSpan.textContent = optionLabel;
684
+ optContent.appendChild(labelSpan);
685
+ option.appendChild(optContent);
686
+
687
+ option.addEventListener("click", function (e) {
688
+ e.stopPropagation();
689
+ if (disabled) return;
690
+ value = optionValue;
691
+ selectedRecord = rec;
692
+ updateTriggerDisplay();
693
+ closeDropdown();
694
+ if (onChange) onChange(value, rec);
695
+ });
696
+
697
+ optionsList.appendChild(option);
698
+ });
699
+
700
+ // Add loading more indicator at the bottom if fetching
701
+ if (isFetchingMore) {
702
+ showLoadingMore();
703
+ }
704
+ }
705
+
706
+ function loadInitialAndRender() {
707
+ showLoading();
708
+ currentPage = 1;
709
+ hasMoreRecords = true;
710
+ totalFetched = 0;
711
+
712
+ fetchRecords(searchTerm, initialLimit, 1).then(function (result) {
713
+ allRecords = result.records;
714
+ filteredRecords = result.records;
715
+ hasMoreRecords = result.hasMore;
716
+ totalFetched = result.records.length;
717
+ currentPage = 1;
718
+
719
+ if (value && !result.records.some(function (r) { return (r.id || r.value) === value; })) {
720
+ loadSelectedRecord();
721
+ } else if (value && result.records.length) {
722
+ var sel = result.records.find(function (r) { return (r.id || r.value) === value; });
723
+ if (sel) selectedRecord = sel;
724
+ updateTriggerDisplay();
725
+ }
726
+ if (filteredRecords.length === 0) {
727
+ showEmpty(searchTerm ? "No results found" : "No records available");
728
+ } else {
729
+ renderOptions();
730
+ }
731
+ }).catch(function () {
732
+ showEmpty("Failed to load records");
733
+ hasMoreRecords = false;
734
+ });
735
+ }
736
+
737
+ function loadMoreRecords() {
738
+ if (isFetchingMore || !hasMoreRecords) return;
739
+
740
+ isFetchingMore = true;
741
+ currentPage += 1;
742
+ showLoadingMore();
743
+
744
+ fetchRecords(searchTerm, initialLimit, currentPage).then(function (result) {
745
+ isFetchingMore = false;
746
+ removeLoadingMore();
747
+
748
+ if (result.records.length > 0) {
749
+ allRecords = allRecords.concat(result.records);
750
+ filteredRecords = filteredRecords.concat(result.records);
751
+ totalFetched += result.records.length;
752
+ hasMoreRecords = result.hasMore;
753
+ renderOptions();
754
+ } else {
755
+ hasMoreRecords = false;
756
+ }
757
+ }).catch(function (err) {
758
+ console.error("[RecordSelect] loadMoreRecords error:", err);
759
+ isFetchingMore = false;
760
+ removeLoadingMore();
761
+ hasMoreRecords = false;
762
+ });
763
+ }
764
+
765
+ function debouncedSearch() {
766
+ if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
767
+ searchDebounceTimer = setTimeout(function () {
768
+ searchDebounceTimer = null;
769
+ showLoading();
770
+ currentPage = 1;
771
+ hasMoreRecords = true;
772
+ totalFetched = 0;
773
+
774
+ fetchRecords(searchTerm, initialLimit, 1).then(function (result) {
775
+ allRecords = result.records;
776
+ filteredRecords = result.records;
777
+ hasMoreRecords = result.hasMore;
778
+ totalFetched = result.records.length;
779
+
780
+ if (result.records.length === 0) {
781
+ showEmpty("No results found");
782
+ } else {
783
+ renderOptions();
784
+ }
785
+ }).catch(function () {
786
+ showEmpty("Search failed");
787
+ hasMoreRecords = false;
788
+ });
789
+ }, 500);
790
+ }
791
+
792
+ function openDropdown() {
793
+ if (disabled) return;
794
+ if (usePopover && popover) {
795
+ popover.show();
796
+ return;
797
+ }
798
+ document
799
+ .querySelectorAll(".custom-select.open, .record-select.open")
800
+ .forEach(function (other) {
801
+ if (other !== container) {
802
+ other.classList.remove("open");
803
+ var t = other.querySelector(
804
+ "button, .custom-select-trigger, .record-select-trigger"
805
+ );
806
+ if (t) t.setAttribute("aria-expanded", "false");
807
+ }
808
+ });
809
+ isOpen = true;
810
+ container.classList.add("open");
811
+ trigger.setAttribute("aria-expanded", "true");
812
+ searchTerm = "";
813
+ if (searchInputWrapper) searchInputWrapper.setValue("");
814
+ else if (searchInputEl) searchInputEl.value = "";
815
+ loadInitialAndRender();
816
+ setTimeout(function () {
817
+ if (searchInputEl) searchInputEl.focus();
818
+ }, 0);
819
+ updatePosition();
820
+ }
821
+
822
+ function closeDropdown() {
823
+ if (usePopover && popover) {
824
+ popover.hide();
825
+ return;
826
+ }
827
+ isOpen = false;
828
+ container.classList.remove("open");
829
+ trigger.setAttribute("aria-expanded", "false");
830
+ searchTerm = "";
831
+ if (searchInputWrapper) searchInputWrapper.setValue("");
832
+ else if (searchInputEl) searchInputEl.value = "";
833
+ if (searchDebounceTimer) {
834
+ clearTimeout(searchDebounceTimer);
835
+ searchDebounceTimer = null;
836
+ }
837
+ }
838
+
839
+ function updatePosition() {
840
+ if (usePopover) return;
841
+ var rect = trigger.getBoundingClientRect();
842
+ var vh = window.innerHeight;
843
+ var below = vh - rect.bottom;
844
+ var above = rect.top;
845
+ if (below < 200 && above > below) {
846
+ content.className = contentBase + "bottom-full mb-1 translate-y-1 group-[.open]:translate-y-0";
847
+ } else {
848
+ content.className = contentBase + "top-full mt-1 -translate-y-1 group-[.open]:translate-y-0";
849
+ }
850
+ }
851
+
852
+ if (!searchInputWrapper && searchInputEl) {
853
+ searchInputEl.addEventListener("input", function () {
854
+ searchTerm = this.value.trim();
855
+ if (!searchTerm) {
856
+ loadInitialAndRender();
857
+ return;
858
+ }
859
+ debouncedSearch();
860
+ });
861
+ }
862
+ if (searchInputEl) {
863
+ searchInputEl.addEventListener("keydown", function (e) {
864
+ e.stopPropagation();
865
+ });
866
+ }
867
+
868
+ if (usePopover && popover) {
869
+ trigger.addEventListener("keydown", function (e) {
870
+ if (disabled) return;
871
+ if (e.key === "Enter" || e.key === " ") {
872
+ e.preventDefault();
873
+ if (isOpen) popover.hide();
874
+ else popover.show();
875
+ }
876
+ });
877
+ } else {
878
+ trigger.addEventListener("click", function (e) {
879
+ e.preventDefault();
880
+ e.stopPropagation();
881
+ if (isOpen) closeDropdown();
882
+ else openDropdown();
883
+ });
884
+ trigger.addEventListener("keydown", function (e) {
885
+ if (disabled) return;
886
+ if (e.key === "Enter" || e.key === " ") {
887
+ e.preventDefault();
888
+ if (isOpen) closeDropdown();
889
+ else openDropdown();
890
+ }
891
+ if (e.key === "Escape" && isOpen) {
892
+ e.preventDefault();
893
+ closeDropdown();
894
+ }
895
+ });
896
+ document.addEventListener("click", function (e) {
897
+ if (isOpen && !container.contains(e.target)) closeDropdown();
898
+ });
899
+ document.addEventListener("keydown", function (e) {
900
+ if (e.key === "Escape" && isOpen) closeDropdown();
901
+ });
902
+ }
903
+
904
+ container.updateValue = function (newVal) {
905
+ setValue(newVal);
906
+ };
907
+
908
+ container.setDisabled = function (isDisabled) {
909
+ disabled = !!isDisabled;
910
+ trigger.disabled = disabled;
911
+ trigger.className = triggerClasses(
912
+ variant,
913
+ size,
914
+ disabled,
915
+ !value,
916
+ canClear && !!value && !disabled
917
+ );
918
+ if (disabled && isOpen) closeDropdown();
919
+ };
920
+
921
+ if (value) loadSelectedRecord();
922
+ else updateTriggerDisplay();
923
+
924
+ return container;
925
+ }
926
+
927
+ global.RecordSelect = {
928
+ create: createRecordSelect,
929
+ };
930
+ })(typeof window !== "undefined" ? window : this);