@skalfa/skalfa-component 1.0.7 → 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 (58) hide show
  1. package/package.json +2 -2
  2. package/src/accordion/Accordion.component.tsx +87 -0
  3. package/src/breadcrumb/Breadcrumb.component.tsx +79 -0
  4. package/src/button/Button.component.tsx +89 -0
  5. package/src/card/AlertCard.component.tsx +69 -0
  6. package/src/card/Card.component.tsx +25 -0
  7. package/src/card/DashboardCard.component.tsx +44 -0
  8. package/src/card/GalleryCard.component.tsx +50 -0
  9. package/src/card/ProductCard.component.tsx +65 -0
  10. package/src/card/ProfileCard.component.tsx +71 -0
  11. package/src/carousel/Carousel.component.tsx +111 -0
  12. package/src/chip/Chip.component.tsx +39 -0
  13. package/src/index.ts +70 -0
  14. package/src/input/Checkbox.component.tsx +102 -0
  15. package/src/input/Input.component.tsx +334 -0
  16. package/src/input/InputCheckbox.component.tsx +174 -0
  17. package/src/input/InputCurrency.component.tsx +165 -0
  18. package/src/input/InputDate.component.tsx +356 -0
  19. package/src/input/InputDatetime.component.tsx +267 -0
  20. package/src/input/InputDocument.component.tsx +360 -0
  21. package/src/input/InputImage.component.tsx +535 -0
  22. package/src/input/InputNumber.component.tsx +194 -0
  23. package/src/input/InputOtp.component.tsx +169 -0
  24. package/src/input/InputPassword.component.tsx +245 -0
  25. package/src/input/InputRadio.component.tsx +174 -0
  26. package/src/input/InputTime.component.tsx +280 -0
  27. package/src/input/InputValues.component.tsx +71 -0
  28. package/src/input/Radio.component.tsx +98 -0
  29. package/src/input/Select.component.tsx +557 -0
  30. package/src/modal/BottomSheet.component.tsx +246 -0
  31. package/src/modal/FloatingPage.component.tsx +103 -0
  32. package/src/modal/Modal.component.tsx +95 -0
  33. package/src/modal/ModalConfirm.component.tsx +219 -0
  34. package/src/modal/Toast.component.tsx +125 -0
  35. package/src/nav/Bottombar.component.tsx +72 -0
  36. package/src/nav/Footer.component.tsx +177 -0
  37. package/src/nav/Headbar.component.tsx +33 -0
  38. package/src/nav/Navbar.component.tsx +138 -0
  39. package/src/nav/Sidebar.component.tsx +298 -0
  40. package/src/nav/Tabbar.component.tsx +61 -0
  41. package/src/nav/Wizard.component.tsx +80 -0
  42. package/src/supervision/FormSupervision.component.tsx +425 -0
  43. package/src/supervision/TableSupervision.component.tsx +688 -0
  44. package/src/table/ControlBar.component.tsx +501 -0
  45. package/src/table/FilterComponent.tsx +519 -0
  46. package/src/table/Pagination.component.tsx +152 -0
  47. package/src/table/Table.component.tsx +436 -0
  48. package/src/types.d.ts +7 -0
  49. package/src/typography/TypographyArticle.component.tsx +26 -0
  50. package/src/typography/TypographyColumn.component.tsx +20 -0
  51. package/src/typography/TypographyContent.component.tsx +20 -0
  52. package/src/typography/TypographyTips.component.tsx +20 -0
  53. package/src/wrap/Draggable.component.tsx +303 -0
  54. package/src/wrap/Image.component.tsx +10 -0
  55. package/src/wrap/OutsideClick.component.tsx +48 -0
  56. package/src/wrap/ScrollContainer.component.tsx +107 -0
  57. package/src/wrap/ShortcutProvider.tsx +57 -0
  58. package/src/wrap/Swipe.component.tsx +121 -0
