@neuctra/ui 0.2.3 → 0.2.5

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 (38) hide show
  1. package/dist/components/basic/Alert.d.ts +0 -1
  2. package/dist/components/basic/Card.d.ts +1 -1
  3. package/dist/components/basic/{CheckRadioInput.d.ts → CheckboxGroup.d.ts} +4 -7
  4. package/dist/components/basic/DropDown.d.ts +24 -34
  5. package/dist/components/basic/Modal.d.ts +2 -9
  6. package/dist/components/basic/RadioGroup.d.ts +25 -0
  7. package/dist/components/basic/SwitchGroup.d.ts +25 -0
  8. package/dist/index.cjs.js +41 -52
  9. package/dist/index.cjs.js.map +1 -1
  10. package/dist/index.d.ts +3 -1
  11. package/dist/index.es.js +1920 -1893
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/src/components/basic/Alert.js +45 -30
  14. package/dist/src/components/basic/Card.js +12 -10
  15. package/dist/src/components/basic/CheckboxGroup.js +40 -0
  16. package/dist/src/components/basic/DropDown.js +140 -294
  17. package/dist/src/components/basic/Modal.js +10 -12
  18. package/dist/src/components/basic/RadioGroup.js +37 -0
  19. package/dist/src/components/basic/SwitchGroup.js +50 -0
  20. package/dist/src/components/basic/Table.js +30 -11
  21. package/dist/src/index.js +4 -2
  22. package/dist/types/src/components/basic/Alert.d.ts +0 -1
  23. package/dist/types/src/components/basic/Card.d.ts +1 -1
  24. package/dist/types/src/components/basic/{CheckRadioInput.d.ts → CheckboxGroup.d.ts} +4 -7
  25. package/dist/types/src/components/basic/DropDown.d.ts +24 -34
  26. package/dist/types/src/components/basic/Modal.d.ts +2 -9
  27. package/dist/types/src/components/basic/RadioGroup.d.ts +25 -0
  28. package/dist/types/src/components/basic/SwitchGroup.d.ts +25 -0
  29. package/dist/types/src/index.d.ts +3 -1
  30. package/dist/ui.css +1 -1
  31. package/package.json +1 -1
  32. package/dist/components/avatar/AvatarGroup.d.ts +0 -9
  33. package/dist/components/avatar/AvatarWithStatus.d.ts +0 -10
  34. package/dist/src/components/avatar/AvatarGroup.js +0 -9
  35. package/dist/src/components/avatar/AvatarWithStatus.js +0 -18
  36. package/dist/src/components/basic/CheckRadioInput.js +0 -83
  37. package/dist/types/src/components/avatar/AvatarGroup.d.ts +0 -9
  38. package/dist/types/src/components/avatar/AvatarWithStatus.d.ts +0 -10
