@superleapai/flow-ui 2.4.6 → 2.5.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.
@@ -0,0 +1,283 @@
1
+ /**
2
+ * CardSelect Component (vanilla JS)
3
+ * Full-width clickable card selection with icon, title, description, and check indicator.
4
+ * Drop-in replacement / upgrade to RadioGroup when visual card UI is preferred.
5
+ */
6
+
7
+ (function (global) {
8
+ "use strict";
9
+
10
+ var COLORS = {
11
+ selectedBorder: "#175259",
12
+ unselectedBorder: "#e5e7eb",
13
+ selectedBg: "#f0f9f8",
14
+ unselectedBg: "#ffffff",
15
+ selectedShadow: "0px 0px 0px 2px #e9f7f5",
16
+ unselectedShadow: "0px 1.5px 4px -1px rgba(10,9,11,0.07)",
17
+ hoverBorder: "#9ca3af",
18
+ hoverShadow: "0px 5px 13px -5px rgba(10,9,11,0.05), 0px 2px 4px -1px rgba(10,9,11,0.02)",
19
+ iconSelectedBg: "#d0ede9",
20
+ iconUnselectedBg: "#f3f4f6",
21
+ iconSelectedColor: "#175259",
22
+ iconUnselectedColor: "#6b7280",
23
+ titleSelected: "#175259",
24
+ titleUnselected: "#111827",
25
+ descSelected: "#35b18b",
26
+ descUnselected: "#6b7280",
27
+ checkBorderSelected: "#175259",
28
+ checkBorderUnselected: "#d1d5db",
29
+ checkBgSelected: "#175259",
30
+ checkBgUnselected: "transparent",
31
+ };
32
+
33
+ var CHECK_ICON =
34
+ '<svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M2 5l2.5 2.5L8 3" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
35
+
36
+ function join() {
37
+ return Array.prototype.filter.call(arguments, Boolean).join(" ");
38
+ }
39
+
40
+ function applyCardStyles(card, isSelected) {
41
+ card.style.borderColor = isSelected ? COLORS.selectedBorder : COLORS.unselectedBorder;
42
+ card.style.background = isSelected ? COLORS.selectedBg : COLORS.unselectedBg;
43
+ card.style.boxShadow = isSelected ? COLORS.selectedShadow : COLORS.unselectedShadow;
44
+ }
45
+
46
+ function applyIconStyles(iconWrapper, isSelected) {
47
+ iconWrapper.style.background = isSelected ? COLORS.iconSelectedBg : COLORS.iconUnselectedBg;
48
+ iconWrapper.style.color = isSelected ? COLORS.iconSelectedColor : COLORS.iconUnselectedColor;
49
+ }
50
+
51
+ function applyTitleStyles(titleEl, isSelected) {
52
+ titleEl.style.color = isSelected ? COLORS.titleSelected : COLORS.titleUnselected;
53
+ }
54
+
55
+ function applyDescStyles(descEl, isSelected) {
56
+ descEl.style.color = isSelected ? COLORS.descSelected : COLORS.descUnselected;
57
+ }
58
+
59
+ function applyCheckStyles(checkEl, isSelected) {
60
+ checkEl.style.borderColor = isSelected ? COLORS.checkBorderSelected : COLORS.checkBorderUnselected;
61
+ checkEl.style.background = isSelected ? COLORS.checkBgSelected : COLORS.checkBgUnselected;
62
+ checkEl.innerHTML = isSelected ? CHECK_ICON : "";
63
+ }
64
+
65
+ /**
66
+ * Create a card select component
67
+ * @param {Object} config
68
+ * @param {string} [config.name] - name attribute for the group (used for id generation)
69
+ * @param {Array} config.options - array of { value, label, description?, icon?, disabled? }
70
+ * @param {string} [config.defaultValue] - initial selected value
71
+ * @param {string} [config.value] - controlled value (takes priority over defaultValue)
72
+ * @param {boolean} [config.disabled] - disable all cards
73
+ * @param {string} [config.className] - extra class on wrapper
74
+ * @param {Function} [config.onChange] - change handler (receives selected value)
75
+ * @returns {HTMLElement} wrapper element with getValue/setValue/setDisabled API
76
+ */
77
+ function create(config) {
78
+ var opts = config || {};
79
+ var name = opts.name || "card-select-" + Math.random().toString(36).substr(2, 9);
80
+ var options = opts.options || [];
81
+ var defaultValue = opts.defaultValue;
82
+ var selectedValue = opts.value !== undefined ? opts.value : defaultValue;
83
+ var disabled = !!opts.disabled;
84
+ var className = opts.className || "";
85
+ var onChange = opts.onChange;
86
+
87
+ // Wrapper container
88
+ var wrapper = document.createElement("div");
89
+ wrapper.setAttribute("role", "radiogroup");
90
+ wrapper.setAttribute("dir", "ltr");
91
+ wrapper.className = join("flex flex-col gap-3 w-full", className);
92
+
93
+ function updateAllCards(newValue) {
94
+ var cards = wrapper.querySelectorAll("[data-card-value]");
95
+ cards.forEach(function (card) {
96
+ var cv = card.dataset.cardValue;
97
+ var active = cv === newValue;
98
+ applyCardStyles(card, active);
99
+ card.setAttribute("aria-checked", active ? "true" : "false");
100
+ var iw = card.querySelector("[data-icon]");
101
+ var titleEl = card.querySelector("[data-title]");
102
+ var descEl = card.querySelector("[data-desc]");
103
+ var checkEl = card.querySelector("[data-check]");
104
+ if (iw) applyIconStyles(iw, active);
105
+ if (titleEl) applyTitleStyles(titleEl, active);
106
+ if (descEl) applyDescStyles(descEl, active);
107
+ if (checkEl) applyCheckStyles(checkEl, active);
108
+ });
109
+ }
110
+
111
+ options.forEach(function (option, index) {
112
+ var optionValue = option.value;
113
+ var optionLabel = option.label || option.value;
114
+ var optionDesc = option.description || "";
115
+ var optionIcon = option.icon || "";
116
+ var optionDisabled = disabled || !!option.disabled;
117
+ var isSelected = optionValue === selectedValue;
118
+
119
+ // Card element
120
+ var card = document.createElement("div");
121
+ card.dataset.cardValue = optionValue;
122
+ card.id = name + "-card-" + index;
123
+ card.setAttribute("role", "radio");
124
+ card.setAttribute("aria-checked", isSelected ? "true" : "false");
125
+ card.setAttribute("tabindex", optionDisabled ? "-1" : "0");
126
+
127
+ card.style.cssText = [
128
+ "display: flex",
129
+ "align-items: flex-start",
130
+ "gap: 16px",
131
+ "padding: 18px 20px",
132
+ "border-radius: 10px",
133
+ "border: 1.5px solid " + (isSelected ? COLORS.selectedBorder : COLORS.unselectedBorder),
134
+ "background: " + (isSelected ? COLORS.selectedBg : COLORS.unselectedBg),
135
+ "cursor: " + (optionDisabled ? "not-allowed" : "pointer"),
136
+ "transition: border-color 0.15s, background 0.15s, box-shadow 0.15s",
137
+ "box-shadow: " + (isSelected ? COLORS.selectedShadow : COLORS.unselectedShadow),
138
+ "user-select: none",
139
+ optionDisabled ? "opacity: 0.5" : "",
140
+ ].filter(Boolean).join("; ");
141
+
142
+ // Icon wrapper (only rendered when icon is provided)
143
+ if (optionIcon) {
144
+ var iconWrapper = document.createElement("div");
145
+ iconWrapper.dataset.icon = "";
146
+ iconWrapper.style.cssText = [
147
+ "flex-shrink: 0",
148
+ "width: 44px",
149
+ "height: 44px",
150
+ "border-radius: 8px",
151
+ "background: " + (isSelected ? COLORS.iconSelectedBg : COLORS.iconUnselectedBg),
152
+ "display: flex",
153
+ "align-items: center",
154
+ "justify-content: center",
155
+ "color: " + (isSelected ? COLORS.iconSelectedColor : COLORS.iconUnselectedColor),
156
+ "transition: background 0.15s, color 0.15s",
157
+ ].join("; ");
158
+ iconWrapper.innerHTML = optionIcon;
159
+ card.appendChild(iconWrapper);
160
+ }
161
+
162
+ // Text wrapper
163
+ var textWrapper = document.createElement("div");
164
+ textWrapper.style.cssText = "display: flex; flex-direction: column; gap: 4px; flex: 1;";
165
+
166
+ var titleEl = document.createElement("span");
167
+ titleEl.dataset.title = "";
168
+ titleEl.textContent = optionLabel;
169
+ titleEl.style.cssText = [
170
+ "font-size: 14px",
171
+ "font-weight: 600",
172
+ "color: " + (isSelected ? COLORS.titleSelected : COLORS.titleUnselected),
173
+ "line-height: 1.4",
174
+ "transition: color 0.15s",
175
+ ].join("; ");
176
+ textWrapper.appendChild(titleEl);
177
+
178
+ if (optionDesc) {
179
+ var descEl = document.createElement("span");
180
+ descEl.dataset.desc = "";
181
+ descEl.textContent = optionDesc;
182
+ descEl.style.cssText = [
183
+ "font-size: 12px",
184
+ "color: " + (isSelected ? COLORS.descSelected : COLORS.descUnselected),
185
+ "line-height: 1.5",
186
+ "transition: color 0.15s",
187
+ ].join("; ");
188
+ textWrapper.appendChild(descEl);
189
+ }
190
+
191
+ card.appendChild(textWrapper);
192
+
193
+ // Check indicator (radio circle in top-right)
194
+ var checkEl = document.createElement("div");
195
+ checkEl.dataset.check = "";
196
+ checkEl.style.cssText = [
197
+ "flex-shrink: 0",
198
+ "width: 18px",
199
+ "height: 18px",
200
+ "border-radius: 50%",
201
+ "border: 2px solid " + (isSelected ? COLORS.checkBorderSelected : COLORS.checkBorderUnselected),
202
+ "background: " + (isSelected ? COLORS.checkBgSelected : COLORS.checkBgUnselected),
203
+ "display: flex",
204
+ "align-items: center",
205
+ "justify-content: center",
206
+ "margin-top: 2px",
207
+ "transition: all 0.15s",
208
+ ].join("; ");
209
+ if (isSelected) {
210
+ checkEl.innerHTML = CHECK_ICON;
211
+ }
212
+ card.appendChild(checkEl);
213
+
214
+ // Hover and focus styles
215
+ if (!optionDisabled) {
216
+ card.addEventListener("mouseenter", function () {
217
+ if (card.getAttribute("aria-checked") !== "true") {
218
+ card.style.borderColor = COLORS.hoverBorder;
219
+ card.style.boxShadow = COLORS.hoverShadow;
220
+ }
221
+ });
222
+ card.addEventListener("mouseleave", function () {
223
+ if (card.getAttribute("aria-checked") !== "true") {
224
+ card.style.borderColor = COLORS.unselectedBorder;
225
+ card.style.boxShadow = COLORS.unselectedShadow;
226
+ }
227
+ });
228
+
229
+ // Click handler
230
+ card.addEventListener("click", function () {
231
+ if (optionDisabled || disabled) return;
232
+ selectedValue = optionValue;
233
+ updateAllCards(selectedValue);
234
+ if (typeof onChange === "function") {
235
+ onChange(selectedValue);
236
+ }
237
+ });
238
+
239
+ // Keyboard support
240
+ card.addEventListener("keydown", function (e) {
241
+ if (optionDisabled || disabled) return;
242
+ if (e.key === " " || e.key === "Enter") {
243
+ e.preventDefault();
244
+ card.click();
245
+ }
246
+ });
247
+ }
248
+
249
+ wrapper.appendChild(card);
250
+ });
251
+
252
+ // Public API
253
+ wrapper.getValue = function () {
254
+ return selectedValue !== undefined ? selectedValue : null;
255
+ };
256
+
257
+ wrapper.setValue = function (newValue) {
258
+ selectedValue = newValue;
259
+ updateAllCards(newValue);
260
+ };
261
+
262
+ wrapper.setDisabled = function (isDisabled) {
263
+ disabled = !!isDisabled;
264
+ wrapper.querySelectorAll("[data-card-value]").forEach(function (card) {
265
+ card.style.cursor = disabled ? "not-allowed" : "pointer";
266
+ card.style.opacity = disabled ? "0.5" : "1";
267
+ card.setAttribute("tabindex", disabled ? "-1" : "0");
268
+ });
269
+ };
270
+
271
+ return wrapper;
272
+ }
273
+
274
+ var CardSelect = {
275
+ create: create,
276
+ };
277
+
278
+ if (typeof module !== "undefined" && module.exports) {
279
+ module.exports = CardSelect;
280
+ } else {
281
+ global.CardSelect = CardSelect;
282
+ }
283
+ })(typeof window !== "undefined" ? window : this);
@@ -79,7 +79,11 @@
79
79
  const body = document.createElement("div");
