@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.
- package/dist/components/basic/Accordation.d.ts +9 -13
- package/dist/components/basic/Alert.d.ts +10 -23
- package/dist/components/basic/Avatar.d.ts +7 -16
- package/dist/components/basic/Badge.d.ts +9 -14
- package/dist/components/basic/Button.d.ts +9 -19
- package/dist/components/basic/CheckboxGroup.d.ts +1 -0
- package/dist/components/basic/Container.d.ts +2 -19
- package/dist/components/basic/Drawer.d.ts +7 -18
- package/dist/components/basic/DropDown.d.ts +20 -40
- package/dist/components/basic/FlexView.d.ts +16 -0
- package/dist/components/basic/GridView.d.ts +4 -9
- package/dist/components/basic/Image.d.ts +10 -31
- package/dist/components/basic/Input.d.ts +22 -35
- package/dist/components/basic/List.d.ts +6 -20
- package/dist/components/basic/Modal.d.ts +8 -8
- package/dist/components/basic/RadioGroup.d.ts +1 -0
- package/dist/components/basic/Stack.d.ts +4 -14
- package/dist/components/basic/SwitchGroup.d.ts +1 -0
- package/dist/components/basic/Table.d.ts +6 -1
- package/dist/components/basic/Text.d.ts +1109 -14
- package/dist/index.cjs.js +82 -52
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.es.js +2004 -2914
- package/dist/index.es.js.map +1 -1
- package/dist/src/components/basic/Accordation.js +25 -26
- package/dist/src/components/basic/Alert.js +33 -138
- package/dist/src/components/basic/AudioPlayer.js +54 -40
- package/dist/src/components/basic/Avatar.js +41 -154
- package/dist/src/components/basic/Badge.js +23 -62
- package/dist/src/components/basic/Button.js +24 -97
- package/dist/src/components/basic/CheckboxGroup.js +36 -13
- package/dist/src/components/basic/Container.js +19 -38
- package/dist/src/components/basic/Drawer.js +22 -73
- package/dist/src/components/basic/DropDown.js +94 -158
- package/dist/src/components/basic/FlexView.js +19 -0
- package/dist/src/components/basic/GridView.js +15 -48
- package/dist/src/components/basic/Image.js +39 -79
- package/dist/src/components/basic/Input.js +68 -109
- package/dist/src/components/basic/List.js +20 -62
- package/dist/src/components/basic/Modal.js +6 -58
- package/dist/src/components/basic/RadioGroup.js +35 -18
- package/dist/src/components/basic/Stack.js +19 -72
- package/dist/src/components/basic/SwitchGroup.js +42 -16
- package/dist/src/components/basic/Table.js +15 -36
- package/dist/src/components/basic/Tabs.js +3 -12
- package/dist/src/components/basic/Text.js +20 -112
- package/dist/src/index.js +3 -5
- package/dist/types/src/components/basic/Accordation.d.ts +9 -13
- package/dist/types/src/components/basic/Alert.d.ts +10 -23
- package/dist/types/src/components/basic/Avatar.d.ts +7 -16
- package/dist/types/src/components/basic/Badge.d.ts +9 -14
- package/dist/types/src/components/basic/Button.d.ts +9 -19
- package/dist/types/src/components/basic/CheckboxGroup.d.ts +1 -0
- package/dist/types/src/components/basic/Container.d.ts +2 -19
- package/dist/types/src/components/basic/Drawer.d.ts +7 -18
- package/dist/types/src/components/basic/DropDown.d.ts +20 -40
- package/dist/types/src/components/basic/FlexView.d.ts +16 -0
- package/dist/types/src/components/basic/GridView.d.ts +4 -9
- package/dist/types/src/components/basic/Image.d.ts +10 -31
- package/dist/types/src/components/basic/Input.d.ts +22 -35
- package/dist/types/src/components/basic/List.d.ts +6 -20
- package/dist/types/src/components/basic/Modal.d.ts +8 -8
- package/dist/types/src/components/basic/RadioGroup.d.ts +1 -0
- package/dist/types/src/components/basic/Stack.d.ts +4 -14
- package/dist/types/src/components/basic/SwitchGroup.d.ts +1 -0
- package/dist/types/src/components/basic/Table.d.ts +6 -1
- package/dist/types/src/components/basic/Text.d.ts +1109 -14
- package/dist/types/src/index.d.ts +3 -4
- package/dist/ui.css +1 -1
- package/package.json +2 -1
- package/dist/components/basic/Card.d.ts +0 -28
- package/dist/components/basic/Flexbox.d.ts +0 -25
- package/dist/components/basic/Section.d.ts +0 -36
- package/dist/src/components/basic/Card.js +0 -47
- package/dist/src/components/basic/Flexbox.js +0 -67
- package/dist/src/components/basic/Section.js +0 -100
- package/dist/types/src/components/basic/Card.d.ts +0 -28
- package/dist/types/src/components/basic/Flexbox.d.ts +0 -25
- 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 {
|
|
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 {
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
el.style.borderColor = color;
|
|
20
|
+
const handleKeyDown = (e) => {
|
|
21
|
+
if (type === "number" && e.key === "-")
|
|
22
|
+
e.preventDefault();
|
|
68
23
|
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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",
|
|
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:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
2
|
+
import { useEffect, useRef, useCallback } from "react";
|
|
3
3
|
import { X } from "lucide-react";
|
|
4
|
-
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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.
|
|
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: "
|
|
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
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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%",
|