@@ -1,316 +1,162 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useRef, useEffect, useCallback } from "react";
3
- export const Dropdown = ({ options, value, defaultValue, onChange, placeholder = "Select an option", disabled = false, searchable = false, multiSelect = false, clearable = false, virtualized = false, optionHeight = 36, visibleOptions = 5,
4
- // Styling defaults
5
- width = "100%", height = "auto", borderColor = "#d1d5db", focusBorderColor = "#2563eb", errorBorderColor = "#dc2626", backgroundColor = "#ffffff", textColor = "#111827", placeholderColor = "#9ca3af", hoverColor = "#f3f4f6", selectedColor = "#eff6ff", disabledColor = "#f3f4f6", padding = "0.5rem 0.75rem", margin = "0", borderRadius = "0.375rem", boxShadow = "0 1px 2px 0 rgba(0, 0, 0, 0.05)", optionPadding = "0.5rem 0.75rem", optionGap = "0.5rem", transitionDuration = "200ms", dropdownMaxHeight = "300px", dropdownMinWidth = "100%",
6
- // Custom classes
7
- className = "", dropdownClassName = "", optionClassName = "", inputClassName = "",
8
- // Custom styles
9
- style, dropdownStyle, optionStyle, inputStyle,
10
- // Icons
11
- iconPrefix, iconSuffix, clearIcon = "×", dropdownIcon = "▼", checkIcon = "✓",
12
- // Accessibility
13
- ariaLabel, ariaLabelledby, ariaDescribedby,
14
- // Callbacks
15
- onFocus, onBlur, onOpen, onClose, }) => {
16
- const [selectedValues, setSelectedValues] = useState([]);
3
+ import { useEffect, useId, useMemo, useState, forwardRef, } from "react";
4
+ /* ----------------------
5
+ * Default theme tokens
6
+ * ---------------------*/
7
+ const THEMES = {
8
+ light: {
9
+ controlBg: "#ffffff",
10
+ menuBg: "#ffffff",
11
+ textColor: "#111827",
12
+ placeholderColor: "#6b7280",
13
+ hoverBg: "#f9fafb",
14
+ selectedBg: "#eff6ff",
15
+ disabledBg: "#f3f4f6",
16
+ disabledTextColor: "#9ca3af",
17
+ borderColor: "#e5e7eb",
18
+ accentColor: "#3b82f6",
19
+ },
20
+ dark: {
21
+ controlBg: "#1f2937",
22
+ menuBg: "#111827",
23
+ textColor: "#f9fafb",
24
+ placeholderColor: "#9ca3af",
25
+ hoverBg: "#374151",
26
+ selectedBg: "#2563eb33",
27
+ disabledBg: "#374151",
28
+ disabledTextColor: "#6b7280",
29
+ borderColor: "#374151",
30
+ accentColor: "#60a5fa",
31
+ },
32
+ custom: {},
33
+ };
34
+ /* ----------------------
35
+ * Component
36
+ * ---------------------*/
37
+ const DropdownInner = (props, ref) => {
38
+ const id = useId();
39
+ const { options, value, values, defaultValue, defaultValues, onChange, placeholder = "Select...", disabled = false, searchable = false, multiSelect = false, clearable = false, virtualized = false, width = "100%", dropdownMaxHeight = "320px", borderRadius = "8px", boxShadow = "0 8px 28px rgba(0,0,0,0.1)", borderColor, accentColor, theme = "light", menuBg, controlBg, textColor, hoverBg, selectedBg, disabledBg, disabledTextColor, placeholderColor, transitionDuration = "180ms", className, controlClassName, menuClassName, optionClassName, style, controlStyle, menuStyle, optionStyle, iconPrefix, iconSuffix, clearIcon = "×", dropdownIcon = "▾", checkIcon = "✓", } = props;
40
+ // merge theme colors
41
+ const themeVars = {
42
+ ...THEMES[theme],
43
+ ...(theme === "custom" ? {} : {}),
44
+ };
45
+ const colors = {
46
+ borderColor: borderColor ?? themeVars.borderColor,
47
+ accentColor: accentColor ?? themeVars.accentColor,
48
+ controlBg: controlBg ?? themeVars.controlBg,
49
+ menuBg: menuBg ?? themeVars.menuBg,
50
+ textColor: textColor ?? themeVars.textColor,
51
+ hoverBg: hoverBg ?? themeVars.hoverBg,
52
+ selectedBg: selectedBg ?? themeVars.selectedBg,
53
+ disabledBg: disabledBg ?? themeVars.disabledBg,
54
+ disabledTextColor: disabledTextColor ?? themeVars.disabledTextColor,
55
+ placeholderColor: placeholderColor ?? themeVars.placeholderColor,
56
+ };
57
+ /* ----------------- State ----------------- */
17
58
  const [isOpen, setIsOpen] = useState(false);
18
- const [searchTerm, setSearchTerm] = useState("");
19
- const [focusedIndex, setFocusedIndex] = useState(null);
20
- const dropdownRef = useRef(null);
21
- const inputRef = useRef(null);
22
- const optionsRef = useRef([]);
23
- // Initialize selected values
24
- useEffect(() => {
25
- if (value) {
26
- setSelectedValues(multiSelect ? value.split(",") : [value]);
27
- }
28
- else if (defaultValue) {
29
- setSelectedValues(multiSelect ? defaultValue.split(",") : [defaultValue]);
30
- }
31
- else {
32
- setSelectedValues([]);
33
- }
34
- }, [value, defaultValue, multiSelect]);
35
- // Filter options based on search term
36
- const filteredOptions = searchable
37
- ? options.filter((option) => option.label.toLowerCase().includes(searchTerm.toLowerCase()))
38
- : options;
39
- // Handle click outside
59
+ const [selected, setSelected] = useState(defaultValues ?? (defaultValue ? [defaultValue] : []));
40
60
  useEffect(() => {
41
- const handleClickOutside = (event) => {
42
- if (dropdownRef.current &&
43
- !dropdownRef.current.contains(event.target)) {
44
- setIsOpen(false);
45
- onClose?.();
46
- }
47
- };
48
- document.addEventListener("mousedown", handleClickOutside);
49
- return () => document.removeEventListener("mousedown", handleClickOutside);
50
- }, [onClose]);
51
- // Keyboard navigation
52
- useEffect(() => {
53
- const handleKeyDown = (event) => {
54
- if (!isOpen)
55
- return;
56
- switch (event.key) {
57
- case "ArrowDown":
58
- event.preventDefault();
59
- setFocusedIndex((prev) => {
60
- const nextIndex = prev === null
61
- ? 0
62
- : Math.min(prev + 1, filteredOptions.length - 1);
63
- scrollToOption(nextIndex);
64
- return nextIndex;
65
- });
66
- break;
67
- case "ArrowUp":
68
- event.preventDefault();
69
- setFocusedIndex((prev) => {
70
- const nextIndex = prev === null ? 0 : Math.max(prev - 1, 0);
71
- scrollToOption(nextIndex);
72
- return nextIndex;
73
- });
74
- break;
75
- case "Enter":
76
- event.preventDefault();
77
- if (focusedIndex !== null) {
78
- handleSelect(filteredOptions[focusedIndex].value);
79
- }
80
- break;
81
- case "Escape":
82
- event.preventDefault();
83
- setIsOpen(false);
84
- onClose?.();
85
- break;
86
- case "Tab":
87
- setIsOpen(false);
88
- onClose?.();
89
- break;
90
- }
91
- };
92
- document.addEventListener("keydown", handleKeyDown);
93
- return () => document.removeEventListener("keydown", handleKeyDown);
94
- }, [isOpen, focusedIndex, filteredOptions]);
95
- const scrollToOption = useCallback((index) => {
96
- optionsRef.current[index]?.scrollIntoView({
97
- block: "nearest",
98
- behavior: "smooth",
99
- });
100
- }, []);
101
- const handleSelect = (value) => {
102
- let newSelectedValues;
61
+ if (value)
62
+ setSelected([value]);
63
+ if (values)
64
+ setSelected(values);
65
+ }, [value, values]);
66
+ const selectedOptions = useMemo(() => options.filter((o) => selected.includes(o.value)), [options, selected]);
67
+ const toggle = () => !disabled && setIsOpen((s) => !s);
68
+ const selectValue = (val) => {
103
69
  if (multiSelect) {
104
- newSelectedValues = selectedValues.includes(value)
105
- ? selectedValues.filter((v) => v !== value)
106
- : [...selectedValues, value];
70
+ const exists = selected.includes(val);
71
+ const next = exists
72
+ ? selected.filter((v) => v !== val)
73
+ : [...selected, val];
74
+ setSelected(next);
75
+ onChange?.(next);
107
76
  }
108
77
  else {
109
- newSelectedValues = [value];
78
+ setSelected([val]);
79
+ onChange?.(val);
110
80
  setIsOpen(false);
111
- onClose?.();
112
81
  }
113
- setSelectedValues(newSelectedValues);
114
- onChange?.(multiSelect ? newSelectedValues.join(",") : value);
115
82
  };
116
- const handleClear = (e) => {
117
- e.stopPropagation();
118
- setSelectedValues([]);
119
- onChange?.("");
120
- setSearchTerm("");
83
+ const clearSelection = (e) => {
84
+ e?.stopPropagation();
85
+ setSelected([]);
86
+ onChange?.(multiSelect ? [] : "");
121
87
  };
122
- const toggleDropdown = () => {
123
- if (disabled)
124
- return;
125
- const newState = !isOpen;
126
- setIsOpen(newState);
127
- if (newState) {
128
- onOpen?.();
129
- if (searchable) {
130
- setTimeout(() => inputRef.current?.focus(), 0);
131
- }
132
- }
133
- else {
134
- onClose?.();
135
- }
136
- };
137
- const selectedOption = options.find((option) => option.value === selectedValues[0]);
138
- const selectedOptions = options.filter((option) => selectedValues.includes(option.value));
139
- // Virtualization setup
140
- const [startIndex, setStartIndex] = useState(0);
141
- const visibleOptionCount = Math.min(visibleOptions, filteredOptions.length);
142
- const endIndex = Math.min(startIndex + visibleOptionCount, filteredOptions.length);
143
- const visibleOptionsList = virtualized
144
- ? filteredOptions.slice(startIndex, endIndex)
145
- : filteredOptions;
146
- return (_jsxs("div", { ref: dropdownRef, className: `dropdown-container ${className}`, style: {
88
+ /* ----------------- Render ----------------- */
89
+ return (_jsxs("div", { ref: ref, className: className, style: {
147
90
  position: "relative",
148
91
  width,
149
- margin,
150
- fontFamily: "'Inter', sans-serif",
92
+ fontFamily: "Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial",
151
93
  ...style,
152
- }, children: [_jsx("style", { children: `
153
- .dropdown-container {
154
- --border-color: ${borderColor};
155
- --focus-border-color: ${focusBorderColor};
156
- --error-border-color: ${errorBorderColor};
157
- --bg-color: ${backgroundColor};
158
- --text-color: ${textColor};
159
- --placeholder-color: ${placeholderColor};
160
- --hover-color: ${hoverColor};
161
- --selected-color: ${selectedColor};
162
- --disabled-color: ${disabledColor};
163
- --transition-duration: ${transitionDuration};
164
- }
165
- ` }), _jsxs("div", { role: "button", onClick: toggleDropdown, "aria-disabled": disabled, "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, className: `dropdown-control ${inputClassName}`, style: {
166
- width: "100%",
167
- minHeight: height,
168
- padding,
169
- backgroundColor: disabled ? disabledColor : backgroundColor,
170
- color: textColor,
171
- borderWidth: "1px",
172
- borderStyle: "solid",
173
- borderColor: isOpen ? focusBorderColor : borderColor,
174
- borderRadius,
175
- boxShadow,
94
+ }, children: [_jsxs("div", { className: controlClassName, role: "button", tabIndex: 0, onClick: toggle, style: {
176
95
  display: "flex",
177
- alignItems: "center",
178
96
  justifyContent: "space-between",
97
+ alignItems: "center",
98
+ background: colors.controlBg,
99
+ color: colors.textColor,
100
+ padding: "0.5rem 0.75rem",
101
+ border: `1px solid ${isOpen ? colors.accentColor : colors.borderColor}`,
102
+ borderRadius,
179
103
  cursor: disabled ? "not-allowed" : "pointer",
180
- opacity: disabled ? 0.7 : 1,
181
- transition: `all ${transitionDuration} ease-in-out`,
182
- textAlign: "left",
183
- ...inputStyle,
184
- ...(isOpen && {
185
- boxShadow: `0 0 0 1px ${focusBorderColor}`,
186
- }),
187
- }, children: [_jsxs("div", { style: {
188
- display: "flex",
189
- alignItems: "center",
190
- gap: optionGap,
191
- flex: 1,
192
- overflow: "hidden",
193
- }, children: [iconPrefix && (_jsx("span", { className: "dropdown-icon-prefix", style: { flexShrink: 0 }, children: iconPrefix })), multiSelect ? (_jsx("div", { style: {
194
- display: "flex",
195
- gap: "0.25rem",
196
- flexWrap: "wrap",
197
- flex: 1,
198
- overflow: "hidden",
199
- }, children: selectedOptions.length > 0 ? (selectedOptions.map((option) => (_jsxs("span", { style: {
200
- backgroundColor: selectedColor,
201
- padding: "0.25rem 0.5rem",
202
- borderRadius: "0.25rem",
203
- fontSize: "0.875rem",
204
- display: "flex",
205
- alignItems: "center",
206
- gap: "0.25rem",
207
- flexShrink: 0,
208
- }, children: [option.icon && (_jsx("span", { style: { flexShrink: 0 }, children: option.icon })), _jsx("span", { style: {
209
- whiteSpace: "nowrap",
210
- overflow: "hidden",
211
- textOverflow: "ellipsis",
212
- }, children: option.label })] }, option.value)))) : (_jsx("span", { style: { color: placeholderColor }, children: placeholder })) })) : (_jsxs("span", { style: {
213
- color: selectedOption ? textColor : placeholderColor,
214
- overflow: "hidden",
215
- textOverflow: "ellipsis",
216
- whiteSpace: "nowrap",
217
- display: "flex",
218
- alignItems: "center",
219
- gap: optionGap,
220
- }, children: [selectedOption?.icon && (_jsx("span", { style: { flexShrink: 0 }, children: selectedOption.icon })), selectedOption ? selectedOption.label : placeholder] }))] }), _jsxs("div", { style: {
221
- display: "flex",
222
- alignItems: "center",
223
- gap: "0.5rem",
224
- marginLeft: "0.5rem",
225
- flexShrink: 0,
226
- }, children: [clearable && selectedValues.length > 0 && (_jsx("span", { onClick: handleClear, style: {
227
- cursor: disabled ? "not-allowed" : "pointer",
228
- fontSize: "1rem",
229
- color: textColor,
230
- opacity: 0.7,
231
- display: "flex",
232
- alignItems: "center",
233
- justifyContent: "center",
234
- }, "aria-label": "Clear selection", children: clearIcon })), _jsx("span", { style: {
235
- transition: `transform ${transitionDuration}`,
104
+ boxShadow: isOpen ? `0 0 0 3px ${colors.accentColor}33` : undefined,
105
+ transition: `all ${transitionDuration} ease`,
106
+ ...controlStyle,
107
+ }, children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [iconPrefix, selectedOptions.length ? (_jsx("span", { children: selectedOptions.map((s) => s.label).join(", ") })) : (_jsx("span", { style: { color: colors.placeholderColor }, children: placeholder }))] }), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [clearable && selected.length > 0 && (_jsx("button", { onClick: clearSelection, style: {
108
+ background: "transparent",
109
+ border: "none",
110
+ color: colors.textColor,
111
+ cursor: "pointer",
112
+ }, children: clearIcon })), iconSuffix, _jsx("div", { style: {
236
113
  transform: isOpen ? "rotate(180deg)" : "rotate(0deg)",
237
- fontSize: "0.75rem",
238
- color: textColor,
239
- opacity: 0.7,
240
- }, children: dropdownIcon })] })] }), isOpen && (_jsxs("div", { className: `dropdown-menu ${dropdownClassName}`, style: {
114
+ transition: `transform ${transitionDuration}`,
115
+ }, children: dropdownIcon })] })] }), isOpen && (_jsx("ul", { className: menuClassName, style: {
241
116
  position: "absolute",
242
- top: "100%",
117
+ zIndex: 100,
118
+ top: "calc(100% + 4px)",
243
119
  left: 0,
244
- zIndex: 1000,
245
- width: "100%",
246
- minWidth: dropdownMinWidth,
120
+ right: 0,
121
+ background: colors.menuBg,
122
+ border: `1px solid ${colors.borderColor}`,
123
+ borderRadius,
124
+ boxShadow,
247
125
  maxHeight: dropdownMaxHeight,
248
126
  overflowY: "auto",
249
- backgroundColor,
250
- border: `1px solid ${borderColor}`,
251
- borderRadius,
252
- boxShadow: `0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)`,
253
- marginTop: "0.25rem",
254
- transition: `opacity ${transitionDuration}, transform ${transitionDuration}`,
255
- opacity: 0,
256
- transform: "translateY(-0.5rem)",
257
- animation: `dropdownFadeIn ${transitionDuration} ease-out forwards`,
258
- ...dropdownStyle,
259
- }, role: "listbox", "aria-multiselectable": multiSelect, children: [searchable && (_jsx("div", { style: {
260
- padding: "0.5rem",
261
- borderBottom: `1px solid ${borderColor}`,
262
- }, children: _jsx("input", { ref: inputRef, type: "text", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), placeholder: "Search...", style: {
263
- width: "100%",
264
- padding: "0.5rem 0.75rem",
265
- borderWidth: "1px",
266
- borderStyle: "solid",
267
- color: textColor,
268
- borderColor: isOpen ? focusBorderColor : borderColor,
269
- borderRadius: "0.25rem",
270
- outline: "none",
271
- transition: `border-color ${transitionDuration}`,
272
- ...(isOpen && {
273
- boxShadow: `0 0 0 1px ${focusBorderColor}`,
274
- }),
275
- }, onFocus: onFocus, onBlur: onBlur }) })), _jsx("ul", { style: { margin: 0, padding: "0.25rem 0", listStyle: "none" }, children: filteredOptions.length > 0 ? (visibleOptionsList.map((option, index) => {
276
- const isSelected = selectedValues.includes(option.value);
277
- const isFocused = focusedIndex === (virtualized ? startIndex + index : index);
278
- const isDisabled = option.disabled;
279
- return (_jsxs("li", { ref: (el) => {
280
- if (el) {
281
- const refIndex = virtualized
282
- ? startIndex + index
283
- : index;
284
- optionsRef.current[refIndex] = el;
285
- }
286
- }, onClick: () => !isDisabled && handleSelect(option.value), onMouseEnter: () => !isDisabled &&
287
- setFocusedIndex(virtualized ? startIndex + index : index), className: `dropdown-option ${optionClassName} ${isDisabled ? "disabled" : ""}`, style: {
288
- padding: optionPadding,
289
- cursor: isDisabled ? "not-allowed" : "pointer",
290
- backgroundColor: isSelected
291
- ? selectedColor
292
- : isFocused
293
- ? hoverColor
294
- : backgroundColor,
295
- color: isDisabled ? disabledColor : textColor,
296
- display: "flex",
297
- alignItems: "center",
298
- gap: optionGap,
299
- transition: `background-color ${transitionDuration}`,
300
- ...optionStyle,
301
- }, role: "option", "aria-selected": isSelected, "aria-disabled": isDisabled, children: [multiSelect && (_jsx("span", { style: { flexShrink: 0 }, children: isSelected ? checkIcon : "○" })), option.icon && (_jsx("span", { style: { flexShrink: 0 }, children: option.icon })), _jsx("span", { style: { flex: 1 }, children: option.label })] }, option.value));
302
- })) : (_jsx("li", { style: {
303
- padding: optionPadding,
304
- color: placeholderColor,
305
- textAlign: "center",
306
- }, children: "No options found" })) }), virtualized && filteredOptions.length > visibleOptionCount && (_jsx("div", { style: {
307
- height: `${(filteredOptions.length - visibleOptionCount) * optionHeight}px`,
308
- } }))] })), _jsx("style", { children: `
309
- @keyframes dropdownFadeIn {
310
- to {
311
- opacity: 1;
312
- transform: translateY(0);
313
- }
314
- }
315
- ` })] }));
127
+ transition: `opacity ${transitionDuration} ease`,
128
+ ...menuStyle,
129
+ }, children: options.map((opt) => {
130
+ const isSelected = selected.includes(opt.value);
131
+ return (_jsxs("li", { onClick: () => !opt.disabled && selectValue(opt.value), className: optionClassName, style: {
132
+ display: "flex",
133
+ alignItems: "center",
134
+ gap: 8,
135
+ padding: "0.5rem 0.75rem",
136
+ background: opt.disabled
137
+ ? colors.disabledBg
138
+ : isSelected
139
+ ? colors.selectedBg
140
+ : "transparent",
141
+ color: opt.disabled
142
+ ? colors.disabledTextColor
143
+ : colors.textColor,
144
+ cursor: opt.disabled ? "not-allowed" : "pointer",
145
+ borderRadius: 6,
146
+ userSelect: "none",
147
+ ...optionStyle,
148
+ }, onMouseEnter: (e) => {
149
+ if (!opt.disabled && !isSelected)
150
+ e.currentTarget.style.backgroundColor = colors.hoverBg;
151
+ }, onMouseLeave: (e) => {
152
+ if (!opt.disabled && !isSelected)
153
+ e.currentTarget.style.backgroundColor = "transparent";
154
+ }, children: [multiSelect && (_jsx("span", { style: { width: 18, textAlign: "center" }, children: isSelected ? checkIcon : "○" })), opt.icon && _jsx("span", { children: opt.icon }), _jsxs("div", { style: { flex: 1 }, children: [opt.label, opt.description && (_jsx("div", { style: {
155
+ fontSize: 12,
156
+ color: colors.placeholderColor,
157
+ marginTop: 2,
158
+ }, children: opt.description }))] })] }, opt.value));
159
+ }) }))] }));
316
160
  };
