@skalfa/skalfa-component 1.0.7 → 1.0.9

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 (107) hide show
  1. package/dist/accordion/Accordion.component.js +2 -3
  2. package/dist/breadcrumb/Breadcrumb.component.js +2 -3
  3. package/dist/button/Button.component.d.ts +2 -1
  4. package/dist/button/Button.component.js +2 -2
  5. package/dist/carousel/Carousel.component.js +2 -3
  6. package/dist/chip/Chip.component.js +2 -3
  7. package/dist/contexts-mock.d.ts +2 -0
  8. package/dist/contexts-mock.js +10 -0
  9. package/dist/input/Checkbox.component.js +2 -3
  10. package/dist/input/Input.component.d.ts +3 -2
  11. package/dist/input/Input.component.js +3 -3
  12. package/dist/input/InputCheckbox.component.js +1 -1
  13. package/dist/input/InputCurrency.component.d.ts +3 -2
  14. package/dist/input/InputCurrency.component.js +3 -3
  15. package/dist/input/InputDate.component.js +4 -5
  16. package/dist/input/InputDatetime.component.d.ts +3 -2
  17. package/dist/input/InputDatetime.component.js +3 -3
  18. package/dist/input/InputDocument.component.js +7 -8
  19. package/dist/input/InputImage.component.js +3 -4
  20. package/dist/input/InputNumber.component.js +3 -4
  21. package/dist/input/InputPassword.component.js +3 -3
  22. package/dist/input/InputRadio.component.js +1 -1
  23. package/dist/input/InputTime.component.js +3 -3
  24. package/dist/input/InputValues.component.js +3 -4
  25. package/dist/input/Select.component.js +6 -7
  26. package/dist/modal/FloatingPage.component.d.ts +3 -1
  27. package/dist/modal/FloatingPage.component.js +7 -3
  28. package/dist/modal/Modal.component.js +1 -2
  29. package/dist/modal/ModalConfirm.component.d.ts +2 -2
  30. package/dist/modal/ModalConfirm.component.js +14 -12
  31. package/dist/modal/Toast.component.js +1 -2
  32. package/dist/nav/Bottombar.component.d.ts +2 -2
  33. package/dist/nav/Bottombar.component.js +7 -8
  34. package/dist/nav/Footer.component.d.ts +2 -2
  35. package/dist/nav/Footer.component.js +6 -7
  36. package/dist/nav/Headbar.component.js +2 -3
  37. package/dist/nav/Navbar.component.d.ts +2 -2
  38. package/dist/nav/Navbar.component.js +3 -4
  39. package/dist/nav/Sidebar.component.d.ts +3 -7
  40. package/dist/nav/Sidebar.component.js +14 -17
  41. package/dist/supervision/FormSupervision.component.d.ts +4 -29
  42. package/dist/supervision/FormSupervision.component.js +84 -136
  43. package/dist/supervision/TableSupervision.component.d.ts +17 -26
  44. package/dist/supervision/TableSupervision.component.js +207 -193
  45. package/dist/table/ControlBar.component.js +15 -16
  46. package/dist/table/FilterComponent.js +4 -6
  47. package/dist/table/Pagination.component.js +2 -3
  48. package/dist/table/Table.component.js +3 -4
  49. package/dist/wrap/Swipe.component.js +2 -2
  50. package/package.json +18 -10
  51. package/src/accordion/Accordion.component.tsx +87 -0
  52. package/src/breadcrumb/Breadcrumb.component.tsx +79 -0
  53. package/src/button/Button.component.tsx +89 -0
  54. package/src/card/AlertCard.component.tsx +69 -0
  55. package/src/card/Card.component.tsx +25 -0
  56. package/src/card/DashboardCard.component.tsx +44 -0
  57. package/src/card/GalleryCard.component.tsx +50 -0
  58. package/src/card/ProductCard.component.tsx +65 -0
  59. package/src/card/ProfileCard.component.tsx +71 -0
  60. package/src/carousel/Carousel.component.tsx +111 -0
  61. package/src/chip/Chip.component.tsx +38 -0
  62. package/src/contexts-mock.ts +6 -0
  63. package/src/index.ts +70 -0
  64. package/src/input/Checkbox.component.tsx +102 -0
  65. package/src/input/Input.component.tsx +334 -0
  66. package/src/input/InputCheckbox.component.tsx +174 -0
  67. package/src/input/InputCurrency.component.tsx +165 -0
  68. package/src/input/InputDate.component.tsx +355 -0
  69. package/src/input/InputDatetime.component.tsx +267 -0
  70. package/src/input/InputDocument.component.tsx +360 -0
  71. package/src/input/InputImage.component.tsx +534 -0
  72. package/src/input/InputNumber.component.tsx +193 -0
  73. package/src/input/InputOtp.component.tsx +169 -0
  74. package/src/input/InputPassword.component.tsx +245 -0
  75. package/src/input/InputRadio.component.tsx +174 -0
  76. package/src/input/InputTime.component.tsx +280 -0
  77. package/src/input/InputValues.component.tsx +70 -0
  78. package/src/input/Radio.component.tsx +98 -0
  79. package/src/input/Select.component.tsx +557 -0
  80. package/src/modal/BottomSheet.component.tsx +246 -0
  81. package/src/modal/FloatingPage.component.tsx +133 -0
  82. package/src/modal/Modal.component.tsx +94 -0
  83. package/src/modal/ModalConfirm.component.tsx +204 -0
  84. package/src/modal/Toast.component.tsx +124 -0
  85. package/src/nav/Bottombar.component.tsx +71 -0
  86. package/src/nav/Footer.component.tsx +176 -0
  87. package/src/nav/Headbar.component.tsx +33 -0
  88. package/src/nav/Navbar.component.tsx +137 -0
  89. package/src/nav/Sidebar.component.tsx +307 -0
  90. package/src/nav/Tabbar.component.tsx +61 -0
  91. package/src/nav/Wizard.component.tsx +80 -0
  92. package/src/supervision/FormSupervision.component.tsx +404 -0
  93. package/src/supervision/TableSupervision.component.tsx +653 -0
  94. package/src/table/ControlBar.component.tsx +501 -0
  95. package/src/table/FilterComponent.tsx +517 -0
  96. package/src/table/Pagination.component.tsx +152 -0
  97. package/src/table/Table.component.tsx +436 -0
  98. package/src/typography/TypographyArticle.component.tsx +26 -0
  99. package/src/typography/TypographyColumn.component.tsx +20 -0
  100. package/src/typography/TypographyContent.component.tsx +20 -0
  101. package/src/typography/TypographyTips.component.tsx +20 -0
  102. package/src/wrap/Draggable.component.tsx +303 -0
  103. package/src/wrap/Image.component.tsx +10 -0
  104. package/src/wrap/OutsideClick.component.tsx +48 -0
  105. package/src/wrap/ScrollContainer.component.tsx +107 -0
  106. package/src/wrap/ShortcutProvider.tsx +57 -0
  107. package/src/wrap/Swipe.component.tsx +121 -0
