@skalfa/skalfa-component 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/index.js +10 -10
  2. package/package.json +2 -2
  3. package/src/accordion/Accordion.component.tsx +87 -0
  4. package/src/breadcrumb/Breadcrumb.component.tsx +79 -0
  5. package/src/button/Button.component.tsx +89 -0
  6. package/src/card/AlertCard.component.tsx +69 -0
  7. package/src/card/Card.component.tsx +25 -0
  8. package/src/card/DashboardCard.component.tsx +44 -0
  9. package/src/card/GalleryCard.component.tsx +50 -0
  10. package/src/card/ProductCard.component.tsx +65 -0
  11. package/src/card/ProfileCard.component.tsx +71 -0
  12. package/src/carousel/Carousel.component.tsx +111 -0
  13. package/src/chip/Chip.component.tsx +39 -0
  14. package/src/index.ts +70 -0
  15. package/src/input/Checkbox.component.tsx +102 -0
  16. package/src/input/Input.component.tsx +334 -0
  17. package/src/input/InputCheckbox.component.tsx +174 -0
  18. package/src/input/InputCurrency.component.tsx +165 -0
  19. package/src/input/InputDate.component.tsx +356 -0
  20. package/src/input/InputDatetime.component.tsx +267 -0
  21. package/src/input/InputDocument.component.tsx +360 -0
  22. package/src/input/InputImage.component.tsx +535 -0
  23. package/src/input/InputNumber.component.tsx +194 -0
  24. package/src/input/InputOtp.component.tsx +169 -0
  25. package/src/input/InputPassword.component.tsx +245 -0
  26. package/src/input/InputRadio.component.tsx +174 -0
  27. package/src/input/InputTime.component.tsx +280 -0
  28. package/src/input/InputValues.component.tsx +71 -0
  29. package/src/input/Radio.component.tsx +98 -0
  30. package/src/input/Select.component.tsx +557 -0
  31. package/src/modal/BottomSheet.component.tsx +246 -0
  32. package/src/modal/FloatingPage.component.tsx +103 -0
  33. package/src/modal/Modal.component.tsx +95 -0
  34. package/src/modal/ModalConfirm.component.tsx +219 -0
  35. package/src/modal/Toast.component.tsx +125 -0
  36. package/src/nav/Bottombar.component.tsx +72 -0
  37. package/src/nav/Footer.component.tsx +177 -0
  38. package/src/nav/Headbar.component.tsx +33 -0
  39. package/src/nav/Navbar.component.tsx +138 -0
  40. package/src/nav/Sidebar.component.tsx +298 -0
  41. package/src/nav/Tabbar.component.tsx +61 -0
  42. package/src/nav/Wizard.component.tsx +80 -0
  43. package/src/supervision/FormSupervision.component.tsx +425 -0
  44. package/src/supervision/TableSupervision.component.tsx +688 -0
  45. package/src/table/ControlBar.component.tsx +501 -0
  46. package/src/table/FilterComponent.tsx +519 -0
  47. package/src/table/Pagination.component.tsx +152 -0
  48. package/src/table/Table.component.tsx +436 -0
  49. package/src/types.d.ts +7 -0
  50. package/src/typography/TypographyArticle.component.tsx +26 -0
  51. package/src/typography/TypographyColumn.component.tsx +20 -0
  52. package/src/typography/TypographyContent.component.tsx +20 -0
  53. package/src/typography/TypographyTips.component.tsx +20 -0
  54. package/src/wrap/Draggable.component.tsx +303 -0
  55. package/src/wrap/Image.component.tsx +10 -0
  56. package/src/wrap/OutsideClick.component.tsx +48 -0
  57. package/src/wrap/ScrollContainer.component.tsx +107 -0
  58. package/src/wrap/ShortcutProvider.tsx +57 -0
  59. package/src/wrap/Swipe.component.tsx +121 -0