161
+ export const Dropdown = forwardRef(DropdownInner);
162
+ export default Dropdown;
@@ -1,10 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useCallback, } from "react";
3
3
  import { X } from "lucide-react";
4
- /* -------------------------------------------------------------------------- */
5
- /* Modal */
6
- /* -------------------------------------------------------------------------- */
7
- export const Modal = ({ isOpen, onClose, children, ariaLabel, title, overlayStyle, modalStyle, closeButtonStyle, disableOverlayClose = false, transitionDuration = 200, className, }) => {
4
+ export const Modal = ({ isOpen, onClose, children, ariaLabel, title, overlayStyle, modalStyle, closeButtonStyle, disableOverlayClose = false, transitionDuration = 200, className, darkMode = false, }) => {
8
5
  const modalRef = useRef(null);
9
6
  /* ------------------------------ Escape Close ----------------------------- */
10
7
  useEffect(() => {
@@ -31,14 +28,13 @@ export const Modal = ({ isOpen, onClose, children, ariaLabel, title, overlayStyl
31
28
  if (!disableOverlayClose)
32
29
  onClose();
33
30
  }, [disableOverlayClose, onClose]);
34
- /* ------------------------------ Early Return ----------------------------- */
35
31
  if (!isOpen)
36
32
  return null;
37
33
  /* ------------------------------- Base Styles ----------------------------- */
38
34
  const baseOverlay = {
39
35
  position: "fixed",
40
36
  inset: 0,
41
- backgroundColor: "rgba(0,0,0,0.6)",
37
+ backgroundColor: darkMode ? "rgba(0,0,0,0.8)" : "rgba(0,0,0,0.6)",
42
38
  display: "flex",
43
39
  justifyContent: "center",
44
40
  alignItems: "center",
@@ -49,14 +45,17 @@ export const Modal = ({ isOpen, onClose, children, ariaLabel, title, overlayStyl
49
45
  };
50
46
  const baseModal = {
51
47
  position: "relative",
52
- backgroundColor: "#fff",
48
+ backgroundColor: darkMode ? "#1f1f1f" : "#fff",
49
+ color: darkMode ? "#f5f5f5" : "#111",
53
50
  borderRadius: 12,
54
51
  width: "90vw",
55
52
  maxWidth: 700,
56
53
  maxHeight: "90vh",
57
54
  overflowY: "auto",
58
55
  padding: 24,
59
- boxShadow: "0 10px 40px rgba(0,0,0,0.25)",
56
+ boxShadow: darkMode
57
+ ? "0 10px 40px rgba(0,0,0,0.7)"
58
+ : "0 10px 40px rgba(0,0,0,0.25)",
60
59
  transform: isOpen ? "scale(1)" : "scale(0.95)",
61
60
  transition: `transform ${transitionDuration}ms ease, opacity ${transitionDuration}ms ease`,
62
61
  ...modalStyle,
@@ -69,16 +68,15 @@ export const Modal = ({ isOpen, onClose, children, ariaLabel, title, overlayStyl
69
68
  border: "none",
70
69
  cursor: "pointer",
71
70
  padding: 4,
72
- color: "#444",
71
+ color: darkMode ? "#f5f5f5" : "#444",
73
72
  transition: "color 0.2s ease, transform 0.2s ease",
74
73
  ...closeButtonStyle,
75
74
  };
76
- /* ----------------------------- Render Content ---------------------------- */
77
75
  return (_jsx("div", { role: "dialog", "aria-modal": "true", "aria-label": ariaLabel || title || "Modal", style: baseOverlay, onClick: handleOverlayClick, className: className, children: _jsxs("div", { ref: modalRef, style: baseModal, onClick: (e) => e.stopPropagation(), children: [_jsx("button", { onClick: onClose, "aria-label": "Close modal", style: baseCloseBtn, onMouseEnter: (e) => {
78
- e.currentTarget.style.color = "#000";
76
+ e.currentTarget.style.color = darkMode ? "#fff" : "#000";
79
77
  e.currentTarget.style.transform = "scale(1.1)";
80
78
  }, onMouseLeave: (e) => {
81
- e.currentTarget.style.color = "#444";
79
+ e.currentTarget.style.color = darkMode ? "#f5f5f5" : "#444";
82
80
  e.currentTarget.style.transform = "scale(1)";
83
81
  }, children: _jsx(X, { size: 24 }) }), title && (_jsx("h2", { style: {
84
82
  fontSize: "1.25rem",
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ export const RadioGroup = ({ name, options, selectedValue, onChange, disabled = false, readOnly = false, required = false, error, className = "", style, labelStyle, iconSize = 20, iconCheckedBgColor = "#2563eb", iconUncheckedBorderColor = "#9ca3af", textColor = "#374151", errorStyle, }) => {
4
+ return (_jsxs("div", { className: className, style: { display: "flex", flexDirection: "column", gap: 8, ...style }, role: "radiogroup", "aria-disabled": disabled, children: [options.map((option) => {
5
+ const isChecked = selectedValue === option.value;
6
+ return (_jsxs("label", { style: {
7
+ display: "flex",
8
+ alignItems: "center",
9
+ justifyContent: "space-between",
10
+ cursor: disabled ? "not-allowed" : "pointer",
11
+ opacity: disabled ? 0.6 : 1,
12
+ gap: 8,
13
+ userSelect: "none",
14
+ ...labelStyle,
15
+ }, children: [_jsx("span", { style: { color: textColor, fontSize: 14 }, children: option.label }), _jsx("input", { type: "radio", name: name, value: option.value, checked: isChecked, disabled: disabled || readOnly, required: required, onChange: () => onChange && onChange(option.value), style: { display: "none" } }), _jsx("span", { style: {
16
+ display: "inline-flex",
17
+ justifyContent: "center",
18
+ alignItems: "center",
19
+ width: iconSize,
20
+ height: iconSize,
21
+ borderRadius: "50%",
22
+ border: `2px solid ${isChecked ? iconCheckedBgColor : iconUncheckedBorderColor}`,
23
+ backgroundColor: isChecked ? iconCheckedBgColor : "transparent",
24
+ transition: "all 0.25s ease",
25
+ }, children: isChecked && (_jsx("span", { style: {
26
+ width: iconSize / 2,
27
+ height: iconSize / 2,
28
+ borderRadius: "50%",
29
+ backgroundColor: "white",
30
+ } })) })] }, option.value));
31
+ }), error && (_jsx("p", { role: "alert", style: {
32
+ color: "#dc2626",
33
+ fontSize: 12,
34
+ marginTop: 4,
35
+ ...errorStyle,
36
+ }, children: error }))] }));
37
+ };
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ export const SwitchGroup = ({ name, options, selectedValues = [], onChange, disabled = false, readOnly = false, required = false, error, className = "", style, labelStyle, iconSize = 20, iconCheckedBgColor = "#2563eb", switchBgColor = "#d1d5db", textColor = "#374151", errorStyle, }) => {
4
+ const handleChange = (value) => {
5
+ if (!onChange)
6
+ return;
7
+ const updatedValues = selectedValues.includes(value)
8
+ ? selectedValues.filter((v) => v !== value)
9
+ : [...selectedValues, value];
10
+ onChange(updatedValues);
11
+ };
12
+ return (_jsxs("div", { className: className, style: { display: "flex", flexDirection: "column", gap: 8, ...style }, role: "group", "aria-disabled": disabled, children: [options.map((option) => {
13
+ const isChecked = selectedValues.includes(option.value);
14
+ return (_jsxs("label", { style: {
15
+ display: "flex",
16
+ alignItems: "center",
17
+ justifyContent: "space-between",
18
+ cursor: disabled ? "not-allowed" : "pointer",
19
+ opacity: disabled ? 0.6 : 1,
20
+ gap: 8,
21
+ userSelect: "none",
22
+ ...labelStyle,
23
+ }, children: [_jsx("span", { style: { color: textColor, fontSize: 14 }, children: option.label }), _jsx("input", { type: "checkbox", name: name, value: option.value, checked: isChecked, disabled: disabled || readOnly, required: required, onChange: () => handleChange(option.value), style: { display: "none" } }), _jsx("span", { style: {
24
+ position: "relative",
25
+ width: iconSize * 2,
26
+ height: iconSize * 1.1,
27
+ borderRadius: 9999,
28
+ backgroundColor: isChecked ? iconCheckedBgColor : switchBgColor,
29
+ transition: "background-color 0.25s ease",
30
+ }, children: _jsx("span", { style: {
31
+ position: "absolute",
32
+ top: "50%",
33
+ left: isChecked
34
+ ? `calc(100% - ${iconSize - 4}px - 2px)`
35
+ : "2px",
36
+ transform: "translateY(-50%)",
37
+ width: iconSize - 4,
38
+ height: iconSize - 4,
39
+ borderRadius: "50%",
40
+ backgroundColor: "#fff",
41
+ boxShadow: "0 1px 2px rgba(0,0,0,0.3)",
42
+ transition: "left 0.25s ease",
43
+ } }) })] }, option.value));
44
+ }), error && (_jsx("p", { role: "alert", style: {
45
+ color: "#dc2626",
46
+ fontSize: 12,
47
+ marginTop: 4,
48
+ ...errorStyle,
49
+ }, children: error }))] }));
50
+ };