@neuctra/ui 0.2.5 → 0.2.7

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 (80) hide show
  1. package/dist/components/basic/Accordation.d.ts +9 -13
  2. package/dist/components/basic/Alert.d.ts +10 -23
  3. package/dist/components/basic/Avatar.d.ts +7 -16
  4. package/dist/components/basic/Badge.d.ts +9 -14
  5. package/dist/components/basic/Button.d.ts +9 -19
  6. package/dist/components/basic/CheckboxGroup.d.ts +1 -0
  7. package/dist/components/basic/Container.d.ts +2 -19
  8. package/dist/components/basic/Drawer.d.ts +7 -18
  9. package/dist/components/basic/DropDown.d.ts +20 -40
  10. package/dist/components/basic/FlexView.d.ts +16 -0
  11. package/dist/components/basic/GridView.d.ts +4 -9
  12. package/dist/components/basic/Image.d.ts +10 -31
  13. package/dist/components/basic/Input.d.ts +22 -35
  14. package/dist/components/basic/List.d.ts +6 -20
  15. package/dist/components/basic/Modal.d.ts +8 -8
  16. package/dist/components/basic/RadioGroup.d.ts +1 -0
  17. package/dist/components/basic/Stack.d.ts +4 -14
  18. package/dist/components/basic/SwitchGroup.d.ts +1 -0
  19. package/dist/components/basic/Table.d.ts +6 -1
  20. package/dist/components/basic/Text.d.ts +1109 -14
  21. package/dist/index.cjs.js +82 -52
  22. package/dist/index.cjs.js.map +1 -1
  23. package/dist/index.d.ts +3 -4
  24. package/dist/index.es.js +2004 -2914
  25. package/dist/index.es.js.map +1 -1
  26. package/dist/src/components/basic/Accordation.js +25 -26
  27. package/dist/src/components/basic/Alert.js +33 -138
  28. package/dist/src/components/basic/AudioPlayer.js +54 -40
  29. package/dist/src/components/basic/Avatar.js +41 -154
  30. package/dist/src/components/basic/Badge.js +23 -62
  31. package/dist/src/components/basic/Button.js +24 -97
  32. package/dist/src/components/basic/CheckboxGroup.js +36 -13
  33. package/dist/src/components/basic/Container.js +19 -38
  34. package/dist/src/components/basic/Drawer.js +22 -73
  35. package/dist/src/components/basic/DropDown.js +94 -158
  36. package/dist/src/components/basic/FlexView.js +19 -0
  37. package/dist/src/components/basic/GridView.js +15 -48
  38. package/dist/src/components/basic/Image.js +39 -79
  39. package/dist/src/components/basic/Input.js +68 -109
  40. package/dist/src/components/basic/List.js +20 -62
  41. package/dist/src/components/basic/Modal.js +6 -58
  42. package/dist/src/components/basic/RadioGroup.js +35 -18
  43. package/dist/src/components/basic/Stack.js +19 -72
  44. package/dist/src/components/basic/SwitchGroup.js +42 -16
  45. package/dist/src/components/basic/Table.js +15 -36
  46. package/dist/src/components/basic/Tabs.js +3 -12
  47. package/dist/src/components/basic/Text.js +20 -112
  48. package/dist/src/index.js +3 -5
  49. package/dist/types/src/components/basic/Accordation.d.ts +9 -13
  50. package/dist/types/src/components/basic/Alert.d.ts +10 -23
  51. package/dist/types/src/components/basic/Avatar.d.ts +7 -16
  52. package/dist/types/src/components/basic/Badge.d.ts +9 -14
  53. package/dist/types/src/components/basic/Button.d.ts +9 -19
  54. package/dist/types/src/components/basic/CheckboxGroup.d.ts +1 -0
  55. package/dist/types/src/components/basic/Container.d.ts +2 -19
  56. package/dist/types/src/components/basic/Drawer.d.ts +7 -18
  57. package/dist/types/src/components/basic/DropDown.d.ts +20 -40
  58. package/dist/types/src/components/basic/FlexView.d.ts +16 -0
  59. package/dist/types/src/components/basic/GridView.d.ts +4 -9
  60. package/dist/types/src/components/basic/Image.d.ts +10 -31
  61. package/dist/types/src/components/basic/Input.d.ts +22 -35
  62. package/dist/types/src/components/basic/List.d.ts +6 -20
  63. package/dist/types/src/components/basic/Modal.d.ts +8 -8
  64. package/dist/types/src/components/basic/RadioGroup.d.ts +1 -0
  65. package/dist/types/src/components/basic/Stack.d.ts +4 -14
  66. package/dist/types/src/components/basic/SwitchGroup.d.ts +1 -0
  67. package/dist/types/src/components/basic/Table.d.ts +6 -1
  68. package/dist/types/src/components/basic/Text.d.ts +1109 -14
  69. package/dist/types/src/index.d.ts +3 -4
  70. package/dist/ui.css +1 -1
  71. package/package.json +2 -1
  72. package/dist/components/basic/Card.d.ts +0 -28
  73. package/dist/components/basic/Flexbox.d.ts +0 -25
  74. package/dist/components/basic/Section.d.ts +0 -36
  75. package/dist/src/components/basic/Card.js +0 -47
  76. package/dist/src/components/basic/Flexbox.js +0 -67
  77. package/dist/src/components/basic/Section.js +0 -100
  78. package/dist/types/src/components/basic/Card.d.ts +0 -28
  79. package/dist/types/src/components/basic/Flexbox.d.ts +0 -25
  80. package/dist/types/src/components/basic/Section.d.ts +0 -36