80
80
  body.className =
81
81
  bodyClassName ||
82
- "text-reg-14 text-typography-secondary-text leading-5 [&_p]:mb-2 [&_p:last-child]:mb-0";
82
+ "text-reg-14 text-typography-secondary-text leading-5 [&_p]:mb-2 [&_p:last-child]:mb-0 max-h-[90vh] overflow-y-auto overflow-x-hidden min-h-0";
83
+ body.style.maxHeight = "90vh";
84
+ body.style.overflowY = "auto";
85
+ body.style.overflowX = "hidden";
86
+ body.style.minHeight = "0";
83
87
  if (panelClassName) {
84
88
  panel.className = panel.className + " " + panelClassName;
85
89
  }
@@ -104,12 +108,15 @@
104
108
  if (!modal) return;
105
109
  if (!backdropEl) {
106
110
  backdropEl = document.createElement("div");
107
- backdropEl.className = "fixed inset-0 z-[9998] bg-transparent pointer-events-auto";
111
+ backdropEl.className =
112
+ "fixed inset-0 z-40 bg-transparent pointer-events-auto";
108
113
  backdropEl.setAttribute("aria-hidden", "true");
109
114
  backdropEl.addEventListener("click", function () {
110
115
  hide();
111
116
  });
112
- backdropEl.addEventListener("wheel", onBackdropWheel, { passive: false });
117
+ backdropEl.addEventListener("wheel", onBackdropWheel, {
118
+ passive: false,
119
+ });
113
120
  }
