@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,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);