@alfatech/livechat 2024.12.3-1.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/.github/workflows/release.yml +33 -0
- package/.husky/pre-commit +2 -0
- package/README.md +30 -0
- package/biome.json +55 -0
- package/index.html +20 -0
- package/package.json +43 -0
- package/postcss.config.cjs +5 -0
- package/public/vite.svg +1 -0
- package/src/api/base.api.ts +118 -0
- package/src/api/error.ts +109 -0
- package/src/assets/react.svg +1 -0
- package/src/components/app/WidgetProvider.tsx +80 -0
- package/src/components/app/button/index.tsx +47 -0
- package/src/components/app/button/types.ts +68 -0
- package/src/components/app/card/profile.tsx +33 -0
- package/src/components/app/icon/index.tsx +144 -0
- package/src/components/app/input/checkbox.tsx +115 -0
- package/src/components/app/input/index.tsx +133 -0
- package/src/components/app/input/label.tsx +33 -0
- package/src/components/app/input/radio.tsx +143 -0
- package/src/components/app/input/select.tsx +54 -0
- package/src/components/app/input/textarea.tsx +68 -0
- package/src/components/app/input/toggle.tsx +162 -0
- package/src/components/app/input/verification.tsx +111 -0
- package/src/features/chat/chat.api.ts +30 -0
- package/src/features/chat/chat.types.ts +50 -0
- package/src/features/chat/partials/ChatCreateForm.tsx +103 -0
- package/src/features/chat/partials/ChatMessageFormFooter.tsx +67 -0
- package/src/features/chat/partials/ChatMessageSection.tsx +103 -0
- package/src/features/widget/components/WidgetIcon.tsx +26 -0
- package/src/features/widget/contexts/page-tracker.tsx +74 -0
- package/src/features/widget/hooks/page-tracker.tsx +57 -0
- package/src/features/widget/widget.api.ts +14 -0
- package/src/features/widget/widget.store.ts +16 -0
- package/src/features/widget/widget.types.ts +28 -0
- package/src/index.css +40 -0
- package/src/layouts/auth.tsx +17 -0
- package/src/layouts/chat.tsx +18 -0
- package/src/lib/cdn.tsx +3 -0
- package/src/lib/cookie.tsx +7 -0
- package/src/lib/debounce.tsx +23 -0
- package/src/lib/hooks/cookie.tsx +22 -0
- package/src/lib/hooks/dayjs.tsx +10 -0
- package/src/lib/hooks/dom.tsx +26 -0
- package/src/lib/hooks/form.tsx +26 -0
- package/src/lib/hooks/router.tsx +31 -0
- package/src/lib/list.tsx +14 -0
- package/src/lib/obj.tsx +23 -0
- package/src/lib/phone.tsx +12 -0
- package/src/main.tsx +31 -0
- package/src/pages/auth/registration-view.tsx +67 -0
- package/src/pages/chat/chat-detail-view.tsx +200 -0
- package/src/router/router.tsx +17 -0
- package/src/router/routes/auth-routes.ts +7 -0
- package/src/router/routes/chat-routes.ts +7 -0
- package/src/router/routes/index.ts +8 -0
- package/src/router/routes/routes.types.ts +0 -0
- package/src/types/i18n.ts +11 -0
- package/src/vite-env.d.ts +1 -0
- package/src/widget.tsx +19 -0
- package/src/ws/events.tsx +69 -0
- package/src/ws/guard.tsx +16 -0
- package/src/ws/hooks.tsx +122 -0
- package/tailwind.config.js +27 -0
- package/tsconfig.app.json +29 -0
- package/tsconfig.app.tsbuildinfo +1 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +22 -0
- package/tsconfig.node.tsbuildinfo +1 -0
- package/version.cjs +5 -0
- package/vite.config.ts +14 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
function Icon() {}
|
|
2
|
+
|
|
3
|
+
type Props = React.SVGProps<SVGSVGElement>;
|
|
4
|
+
|
|
5
|
+
Icon.Home = function Home(props: Props) {
|
|
6
|
+
return (
|
|
7
|
+
<svg
|
|
8
|
+
viewBox="0 0 24 24"
|
|
9
|
+
fill="currentColor"
|
|
10
|
+
height="1em"
|
|
11
|
+
width="1em"
|
|
12
|
+
{...props}
|
|
13
|
+
>
|
|
14
|
+
<path d="M3 13h1v7c0 1.103.897 2 2 2h12c1.103 0 2-.897 2-2v-7h1a1 1 0 00.707-1.707l-9-9a.999.999 0 00-1.414 0l-9 9A1 1 0 003 13zm7 7v-5h4v5h-4zm2-15.586l6 6V15l.001 5H16v-5c0-1.103-.897-2-2-2h-4c-1.103 0-2 .897-2 2v5H6v-9.586l6-6z" />
|
|
15
|
+
</svg>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
Icon.Chat = (props: Props) => (
|
|
20
|
+
<svg
|
|
21
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
22
|
+
fill="none"
|
|
23
|
+
viewBox="0 0 24 24"
|
|
24
|
+
strokeWidth={1.5}
|
|
25
|
+
stroke="currentColor"
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
<path
|
|
29
|
+
strokeLinecap="round"
|
|
30
|
+
strokeLinejoin="round"
|
|
31
|
+
d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z"
|
|
32
|
+
/>
|
|
33
|
+
</svg>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
Icon.MessageTyping = ({ className = "size-5", ...props }: Props) => (
|
|
37
|
+
<svg
|
|
38
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
39
|
+
fill="none"
|
|
40
|
+
viewBox="0 0 24 24"
|
|
41
|
+
strokeWidth={1.5}
|
|
42
|
+
stroke="currentColor"
|
|
43
|
+
className={className}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<path
|
|
47
|
+
strokeLinecap="round"
|
|
48
|
+
strokeLinejoin="round"
|
|
49
|
+
d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375m-13.5 3.01c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.184-4.183a1.14 1.14 0 0 1 .778-.332 48.294 48.294 0 0 0 5.83-.498c1.585-.233 2.708-1.626 2.708-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z"
|
|
50
|
+
/>
|
|
51
|
+
</svg>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
Icon.Check = function Check(props: Props) {
|
|
55
|
+
return (
|
|
56
|
+
<svg
|
|
57
|
+
fill="currentColor"
|
|
58
|
+
viewBox="0 0 16 16"
|
|
59
|
+
height="1em"
|
|
60
|
+
width="1em"
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
<path d="M12.736 3.97a.733.733 0 011.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 01-1.065.02L3.217 8.384a.757.757 0 010-1.06.733.733 0 011.047 0l3.052 3.093 5.4-6.425a.247.247 0 01.02-.022z" />
|
|
64
|
+
</svg>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
Icon.Website = function Website(props: Props) {
|
|
69
|
+
return (
|
|
70
|
+
<svg fill="none" viewBox="0 0 24 24" height="1em" width="1em" {...props}>
|
|
71
|
+
<path
|
|
72
|
+
fill="currentColor"
|
|
73
|
+
fillRule="evenodd"
|
|
74
|
+
d="M14 7a1 1 0 00-1 1v8a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1h-4zm3 2h-2v6h2V9z"
|
|
75
|
+
clipRule="evenodd"
|
|
76
|
+
/>
|
|
77
|
+
<path
|
|
78
|
+
fill="currentColor"
|
|
79
|
+
d="M6 7a1 1 0 000 2h4a1 1 0 100-2H6zM6 11a1 1 0 100 2h4a1 1 0 100-2H6zM5 16a1 1 0 011-1h4a1 1 0 110 2H6a1 1 0 01-1-1z"
|
|
80
|
+
/>
|
|
81
|
+
<path
|
|
82
|
+
fill="currentColor"
|
|
83
|
+
fillRule="evenodd"
|
|
84
|
+
d="M4 3a3 3 0 00-3 3v12a3 3 0 003 3h16a3 3 0 003-3V6a3 3 0 00-3-3H4zm16 2H4a1 1 0 00-1 1v12a1 1 0 001 1h16a1 1 0 001-1V6a1 1 0 00-1-1z"
|
|
85
|
+
clipRule="evenodd"
|
|
86
|
+
/>
|
|
87
|
+
</svg>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
Icon.ArrowDown = (props: Props) => (
|
|
92
|
+
<svg
|
|
93
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
94
|
+
fill="none"
|
|
95
|
+
viewBox="0 0 24 24"
|
|
96
|
+
strokeWidth={1.5}
|
|
97
|
+
stroke="currentColor"
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
<path
|
|
101
|
+
strokeLinecap="round"
|
|
102
|
+
strokeLinejoin="round"
|
|
103
|
+
d="M19.5 13.5 12 21m0 0-7.5-7.5M12 21V3"
|
|
104
|
+
/>
|
|
105
|
+
</svg>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
Icon.XCircle = ({ className = "size-5", ...props }: Props) => (
|
|
109
|
+
<svg
|
|
110
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
111
|
+
fill="none"
|
|
112
|
+
viewBox="0 0 24 24"
|
|
113
|
+
strokeWidth={1.5}
|
|
114
|
+
stroke="currentColor"
|
|
115
|
+
className={className}
|
|
116
|
+
{...props}
|
|
117
|
+
>
|
|
118
|
+
<path
|
|
119
|
+
strokeLinecap="round"
|
|
120
|
+
strokeLinejoin="round"
|
|
121
|
+
d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
|
122
|
+
/>
|
|
123
|
+
</svg>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
Icon.X = ({ className = "size-5", ...props }: Props) => (
|
|
127
|
+
<svg
|
|
128
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
129
|
+
fill="none"
|
|
130
|
+
viewBox="0 0 24 24"
|
|
131
|
+
strokeWidth={1.5}
|
|
132
|
+
stroke="currentColor"
|
|
133
|
+
className={className}
|
|
134
|
+
{...props}
|
|
135
|
+
>
|
|
136
|
+
<path
|
|
137
|
+
strokeLinecap="round"
|
|
138
|
+
strokeLinejoin="round"
|
|
139
|
+
d="M6 18 18 6M6 6l12 12"
|
|
140
|
+
/>
|
|
141
|
+
</svg>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
export default Icon;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
|
+
import Icon from "../icon";
|
|
3
|
+
|
|
4
|
+
type Variant = "blue" | "indigo" | "green" | "red" | "orange";
|
|
5
|
+
type Size = "sm" | "md" | "lg";
|
|
6
|
+
type Effect = "hover" | "ring";
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
name: string;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
value?: string;
|
|
12
|
+
checked?: boolean;
|
|
13
|
+
id?: string;
|
|
14
|
+
red?: string;
|
|
15
|
+
reversed?: boolean;
|
|
16
|
+
effect?: Effect;
|
|
17
|
+
variant?: Variant;
|
|
18
|
+
size?: Size;
|
|
19
|
+
onChange?: (value: boolean) => void;
|
|
20
|
+
onBlur?: (value: boolean) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type EffectType = {
|
|
24
|
+
label: string;
|
|
25
|
+
input: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const effects: Record<Effect, EffectType> = {
|
|
29
|
+
ring: {
|
|
30
|
+
label: "",
|
|
31
|
+
input:
|
|
32
|
+
"relative before:content[''] before:absolute before:top-2/4 before:left-2/4 before:block before:h-12 before:w-12 before:-translate-y-2/4 before:-translate-x-2/4 before:rounded-full before:bg-blue-gray-500 before:opacity-0 before:transition-opacity hover:before:opacity-10",
|
|
33
|
+
},
|
|
34
|
+
hover: {
|
|
35
|
+
label:
|
|
36
|
+
"hover:bg-second transition-colors duration-200 px-2 py-1 rounded-md",
|
|
37
|
+
input: "",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const variants: Record<Variant, string> = {
|
|
42
|
+
blue: "checked:border-blue-500 checked:bg-blue-500 checked:before:bg-blue-500",
|
|
43
|
+
indigo:
|
|
44
|
+
"checked:border-indigo-500 checked:bg-indigo-500 checked:before:bg-indigo-500",
|
|
45
|
+
green:
|
|
46
|
+
"checked:border-green-500 checked:bg-green-500 checked:before:bg-green-500",
|
|
47
|
+
red: "checked:border-red-500 checked:bg-red-500 checked:before:bg-red-500",
|
|
48
|
+
orange:
|
|
49
|
+
"checked:border-orange-500 checked:bg-orange-500 checked:before:bg-orange-500",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const sizes: Record<Size, string> = {
|
|
53
|
+
sm: "h-4 w-4 lg:h-3 lg:w-3",
|
|
54
|
+
md: "h-6 w-6 lg:h-5 lg:w-5",
|
|
55
|
+
lg: "h-8 w-8 lg:h-7 lg:w-7",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const svgSizes: Record<Size, string> = {
|
|
59
|
+
sm: "h-3 w-3 lg:h-2.5 lg:w-2.5",
|
|
60
|
+
md: "h-5 w-5 lg:h-4 lg:w-4",
|
|
61
|
+
lg: "h-7 w-7 lg:h-6 lg:w-6",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default function Checkbox({
|
|
65
|
+
children,
|
|
66
|
+
name,
|
|
67
|
+
required,
|
|
68
|
+
value,
|
|
69
|
+
checked,
|
|
70
|
+
onChange,
|
|
71
|
+
onBlur,
|
|
72
|
+
red,
|
|
73
|
+
id = name,
|
|
74
|
+
size = "md",
|
|
75
|
+
effect = "ring",
|
|
76
|
+
reversed = false,
|
|
77
|
+
variant = "blue",
|
|
78
|
+
...props
|
|
79
|
+
}: PropsWithChildren<Props>) {
|
|
80
|
+
return (
|
|
81
|
+
<div>
|
|
82
|
+
<div
|
|
83
|
+
className={`flex items-center justify-between ${reversed ? "flex-row-reverse" : ""} ${effects[effect].label}`}
|
|
84
|
+
>
|
|
85
|
+
<label
|
|
86
|
+
className="disable-highlight relative mr-3 flex cursor-pointer items-center rounded-full"
|
|
87
|
+
htmlFor={id}
|
|
88
|
+
>
|
|
89
|
+
<input
|
|
90
|
+
id={id}
|
|
91
|
+
name={name}
|
|
92
|
+
type="checkbox"
|
|
93
|
+
className={`border-blue-gray-200 peer cursor-pointer appearance-none rounded-md border bg-white transition-all dark:bg-inherit ${variants[variant]} ${sizes[size]} ${effects[effect].input}`}
|
|
94
|
+
required={required}
|
|
95
|
+
checked={checked}
|
|
96
|
+
value={value}
|
|
97
|
+
onChange={(e) => onChange?.((e.target as any).checked)}
|
|
98
|
+
onBlur={(e) => onBlur?.((e.target as any).checked)}
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
<div className="pointer-events-none absolute left-2/4 top-2/4 -translate-x-2/4 -translate-y-2/4 text-white opacity-0 transition-opacity peer-checked:opacity-100">
|
|
102
|
+
<Icon.Check className={svgSizes[size]} />
|
|
103
|
+
</div>
|
|
104
|
+
</label>
|
|
105
|
+
<label
|
|
106
|
+
className="disable-highlight mt-px flex grow cursor-pointer select-none items-center justify-between font-light text-gray-800 dark:text-gray-400"
|
|
107
|
+
htmlFor={id}
|
|
108
|
+
>
|
|
109
|
+
{children}
|
|
110
|
+
</label>
|
|
111
|
+
</div>
|
|
112
|
+
{red && <small className="text-xs text-red-500">{red}</small>}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import cn from "classnames";
|
|
3
|
+
import type { ChangeEvent } from "react";
|
|
4
|
+
import Label from "./label";
|
|
5
|
+
|
|
6
|
+
type Size = "sm" | "md" | "lg";
|
|
7
|
+
type InputValue = string | number | readonly string[] | undefined;
|
|
8
|
+
type InputType =
|
|
9
|
+
| "button"
|
|
10
|
+
| "checkbox"
|
|
11
|
+
| "color"
|
|
12
|
+
| "date"
|
|
13
|
+
| "datetime-local"
|
|
14
|
+
| "email"
|
|
15
|
+
| "file"
|
|
16
|
+
| "hidden"
|
|
17
|
+
| "image"
|
|
18
|
+
| "month"
|
|
19
|
+
| "number"
|
|
20
|
+
| "password"
|
|
21
|
+
| "radio"
|
|
22
|
+
| "range"
|
|
23
|
+
| "reset"
|
|
24
|
+
| "search"
|
|
25
|
+
| "submit"
|
|
26
|
+
| "tel"
|
|
27
|
+
| "text"
|
|
28
|
+
| "time"
|
|
29
|
+
| "url"
|
|
30
|
+
| "week";
|
|
31
|
+
|
|
32
|
+
type Props<V extends InputValue = string> = {
|
|
33
|
+
id: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
value?: V;
|
|
36
|
+
name?: string;
|
|
37
|
+
size?: Size;
|
|
38
|
+
type?: InputType;
|
|
39
|
+
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
40
|
+
onBlur?: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
41
|
+
autoComplete?: string;
|
|
42
|
+
required?: boolean;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
readOnly?: boolean;
|
|
45
|
+
placeholder?: string;
|
|
46
|
+
className?: string;
|
|
47
|
+
pattern?: string;
|
|
48
|
+
noMargin?: boolean;
|
|
49
|
+
error?: string;
|
|
50
|
+
min?: number;
|
|
51
|
+
max?: number;
|
|
52
|
+
minLength?: number;
|
|
53
|
+
maxLength?: number;
|
|
54
|
+
ariaLabel?: string;
|
|
55
|
+
autoFocus?: boolean;
|
|
56
|
+
ariaDescribedBy?: string;
|
|
57
|
+
ariaInvalid?: boolean;
|
|
58
|
+
suffix?: string | JSX.Element;
|
|
59
|
+
prefix?: string | JSX.Element;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const sizes: Record<Size, string> = {
|
|
63
|
+
sm: "px-3 py-1 text-xs",
|
|
64
|
+
md: "px-4 py-2 text-sm",
|
|
65
|
+
lg: "px-5 py-3 text-15",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default function Input<T extends InputValue = string>({
|
|
69
|
+
id,
|
|
70
|
+
label,
|
|
71
|
+
suffix,
|
|
72
|
+
prefix,
|
|
73
|
+
size = "md",
|
|
74
|
+
type = "text",
|
|
75
|
+
noMargin = false,
|
|
76
|
+
required = false,
|
|
77
|
+
error,
|
|
78
|
+
ariaDescribedBy,
|
|
79
|
+
className,
|
|
80
|
+
ariaInvalid,
|
|
81
|
+
ariaLabel,
|
|
82
|
+
autoFocus,
|
|
83
|
+
...props
|
|
84
|
+
}: Props<T>) {
|
|
85
|
+
return (
|
|
86
|
+
<div className={classNames({ "mb-3": !noMargin }, className)}>
|
|
87
|
+
<Label id={id} text={label} required={required} />
|
|
88
|
+
<div className="flex items-center">
|
|
89
|
+
{prefix && (
|
|
90
|
+
<span
|
|
91
|
+
className={cn(
|
|
92
|
+
"inline-block border border-r-0 border-slate-200 bg-slate-100 rounded-l-md",
|
|
93
|
+
sizes[size],
|
|
94
|
+
{ "!border-red-500": !!error },
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
{prefix}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
<input
|
|
101
|
+
id={id}
|
|
102
|
+
className={cn(
|
|
103
|
+
"border rounded-md block text-base py-2 px-4 w-full border-slate-200 focus:outline-none focus:border-custom-500 disabled:bg-slate-100 disabled:border-slate-300 disabled:text-slate-500 placeholder:text-slate-400",
|
|
104
|
+
sizes[size],
|
|
105
|
+
{
|
|
106
|
+
"rounded-l-none": !!prefix,
|
|
107
|
+
"rounded-r-none": !!suffix,
|
|
108
|
+
"!border-red-500 !text-red-500": !!error,
|
|
109
|
+
},
|
|
110
|
+
)}
|
|
111
|
+
type={type}
|
|
112
|
+
required={required}
|
|
113
|
+
aria-describedby={ariaDescribedBy}
|
|
114
|
+
aria-invalid={ariaInvalid}
|
|
115
|
+
aria-label={ariaLabel}
|
|
116
|
+
{...props}
|
|
117
|
+
/>
|
|
118
|
+
{suffix && (
|
|
119
|
+
<span
|
|
120
|
+
className={cn(
|
|
121
|
+
"inline-block border border-l-0 border-slate-200 bg-slate-100rounded-r-md",
|
|
122
|
+
sizes[size],
|
|
123
|
+
{ "!border-red-500": !!error },
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
{suffix}
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
{!!error && <p className="mt-1 text-sm text-red-500">{error}</p>}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
id?: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
className?: string;
|
|
8
|
+
noMargin?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function Label({
|
|
12
|
+
id,
|
|
13
|
+
text,
|
|
14
|
+
required,
|
|
15
|
+
className,
|
|
16
|
+
noMargin,
|
|
17
|
+
}: Props) {
|
|
18
|
+
if (!text) return null;
|
|
19
|
+
return (
|
|
20
|
+
<label
|
|
21
|
+
htmlFor={id}
|
|
22
|
+
className={classNames(
|
|
23
|
+
className,
|
|
24
|
+
{
|
|
25
|
+
"mb-2": !noMargin,
|
|
26
|
+
},
|
|
27
|
+
"inline-block text-base font-medium",
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
{text} {required && <span className="text-red-500">*</span>}
|
|
31
|
+
</label>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
|
+
import Icon from "../icon";
|
|
3
|
+
|
|
4
|
+
type Variant = "blue" | "emerald" | "red" | "orange";
|
|
5
|
+
type Effect = "hover" | "ring";
|
|
6
|
+
type Size = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
interface RadioProps {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
value?: boolean;
|
|
12
|
+
variant?: Variant;
|
|
13
|
+
effect?: Effect;
|
|
14
|
+
reverse?: boolean;
|
|
15
|
+
size?: Size;
|
|
16
|
+
onChange?: (value: boolean) => void;
|
|
17
|
+
onBlur?: (value: boolean) => void;
|
|
18
|
+
onClick?: (value: boolean) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const variants: Record<Variant, string> = {
|
|
22
|
+
blue: "text-blue-500 border-blue-gray-200 checked:bg-blue-500 checked:border-blue-500 checked:before:bg-blue-500 before:bg-blue-gray-500",
|
|
23
|
+
red: "text-red-500 border-blue-gray-200 checked:border-red-500 checked:before:bg-red-500 before:bg-blue-gray-500",
|
|
24
|
+
emerald:
|
|
25
|
+
"text-emerald-500 border-blue-gray-200 checked:border-emerald-500 checked:before:bg-emerald-500 before:bg-blue-gray-500",
|
|
26
|
+
orange:
|
|
27
|
+
"text-orange-500 border-blue-gray-200 checked:border-orange-500 checked:before:bg-orange-500 before:bg-blue-gray-500",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const svgVariants: Record<Variant, string> = {
|
|
31
|
+
blue: "text-white",
|
|
32
|
+
red: "text-red-500",
|
|
33
|
+
emerald: "text-emerald-500",
|
|
34
|
+
orange: "text-orange-500",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const sizes: Record<Size, string> = {
|
|
38
|
+
sm: "h4 w-4 lg:h-3 lg:w-3",
|
|
39
|
+
md: "h-6 w-6 lg:h-5 lg:w-5",
|
|
40
|
+
lg: "h-8 w-8 lg:h-7 lg:w-7",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const iconSizes: Record<Size, string> = {
|
|
44
|
+
sm: "size-3",
|
|
45
|
+
md: "size-4",
|
|
46
|
+
lg: "size-5",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type EffectType = {
|
|
50
|
+
label: string;
|
|
51
|
+
input: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const effects: Record<Effect, EffectType> = {
|
|
55
|
+
ring: {
|
|
56
|
+
label: "",
|
|
57
|
+
input:
|
|
58
|
+
"relative before:content[''] before:absolute before:top-2/4 before:left-2/4 before:block before:h-12 before:w-12 before:-translate-y-2/4 before:-translate-x-2/4 before:rounded-full before:opacity-0 before:transition-opacity hover:before:opacity-10",
|
|
59
|
+
},
|
|
60
|
+
hover: {
|
|
61
|
+
label: "hover:bg-second transition-colors duration-200",
|
|
62
|
+
input: "",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function Radio({
|
|
67
|
+
children,
|
|
68
|
+
id,
|
|
69
|
+
name,
|
|
70
|
+
size = "md",
|
|
71
|
+
variant = "blue",
|
|
72
|
+
effect = "ring",
|
|
73
|
+
value = false,
|
|
74
|
+
reverse = false,
|
|
75
|
+
onChange,
|
|
76
|
+
onBlur,
|
|
77
|
+
onClick,
|
|
78
|
+
}: PropsWithChildren<RadioProps>) {
|
|
79
|
+
return (
|
|
80
|
+
<label
|
|
81
|
+
className={`disable-highlight flex cursor-pointer items-center rounded-md ${
|
|
82
|
+
reverse ? "flex-row-reverse justify-between" : ""
|
|
83
|
+
} ${effects[effect].label} `}
|
|
84
|
+
htmlFor={id}
|
|
85
|
+
>
|
|
86
|
+
<label
|
|
87
|
+
className="relative flex items-center p-2 rounded-full cursor-pointer disable-highlight"
|
|
88
|
+
htmlFor={id}
|
|
89
|
+
>
|
|
90
|
+
<input
|
|
91
|
+
id={id}
|
|
92
|
+
name={name}
|
|
93
|
+
type="radio"
|
|
94
|
+
className={`disable-highlight !bg-none peer cursor-pointer appearance-none rounded-full border dark:border-zink-400 bg-inherit transition-all dark:bg-inherit ${variants[variant]} ${sizes[size]} ${effects[effect].input}`}
|
|
95
|
+
checked={value}
|
|
96
|
+
onChange={(e) => onChange?.(e.target.checked)}
|
|
97
|
+
onBlur={(e) => onBlur?.(e.target.checked)}
|
|
98
|
+
onClick={() => onClick?.(!value)}
|
|
99
|
+
/>
|
|
100
|
+
<div
|
|
101
|
+
className={`pointer-events-none absolute left-2/4 top-2/4 flex -translate-x-2/4 -translate-y-2/4 items-center justify-center opacity-0 transition-opacity peer-checked:opacity-100 ${svgVariants[variant]}`}
|
|
102
|
+
>
|
|
103
|
+
<Icon.Check className={iconSizes[size]} />
|
|
104
|
+
</div>
|
|
105
|
+
</label>
|
|
106
|
+
<label
|
|
107
|
+
className={
|
|
108
|
+
"disable-highlight flex cursor-pointer select-none items-center font-light text-gray-500 dark:text-gray-400"
|
|
109
|
+
}
|
|
110
|
+
htmlFor={id}
|
|
111
|
+
>
|
|
112
|
+
{children}
|
|
113
|
+
</label>
|
|
114
|
+
</label>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type GroupProps = {
|
|
119
|
+
label: string;
|
|
120
|
+
id: string;
|
|
121
|
+
required?: boolean;
|
|
122
|
+
className?: string;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
Radio.Group = function RadioGroup({
|
|
126
|
+
label,
|
|
127
|
+
id,
|
|
128
|
+
className,
|
|
129
|
+
required,
|
|
130
|
+
children,
|
|
131
|
+
}: PropsWithChildren<GroupProps>) {
|
|
132
|
+
return (
|
|
133
|
+
<div className={className}>
|
|
134
|
+
<label htmlFor={id} className="inline-block mb-2 text-base font-medium">
|
|
135
|
+
{label} {required && <span className="text-red-500">*</span>}
|
|
136
|
+
</label>
|
|
137
|
+
|
|
138
|
+
<div className="col-span-12 flex items-center gap-4 p-2">{children}</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export default Radio;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ChangeEvent, PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
label: string;
|
|
5
|
+
id: string;
|
|
6
|
+
value?: string;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
multiple?: boolean;
|
|
9
|
+
name?: string;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
autoComplete?: string;
|
|
14
|
+
autoFocus?: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
min?: number;
|
|
17
|
+
max?: number;
|
|
18
|
+
onChange?: (e: ChangeEvent<HTMLSelectElement>) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function Select({
|
|
22
|
+
label,
|
|
23
|
+
id,
|
|
24
|
+
value = "",
|
|
25
|
+
className,
|
|
26
|
+
placeholder,
|
|
27
|
+
children,
|
|
28
|
+
required,
|
|
29
|
+
error,
|
|
30
|
+
...props
|
|
31
|
+
}: PropsWithChildren<Props>) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={className ? className : ""}>
|
|
34
|
+
{label && (
|
|
35
|
+
<label htmlFor={id} className="inline-block mb-2 text-base font-medium">
|
|
36
|
+
{label} {required && <span className="text-red-500">*</span>}
|
|
37
|
+
</label>
|
|
38
|
+
)}
|
|
39
|
+
<select
|
|
40
|
+
id={id}
|
|
41
|
+
aria-placeholder={placeholder}
|
|
42
|
+
value={value}
|
|
43
|
+
aria-label={label}
|
|
44
|
+
className="bg-light form-select dark:bg-zink-700 dark:border-zink-500 transition-colors duraiton-200 border text-gray-900 dark:text-gray-200 sm:text-sm rounded-md focus:ring-emerald-500 focus-visible:ring-0 focus-visible:shadow-none outline-none focus-visible:outline-none focus-visible:border-emerald-500 focus:border-emerald-500 block w-full px-2 py-2 placeholder-fuchsia-600 caret-emerald-500"
|
|
45
|
+
onChange={props.onChange}
|
|
46
|
+
required={required}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</select>
|
|
51
|
+
{!!error && <p className="mt-1 text-sm text-red-500">{error}</p>}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import cn from "classnames";
|
|
3
|
+
import type { ChangeEvent } from "react";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
value?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
rows?: number;
|
|
11
|
+
type?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
noMargin?: boolean;
|
|
14
|
+
onChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void;
|
|
15
|
+
onBlur?: (e: ChangeEvent<HTMLTextAreaElement>) => void;
|
|
16
|
+
autoComplete?: string;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
readOnly?: boolean;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
pattern?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
min?: number;
|
|
24
|
+
max?: number;
|
|
25
|
+
minLength?: number;
|
|
26
|
+
maxLength?: number;
|
|
27
|
+
ariaLabel?: string;
|
|
28
|
+
ariaDescribedBy?: string;
|
|
29
|
+
ariaInvalid?: boolean;
|
|
30
|
+
suffix?: string | JSX.Element;
|
|
31
|
+
prefix?: string | JSX.Element;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default function Textarea({
|
|
35
|
+
id,
|
|
36
|
+
label,
|
|
37
|
+
suffix,
|
|
38
|
+
prefix,
|
|
39
|
+
className,
|
|
40
|
+
noMargin,
|
|
41
|
+
rows = 3,
|
|
42
|
+
type = "text",
|
|
43
|
+
required = false,
|
|
44
|
+
error,
|
|
45
|
+
ariaInvalid,
|
|
46
|
+
...props
|
|
47
|
+
}: Props) {
|
|
48
|
+
return (
|
|
49
|
+
<div className={classNames({ "mb-3": !noMargin }, className)}>
|
|
50
|
+
<label htmlFor={id} className="inline-block mb-2 text-base font-medium">
|
|
51
|
+
{label}
|
|
52
|
+
</label>
|
|
53
|
+
<textarea
|
|
54
|
+
className={cn(
|
|
55
|
+
"form-input border-slate-200 dark:border-zink-500 focus:outline-none focus:border-custom-500 disabled:bg-slate-100 dark:disabled:bg-zink-600 disabled:border-slate-300 dark:disabled:border-zink-500 dark:disabled:text-zink-200 disabled:text-slate-500 dark:text-zink-100 dark:bg-zink-700 dark:focus:border-custom-800 placeholder:text-slate-400 dark:placeholder:text-zink-200 valid:border-green-500 invalid:border-red-500 dark:valid:border-green-800 dark:invalid:border-red-800 invalid:text-red-500 dark:invalid:text-red-800",
|
|
56
|
+
{
|
|
57
|
+
"!border-red-500 !dark:border-red-800 !text-red-500 !dark:text-red-800":
|
|
58
|
+
!!error,
|
|
59
|
+
},
|
|
60
|
+
)}
|
|
61
|
+
id={id}
|
|
62
|
+
aria-invalid={ariaInvalid}
|
|
63
|
+
{...props}
|
|
64
|
+
></textarea>
|
|
65
|
+
{!!error && <p className="text-red-500 mt-1 text-sm">{error}</p>}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|