@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,267 @@
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, useResponsive, useValidation, validation, ValidationRules } from "@utils";
6
+ import { OutsideClickComponent } from "../wrap/OutsideClick.component";
7
+ import { InputDatePickerComponent } from "./InputDate.component";
8
+ import { InputTimePickerComponent } from "./InputTime.component";
9
+ import { BottomSheetComponent } from "../modal/BottomSheet.component";
10
+ import { ButtonComponent } from "../button/Button.component";
11
+ import { TabbarComponent } from "../nav/Tabbar.component";
12
+
13
+
14
+
15
+ type CT = "label" | "tip" | "error" | "input" | "icon";
16
+
17
+ export interface InputDateTimeProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
18
+ label ?: string;
19
+ tip ?: string | ReactNode;
20
+ leftIcon ?: any;
21
+ rightIcon ?: any;
22
+
23
+ value ?: string;
24
+ invalid ?: string;
25
+ validations ?: ValidationRules;
26
+
27
+ onChange ?: (value: string) => any;
28
+ register ?: (name: string, validations?: ValidationRules) => void;
29
+ unregister ?: (name: string) => void;
30
+
31
+ /** Use custom class with: "label::", "tip::", "error::", "icon::", "suggest::", "suggest-item::". */
32
+ className ?: string;
33
+ }
34
+
35
+
36
+
37
+ export function InputDatetimeComponent({
38
+ label,
39
+ tip,
40
+ leftIcon,
41
+ rightIcon,
42
+
43
+ value,
44
+ invalid,
45
+ validations,
46
+
47
+ register,
48
+ unregister,
49
+ onChange,
50
+
51
+ className = "",
52
+ ...props
53
+ }: InputDateTimeProps) {
54
+ const { isSm } = useResponsive();
55
+
56
+ const [pickerType, setPickerType] = useState<"date" | "time">("date");
57
+ const [dateValue, setDateValue] = useState("");
58
+ const [timeValue, setTimeValue] = useState("");
59
+
60
+
61
+ // =========================>
62
+ // ## Initial
63
+ // =========================>
64
+ const inputHandler = useInputHandler(props.name, value, validations, register, unregister, false)
65
+ const randomId = useInputRandomId()
66
+
67
+
68
+ // =========================>
69
+ // ## Invalid handler
70
+ // =========================>
71
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
72
+
73
+
74
+ // =========================>
75
+ // ## change value handler
76
+ // =========================>
77
+ useEffect(() => {
78
+ inputHandler.setValue(value || "");
79
+ value && inputHandler.setValue(false);
80
+
81
+ if (value) {
82
+ const [d, t] = value.split(" ");
83
+ setDateValue(d || "");
84
+ setTimeValue(t || "");
85
+ }
86
+ }, [value]);
87
+
88
+
89
+ const handleChange = (date: string, time: string) => {
90
+ const newVal = `${date} ${time}`;
91
+ inputHandler.setValue(newVal.trim());
92
+ onChange?.(newVal.trim());
93
+ };
94
+
95
+
96
+ return (
97
+ <>
98
+ <div className="input-container">
99
+ <label
100
+ htmlFor={randomId}
101
+ className={cn(
102
+ "input-label",
103
+ props.disabled && "input-label-disabled",
104
+ inputHandler.focus && "input-label-focus",
105
+ !!invalidMessage && "input-label-error",
106
+ pcn<CT>(className, "label"),
107
+ )}
108
+ >
109
+ {label}
110
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
111
+ </label>
112
+
113
+ {tip && (
114
+ <small
115
+ className={cn(
116
+ "input-tip",
117
+ props.disabled && "input-tip-disabled",
118
+ pcn<CT>(className, "tip"),
119
+ )}
120
+ >{tip}</small>
121
+ )}
122
+
123
+ <OutsideClickComponent onOutsideClick={!isSm ? () => inputHandler.setFocus(false) : undefined}>
124
+ <div className="relative">
125
+ <input
126
+ {...props}
127
+ id={randomId}
128
+ readOnly
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
+ )}
136
+ value={inputHandler.value}
137
+ onFocus={(e) => {
138
+ props.onFocus?.(e);
139
+ inputHandler.setFocus(true);
140
+ }}
141
+ autoComplete="off"
142
+ inputMode={isSm ? "none" : undefined}
143
+ />
144
+
145
+ {leftIcon && (
146
+ <FontAwesomeIcon
147
+ className={cn(
148
+ "input-icon",
149
+ "input-icon-left",
150
+ props.disabled && "input-icon-disabled",
151
+ inputHandler.focus && "input-icon-focus",
152
+ pcn<CT>(className, "icon"),
153
+ )}
154
+ icon={leftIcon}
155
+ />
156
+ )}
157
+
158
+ {rightIcon && (
159
+ <FontAwesomeIcon
160
+ className={cn(
161
+ "input-icon",
162
+ "input-icon-right",
163
+ props.disabled && "input-icon-disabled",
164
+ inputHandler.focus && "input-icon-focus",
165
+ pcn<CT>(className, "icon"),
166
+ )}
167
+ icon={rightIcon}
168
+ />
169
+ )}
170
+
171
+ {!isSm && inputHandler.focus && (
172
+ <>
173
+ <div className="input-datetime-picker-popover">
174
+ <TabbarComponent
175
+ items={[
176
+ {
177
+ label: "Tanggal",
178
+ value: 'date'
179
+ },
180
+ {
181
+ label: "Jam",
182
+ value: 'time'
183
+ },
184
+ ]}
185
+ active={pickerType}
186
+ onChange={(e) => setPickerType(e as "time" | "date")}
187
+ className="mb-4"
188
+ />
189
+ {pickerType === "date" ? (
190
+ <InputDatePickerComponent
191
+ onChange={(e) => {
192
+ setDateValue(e);
193
+ handleChange(e, timeValue);
194
+ }}
195
+ />
196
+ ) : (
197
+ <InputTimePickerComponent
198
+ onChange={(e) => {
199
+ setTimeValue(e);
200
+ handleChange(dateValue, e);
201
+ }}
202
+ />
203
+ )}
204
+ </div>
205
+ </>
206
+ )}
207
+ </div>
208
+ </OutsideClickComponent>
209
+
210
+ {invalidMessage && (
211
+ <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
212
+ )}
213
+ </div>
214
+
215
+ {isSm && (
216
+ <BottomSheetComponent
217
+ show={inputHandler.focus}
218
+ onClose={() => inputHandler.setFocus(false)}
219
+ size={430}
220
+ footer={
221
+ <div className="p-4">
222
+ <ButtonComponent
223
+ label="Selesai"
224
+ variant="outline"
225
+ onClick={() => inputHandler.setFocus(false)}
226
+ block
227
+ />
228
+ </div>
229
+ }
230
+ >
231
+ <div className="p-4">
232
+ <TabbarComponent
233
+ items={[
234
+ {
235
+ label: "Tanggal",
236
+ value: 'date'
237
+ },
238
+ {
239
+ label: "Jam",
240
+ value: 'time'
241
+ },
242
+ ]}
243
+ active={pickerType}
244
+ onChange={(e) => setPickerType(e as "time" | "date")}
245
+ className="mb-4"
246
+ />
247
+ {pickerType === "date" ? (
248
+ <InputDatePickerComponent
249
+ onChange={(e) => {
250
+ setDateValue(e);
251
+ handleChange(e, timeValue);
252
+ }}
253
+ />
254
+ ) : (
255
+ <InputTimePickerComponent
256
+ onChange={(e) => {
257
+ setTimeValue(e);
258
+ handleChange(dateValue, e);
259
+ }}
260
+ />
261
+ )}
262
+ </div>
263
+ </BottomSheetComponent>
264
+ )}
265
+ </>
266
+ );
267
+ }
@@ -0,0 +1,360 @@
1
+ "use client"
2
+
3
+ import { useState, useRef, InputHTMLAttributes, ReactNode } from "react";
4
+ import { faPlus, faTimes } from "@fortawesome/free-solid-svg-icons";
5
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
+ import { cn, pcn, registry, useInputHandler, useInputRandomId, useResponsive, useValidation, validation, ValidationRules } from "@utils";
7
+ import { ButtonComponent } from "../button/Button.component";
8
+ import { FloatingPageComponent } from "../modal/FloatingPage.component";
9
+ import { BottomSheetComponent } from "../modal/BottomSheet.component";
10
+
11
+ const DocumentViewerComponent = (props: any) => {
12
+ const Comp = registry.get("DocumentViewerComponent");
13
+ return Comp ? <Comp {...props} /> : null;
14
+ };
15
+
16
+ const DocumentViewerIcon = (ext: string) => {
17
+ const getIcon = registry.get("DocumentViewerIcon");
18
+ return getIcon ? getIcon(ext) : null;
19
+ };
20
+
21
+
22
+
23
+ type DocFile = {
24
+ id: string;
25
+ file: File;
26
+ url: string;
27
+ type: string;
28
+ };
29
+
30
+ type CT = "label" | "tip" | "error" | "base" | "icon";
31
+
32
+ export interface InputDocumentProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange"> {
33
+ label ?: string;
34
+ tip ?: string | ReactNode;
35
+ leftIcon ?: any;
36
+ rightIcon ?: any;
37
+
38
+ value ?: any;
39
+ invalid ?: string;
40
+
41
+ validations ?: ValidationRules;
42
+
43
+ onChange ?: (value: any) => any;
44
+ register ?: (name: string, validations ?: ValidationRules) => void;
45
+ unregister ?: (name: string) => void;
46
+
47
+ /** Use custom class with: "label::", "tip::", "error::", "icon::", "suggest::", "suggest-item::". */
48
+ className ?: string;
49
+ }
50
+
51
+
52
+
53
+ export function InputDocumentComponent({
54
+ label,
55
+ tip,
56
+ leftIcon,
57
+ rightIcon,
58
+ className = "",
59
+
60
+ value,
61
+ invalid,
62
+
63
+ validations,
64
+
65
+ register,
66
+ unregister,
67
+ onChange,
68
+
69
+ ...props
70
+ } : InputDocumentProps) {
71
+ const { isSm } = useResponsive();
72
+
73
+ // =========================>
74
+ // ## Initial
75
+ // =========================>
76
+ const inputHandler = useInputHandler(props.name, value, validations, register, unregister, props.type == "file")
77
+ const randomId = useInputRandomId()
78
+
79
+ // =========================>
80
+ // ## Invalid handler
81
+ // =========================>
82
+ const [invalidMessage] = useValidation(inputHandler.value, validations, invalid, inputHandler.idle);
83
+
84
+
85
+ return (
86
+ <>
87
+ <div className="input-container">
88
+ <label
89
+ htmlFor={randomId}
90
+ className={cn(
91
+ "input-label",
92
+ props.disabled && "input-label-disabled",
93
+ inputHandler.focus && "input-label-focus",
94
+ !!invalidMessage && "input-label-error",
95
+ pcn<CT>(className, "label"),
96
+ props.disabled && pcn<CT>(className, "label", "disabled"),
97
+ inputHandler.focus && pcn<CT>(className, "label", "focus"),
98
+ !!invalidMessage && pcn<CT>(className, "label", "focus"),
99
+ )}
100
+ >
101
+ {label}
102
+ {validations && validation.hasRules(validations, "required") && <span className="text-danger ml-1">*</span>}
103
+ </label>
104
+
105
+ {tip && (
106
+ <small
107
+ className={cn(
108
+ "input-tip",
109
+ props.disabled && "input-tip-disabled",
110
+ pcn<CT>(className, "tip"),
111
+ props.disabled && pcn<CT>(className, "tip", "disabled"),
112
+ )}
113
+ >{tip}</small>
114
+ )}
115
+
116
+ <div className="relative">
117
+ <input
118
+ {...props}
119
+ id={randomId}
120
+ placeholder={!inputHandler.value ? props.placeholder : ""}
121
+ className={cn(
122
+ "input cursor-pointer",
123
+ props.type == "file" && "input-file",
124
+ leftIcon && "input-with-left-icon",
125
+ rightIcon && "input-with-right-icon",
126
+ pcn<CT>(className, "base"),
127
+ !!invalidMessage && "input-error",
128
+ !!invalidMessage && pcn<CT>(className, "base", "error"),
129
+ )}
130
+ onFocus={(e) => {
131
+ props.onFocus?.(e);
132
+ inputHandler.setFocus(true);
133
+ }}
134
+ inputMode="none"
135
+ />
136
+
137
+
138
+ {(inputHandler.value) && (
139
+ <div
140
+ className={cn(
141
+ "input-document-values",
142
+ leftIcon ? "input-document-values-left-icon" : "input-document-values-no-icon"
143
+ )}
144
+ style={{ maxWidth: `calc(100% - ${leftIcon ? "5.2rem" : "2rem"})` }}
145
+ onClick={() => {
146
+ inputHandler.setFocus(true);
147
+ }}
148
+ >
149
+ {(Array.isArray(inputHandler.value) ? inputHandler.value : []).map((f) => {
150
+ return (
151
+ <span
152
+ key={f.id}
153
+ className="input-document-value-item"
154
+ >
155
+ <FontAwesomeIcon icon={DocumentViewerIcon(f.file.name.split(".").pop()?.toLowerCase())} className="text-light-foreground" />
156
+ <span className="line-clamp-1">{f.file.name}</span>
157
+ </span>
158
+ )
159
+ })}
160
+ </div>
161
+ )}
162
+
163
+ {leftIcon && (
164
+ <FontAwesomeIcon
165
+ className={cn(
166
+ "input-icon",
167
+ "input-icon-left",
168
+ props.disabled && "input-icon-disabled",
169
+ inputHandler.focus && "input-icon-focus",
170
+ pcn<CT>(className, "icon"),
171
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
172
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
173
+ )}
174
+ icon={leftIcon}
175
+ />
176
+ )}
177
+
178
+ {rightIcon && (
179
+ <FontAwesomeIcon
180
+ className={cn(
181
+ "input-icon",
182
+ "input-icon-right",
183
+ props.disabled && "input-icon-disabled",
184
+ inputHandler.focus && "input-icon-focus",
185
+ pcn<CT>(className, "icon"),
186
+ props.disabled && pcn<CT>(className, "icon", "disabled"),
187
+ inputHandler.focus && pcn<CT>(className, "icon", "focus"),
188
+ )}
189
+ icon={rightIcon}
190
+ />
191
+ )}
192
+ </div>
193
+
194
+ {invalidMessage && <small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>}
195
+ </div>
196
+
197
+ {!isSm ? (
198
+ <FloatingPageComponent
199
+ show={inputHandler.focus}
200
+ onClose={() => inputHandler.setFocus(false)}
201
+ title={label}
202
+ footer={
203
+ <ButtonComponent
204
+ label="Selesai"
205
+ variant="outline"
206
+ onClick={() => inputHandler.setFocus(false)}
207
+ block
208
+ />
209
+ }
210
+ >
211
+ <InputDocumentPicker
212
+ value={inputHandler.value}
213
+ onChange={(e) => {
214
+ inputHandler.setValue(e);
215
+ if (inputHandler.idle) inputHandler.setIdle(false);
216
+ onChange?.(e)
217
+ }}
218
+ />
219
+
220
+ </FloatingPageComponent>
221
+ ) : (
222
+ <BottomSheetComponent
223
+ show={inputHandler.focus}
224
+ onClose={() => inputHandler.setFocus(false)}
225
+ size={'98vh'}
226
+ footer={
227
+ <ButtonComponent
228
+ label="Selesai"
229
+ variant="outline"
230
+ onClick={() => inputHandler.setFocus(false)}
231
+ block
232
+ />
233
+ }
234
+ >
235
+ <InputDocumentPicker
236
+ value={inputHandler.value}
237
+ onChange={(e) => {
238
+ inputHandler.setValue(e);
239
+ if (inputHandler.idle) inputHandler.setIdle(false);
240
+ onChange?.(e)
241
+ }}
242
+ />
243
+
244
+ </BottomSheetComponent>
245
+ )}
246
+ </>
247
+ );
248
+ }
249
+
250
+
251
+ export interface InputDocumentPickerProps {
252
+ value ?: any[];
253
+ onChange ?: (value: any[]) => void;
254
+ }
255
+
256
+ export const InputDocumentPicker: React.FC<InputDocumentPickerProps> = ({ value, onChange }) => {
257
+ const { isSm } = useResponsive();
258
+ const fileInputRef = useRef<HTMLInputElement>(null);
259
+
260
+ const [previewActive, setPreviewActive] = useState<string | null>(null);
261
+
262
+ const files: DocFile[] = Array.isArray(value) ? value : [];
263
+
264
+ function updateFiles(next: DocFile[]) {
265
+ onChange?.(next);
266
+ }
267
+
268
+ function handleFilePick(e: React.ChangeEvent<HTMLInputElement>) {
269
+ const picked = e.target.files;
270
+ if (!picked) return;
271
+
272
+ const next = [...files];
273
+
274
+ Array.from(picked).forEach((f) => {
275
+ const id = Math.random().toString(36).substring(7);
276
+ next.push({
277
+ id,
278
+ file: f,
279
+ url: URL.createObjectURL(f),
280
+ type: f.type,
281
+ });
282
+
283
+ if (!previewActive) setPreviewActive(id);
284
+ });
285
+
286
+ updateFiles(next);
287
+ e.target.value = "";
288
+ }
289
+
290
+ function removeFile(id: string) {
291
+ const next = files.filter((x) => x.id !== id);
292
+
293
+ updateFiles(next);
294
+
295
+ if (previewActive === id) {
296
+ setPreviewActive(next[0]?.id || null);
297
+ }
298
+ }
299
+
300
+ return (<>
301
+ <div className="p-4 flex flex-col gap-4">
302
+ <div className="input-document-dropzone">
303
+ {files.find((x) => x.id === previewActive)?.file ? (
304
+ <DocumentViewerComponent
305
+ file={files.find((x) => x.id === previewActive)?.file}
306
+ />
307
+ ) : (
308
+ <div className="input-document-empty-box" onClick={() => fileInputRef.current?.click()}>
309
+ <FontAwesomeIcon icon={faPlus} className="text-3xl" />
310
+ <p className="text-lg">Tambah Dokumen</p>
311
+ </div>
312
+ )}
313
+ </div>
314
+
315
+ <input
316
+ ref={fileInputRef}
317
+ type="file"
318
+ multiple
319
+ className="hidden"
320
+ onChange={handleFilePick}
321
+ />
322
+
323
+ <div className="input-document-thumbs-container">
324
+ <div className="input-document-thumbs-grid">
325
+ {files.map((f) => {
326
+ return (
327
+ <div
328
+ key={f.id}
329
+ className={cn(
330
+ "input-document-thumb-wrapper",
331
+ previewActive === f.id && "input-document-thumb-wrapper-active"
332
+ )}
333
+ onClick={() => setPreviewActive(f.id)}
334
+ >
335
+ <DocumentViewerComponent
336
+ file={f.file}
337
+ mode="thumb"
338
+ />
339
+
340
+ <ButtonComponent
341
+ icon={faTimes}
342
+ onClick={() => removeFile(f.id)}
343
+ variant="light"
344
+ paint="danger"
345
+ className="absolute top-2 right-2"
346
+ size={isSm ? "xs" : "sm"}
347
+ />
348
+ </div>
349
+ );
350
+ })}
351
+
352
+ <div className="input-document-upload-box" onClick={() => fileInputRef.current?.click()}>
353
+ <FontAwesomeIcon icon={faPlus} />
354
+ <p className="text-[10px] text-center">Tambah Dokumen</p>
355
+ </div>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ </>)
360
+ }