@superleapai/flow-ui 2.5.2 → 2.5.3

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);
package/core/flow.js CHANGED
@@ -591,6 +591,81 @@
591
591
  return field;
592
592
  }
593
593
 
594
+ /**
595
+ * Create a card select field (uses CardSelect component when available, else radio fallback)
596
+ * @param {Object} config - Configuration object
597
+ * @param {string} config.label - Field label
598
+ * @param {string} config.fieldId - State key for this field
599
+ * @param {Array} config.options - Array of { value, label, description?, icon?, disabled? }
600
+ * @param {boolean} [config.required] - Whether field is required
601
+ * @param {Function} [config.onChange] - Optional change handler (receives selected value)
602
+ * @param {string} [config.helpText] - Optional help text for tooltip
603
+ * @param {boolean} [config.disabled] - Whether all cards are disabled
604
+ * @param {string} [config.className] - Extra CSS class on card container
605
+ * @returns {HTMLElement} Field wrapper element
606
+ */
607
+ function createCardSelect(config) {
608
+ const { label, fieldId, options = [], required = false, onChange, helpText = null, disabled = false, className } = config;
609
+
610
+ const field = createFieldWrapper(label, required, helpText);
611
+
612
+ if (getComponent("CardSelect") && getComponent("CardSelect").create) {
613
+ const currentValue = get(fieldId);
614
+ const cardSelectEl = getComponent("CardSelect").create({
615
+ name: fieldId,
616
+ options: options.map((opt) => ({
617
+ value: opt.value,
618
+ label: opt.label || opt.value,
619
+ description: opt.description,
620
+ icon: opt.icon,
621
+ disabled: opt.disabled,
622
+ })),
623
+ value: currentValue,
624
+ disabled,
625
+ className,
626
+ onChange: (value) => {
627
+ set(fieldId, value);
628
+ if (onChange) onChange(value);
629
+ },
630
+ });
631
+ cardSelectEl._fieldId = fieldId;
632
+ field.appendChild(cardSelectEl);
633
+ return field;
634
+ }
635
+
636
+ // Fallback: native radio buttons
637
+ const radioGroup = document.createElement("div");
638
+ radioGroup.className = "card-select-fallback";
639
+
640
+ options.forEach((opt) => {
641
+ const wrapper = document.createElement("div");
642
+ wrapper.className = "card-option";
643
+
644
+ const radio = document.createElement("input");
645
+ radio.type = "radio";
646
+ radio.name = fieldId;
647
+ radio.value = opt.value;
648
+ radio.id = `${fieldId}-${opt.value}`;
649
+ radio.checked = get(fieldId) === opt.value;
650
+ radio.disabled = disabled || !!opt.disabled;
651
+ radio.addEventListener("change", () => {
652
+ set(fieldId, opt.value);
653
+ if (onChange) { onChange(opt.value); }
654
+ });
655
+
656
+ const radioLabel = document.createElement("label");
657
+ radioLabel.htmlFor = `${fieldId}-${opt.value}`;
658
+ radioLabel.textContent = opt.label || opt.value;
659
+
660
+ wrapper.appendChild(radio);
661
+ wrapper.appendChild(radioLabel);
662
+ radioGroup.appendChild(wrapper);
663
+ });
664
+
665
+ field.appendChild(radioGroup);
666
+ return field;
667
+ }
668
+
594
669
  /**
595
670
  * Create a multi-select field (uses MultiSelect component when available, else checkbox group)
596
671
  * @param {Object} config - Configuration object
@@ -1775,6 +1850,7 @@
1775
1850
  createTimePicker,
1776
1851
  createDateTimePicker,
1777
1852
  createRadioGroup,
1853
+ createCardSelect,
1778
1854
  createMultiSelect,
1779
1855
  createRecordSelect,
1780
1856
  createRecordMultiSelect,