@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,246 @@
1
+ "use client"
2
+
3
+ import { MouseEvent, ReactNode, TouchEvent, useEffect, useRef, useState } from "react";
4
+ import dynamic from 'next/dynamic';
5
+ import { cn, pcn } from "@utils";
6
+
7
+ type CT = "base" | "backdrop" | "footer";
8
+
9
+ export type BottomSheetProps = {
10
+ show : boolean;
11
+ children : ReactNode;
12
+ onClose : () => void;
13
+ size ?: string | number;
14
+ maxSize ?: string | number;
15
+ footer ?: ReactNode;
16
+
17
+ /** Use custom class with: "backdrop::", "footer::". */
18
+ className ?: string;
19
+ };
20
+
21
+ function sizeToPx(value: string | number | undefined): number {
22
+ if (typeof window === "undefined") return 0;
23
+ if (value === undefined || value === null) return 0;
24
+
25
+ if (typeof value === "number") return value;
26
+
27
+ const v = value.trim();
28
+
29
+ if (v.endsWith("vh")) {
30
+ const n = parseFloat(v.replace("vh", ""));
31
+ return (n / 100) * window.innerHeight;
32
+ }
33
+ if (v.endsWith("px")) {
34
+ return parseFloat(v.replace("px", ""));
35
+ }
36
+ return parseFloat(v) || 0;
37
+ }
38
+
39
+ const BottomSheet = ({
40
+ show,
41
+ children,
42
+ onClose,
43
+ size = 500,
44
+ maxSize,
45
+ footer,
46
+ className = "",
47
+ }: BottomSheetProps) => {
48
+ const scrollRef = useRef<HTMLDivElement | null>(null);
49
+ const sheetRef = useRef<HTMLDivElement | null>(null);
50
+
51
+ const startY = useRef(0);
52
+ const lastY = useRef(0);
53
+ const dragging = useRef(false);
54
+
55
+ const [offset, setOffset] = useState(0);
56
+ const [isExpanded, setIsExpanded] = useState(false);
57
+ const [scrollLocked, setScrollLocked] = useState(false);
58
+ const [contentScrollable, setContentScrollable] = useState(false);
59
+
60
+ const realMaxSize = maxSize ?? size;
61
+
62
+ const clamp = (v: number) => {
63
+ const max = window.innerHeight;
64
+ return Math.max(-200, Math.min(v, max));
65
+ };
66
+
67
+ const animateTo = (target: number, onFinish?: () => void) => {
68
+ lastY.current = target;
69
+ setOffset(clamp(target));
70
+ onFinish?.();
71
+ };
72
+
73
+ useEffect(() => {
74
+ if (show) {
75
+ lastY.current = 0;
76
+ setOffset(0);
77
+ setIsExpanded(false);
78
+ } else {
79
+ const t = setTimeout(() => {
80
+ lastY.current = 0;
81
+ setOffset(0);
82
+ setIsExpanded(false);
83
+ }, 250);
84
+ return () => clearTimeout(t);
85
+ }
86
+ }, [show]);
87
+
88
+ const onStart = (clientY: number) => {
89
+ const sc = scrollRef.current;
90
+
91
+ if (sc && sc.scrollTop < 0) {
92
+ setScrollLocked(true);
93
+ return;
94
+ }
95
+
96
+ setScrollLocked(false);
97
+
98
+ dragging.current = true;
99
+ startY.current = clientY;
100
+ lastY.current = offset;
101
+ };
102
+
103
+ const onMove = (clientY: number) => {
104
+ if (scrollLocked) return;
105
+ if (!dragging.current) return;
106
+
107
+ const diff = clientY - startY.current;
108
+ setOffset(clamp(lastY.current + diff));
109
+ };
110
+
111
+ const onEnd = () => {
112
+ if (scrollLocked) {
113
+ setScrollLocked(false);
114
+ return;
115
+ }
116
+
117
+ if (!dragging.current) return;
118
+ dragging.current = false;
119
+
120
+ const current = offset;
121
+
122
+ const thresholdDown = 120;
123
+ const thresholdUp = -40;
124
+
125
+ if (!isExpanded && current < thresholdUp && maxSize !== undefined) {
126
+ setIsExpanded(true);
127
+ animateTo(0);
128
+ return;
129
+ }
130
+
131
+ if (isExpanded && current > thresholdDown) {
132
+ setIsExpanded(false);
133
+ animateTo(0);
134
+ return;
135
+ }
136
+
137
+ if (!isExpanded && current > thresholdDown) {
138
+ animateTo(window.innerHeight, () => {
139
+ onClose();
140
+ lastY.current = 0;
141
+ setOffset(0);
142
+ });
143
+ return;
144
+ }
145
+
146
+ animateTo(0);
147
+ };
148
+
149
+ const collapsedPx = sizeToPx(size);
150
+ const expandedPx = sizeToPx(realMaxSize);
151
+
152
+ const topPx = isExpanded ? window.innerHeight - expandedPx : window.innerHeight - collapsedPx;
153
+
154
+ const bindTouch = {
155
+ onTouchStart : (e: TouchEvent) => onStart(e.touches[0].clientY),
156
+ onTouchMove : (e: TouchEvent) => onMove(e.touches[0].clientY),
157
+ onTouchEnd : () => onEnd(),
158
+ };
159
+
160
+ const bindMouse = {
161
+ onMouseDown : (e: MouseEvent) => onStart(e.clientY),
162
+ onMouseMove : (e: MouseEvent) => dragging.current && onMove(e.clientY),
163
+ onMouseUp : () => onEnd(),
164
+ onMouseLeave : () => onEnd(),
165
+ };
166
+
167
+ useEffect(() => {
168
+ const sc = scrollRef.current;
169
+ if (!sc) return;
170
+
171
+ const canScroll = sc.scrollHeight > sc.clientHeight;
172
+ setContentScrollable(canScroll);
173
+ }, [show, size, maxSize]);
174
+
175
+ useEffect(() => {
176
+ if (show) {
177
+ history.pushState({ bottomsheet: true }, "");
178
+ }
179
+ }, [show]);
180
+
181
+ useEffect(() => {
182
+ const onPopState = (event: PopStateEvent) => {
183
+ if (show) {
184
+ event.preventDefault();
185
+
186
+ onClose();
187
+ history.pushState({}, "");
188
+ }
189
+ };
190
+
191
+ window.addEventListener("popstate", onPopState);
192
+ return () => window.removeEventListener("popstate", onPopState);
193
+ }, [show, onClose]);
194
+
195
+ return (
196
+ <>
197
+ <div
198
+ className={cn(
199
+ "modal-backdrop",
200
+ !show && "translate-y-full",
201
+ pcn<CT>(className, "backdrop"),
202
+ )}
203
+ onClick={onClose}
204
+ />
205
+
206
+ <div
207
+ ref={sheetRef}
208
+ className="bottom-sheet"
209
+ style={{
210
+ top: show ? `${topPx}px` : "150vh",
211
+ transform: `translateY(${offset}px)`,
212
+ touchAction: "none",
213
+ }}
214
+ {...bindTouch}
215
+ {...bindMouse}
216
+ >
217
+ <div className="bottom-sheet-container">
218
+ <div className="bottom-sheet-handle-wrapper">
219
+ <div className="bottom-sheet-handle" />
220
+ </div>
221
+
222
+ <div
223
+ ref={scrollRef}
224
+ className="overflow-y-auto"
225
+ style={{
226
+ height: isExpanded ? realMaxSize : size,
227
+ touchAction: contentScrollable ? "auto" : "none",
228
+ overscrollBehaviorY: "contain",
229
+ }}
230
+ >
231
+ {children}
232
+ </div>
233
+ </div>
234
+ </div>
235
+
236
+ {show && footer && (
237
+ <div className="bottom-sheet-footer">
238
+ {footer}
239
+ </div>
240
+ )}
241
+ </>
242
+ );
243
+ }
244
+
245
+
246
+ export const BottomSheetComponent = dynamic(() => Promise.resolve(BottomSheet), { ssr: false })
@@ -0,0 +1,103 @@
1
+ "use client"
2
+
3
+ import { ReactNode, useEffect } from "react";
4
+ import { faTimes } from "@fortawesome/free-solid-svg-icons";
5
+ import { ButtonComponent } from "../button/Button.component";
6
+ import { cn, pcn, shortcut } from "@utils";
7
+
8
+
9
+
10
+ type CT = "base" | "backdrop" | "header" | "footer";
11
+
12
+ export interface FloatingPageProps {
13
+ show : boolean;
14
+ onClose : () => void;
15
+ title ?: string | ReactNode;
16
+ children ?: any;
17
+ tip ?: string | ReactNode;
18
+ footer ?: string | ReactNode;
19
+
20
+ /** Use custom class with: "backdrop::", "header::", "footer::". */
21
+ className ?: string;
22
+ };
23
+
24
+
25
+
26
+ export function FloatingPageComponent({
27
+ show,
28
+ onClose,
29
+ title,
30
+ children,
31
+ tip,
32
+ footer,
33
+ className = "",
34
+ }: FloatingPageProps) {
35
+
36
+ useEffect(() => {
37
+ if (show) {
38
+ document.getElementsByTagName("body")[0].style.overflow = "hidden";
39
+
40
+ shortcut.register("escape", () => {
41
+ onClose?.()
42
+ }, "Kembali")
43
+
44
+ } else {
45
+ document.getElementsByTagName("body")[0].style.removeProperty("overflow");
46
+ }
47
+
48
+ return () => {
49
+ shortcut.unregister("escape")
50
+ }
51
+ }, [show]);
52
+
53
+ return (
54
+ <>
55
+ <div
56
+ className={cn(
57
+ "modal-backdrop",
58
+ !show && "opacity-0 scale-0 -translate-y-full",
59
+ pcn<CT>(className, "backdrop"),
60
+ )}
61
+ onClick={() => onClose()}
62
+ ></div>
63
+
64
+ <div
65
+ className={cn(
66
+ "floating-page",
67
+ !show && "top-[200vh] md:top-0 md:-right-[200vw]",
68
+ pcn<CT>(className, "base"),
69
+ )}
70
+ >
71
+ <div className={cn("modal-header", pcn<CT>(className, "header"))}>
72
+ {title && (
73
+ <div>
74
+ <h6 className="modal-title">{title}</h6>
75
+ {tip && <p className="modal-tip">{tip}</p>}
76
+ </div>
77
+ )}
78
+
79
+ <ButtonComponent
80
+ icon={faTimes}
81
+ variant="simple"
82
+ paint="danger"
83
+ onClick={() => onClose()}
84
+ />
85
+ </div>
86
+
87
+
88
+ {show && children}
89
+
90
+ {footer && (
91
+ <div
92
+ className={cn(
93
+ "modal-footer absolute bottom-0 w-full",
94
+ pcn<CT>(className, "footer"),
95
+ )}
96
+ >
97
+ {show && footer}
98
+ </div>
99
+ )}
100
+ </div>
101
+ </>
102
+ );
103
+ }
@@ -0,0 +1,95 @@
1
+ "use client"
2
+
3
+ import { ReactNode, useEffect } from "react";
4
+ import { faTimes } from "@fortawesome/free-solid-svg-icons";
5
+ import { cn, pcn, shortcut } from "@utils";
6
+ import { ButtonComponent } from "../button/Button.component";
7
+
8
+
9
+
10
+ type CT = "base" | "backdrop" | "header" | "footer";
11
+
12
+ export interface ModalProps {
13
+ show : boolean;
14
+ onClose : () => void;
15
+ title ?: string | ReactNode;
16
+ children ?: any;
17
+ tip ?: string | ReactNode;
18
+ footer ?: string | ReactNode;
19
+
20
+ /** Use custom class with: "backdrop::", "header::", "footer::". */
21
+ className ?: string;
22
+ };
23
+
24
+
25
+
26
+ export function ModalComponent({
27
+ show,
28
+ onClose,
29
+ title,
30
+ children,
31
+ tip,
32
+ footer,
33
+ className = "",
34
+ }: ModalProps) {
35
+ useEffect(() => {
36
+ if (show) {
37
+ document.getElementsByTagName("body")[0].style.overflow = "hidden";
38
+
39
+ shortcut.register("escape", () => {
40
+ onClose?.()
41
+ }, "Kembali")
42
+ } else {
43
+ document.getElementsByTagName("body")[0].style.removeProperty("overflow");
44
+ }
45
+
46
+ return () => {
47
+ shortcut.unregister("escape")
48
+ }
49
+ }, [show]);
50
+
51
+ return (
52
+ <>
53
+ <div
54
+ className={cn(
55
+ "modal-backdrop",
56
+ !show && "opacity-0 scale-0 -translate-y-full",
57
+ pcn<CT>(className, "backdrop"),
58
+ )}
59
+ onClick={() => onClose()}
60
+ ></div>
61
+
62
+ <div
63
+ className={cn(
64
+ "modal",
65
+ !show && "-translate-y-full opacity-0 scale-y-0",
66
+ pcn<CT>(className, "base"),
67
+ )}
68
+ >
69
+ {title && (
70
+ <div className={cn("modal-header", pcn<CT>(className, "header"))}>
71
+ <div>
72
+ <h6 className="modal-title">{title}</h6>
73
+ {tip && <p className="modal-tip">{tip}</p>}
74
+ </div>
75
+
76
+ <ButtonComponent
77
+ icon={faTimes}
78
+ variant="simple"
79
+ paint="danger"
80
+ onClick={() => onClose()}
81
+ />
82
+ </div>
83
+ )}
84
+
85
+ {show && children}
86
+
87
+ {footer && (
88
+ <div className={cn("modal-footer", pcn<CT>(className, "footer"))}>
89
+ {show && footer}
90
+ </div>
91
+ )}
92
+ </div>
93
+ </>
94
+ );
95
+ }
@@ -0,0 +1,219 @@
1
+ "use client"
2
+
3
+ import { ReactNode, useEffect, useMemo, useState } from "react";
4
+ import { faQuestion } from "@fortawesome/free-solid-svg-icons";
5
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6
+ import { api, ApiType, cn, pcn, registry, shortcut, useResponsive } from "@utils";
7
+ import { ToastComponent } from "./Toast.component";
8
+ import { ButtonComponent, ButtonProps } from "../button/Button.component";
9
+ import { BottomSheetComponent } from "./BottomSheet.component";
10
+
11
+
12
+
13
+ type CT = "base" | "backdrop" | "header" | "footer";
14
+
15
+ type SubmitIDB = { idb: { store: string , id: string | number}}
16
+
17
+ export interface ModalConfirmProps {
18
+ show : boolean;
19
+ onClose : () => void;
20
+ title ?: string | ReactNode;
21
+ children ?: any;
22
+ icon ?: any;
23
+ footer ?: string | ReactNode;
24
+ submitControl ?: ButtonProps & {
25
+ onSubmit ?: ApiType | SubmitIDB | (() => void);
26
+ onSuccess ?: () => void;
27
+ onError ?: () => void;
28
+ };
29
+
30
+ /** Use custom class with: "backdrop::", "header::", "footer::". */
31
+ className ?: string;
32
+ };
33
+
34
+
35
+
36
+ export function ModalConfirmComponent({
37
+ show,
38
+ title,
39
+ children,
40
+ icon,
41
+ footer,
42
+
43
+ submitControl,
44
+ onClose,
45
+
46
+ className = "",
47
+ }: ModalConfirmProps) {
48
+ const { isSm } = useResponsive();
49
+ const [toast, setToast] = useState<boolean | "success" | "failed">(false);
50
+ const [loading, setLoading] = useState(false);
51
+
52
+ useEffect(() => {
53
+ if (show) {
54
+ document.getElementsByTagName("body")[0].style.overflow = "hidden";
55
+
56
+ shortcut.register("escape", () => {
57
+ onClose?.()
58
+ }, "Kembali")
59
+ } else {
60
+ document.getElementsByTagName("body")[0].style.removeProperty("overflow");
61
+ }
62
+
63
+ return () => {
64
+ shortcut.unregister("escape")
65
+ }
66
+ }, [show]);
67
+
68
+
69
+ const renderChildren = useMemo(() => {
70
+ return (
71
+ <>
72
+ {title && (
73
+ <div
74
+ className={cn(
75
+ "flex flex-col gap-2 items-center text-primary",
76
+ pcn<CT>(className, "header")
77
+ )}
78
+ >
79
+ <div className="mt-6">
80
+ <FontAwesomeIcon
81
+ icon={icon || faQuestion}
82
+ className={`text-xl`}
83
+ />
84
+ </div>
85
+
86
+ <h6 className="font-semibold text-lg">{title}</h6>
87
+ </div>
88
+ )}
89
+
90
+ {children}
91
+
92
+ {footer && (
93
+ <div className={cn("modal-footer", pcn<CT>(className, "footer"))}>
94
+ {footer}
95
+ </div>
96
+ )}
97
+ </>
98
+ )
99
+ }, [title, footer, children])
100
+
101
+
102
+ const renderAction = (size: ButtonProps["size"] = 'md') => {
103
+ return (
104
+ <div className="flex justify-center pt-6">
105
+ <ButtonComponent
106
+ label="Batal"
107
+ variant="simple"
108
+ onClick={() => onClose()}
109
+ className="text-foreground bg-background rounded-none"
110
+ block
111
+ size={size}
112
+ />
113
+ <ButtonComponent
114
+ label={"Konfirmasi"}
115
+ loading={loading}
116
+ onClick={async () => {
117
+ if(!submitControl?.onSubmit) return;
118
+
119
+ setLoading(true);
120
+ if (typeof submitControl?.onSubmit == "function") {
121
+ submitControl?.onSubmit?.();
122
+ } else {
123
+ let response: any = null;
124
+
125
+ if ("path" in submitControl?.onSubmit || "url" in submitControl?.onSubmit) {
126
+ response = await api(submitControl?.onSubmit as ApiType)
127
+ }
128
+
129
+ if ("idb" in submitControl?.onSubmit) {
130
+ const idb = registry.get("idb");
131
+ if (!idb) {
132
+ throw new Error("IndexedDB (IDB) extension is not installed.");
133
+ }
134
+ await idb.delete((submitControl?.onSubmit as SubmitIDB).idb.store, (submitControl?.onSubmit as SubmitIDB).idb.id)
135
+
136
+ response = { status: 200 }
137
+ }
138
+
139
+ if (response?.status == 200 || response?.status == 201) {
140
+ setToast("success");
141
+ submitControl?.onSuccess?.();
142
+ setLoading(false);
143
+ } else {
144
+ setToast("failed");
145
+ submitControl?.onError?.();
146
+ setLoading(false);
147
+ }
148
+ }
149
+ }}
150
+ className="rounded-none"
151
+ block
152
+ size={size}
153
+ {...submitControl}
154
+ />
155
+ </div>
156
+ )
157
+ }
158
+
159
+ return (
160
+ <>
161
+ {!isSm ? (
162
+ <>
163
+ <div
164
+ className={cn(
165
+ "modal-backdrop",
166
+ !show && "opacity-0 scale-0 -translate-y-full",
167
+ pcn<CT>(className, "backdrop")
168
+ )}
169
+ onClick={() => onClose()}
170
+ ></div>
171
+
172
+ <div
173
+ className={cn(
174
+ "modal modal-confirm",
175
+ !show && "-translate-y-full opacity-0 scale-y-0",
176
+ pcn<CT>(className, "base")
177
+ )}
178
+ >
179
+ {renderChildren}
180
+
181
+ {renderAction()}
182
+ </div>
183
+ </>
184
+ ) : (
185
+ <>
186
+ <BottomSheetComponent
187
+ show={show}
188
+ onClose={onClose}
189
+ size={220}
190
+ footer={renderAction('lg')}
191
+ >
192
+ {renderChildren}
193
+ </BottomSheetComponent>
194
+ </>
195
+ )}
196
+
197
+
198
+ <ToastComponent
199
+ show={toast == "failed"}
200
+ onClose={() => setToast(false)}
201
+ title="Gagal"
202
+ className="!border-danger header::text-danger"
203
+ >
204
+ <p className="px-3 pb-2 text-sm">
205
+ Gagal {title || ""}! cek data dan koneksi internet dan coba kembali!
206
+ </p>
207
+ </ToastComponent>
208
+
209
+ <ToastComponent
210
+ show={toast == "success"}
211
+ onClose={() => setToast(false)}
212
+ title="Berhasil"
213
+ className="!border-success header::text-success"
214
+ >
215
+ <p className="px-3 pb-2 text-sm">Berhasil {title || ""}!</p>
216
+ </ToastComponent>
217
+ </>
218
+ );
219
+ }