@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,436 @@
1
+ "use client"
2
+
3
+ import { isValidElement, ReactNode, useEffect, useMemo, useRef, useState } from "react";
4
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5
+ import { faArrowDownZA, faArrowUpAZ, faChevronLeft, faChevronRight } from "@fortawesome/free-solid-svg-icons";
6
+ import { ApiFilterType, cn, pcn, useLazySearch, useResponsive, conversion } from "@utils";
7
+ import { ControlBarComponent, ControlBarOptionType } from "./ControlBar.component";
8
+ import { PaginationComponent, PaginationProps } from "./Pagination.component";
9
+ import { ScrollContainerComponent } from "../wrap/ScrollContainer.component";
10
+ import { FilterColumnOption } from "./FilterComponent";
11
+ import { CheckboxComponent } from "../input/Checkbox.component";
12
+ import { SwipeComponent, SwipeActionType } from "../wrap/Swipe.component";
13
+
14
+ type CT = "controller-bar" | "head-column" | "column" | "row" | "floating-action" | "base";
15
+
16
+ export interface TableColumnType {
17
+ selector : string;
18
+ label : string | ReactNode;
19
+ width ?: string;
20
+ sortable ?: boolean;
21
+ searchable ?: boolean;
22
+ filterable ?: boolean | {
23
+ type : "text" | "number" | "currency" | "date";
24
+ } | {
25
+ type : "select";
26
+ options : { label: string; value: any }[];
27
+ };
28
+ conversion ?: keyof typeof conversion;
29
+ className ?: string;
30
+ item ?: (data: any) => string | ReactNode;
31
+ tip ?: string | ((data: any) => string);
32
+ }
33
+
34
+ export interface TableProps {
35
+ id ?: string;
36
+
37
+ controlBar ?: false | ControlBarOptionType[];
38
+
39
+ columns : TableColumnType[];
40
+ data : Record<string, any>[];
41
+ pagination ?: PaginationProps | false;
42
+
43
+ loading ?: boolean;
44
+ sortBy ?: string[];
45
+ onChangeSortBy ?: (sort: string[]) => void;
46
+ search ?: string;
47
+ onChangeSearch ?: (search: string) => void;
48
+ searchableColumn ?: string[];
49
+ onChangeSearchableColumn ?: (column: string) => void;
50
+ filter ?: ApiFilterType[];
51
+ onChangeFilter ?: (filters: ApiFilterType[]) => void;
52
+ checks ?: (string | number)[];
53
+ onChangeChecks ?: (checks: (string | number)[]) => void;
54
+ actionBulking ?: ((checks: (string | number)[]) => ReactNode) | false;
55
+ focus ?: number | null;
56
+ setFocus ?: (focus: number | null) => void;
57
+
58
+ onRowClick ?: (data: Record<string, any>, key: number) => void;
59
+ onRefresh ?: () => void;
60
+
61
+ block ?: boolean;
62
+ noIndex ?: boolean;
63
+ responsiveControl ?: {
64
+ mobile ?: {
65
+ item ?: (item: Record<string, any>, key: number) => ReactNode,
66
+ leftActionControl ?: Omit<SwipeActionType, "onAction"> & { onAction?: (item: Record<string, any>, key?: number) => void },
67
+ rightActionControl ?: Omit<SwipeActionType, "onAction"> & { onAction?: (item: Record<string, any>, key?: number) => void },
68
+ }
69
+ };
70
+
71
+ /** Use custom class with: "controller-bar::", "head-column::", "column::", "floating-action::", "row::". */
72
+ className?: string;
73
+ }
74
+
75
+ export function TableComponent({
76
+ id,
77
+ controlBar,
78
+ columns,
79
+ data,
80
+ pagination,
81
+ loading,
82
+
83
+ sortBy,
84
+ onChangeSortBy,
85
+ search,
86
+ onChangeSearch,
87
+ searchableColumn,
88
+ onChangeSearchableColumn,
89
+ filter,
90
+ onChangeFilter,
91
+ checks,
92
+ onChangeChecks,
93
+ actionBulking,
94
+ focus,
95
+
96
+ onRowClick,
97
+ onRefresh,
98
+
99
+ block,
100
+ noIndex,
101
+ responsiveControl,
102
+
103
+ className = "",
104
+ }: TableProps) {
105
+ const [displayColumns, setDisplayColumns] = useState<string[]>([]);
106
+ const [showFloatingAction, setShowFloatingAction] = useState(false);
107
+ const [floatingActionActive, setFloatingActionActive] = useState<false | number>(false);
108
+ const [keyword, setKeyword] = useState<string>("");
109
+ const [keywordSearch] = useLazySearch(keyword);
110
+ const { isSm } = useResponsive();
111
+
112
+ const actionColumnRef = useRef<HTMLDivElement>(null);
113
+
114
+ useEffect(() => {
115
+ if (columns) setDisplayColumns([...columns.map((column) => column.selector)]);
116
+ }, [columns]);
117
+
118
+
119
+ useEffect(() => {
120
+ setKeyword(search || "");
121
+ }, [search]);
122
+
123
+
124
+ useEffect(() => {
125
+ keywordSearch ? onChangeSearch?.(keywordSearch) : onChangeSearch?.("");
126
+
127
+ if(pagination != false) {
128
+ pagination?.onChange?.(pagination.totalRow, pagination.paginate, 1);
129
+ }
130
+ }, [keywordSearch]);
131
+
132
+ const columnMapping = useMemo(() => {
133
+ return ( columns?.filter((column) => displayColumns.includes(column.selector)) || []);
134
+ }, [columns, displayColumns]);
135
+
136
+
137
+ const numberOfRow = (key: number) => pagination && (pagination?.page || 1) != 1 ? pagination?.paginate * ((pagination?.page || 1) - 1) + key + 1 : key + 1;
138
+
139
+
140
+ function renderHead() {
141
+ return (
142
+ <>
143
+ {columnMapping?.map((column, key) => {
144
+ const sortColumn = sortBy?.find((e) => e.split(" ")?.at(0) == column.selector)?.split(" ")?.at(0) || "";
145
+ const sortDirection = sortBy?.find((e) => e.split(" ")?.at(0) == column.selector)?.split(" ")?.at(1) || "";
146
+
147
+ return (
148
+ <div
149
+ key={key}
150
+ className={cn(
151
+ "table-head-column",
152
+ column.sortable && "cursor-pointer",
153
+ pcn<CT>(className, "head-column")
154
+ )}
155
+ style={{ width: column.width ? column.width : 200 }}
156
+ onClick={() => column.sortable && onChangeSortBy?.([`${column.selector} ${sortDirection == "desc" ? "asc" : "desc"}`])}
157
+ >
158
+ {column.label}
159
+
160
+ {!!sortColumn && (
161
+ <FontAwesomeIcon
162
+ icon={sortDirection == "desc" ? faArrowDownZA : faArrowUpAZ}
163
+ className="text-light-foreground/70"
164
+ />
165
+ )}
166
+ </div>
167
+ );
168
+ })}
169
+ </>
170
+ );
171
+ }
172
+
173
+
174
+ function renderItem(item: Record<string, any>, itemKey: number) {
175
+ const itemMapping = columnMapping.map((column) => {
176
+ if (column?.item) {
177
+ return column.item(item);
178
+ }
179
+
180
+ let value = item[column.selector];
181
+
182
+ if (column.conversion && conversion[column.conversion]) {
183
+ value = (conversion as any)[column.conversion](value as never);
184
+ }
185
+
186
+ if (isValidElement(value)) {
187
+ return value;
188
+ }
189
+
190
+ if (value === null || value === undefined) {
191
+ return "-";
192
+ }
193
+
194
+ if (typeof value === "object") {
195
+ return JSON.stringify(value);
196
+ }
197
+
198
+ return value;
199
+ });
200
+
201
+ if(!isSm || !responsiveControl?.mobile) {
202
+ return (
203
+ <>
204
+ {itemMapping?.map((one, key) => {
205
+ const column = columnMapping?.[key];
206
+
207
+ let title = one as string;
208
+ if (column?.tip) {
209
+ if (typeof column.tip === "string") {
210
+ title = (item[column.tip as keyof object] as any)?.toString() || "-";
211
+ } else if (typeof column.tip === "function") {
212
+ title = column.tip(item);
213
+ }
214
+ }
215
+ return (
216
+ <div
217
+ key={key}
218
+ className={cn("table-body-column", onRowClick && "cursor-pointer", pcn<CT>(className, "column"))}
219
+ style={{ width: columnMapping?.at(key)?.width || 200 }}
220
+ onClick={() => onRowClick?.(item, itemKey) }
221
+ title={title}
222
+ >
223
+ {one}
224
+ </div>
225
+ );
226
+ })}
227
+ </>
228
+ );
229
+ } else {
230
+
231
+ const { onAction: onLeftAction, ...restLeftAction } = responsiveControl?.mobile?.leftActionControl || {};
232
+ const { onAction: onRightAction, ...restRightAction } = responsiveControl?.mobile?.rightActionControl || {};
233
+
234
+ return (
235
+ <SwipeComponent
236
+ className="rounded-lg"
237
+ leftActionControl={!!responsiveControl?.mobile?.leftActionControl ? {
238
+ ...restLeftAction,
239
+ ...(onLeftAction ? { onAction: () => onLeftAction?.(item, itemKey)} : {})
240
+ } : undefined}
241
+ rightActionControl={!!responsiveControl?.mobile?.rightActionControl ? {
242
+ ...restRightAction,
243
+ ...(onRightAction ? { onAction: () => onRightAction?.(item, itemKey)} : {})
244
+ } : undefined}
245
+ >
246
+ <div onClick={() => onRowClick?.(item, itemKey)}>
247
+ {responsiveControl?.mobile?.item ? responsiveControl?.mobile?.item(item, itemKey) : (
248
+ <>
249
+ <p className="font-semibold">{Object.values(itemMapping)[0] as any}</p>
250
+ <p className="text-sm">{Object.values(itemMapping)[1] as any}</p>
251
+ </>
252
+ )}
253
+ </div>
254
+ </SwipeComponent>
255
+ );
256
+ }
257
+ }
258
+
259
+
260
+ return (
261
+ <div className={cn("relative", pcn<CT>(className, "base"))}>
262
+ {controlBar != false && (
263
+ <ControlBarComponent
264
+ id={id}
265
+ options={!controlBar ? ["SEARCH", "SELECTABLE", "REFRESH"] : controlBar}
266
+ searchableOptions={columns?.filter((c: TableColumnType) => c.searchable)}
267
+ onSearchable={(e) => onChangeSearchableColumn?.(String(e))}
268
+ searchable={searchableColumn || []}
269
+ onSearch={(e) => setKeyword(e)}
270
+ search={keyword}
271
+ selectableOptions={columns}
272
+ onSelectable={(e) => setDisplayColumns(e)}
273
+ selectable={displayColumns}
274
+ sortableOptions={columns?.filter((c: TableColumnType) => c.sortable)}
275
+ sort={sortBy}
276
+ onSort={(sort) => onChangeSortBy?.(sort)}
277
+ onRefresh={() => onRefresh?.()}
278
+ filterableColumns={columns?.filter((c) => !!c?.filterable)?.map((c) => ({
279
+ label: c.label,
280
+ selector: c.selector,
281
+ type: typeof c?.filterable == "object" ? c?.filterable?.type : "text",
282
+ options: typeof c?.filterable == "object" && c?.filterable?.type == "select" ? c?.filterable?.options : undefined
283
+ })) as FilterColumnOption[]}
284
+ onFilter={(filters) => onChangeFilter?.(filters)}
285
+ filter={filter}
286
+ className={pcn<CT>(className, "controller-bar") || ""}
287
+ />
288
+ )}
289
+
290
+ <div className="relative">
291
+ <ScrollContainerComponent
292
+ scrollFloating={!isSm && block}
293
+ className="w-full"
294
+ onScroll={(e) => {
295
+ actionColumnRef.current?.clientWidth && e.scrollLeft &&
296
+ setShowFloatingAction(e.scrollLeft + e.clientWidth <= e.scrollWidth - actionColumnRef.current?.clientWidth);
297
+ }}
298
+ footer={
299
+ <>
300
+ {block && pagination && (
301
+ <>
302
+ <div className="py-6"></div>
303
+ <div className="my-2 absolute bottom-0 w-full">
304
+ <PaginationComponent {...pagination} />
305
+ </div>
306
+ </>
307
+ )}
308
+ </>
309
+ }
310
+ >
311
+ {loading ? (
312
+ <div className="w-max min-w-full">
313
+ <div className="table-loading-container">
314
+ <h1 className="table-loading-text">
315
+ Memuat data...
316
+ </h1>
317
+ </div>
318
+ </div>
319
+ ) : !data || !data.length ? (
320
+ <div className="table-empty-container">
321
+ <h1 className="table-empty-text">
322
+ Belum Ada Data
323
+ </h1>
324
+ </div>
325
+ ) : (
326
+ <>
327
+ {!isSm || !responsiveControl?.mobile ? (
328
+ <div className="w-max min-w-full">
329
+ <div className={cn("table-head-row", pcn<CT>(className, "row"))}>
330
+ {!!actionBulking && (
331
+ <div className="table-head-column w-max">
332
+ <CheckboxComponent
333
+ name="selected_table"
334
+ className="w-5 h-5"
335
+ checked={data.length > 0 && checks?.length === data.length}
336
+ onChange={() => data.length > 0 && checks?.length === data.length ? onChangeChecks?.([]) : onChangeChecks?.(data.map((d) => d.id))}
337
+ />
338
+ </div>
339
+ )}
340
+ {!noIndex && <div className={cn("table-head-column w-8", pcn<CT>(className, "head-column"))}>#</div>}
341
+ {renderHead()}
342
+ </div>
343
+
344
+ <div className="table-body">
345
+ {data.map((item: Record<string, any>, key) => {
346
+ return (
347
+ <div
348
+ style={{ animationDelay: `${(key + 1) * 0.05}s` }}
349
+ className={cn(
350
+ "table-body-row",
351
+ key % 2 ? "bg-light-primary/10" : "bg-white",
352
+ focus == key && "bg-light-primary/30",
353
+ pcn<CT>(className, "row")
354
+ )}
355
+ key={key}
356
+ >
357
+ {!!actionBulking && (
358
+ <div className={cn("table-body-column w-max", pcn<CT>(className, "column"))}>
359
+ <CheckboxComponent
360
+ name="selected_table"
361
+ className="w-5 h-5"
362
+ checked={checks?.includes(item?.id)}
363
+ onChange={() => checks?.includes(item?.id) ? onChangeChecks?.(checks.filter((i) => i !== item?.id)) : onChangeChecks?.([...(checks || []), item?.id])}
364
+ />
365
+ </div>
366
+ )}
367
+ {!noIndex && <div className={cn("table-body-column w-8", pcn<CT>(className, "column"))}>{numberOfRow(key)}</div>}
368
+ {renderItem(item, key)}
369
+ <div ref={actionColumnRef} className="table-action-column">
370
+ {item["action" as keyof object]}
371
+ </div>
372
+
373
+ {item["action" as keyof object] && showFloatingAction && (
374
+ <div
375
+ className={cn("table-floating-action", pcn<CT>(className, "floating-action"))}
376
+ onClick={() =>
377
+ floatingActionActive !== false &&
378
+ floatingActionActive == key ? setFloatingActionActive(false) : setFloatingActionActive(key)
379
+ }
380
+ >
381
+ <div className="table-floating-action-icon-wrapper">
382
+ <FontAwesomeIcon icon={floatingActionActive === false || floatingActionActive != key ? faChevronLeft : faChevronRight}/>
383
+ </div>
384
+
385
+ <div className={cn("table-floating-action-content", floatingActionActive === key && "table-floating-action-content-active")}>
386
+ {item["action" as keyof object]}
387
+ </div>
388
+ </div>
389
+ )}
390
+ </div>
391
+ );
392
+ })}
393
+ </div>
394
+ </div>
395
+ ) : (
396
+ <div className="table-mobile-list">
397
+ {data.map((item: Record<string, any>, key) => {
398
+ return (
399
+ <div
400
+ style={{ animationDelay: `${(key + 1) * 0.05}s` }}
401
+ key={key}
402
+ >
403
+ {renderItem(item, key)}
404
+ </div>
405
+ );
406
+ })}
407
+ </div>
408
+ )}
409
+ </>
410
+ )}
411
+ </ScrollContainerComponent>
412
+ </div>
413
+
414
+ {!!actionBulking && !!checks?.length && (
415
+ <div className="table-bulk-actions-bar">
416
+ <div className="table-bulk-actions-title">{checks?.length} Data Terpilih</div>
417
+ <div className="table-bulk-actions-buttons">
418
+ {actionBulking?.(checks)}
419
+ </div>
420
+ </div>
421
+ )}
422
+
423
+ {!block && pagination && (
424
+ <div className="table-pagination-desktop">
425
+ <PaginationComponent {...pagination} />
426
+ </div>
427
+ )}
428
+
429
+ {pagination && (
430
+ <div className="table-pagination-mobile">
431
+ <PaginationComponent {...pagination} />
432
+ </div>
433
+ )}
434
+ </div>
435
+ );
436
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ declare module "@contexts" {
2
+ export function useToggleContext(): {
3
+ setToggle: (key: string, value?: any) => void;
4
+ toggle: Record<string, any>;
5
+ };
6
+ export function useAppContext(): any;
7
+ }
@@ -0,0 +1,26 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ export interface TypographyArticleProps {
4
+ title : string | ReactNode;
5
+ content : string | ReactNode;
6
+ header ?: string | ReactNode;
7
+ footer ?: string | ReactNode;
8
+ }
9
+
10
+ export function TypographyArticleComponent({
11
+ title,
12
+ content,
13
+ header,
14
+ footer,
15
+ } : TypographyArticleProps) {
16
+ return (
17
+ <>
18
+ <h4 className="typography-article-header">{header}</h4>
19
+ <h1 className="typography-article-title">{title}</h1>
20
+ <div className="typography-article-content">{content}</div>
21
+ <div className="typography-article-footer">
22
+ {footer}
23
+ </div>
24
+ </>
25
+ )
26
+ }
@@ -0,0 +1,20 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ export interface TypographyColumnProps {
4
+ title : string | ReactNode;
5
+ content : string | ReactNode;
6
+ }
7
+
8
+ export function TypographyColumnComponent({
9
+ title,
10
+ content,
11
+ } : TypographyColumnProps) {
12
+ return (
13
+ <>
14
+ <div>
15
+ <div className="typography-column-title">{title}</div>
16
+ <div>{content}</div>
17
+ </div>
18
+ </>
19
+ )
20
+ }
@@ -0,0 +1,20 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ export interface TypographyContentProps {
4
+ title : string | ReactNode;
5
+ content : string | ReactNode;
6
+ }
7
+
8
+ export function TypographyContentComponent({
9
+ title,
10
+ content,
11
+ } : TypographyContentProps) {
12
+ return (
13
+ <>
14
+ <div>
15
+ <p className="typography-content-title">{title}</p>
16
+ <p className="typography-content-body">{content}</p>
17
+ </div>
18
+ </>
19
+ )
20
+ }
@@ -0,0 +1,20 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ export interface TypographyTipsProps {
4
+ title : string | ReactNode;
5
+ content : string | ReactNode;
6
+ }
7
+
8
+ export function TypographyTipsComponent({
9
+ title,
10
+ content,
11
+ } : TypographyTipsProps) {
12
+ return (
13
+ <>
14
+ <div className="typography-tips">
15
+ <p className="typography-tips-title">{title}</p>
16
+ <p className="typography-tips-body">{content}</p>
17
+ </div>
18
+ </>
19
+ )
20
+ }