@optigrit/optigrit-ui 0.0.1
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/bin/cli.js +99 -0
- package/dist/Input-CL3wQvR_.d.ts +40 -0
- package/dist/ThemeProvider-D_1vZWeu.d.ts +17 -0
- package/dist/chunk-2LJSVQ6B.js +465 -0
- package/dist/chunk-KBOYMK4Y.js +76 -0
- package/dist/chunk-MCQS3QNN.js +8 -0
- package/dist/chunk-U65NGM6F.js +48 -0
- package/dist/components/index.d.ts +69 -0
- package/dist/components/index.js +575 -0
- package/dist/core/index.d.ts +41 -0
- package/dist/core/index.js +14 -0
- package/dist/hooks/index.d.ts +31 -0
- package/dist/hooks/index.js +12 -0
- package/dist/theme/index.d.ts +7 -0
- package/dist/theme/index.js +12 -0
- package/index.css +131 -0
- package/package.json +91 -0
- package/tailwind.preset.js +87 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// src/theme/utils.ts
|
|
2
|
+
function colorMix(primaryColor = "", colorRatio = 100, secondaryColor = "transparent", secondaryColorRatio = 100) {
|
|
3
|
+
return `color-mix(in srgb, ${primaryColor} ${colorRatio}%, ${secondaryColor} ${secondaryColorRatio}%)`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
colorMix
|
|
8
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/theme/ThemeProvider.tsx
|
|
2
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
var initialState = {
|
|
5
|
+
theme: "system",
|
|
6
|
+
setTheme: () => null
|
|
7
|
+
};
|
|
8
|
+
var ThemeProviderContext = createContext(initialState);
|
|
9
|
+
function ThemeProvider({
|
|
10
|
+
children,
|
|
11
|
+
defaultTheme = "system",
|
|
12
|
+
storageKey = "optigrit-ui-theme",
|
|
13
|
+
...props
|
|
14
|
+
}) {
|
|
15
|
+
const [theme, setTheme] = useState(
|
|
16
|
+
() => localStorage.getItem(storageKey) || defaultTheme
|
|
17
|
+
);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const root = window.document.documentElement;
|
|
20
|
+
root.classList.remove("light", "dark");
|
|
21
|
+
if (theme === "system") {
|
|
22
|
+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
23
|
+
root.classList.add(systemTheme);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
root.classList.add(theme);
|
|
27
|
+
}, [theme]);
|
|
28
|
+
const value = {
|
|
29
|
+
theme,
|
|
30
|
+
setTheme: (theme2) => {
|
|
31
|
+
localStorage.setItem(storageKey, theme2);
|
|
32
|
+
setTheme(theme2);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const Provider = ThemeProviderContext.Provider;
|
|
36
|
+
return /* @__PURE__ */ jsx(Provider, { ...props, value, children });
|
|
37
|
+
}
|
|
38
|
+
var useTheme = () => {
|
|
39
|
+
const context = useContext(ThemeProviderContext);
|
|
40
|
+
if (context === void 0)
|
|
41
|
+
throw new Error("useTheme must be used within a ThemeProvider");
|
|
42
|
+
return context;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
ThemeProvider,
|
|
47
|
+
useTheme
|
|
48
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ButtonHTMLAttributes, ReactNode, HTMLAttributes, RefObject } from 'react';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
import { P as PopoverProps, a as InputProps } from '../Input-CL3wQvR_.js';
|
|
5
|
+
|
|
6
|
+
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "destructive" | "link";
|
|
7
|
+
type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl";
|
|
8
|
+
type ButtonRoundness = "none" | "sm" | "md" | "lg" | "full";
|
|
9
|
+
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
10
|
+
variant?: ButtonVariant;
|
|
11
|
+
size?: ButtonSize;
|
|
12
|
+
roundness?: ButtonRoundness;
|
|
13
|
+
fullWidth?: boolean;
|
|
14
|
+
loading?: boolean;
|
|
15
|
+
leftIcon?: ReactNode;
|
|
16
|
+
rightIcon?: ReactNode;
|
|
17
|
+
iconOnly?: boolean;
|
|
18
|
+
disableRipple?: boolean;
|
|
19
|
+
}
|
|
20
|
+
declare const Button: react.ForwardRefExoticComponent<ButtonProps & react.RefAttributes<HTMLButtonElement>>;
|
|
21
|
+
|
|
22
|
+
type SIZE = "xs" | "sm" | "md" | "lg" | "xl";
|
|
23
|
+
|
|
24
|
+
type TooltipProps = HTMLAttributes<HTMLDivElement> & {
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
label: ReactNode;
|
|
27
|
+
labelPosition?: PopoverProps['position'];
|
|
28
|
+
size?: SIZE | 'none';
|
|
29
|
+
labelContainerProps?: HTMLAttributes<HTMLDivElement>;
|
|
30
|
+
popoverProps?: Omit<PopoverProps, 'targetRef' | 'open' | 'onClose' | 'position' | 'children'>;
|
|
31
|
+
};
|
|
32
|
+
declare function Tooltip(props: TooltipProps): react_jsx_runtime.JSX.Element;
|
|
33
|
+
|
|
34
|
+
type InputFieldProps = {
|
|
35
|
+
value?: string;
|
|
36
|
+
errorColor?: string;
|
|
37
|
+
validateValue?: (value: string) => string | null;
|
|
38
|
+
onChangeValue?: (value: string) => void;
|
|
39
|
+
containerProps?: HTMLAttributes<HTMLDivElement> & {
|
|
40
|
+
ref: RefObject<HTMLDivElement | null>;
|
|
41
|
+
};
|
|
42
|
+
} & Omit<InputProps, 'value'>;
|
|
43
|
+
declare function InputField(props: InputFieldProps): react_jsx_runtime.JSX.Element;
|
|
44
|
+
|
|
45
|
+
type OPTION$1 = {
|
|
46
|
+
label: string;
|
|
47
|
+
value: string;
|
|
48
|
+
};
|
|
49
|
+
type SelectProps = {
|
|
50
|
+
options: OPTION$1[];
|
|
51
|
+
optionsPosition?: PopoverProps['position'];
|
|
52
|
+
renderOption?: (option: OPTION$1, isActive: boolean) => React.ReactNode;
|
|
53
|
+
} & Omit<InputFieldProps, 'readOnly' | 'onChange'>;
|
|
54
|
+
declare function Select(props: SelectProps): react_jsx_runtime.JSX.Element;
|
|
55
|
+
|
|
56
|
+
type OPTION = {
|
|
57
|
+
label: string;
|
|
58
|
+
value: string;
|
|
59
|
+
};
|
|
60
|
+
type MultiSelectProps = {
|
|
61
|
+
options: OPTION[];
|
|
62
|
+
value?: string[];
|
|
63
|
+
onChangeValue?: (value: string[]) => void;
|
|
64
|
+
optionsPosition?: PopoverProps['position'];
|
|
65
|
+
renderOption?: (option: OPTION, isActive: boolean) => React.ReactNode;
|
|
66
|
+
} & Omit<InputFieldProps, 'readOnly' | 'value' | 'onChange' | 'onChangeValue'>;
|
|
67
|
+
declare function MultiSelect(props: MultiSelectProps): react_jsx_runtime.JSX.Element;
|
|
68
|
+
|
|
69
|
+
export { Button, type ButtonProps, type ButtonRoundness, type ButtonSize, type ButtonVariant, InputField, type InputFieldProps, MultiSelect, type MultiSelectProps, Select, type SelectProps, Tooltip, type TooltipProps };
|
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Input,
|
|
3
|
+
Popover,
|
|
4
|
+
cn
|
|
5
|
+
} from "../chunk-2LJSVQ6B.js";
|
|
6
|
+
import "../chunk-MCQS3QNN.js";
|
|
7
|
+
import {
|
|
8
|
+
useKeyboardShortcuts
|
|
9
|
+
} from "../chunk-KBOYMK4Y.js";
|
|
10
|
+
import "../chunk-U65NGM6F.js";
|
|
11
|
+
|
|
12
|
+
// src/components/Buttons/Button/index.tsx
|
|
13
|
+
import {
|
|
14
|
+
forwardRef,
|
|
15
|
+
useCallback
|
|
16
|
+
} from "react";
|
|
17
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
18
|
+
var variantClasses = {
|
|
19
|
+
primary: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
20
|
+
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
21
|
+
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
22
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
23
|
+
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
24
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
25
|
+
};
|
|
26
|
+
var sizeClasses = {
|
|
27
|
+
xs: "text-xs px-2 py-1 gap-1",
|
|
28
|
+
sm: "text-sm px-3 py-1.5 gap-1.5",
|
|
29
|
+
md: "text-sm px-4 py-2 gap-2",
|
|
30
|
+
lg: "text-base px-5 py-2.5 gap-2.5",
|
|
31
|
+
xl: "text-lg px-6 py-3 gap-3"
|
|
32
|
+
};
|
|
33
|
+
var iconOnlySizeClasses = {
|
|
34
|
+
xs: "text-xs p-1",
|
|
35
|
+
sm: "text-sm p-1.5",
|
|
36
|
+
md: "text-sm p-2",
|
|
37
|
+
lg: "text-base p-2.5",
|
|
38
|
+
xl: "text-lg p-3"
|
|
39
|
+
};
|
|
40
|
+
var roundnessClasses = {
|
|
41
|
+
none: "rounded-none",
|
|
42
|
+
sm: "rounded-sm",
|
|
43
|
+
md: "rounded-md",
|
|
44
|
+
lg: "rounded-lg",
|
|
45
|
+
full: "rounded-full"
|
|
46
|
+
};
|
|
47
|
+
function Spinner({ className = "" }) {
|
|
48
|
+
return /* @__PURE__ */ jsxs(
|
|
49
|
+
"svg",
|
|
50
|
+
{
|
|
51
|
+
className: `animate-spin ${className}`,
|
|
52
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
53
|
+
fill: "none",
|
|
54
|
+
viewBox: "0 0 24 24",
|
|
55
|
+
"aria-hidden": "true",
|
|
56
|
+
children: [
|
|
57
|
+
/* @__PURE__ */ jsx(
|
|
58
|
+
"circle",
|
|
59
|
+
{
|
|
60
|
+
className: "opacity-25",
|
|
61
|
+
cx: "12",
|
|
62
|
+
cy: "12",
|
|
63
|
+
r: "10",
|
|
64
|
+
stroke: "currentColor",
|
|
65
|
+
strokeWidth: "4"
|
|
66
|
+
}
|
|
67
|
+
),
|
|
68
|
+
/* @__PURE__ */ jsx(
|
|
69
|
+
"path",
|
|
70
|
+
{
|
|
71
|
+
className: "opacity-75",
|
|
72
|
+
fill: "currentColor",
|
|
73
|
+
d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
function cn2(...classes) {
|
|
81
|
+
return classes.filter(Boolean).join(" ");
|
|
82
|
+
}
|
|
83
|
+
var rippleColor = {
|
|
84
|
+
primary: "rgba(255, 255, 255, 0.45)",
|
|
85
|
+
secondary: "rgba(0, 0, 0, 0.12)",
|
|
86
|
+
outline: "rgba(0, 0, 0, 0.10)",
|
|
87
|
+
ghost: "rgba(0, 0, 0, 0.10)",
|
|
88
|
+
destructive: "rgba(255, 255, 255, 0.45)",
|
|
89
|
+
link: "rgba(0, 0, 0, 0.10)"
|
|
90
|
+
};
|
|
91
|
+
var Button = forwardRef(
|
|
92
|
+
({
|
|
93
|
+
variant = "primary",
|
|
94
|
+
size = "md",
|
|
95
|
+
roundness = "md",
|
|
96
|
+
fullWidth = false,
|
|
97
|
+
loading = false,
|
|
98
|
+
leftIcon,
|
|
99
|
+
rightIcon,
|
|
100
|
+
iconOnly = false,
|
|
101
|
+
disableRipple = false,
|
|
102
|
+
disabled,
|
|
103
|
+
className,
|
|
104
|
+
children,
|
|
105
|
+
type = "button",
|
|
106
|
+
onClick,
|
|
107
|
+
...rest
|
|
108
|
+
}, ref) => {
|
|
109
|
+
const isDisabled = disabled || loading;
|
|
110
|
+
const spinnerSize = {
|
|
111
|
+
xs: "h-3 w-3",
|
|
112
|
+
sm: "h-3.5 w-3.5",
|
|
113
|
+
md: "h-4 w-4",
|
|
114
|
+
lg: "h-5 w-5",
|
|
115
|
+
xl: "h-5 w-5"
|
|
116
|
+
};
|
|
117
|
+
const handleClick = useCallback(
|
|
118
|
+
(e) => {
|
|
119
|
+
if (!disableRipple) {
|
|
120
|
+
const button = e.currentTarget;
|
|
121
|
+
const rect = button.getBoundingClientRect();
|
|
122
|
+
const diameter = Math.max(button.clientWidth, button.clientHeight);
|
|
123
|
+
const radius = diameter / 2;
|
|
124
|
+
const circle = document.createElement("span");
|
|
125
|
+
circle.style.position = "absolute";
|
|
126
|
+
circle.style.width = `${diameter}px`;
|
|
127
|
+
circle.style.height = `${diameter}px`;
|
|
128
|
+
circle.style.left = `${e.clientX - rect.left - radius}px`;
|
|
129
|
+
circle.style.top = `${e.clientY - rect.top - radius}px`;
|
|
130
|
+
circle.style.borderRadius = "9999px";
|
|
131
|
+
circle.style.pointerEvents = "none";
|
|
132
|
+
circle.style.backgroundColor = rippleColor[variant];
|
|
133
|
+
button.appendChild(circle);
|
|
134
|
+
const anim = circle.animate(
|
|
135
|
+
[
|
|
136
|
+
{ transform: "scale(0)", opacity: "1" },
|
|
137
|
+
{ transform: "scale(4)", opacity: "0" }
|
|
138
|
+
],
|
|
139
|
+
{ duration: 600, easing: "linear" }
|
|
140
|
+
);
|
|
141
|
+
anim.onfinish = () => circle.remove();
|
|
142
|
+
}
|
|
143
|
+
onClick?.(e);
|
|
144
|
+
},
|
|
145
|
+
[disableRipple, variant, onClick]
|
|
146
|
+
);
|
|
147
|
+
return /* @__PURE__ */ jsxs(
|
|
148
|
+
"button",
|
|
149
|
+
{
|
|
150
|
+
ref,
|
|
151
|
+
type,
|
|
152
|
+
disabled: isDisabled,
|
|
153
|
+
"aria-busy": loading || void 0,
|
|
154
|
+
onClick: handleClick,
|
|
155
|
+
className: cn2(
|
|
156
|
+
"relative overflow-hidden inline-flex items-center justify-center font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
157
|
+
isDisabled && "pointer-events-none opacity-50",
|
|
158
|
+
variantClasses[variant],
|
|
159
|
+
iconOnly ? iconOnlySizeClasses[size] : sizeClasses[size],
|
|
160
|
+
roundnessClasses[roundness],
|
|
161
|
+
fullWidth && "w-full",
|
|
162
|
+
className
|
|
163
|
+
),
|
|
164
|
+
...rest,
|
|
165
|
+
children: [
|
|
166
|
+
loading && /* @__PURE__ */ jsx(Spinner, { className: spinnerSize[size] }),
|
|
167
|
+
!loading && leftIcon && /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: leftIcon }),
|
|
168
|
+
children,
|
|
169
|
+
!loading && rightIcon && /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", children: rightIcon })
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
Button.displayName = "Button";
|
|
176
|
+
var Button_default = Button;
|
|
177
|
+
|
|
178
|
+
// src/components/Feedback/Tooltip/Tooltip.tsx
|
|
179
|
+
import { useRef, useState } from "react";
|
|
180
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
181
|
+
function Tooltip(props) {
|
|
182
|
+
const {
|
|
183
|
+
label,
|
|
184
|
+
labelPosition = "bottom-center",
|
|
185
|
+
children,
|
|
186
|
+
className,
|
|
187
|
+
size = "sm",
|
|
188
|
+
labelContainerProps = {},
|
|
189
|
+
popoverProps = {},
|
|
190
|
+
...containerProps
|
|
191
|
+
} = props;
|
|
192
|
+
const container = useRef(null);
|
|
193
|
+
const [open, setOpen] = useState(false);
|
|
194
|
+
return /* @__PURE__ */ jsxs2(
|
|
195
|
+
"div",
|
|
196
|
+
{
|
|
197
|
+
...containerProps,
|
|
198
|
+
ref: container,
|
|
199
|
+
className: cn("cursor-pointer", className),
|
|
200
|
+
onMouseEnter: () => setOpen(true),
|
|
201
|
+
onMouseLeave: () => setOpen(false),
|
|
202
|
+
children: [
|
|
203
|
+
children,
|
|
204
|
+
/* @__PURE__ */ jsx2(
|
|
205
|
+
Popover,
|
|
206
|
+
{
|
|
207
|
+
...popoverProps,
|
|
208
|
+
open,
|
|
209
|
+
targetRef: container,
|
|
210
|
+
position: labelPosition,
|
|
211
|
+
className: cn("p-1", popoverProps.className),
|
|
212
|
+
children: /* @__PURE__ */ jsx2(
|
|
213
|
+
"div",
|
|
214
|
+
{
|
|
215
|
+
...labelContainerProps,
|
|
216
|
+
className: cn(
|
|
217
|
+
"bg-black text-white",
|
|
218
|
+
size !== "none" ? `text-${size} rounded-${size}` : "",
|
|
219
|
+
{
|
|
220
|
+
xs: "py-1 px-2",
|
|
221
|
+
sm: "py-2 px-4",
|
|
222
|
+
md: "py-3 px-6",
|
|
223
|
+
lg: "py-4 px-8",
|
|
224
|
+
xl: "py-5 px-10",
|
|
225
|
+
none: "py-0 px-0"
|
|
226
|
+
}[size],
|
|
227
|
+
labelContainerProps.className
|
|
228
|
+
),
|
|
229
|
+
children: label
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/components/Forms/InputField/InputField.tsx
|
|
240
|
+
import { useLayoutEffect, useRef as useRef2, useState as useState2 } from "react";
|
|
241
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
242
|
+
function InputField(props) {
|
|
243
|
+
const {
|
|
244
|
+
validateValue,
|
|
245
|
+
onChangeValue,
|
|
246
|
+
containerProps = {},
|
|
247
|
+
errorColor = "rgb(240, 70, 70)",
|
|
248
|
+
...inputProps
|
|
249
|
+
} = props;
|
|
250
|
+
const initRef = useRef2({ isUserTyped: false });
|
|
251
|
+
const [error, setError] = useState2("");
|
|
252
|
+
function handleError(value) {
|
|
253
|
+
if (!initRef.current.isUserTyped) return;
|
|
254
|
+
setError(() => {
|
|
255
|
+
const error2 = validateValue?.(value);
|
|
256
|
+
if (error2) return error2;
|
|
257
|
+
if (props.required && !value)
|
|
258
|
+
return "This field is required!";
|
|
259
|
+
return "";
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function handleOnChange(event) {
|
|
263
|
+
onChangeValue?.(event.target.value);
|
|
264
|
+
handleError(event.target.value);
|
|
265
|
+
props.onChange?.(event);
|
|
266
|
+
}
|
|
267
|
+
function handleOnBlur(event) {
|
|
268
|
+
initRef.current.isUserTyped = true;
|
|
269
|
+
handleError(event.target.value);
|
|
270
|
+
props.onBlur?.(event);
|
|
271
|
+
}
|
|
272
|
+
function handleOnFocus(event) {
|
|
273
|
+
props.onFocus?.(event);
|
|
274
|
+
}
|
|
275
|
+
useLayoutEffect(() => {
|
|
276
|
+
if (props.value) {
|
|
277
|
+
initRef.current.isUserTyped = true;
|
|
278
|
+
handleError(props.value);
|
|
279
|
+
}
|
|
280
|
+
}, [props.value]);
|
|
281
|
+
return /* @__PURE__ */ jsxs3("div", { ...containerProps, children: [
|
|
282
|
+
/* @__PURE__ */ jsx3(
|
|
283
|
+
Input,
|
|
284
|
+
{
|
|
285
|
+
...inputProps,
|
|
286
|
+
textColor: error ? errorColor : inputProps.textColor,
|
|
287
|
+
borderColor: error ? errorColor : inputProps.borderColor,
|
|
288
|
+
primaryColor: error ? errorColor : inputProps.primaryColor,
|
|
289
|
+
onChange: handleOnChange,
|
|
290
|
+
onFocus: handleOnFocus,
|
|
291
|
+
onBlur: handleOnBlur
|
|
292
|
+
}
|
|
293
|
+
),
|
|
294
|
+
/* @__PURE__ */ jsx3("p", { className: "pl-1 text-xs", style: { color: errorColor, display: error ? "block" : "none" }, children: error })
|
|
295
|
+
] });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/components/Forms/Select/index.tsx
|
|
299
|
+
import { Fragment, useRef as useRef3, useState as useState3 } from "react";
|
|
300
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
301
|
+
function Select(props) {
|
|
302
|
+
const {
|
|
303
|
+
options = [{ label: "None", value: "" }],
|
|
304
|
+
renderOption,
|
|
305
|
+
optionsPosition = "bottom-start",
|
|
306
|
+
...inputProps
|
|
307
|
+
} = props;
|
|
308
|
+
const container = useRef3(null);
|
|
309
|
+
const optionsContainer = useRef3(null);
|
|
310
|
+
const [isOpen, setIsOpen] = useState3(false);
|
|
311
|
+
const [activeOption, setActiveOption] = useState3("");
|
|
312
|
+
useKeyboardShortcuts([
|
|
313
|
+
{
|
|
314
|
+
key: "ArrowDown",
|
|
315
|
+
callback: () => handleKeyboardNavigation("ArrowDown"),
|
|
316
|
+
options: { preventDefault: isOpen }
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
key: "ArrowUp",
|
|
320
|
+
callback: () => handleKeyboardNavigation("ArrowUp"),
|
|
321
|
+
options: { preventDefault: isOpen }
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
key: "Enter",
|
|
325
|
+
callback: () => {
|
|
326
|
+
if (isOpen) {
|
|
327
|
+
const option = options.find((option2) => option2.value === activeOption);
|
|
328
|
+
if (option) handleOnChange(option);
|
|
329
|
+
setActiveOption("");
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
options: { preventDefault: isOpen }
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
key: "Escape",
|
|
336
|
+
callback: () => {
|
|
337
|
+
if (isOpen) {
|
|
338
|
+
setIsOpen(false);
|
|
339
|
+
setActiveOption("");
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
options: { preventDefault: isOpen }
|
|
343
|
+
}
|
|
344
|
+
]);
|
|
345
|
+
function handleKeyboardNavigation(key) {
|
|
346
|
+
if (isOpen) {
|
|
347
|
+
setActiveOption((pre) => {
|
|
348
|
+
const currentIndex = options.findIndex((option) => option.value === pre);
|
|
349
|
+
const nextIndex = (currentIndex + (key === "ArrowDown" ? 1 : -1) + options.length) % options.length;
|
|
350
|
+
const optionNode = optionsContainer.current?.querySelector(`#${options[nextIndex].value}`);
|
|
351
|
+
optionNode?.scrollIntoView({ block: "nearest" });
|
|
352
|
+
const value = options[nextIndex].value;
|
|
353
|
+
setActiveOption(value);
|
|
354
|
+
return value;
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function handleOnChange(options2) {
|
|
359
|
+
setIsOpen(false);
|
|
360
|
+
props.onChangeValue?.(options2.value);
|
|
361
|
+
setActiveOption("");
|
|
362
|
+
const input = container.current?.querySelector("input");
|
|
363
|
+
if (input) input.value = options2.label;
|
|
364
|
+
}
|
|
365
|
+
return /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
366
|
+
/* @__PURE__ */ jsx4(
|
|
367
|
+
InputField,
|
|
368
|
+
{
|
|
369
|
+
...inputProps,
|
|
370
|
+
readOnly: true,
|
|
371
|
+
value: options.find((option) => option.value === props.value)?.label || void 0,
|
|
372
|
+
containerProps: {
|
|
373
|
+
ref: container,
|
|
374
|
+
className: "[&_*]:cursor-pointer",
|
|
375
|
+
onClick: () => setIsOpen(true)
|
|
376
|
+
},
|
|
377
|
+
onBlur: (event) => {
|
|
378
|
+
setIsOpen(false);
|
|
379
|
+
inputProps.onBlur?.(event);
|
|
380
|
+
},
|
|
381
|
+
endIcon: props.endIcon ? props.endIcon : /* @__PURE__ */ jsx4("div", { className: "relative h-full aspect-square pr-1 flex items-center justify-center", children: /* @__PURE__ */ jsx4("div", { className: cn(
|
|
382
|
+
"w-0 h-0 border-l-[5px] border-r-[5px] border-t-[5px] border-l-transparent border-r-transparent border-t-gray-600",
|
|
383
|
+
isOpen ? "rotate-180" : ""
|
|
384
|
+
) }) })
|
|
385
|
+
}
|
|
386
|
+
),
|
|
387
|
+
/* @__PURE__ */ jsx4(
|
|
388
|
+
Popover,
|
|
389
|
+
{
|
|
390
|
+
open: isOpen,
|
|
391
|
+
targetRef: container,
|
|
392
|
+
position: optionsPosition,
|
|
393
|
+
className: "p-1 min-h-[100px]",
|
|
394
|
+
onClose: () => {
|
|
395
|
+
setIsOpen(false);
|
|
396
|
+
},
|
|
397
|
+
onOpen: (node) => {
|
|
398
|
+
node.style.minWidth = `${container.current?.offsetWidth ?? 0}px`;
|
|
399
|
+
},
|
|
400
|
+
children: /* @__PURE__ */ jsx4(
|
|
401
|
+
"div",
|
|
402
|
+
{
|
|
403
|
+
ref: optionsContainer,
|
|
404
|
+
className: "w-full max-h-[200px] overflow-y-scroll scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden bg-zinc-200 p-1 rounded-md flex flex-col gap-1",
|
|
405
|
+
onMouseDown: (event) => {
|
|
406
|
+
event.preventDefault();
|
|
407
|
+
},
|
|
408
|
+
children: options.map((option) => renderOption ? renderOption(option, activeOption === option.value) : /* @__PURE__ */ jsx4(
|
|
409
|
+
"div",
|
|
410
|
+
{
|
|
411
|
+
id: option.value,
|
|
412
|
+
onClick: () => handleOnChange(option),
|
|
413
|
+
className: cn(
|
|
414
|
+
"p-2 hover:bg-gray-100 rounded cursor-pointer w-full",
|
|
415
|
+
activeOption === option.value ? "border-2 border-blue-500" : "",
|
|
416
|
+
option.value === props.value ? "bg-white" : ""
|
|
417
|
+
),
|
|
418
|
+
children: option.label
|
|
419
|
+
},
|
|
420
|
+
option.value
|
|
421
|
+
))
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
] });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/components/Forms/MultiSelect/index.tsx
|
|
430
|
+
import { Fragment as Fragment2, useRef as useRef4, useState as useState4 } from "react";
|
|
431
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
432
|
+
function MultiSelect(props) {
|
|
433
|
+
const {
|
|
434
|
+
renderOption,
|
|
435
|
+
onChangeValue,
|
|
436
|
+
value = [],
|
|
437
|
+
options = [{ label: "None", value: "" }],
|
|
438
|
+
optionsPosition = "bottom-start",
|
|
439
|
+
...inputProps
|
|
440
|
+
} = props;
|
|
441
|
+
const labelMap = new Map(options.map((option) => [option.value, option.label]));
|
|
442
|
+
const container = useRef4(null);
|
|
443
|
+
const optionsContainer = useRef4(null);
|
|
444
|
+
const [isOpen, setIsOpen] = useState4(false);
|
|
445
|
+
const [activeOption, setActiveOption] = useState4("");
|
|
446
|
+
useKeyboardShortcuts([
|
|
447
|
+
{
|
|
448
|
+
key: "ArrowDown",
|
|
449
|
+
callback: () => handleKeyboardNavigation("ArrowDown"),
|
|
450
|
+
options: { preventDefault: isOpen }
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
key: "ArrowUp",
|
|
454
|
+
callback: () => handleKeyboardNavigation("ArrowUp"),
|
|
455
|
+
options: { preventDefault: isOpen }
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
key: "Enter",
|
|
459
|
+
callback: () => {
|
|
460
|
+
if (isOpen) {
|
|
461
|
+
const option = options?.find((option2) => option2.value === activeOption);
|
|
462
|
+
if (option) handleOnChange(option);
|
|
463
|
+
setActiveOption("");
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
options: { preventDefault: isOpen }
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
key: "Escape",
|
|
470
|
+
callback: () => {
|
|
471
|
+
if (isOpen) {
|
|
472
|
+
setIsOpen(false);
|
|
473
|
+
setActiveOption("");
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
options: { preventDefault: isOpen }
|
|
477
|
+
}
|
|
478
|
+
]);
|
|
479
|
+
function handleKeyboardNavigation(key) {
|
|
480
|
+
if (isOpen) {
|
|
481
|
+
setActiveOption((pre) => {
|
|
482
|
+
const currentIndex = options.findIndex((option) => option.value === pre);
|
|
483
|
+
const nextIndex = (currentIndex + (key === "ArrowDown" ? 1 : -1) + options.length) % options.length;
|
|
484
|
+
const optionNode = optionsContainer.current?.querySelector(`#${options[nextIndex].value}`);
|
|
485
|
+
optionNode?.scrollIntoView({ block: "nearest" });
|
|
486
|
+
const value2 = options[nextIndex].value;
|
|
487
|
+
setActiveOption(value2);
|
|
488
|
+
return value2;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function handleOnChange(option) {
|
|
493
|
+
setIsOpen(false);
|
|
494
|
+
const newValue = (() => {
|
|
495
|
+
if (props.value?.includes(option.value)) {
|
|
496
|
+
return props.value?.filter((val) => val !== option.value);
|
|
497
|
+
} else {
|
|
498
|
+
return [...props.value || [], option.value];
|
|
499
|
+
}
|
|
500
|
+
})();
|
|
501
|
+
onChangeValue?.(newValue);
|
|
502
|
+
setActiveOption("");
|
|
503
|
+
const input = container.current?.querySelector("input");
|
|
504
|
+
if (input) input.value = newValue.map((value2) => labelMap.get(value2)).join(", ") || "";
|
|
505
|
+
}
|
|
506
|
+
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
507
|
+
/* @__PURE__ */ jsx5(
|
|
508
|
+
InputField,
|
|
509
|
+
{
|
|
510
|
+
...inputProps,
|
|
511
|
+
readOnly: true,
|
|
512
|
+
value: props.value?.map((value2) => labelMap.get(value2)).join(", ") || void 0,
|
|
513
|
+
containerProps: {
|
|
514
|
+
ref: container,
|
|
515
|
+
className: "[&_*]:cursor-pointer",
|
|
516
|
+
onClick: () => setIsOpen(true)
|
|
517
|
+
},
|
|
518
|
+
onBlur: (event) => {
|
|
519
|
+
setIsOpen(false);
|
|
520
|
+
inputProps.onBlur?.(event);
|
|
521
|
+
},
|
|
522
|
+
endIcon: props.endIcon ? props.endIcon : /* @__PURE__ */ jsx5("div", { className: "relative h-full aspect-square pr-1 flex items-center justify-center", children: /* @__PURE__ */ jsx5("div", { className: cn(
|
|
523
|
+
"w-0 h-0 border-l-[5px] border-r-[5px] border-t-[5px] border-l-transparent border-r-transparent border-t-gray-600",
|
|
524
|
+
isOpen ? "rotate-180" : ""
|
|
525
|
+
) }) })
|
|
526
|
+
}
|
|
527
|
+
),
|
|
528
|
+
/* @__PURE__ */ jsx5(
|
|
529
|
+
Popover,
|
|
530
|
+
{
|
|
531
|
+
open: isOpen,
|
|
532
|
+
targetRef: container,
|
|
533
|
+
position: optionsPosition,
|
|
534
|
+
className: "p-1 min-h-[100px]",
|
|
535
|
+
onClose: () => {
|
|
536
|
+
setIsOpen(false);
|
|
537
|
+
},
|
|
538
|
+
onOpen: (node) => {
|
|
539
|
+
node.style.minWidth = `${container.current?.offsetWidth ?? 0}px`;
|
|
540
|
+
},
|
|
541
|
+
children: /* @__PURE__ */ jsx5(
|
|
542
|
+
"div",
|
|
543
|
+
{
|
|
544
|
+
ref: optionsContainer,
|
|
545
|
+
className: "w-full max-h-[200px] overflow-y-scroll scrollbar-hide [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden bg-zinc-200 p-1 rounded-md flex flex-col gap-1",
|
|
546
|
+
onMouseDown: (event) => {
|
|
547
|
+
event.preventDefault();
|
|
548
|
+
},
|
|
549
|
+
children: options.map((option) => renderOption ? renderOption(option, activeOption === option.value) : /* @__PURE__ */ jsx5(
|
|
550
|
+
"div",
|
|
551
|
+
{
|
|
552
|
+
id: option.value,
|
|
553
|
+
onClick: () => handleOnChange(option),
|
|
554
|
+
className: cn(
|
|
555
|
+
"p-2 hover:bg-gray-100 rounded cursor-pointer w-full",
|
|
556
|
+
activeOption === option.value ? "border-2 border-blue-500" : "",
|
|
557
|
+
props.value?.includes(option.value) ? "bg-white" : ""
|
|
558
|
+
),
|
|
559
|
+
children: option.label
|
|
560
|
+
},
|
|
561
|
+
option.value
|
|
562
|
+
))
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
] });
|
|
568
|
+
}
|
|
569
|
+
export {
|
|
570
|
+
Button_default as Button,
|
|
571
|
+
InputField,
|
|
572
|
+
MultiSelect,
|
|
573
|
+
Select,
|
|
574
|
+
Tooltip
|
|
575
|
+
};
|