@@ -0,0 +1,334 @@
1
+ "use client"
2
+
3
+ import { InputHTMLAttributes, ReactNode, Ref, useEffect, useState } from "react";
4
+ import { Icon, type IconName } from "@skalfa/skalfa-icon";
5
+ import { cn, pcn, useInputHandler, useInputRandomId, useValidation, validation, ValidationRules } from "@utils";
6
+ import { InputValues } from "./InputValues.component";
7
+
8
+
9
+
10
+ type CT = "label" | "tip" | "error" | "base" | "icon" | "suggest" | "suggest-item";
11
+
12
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
13
+ label ?: string;
14
+ tip ?: string | ReactNode;
15
+ leftIcon ?: IconName;
16
+ rightIcon ?: IconName;
17
+
18
+ value ?: any;
19
+ invalid ?: string;
20
+ suggestions ?: string[];
21
+
22
+ validations ?: ValidationRules;
23
+ onlyAlphabet ?: boolean;
24
+ uppercase ?: boolean;
25
+ lowercase ?: boolean;
26
+ multiple ?: boolean;
27
+
28
+ onChange ?: (value: any) => any;
29
+ register ?: (name: string, validations?: ValidationRules) => void;
30
+ unregister ?: (name: string) => void;
31
+
32
+ ref ?: Ref<HTMLInputElement>,
33
+
34
+ /** Use custom class with: "label::", "tip::", "error::", "icon::", "suggest::", "suggest-item::". */
35
+ className ?: string;
36
+ }
37
+
38
+
39
+
40
+ export function InputComponent({
41
+ label,
42
+ tip,
43
+ leftIcon,
44
+ rightIcon,
45
+ className = "",
46
+
47
+ value,
48
+ invalid,
49
+ suggestions,
50
+
51
+ validations,
52
+ onlyAlphabet,
53
+ uppercase,
54
+ lowercase,
55
+ multiple,
56
+
57
+ register,
58
+ unregister,
59
+ onChange,
60
+
61
+ ref,
62
+ ...props
63
+ }: InputProps) {
64
+
65
+
66
+ const [activeSuggestion, setActiveSuggestion] = useState(0);
67
+ const [showSuggestions, setShowSuggestions] = useState(false);
68
+ const [dataSuggestions, setDataSuggestions] = useState<string[] | undefined>([]);
69
+ const [filteredSuggestions, setFilteredSuggestions] = useState<string[] | undefined>([]);
70
+
71
+
72
+ // =========================>
73
+ // ## Initial
74
+ // =========================>
75
+ const inputHandler = useInputHandler(props.name, value, validations, register, props.type == "file")
76
+ const randomId = useInputRandomId()
77
+
78
+
79
+ // =========================>
80
+ // ## Invalid handler
81
+ // =========================>
82
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
83
+
84
+
85
+ // =========================>
86
+ // ## Change value handler
87
+ // =========================>
88
+ useEffect(() => {
89
+ if (inputHandler.value && typeof inputHandler.value === "string") {
90
+ let newVal = onlyAlphabet ? inputHandler.value.replace(/[^A-Za-z ]+/g, "") : inputHandler.value;
91
+
92
+ if (uppercase) newVal = newVal.toUpperCase();
93
+ if (lowercase) newVal = newVal.toLowerCase();
94
+
95
+ if (validations && validation.hasRules(validations, "max")) newVal = newVal.slice(0, parseInt(validation.getRules(validations, "max") || "0"));
96
+
97
+ inputHandler.setValue(newVal);
98
+ }
99
+ }, [inputHandler.value, onlyAlphabet, uppercase, lowercase, validations]);
100
+
101
+
102
+ // =========================>
103
+ // ## suggestions handler
104
+ // =========================>
105
+ useEffect(() => {
106
+ setDataSuggestions(suggestions);
107
+ }, [suggestions]);
108
+
109
+ const filterSuggestion = (e: any) => {
110
+ if (dataSuggestions?.length) {
111
+ let filteredSuggestion = [];
112
+
113
+ if (e.target.value) {
114
+ filteredSuggestion = dataSuggestions
115
+ .filter((suggestion) => suggestion.toLowerCase().indexOf(e.target.value.toLowerCase()) > -1)
116
+ .slice(0, 10);
117
+ } else {
118
+ filteredSuggestion = dataSuggestions.slice(0, 10);
119
+ }
120
+
121
+ setActiveSuggestion(-1);
122
+ setFilteredSuggestions(filteredSuggestion);
123
+ setShowSuggestions(true);
124
+ }
125
+ };
126
+
127
+
128
+ const onKeyDownSuggestion = (e: any) => {
129
+ if (dataSuggestions?.length) {
130
+ if (e.keyCode === 13) {
131
+ const resultValue = filteredSuggestions?.at(activeSuggestion);
132
+ setActiveSuggestion(-1);
133
+ setFilteredSuggestions([]);
134
+ setShowSuggestions(false);
135
+ inputHandler.setValue(resultValue ? resultValue : inputHandler.value);
136
+ if (onChange) {
137
+ onChange(resultValue ? resultValue : inputHandler.value);
138
+ }
139
+ e.preventDefault();
140
+ } else if (e.keyCode === 38) {
141
+ if (activeSuggestion === 0) {
142
+ return;
143
+ }
144
+
145
+ setActiveSuggestion(activeSuggestion - 1);
146
+ } else if (e.keyCode === 40) {
147
+ if (activeSuggestion + 1 >= (filteredSuggestions?.length || 0)) {
148
+ return;
149
+ }
150
+
151
+ setActiveSuggestion(activeSuggestion + 1);
152
+ }
153
+ }
154
+ };
155
+
156
+ return (
157
+ <>
158
+ <div className="relative flex flex-col gap-y-0.5">
159
+ <label
160
+ htmlFor={randomId}
161
+ className={cn(
162
+ "input-label",
163
+ props.disabled && "input-label-disabled",
164
+ inputHandler.focus && "input-label-focus",
165
+ !!invalidMessage && "input-label-error",
166
+ pcn<CT>(className, "label"),
167
+ props.disabled && pcn<CT>(className, "label", "disabled"),
168
+ inputHandler.focus && pcn<CT>(className, "label", "focus"),
169
+ !!invalidMessage && pcn<CT>(className, "label", "error"),
170
+ )}
171
+ >
172
+ {label}
173
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
174
+ </label>
175
+
176
+ {tip && (
177
+ <small
178
+ className={cn(
179
+ "input-tip",
180
+ props.disabled && "input-tip-disabled",
181
+ pcn<CT>(className, "tip"),
182
+ props.disabled && pcn<CT>(className, "tip", "disabled"),
183
+ )}
184
+ >{tip}</small>
185
+ )}
186
+
187
+ <div className="relative">
188
+ <input
189
+ {...props}
190
+ ref={ref}
191
+ id={randomId}
192
+ placeholder={!multiple || (multiple && !inputHandler.value?.length) ? props.placeholder : ""}
193
+ className={cn(
194
+ "input",
195
+ props.type == "file" && "input-file",
196
+ leftIcon && "input-with-left-icon",
197
+ rightIcon && "input-with-right-icon",
198
+ !!invalidMessage && "input-error",
199
+ pcn<CT>(className, "base"),
200
+ !!invalidMessage && pcn<CT>(className, "base", "error"),
201
+ )}
202
+ value={!multiple ? inputHandler.value: undefined}
203
+ onChange={(e) => {
204
+ if(!multiple) {
205
+ inputHandler.setValue(e.target.value);
206
+ inputHandler.setIdle(false);
207
+ onChange?.(props.type == "file" ? e.target?.files && e.target?.files[0] : e.target.value);
208
+ dataSuggestions?.length && filterSuggestion(e);
209
+ }
210
+ }}
211
+ onFocus={(e) => {
212
+ props.onFocus?.(e);
213
+ inputHandler.setFocus(true);
214
+ dataSuggestions?.length && filterSuggestion(e);
215
+ }}
216
+ onBlur={(e) => {
217
+ props.onBlur?.(e);
218
+ setTimeout(() => inputHandler.setFocus(false), 100);
219
+ }}
220
+ onKeyDown={(e) => {
221
+ dataSuggestions?.length && onKeyDownSuggestion(e);
222
+
223
+ if (multiple && e.key === "Enter" || e.key === ",") {
224
+ e.preventDefault();
225
+ const currentValue = e.currentTarget.value.trim();
226
+ if (!currentValue) return;
227
+
228
+ const currentValues = Array.isArray(inputHandler.value) ? [...inputHandler.value] : [];
229
+ if (!currentValues.includes(currentValue)) {
230
+ const newValues = [...currentValues, currentValue];
231
+ onChange?.(newValues);
232
+ inputHandler.setValue(newValues);
233
+ e.currentTarget.value = "";
234
+ }
235
+ }
236
+ }}
237
+ autoComplete={props.autoComplete || dataSuggestions?.length ? "off" : ""}
238
+ />
239
+
240
+
241
+ {(multiple) && (
242
+ <InputValues
243
+ value={inputHandler.value || []}
244
+ isFocus={inputHandler.focus}
245
+ onFocus={() => setTimeout(() => inputHandler.setFocus(true), 110)}
246
+ onDelete={(_, index) => {
247
+ const values = Array().concat(inputHandler.value);
248
+ const newValues = values.filter((_, val) => val != index);
249
+
250
+ inputHandler.setValue(newValues);
251
+ onChange?.(newValues);
252
+ }}
253
+ className={`${!inputHandler.focus && (leftIcon ? "ml-[2.5rem]" : "ml-[1rem]")}`}
254
+ style={{ maxWidth: `calc(100% - ${leftIcon ? "5.2rem" : "2rem"})` }}
255
+ />
256
+ )}
257
+
258
+ {leftIcon && (
259
+ <Icon
260
+ className={cn(
261
+ "input-icon",
262
+ "input-icon-left",
263
+ props.disabled && "input-icon-disabled",
264
+ inputHandler.focus && "input-icon-focus",
265
+ pcn<CT>(className, "icon"),
266
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
267
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
268
+ )}
269
+ icon={leftIcon}
270
+ />
271
+ )}
272
+
273
+ {rightIcon && (
274
+ <Icon
275
+ className={cn(
276
+ "input-icon",
277
+ "input-icon-right",
278
+ props.disabled && "input-icon-disabled",
279
+ inputHandler.focus && "input-icon-focus",
280
+ pcn<CT>(className, "icon"),
281
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
282
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
283
+ )}
284
+ icon={rightIcon}
285
+ />
286
+ )}
287
+ </div>
288
+
289
+ {!!dataSuggestions?.length && showSuggestions && !!filteredSuggestions?.length && (
290
+ <div>
291
+ <ul
292
+ className={cn(
293
+ "input-suggest-container",
294
+ inputHandler.focus && "input-suggest-container-active",
295
+ pcn<CT>(className, "suggest"),
296
+ )}
297
+ >
298
+ {filteredSuggestions.map((suggestion, key) => {
299
+ return (
300
+ <li
301
+ className={cn(
302
+ "input-suggest",
303
+ inputHandler.value == suggestion && "input-suggest-active",
304
+ pcn<CT>(className, "suggest-item"),
305
+ inputHandler.value == suggestion && pcn<CT>(className, "suggest-item", "active"),
306
+ )}
307
+ key={suggestion}
308
+ onMouseDown={() => {
309
+ setTimeout(() => inputHandler.setFocus(true), 110);
310
+ }}
311
+ onMouseUp={() => {
312
+ setActiveSuggestion(key);
313
+ setFilteredSuggestions([]);
314
+ setShowSuggestions(false);
315
+ inputHandler.setValue(filteredSuggestions[key] || inputHandler.value);
316
+ onChange?.(filteredSuggestions[key] || inputHandler.value);
317
+ setTimeout(() => inputHandler.setFocus(false), 120);
318
+ }}
319
+ >
320
+ {suggestion}
321
+ </li>
322
+ );
323
+ })}
324
+ </ul>
325
+ </div>
326
+ )}
327
+
328
+ {invalidMessage && (
329
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
330
+ )}
331
+ </div>
332
+ </>
333
+ );
334
+ }
@@ -0,0 +1,174 @@
1
+ "use client"
2
+
3
+ import { ReactNode, useEffect, useState } from "react";
4
+ import { api, ApiType, cn, pcn, useInputHandler, useValidation, validation, ValidationRules } from "@utils";
5
+ import { CheckboxComponent } from "./Checkbox.component";
6
+
7
+
8
+
9
+ type CT = "label" | "tip" | "error" | "input" | "icon";
10
+
11
+ export interface InputCheckboxOptionProps {
12
+ value: string | number;
13
+ label: string;
14
+ };
15
+
16
+ export interface InputCheckboxProps {
17
+ name : string;
18
+ label ?: string;
19
+ tip ?: string | ReactNode;
20
+ vertical ?: boolean;
21
+
22
+ value ?: string[] | number[];
23
+ disabled ?: boolean;
24
+ invalid ?: string;
25
+
26
+ options ?: InputCheckboxOptionProps[];
27
+ serverOptionControl ?: ApiType;
28
+ customOptions ?: any;
29
+ validations ?: ValidationRules;
30
+
31
+ onChange ?: (value: string[] | number[]) => any;
32
+ register ?: (name: string, validations?: ValidationRules) => void;
33
+ unregister ?: (name: string) => void;
34
+
35
+ /** Use custom class with: "label::", "tip::", "error::", "icon::". */
36
+ className ?: string;
37
+
38
+ /** Use custom class with: "label::", "checked::", "error::". */
39
+ classNameCheckbox ?: string;
40
+ };
41
+
42
+
43
+
44
+ export function InputCheckboxComponent({
45
+ name,
46
+ label,
47
+ tip,
48
+ vertical,
49
+ className="",
50
+ classNameCheckbox="",
51
+
52
+ value,
53
+ disabled,
54
+ invalid,
55
+
56
+ options,
57
+ serverOptionControl,
58
+ customOptions,
59
+ validations,
60
+
61
+ register,
62
+ unregister,
63
+ onChange,
64
+ }: InputCheckboxProps) {
65
+
66
+ const [dataOptions, setDataOptions] = useState<InputCheckboxOptionProps[]>([]);
67
+ const [loading, setLoading] = useState(false);
68
+
69
+
70
+ // =========================>
71
+ // ## initial
72
+ // =========================>
73
+ const inputHandler = useInputHandler(name, value, validations, register, false)
74
+
75
+
76
+ // =========================>
77
+ // ## Invalid handler
78
+ // =========================>
79
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
80
+
81
+
82
+ // =========================>
83
+ // ## fetch option
84
+ // =========================>
85
+ useEffect(() => {
86
+ const fetchOptions = async () => {
87
+ setLoading(true);
88
+ const mutateOptions = await api(serverOptionControl || {});
89
+ if (mutateOptions?.status == 200) {
90
+ customOptions ? setDataOptions([customOptions, ...mutateOptions.data]) : setDataOptions(mutateOptions.data);
91
+ setLoading(false);
92
+ }
93
+ };
94
+
95
+ if (serverOptionControl?.path || serverOptionControl?.url) {
96
+ fetchOptions();
97
+ } else {
98
+ !options && setDataOptions([]);
99
+ }
100
+ }, [serverOptionControl?.path, serverOptionControl?.url]);
101
+
102
+
103
+ return (
104
+ <>
105
+ <div className="input-container w-full">
106
+ <label
107
+ className={cn(
108
+ "input-label",
109
+ disabled && "input-label-disabled",
110
+ invalidMessage && "input-label-error",
111
+ pcn<CT>(className, "label"),
112
+ disabled && pcn<CT>(className, "label", "disabled"),
113
+ invalidMessage && pcn<CT>(className, "label", "focus"),
114
+ )}
115
+ >
116
+ {label}
117
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
118
+ </label>
119
+
120
+ {tip && (
121
+ <small
122
+ className={cn(
123
+ "input-tip",
124
+ disabled && "input-tip-disabled",
125
+ pcn<CT>(className, "tip"),
126
+ disabled && pcn<CT>(className, "tip", "disabled"),
127
+ )}
128
+ >{tip}</small>
129
+ )}
130
+
131
+ <div
132
+ className={cn(
133
+ "input input-checkbox-list",
134
+ vertical && "input-checkbox-list-vertical",
135
+ pcn<CT>(className, "input"),
136
+ invalidMessage && "input-error",
137
+ invalidMessage && pcn<CT>(className, "input", "error"),
138
+ )}
139
+ >
140
+ {loading && (vertical ? [1, 2, 3, 4, 5, 6, 7, 8, 9] : [1, 2, 3]).map((_, key) => {
141
+ return <div key={key} className="w-1/3 h-6 rounded-lg"></div>;
142
+ })}
143
+
144
+ {(options || dataOptions) && (options || dataOptions)?.map((option, key) => {
145
+ const checked = Array().concat(inputHandler.value).find((val) => val == option.value);
146
+
147
+ return (
148
+ <CheckboxComponent
149
+ key={key}
150
+ label={option.label}
151
+ name={`option[${option.value}]#${name}`}
152
+ checked={!!checked}
153
+ disabled={disabled}
154
+ className={classNameCheckbox}
155
+ onChange={() => {
156
+ const newVal = (Array.isArray(inputHandler.value) ? inputHandler.value : [])
157
+ .filter((val) => val !== option.value)
158
+ .concat(checked ? [] : [option.value]);
159
+
160
+ inputHandler.setValue(newVal);
161
+ onChange?.(newVal);
162
+ }}
163
+ />
164
+ );
165
+ })}
166
+ </div>
167
+
168
+ {invalidMessage && (
169
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
170
+ )}
171
+ </div>
172
+ </>
173
+ );
174
+ }
@@ -0,0 +1,165 @@
1
+ "use client"
2
+
3
+ import { InputHTMLAttributes, ReactNode } from "react";
4
+ import { Icon, type IconName } from "@skalfa/skalfa-icon";
5
+ import { cn, conversion, pcn, useInputHandler, useInputRandomId, useValidation, validation, ValidationRules } from "@utils";
6
+
7
+
8
+
9
+ type CT = "label" | "tip" | "error" | "input" | "icon";
10
+
11
+ export interface InputCurrencyProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
12
+ label ?: string;
13
+ tip ?: string | ReactNode;
14
+ leftIcon ?: IconName;
15
+ rightIcon ?: IconName;
16
+
17
+ value ?: number;
18
+ invalid ?: string;
19
+ validations ?: ValidationRules;
20
+ format ?: {
21
+ locale ?: string;
22
+ currency ?: string;
23
+ };
24
+
25
+ onChange ?: (value: number) => any;
26
+ register ?: (name: string, validations?: ValidationRules) => void;
27
+ unregister ?: (name: string) => void;
28
+
29
+ /** Use custom class with: "label::", "tip::", "error::", "icon::". */
30
+ className ?: string;
31
+ }
32
+
33
+
34
+
35
+ export function InputCurrencyComponent({
36
+ label,
37
+ tip,
38
+ leftIcon,
39
+ rightIcon,
40
+
41
+ value,
42
+ invalid,
43
+
44
+ validations,
45
+ format,
46
+
47
+ register,
48
+ unregister,
49
+ onChange,
50
+
51
+ className = "",
52
+ ...props
53
+ }: InputCurrencyProps) {
54
+
55
+ // =========================>
56
+ // ## Initial
57
+ // =========================>
58
+ const inputHandler = useInputHandler(props.name, value, validations, register, false)
59
+ const randomId = useInputRandomId()
60
+
61
+
62
+ // =========================>
63
+ // ## Invalid handler
64
+ // =========================>
65
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
66
+
67
+
68
+ return (
69
+ <div className="input-container">
70
+ <label
71
+ htmlFor={randomId}
72
+ className={cn(
73
+ "input-label",
74
+ props.disabled && "input-label-disabled",
75
+ inputHandler.focus && "input-label-focus",
76
+ !!invalidMessage && "input-label-error",
77
+ pcn<CT>(className, "label"),
78
+ props.disabled && pcn<CT>(className, "label", "disabled"),
79
+ inputHandler.focus && pcn<CT>(className, "label", "focus"),
80
+ !!invalidMessage && pcn<CT>(className, "label", "focus"),
81
+ )}
82
+ >
83
+ {label}
84
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
85
+ </label>
86
+
87
+ {tip && (
88
+ <small
89
+ className={cn(
90
+ "input-tip",
91
+ props.disabled && "input-tip-disabled",
92
+ pcn<CT>(className, "tip"),
93
+ props.disabled && pcn<CT>(className, "tip", "disabled"),
94
+ )}
95
+ >{tip}</small>
96
+ )}
97
+
98
+ <div className="relative">
99
+ <input
100
+ {...props}
101
+ id={randomId}
102
+ className={cn(
103
+ "input",
104
+ leftIcon && "input-with-left-icon",
105
+ rightIcon && "input-with-right-icon",
106
+ pcn<CT>(className, "input"),
107
+ !!invalidMessage && "input-error",
108
+ !!invalidMessage && pcn<CT>(className, "input", "error"),
109
+ )}
110
+ value={inputHandler.value ? conversion.currency(inputHandler.value, format?.locale, format?.currency) : ""}
111
+ onChange={(e) => {
112
+ const val = Number(e.target.value.replace(/[^0-9]/g,""));
113
+
114
+ inputHandler.setValue(val);
115
+ inputHandler.setIdle(false);
116
+
117
+ onChange?.(val);
118
+ }}
119
+ onFocus={(e) => {
120
+ props.onFocus?.(e);
121
+ inputHandler.setFocus(true);
122
+ }}
123
+ onBlur={(e) => {
124
+ props.onBlur?.(e);
125
+ setTimeout(() => inputHandler.setFocus(false), 100);
126
+ }}
127
+ autoComplete="off"
128
+ />
129
+
130
+ {leftIcon && (
131
+ <Icon
132
+ className={cn(
133
+ "input-icon",
134
+ "input-icon-left",
135
+ props.disabled && "input-icon-disabled",
136
+ inputHandler.focus && "input-icon-focus",
137
+ pcn<CT>(className, "icon"),
138
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
139
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
140
+ )}
141
+ icon={leftIcon}
142
+ />
143
+ )}
144
+ {rightIcon && (
145
+ <Icon
146
+ className={cn(
147
+ "input-icon",
148
+ "input-icon-right",
149
+ props.disabled && "input-icon-disabled",
150
+ inputHandler.focus && "input-icon-focus",
151
+ pcn<CT>(className, "icon"),
152
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
153
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
154
+ )}
155
+ icon={rightIcon}
156
+ />
157
+ )}
158
+ </div>
159
+
160
+ {invalidMessage && (
161
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
162
+ )}
163
+ </div>
164
+ );
165
+ }