@@ -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 { RadioComponent } from "./Radio.component";
6
+
7
+
8
+
9
+ type CT = "label" | "tip" | "error" | "input" | "icon";
10
+
11
+ export type InputRadioOptionProps = {
12
+ value: string | number;
13
+ label: string;
14
+ };
15
+
16
+ export type InputRadioProps = {
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
+ options ?: InputRadioOptionProps[];
26
+ serverOptionControl ?: ApiType;
27
+ customOptions ?: any;
28
+ validations ?: ValidationRules;
29
+
30
+ onChange ?: (value: string | number) => any;
31
+ register ?: (name: string, validations?: ValidationRules) => void;
32
+ unregister ?: (name: string) => void;
33
+
34
+ /** Use custom class with: "label::", "tip::", "error::", "icon::", "suggest::", "suggest-item::". */
35
+ className ?: string;
36
+ /** Use custom class with: "label::", "checked::", "error::". */
37
+ classNameCheckbox ?: string;
38
+ };
39
+
40
+
41
+
42
+ export function InputRadioComponent({
43
+ name,
44
+ label,
45
+ tip,
46
+ vertical,
47
+
48
+ value,
49
+ disabled,
50
+ invalid,
51
+
52
+ options,
53
+ serverOptionControl,
54
+ customOptions,
55
+ validations,
56
+
57
+ onChange,
58
+ register,
59
+ unregister,
60
+
61
+ className = "",
62
+ classNameCheckbox = "",
63
+ }: InputRadioProps) {
64
+ const [dataOptions, setDataOptions] = useState<InputRadioOptionProps[]>([]);
65
+ const [loading, setLoading] = useState(false);
66
+
67
+
68
+ // =========================>
69
+ // ## Initial
70
+ // =========================>
71
+ const inputHandler = useInputHandler(name, value, validations, register, unregister, false)
72
+
73
+
74
+ // =========================>
75
+ // ## Invalid handler
76
+ // =========================>
77
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
78
+
79
+
80
+ // =========================>
81
+ // ## Fetch option
82
+ // =========================>
83
+ useEffect(() => {
84
+ const fetchOptions = async () => {
85
+ setLoading(true);
86
+ const mutateOptions = await api(serverOptionControl || {});
87
+ if (mutateOptions?.status == 200) {
88
+ customOptions
89
+ ? setDataOptions([customOptions, ...mutateOptions.data])
90
+ : 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-radio-list",
134
+ vertical && "input-radio-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) &&
145
+ (options || dataOptions)?.map((option, key) => {
146
+ return (
147
+ <RadioComponent
148
+ key={key}
149
+ label={option.label}
150
+ name={`option[${option.value}]#${name}`}
151
+ checked={inputHandler.value == option.value}
152
+ disabled={disabled}
153
+ className={classNameCheckbox}
154
+ onChange={() => {
155
+ if (inputHandler.value == option.value) {
156
+ inputHandler.setValue("");
157
+ onChange?.("");
158
+ } else {
159
+ inputHandler.setValue(option.value || "");
160
+ onChange?.(option.value || "");
161
+ }
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,280 @@
1
+ "use client"
2
+
3
+ import { FC, InputHTMLAttributes, ReactNode, useEffect, useMemo, useState } from "react";
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import { cn, pcn, useInputHandler, useInputRandomId, useResponsive, useValidation, validation, ValidationRules } from "@utils";
6
+ import { BottomSheetComponent } from "../modal/BottomSheet.component";
7
+ import { ButtonComponent } from "../button/Button.component";
8
+ import { OutsideClickComponent } from "../wrap/OutsideClick.component";
9
+
10
+
11
+
12
+ type CT = "label" | "tip" | "error" | "input"| "icon";
13
+
14
+ export interface InputTimeProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
15
+ label ?: string;
16
+ tip ?: string | ReactNode;
17
+ leftIcon ?: any;
18
+ rightIcon ?: any;
19
+
20
+ value ?: string;
21
+ invalid ?: string;
22
+ validations ?: ValidationRules;
23
+
24
+ onChange ?: (value: string) => any;
25
+ register ?: (name: string, validations?: ValidationRules) => void;
26
+ unregister ?: (name: string) => void;
27
+
28
+ /** Use custom class with: "label::", "tip::", "error::", "icon::". */
29
+ className ?: string;
30
+ }
31
+
32
+
33
+
34
+ export function InputTimeComponent({
35
+ label,
36
+ tip,
37
+ leftIcon,
38
+ rightIcon,
39
+
40
+ value,
41
+ invalid,
42
+ validations,
43
+
44
+ register,
45
+ unregister,
46
+ onChange,
47
+
48
+ className = "",
49
+ ...props
50
+ }: InputTimeProps) {
51
+ const { isSm } = useResponsive();
52
+
53
+ // =========================>
54
+ // ## Initial
55
+ // =========================>
56
+ const inputHandler = useInputHandler(props.name, value, validations, register, unregister, false)
57
+ const randomId = useInputRandomId()
58
+
59
+
60
+ // =========================>
61
+ // ## Invalid handler
62
+ // =========================>
63
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
64
+
65
+
66
+ return (
67
+ <>
68
+ <div className="input-container">
69
+ <label
70
+ htmlFor={randomId}
71
+ className={cn(
72
+ "input-label",
73
+ props.disabled && "input-label-disabled",
74
+ inputHandler.focus && "input-label-focus",
75
+ !!invalidMessage && "input-label-error",
76
+ pcn<CT>(className, "label"),
77
+ )}
78
+ >
79
+ {label}
80
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
81
+ </label>
82
+
83
+ {tip && (
84
+ <small
85
+ className={cn(
86
+ "input-tip",
87
+ props.disabled && "input-tip-disabled",
88
+ pcn<CT>(className, "tip"),
89
+ )}
90
+ >{tip}</small>
91
+ )}
92
+
93
+ <OutsideClickComponent onOutsideClick={!isSm ? () => inputHandler.setFocus(false) : undefined}>
94
+ <div className="relative">
95
+ <input
96
+ {...props}
97
+ id={randomId}
98
+ className={cn(
99
+ "input",
100
+ leftIcon && "input-with-left-icon",
101
+ rightIcon && "input-with-right-icon",
102
+ pcn<CT>(className, "input"),
103
+ !!invalidMessage && "input-error"
104
+ )}
105
+ value={inputHandler.value}
106
+ onChange={(e) => {
107
+ inputHandler.setValue(e.target.value);
108
+ inputHandler.setIdle(false);
109
+ onChange?.(e.target.value);
110
+ }}
111
+ onFocus={(e) => {
112
+ props.onFocus?.(e);
113
+ inputHandler.setFocus(true);
114
+ }}
115
+ autoComplete="off"
116
+ inputMode={isSm ? "none" : undefined}
117
+ />
118
+
119
+ {leftIcon && (
120
+ <FontAwesomeIcon
121
+ className={cn(
122
+ "input-icon",
123
+ "input-icon-left",
124
+ props.disabled && "input-icon-disabled",
125
+ inputHandler.focus && "input-icon-focus",
126
+ pcn<CT>(className, "icon"),
127
+ )}
128
+ icon={leftIcon}
129
+ />
130
+ )}
131
+
132
+ {rightIcon && (
133
+ <FontAwesomeIcon
134
+ className={cn(
135
+ "input-icon",
136
+ "input-icon-right",
137
+ props.disabled && "input-icon-disabled",
138
+ inputHandler.focus && "input-icon-focus",
139
+ pcn<CT>(className, "icon"),
140
+ )}
141
+ icon={rightIcon}
142
+ />
143
+ )}
144
+
145
+ {!isSm && inputHandler.focus && (
146
+ <div className="input-time-picker-popover">
147
+ <InputTimePickerComponent
148
+ onChange={(time) => {
149
+ inputHandler.setValue(time);
150
+ onChange?.(time);
151
+ }}
152
+ />
153
+ </div>
154
+ )}
155
+ </div>
156
+ </OutsideClickComponent>
157
+
158
+ {invalidMessage && (
159
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
160
+ )}
161
+ </div>
162
+
163
+ {isSm && (
164
+ <BottomSheetComponent
165
+ show={inputHandler.focus}
166
+ onClose={() => inputHandler.setFocus(false)}
167
+ size={380}
168
+ footer={
169
+ <div className="p-4">
170
+ <ButtonComponent
171
+ label="Selesai"
172
+ variant="outline"
173
+ onClick={() => inputHandler.setFocus(false)}
174
+ block
175
+ />
176
+ </div>
177
+ }
178
+ >
179
+ <div className="p-4">
180
+ <InputTimePickerComponent
181
+ onChange={(time) => {
182
+ inputHandler.setValue(time);
183
+ onChange?.(time);
184
+ }}
185
+ />
186
+ </div>
187
+ </BottomSheetComponent>
188
+ )}
189
+ </>
190
+ );
191
+ }
192
+
193
+
194
+
195
+ interface InputTimePickerProps {
196
+ onChange ?: (time: string) => void;
197
+ rightElement ?: ReactNode;
198
+ };
199
+
200
+
201
+
202
+
203
+ export const InputTimePickerComponent: FC<InputTimePickerProps> = ({
204
+ onChange,
205
+ rightElement,
206
+ }) => {
207
+ const [hour, setHour] = useState(0);
208
+ const [minute, setMinute] = useState(0);
209
+ const [second, setSecond] = useState(0);
210
+
211
+ const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, "0"));
212
+ const minutes = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, "0"));
213
+ const seconds = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, "0"));
214
+
215
+ const handleSelect = (type: "h" | "m" | "s", val: string) => {
216
+ if (type === "h") setHour(Number(val));
217
+ if (type === "m") setMinute(Number(val));
218
+ if (type === "s") setSecond(Number(val));
219
+ };
220
+
221
+ useEffect(() => {
222
+ const formatted = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:${String(second).padStart(2, "0")}`;
223
+ onChange?.(formatted);
224
+ }, [hour, minute, second]);
225
+
226
+ const renderColumn = (items: string[], type: "h" | "m" | "s", activeValue: number) => (
227
+ <div className="flex-1 overflow-y-auto text-center input-scroll">
228
+ {items.map((item) => {
229
+ const active = Number(item) === activeValue;
230
+
231
+ return (
232
+ <div
233
+ key={item}
234
+ onClick={() => handleSelect(type, item)}
235
+ className={cn(
236
+ "p-2 cursor-pointer rounded text-sm",
237
+ active ? "bg-primary text-background font-semibold" : "hover:bg-light-primary"
238
+ )}
239
+ >{item}</div>
240
+ );
241
+ })}
242
+ </div>
243
+ );
244
+
245
+ const timeSlots = useMemo(() => {
246
+ const newTimeSlots = [];
247
+
248
+ for (let i = 0; i <= 24 * 60; i += 30) {
249
+ const hours = Math.floor(i / 60);
250
+ const minutes = i % 60;
251
+ const label = `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`;
252
+
253
+ newTimeSlots.push(label);
254
+ }
255
+
256
+ return newTimeSlots;
257
+ }, [])
258
+
259
+ return (
260
+ <div className="w-full max-h-[260] flex gap-2">
261
+ <div className="w-1/3 overflow-y-auto bg-background rounded-[6px] input-scroll">
262
+ {timeSlots.map((time) => (
263
+ <div
264
+ key={time}
265
+ className="p-2 text-sm rounded cursor-pointer hover:bg-light-primary text-center"
266
+ onClick={() => onChange?.(time)}
267
+ >{time}</div>
268
+ ))}
269
+ </div>
270
+
271
+ <div className="w-2/3 flex gap-2">
272
+ {renderColumn(hours, "h", hour)}
273
+ {renderColumn(minutes, "m", minute)}
274
+ {renderColumn(seconds, "s", second)}
275
+ </div>
276
+
277
+ {rightElement && <div>{rightElement}</div>}
278
+ </div>
279
+ );
280
+ };
@@ -0,0 +1,71 @@
1
+ import { CSSProperties } from 'react';
2
+ import { faTimes } from '@fortawesome/free-solid-svg-icons';
3
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4
+ import { cn } from '@utils';
5
+
6
+ export function InputValues({
7
+ value,
8
+ className,
9
+ onDelete,
10
+ isFocus,
11
+ onFocus,
12
+ style,
13
+ } : {
14
+ value?: string[] | number[];
15
+ className?: string;
16
+ onDelete?: (val: string | number, index: string | number) => void;
17
+ isFocus?: boolean,
18
+ onFocus?: () => void;
19
+ style?: CSSProperties
20
+ }) {
21
+ if(isFocus) {
22
+ return (
23
+ <>
24
+ {!!value?.length && (
25
+ <div
26
+ className={cn("input-values-dropdown", className)}
27
+ onClick={() => onFocus?.()}
28
+ >
29
+ {value?.map((item, key) => {
30
+ return (
31
+ <div key={key} className="input-values-item">
32
+ <span className="">{item}</span>
33
+ <FontAwesomeIcon
34
+ icon={faTimes}
35
+ className="input-values-delete"
36
+ onClick={() => onDelete?.(item, key)}
37
+ />
38
+ </div>
39
+ );
40
+ })}
41
+ </div>
42
+ )}
43
+ </>
44
+ )
45
+ } else {
46
+ return (
47
+ <>
48
+ <div
49
+ className={cn("input-values-preview", className)}
50
+ style={style}
51
+ onClick={() => onFocus?.()}
52
+ >
53
+ <div className="input-values-container">
54
+ {value?.map((item, key) => {
55
+ return (
56
+ <div key={key} className="input-values-item">
57
+ <span>{item}</span>
58
+ <FontAwesomeIcon
59
+ icon={faTimes}
60
+ className="input-values-delete"
61
+ onClick={() => onDelete?.(item, key)}
62
+ />
63
+ </div>
64
+ );
65
+ })}
66
+ </div>
67
+ </div>
68
+ </>
69
+ )
70
+ }
71
+ }
@@ -0,0 +1,98 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { cn, pcn, useInputRandomId } from "@utils";
5
+
6
+ type CT = "label" | "checked" | "error" | "input";
7
+
8
+ export interface RadioProps {
9
+ name : string;
10
+ label ?: string;
11
+
12
+ value ?: string;
13
+ disabled ?: boolean;
14
+ checked ?: boolean;
15
+ invalid ?: string;
16
+
17
+ onChange ?: () => void;
18
+
19
+ /** Use custom class with: "label::", "checked::", "error::". */
20
+ className ?: string;
21
+ };
22
+
23
+ export function RadioComponent({
24
+ name,
25
+ label,
26
+
27
+ value,
28
+ disabled,
29
+ checked,
30
+ invalid,
31
+
32
+ onChange,
33
+
34
+ className = "",
35
+ }: RadioProps) {
36
+
37
+ // =========================>
38
+ // ## initial
39
+ // =========================>
40
+ const randomId = useInputRandomId()
41
+ const [invalidMessage, setInvalidMessage] = useState("");
42
+
43
+
44
+ // =========================>
45
+ // ## invalid handler
46
+ // =========================>
47
+ useEffect(() => {
48
+ setInvalidMessage(invalid || "");
49
+ }, [invalid]);
50
+
51
+
52
+ return (
53
+ <>
54
+ <div className="radio-container">
55
+ <input
56
+ type="checkbox"
57
+ className="hidden"
58
+ id={randomId}
59
+ name={name}
60
+ onChange={onChange}
61
+ defaultChecked={checked}
62
+ value={value}
63
+ disabled={disabled}
64
+ />
65
+
66
+ <label
67
+ htmlFor={randomId}
68
+ className={cn(
69
+ "radio-wrapper",
70
+ disabled && "radio-wrapper-disabled"
71
+ )}
72
+ >
73
+ <div
74
+ className={cn(
75
+ "radio-input",
76
+ checked && "radio-input-checked",
77
+ checked && pcn<CT>(className, "checked"),
78
+ pcn<CT>(className, "input"),
79
+ )}
80
+ ></div>
81
+ <div
82
+ className={cn(
83
+ "radio-label",
84
+ checked && "radio-label-checked",
85
+ pcn<CT>(className, "label"),
86
+ checked && pcn<CT>(className, "label", "checked"),
87
+ disabled && pcn<CT>(className, "label", "disabled"),
88
+ )}
89
+ >{label}</div>
90
+ </label>
91
+
92
+ {invalidMessage && (
93
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
94
+ )}
95
+ </div>
96
+ </>
97
+ );
98
+ }