114
121
  document.body.appendChild(backdropEl);
115
122
  wrapper.style.zIndex = "999";
@@ -129,8 +136,10 @@
129
136
  const triggerRect = triggerEl.getBoundingClientRect();
130
137
  const panelRect = panel.getBoundingClientRect();
131
138
  const gap = 8;
132
- const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
133
- const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
139
+ const viewportHeight =
140
+ window.innerHeight || document.documentElement.clientHeight;
141
+ const viewportWidth =
142
+ window.innerWidth || document.documentElement.clientWidth;
134
143
  const spaceBelow = viewportHeight - triggerRect.bottom;
135
144
  const spaceAbove = triggerRect.top;
136
145
  const spaceRight = viewportWidth - triggerRect.right;
@@ -138,13 +147,29 @@
138
147
 
139
148
  // Flip placement when there is not enough space (prefer requested side, flip only when needed)
140
149
  let effectivePlacement = placement;
141
- if (placement === "bottom" && spaceBelow < panelRect.height + gap && spaceAbove >= panelRect.height + gap) {
150
+ if (
151
+ placement === "bottom" &&
152
+ spaceBelow < panelRect.height + gap &&
153
+ spaceAbove >= panelRect.height + gap
154
+ ) {
142
155
  effectivePlacement = "top";
143
- } else if (placement === "top" && spaceAbove < panelRect.height + gap && spaceBelow >= panelRect.height + gap) {
156
+ } else if (
157
+ placement === "top" &&
158
+ spaceAbove < panelRect.height + gap &&
159
+ spaceBelow >= panelRect.height + gap
160
+ ) {
144
161
  effectivePlacement = "bottom";
145
- } else if (placement === "right" && spaceRight < panelRect.width + gap && spaceLeft >= panelRect.width + gap) {
162
+ } else if (
163
+ placement === "right" &&
164
+ spaceRight < panelRect.width + gap &&
165
+ spaceLeft >= panelRect.width + gap
166
+ ) {
146
167
  effectivePlacement = "left";
147
- } else if (placement === "left" && spaceLeft < panelRect.width + gap && spaceRight >= panelRect.width + gap) {
168
+ } else if (
169
+ placement === "left" &&
170
+ spaceLeft < panelRect.width + gap &&
171
+ spaceRight >= panelRect.width + gap
172
+ ) {
148
173
  effectivePlacement = "right";
149
174
  }
150
175
 
@@ -152,8 +177,18 @@
152
177
 
153
178
  let top = 0;
154
179
  let left = 0;
155
- const alignLeft = (align === "center" ? (triggerRect.width - panelRect.width) / 2 : align === "end" ? triggerRect.width - panelRect.width : 0);
156
- const alignTop = (align === "center" ? (triggerRect.height - panelRect.height) / 2 : align === "end" ? triggerRect.height - panelRect.height : 0);
180
+ const alignLeft =
181
+ align === "center"
182
+ ? (triggerRect.width - panelRect.width) / 2
183
+ : align === "end"
184
+ ? triggerRect.width - panelRect.width
185
+ : 0;
186
+ const alignTop =
187
+ align === "center"
188
+ ? (triggerRect.height - panelRect.height) / 2
189
+ : align === "end"
190
+ ? triggerRect.height - panelRect.height
191
+ : 0;
157
192
 
158
193
  switch (effectivePlacement) {
159
194
  case "bottom":
@@ -207,7 +242,11 @@
207
242
  applyModalOpen();
208
243
  requestAnimationFrame(function () {
209
244
  position();
210
- wrapper.classList.remove("invisible", "opacity-0", "pointer-events-none");
245
+ wrapper.classList.remove(
246
+ "invisible",
247
+ "opacity-0",
248
+ "pointer-events-none",
249
+ );
211
250
  wrapper.classList.add("visible", "opacity-100", "pointer-events-auto");
212
251
  wrapper.setAttribute("aria-hidden", "false");
213
252
  window.addEventListener("scroll", onScrollOrResize, true);
@@ -288,6 +327,8 @@
288
327
  show,
289
328
  hide,
290
329
  destroy,
330
+ /** Re-run positioning (e.g. after async content load). Call when panel size changes. */
331
+ updatePosition: position,
291
332
  setContent(newContent) {
292
333
  body.innerHTML = "";
293
334
  if (typeof newContent === "string") {
@@ -152,6 +152,7 @@
152
152
  * @param {string} [config.size] - 'default' | 'large' | 'small'
153
153
  * @param {number} [config.initialLimit] - Initial fetch limit (default 50)
154
154
  * @param {Array<string>} [config.displayFields] - Fields to display as secondary info (e.g. ["email", "phone"])
155
+ * @param {Object} [config.initialFilter] - Optional filter object to merge with search (e.g. { field: "status", operator: "exact", value: "active" } or { and: [...] })
155
156
  * @param {Object} [config.objectSchema] - Optional object type/schema; properties.icon_data { icon?, color? } used for static icon (not used for user; user shows Vivid Avatar)
156
157
  * @returns {HTMLElement} Record multiselect container element
157
158
  */
@@ -166,6 +167,7 @@
166
167
  var variant = config.variant || "default";
167
168
  var size = config.size || "default";
168
169
  var initialLimit = config.initialLimit != null ? config.initialLimit : 50;
170
+ var initialFilter = config.initialFilter || null; // Can be array, object, or function returning either
169
171
  var displayFields = config.displayFields || [];
170
172
 
171
173
  var disabled = config.disabled === true;
@@ -463,14 +465,23 @@
463
465
  try {
464
466
  if (model && typeof model.select === "function") {
465
467
  var q = model.select.apply(model, fields);
468
+ var filters = [];
469
+ var resolvedFilter = typeof initialFilter === 'function' ? initialFilter() : initialFilter;
470
+ console.log('[RecordMultiselect] initialFilter:', resolvedFilter, '| search:', search);
471
+ if (resolvedFilter) {
472
+ filters = filters.concat(Array.isArray(resolvedFilter) ? resolvedFilter : [resolvedFilter]);
473
+ }
466
474
  if (search && search.trim()) {
467
- q = q.filterBy({
475
+ filters.push({
468
476
  or: [
469
477
  { field: "name", operator: "contains", value: search.trim() },
470
478
  { field: "id", operator: "eq", value: search.trim() },
471
479
  ],
472
480
  });
473
481
  }
482
+ if (filters.length > 0) {
483
+ q = q.filterBy(filters.length === 1 ? filters[0] : { and: filters });
484
+ }
474
485
  var orderBy = ["name"];
475
486
  if (objectSlug === "account") orderBy.push("-ParentId");
476
487
  return q