@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,425 @@
1
+ "use client"
2
+
3
+ import React, { ReactNode, useEffect, useRef, useState } from "react";
4
+ import { faSave, faQuestionCircle, faPlus, faTimes } from "@fortawesome/free-solid-svg-icons";
5
+ import { ApiType, cn, pcn, FormErrorType, FormRegisterType, FormValueType, useForm, ValidationRules, DBSchema } from "@utils";
6
+ import { InputCheckboxComponent, InputCheckboxProps } from "../input/InputCheckbox.component";
7
+ import { InputComponent, InputProps } from "../input/Input.component";
8
+ import { InputCurrencyComponent, InputCurrencyProps } from "../input/InputCurrency.component";
9
+ import { InputDateComponent, InputDateProps } from "../input/InputDate.component";
10
+ import { InputNumberComponent, InputNumberProps } from "../input/InputNumber.component";
11
+ import { InputOtpComponent, InputOtpProps } from "../input/InputOtp.component";
12
+ import { InputPasswordComponent, InputPasswordProps } from "../input/InputPassword.component";
13
+ import { InputRadioComponent, InputRadioProps } from "../input/InputRadio.component";
14
+ import { SelectComponent, SelectProps } from "../input/Select.component";
15
+ import { ButtonComponent } from "../button/Button.component";
16
+ import { ModalConfirmComponent } from "../modal/ModalConfirm.component";
17
+ import { ToastComponent } from "../modal/Toast.component";
18
+ import { InputTimeProps, InputTimeComponent } from "../input/InputTime.component";
19
+ import { InputImageProps, InputImageComponent } from "../input/InputImage.component";
20
+ import { InputDateTimeProps, InputDatetimeComponent } from "../input/InputDatetime.component";
21
+
22
+
23
+
24
+ type CT = "base" | "title" | "submit";
25
+
26
+ type formCustomConstructionProps = ({
27
+ formControl,
28
+ values,
29
+ setValues,
30
+ setRegister,
31
+ errors,
32
+ setErrors,
33
+ }: {
34
+ formControl : (name: string) => {
35
+ register: (regName: string, regValidations?: ValidationRules | undefined) => void;
36
+ unregister: (regName: string) => void;
37
+ onChange: (e: any) => void;
38
+ value: any;
39
+ invalid: any;
40
+ };
41
+ values : { name: string; value?: any }[];
42
+ setValues : (values: FormValueType[]) => void;
43
+ errors : FormErrorType[];
44
+ setErrors : (errors: FormErrorType[]) => void;
45
+ setRegister : (registers: FormRegisterType) => void;
46
+ prefixName ?: string;
47
+ }) => ReactNode;
48
+
49
+ type ClusterConstruction = {
50
+ name : string;
51
+ label : string;
52
+ tip : string;
53
+ fields : FormType[];
54
+ wrap : boolean;
55
+ min ?: number;
56
+
57
+ /** Use custom class with: "label::", "tip::", "error::", "icon::", "suggest::", "suggest-item::". */
58
+ className : string;
59
+ };
60
+
61
+ type ConstructionMap = {
62
+ default : InputProps;
63
+ check : InputCheckboxProps;
64
+ currency : InputCurrencyProps;
65
+ date : InputDateProps;
66
+ datetime : InputDateTimeProps;
67
+ time : InputTimeProps;
68
+ image : InputImageProps;
69
+ cluster : ClusterConstruction;
70
+ number : InputNumberProps;
71
+ radio : InputRadioProps;
72
+ select : SelectProps;
73
+ "enter-password" : InputPasswordProps;
74
+ otp : InputOtpProps;
75
+ custom : formCustomConstructionProps;
76
+ };
77
+
78
+ type TypeKeys = keyof ConstructionMap;
79
+
80
+ export type WatchContext = {
81
+ values : Record<string, any>
82
+ self : string
83
+ prev : WatchAction
84
+ }
85
+
86
+ export type WatchAction = {
87
+ disabled ?: boolean
88
+ hidden ?: boolean
89
+ value ?: any
90
+ required ?: boolean
91
+ readonly ?: boolean
92
+ reset ?: boolean
93
+ }
94
+
95
+ export interface FormType<T extends TypeKeys = keyof ConstructionMap> {
96
+ col ?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | string;
97
+ className ?: string;
98
+ construction ?: ConstructionMap[T];
99
+ type ?: T;
100
+ onHide ?: (values: any) => boolean;
101
+ watch ?: (ctx: WatchContext) => WatchAction | undefined;
102
+ }
103
+
104
+ export interface formSupervisionProps {
105
+ title ?: string;
106
+ fields : FormType[];
107
+ confirmation ?: boolean;
108
+ defaultValue ?: object | null;
109
+ payload ?: (values: any) => Promise<object> | object;
110
+ submitControl : (ApiType & { idb?: never }) | { idb: { store: string, schema?: DBSchema }};
111
+ footerControl ?: ({ loading }: { loading: boolean }) => ReactNode;
112
+ onSuccess ?: (data: any) => void;
113
+ onError ?: (code: number) => void;
114
+ className ?: string;
115
+ }
116
+
117
+
118
+
119
+ export function FormSupervisionComponent({
120
+ title,
121
+ fields,
122
+ submitControl,
123
+ confirmation,
124
+ defaultValue,
125
+ onSuccess,
126
+ onError,
127
+ footerControl,
128
+ payload,
129
+ className = "",
130
+ }: formSupervisionProps) {
131
+ const [modal, setModal] = useState<boolean | "success" | "failed">(false);
132
+ const [fresh, setFresh] = useState<boolean>(true);
133
+ const [mapGroups, setMapGroups] = useState<Record<string, number[]>>({});
134
+ const [watchState, setWatchState] = useState<Record<string, WatchAction>>({});
135
+ const watchRef = useRef<Record<string, WatchAction>>({});
136
+
137
+ const { formControl, setRegister, unregister, unregisterPrefix, values, setValues, errors, setErrors, setDefaultValues, submit, loading, confirm } = useForm({
138
+ ...submitControl,
139
+ payload,
140
+ confirmation,
141
+ onSuccess: (data: any) => {
142
+ onSuccess?.(data);
143
+ setModal("success");
144
+ resetFresh();
145
+ },
146
+ onFailed: (code: number) => {
147
+ onError?.(code);
148
+ if (code == 422) confirm.onClose();
149
+ else setModal("failed");
150
+ },
151
+ });
152
+
153
+ const resetFresh = () => {
154
+ setFresh(false);
155
+ setTimeout(() => setFresh(true), 300);
156
+ };
157
+
158
+ useEffect(() => {
159
+ resetFresh();
160
+ }, [fields]);
161
+
162
+ useEffect(() => {
163
+ if (defaultValue) setDefaultValues(defaultValue);
164
+ else {
165
+ setDefaultValues(null);
166
+ resetFresh();
167
+ }
168
+ }, [defaultValue]);
169
+
170
+ // ==============================>
171
+ // ## Watch: collect watchers from fields
172
+ // ==============================>
173
+ const collectWatchers = (fieldList: FormType[], prefix?: string): { name: string, watch: NonNullable<FormType['watch']>, construction: any }[] => {
174
+ const result: { name: string, watch: NonNullable<FormType['watch']>, construction: any }[] = [];
175
+
176
+ for (const f of fieldList) {
177
+ const inputType = f.type || "default";
178
+ const name = prefix ? `${prefix}.${f.construction?.name}` : f.construction?.name || "";
179
+
180
+ if (inputType === "cluster") {
181
+ const cluster = f.construction as ClusterConstruction;
182
+ const groupKey = prefix ? `${prefix}.${cluster.name}` : cluster.name;
183
+ const group = mapGroups[groupKey] || [0];
184
+
185
+ for (const gIndex of group) {
186
+ result.push(...collectWatchers(cluster.fields, `${cluster.name}[${gIndex}]`));
187
+ }
188
+ } else if (f.watch) {
189
+ result.push({ name, watch: f.watch, construction: f.construction });
190
+ }
191
+ }
192
+
193
+ return result;
194
+ };
195
+
196
+
197
+ // ==============================>
198
+ // ## Watch: execute watchers on value change
199
+ // ==============================>
200
+ useEffect(() => {
201
+ const watchers = collectWatchers(fields);
202
+ if (watchers.length === 0) {
203
+ if (Object.keys(watchRef.current).length > 0) {
204
+ watchRef.current = {};
205
+ setWatchState({});
206
+ }
207
+ return;
208
+ }
209
+
210
+ const valMap = (values as any[]).reduce((acc, v) => { acc[v.name] = v.value; return acc; }, {} as any);
211
+
212
+ const nextState : Record<string, WatchAction> = {};
213
+ const valueUpdates : FormValueType[] = [];
214
+
215
+ for (const { name, watch, construction } of watchers) {
216
+ const prev = watchRef.current[name] || {};
217
+ const action = watch({ values: valMap, self: name, prev });
218
+
219
+ if (!action) continue;
220
+
221
+ nextState[name] = action;
222
+
223
+ if (action.hidden && !prev.hidden) unregister(name);
224
+
225
+ if (action.required !== prev.required) {
226
+ const baseValidations = Array.isArray(construction?.validations) ? [...construction.validations] : [];
227
+ const newValidations = action.required ? (baseValidations.includes("required") ? baseValidations : [...baseValidations, "required"]) : baseValidations.filter((v: string) => v !== "required");
228
+
229
+ setRegister({ name, validations: newValidations });
230
+ }
231
+
232
+ if (action.reset) {
233
+ const cur = valMap[name];
234
+
235
+ if (cur != null && cur !== "") valueUpdates.push({ name, value: "" });
236
+ } else if (action.value !== undefined && action.value !== valMap[name]) {
237
+ valueUpdates.push({ name, value: action.value });
238
+ }
239
+ }
240
+
241
+ if (JSON.stringify(watchRef.current) !== JSON.stringify(nextState)) {
242
+ watchRef.current = nextState;
243
+ setWatchState(nextState);
244
+ }
245
+
246
+ if (valueUpdates.length > 0) {
247
+ const merged = [...values];
248
+
249
+ for (const upd of valueUpdates) {
250
+ const idx = merged.findIndex(v => v.name === upd.name);
251
+ if (idx >= 0) merged[idx] = upd;
252
+ else merged.push(upd);
253
+ }
254
+
255
+ setValues(merged);
256
+ }
257
+ }, [values, fields, mapGroups]);
258
+
259
+
260
+ const generateColClass = (col: string | number) => String(col).split(" ").map((c) => (c.includes(":") ? `${c.replace(":", ":col-span-")}` : `col-span-${c}`)).join(" ");
261
+
262
+ const inputMap: Record<TypeKeys, React.FC<any>> = {
263
+ default : InputComponent,
264
+ check : InputCheckboxComponent,
265
+ currency : InputCurrencyComponent,
266
+ date : InputDateComponent,
267
+ datetime : InputDatetimeComponent,
268
+ time : InputTimeComponent,
269
+ number : InputNumberComponent,
270
+ radio : InputRadioComponent,
271
+ select : SelectComponent,
272
+ "enter-password" : InputPasswordComponent,
273
+ otp : InputOtpComponent,
274
+ image : InputImageComponent,
275
+ cluster : () => null,
276
+ custom : () => null,
277
+ };
278
+
279
+ const renderInput = (form: FormType, key: number, prefix?: string) => {
280
+ const inputType = form.type || "default";
281
+ const name = prefix ? `${prefix}.${form.construction?.name}` : form.construction?.name || "input_name";
282
+
283
+ if (form?.onHide?.(values)) return null;
284
+
285
+ const ws = watchState[name];
286
+ if (ws?.hidden) return null;
287
+
288
+ if (inputType === "cluster") {
289
+ const { name: mapName, fields: innerForms, label, tip, wrap, className, min = 0 } = form.construction as ClusterConstruction;
290
+
291
+ const groupKey = prefix ? `${prefix}.${mapName}` : mapName;
292
+ const group = mapGroups[groupKey] || Array.from({ length: Math.max(min, 1) }, (_, i) => i);
293
+
294
+ const addGroup = () => setMapGroups((prev) => ({ ...prev, [groupKey]: [...group, group.length > 0 ? Math.max(...group) + 1 : 0] }));
295
+
296
+ const removeGroup = (gIndex: number) => {
297
+ setMapGroups((prev) => ({ ...prev, [groupKey]: group.filter((g) => g !== gIndex) }));
298
+
299
+ unregisterPrefix(`${groupKey}[${gIndex}]`);
300
+ };
301
+
302
+ return (
303
+ <div key={key} className={cn("flex flex-col gap-4", generateColClass(form.col || "12"))}>
304
+ {group.map((gIndex) => (
305
+ <div
306
+ key={gIndex}
307
+ className={cn(
308
+ "form-supervision-cluster-item",
309
+ wrap && "form-supervision-cluster-item-wrapped",
310
+ className
311
+ )}
312
+ >
313
+ {label && <p className="input-label">{label} {gIndex + 1}</p>}
314
+ {tip && <small className="input-tip">{tip}</small>}
315
+ {(label || tip) && <div className="mb-2"></div>}
316
+
317
+ <div className="form-supervision-cluster-grid">
318
+ {innerForms.map((inner, i) => renderInput(inner, i, `${mapName}[${gIndex}]`))}
319
+ </div>
320
+
321
+ {group.length > min && (
322
+ <ButtonComponent
323
+ icon={faTimes}
324
+ paint="danger"
325
+ variant="outline"
326
+ size="xs"
327
+ className={cn(
328
+ "form-supervision-cluster-remove-btn",
329
+ wrap && "form-supervision-cluster-remove-btn-wrapped"
330
+ )}
331
+ onClick={() => removeGroup(gIndex)}
332
+ />
333
+ )}
334
+ </div>
335
+ ))}
336
+
337
+ <div>
338
+ <ButtonComponent
339
+ icon={faPlus}
340
+ label={`Tambah ${label || mapName}`}
341
+ variant="outline"
342
+ size="sm"
343
+ onClick={addGroup}
344
+ />
345
+ </div>
346
+ </div>
347
+ );
348
+ }
349
+
350
+ if (inputType === "custom") {
351
+ const customRender = form.construction as formCustomConstructionProps;
352
+ return (
353
+ <div key={key} className={cn(form.className, generateColClass(form.col || "12"))}>
354
+ {customRender?.({ formControl, values, setValues, errors, setErrors, setRegister, prefixName: prefix })}
355
+ </div>
356
+ );
357
+ }
358
+
359
+ const Component = inputMap[inputType] || InputComponent;
360
+ return (
361
+ <div key={key} className={cn(form.className, generateColClass(form.col || "12"))}>
362
+ <Component
363
+ {...(form.construction as any)}
364
+ {...formControl(name)}
365
+ disabled={ws?.disabled}
366
+ readOnly={ws?.readonly}
367
+ />
368
+ </div>
369
+ );
370
+ };
371
+
372
+ return (
373
+ <>
374
+ {title && <h4 className={cn("form-supervision-title", pcn<CT>(className, "title"))}>{title}</h4>}
375
+
376
+ <form className={cn("form-supervision-base", pcn<CT>(className, "base"))} onSubmit={submit}>
377
+ {fresh && fields.map((f, i) => renderInput(f, i))}
378
+
379
+ <div className="col-span-12">
380
+ {footerControl?.({ loading }) || (
381
+ <div className="form-supervision-footer">
382
+ <ButtonComponent
383
+ type="submit"
384
+ label="Simpan"
385
+ icon={faSave}
386
+ loading={loading}
387
+ className={pcn<CT>(className, "submit")}
388
+ />
389
+ </div>
390
+ )}
391
+ </div>
392
+ </form>
393
+
394
+ <ModalConfirmComponent
395
+ show={confirm.show}
396
+ onClose={() => confirm.onClose()}
397
+ icon={faQuestionCircle}
398
+ title="Yakin"
399
+ submitControl={{ onSubmit: () => confirm?.onConfirm(), paint: "primary" }}
400
+ >
401
+ <p className="form-supervision-confirm-text">Yakin semua data sudah benar?</p>
402
+ </ModalConfirmComponent>
403
+
404
+ <ToastComponent
405
+ show={modal == "failed"}
406
+ onClose={() => setModal(false)}
407
+ title="Gagal"
408
+ className="form-supervision-toast-error header::text-danger"
409
+ >
410
+ <p className="form-supervision-toast-text">
411
+ Data gagal disimpan, cek data dan koneksi internet lalu coba kembali!
412
+ </p>
413
+ </ToastComponent>
414
+
415
+ <ToastComponent
416
+ show={modal == "success"}
417
+ onClose={() => setModal(false)}
418
+ title="Berhasil"
419
+ className="form-supervision-toast-success header::text-success"
420
+ >
421
+ <p className="form-supervision-toast-text">Data berhasil disimpan!</p>
422
+ </ToastComponent>
423
+ </>
424
+ );
425
+ }