@@ -1,123 +1,82 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState, useRef, useImperativeHandle, forwardRef, } from "react";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { forwardRef, useState, useImperativeHandle, useRef, } from "react";
4
4
  import { Eye, EyeOff } from "lucide-react";
5
5
  export const Input = forwardRef((props, ref) => {
6
- const { type = "text", label, placeholder = "", name = "", value, defaultValue, onChange, disabled = false, readOnly = false, required = false, error, success = false, autoFocus = false, iconLeft, iconRight,
7
- /** 🎨 Styling props */
8
- labelColor = "#374151", placeholderColor = "#9ca3af", backgroundColor = "#ffffff", textColor = "#111827", borderColor = "#d1d5db", hoverBorderColor = "#9ca3af", focusBorderColor = "#2563eb", errorColor = "#dc2626", successColor = "#16a34a", iconColor = "#6b7280", shadow = "0 1px 2px rgba(0,0,0,0.05)",
9
- /** 📏 Layout + size */
10
- size = "md", fontSize = "14px", fontFamily = "Inter, system-ui, sans-serif", radius = "8px", rows = 4, cols, maxLength, resize = true, showCharacterCount = true, paddingX, paddingY, className, style, } = props;
6
+ const { label, name = "", type = "text", placeholder = "", value, defaultValue, onChange, required, disabled, readOnly, error, success, helperText, icon: LabelIcon, prefix, prefixIcon: PrefixIcon, suffixIcon, min, max, step, rows = 4, primaryTheme = true, primaryColor = "#3b82f6", className = "", } = props;
11
7
  const inputRef = useRef(null);
8
+ useImperativeHandle(ref, () => inputRef.current);
12
9
  const [localValue, setLocalValue] = useState(defaultValue || "");
13
10
  const [visible, setVisible] = useState(false);
14
- useImperativeHandle(ref, () => inputRef.current);
15
- /** Make it controlled properly */
16
- const handleChange = (e) => {
17
- const newValue = e.target.value;
18
- if (maxLength && newValue.length > maxLength)
19
- return;
20
- setLocalValue(newValue);
21
- if (onChange)
22
- onChange(name, newValue);
23
- };
11
+ const hasPrefixIcon = Boolean(PrefixIcon);
12
+ const hasPrefixText = Boolean(prefix);
13
+ const hasPrefix = hasPrefixIcon || hasPrefixText;
24
14
  const currentValue = value !== undefined ? value : localValue;
25
- /** 🎯 Dynamic border colors */
26
- const currentBorderColor = error
27
- ? errorColor
28
- : success
29
- ? successColor
30
- : borderColor;
31
- /** 🧠 Size tokens */
32
- const sizes = {
33
- sm: { paddingY: "6px", paddingX: "10px", font: "13px" },
34
- md: { paddingY: "10px", paddingX: "14px", font: "14px" },
35
- lg: { paddingY: "14px", paddingX: "18px", font: "16px" },
36
- }[size];
37
- const px = paddingX || sizes.paddingX;
38
- const py = paddingY || sizes.paddingY;
39
- /** 💅 Base input styles */
40
- const baseInputStyle = {
41
- width: "100%",
42
- border: `1px solid ${currentBorderColor}`,
43
- borderRadius: radius,
44
- backgroundColor,
45
- color: textColor,
46
- fontFamily,
47
- fontSize,
48
- padding: `${py} ${px}`,
49
- paddingLeft: iconLeft ? "40px" : px,
50
- paddingRight: iconRight || type === "password" ? "40px" : px,
51
- outline: "none",
52
- transition: "border-color 0.25s ease, box-shadow 0.25s ease",
53
- resize: type === "textarea" && !resize ? "none" : undefined,
54
- boxShadow: shadow,
55
- ...style,
56
- };
57
- /** 🧠 Placeholder dynamic color */
58
- const dynamicPlaceholder = {
59
- "::placeholder": {
60
- color: placeholderColor,
61
- opacity: 1,
62
- },
15
+ const handleChange = (e) => {
16
+ const val = e.target.value;
17
+ setLocalValue(val);
18
+ onChange?.(name, val);
63
19
  };
64
- /** 🎨 Dynamic border behavior */
65
- const applyDynamicBorder = (el, color) => {
66
- if (el)
67
- el.style.borderColor = color;
20
+ const handleKeyDown = (e) => {
21
+ if (type === "number" && e.key === "-")
22
+ e.preventDefault();
68
23
  };
69
- const commonEvents = {
70
- onFocus: (e) => applyDynamicBorder(e.currentTarget, focusBorderColor),
71
- onBlur: (e) => applyDynamicBorder(e.currentTarget, currentBorderColor),
72
- onMouseEnter: (e) => applyDynamicBorder(e.currentTarget, hoverBorderColor),
73
- onMouseLeave: (e) => applyDynamicBorder(e.currentTarget, currentBorderColor),
24
+ /** Padding logic */
25
+ const getPadding = () => {
26
+ if (!hasPrefix)
27
+ return "px-4";
28
+ if (hasPrefixIcon && hasPrefixText)
29
+ return "pl-20 pr-4";
30
+ if (hasPrefixText)
31
+ return "pl-14 pr-4";
32
+ if (hasPrefixIcon)
33
+ return "pl-10 pr-4";
34
+ return "px-4";
74
35
  };
75
- return (_jsxs("div", { className: className, style: {
76
- width: "100%",
77
- display: "flex",
78
- flexDirection: "column",
79
- fontFamily,
80
- }, children: [label && (_jsxs("label", { htmlFor: name, style: {
81
- marginBottom: 6,
82
- color: labelColor,
83
- fontWeight: 500,
84
- fontSize: "14px",
85
- }, children: [label, required && _jsx("span", { style: { color: errorColor }, children: " *" })] })), _jsxs("div", { style: { position: "relative", width: "100%" }, children: [iconLeft && (_jsx("span", { style: {
86
- position: "absolute",
87
- left: 12,
88
- top: "50%",
89
- transform: "translateY(-50%)",
90
- color: iconColor,
91
- pointerEvents: "none",
92
- }, children: iconLeft })), type === "textarea" ? (_jsx("textarea", { ref: inputRef, name: name, value: currentValue, placeholder: placeholder, disabled: disabled, readOnly: readOnly, rows: rows, cols: cols, maxLength: maxLength, autoFocus: autoFocus, style: { ...baseInputStyle, ...dynamicPlaceholder }, onChange: handleChange, ...commonEvents })) : (_jsx("input", { ref: inputRef, id: name, type: type === "password"
36
+ /** 🔥 Theme Styles */
37
+ const dynamicFocusStyle = !primaryTheme
38
+ ? {
39
+ borderColor: primaryColor,
40
+ boxShadow: `0 0 0 1px ${primaryColor}`,
41
+ }
42
+ : {};
43
+ const dynamicColorStyle = !primaryTheme
44
+ ? { color: primaryColor }
45
+ : {};
46
+ /** Border */
47
+ const borderStyle = error
48
+ ? "border-red-500"
49
+ : success
50
+ ? "border-emerald-500"
51
+ : "border-zinc-300 dark:border-zinc-800";
52
+ return (_jsxs("div", { className: `w-full space-y-1 ${className}`, children: [label && (_jsxs("label", { className: "flex items-center gap-2 text-[12px] font-medium text-gray-700 dark:text-zinc-100", children: [LabelIcon && (_jsx(LabelIcon, { size: 16, className: primaryTheme ? "text-[var(--primary)]" : "", style: !primaryTheme ? dynamicColorStyle : undefined })), label, required && _jsx("span", { className: "text-red-500", children: "*" })] })), _jsxs("div", { className: "relative", children: [hasPrefix && (_jsxs("div", { className: "absolute inset-y-0 left-0 flex items-center pl-3 gap-2 text-sm text-zinc-400 pointer-events-none", children: [PrefixIcon && _jsx(PrefixIcon, { size: 14 }), hasPrefixText && (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-medium text-zinc-600 dark:text-zinc-200", children: prefix }), _jsx("span", { className: "h-4 w-px bg-zinc-300 dark:bg-zinc-700" })] }))] })), type === "textarea" ? (_jsx("textarea", { ref: inputRef, name: name, value: currentValue, onChange: handleChange, placeholder: placeholder, required: required, disabled: disabled, readOnly: readOnly, rows: rows, style: !primaryTheme ? dynamicFocusStyle : undefined, className: `
53
+ w-full rounded-lg text-sm
54
+ bg-white dark:bg-zinc-900 border
55
+ text-gray-900 dark:text-white
56
+ placeholder:text-zinc-400
57
+ py-2.5 outline-none
58
+ focus:ring-1
59
+ ${primaryTheme && "focus:ring-[var(--primary)] focus:border-[var(--primary)]"}
60
+ ${getPadding()}
61
+ ${borderStyle}
62
+ ` })) : (_jsx("input", { ref: inputRef, type: type === "password"
93
63
  ? visible
94
64
  ? "text"
95
65
  : "password"
96
- : type, name: name, value: currentValue, placeholder: placeholder, disabled: disabled, readOnly: readOnly, autoFocus: autoFocus, style: { ...baseInputStyle, ...dynamicPlaceholder }, onChange: handleChange, ...commonEvents })), type === "password" && (_jsx("button", { type: "button", onClick: () => setVisible(!visible), style: {
97
- position: "absolute",
98
- right: 10,
99
- top: "50%",
100
- transform: "translateY(-50%)",
101
- background: "transparent",
102
- border: "none",
103
- cursor: "pointer",
104
- color: iconColor,
105
- padding: 0,
106
- }, children: visible ? _jsx(EyeOff, { size: 18 }) : _jsx(Eye, { size: 18 }) })), iconRight && type !== "password" && (_jsx("span", { style: {
107
- position: "absolute",
108
- right: 12,
109
- top: "50%",
110
- transform: "translateY(-50%)",
111
- color: iconColor,
112
- pointerEvents: "none",
113
- }, children: iconRight }))] }), type === "textarea" && showCharacterCount && maxLength && (_jsxs("div", { style: {
114
- textAlign: "right",
115
- fontSize: "12px",
116
- color: "#6b7280",
117
- marginTop: 4,
118
- }, children: [currentValue.length, "/", maxLength] })), error && (_jsx("div", { style: {
119
- color: errorColor,
120
- fontSize: "12px",
121
- marginTop: 4,
122
- }, children: error }))] }));
66
+ : type, name: name, value: currentValue, onChange: handleChange, onKeyDown: handleKeyDown, placeholder: placeholder, required: required, disabled: disabled, readOnly: readOnly, min: type === "number" ? min ?? 0 : undefined, max: max, step: step, style: !primaryTheme ? dynamicFocusStyle : undefined, className: `
67
+ w-full rounded-lg text-sm
68
+ bg-white dark:bg-zinc-900 border
69
+ text-gray-900 dark:text-white
70
+ placeholder:text-zinc-400
71
+ py-2.5 outline-none
72
+ focus:ring-1
73
+ disabled:opacity-50 disabled:cursor-not-allowed
74
+ ${primaryTheme && "focus:ring-[var(--primary)] focus:border-[var(--primary)]"}
75
+ ${getPadding()}
76
+ ${borderStyle}
77
+ ` })), type === "password" && (_jsx("button", { type: "button", onClick: () => setVisible(!visible), className: "absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400", children: visible ? _jsx(EyeOff, { size: 18 }) : _jsx(Eye, { size: 18 }) })), suffixIcon && type !== "password" && (_jsx("span", { className: "absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400", children: suffixIcon }))] }), (helperText || error) && (_jsx("p", { className: `text-xs ${error
78
+ ? "text-red-500"
79
+ : success
80
+ ? "text-emerald-500"
81
+ : "text-zinc-500"}`, children: error && typeof error === "string" ? error : helperText }))] }));
123
82
  });
@@ -1,71 +1,29 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- export const ListItem = ({ text, icon, onClick, subItems, bulletColor = "#2563eb", textColor = "#111827", fontSize = "15px", fontWeight = 500, spacing = "12px", isInline = false, }) => {
3
- const itemContainerStyle = {
4
- display: "flex",
5
- flexDirection: "column",
6
- gap: "6px",
7
- marginBottom: isInline ? "0" : spacing,
8
- };
9
- const contentStyle = {
10
- display: "flex",
11
- alignItems: "center",
12
- gap: "8px",
13
- fontSize,
14
- fontWeight,
15
- color: textColor,
16
- cursor: onClick ? "pointer" : "default",
17
- transition: "color 0.2s ease, transform 0.2s ease",
18
- };
19
- const bulletStyle = {
20
- width: "8px",
21
- height: "8px",
22
- backgroundColor: bulletColor,
23
- borderRadius: "50%",
24
- flexShrink: 0,
25
- };
26
- const subListStyle = {
27
- listStyleType: "disc",
28
- paddingLeft: "20px",
29
- margin: 0,
30
- };
31
- return (_jsxs("li", { style: itemContainerStyle, children: [_jsxs("div", { style: contentStyle, onClick: onClick, onMouseEnter: (e) => (e.currentTarget.style.color = bulletColor), onMouseLeave: (e) => (e.currentTarget.style.color = textColor), children: [icon ? (_jsx("span", { style: { fontSize: "16px", color: textColor }, children: icon })) : (!isInline && _jsx("span", { style: bulletStyle })), _jsx("span", { children: text })] }), subItems && subItems.length > 0 && (_jsx("ul", { style: subListStyle, children: subItems.map((sub, index) => (_jsx(ListItem, { ...sub, bulletColor: bulletColor, textColor: textColor, fontSize: fontSize, fontWeight: fontWeight, spacing: spacing, isInline: false }, index))) }))] }));
2
+ import clsx from "clsx";
3
+ const ListItem = ({ text, icon, onClick, subItems, isInline, isOrdered, primaryTheme = true, primaryColor = "#3b82f6", itemClassName, bulletClassName, }) => {
4
+ // ✅ Dynamic style for primary color
5
+ const dynamicStyle = !primaryTheme
6
+ ? { color: primaryColor }
7
+ : {};
8
+ const bulletStyle = !primaryTheme
9
+ ? { backgroundColor: primaryColor }
10
+ : {};
11
+ return (_jsxs("li", { className: clsx(!isInline && "mb-3"), children: [_jsxs("div", { onClick: onClick, style: !primaryTheme ? dynamicStyle : undefined, className: clsx("flex items-center gap-2 text-sm text-zinc-800 dark:text-zinc-200 transition-all", onClick &&
12
+ (primaryTheme
13
+ ? "cursor-pointer hover:text-[var(--primary)]"
14
+ : "cursor-pointer"), itemClassName), children: [icon ? (_jsx("span", { className: "text-base", children: icon })) : (!isInline &&
15
+ !isOrdered && (_jsx("span", { style: !primaryTheme ? bulletStyle : undefined, className: clsx("w-2 h-2 rounded-full", primaryTheme && "bg-[var(--primary)]", bulletClassName) }))), _jsx("span", { children: text })] }), subItems && subItems.length > 0 && (_jsx("ul", { className: clsx("pl-5 mt-2 space-y-2", isOrdered ? "list-decimal" : "list-none"), children: subItems.map((sub, i) => (_jsx(ListItem, { ...sub, isInline: false, isOrdered: isOrdered, primaryTheme: primaryTheme, primaryColor: primaryColor, itemClassName: itemClassName, bulletClassName: bulletClassName }, i))) }))] }));
32
16
  };
33
17
  /* -------------------------------------------------------------------------- */
34
18
  /* 📋 List */
35
19
  /* -------------------------------------------------------------------------- */
36
- export const List = ({ title, titleIcon, items, type = "unordered", bulletColor = "#2563eb", textColor = "#111827", backgroundColor = "#fff", borderColor = "#e5e7eb", fontSize = "15px", fontWeight = 500, borderRadius = "12px", padding = "16px", spacing = "12px", className, style, }) => {
20
+ export const List = ({ title, titleIcon, items, type = "unordered", primaryTheme = true, primaryColor = "#3b82f6", className, itemClassName, titleClassName, bulletClassName, }) => {
37
21
  const isOrdered = type === "ordered";
38
22
  const isInline = type === "inline";
39
- const containerStyle = {
40
- backgroundColor,
41
- borderColor,
42
- color: textColor,
43
- borderWidth: borderColor ? "1px" : "0px",
44
- borderStyle: "solid",
45
- borderRadius,
46
- padding,
47
- ...style,
48
- };
49
- const listStyle = isInline
50
- ? {
51
- display: "flex",
52
- gap: spacing,
53
- paddingLeft: 0,
54
- listStyleType: "none",
55
- margin: 0,
56
- }
57
- : {
58
- listStyleType: isOrdered ? "decimal" : "none",
59
- paddingLeft: isOrdered ? "20px" : "0",
60
- margin: 0,
61
- };
62
23
  const ListTag = isOrdered ? "ol" : "ul";
63
- return (_jsxs("div", { className: className, style: containerStyle, children: [title && (_jsxs("div", { style: {
64
- display: "flex",
65
- alignItems: "center",
66
- fontSize: "17px",
67
- fontWeight: 600,
68
- marginBottom: "10px",
69
- gap: "8px",
70
- }, children: [titleIcon && _jsx("span", { style: { fontSize: "18px" }, children: titleIcon }), _jsx("span", { children: title })] })), _jsx(ListTag, { style: listStyle, children: items.map((item, index) => (_jsx(ListItem, { ...item, bulletColor: bulletColor, textColor: textColor, fontSize: fontSize, fontWeight: fontWeight, spacing: spacing, isInline: isInline }, index))) })] }));
24
+ return (_jsxs("div", { className: clsx("bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl p-4", className), children: [title && (_jsxs("div", { className: clsx("flex items-center gap-2 mb-3 text-base font-semibold text-zinc-900 dark:text-white", titleClassName), children: [titleIcon && _jsx("span", { children: titleIcon }), _jsx("span", { children: title })] })), _jsx(ListTag, { className: clsx(isInline
25
+ ? "flex flex-wrap gap-4"
26
+ : isOrdered
27
+ ? "list-decimal pl-5 space-y-2"
28
+ : "list-none p-0"), children: items.map((item, i) => (_jsx(ListItem, { ...item, isInline: isInline, isOrdered: isOrdered, primaryTheme: primaryTheme, primaryColor: primaryColor, itemClassName: itemClassName, bulletClassName: bulletClassName }, i))) })] }));
71
29
  };
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useCallback, } from "react";
2
+ import { useEffect, useRef, useCallback } from "react";
3
3
  import { X } from "lucide-react";
4
- export const Modal = ({ isOpen, onClose, children, ariaLabel, title, overlayStyle, modalStyle, closeButtonStyle, disableOverlayClose = false, transitionDuration = 200, className, darkMode = false, }) => {
4
+ import clsx from "clsx";
5
+ export const Modal = ({ isOpen, onClose, children, title, icon, ariaLabel, className, overlayClassName, contentClassName, closeButtonClassName, disableOverlayClose = false, darkMode = false, transitionDuration = 200, }) => {
5
6
  const modalRef = useRef(null);
6
- /* ------------------------------ Escape Close ----------------------------- */
7
+ /* Escape key close */
7
8
  useEffect(() => {
8
9
  const handleEsc = (e) => {
9
10
  if (e.key === "Escape")
@@ -13,7 +14,7 @@ export const Modal = ({ isOpen, onClose, children, ariaLabel, title, overlayStyl
13
14
  document.addEventListener("keydown", handleEsc);
14
15
  return () => document.removeEventListener("keydown", handleEsc);
15
16
  }, [isOpen, onClose]);
16
- /* ------------------------------ Scroll Lock ------------------------------ */
17
+ /* Scroll lock */
17
18
  useEffect(() => {
18
19
  if (isOpen) {
19
20
  const prev = document.body.style.overflow;
@@ -23,64 +24,11 @@ export const Modal = ({ isOpen, onClose, children, ariaLabel, title, overlayStyl
23
24
  };
24
25
  }
25
26
  }, [isOpen]);
26
- /* ------------------------------ Click Outside ---------------------------- */
27
27
  const handleOverlayClick = useCallback(() => {
28
28
  if (!disableOverlayClose)
29
29
  onClose();
30
30
  }, [disableOverlayClose, onClose]);
31
31
  if (!isOpen)
32
32
  return null;
33
- /* ------------------------------- Base Styles ----------------------------- */
34
- const baseOverlay = {
35
- position: "fixed",
36
- inset: 0,
37
- backgroundColor: darkMode ? "rgba(0,0,0,0.8)" : "rgba(0,0,0,0.6)",
38
- display: "flex",
39
- justifyContent: "center",
40
- alignItems: "center",
41
- zIndex: 1000,
42
- opacity: isOpen ? 1 : 0,
43
- transition: `opacity ${transitionDuration}ms ease`,
44
- ...overlayStyle,
45
- };
46
- const baseModal = {
47
- position: "relative",
48
- backgroundColor: darkMode ? "#1f1f1f" : "#fff",
49
- color: darkMode ? "#f5f5f5" : "#111",
50
- borderRadius: 12,
51
- width: "90vw",
52
- maxWidth: 700,
53
- maxHeight: "90vh",
54
- overflowY: "auto",
55
- padding: 24,
56
- boxShadow: darkMode
57
- ? "0 10px 40px rgba(0,0,0,0.7)"
58
- : "0 10px 40px rgba(0,0,0,0.25)",
59
- transform: isOpen ? "scale(1)" : "scale(0.95)",
60
- transition: `transform ${transitionDuration}ms ease, opacity ${transitionDuration}ms ease`,
61
- ...modalStyle,
62
- };
63
- const baseCloseBtn = {
64
- position: "absolute",
65
- top: 16,
66
- right: 16,
67
- background: "transparent",
68
- border: "none",
69
- cursor: "pointer",
70
- padding: 4,
71
- color: darkMode ? "#f5f5f5" : "#444",
72
- transition: "color 0.2s ease, transform 0.2s ease",
73
- ...closeButtonStyle,
74
- };
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) => {
76
- e.currentTarget.style.color = darkMode ? "#fff" : "#000";
77
- e.currentTarget.style.transform = "scale(1.1)";
78
- }, onMouseLeave: (e) => {
79
- e.currentTarget.style.color = darkMode ? "#f5f5f5" : "#444";
80
- e.currentTarget.style.transform = "scale(1)";
81
- }, children: _jsx(X, { size: 24 }) }), title && (_jsx("h2", { style: {
82
- fontSize: "1.25rem",
83
- fontWeight: 600,
84
- marginBottom: "1rem",
85
- }, children: title })), children] }) }));
33
+ return (_jsx("div", { role: "dialog", "aria-modal": "true", "aria-label": ariaLabel || title || "Modal", className: clsx("fixed inset-0 flex items-center justify-center z-50 transition-opacity", darkMode ? "bg-black/80" : "bg-black/60", overlayClassName), style: { transitionDuration: `${transitionDuration}ms` }, onClick: handleOverlayClick, children: _jsxs("div", { ref: modalRef, className: clsx("relative w-[90vw] max-w-2xl max-h-[90vh] overflow-y-auto p-6 rounded-lg shadow-lg transition-transform", darkMode ? "bg-zinc-900 text-white" : "bg-white text-gray-900", "scale-100", contentClassName), style: { transitionDuration: `${transitionDuration}ms` }, onClick: (e) => e.stopPropagation(), children: [_jsx("button", { onClick: onClose, "aria-label": "Close modal", className: clsx("absolute top-4 right-4 p-1 rounded-full hover:scale-110 transition-transform", darkMode ? "text-white hover:text-gray-200" : "text-gray-700 hover:text-gray-900", closeButtonClassName), children: _jsx(X, { size: 24 }) }), title && (_jsxs("div", { className: "flex items-center gap-2 mb-4", children: [icon && _jsx("span", { className: "flex-shrink-0", children: icon }), _jsx("h2", { className: "text-xl font-semibold", children: title })] })), children] }) }));
86
34
  };
@@ -1,32 +1,49 @@
1
1
  "use client";
2
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) => {
3
+ import { useState, useEffect, useRef } from "react";
4
+ import clsx from "clsx";
5
+ 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, darkMode = false, }) => {
6
+ const containerRef = useRef(null);
7
+ const [focusedIndex, setFocusedIndex] = useState(null);
8
+ // Keyboard navigation
9
+ useEffect(() => {
10
+ const container = containerRef.current;
11
+ if (!container)
12
+ return;
13
+ const handleKeyDown = (e) => {
14
+ if (disabled)
15
+ return;
16
+ const currentIndex = focusedIndex ?? options.findIndex((o) => o.value === selectedValue) ?? 0;
17
+ if (e.key === "ArrowDown" || e.key === "ArrowRight") {
18
+ e.preventDefault();
19
+ const nextIndex = (currentIndex + 1) % options.length;
20
+ onChange && onChange(options[nextIndex].value);
21
+ setFocusedIndex(nextIndex);
22
+ }
23
+ if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
24
+ e.preventDefault();
25
+ const prevIndex = (currentIndex - 1 + options.length) % options.length;
26
+ onChange && onChange(options[prevIndex].value);
27
+ setFocusedIndex(prevIndex);
28
+ }
29
+ };
30
+ container.addEventListener("keydown", handleKeyDown);
31
+ return () => container.removeEventListener("keydown", handleKeyDown);
32
+ }, [focusedIndex, options, selectedValue, onChange, disabled]);
33
+ return (_jsxs("div", { ref: containerRef, role: "radiogroup", "aria-disabled": disabled, "aria-invalid": !!error, className: clsx("flex flex-col gap-2", className), style: { ...style }, tabIndex: 0, children: [options.map((option, index) => {
5
34
  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",
35
+ const isFocused = focusedIndex === index;
36
+ return (_jsxs("label", { className: clsx("flex items-center justify-between cursor-pointer select-none gap-2", disabled ? "opacity-50 cursor-not-allowed" : "opacity-100", isFocused ? "ring-2 ring-blue-400" : ""), style: { ...labelStyle }, 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", { className: clsx("inline-flex items-center justify-center rounded-full transition-all"), style: {
19
37
  width: iconSize,
20
38
  height: iconSize,
21
- borderRadius: "50%",
22
39
  border: `2px solid ${isChecked ? iconCheckedBgColor : iconUncheckedBorderColor}`,
23
40
  backgroundColor: isChecked ? iconCheckedBgColor : "transparent",
24
- transition: "all 0.25s ease",
41
+ transition: "all 0.2s ease",
25
42
  }, children: isChecked && (_jsx("span", { style: {
26
43
  width: iconSize / 2,
27
44
  height: iconSize / 2,
28
45
  borderRadius: "50%",
29
- backgroundColor: "white",
46
+ backgroundColor: darkMode ? "#fff" : "#fff",
30
47
  } })) })] }, option.value));
31
48
  }), error && (_jsx("p", { role: "alert", style: {
32
49
  color: "#dc2626",
@@ -1,75 +1,22 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState, useEffect, useMemo } from "react";
3
- const getScreenSize = (width) => {
4
- if (width < 768)
5
- return "sm";
6
- if (width < 1024)
7
- return "md";
8
- return "lg";
2
+ import clsx from "clsx";
3
+ /**
4
+ * Convert responsive prop to Tailwind class string
5
+ */
6
+ const toResponsiveClass = (prefix, value) => {
7
+ if (value == null)
8
+ return "";
9
+ if (typeof value !== "object")
10
+ return `${prefix}-${value}`;
11
+ return Object.entries(value)
12
+ .map(([bp, val]) => `${bp}:${prefix}-${val}`)
13
+ .join(" ");
9
14
  };
10
- const resolveResponsive = (prop, screen, fallback) => {
11
- if (prop == null)
12
- return fallback;
13
- if (typeof prop !== "object")
14
- return prop;
15
- return prop[screen] ?? fallback;
16
- };
17
- export const Stack = ({ direction = { sm: "vertical", md: "horizontal", lg: "horizontal" }, gap = 12, align = "center", justify = "flex-start", wrap = "nowrap", padding, margin, width = "100%", maxWidth, height = "auto", backgroundColor = "transparent", borderRadius, border, boxShadow, overflow, className, style, children, }) => {
18
- const [screen, setScreen] = useState("lg");
19
- useEffect(() => {
20
- const updateScreen = () => setScreen(getScreenSize(window.innerWidth));
21
- updateScreen();
22
- window.addEventListener("resize", updateScreen);
23
- return () => window.removeEventListener("resize", updateScreen);
24
- }, []);
25
- const computedStyle = useMemo(() => {
26
- const toCssValue = (val) => typeof val === "number" ? `${val}px` : val;
27
- const resolvedDir = resolveResponsive(direction, screen, "vertical");
28
- const flexDir = resolvedDir === "vertical" ? "column" : "row";
29
- return {
30
- display: "flex",
31
- flexDirection: flexDir,
32
- alignItems: resolveResponsive(align, screen, "center"),
33
- justifyContent: resolveResponsive(justify, screen, "flex-start"),
34
- flexWrap: resolveResponsive(wrap, screen, "nowrap"),
35
- gap: toCssValue(resolveResponsive(gap, screen, 12)),
36
- padding: toCssValue(resolveResponsive(padding, screen, undefined)),
37
- margin: toCssValue(resolveResponsive(margin, screen, undefined)),
38
- width: resolveResponsive(width, screen, "100%"),
39
- maxWidth: resolveResponsive(maxWidth, screen, undefined),
40
- height: resolveResponsive(height, screen, undefined),
41
- backgroundColor: resolveResponsive(backgroundColor, screen, undefined),
42
- borderRadius: resolveResponsive(borderRadius, screen, undefined),
43
- border: resolveResponsive(border, screen, undefined),
44
- boxShadow: resolveResponsive(boxShadow, screen, undefined),
45
- overflow: resolveResponsive(overflow, screen, undefined),
46
- boxSizing: "border-box",
47
- ...style,
48
- };
49
- }, [
50
- direction,
51
- gap,
52
- align,
53
- justify,
54
- wrap,
55
- padding,
56
- margin,
57
- width,
58
- maxWidth,
59
- height,
60
- backgroundColor,
61
- borderRadius,
62
- border,
63
- boxShadow,
64
- overflow,
65
- style,
66
- screen,
67
- ]);
68
- return (_jsx("div", { className: className, style: computedStyle, children: children }));
69
- };
70
- export const HStack = (props) => {
71
- return _jsx(Stack, { direction: "horizontal", ...props });
72
- };
73
- export const VStack = (props) => {
74
- return _jsx(Stack, { direction: "vertical", ...props });
15
+ export const Stack = ({ direction = { sm: "vertical", md: "horizontal" }, gap = 4, align = "center", justify = "start", wrap = "nowrap", className, children, }) => {
16
+ const classes = clsx("flex", toResponsiveClass("flex", direction), // flex-col or flex-row
17
+ toResponsiveClass("gap", gap), align !== "stretch" ? `items-${align}` : "items-stretch", justify !== "start" ? `justify-${justify}` : "justify-start", wrap !== "nowrap" ? `flex-${wrap}` : "flex-nowrap", className);
18
+ return _jsx("div", { className: classes, children: children });
75
19
  };
20
+ /** Shortcuts for horizontal & vertical stacks */
21
+ export const HStack = (props) => (_jsx(Stack, { direction: "horizontal", ...props }));
22
+ export const VStack = (props) => (_jsx(Stack, { direction: "vertical", ...props }));
@@ -1,32 +1,58 @@
1
1
  "use client";
2
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, }) => {
3
+ import { useState, useEffect, useRef } from "react";
4
+ import clsx from "clsx";
5
+ 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, darkMode = false, }) => {
6
+ const containerRef = useRef(null);
7
+ const [focusedIndex, setFocusedIndex] = useState(null);
4
8
  const handleChange = (value) => {
5
- if (!onChange)
9
+ if (!onChange || disabled || readOnly)
6
10
  return;
7
11
  const updatedValues = selectedValues.includes(value)
8
12
  ? selectedValues.filter((v) => v !== value)
9
13
  : [...selectedValues, value];
10
14
  onChange(updatedValues);
11
15
  };
12
- return (_jsxs("div", { className: className, style: { display: "flex", flexDirection: "column", gap: 8, ...style }, role: "group", "aria-disabled": disabled, children: [options.map((option) => {
16
+ // Keyboard navigation
17
+ useEffect(() => {
18
+ const container = containerRef.current;
19
+ if (!container)
20
+ return;
21
+ const handleKeyDown = (e) => {
22
+ if (disabled)
23
+ return;
24
+ if (focusedIndex === null)
25
+ return;
26
+ const currentIndex = focusedIndex;
27
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") {
28
+ e.preventDefault();
29
+ const nextIndex = (currentIndex + 1) % options.length;
30
+ setFocusedIndex(nextIndex);
31
+ }
32
+ if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
33
+ e.preventDefault();
34
+ const prevIndex = (currentIndex - 1 + options.length) % options.length;
35
+ setFocusedIndex(prevIndex);
36
+ }
37
+ if (e.key === " " || e.key === "Enter") {
38
+ e.preventDefault();
39
+ handleChange(options[currentIndex].value);
40
+ }
41
+ };
42
+ container.addEventListener("keydown", handleKeyDown);
43
+ return () => container.removeEventListener("keydown", handleKeyDown);
44
+ }, [focusedIndex, options, selectedValues, disabled]);
45
+ return (_jsxs("div", { ref: containerRef, role: "group", "aria-disabled": disabled, "aria-invalid": !!error, tabIndex: 0, className: clsx("flex flex-col gap-2", className), style: { ...style }, children: [options.map((option, index) => {
13
46
  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",
47
+ const isFocused = focusedIndex === index;
48
+ return (_jsxs("label", { className: clsx("flex items-center justify-between cursor-pointer select-none transition-opacity", disabled ? "opacity-50 cursor-not-allowed" : "opacity-100", isFocused ? "ring-2 ring-blue-400" : ""), style: { ...labelStyle }, onFocus: () => setFocusedIndex(index), 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", { className: clsx("relative inline-flex rounded-full transition-colors"), style: {
25
49
  width: iconSize * 2,
26
50
  height: iconSize * 1.1,
27
- borderRadius: 9999,
28
- backgroundColor: isChecked ? iconCheckedBgColor : switchBgColor,
51
+ backgroundColor: isChecked
52
+ ? iconCheckedBgColor
53
+ : switchBgColor,
29
54
  transition: "background-color 0.25s ease",
55
+ borderRadius: 9999,
30
56
  }, children: _jsx("span", { style: {
31
57
  position: "absolute",
32
58
  top: "50%",