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