@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,240 @@
1
+ /**
2
+ * Popover Component (shadcn-style)
3
+ * Floating panel anchored to a trigger element.
4
+ */
5
+
6
+ (function (global) {
7
+ "use strict";
8
+
9
+ /**
10
+ * Create a popover
11
+ * @param {Object} config - Configuration object
12
+ * @param {HTMLElement|string} config.trigger - Trigger element or CSS selector
13
+ * @param {HTMLElement|string} config.content - Popover content (element or HTML string)
14
+ * @param {string} [config.placement='bottom'] - 'top' | 'bottom' | 'left' | 'right'
15
+ * @param {string} [config.align='start'] - 'start' | 'center' | 'end' - align panel to trigger
16
+ * @param {string} [config.title] - Optional title text
17
+ * @param {boolean} [config.closeOnClickOutside=true] - Close when clicking outside
18
+ * @param {Function} [config.onClose] - Called when popover closes
19
+ * @param {Function} [config.onOpen] - Called when popover opens (before positioning)
20
+ * @param {string} [config.bodyClassName] - Optional class for body wrapper (overrides default padding)
21
+ * @param {string} [config.panelClassName] - Optional class to add to panel (e.g. for width)
22
+ * @returns {Object} Popover API {show, hide, destroy, element}
23
+ */
24
+ function create(config = {}) {
25
+ const {
26
+ trigger,
27
+ content = "",
28
+ placement = "bottom",
29
+ align = "start",
30
+ title = "",
31
+ closeOnClickOutside = true,
32
+ onClose = null,
33
+ onOpen = null,
34
+ bodyClassName = "",
35
+ panelClassName = "",
36
+ } = config;
37
+
38
+ const triggerEl =
39
+ typeof trigger === "string" ? document.querySelector(trigger) : trigger;
40
+ if (!triggerEl) {
41
+ console.warn("[Popover] Trigger element not found");
42
+ return { show: noop, hide: noop, destroy: noop, element: null };
43
+ }
44
+
45
+ const wrapper = document.createElement("div");
46
+ wrapper.className =
47
+ "fixed z-50 pointer-events-none opacity-0 invisible transition-opacity duration-150 ease-out";
48
+ wrapper.setAttribute("aria-hidden", "true");
49
+
50
+ const panel = document.createElement("div");
51
+ panel.className =
52
+ "relative z-50 rounded-4 bg-fill-quarternary-fill-white text-typography-primary-text shadow-default-medium outline-none border-1/2 border-border-primary min-w-0 pointer-events-auto " +
53
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 " +
54
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2";
55
+ panel.setAttribute("role", "dialog");
56
+ panel.setAttribute("data-side", placement);
57
+ panel.setAttribute("data-state", "closed");
58
+ panel.setAttribute("tabindex", "-1");
59
+
60
+ if (title) {
61
+ const titleEl = document.createElement("div");
62
+ titleEl.className =
63
+ "text-semi-14 text-typography-primary-text border-b-1/2 border-border-primary";
64
+ titleEl.textContent = title;
65
+ panel.appendChild(titleEl);
66
+ }
67
+
68
+ const body = document.createElement("div");
69
+ body.className =
70
+ bodyClassName ||
71
+ "text-reg-14 text-typography-secondary-text leading-5 [&_p]:mb-2 [&_p:last-child]:mb-0";
72
+ if (panelClassName) {
73
+ panel.className = panel.className + " " + panelClassName;
74
+ }
75
+ if (typeof content === "string") {
76
+ body.innerHTML = content;
77
+ } else if (content instanceof HTMLElement) {
78
+ body.appendChild(content);
79
+ }
80
+ panel.appendChild(body);
81
+
82
+ wrapper.appendChild(panel);
83
+
84
+ function noop() {}
85
+
86
+ function position() {
87
+ const rect = triggerEl.getBoundingClientRect();
88
+ const panelRect = panel.getBoundingClientRect();
89
+ const gap = 8;
90
+ let top = 0;
91
+ let left = 0;
92
+
93
+ // Alignment offset: start = 0, center = half diff, end = full diff
94
+ const alignLeft = (align === "center" ? (rect.width - panelRect.width) / 2 : align === "end" ? rect.width - panelRect.width : 0);
95
+ const alignTop = (align === "center" ? (rect.height - panelRect.height) / 2 : align === "end" ? rect.height - panelRect.height : 0);
96
+
97
+ switch (placement) {
98
+ case "bottom":
99
+ top = rect.bottom + gap;
100
+ left = rect.left + alignLeft;
101
+ break;
102
+ case "top":
103
+ top = rect.top - panelRect.height - gap;
104
+ left = rect.left + alignLeft;
105
+ break;
106
+ case "right":
107
+ top = rect.top + alignTop;
108
+ left = rect.right + gap;
109
+ break;
110
+ case "left":
111
+ top = rect.top + alignTop;
112
+ left = rect.left - panelRect.width - gap;
113
+ break;
114
+ default:
115
+ top = rect.bottom + gap;
116
+ left = rect.left + alignLeft;
117
+ }
118
+
119
+ // Keep within viewport
120
+ const padding = 8;
121
+ if (left < padding) left = padding;
122
+ if (left + panelRect.width > window.innerWidth - padding) {
123
+ left = window.innerWidth - panelRect.width - padding;
124
+ }
125
+ if (top < padding) top = padding;
126
+ if (top + panelRect.height > window.innerHeight - padding) {
127
+ top = window.innerHeight - panelRect.height - padding;
128
+ }
129
+
130
+ wrapper.style.left = "0";
131
+ wrapper.style.top = "0";
132
+ wrapper.style.transform = "translate(" + left + "px, " + top + "px)";
133
+ // Force reflow so transform is applied before we show (avoids flash from left/top-left)
134
+ wrapper.offsetHeight;
135
+ }
136
+
137
+ function hide() {
138
+ panel.setAttribute("data-state", "closed");
139
+ wrapper.classList.add("invisible", "opacity-0", "pointer-events-none");
140
+ wrapper.classList.remove("visible", "opacity-100", "pointer-events-auto");
141
+ wrapper.setAttribute("aria-hidden", "true");
142
+ if (onClose) onClose();
143
+ }
144
+
145
+ function show() {
146
+ if (onOpen) onOpen();
147
+ var justAppended = !wrapper.parentNode;
148
+ if (justAppended) {
149
+ document.body.appendChild(wrapper);
150
+ }
151
+ // On first open, wait for layout so getBoundingClientRect() is correct (avoids wrong position / "from left" look)
152
+ if (justAppended) {
153
+ requestAnimationFrame(function () {
154
+ position();
155
+ wrapper.classList.remove("invisible", "opacity-0", "pointer-events-none");
156
+ wrapper.classList.add("visible", "opacity-100", "pointer-events-auto");
157
+ wrapper.setAttribute("aria-hidden", "false");
158
+ requestAnimationFrame(function () {
159
+ requestAnimationFrame(function () {
160
+ panel.setAttribute("data-state", "open");
161
+ });
162
+ });
163
+ });
164
+ } else {
165
+ position();
166
+ wrapper.classList.remove("invisible", "opacity-0", "pointer-events-none");
167
+ wrapper.classList.add("visible", "opacity-100", "pointer-events-auto");
168
+ wrapper.setAttribute("aria-hidden", "false");
169
+ requestAnimationFrame(function () {
170
+ requestAnimationFrame(function () {
171
+ panel.setAttribute("data-state", "open");
172
+ });
173
+ });
174
+ }
175
+ }
176
+
177
+ function destroy() {
178
+ hide();
179
+ if (wrapper.parentNode) {
180
+ wrapper.parentNode.removeChild(wrapper);
181
+ }
182
+ if (closeOnClickOutside) {
183
+ document.removeEventListener("click", outsideClick);
184
+ }
185
+ triggerEl.removeEventListener("click", toggleClick);
186
+ document.removeEventListener("keydown", handleKeyDown);
187
+ }
188
+
189
+ function toggleClick(e) {
190
+ e.stopPropagation();
191
+ if (wrapper.classList.contains("visible")) {
192
+ hide();
193
+ } else {
194
+ show();
195
+ }
196
+ }
197
+
198
+ function outsideClick(e) {
199
+ if (
200
+ wrapper.classList.contains("visible") &&
201
+ !wrapper.contains(e.target) &&
202
+ !triggerEl.contains(e.target)
203
+ ) {
204
+ hide();
205
+ }
206
+ }
207
+
208
+ function handleKeyDown(e) {
209
+ if (e.key === "Escape" && wrapper.classList.contains("visible")) {
210
+ hide();
211
+ }
212
+ }
213
+
214
+ triggerEl.addEventListener("click", toggleClick);
215
+ if (closeOnClickOutside) {
216
+ document.addEventListener("click", outsideClick);
217
+ }
218
+ document.addEventListener("keydown", handleKeyDown);
219
+
220
+ return {
221
+ show,
222
+ hide,
223
+ destroy,
224
+ setContent(newContent) {
225
+ body.innerHTML = "";
226
+ if (typeof newContent === "string") {
227
+ body.innerHTML = newContent;
228
+ } else if (newContent instanceof HTMLElement) {
229
+ body.appendChild(newContent);
230
+ }
231
+ },
232
+ element: wrapper,
233
+ panel,
234
+ };
235
+ }
236
+
237
+ global.Popover = {
238
+ create,
239
+ };
240
+ })(typeof window !== "undefined" ? window : this);
@@ -0,0 +1,435 @@
1
+ /**
2
+ * RadioGroup Component (vanilla JS)
3
+ * Design-system radio group with variants, sizes, and label support.
4
+ * Ref: React RadioGroup with Radix UI primitives; no React/Radix dependency.
5
+ */
6
+
7
+ (function (global) {
8
+ "use strict";
9
+
10
+ // Inline SVG for circle icon (checked state indicator)
11
+ var CIRCLE_ICON =
12
+ '<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 24 24" fill="white" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0"></path></svg>';
13
+
14
+ var RADIO_GROUP_BASE_CLASS = "flex content-center gap-6";
15
+
16
+ var RADIO_ITEM_BASE_CLASS =
17
+ "size-16 rounded-128 border focus:ring-0 transition-all cursor-pointer";
18
+
19
+ var RADIO_ITEM_UNCHECKED_CLASS =
20
+ "bg-fill-quarternary-fill-white border-typography-quaternary-text hover:border-primary-base hover:shadow-primary-focused";
21
+
22
+ var RADIO_ITEM_CHECKED_CLASS =
23
+ "bg-primary-base border-primary-base hover:border-primary-base hover:shadow-primary-focused";
24
+
25
+ var RADIO_ITEM_DISABLED_CLASS =
26
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:shadow-none";
27
+
28
+ var RADIO_INDICATOR_CLASS =
29
+ "size-8 rounded-128 bg-fill-quarternary-fill-white text-fill-quarternary-fill-white data-[state=checked]:text-typography-quaternary-text group-disabled:text-typography-quaternary-text data-[state=checked]:group-disabled:text-typography-quaternary-text";
30
+
31
+ var LABEL_BASE_CLASS =
32
+ "cursor-pointer text-reg-12 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70";
33
+
34
+ function join() {
35
+ return Array.prototype.filter.call(arguments, Boolean).join(" ");
36
+ }
37
+
38
+ /**
39
+ * Create a radio group component
40
+ * @param {Object} config
41
+ * @param {string} [config.name] - name attribute for all radio inputs (required for grouping)
42
+ * @param {Array} config.options - array of { value, label, disabled? }
43
+ * @param {string} [config.defaultValue] - initial selected value
44
+ * @param {string} [config.value] - controlled value
45
+ * @param {boolean} [config.disabled] - disable all radio buttons
46
+ * @param {string} [config.className] - extra class on wrapper
47
+ * @param {string} [config.orientation] - 'horizontal' | 'vertical' (default: horizontal)
48
+ * @param {Function} [config.onChange] - change handler (receives selected value)
49
+ * @returns {HTMLElement} wrapper element containing all radio items
50
+ */
51
+ function create(config) {
52
+ var opts = config || {};
53
+ var name = opts.name || "radio-group-" + Math.random().toString(36).substr(2, 9);
54
+ var options = opts.options || [];
55
+ var defaultValue = opts.defaultValue;
56
+ var value = opts.value !== undefined ? opts.value : defaultValue;
57
+ var disabled = !!opts.disabled;
58
+ var className = opts.className || "";
59
+ var orientation = opts.orientation || "horizontal";
60
+ var onChange = opts.onChange;
61
+
62
+ // Wrapper container (RadioGroup root)
63
+ var wrapper = document.createElement("div");
64
+ wrapper.setAttribute("role", "radiogroup");
65
+ wrapper.setAttribute("aria-required", "false");
66
+ wrapper.setAttribute("dir", "ltr");
67
+ wrapper.className = join(
68
+ RADIO_GROUP_BASE_CLASS,
69
+ orientation === "vertical" ? "flex-col" : "",
70
+ className
71
+ );
72
+ wrapper.tabIndex = 0;
73
+
74
+ var selectedValue = value;
75
+
76
+ // Create radio items
77
+ options.forEach(function (option, index) {
78
+ var optionValue = option.value;
79
+ var optionLabel = option.label || option.value;
80
+ var optionDisabled = disabled || !!option.disabled;
81
+ var isChecked = optionValue === selectedValue;
82
+
83
+ var itemWrapper = document.createElement("div");
84
+ itemWrapper.className = "flex items-center gap-8";
85
+
86
+ // Hidden native radio input
87
+ var input = document.createElement("input");
88
+ input.type = "radio";
89
+ input.name = name;
90
+ input.value = optionValue;
91
+ input.id = name + "-" + index;
92
+ input.checked = isChecked;
93
+ input.disabled = optionDisabled;
94
+ input.className = "absolute opacity-0 w-0 h-0 peer";
95
+ input.style.position = "absolute";
96
+ input.style.opacity = "0";
97
+ input.style.width = "0";
98
+ input.style.height = "0";
99
+ input.style.pointerEvents = "none";
100
+
101
+ // Custom radio visual (RadioGroupItem)
102
+ var radioButton = document.createElement("button");
103
+ radioButton.type = "button";
104
+ radioButton.setAttribute("role", "radio");
105
+ radioButton.setAttribute("aria-checked", isChecked ? "true" : "false");
106
+ radioButton.setAttribute("data-state", isChecked ? "checked" : "unchecked");
107
+ radioButton.value = optionValue;
108
+ radioButton.id = name + "-button-" + index;
109
+ radioButton.tabIndex = -1;
110
+ radioButton.className = join(
111
+ RADIO_ITEM_BASE_CLASS,
112
+ isChecked ? RADIO_ITEM_CHECKED_CLASS : RADIO_ITEM_UNCHECKED_CLASS,
113
+ RADIO_ITEM_DISABLED_CLASS
114
+ );
115
+ if (optionDisabled) {
116
+ radioButton.disabled = true;
117
+ }
118
+
119
+ // Indicator (circle icon when checked)
120
+ var indicator = document.createElement("span");
121
+ indicator.setAttribute("data-state", isChecked ? "checked" : "unchecked");
122
+ indicator.className = "flex items-center justify-center";
123
+ if (isChecked) {
124
+ indicator.innerHTML = CIRCLE_ICON;
125
+ var svg = indicator.querySelector("svg");
126
+ if (svg) {
127
+ svg.setAttribute("class", RADIO_INDICATOR_CLASS);
128
+ }
129
+ }
130
+
131
+ radioButton.appendChild(indicator);
132
+
133
+ // Update visual state
134
+ function updateCheckedState(newValue) {
135
+ var isNowChecked = newValue === optionValue;
136
+ input.checked = isNowChecked;
137
+ radioButton.setAttribute("aria-checked", isNowChecked ? "true" : "false");
138
+ radioButton.setAttribute("data-state", isNowChecked ? "checked" : "unchecked");
139
+ indicator.setAttribute("data-state", isNowChecked ? "checked" : "unchecked");
140
+
141
+ // Update button classes
142
+ if (isNowChecked) {
143
+ radioButton.className = join(
144
+ RADIO_ITEM_BASE_CLASS,
145
+ RADIO_ITEM_CHECKED_CLASS,
146
+ RADIO_ITEM_DISABLED_CLASS
147
+ );
148
+ indicator.innerHTML = CIRCLE_ICON;
149
+ var svg = indicator.querySelector("svg");
150
+ if (svg) {
151
+ svg.setAttribute("class", RADIO_INDICATOR_CLASS);
152
+ }
153
+ } else {
154
+ radioButton.className = join(
155
+ RADIO_ITEM_BASE_CLASS,
156
+ RADIO_ITEM_UNCHECKED_CLASS,
157
+ RADIO_ITEM_DISABLED_CLASS
158
+ );
159
+ indicator.innerHTML = "";
160
+ }
161
+ }
162
+
163
+ // Click handler
164
+ radioButton.addEventListener("click", function () {
165
+ if (optionDisabled) return;
166
+ selectedValue = optionValue;
167
+
168
+ // Update all radio items in this group
169
+ var allInputs = wrapper.querySelectorAll('input[name="' + name + '"]');
170
+ var allInputsArray = Array.prototype.slice.call(allInputs);
171
+ allInputsArray.forEach(function (inp, idx) {
172
+ var btn = wrapper.querySelector('[id="' + name + '-button-' + idx + '"]');
173
+ if (btn) {
174
+ updateRadioState(inp, btn, inp.value === selectedValue);
175
+ }
176
+ });
177
+
178
+ if (typeof onChange === "function") {
179
+ onChange(selectedValue);
180
+ }
181
+ input.dispatchEvent(new Event("change", { bubbles: true }));
182
+ });
183
+
184
+ // Keyboard support
185
+ radioButton.addEventListener("keydown", function (e) {
186
+ if (optionDisabled) return;
187
+ if (e.key === " " || e.key === "Enter") {
188
+ e.preventDefault();
189
+ radioButton.click();
190
+ }
191
+ });
192
+
193
+ itemWrapper.appendChild(input);
194
+ itemWrapper.appendChild(radioButton);
195
+
196
+ // Label
197
+ if (optionLabel) {
198
+ var labelEl = document.createElement("label");
199
+ labelEl.htmlFor = name + "-" + index;
200
+ labelEl.className = join(
201
+ LABEL_BASE_CLASS,
202
+ optionDisabled ? "text-typography-quaternary-text" : ""
203
+ );
204
+ labelEl.textContent = optionLabel;
205
+ labelEl.addEventListener("click", function () {
206
+ if (!optionDisabled) {
207
+ radioButton.click();
208
+ }
209
+ });
210
+ itemWrapper.appendChild(labelEl);
211
+ }
212
+
213
+ wrapper.appendChild(itemWrapper);
214
+ });
215
+
216
+ // Helper function to update radio state
217
+ function updateRadioState(input, button, isChecked) {
218
+ input.checked = isChecked;
219
+ button.setAttribute("aria-checked", isChecked ? "true" : "false");
220
+ button.setAttribute("data-state", isChecked ? "checked" : "unchecked");
221
+
222
+ // Update button classes
223
+ if (isChecked) {
224
+ button.className = join(
225
+ RADIO_ITEM_BASE_CLASS,
226
+ RADIO_ITEM_CHECKED_CLASS,
227
+ RADIO_ITEM_DISABLED_CLASS
228
+ );
229
+ } else {
230
+ button.className = join(
231
+ RADIO_ITEM_BASE_CLASS,
232
+ RADIO_ITEM_UNCHECKED_CLASS,
233
+ RADIO_ITEM_DISABLED_CLASS
234
+ );
235
+ }
236
+
237
+ var indicator = button.querySelector("span");
238
+ if (indicator) {
239
+ indicator.setAttribute("data-state", isChecked ? "checked" : "unchecked");
240
+ if (isChecked) {
241
+ indicator.innerHTML = CIRCLE_ICON;
242
+ var svg = indicator.querySelector("svg");
243
+ if (svg) {
244
+ svg.setAttribute("class", RADIO_INDICATOR_CLASS);
245
+ }
246
+ } else {
247
+ indicator.innerHTML = "";
248
+ }
249
+ }
250
+ }
251
+
252
+ // Public API
253
+ wrapper.getValue = function () {
254
+ var checkedInput = wrapper.querySelector('input[name="' + name + '"]:checked');
255
+ return checkedInput ? checkedInput.value : null;
256
+ };
257
+
258
+ wrapper.setValue = function (newValue) {
259
+ selectedValue = newValue;
260
+ var allInputs = wrapper.querySelectorAll('input[name="' + name + '"]');
261
+ allInputs.forEach(function (inp, idx) {
262
+ var btn = wrapper.querySelector('[id="' + name + '-button-' + idx + '"]');
263
+ if (btn) {
264
+ updateRadioState(inp, btn, inp.value === newValue);
265
+ }
266
+ });
267
+ };
268
+
269
+ wrapper.setDisabled = function (isDisabled) {
270
+ disabled = !!isDisabled;
271
+ var allInputs = wrapper.querySelectorAll('input[name="' + name + '"]');
272
+ allInputs.forEach(function (inp, idx) {
273
+ inp.disabled = disabled;
274
+ var btn = wrapper.querySelector('[id="' + name + '-button-' + idx + '"]');
275
+ if (btn) {
276
+ btn.disabled = disabled;
277
+ }
278
+ });
279
+ };
280
+
281
+ return wrapper;
282
+ }
283
+
284
+ /**
285
+ * Create a standalone radio group item (for custom layouts)
286
+ * @param {Object} config
287
+ * @param {string} config.value - radio value
288
+ * @param {string} [config.name] - name attribute
289
+ * @param {string} [config.id] - radio id
290
+ * @param {boolean} [config.checked] - initial checked state
291
+ * @param {boolean} [config.disabled] - disabled state
292
+ * @param {string} [config.className] - extra class
293
+ * @param {Function} [config.onChange] - change handler
294
+ * @returns {HTMLElement}
295
+ */
296
+ function createItem(config) {
297
+ var opts = config || {};
298
+ var value = opts.value;
299
+ var name = opts.name || "radio-" + Math.random().toString(36).substr(2, 9);
300
+ var id = opts.id || name + "-" + value;
301
+ var checked = !!opts.checked;
302
+ var disabled = !!opts.disabled;
303
+ var className = opts.className || "";
304
+ var onChange = opts.onChange;
305
+
306
+ var wrapper = document.createElement("div");
307
+ wrapper.className = "flex items-center gap-8";
308
+
309
+ // Hidden native radio input
310
+ var input = document.createElement("input");
311
+ input.type = "radio";
312
+ input.name = name;
313
+ input.value = value;
314
+ input.id = id;
315
+ input.checked = checked;
316
+ input.disabled = disabled;
317
+ input.className = "absolute opacity-0 w-0 h-0 peer";
318
+ input.style.position = "absolute";
319
+ input.style.opacity = "0";
320
+ input.style.width = "0";
321
+ input.style.height = "0";
322
+ input.style.pointerEvents = "none";
323
+
324
+ // Custom radio visual
325
+ var radioButton = document.createElement("button");
326
+ radioButton.type = "button";
327
+ radioButton.setAttribute("role", "radio");
328
+ radioButton.setAttribute("aria-checked", checked ? "true" : "false");
329
+ radioButton.setAttribute("data-state", checked ? "checked" : "unchecked");
330
+ radioButton.value = value;
331
+ radioButton.tabIndex = disabled ? -1 : 0;
332
+ radioButton.className = join(
333
+ RADIO_ITEM_BASE_CLASS,
334
+ checked ? RADIO_ITEM_CHECKED_CLASS : RADIO_ITEM_UNCHECKED_CLASS,
335
+ RADIO_ITEM_DISABLED_CLASS,
336
+ className
337
+ );
338
+ if (disabled) {
339
+ radioButton.disabled = true;
340
+ }
341
+
342
+ // Indicator
343
+ var indicator = document.createElement("span");
344
+ indicator.setAttribute("data-state", checked ? "checked" : "unchecked");
345
+ indicator.className = "flex items-center justify-center";
346
+ if (checked) {
347
+ indicator.innerHTML = CIRCLE_ICON;
348
+ var svg = indicator.querySelector("svg");
349
+ if (svg) {
350
+ svg.setAttribute("class", RADIO_INDICATOR_CLASS);
351
+ }
352
+ }
353
+
354
+ radioButton.appendChild(indicator);
355
+
356
+ // Update visual state
357
+ function updateCheckedState() {
358
+ var isChecked = input.checked;
359
+ radioButton.setAttribute("aria-checked", isChecked ? "true" : "false");
360
+ radioButton.setAttribute("data-state", isChecked ? "checked" : "unchecked");
361
+ indicator.setAttribute("data-state", isChecked ? "checked" : "unchecked");
362
+
363
+ // Update button classes
364
+ if (isChecked) {
365
+ radioButton.className = join(
366
+ RADIO_ITEM_BASE_CLASS,
367
+ RADIO_ITEM_CHECKED_CLASS,
368
+ RADIO_ITEM_DISABLED_CLASS,
369
+ className
370
+ );
371
+ indicator.innerHTML = CIRCLE_ICON;
372
+ var svg = indicator.querySelector("svg");
373
+ if (svg) {
374
+ svg.setAttribute("class", RADIO_INDICATOR_CLASS);
375
+ }
376
+ } else {
377
+ radioButton.className = join(
378
+ RADIO_ITEM_BASE_CLASS,
379
+ RADIO_ITEM_UNCHECKED_CLASS,
380
+ RADIO_ITEM_DISABLED_CLASS,
381
+ className
382
+ );
383
+ indicator.innerHTML = "";
384
+ }
385
+ }
386
+
387
+ // Click handler
388
+ radioButton.addEventListener("click", function () {
389
+ if (disabled) return;
390
+ input.checked = true;
391
+ updateCheckedState();
392
+ if (typeof onChange === "function") {
393
+ onChange(value);
394
+ }
395
+ input.dispatchEvent(new Event("change", { bubbles: true }));
396
+ });
397
+
398
+ // Keyboard support
399
+ radioButton.addEventListener("keydown", function (e) {
400
+ if (disabled) return;
401
+ if (e.key === " " || e.key === "Enter") {
402
+ e.preventDefault();
403
+ radioButton.click();
404
+ }
405
+ });
406
+
407
+ wrapper.appendChild(input);
408
+ wrapper.appendChild(radioButton);
409
+
410
+ // Public API
411
+ wrapper.getInput = function () {
412
+ return input;
413
+ };
414
+ wrapper.setChecked = function (isChecked) {
415
+ input.checked = !!isChecked;
416
+ updateCheckedState();
417
+ };
418
+ wrapper.getChecked = function () {
419
+ return input.checked;
420
+ };
421
+
422
+ return wrapper;
423
+ }
424
+
425
+ var RadioGroup = {
426
+ create: create,
427
+ createItem: createItem,
428
+ };
429
+
430
+ if (typeof module !== "undefined" && module.exports) {
431
+ module.exports = RadioGroup;
432
+ } else {
433
+ global.RadioGroup = RadioGroup;
434
+ }
435
+ })(typeof window !== "undefined" ? window : this);