@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
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select Component (vanilla JS, Tailwind)
|
|
3
|
+
* Design-system select; ref: Radix/cva select trigger, content, item variants.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function (global) {
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
var CHEVRON_SVG =
|
|
10
|
+
'<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>';
|
|
11
|
+
|
|
12
|
+
var X_SVG =
|
|
13
|
+
'<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>';
|
|
14
|
+
|
|
15
|
+
function triggerClasses(variant, size, disabled, placeholder, hasClear) {
|
|
16
|
+
var v = variant || "default";
|
|
17
|
+
var base =
|
|
18
|
+
"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 ";
|
|
19
|
+
var variantClasses = {
|
|
20
|
+
default:
|
|
21
|
+
"border-border-primary hover:border-primary-border focus:border-1/2 focus:border-primary-border",
|
|
22
|
+
error:
|
|
23
|
+
"border-error-border hover:border-error-border-hover focus:border-1/2 focus:border-error-border-hover",
|
|
24
|
+
warning:
|
|
25
|
+
"border-warning-border hover:border-warning-border-hover focus:border-1/2 focus:border-warning-border-hover",
|
|
26
|
+
borderless: "border-none shadow-none rounded-0 bg-fill-quarternary-fill-white",
|
|
27
|
+
inline:
|
|
28
|
+
"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",
|
|
29
|
+
};
|
|
30
|
+
var sizeClasses = {
|
|
31
|
+
default: "px-12 py-6",
|
|
32
|
+
large: "px-12 py-8",
|
|
33
|
+
small: "px-8 py-4",
|
|
34
|
+
};
|
|
35
|
+
var placeholderClass = placeholder ? " text-typography-quaternary-text" : "";
|
|
36
|
+
var disabledClass = disabled
|
|
37
|
+
? " pointer-events-none cursor-not-allowed bg-fill-tertiary-fill-light-gray text-typography-quaternary-text hover:border-border-primary"
|
|
38
|
+
: "";
|
|
39
|
+
var clearPadding = hasClear ? " pr-24" : "";
|
|
40
|
+
return (
|
|
41
|
+
base +
|
|
42
|
+
(variantClasses[v] || variantClasses.default) +
|
|
43
|
+
" " +
|
|
44
|
+
(sizeClasses[size] || sizeClasses.default) +
|
|
45
|
+
placeholderClass +
|
|
46
|
+
disabledClass +
|
|
47
|
+
clearPadding
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function join() {
|
|
52
|
+
return Array.prototype.filter.call(arguments, Boolean).join(" ");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a custom select component
|
|
57
|
+
* @param {Object} config
|
|
58
|
+
* @param {string} config.fieldId - Field ID for state management
|
|
59
|
+
* @param {Array} config.options - Array of { value, label } or { slug, display_name } objects
|
|
60
|
+
* @param {string} config.placeholder - Placeholder text
|
|
61
|
+
* @param {string} config.value - Current value
|
|
62
|
+
* @param {Function} config.onChange - Change handler
|
|
63
|
+
* @param {boolean} config.disabled - Whether select is disabled
|
|
64
|
+
* @param {string} [config.variant] - 'default' | 'error' | 'warning' | 'borderless' | 'inline'
|
|
65
|
+
* @param {string} [config.size] - 'default' | 'large' | 'small'
|
|
66
|
+
* @param {boolean} [config.canClear] - Show clear button when value is set
|
|
67
|
+
* @param {Function} [config.onClear] - Called when clear is clicked
|
|
68
|
+
* @returns {HTMLElement} Select container element
|
|
69
|
+
*/
|
|
70
|
+
function createCustomSelect(config) {
|
|
71
|
+
var fieldId = config.fieldId;
|
|
72
|
+
var options = config.options || [];
|
|
73
|
+
var placeholder = config.placeholder || "Select an option";
|
|
74
|
+
var onChange = config.onChange;
|
|
75
|
+
var variant = config.variant || "default";
|
|
76
|
+
var size = config.size || "default";
|
|
77
|
+
var canClear = !!config.canClear;
|
|
78
|
+
var onClear = config.onClear;
|
|
79
|
+
|
|
80
|
+
var disabled = config.disabled === true;
|
|
81
|
+
var value =
|
|
82
|
+
config.value !== undefined && config.value !== null ? config.value : "";
|
|
83
|
+
|
|
84
|
+
var selectedOption = options.find(function (opt) {
|
|
85
|
+
var optValue =
|
|
86
|
+
opt.value !== undefined && opt.value !== null
|
|
87
|
+
? opt.value
|
|
88
|
+
: opt.slug || opt.id;
|
|
89
|
+
return optValue === value;
|
|
90
|
+
});
|
|
91
|
+
var displayText = selectedOption
|
|
92
|
+
? selectedOption.label || selectedOption.name || selectedOption.display_name || selectedOption.value
|
|
93
|
+
: placeholder;
|
|
94
|
+
|
|
95
|
+
var container = document.createElement("div");
|
|
96
|
+
container.className = "custom-select relative w-full group";
|
|
97
|
+
container.setAttribute("data-field-id", fieldId);
|
|
98
|
+
|
|
99
|
+
var triggerWrapper = document.createElement("span");
|
|
100
|
+
triggerWrapper.className =
|
|
101
|
+
"select-trigger-wrapper relative flex w-full items-center justify-between gap-8";
|
|
102
|
+
|
|
103
|
+
var trigger = document.createElement("button");
|
|
104
|
+
trigger.type = "button";
|
|
105
|
+
trigger.className = triggerClasses(variant, size, disabled, !value, canClear && !!value && !disabled);
|
|
106
|
+
trigger.disabled = disabled;
|
|
107
|
+
trigger.setAttribute("aria-haspopup", "listbox");
|
|
108
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
109
|
+
trigger.setAttribute("aria-label", placeholder);
|
|
110
|
+
|
|
111
|
+
var triggerText = document.createElement("div");
|
|
112
|
+
triggerText.className = "truncate text-inherit";
|
|
113
|
+
triggerText.textContent = displayText;
|
|
114
|
+
trigger.appendChild(triggerText);
|
|
115
|
+
|
|
116
|
+
var chevron = document.createElement("span");
|
|
117
|
+
chevron.className =
|
|
118
|
+
"ml-4 box-content flex size-16 items-center justify-center shrink-0 transition-transform duration-200 group-[.open]:rotate-180";
|
|
119
|
+
chevron.innerHTML = CHEVRON_SVG;
|
|
120
|
+
chevron.setAttribute("aria-hidden", "true");
|
|
121
|
+
|
|
122
|
+
var showChevron = variant !== "inline" || !value;
|
|
123
|
+
if (showChevron) {
|
|
124
|
+
trigger.appendChild(chevron);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
triggerWrapper.appendChild(trigger);
|
|
128
|
+
|
|
129
|
+
// Create clear button (will be shown/hidden dynamically)
|
|
130
|
+
var clearBtn = document.createElement("button");
|
|
131
|
+
clearBtn.type = "button";
|
|
132
|
+
clearBtn.className =
|
|
133
|
+
"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";
|
|
134
|
+
clearBtn.innerHTML = X_SVG;
|
|
135
|
+
clearBtn.setAttribute("aria-label", "Clear selection");
|
|
136
|
+
clearBtn.style.display = canClear && value && !disabled ? "" : "none";
|
|
137
|
+
clearBtn.addEventListener("click", function (e) {
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
value = "";
|
|
140
|
+
var sel = options.find(function (opt) {
|
|
141
|
+
var ov =
|
|
142
|
+
opt.value !== undefined && opt.value !== null
|
|
143
|
+
? opt.value
|
|
144
|
+
: opt.slug || opt.id;
|
|
145
|
+
return ov === value;
|
|
146
|
+
});
|
|
147
|
+
triggerText.textContent = placeholder;
|
|
148
|
+
trigger.className = triggerClasses(
|
|
149
|
+
variant,
|
|
150
|
+
size,
|
|
151
|
+
disabled,
|
|
152
|
+
true,
|
|
153
|
+
false
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Update chevron visibility for inline variant
|
|
157
|
+
var shouldShowChevron = variant !== "inline" || !value;
|
|
158
|
+
if (shouldShowChevron && !chevron.parentNode) {
|
|
159
|
+
trigger.appendChild(chevron);
|
|
160
|
+
} else if (!shouldShowChevron && chevron.parentNode) {
|
|
161
|
+
chevron.parentNode.removeChild(chevron);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
updateClearButton();
|
|
165
|
+
updateOptionsSelection();
|
|
166
|
+
|
|
167
|
+
if (onClear) onClear();
|
|
168
|
+
if (onChange) onChange("");
|
|
169
|
+
});
|
|
170
|
+
triggerWrapper.appendChild(clearBtn);
|
|
171
|
+
|
|
172
|
+
// Helper to update clear button visibility
|
|
173
|
+
function updateClearButton() {
|
|
174
|
+
if (clearBtn) {
|
|
175
|
+
clearBtn.style.display = canClear && value && !disabled ? "" : "none";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
container.appendChild(triggerWrapper);
|
|
180
|
+
|
|
181
|
+
var content = document.createElement("div");
|
|
182
|
+
content.setAttribute("role", "listbox");
|
|
183
|
+
var contentBase =
|
|
184
|
+
"custom-select-content absolute left-0 right-0 z-50 max-h-[200px] min-w-[8rem] overflow-hidden rounded-4 bg-fill-quarternary-fill-white shadow-default-medium opacity-0 invisible transition-all duration-150 ease-out " +
|
|
185
|
+
"group-[.open]:opacity-100 group-[.open]:visible ";
|
|
186
|
+
content.className =
|
|
187
|
+
contentBase + "top-full mt-1 -translate-y-1 group-[.open]:translate-y-0";
|
|
188
|
+
|
|
189
|
+
var optionsList = document.createElement("div");
|
|
190
|
+
optionsList.className =
|
|
191
|
+
"overflow-y-auto max-h-[200px] p-2 w-full rounded-4 bg-fill-quarternary-fill-white";
|
|
192
|
+
|
|
193
|
+
if (options.length === 0) {
|
|
194
|
+
var noOpt = document.createElement("div");
|
|
195
|
+
noOpt.className =
|
|
196
|
+
"flex h-full min-h-[100px] w-full items-center justify-center p-4 !text-reg-13 text-typography-quaternary-text";
|
|
197
|
+
noOpt.textContent = "No options available";
|
|
198
|
+
optionsList.appendChild(noOpt);
|
|
199
|
+
} else {
|
|
200
|
+
options.forEach(function (opt) {
|
|
201
|
+
var optionValue =
|
|
202
|
+
opt.value !== undefined && opt.value !== null
|
|
203
|
+
? opt.value
|
|
204
|
+
: opt.slug || opt.id;
|
|
205
|
+
var optionLabel =
|
|
206
|
+
opt.label || opt.name || opt.display_name || opt.value;
|
|
207
|
+
var isSelected = optionValue === value;
|
|
208
|
+
|
|
209
|
+
var option = document.createElement("div");
|
|
210
|
+
option.setAttribute("role", "option");
|
|
211
|
+
option.setAttribute("data-value", optionValue);
|
|
212
|
+
option.setAttribute("aria-selected", isSelected);
|
|
213
|
+
option.className = join(
|
|
214
|
+
"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",
|
|
215
|
+
"hover:bg-fill-tertiary-fill-light-gray focus:bg-fill-tertiary-fill-light-gray",
|
|
216
|
+
isSelected
|
|
217
|
+
? "bg-primary-surface hover:!bg-primary-surface-hover"
|
|
218
|
+
: ""
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
var optionContent = document.createElement("span");
|
|
222
|
+
optionContent.className = "flex items-center gap-8 flex-1 truncate";
|
|
223
|
+
optionContent.textContent = optionLabel;
|
|
224
|
+
option.appendChild(optionContent);
|
|
225
|
+
|
|
226
|
+
option.addEventListener("click", function (e) {
|
|
227
|
+
e.stopPropagation();
|
|
228
|
+
if (disabled) return;
|
|
229
|
+
selectOption(optionValue);
|
|
230
|
+
});
|
|
231
|
+
option.addEventListener("mouseenter", function () {
|
|
232
|
+
if (disabled) return;
|
|
233
|
+
highlightOption(option);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
optionsList.appendChild(option);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
content.appendChild(optionsList);
|
|
241
|
+
container.appendChild(content);
|
|
242
|
+
|
|
243
|
+
var isOpen = false;
|
|
244
|
+
var highlightedIndex = -1;
|
|
245
|
+
|
|
246
|
+
function openDropdown() {
|
|
247
|
+
if (disabled) return;
|
|
248
|
+
document
|
|
249
|
+
.querySelectorAll(".custom-select.open, .record-select.open")
|
|
250
|
+
.forEach(function (other) {
|
|
251
|
+
if (other !== container) {
|
|
252
|
+
other.classList.remove("open");
|
|
253
|
+
var t = other.querySelector(
|
|
254
|
+
"button, .custom-select-trigger, .record-select-trigger"
|
|
255
|
+
);
|
|
256
|
+
if (t) t.setAttribute("aria-expanded", "false");
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
isOpen = true;
|
|
260
|
+
container.classList.add("open");
|
|
261
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
262
|
+
highlightOptionByValue(value);
|
|
263
|
+
updatePosition();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function closeDropdown() {
|
|
267
|
+
isOpen = false;
|
|
268
|
+
container.classList.remove("open");
|
|
269
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
270
|
+
highlightedIndex = -1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function toggleDropdown() {
|
|
274
|
+
var others = document.querySelectorAll(
|
|
275
|
+
".custom-select.open, .record-select.open"
|
|
276
|
+
);
|
|
277
|
+
var hasOther = Array.from(others).some(function (s) {
|
|
278
|
+
return s !== container;
|
|
279
|
+
});
|
|
280
|
+
if (hasOther) openDropdown();
|
|
281
|
+
else if (isOpen) closeDropdown();
|
|
282
|
+
else openDropdown();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function updateOptionsSelection() {
|
|
286
|
+
optionsList.querySelectorAll("[role=option]").forEach(function (opt) {
|
|
287
|
+
var dv = opt.getAttribute("data-value");
|
|
288
|
+
var match =
|
|
289
|
+
dv === String(value) ||
|
|
290
|
+
(dv === "false" && value === false) ||
|
|
291
|
+
(dv === "true" && value === true);
|
|
292
|
+
opt.setAttribute("aria-selected", !!match);
|
|
293
|
+
opt.classList.remove("bg-primary-surface", "hover:!bg-primary-surface-hover");
|
|
294
|
+
opt.classList.add("hover:bg-fill-tertiary-fill-light-gray");
|
|
295
|
+
if (match) {
|
|
296
|
+
opt.classList.add("bg-primary-surface", "hover:!bg-primary-surface-hover");
|
|
297
|
+
opt.classList.remove("hover:bg-fill-tertiary-fill-light-gray");
|
|
298
|
+
} else {
|
|
299
|
+
opt.classList.remove("bg-primary-surface", "hover:!bg-primary-surface-hover");
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function selectOption(optionValue) {
|
|
305
|
+
var selected = options.find(function (opt) {
|
|
306
|
+
var ov =
|
|
307
|
+
opt.value !== undefined && opt.value !== null
|
|
308
|
+
? opt.value
|
|
309
|
+
: opt.slug || opt.id;
|
|
310
|
+
return ov === optionValue;
|
|
311
|
+
});
|
|
312
|
+
if (!selected) return;
|
|
313
|
+
|
|
314
|
+
value = optionValue;
|
|
315
|
+
triggerText.textContent =
|
|
316
|
+
selected.label || selected.name || selected.display_name || selected.value;
|
|
317
|
+
trigger.className = triggerClasses(
|
|
318
|
+
variant,
|
|
319
|
+
size,
|
|
320
|
+
disabled,
|
|
321
|
+
false,
|
|
322
|
+
canClear && !!value && !disabled
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// Update chevron visibility for inline variant
|
|
326
|
+
var shouldShowChevron = variant !== "inline" || !value;
|
|
327
|
+
if (shouldShowChevron && !chevron.parentNode) {
|
|
328
|
+
trigger.appendChild(chevron);
|
|
329
|
+
} else if (!shouldShowChevron && chevron.parentNode) {
|
|
330
|
+
chevron.parentNode.removeChild(chevron);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
updateClearButton();
|
|
334
|
+
updateOptionsSelection();
|
|
335
|
+
|
|
336
|
+
closeDropdown();
|
|
337
|
+
if (onChange) onChange(optionValue);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function highlightOption(el) {
|
|
341
|
+
optionsList.querySelectorAll("[role=option]").forEach(function (o) {
|
|
342
|
+
o.classList.remove("bg-fill-tertiary-fill-light-gray");
|
|
343
|
+
});
|
|
344
|
+
if (!el.classList.contains("bg-primary-surface")) {
|
|
345
|
+
el.classList.add("bg-fill-tertiary-fill-light-gray");
|
|
346
|
+
}
|
|
347
|
+
highlightedIndex = Array.from(optionsList.children).indexOf(el);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function highlightOptionByValue(val) {
|
|
351
|
+
var opt = optionsList.querySelector('[data-value="' + val + '"]');
|
|
352
|
+
if (opt) {
|
|
353
|
+
highlightOption(opt);
|
|
354
|
+
scrollToOption(opt);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function scrollToOption(opt) {
|
|
359
|
+
if (!opt) return;
|
|
360
|
+
var cr = content.getBoundingClientRect();
|
|
361
|
+
var top = opt.offsetTop;
|
|
362
|
+
var bottom = top + opt.offsetHeight;
|
|
363
|
+
var st = content.scrollTop;
|
|
364
|
+
var sb = st + cr.height;
|
|
365
|
+
if (top < st) content.scrollTop = top - 8;
|
|
366
|
+
else if (bottom > sb)
|
|
367
|
+
content.scrollTop = bottom - cr.height + 8;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function updatePosition() {
|
|
371
|
+
var rect = trigger.getBoundingClientRect();
|
|
372
|
+
var vh = window.innerHeight;
|
|
373
|
+
var below = vh - rect.bottom;
|
|
374
|
+
var above = rect.top;
|
|
375
|
+
if (below < 200 && above > below) {
|
|
376
|
+
content.className =
|
|
377
|
+
contentBase +
|
|
378
|
+
"bottom-full mb-1 translate-y-1 group-[.open]:translate-y-0";
|
|
379
|
+
} else {
|
|
380
|
+
content.className =
|
|
381
|
+
contentBase +
|
|
382
|
+
"top-full mt-1 -translate-y-1 group-[.open]:translate-y-0";
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
trigger.addEventListener("click", function (e) {
|
|
387
|
+
e.preventDefault();
|
|
388
|
+
e.stopPropagation();
|
|
389
|
+
toggleDropdown();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
trigger.addEventListener("keydown", function (e) {
|
|
393
|
+
if (disabled) return;
|
|
394
|
+
switch (e.key) {
|
|
395
|
+
case "Enter":
|
|
396
|
+
case " ":
|
|
397
|
+
e.preventDefault();
|
|
398
|
+
toggleDropdown();
|
|
399
|
+
break;
|
|
400
|
+
case "ArrowDown":
|
|
401
|
+
e.preventDefault();
|
|
402
|
+
if (!isOpen) openDropdown();
|
|
403
|
+
else navigateOptions(1);
|
|
404
|
+
break;
|
|
405
|
+
case "ArrowUp":
|
|
406
|
+
e.preventDefault();
|
|
407
|
+
if (!isOpen) openDropdown();
|
|
408
|
+
else navigateOptions(-1);
|
|
409
|
+
break;
|
|
410
|
+
case "Escape":
|
|
411
|
+
if (isOpen) {
|
|
412
|
+
e.preventDefault();
|
|
413
|
+
closeDropdown();
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
function navigateOptions(dir) {
|
|
420
|
+
var list = Array.from(optionsList.children).filter(function (n) {
|
|
421
|
+
return n.getAttribute("role") === "option";
|
|
422
|
+
});
|
|
423
|
+
if (list.length === 0) return;
|
|
424
|
+
highlightedIndex += dir;
|
|
425
|
+
if (highlightedIndex < 0) highlightedIndex = list.length - 1;
|
|
426
|
+
else if (highlightedIndex >= list.length) highlightedIndex = 0;
|
|
427
|
+
var opt = list[highlightedIndex];
|
|
428
|
+
highlightOption(opt);
|
|
429
|
+
scrollToOption(opt);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
document.addEventListener("click", function (e) {
|
|
433
|
+
if (isOpen && !container.contains(e.target)) closeDropdown();
|
|
434
|
+
});
|
|
435
|
+
document.addEventListener("keydown", function (e) {
|
|
436
|
+
if (e.key === "Escape" && isOpen) closeDropdown();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
container.updateValue = function (newValue) {
|
|
440
|
+
value =
|
|
441
|
+
newValue !== undefined && newValue !== null ? newValue : "";
|
|
442
|
+
var sel = options.find(function (opt) {
|
|
443
|
+
var ov =
|
|
444
|
+
opt.value !== undefined && opt.value !== null
|
|
445
|
+
? opt.value
|
|
446
|
+
: opt.slug || opt.id;
|
|
447
|
+
return ov === value;
|
|
448
|
+
});
|
|
449
|
+
triggerText.textContent = sel
|
|
450
|
+
? sel.label || sel.name || sel.display_name || sel.value
|
|
451
|
+
: placeholder;
|
|
452
|
+
trigger.className = triggerClasses(
|
|
453
|
+
variant,
|
|
454
|
+
size,
|
|
455
|
+
disabled,
|
|
456
|
+
!value,
|
|
457
|
+
canClear && !!value && !disabled
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
// Update chevron visibility for inline variant
|
|
461
|
+
var shouldShowChevron = variant !== "inline" || !value;
|
|
462
|
+
if (shouldShowChevron && !chevron.parentNode) {
|
|
463
|
+
trigger.appendChild(chevron);
|
|
464
|
+
} else if (!shouldShowChevron && chevron.parentNode) {
|
|
465
|
+
chevron.parentNode.removeChild(chevron);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
updateClearButton();
|
|
469
|
+
updateOptionsSelection();
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
container.updateOptions = function (newOptions) {
|
|
473
|
+
options.length = 0;
|
|
474
|
+
options.push.apply(options, newOptions);
|
|
475
|
+
optionsList.innerHTML = "";
|
|
476
|
+
|
|
477
|
+
if (newOptions.length === 0) {
|
|
478
|
+
var noOpt = document.createElement("div");
|
|
479
|
+
noOpt.className =
|
|
480
|
+
"flex h-full min-h-[100px] w-full items-center justify-center p-4 !text-reg-13 text-typography-quaternary-text";
|
|
481
|
+
noOpt.textContent = "No options available";
|
|
482
|
+
optionsList.appendChild(noOpt);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
newOptions.forEach(function (opt) {
|
|
487
|
+
var optionValue =
|
|
488
|
+
opt.value !== undefined && opt.value !== null
|
|
489
|
+
? opt.value
|
|
490
|
+
: opt.slug || opt.id;
|
|
491
|
+
var optionLabel =
|
|
492
|
+
opt.label || opt.name || opt.display_name || opt.value;
|
|
493
|
+
var isSelected = optionValue === value;
|
|
494
|
+
|
|
495
|
+
var option = document.createElement("div");
|
|
496
|
+
option.setAttribute("role", "option");
|
|
497
|
+
option.setAttribute("data-value", optionValue);
|
|
498
|
+
option.setAttribute("aria-selected", isSelected);
|
|
499
|
+
option.className = join(
|
|
500
|
+
"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",
|
|
501
|
+
"hover:bg-fill-tertiary-fill-light-gray focus:bg-fill-tertiary-fill-light-gray",
|
|
502
|
+
isSelected
|
|
503
|
+
? "bg-primary-surface hover:!bg-primary-surface-hover"
|
|
504
|
+
: ""
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
var optionContent = document.createElement("span");
|
|
508
|
+
optionContent.className = "flex items-center gap-8 flex-1 truncate";
|
|
509
|
+
optionContent.textContent = optionLabel;
|
|
510
|
+
option.appendChild(optionContent);
|
|
511
|
+
|
|
512
|
+
option.addEventListener("click", function () {
|
|
513
|
+
if (disabled) return;
|
|
514
|
+
selectOption(optionValue);
|
|
515
|
+
});
|
|
516
|
+
option.addEventListener("mouseenter", function () {
|
|
517
|
+
if (disabled) return;
|
|
518
|
+
highlightOption(option);
|
|
519
|
+
});
|
|
520
|
+
optionsList.appendChild(option);
|
|
521
|
+
});
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
container.setDisabled = function (isDisabled) {
|
|
525
|
+
disabled = !!isDisabled;
|
|
526
|
+
trigger.disabled = disabled;
|
|
527
|
+
trigger.className = triggerClasses(
|
|
528
|
+
variant,
|
|
529
|
+
size,
|
|
530
|
+
disabled,
|
|
531
|
+
!value,
|
|
532
|
+
canClear && !!value && !disabled
|
|
533
|
+
);
|
|
534
|
+
updateClearButton();
|
|
535
|
+
if (disabled && isOpen) closeDropdown();
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
return container;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
global.Select = {
|
|
542
|
+
create: createCustomSelect,
|
|
543
|
+
};
|
|
544
|
+
})(typeof window !== "undefined" ? window : this);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spinner Component (shadcn-style)
|
|
3
|
+
* A reusable loading spinner component. Styles via Tailwind CSS.
|
|
4
|
+
*/
|
|
5
|
+
(function (global) {
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a spinner element
|
|
10
|
+
* @param {Object} config - Configuration options
|
|
11
|
+
* @param {string} config.size - Size: 'small' (16px), 'medium' (24px), 'large' (32px), or custom pixel value
|
|
12
|
+
* @param {string} config.color - Color of the spinner (default: currentColor)
|
|
13
|
+
* @param {string} config.text - Optional loading text to display next to spinner
|
|
14
|
+
* @returns {HTMLElement} Spinner container element
|
|
15
|
+
*/
|
|
16
|
+
function createSpinner(config = {}) {
|
|
17
|
+
const {
|
|
18
|
+
size = "medium",
|
|
19
|
+
color = "currentColor",
|
|
20
|
+
text = null,
|
|
21
|
+
} = config;
|
|
22
|
+
|
|
23
|
+
// Determine pixel size
|
|
24
|
+
let pixelSize;
|
|
25
|
+
switch (size) {
|
|
26
|
+
case "small": {
|
|
27
|
+
pixelSize = "16px";
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
case "medium": {
|
|
31
|
+
pixelSize = "24px";
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "large": {
|
|
35
|
+
pixelSize = "32px";
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
default: {
|
|
39
|
+
pixelSize = size;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create container (Tailwind: inline-flex items-center gap-2)
|
|
44
|
+
const container = document.createElement("div");
|
|
45
|
+
container.setAttribute("data-spinner", "container");
|
|
46
|
+
container.className = "inline-flex items-center gap-2";
|
|
47
|
+
|
|
48
|
+
// Create SVG element (Tailwind: animate-spin)
|
|
49
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
50
|
+
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
|
51
|
+
svg.setAttribute("width", pixelSize);
|
|
52
|
+
svg.setAttribute("height", pixelSize);
|
|
53
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
54
|
+
svg.setAttribute("fill", "none");
|
|
55
|
+
svg.setAttribute("stroke", color);
|
|
56
|
+
svg.setAttribute("stroke-width", "2");
|
|
57
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
58
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
59
|
+
svg.classList.add("animate-spin");
|
|
60
|
+
|
|
61
|
+
// Create path elements
|
|
62
|
+
const path1 = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
63
|
+
path1.setAttribute("stroke", "none");
|
|
64
|
+
path1.setAttribute("d", "M0 0h24v24H0z");
|
|
65
|
+
path1.setAttribute("fill", "none");
|
|
66
|
+
|
|
67
|
+
const path2 = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
68
|
+
path2.setAttribute("d", "M12 3a9 9 0 1 0 9 9");
|
|
69
|
+
|
|
70
|
+
svg.appendChild(path1);
|
|
71
|
+
svg.appendChild(path2);
|
|
72
|
+
|
|
73
|
+
container.appendChild(svg);
|
|
74
|
+
|
|
75
|
+
// Add text if provided (Tailwind: text-sm text-typography-tertiary-text)
|
|
76
|
+
if (text) {
|
|
77
|
+
const textSpan = document.createElement("span");
|
|
78
|
+
textSpan.className = "text-sm text-typography-tertiary-text";
|
|
79
|
+
textSpan.textContent = text;
|
|
80
|
+
container.appendChild(textSpan);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return container;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a centered spinner (for full-width loading states)
|
|
88
|
+
* @param {Object} config - Configuration options (same as createSpinner)
|
|
89
|
+
* @returns {HTMLElement} Centered spinner container
|
|
90
|
+
*/
|
|
91
|
+
function createCenteredSpinner(config = {}) {
|
|
92
|
+
const spinner = createSpinner(config);
|
|
93
|
+
spinner.classList.add("flex", "justify-center", "items-center", "p-5");
|
|
94
|
+
return spinner;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Show spinner in a container
|
|
99
|
+
* @param {HTMLElement} container - Container element
|
|
100
|
+
* @param {Object} config - Configuration options
|
|
101
|
+
* @returns {HTMLElement} The spinner element (for later removal)
|
|
102
|
+
*/
|
|
103
|
+
function showSpinner(container, config = {}) {
|
|
104
|
+
if (!container) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const spinner = createCenteredSpinner(config);
|
|
108
|
+
container.innerHTML = "";
|
|
109
|
+
container.appendChild(spinner);
|
|
110
|
+
return spinner;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Remove spinner from a container
|
|
115
|
+
* @param {HTMLElement} container - Container element
|
|
116
|
+
*/
|
|
117
|
+
function hideSpinner(container) {
|
|
118
|
+
if (!container) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const spinner = container.querySelector("[data-spinner=\"container\"]");
|
|
122
|
+
if (spinner) {
|
|
123
|
+
spinner.remove();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Export to global
|
|
128
|
+
global.Spinner = {
|
|
129
|
+
create: createSpinner,
|
|
130
|
+
createCentered: createCenteredSpinner,
|
|
131
|
+
show: showSpinner,
|
|
132
|
+
hide: hideSpinner,
|
|
133
|
+
};
|
|
134
|
+
// Backward compatibility
|
|
135
|
+
global.Loader = global.Spinner;
|
|
136
|
+
})(typeof window !== "undefined" ? window : this);
|