@@ -0,0 +1,194 @@
1
+ "use client"
2
+
3
+ import React, { InputHTMLAttributes, ReactNode } from "react";
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import { faSortDown, faSortUp } from "@fortawesome/free-solid-svg-icons";
6
+ import { cn, pcn, useInputHandler, useInputRandomId, useValidation, validation, ValidationRules } from "@utils";
7
+
8
+
9
+
10
+ type CT = "label" | "tip" | "error" | "input" | "icon";
11
+
12
+ export interface InputNumberProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
13
+ label ?: string;
14
+ tip ?: string | ReactNode;
15
+ leftIcon ?: any;
16
+ rightIcon ?: any;
17
+
18
+ value ?: number;
19
+ invalid ?: string;
20
+ validations ?: ValidationRules;
21
+ min ?: number;
22
+ max ?: number;
23
+
24
+ onChange ?: (value: number) => any;
25
+ register ?: (name: string, validations?: ValidationRules) => void;
26
+ unregister ?: (name: string) => void;
27
+
28
+ /** Use custom class with: "label::", "tip::", "error::", "icon::", "suggest::", "suggest-item::". */
29
+ className ?: string;
30
+ }
31
+
32
+
33
+
34
+ export function InputNumberComponent({
35
+ label,
36
+ tip,
37
+ leftIcon,
38
+ rightIcon,
39
+
40
+ value,
41
+ invalid,
42
+ validations,
43
+ min,
44
+ max,
45
+
46
+ onChange,
47
+ register,
48
+ unregister,
49
+
50
+ className = "",
51
+ ...props
52
+ }: InputNumberProps) {
53
+
54
+ // =========================>
55
+ // ## Initial
56
+ // =========================>
57
+ const inputHandler = useInputHandler(props.name, value, validations, register, unregister, false)
58
+ const randomId = useInputRandomId()
59
+
60
+
61
+ // =========================>
62
+ // ## Invalid handler
63
+ // =========================>
64
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
65
+
66
+
67
+ // =========================>
68
+ // ## Change value handler
69
+ // =========================>
70
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
71
+ const newValue = e.target.value;
72
+
73
+ const regex = /^-?\d*\.?\d*$/;
74
+ if (regex.test(newValue)) {
75
+ inputHandler.setValue(newValue);
76
+ inputHandler.setIdle(false);
77
+ onChange?.(Number(newValue));
78
+ }
79
+ };
80
+
81
+
82
+ const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
83
+ props.onBlur?.(e);
84
+ setTimeout(() => inputHandler.setFocus(false), 100);
85
+ };
86
+
87
+
88
+ const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
89
+ props.onFocus?.(e);
90
+ inputHandler.setFocus(true);
91
+ };
92
+
93
+
94
+ return (
95
+ <>
96
+ <div className="input-container">
97
+ <label
98
+ htmlFor={randomId}
99
+ className={cn(
100
+ "input-label",
101
+ props.disabled && "input-label-disabled",
102
+ inputHandler.focus && "input-label-focus",
103
+ !!invalidMessage && "input-label-error",
104
+ pcn<CT>(className, "label"),
105
+ props.disabled && pcn<CT>(className, "label", "disabled"),
106
+ inputHandler.focus && pcn<CT>(className, "label", "focus"),
107
+ !!invalidMessage && pcn<CT>(className, "label", "focus"),
108
+ )}
109
+ >
110
+ {label}
111
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
112
+ </label>
113
+
114
+ {tip && (
115
+ <small
116
+ className={cn(
117
+ "input-tip",
118
+ props.disabled && "input-tip-disabled",
119
+ pcn<CT>(className, "tip"),
120
+ props.disabled && pcn<CT>(className, "tip", "disabled"),
121
+ )}
122
+ >{tip}</small>
123
+ )}
124
+
125
+ <div className="relative">
126
+ <input
127
+ {...props}
128
+ id={randomId}
129
+ className={cn(
130
+ "input",
131
+ leftIcon && "input-with-left-icon",
132
+ rightIcon && "input-with-right-icon",
133
+ pcn<CT>(className, "input"),
134
+ !!invalidMessage && "input-error",
135
+ !!invalidMessage && pcn<CT>(className, "input", "error"),
136
+ )}
137
+ value={inputHandler.value}
138
+ onChange={handleInputChange}
139
+ onFocus={handleFocus}
140
+ onBlur={handleBlur}
141
+ autoComplete="off"
142
+ min={min}
143
+ max={max}
144
+ />
145
+
146
+ {leftIcon && (
147
+ <FontAwesomeIcon
148
+ className={cn(
149
+ "input-icon",
150
+ "input-icon-left",
151
+ props.disabled && "input-icon-disabled",
152
+ inputHandler.focus && "input-icon-focus",
153
+ pcn<CT>(className, "icon"),
154
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
155
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
156
+ )}
157
+ icon={leftIcon}
158
+ />
159
+ )}
160
+
161
+ <label
162
+ htmlFor={randomId}
163
+ className={cn(
164
+ "input-icon",
165
+ "input-icon-right",
166
+ props.disabled && "input-icon-disabled",
167
+ inputHandler.focus && "input-icon-focus",
168
+ pcn<CT>(className, "icon"),
169
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
170
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
171
+ )}
172
+ >
173
+ <div className="input-number-sort-container">
174
+ <FontAwesomeIcon
175
+ className="input-number-sort-up"
176
+ icon={faSortUp}
177
+ onClick={() => inputHandler.setValue(String(Number(inputHandler.value) + 1))}
178
+ />
179
+ <FontAwesomeIcon
180
+ className="input-number-sort-down"
181
+ icon={faSortDown}
182
+ onClick={() => inputHandler.setValue(String(Number(inputHandler.value) - 1))}
183
+ />
184
+ </div>
185
+ </label>
186
+ </div>
187
+
188
+ {invalidMessage && (
189
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
190
+ )}
191
+ </div>
192
+ </>
193
+ );
194
+ }
@@ -0,0 +1,169 @@
1
+ "use client"
2
+
3
+ import { ClipboardEvent, FC, KeyboardEvent, ReactNode, useEffect, useRef, useState } from "react";
4
+ import { cn, pcn } from "@utils";
5
+
6
+
7
+
8
+ type CT = "label" | "tip" | "error" | "base" | "icon";
9
+
10
+ export interface InputOtpProps {
11
+ label ?: string;
12
+ tip ?: string | ReactNode;
13
+ name : string;
14
+ disabled ?: boolean;
15
+
16
+ value ?: string;
17
+ invalid ?: string;
18
+ length ?: number;
19
+
20
+ onChange ?: (value: string) => any;
21
+
22
+ /** Use custom class with: "label::", "tip::", "error::". */
23
+ className ?: string;
24
+ }
25
+
26
+
27
+
28
+ export const InputOtpComponent: FC<InputOtpProps> = ({
29
+ label,
30
+ tip,
31
+ name,
32
+ disabled,
33
+
34
+ value,
35
+ invalid,
36
+ length = 6,
37
+
38
+ onChange,
39
+
40
+ className = "",
41
+ }) => {
42
+ const [isFocus, setIsFocus] = useState(false);
43
+ const inputsRef = useRef<(HTMLInputElement | null)[]>([]);
44
+ const [otp, setOtp] = useState<string[]>((value || "").split("").concat(Array(length).fill("")).slice(0, length));
45
+
46
+
47
+ useEffect(() => {
48
+ const handleFocusChange = () => {
49
+ const anyFocused = inputsRef.current.some((input) => input === document.activeElement);
50
+ setIsFocus(anyFocused);
51
+ };
52
+
53
+ window.addEventListener("focusin", handleFocusChange);
54
+ window.addEventListener("focusout", handleFocusChange);
55
+
56
+ return () => {
57
+ window.removeEventListener("focusin", handleFocusChange);
58
+ window.removeEventListener("focusout", handleFocusChange);
59
+ };
60
+ }, []);
61
+
62
+
63
+ const emitChange = (newOtp: string[]) => {
64
+ const val = newOtp.join("");
65
+ onChange?.(val);
66
+ };
67
+
68
+
69
+ const handleChange = (val: string, index: number) => {
70
+ if (!/^[0-9]?$/.test(val)) return;
71
+ const newOtp = [...otp];
72
+ newOtp[index] = val;
73
+
74
+ setOtp(newOtp);
75
+ emitChange(newOtp);
76
+
77
+ (val && index < length - 1) && inputsRef.current[index + 1]?.focus();
78
+ };
79
+
80
+
81
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>, index: number) => {
82
+ if (e.key === "Backspace") {
83
+ if (otp[index]) {
84
+ const newOtp = [...otp];
85
+ newOtp[index] = "";
86
+ setOtp(newOtp);
87
+ emitChange(newOtp);
88
+ } else if (index > 0) {
89
+ inputsRef.current[index - 1]?.focus();
90
+ }
91
+ }
92
+ };
93
+
94
+
95
+ const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
96
+ e.preventDefault();
97
+ const paste = e.clipboardData.getData("text").trim();
98
+ if (!/^[0-9]+$/.test(paste)) return;
99
+ const pasted = paste.slice(0, length).split("");
100
+ const newOtp = [...otp];
101
+ for (let i = 0; i < pasted.length; i++) newOtp[i] = pasted[i];
102
+ setOtp(newOtp);
103
+ emitChange(newOtp);
104
+ inputsRef.current[Math.min(pasted.length, length - 1)]?.focus();
105
+ };
106
+
107
+
108
+ return (
109
+ <>
110
+ <div className="input-container">
111
+ <label
112
+ className={cn(
113
+ "input-label",
114
+ disabled && "input-label-disabled",
115
+ isFocus && "input-label-focus",
116
+ !!invalid && "input-label-error",
117
+ pcn<CT>(className, "label"),
118
+ disabled && pcn<CT>(className, "label", "disabled"),
119
+ isFocus && pcn<CT>(className, "label", "focus"),
120
+ !!invalid && pcn<CT>(className, "label", "error"),
121
+ )}
122
+ >
123
+ {label}
124
+ </label>
125
+
126
+ {tip && (
127
+ <small
128
+ className={cn(
129
+ "input-tip",
130
+ disabled && "input-tip-disabled",
131
+ pcn<CT>(className, "tip"),
132
+ disabled && pcn<CT>(className, "tip", "disabled"),
133
+ )}
134
+ >{tip}</small>
135
+ )}
136
+ <div className={cn(
137
+ "input input-otp-box",
138
+ isFocus && "input-otp-box-focus",
139
+ !!invalid && "input-error"
140
+ )}
141
+ >
142
+ <div className="input-otp-digits-container">
143
+ {otp.map((digit, index) => (
144
+ <input
145
+ key={index}
146
+ type="text"
147
+ ref={(el) => {inputsRef.current[index] = el }}
148
+ inputMode="numeric"
149
+ maxLength={1}
150
+ value={digit}
151
+ onChange={(e) => handleChange(e.target.value, index)}
152
+ onKeyDown={(e) => handleKeyDown(e, index)}
153
+ onPaste={handlePaste}
154
+ className="input-otp-cell"
155
+ placeholder="-"
156
+ autoFocus={index === 0}
157
+ />
158
+ ))}
159
+ </div>
160
+ <input type="hidden" name={name} value={otp.join("")} />
161
+ </div>
162
+
163
+ {invalid && (
164
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalid}</small>
165
+ )}
166
+ </div>
167
+ </>
168
+ );
169
+ };
@@ -0,0 +1,245 @@
1
+ "use client"
2
+
3
+ import { InputHTMLAttributes, ReactNode, useEffect, useState } from "react";
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import { cn, pcn, useInputHandler, useInputRandomId, useValidation, validation, ValidationRules } from "@utils";
6
+
7
+
8
+
9
+ type CT = "label" | "tip" | "error" | "base" | "icon";
10
+
11
+ export interface InputPasswordProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
12
+ label ?: string;
13
+ tip ?: string | ReactNode;
14
+ leftIcon ?: any;
15
+ rightIcon ?: any;
16
+
17
+ value ?: string;
18
+ invalid ?: string;
19
+ validations ?: ValidationRules;
20
+
21
+ onChange ?: (value: string, confirm?: string) => any;
22
+ register ?: (name: string, validations?: ValidationRules) => void;
23
+ unregister ?: (name: string) => void;
24
+
25
+ /** Use custom class with: "label::", "tip::", "error::", "icon::". */
26
+ className ?: string;
27
+ }
28
+
29
+
30
+
31
+ export function InputPasswordComponent({
32
+ label,
33
+ tip,
34
+ leftIcon,
35
+ rightIcon,
36
+
37
+ value,
38
+ invalid,
39
+ validations,
40
+
41
+ register,
42
+ unregister,
43
+ onChange,
44
+
45
+ className = "",
46
+ ...props
47
+ }: InputPasswordProps) {
48
+ const [password, setPassword] = useState("");
49
+ const [confirmPassword, setConfirmPassword] = useState("");
50
+ const [strength, setStrength] = useState<"weak" | "strong" | "excellent" | "">("");
51
+
52
+
53
+ // =========================>
54
+ // ## Initial
55
+ // =========================>
56
+ const inputHandler = useInputHandler(props.name, value, validations, register, unregister, false)
57
+ const randomId = useInputRandomId()
58
+ const randomConfirmId = useInputRandomId()
59
+
60
+
61
+ // =========================>
62
+ // ## Invalid handler
63
+ // =========================>
64
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
65
+
66
+
67
+ // =========================>
68
+ // ## Password strength handler
69
+ // =========================>
70
+ useEffect(() => {
71
+ if (!password) return setStrength("");
72
+
73
+ const hasLetter = /[A-Za-z]/.test(password);
74
+ const hasNumber = /\d/.test(password);
75
+ const hasSymbol = /[^A-Za-z0-9]/.test(password);
76
+
77
+ if (hasLetter && !hasNumber && !hasSymbol) setStrength("weak");
78
+ else if (hasLetter && hasNumber && !hasSymbol) setStrength("strong");
79
+ else if (hasLetter && hasNumber && hasSymbol) setStrength("excellent");
80
+ else setStrength("weak");
81
+ }, [password]);
82
+
83
+
84
+ // =========================>
85
+ // ## Check match confirm password
86
+ // =========================>
87
+ const isConfirmMismatch = confirmPassword && password !== confirmPassword;
88
+
89
+
90
+ return (
91
+ <div className="input-container">
92
+ {label && (
93
+ <label
94
+ htmlFor={randomId}
95
+ className={cn(
96
+ "input-label",
97
+ props.disabled && "input-label-disabled",
98
+ inputHandler.focus && "input-label-focus",
99
+ !!invalidMessage && "input-label-error",
100
+ pcn<CT>(className, "label"),
101
+ )}
102
+ >
103
+ {label}
104
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
105
+ </label>
106
+ )}
107
+
108
+ {tip && (
109
+ <small
110
+ className={cn(
111
+ "input-tip",
112
+ props.disabled && "input-tip-disabled",
113
+ pcn<CT>(className, "tip"),
114
+ )}
115
+ >{tip}</small>
116
+ )}
117
+
118
+ <div className="relative">
119
+ <input
120
+ {...props}
121
+ type="password"
122
+ id={randomId}
123
+ className={cn(
124
+ "input",
125
+ leftIcon && "input-with-left-icon",
126
+ rightIcon && "input-with-right-icon",
127
+ !!invalidMessage && "input-error",
128
+ pcn<CT>(className, "base"),
129
+ )}
130
+ value={password}
131
+ onChange={(e) => {
132
+ setPassword(e.target.value);
133
+ inputHandler.setIdle(false);
134
+ onChange?.(e.target.value, confirmPassword);
135
+ }}
136
+ onFocus={(e) => {
137
+ props.onFocus?.(e);
138
+ inputHandler.setFocus(true);
139
+ }}
140
+ onBlur={(e) => {
141
+ props.onBlur?.(e);
142
+ setTimeout(() => inputHandler.setFocus(false), 100);
143
+ }}
144
+ autoComplete="off"
145
+ />
146
+
147
+ {leftIcon && (
148
+ <FontAwesomeIcon
149
+ className={cn(
150
+ "input-icon",
151
+ "input-icon-left",
152
+ pcn<CT>(className, "icon")
153
+ )}
154
+ icon={leftIcon}
155
+ />
156
+ )}
157
+ {rightIcon && (
158
+ <FontAwesomeIcon
159
+ className={cn(
160
+ "input-icon",
161
+ "input-icon-right",
162
+ pcn<CT>(className, "icon")
163
+ )}
164
+ icon={rightIcon}
165
+ />
166
+ )}
167
+ </div>
168
+
169
+ {invalidMessage && (
170
+ <small className="input-error-message">
171
+ {invalidMessage}
172
+ </small>
173
+ )}
174
+
175
+ {strength && (
176
+ <div className="flex items-center gap-2 mt-1">
177
+ <div
178
+ className={cn(
179
+ "password-strength-bar",
180
+ strength === "weak" && "password-strength-bar-weak",
181
+ strength === "strong" && "password-strength-bar-strong",
182
+ strength === "excellent" && "password-strength-bar-excellent",
183
+ )}
184
+ />
185
+ <div
186
+ className={cn(
187
+ "password-strength-bar",
188
+ strength === "weak" && "password-strength-bar-empty",
189
+ strength === "strong" && "password-strength-bar-strong",
190
+ strength === "excellent" && "password-strength-bar-excellent",
191
+ )}
192
+ />
193
+ <div
194
+ className={cn(
195
+ "password-strength-bar",
196
+ (strength === "weak" || strength === "strong") && "password-strength-bar-empty",
197
+ strength === "excellent" && "password-strength-bar-excellent",
198
+ )}
199
+ />
200
+ <span
201
+ className={cn(
202
+ "password-strength-text",
203
+ strength === "weak" && "password-strength-text-weak",
204
+ strength === "strong" && "password-strength-text-strong",
205
+ strength === "excellent" && "password-strength-text-excellent",
206
+ )}
207
+ >
208
+ {strength === "weak" ? "Weak" : strength === "strong" ? "Strong" : "Excellent"}
209
+ </span>
210
+ </div>
211
+ )}
212
+
213
+ <div className="input-password-confirm-wrapper">
214
+ <label
215
+ htmlFor={randomConfirmId}
216
+ className={cn("input-label", pcn<CT>(className, "label"))}
217
+ >Password Confirm</label>
218
+ <div className="relative">
219
+ <input
220
+ {...props}
221
+ id={randomConfirmId}
222
+ type="password"
223
+ className={cn(
224
+ "input",
225
+ leftIcon && "input-with-left-icon",
226
+ rightIcon && "input-with-right-icon",
227
+ pcn<CT>(className, "base"),
228
+ isConfirmMismatch && "input-error",
229
+ )}
230
+ value={confirmPassword}
231
+ onChange={(e) => {
232
+ setConfirmPassword(e.target.value);
233
+ onChange?.(password, e.target.value);
234
+ }}
235
+ autoComplete="off"
236
+ />
237
+ </div>
238
+ </div>
239
+
240
+ {isConfirmMismatch && (
241
+ <small className="input-error-message">Password confirmation not match</small>
242
+ )}
243
+ </div>
244
+ );
245
+ }