gbs-add-block 1.2.4 → 1.2.6
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.
- package/README.md +3 -3
- package/package.json +36 -30
- package/source/components/bargraph/index.tsx +4 -3
- package/source/components/bargraph/type.ts +1 -0
- package/source/components/button/index.tsx +41 -24
- package/source/components/card/index.tsx +30 -9
- package/source/components/checkbox/index.tsx +24 -15
- package/source/components/datagrid/context/GridContext.tsx +3 -3
- package/source/components/datepicker/index.tsx +2 -1
- package/source/components/input/index.tsx +141 -143
- package/source/components/modal/index.tsx +151 -139
- package/source/components/modal/types.ts +18 -16
- package/source/components/multiselect/PortalDropDown.tsx +2 -1
- package/source/components/multiselect/index.tsx +3 -2
- package/source/components/select/PortalDropDown.tsx +21 -5
- package/source/components/select/index.tsx +8 -6
- package/source/components/select/types.ts +2 -0
- package/source/components/tabs/index.tsx +70 -51
- package/source/components/tabs/types.ts +15 -14
- package/source/components/textarea/index.tsx +122 -149
- package/source/globalStyle.ts +60 -34
- package/source/icon/iconPaths.ts +6 -0
- package/source/theme.ts +40 -0
|
@@ -1,140 +1,152 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Grampro Business Services and affiliates.
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the MIT license found in the
|
|
5
|
-
* LICENSE file in the root directory of this source tree.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import React, { useEffect, useCallback, memo, useRef } from "react";
|
|
9
|
-
import { twMerge } from "tailwind-merge";
|
|
10
|
-
import Icon from "../icon/Icon";
|
|
11
|
-
import { x } from "../icon/iconPaths";
|
|
12
|
-
import type { ModalProps } from "./types";
|
|
13
|
-
|
|
14
|
-
const defaultClasses = {
|
|
15
|
-
modal:
|
|
16
|
-
"fixed z-50
|
|
17
|
-
modalContent:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
document.removeEventListener("keydown", handleEscKey);
|
|
86
|
-
document.body.style.overflow = "";
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Grampro Business Services and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useEffect, useCallback, memo, useRef } from "react";
|
|
9
|
+
import { twMerge } from "tailwind-merge";
|
|
10
|
+
import Icon from "../icon/Icon";
|
|
11
|
+
import { x } from "../icon/iconPaths";
|
|
12
|
+
import type { ModalProps } from "./types";
|
|
13
|
+
|
|
14
|
+
const defaultClasses = {
|
|
15
|
+
modal:
|
|
16
|
+
"fixed inset-0 z-50 flex items-center justify-center overflow-y-auto bg-slate-950/55 p-4 backdrop-blur-sm",
|
|
17
|
+
modalContent:
|
|
18
|
+
"relative max-h-[90vh] overflow-hidden rounded-lg border border-slate-200 bg-white shadow-xl shadow-slate-950/20 dark:border-slate-700 dark:bg-slate-950",
|
|
19
|
+
modalTitle:
|
|
20
|
+
"flex items-center justify-between gap-4 px-5 py-4 text-base font-semibold text-slate-950 dark:text-slate-50",
|
|
21
|
+
closeButton:
|
|
22
|
+
"absolute right-3 top-3 rounded-md p-2 text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 dark:hover:bg-slate-800 dark:hover:text-white",
|
|
23
|
+
closeIcon: "h-5 w-5 stroke-current fill-none",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const sizeClasses = {
|
|
27
|
+
sm: "w-full max-w-sm",
|
|
28
|
+
md: "w-full max-w-lg",
|
|
29
|
+
lg: "w-full max-w-2xl",
|
|
30
|
+
xl: "w-full max-w-4xl",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Modal = memo(
|
|
34
|
+
({
|
|
35
|
+
showModal = false,
|
|
36
|
+
setShowModal,
|
|
37
|
+
modalTitle = "Modal Title",
|
|
38
|
+
modalClass = defaultClasses.modal,
|
|
39
|
+
modalContentClass = defaultClasses.modalContent,
|
|
40
|
+
classModalContent = "",
|
|
41
|
+
modalTitleClass = defaultClasses.modalTitle,
|
|
42
|
+
classModalTitle = "",
|
|
43
|
+
className = "",
|
|
44
|
+
size = "md",
|
|
45
|
+
children,
|
|
46
|
+
showCloseButton = false,
|
|
47
|
+
dismissible = false,
|
|
48
|
+
titleId = "modal-title",
|
|
49
|
+
closeButtonContent,
|
|
50
|
+
animationDuration = 200,
|
|
51
|
+
showTitle = true,
|
|
52
|
+
}: ModalProps) => {
|
|
53
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
54
|
+
const previousActiveElement = useRef<HTMLElement | null>(null);
|
|
55
|
+
|
|
56
|
+
const handleEscKey = useCallback(
|
|
57
|
+
(event: KeyboardEvent) => {
|
|
58
|
+
if (event.key === "Escape" && showModal && dismissible) {
|
|
59
|
+
setShowModal(false);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
[showModal, dismissible, setShowModal]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const handleOutsideClick = useCallback(
|
|
66
|
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
67
|
+
if (e.target === e.currentTarget && dismissible) {
|
|
68
|
+
setShowModal(false);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[dismissible, setShowModal]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const handleClose = useCallback(() => {
|
|
75
|
+
setShowModal(false);
|
|
76
|
+
}, [setShowModal]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (showModal) {
|
|
80
|
+
previousActiveElement.current = document.activeElement as HTMLElement;
|
|
81
|
+
document.addEventListener("keydown", handleEscKey);
|
|
82
|
+
document.body.style.overflow = "hidden";
|
|
83
|
+
modalRef.current?.focus();
|
|
84
|
+
} else {
|
|
85
|
+
document.removeEventListener("keydown", handleEscKey);
|
|
86
|
+
document.body.style.overflow = "";
|
|
87
|
+
previousActiveElement.current?.focus();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
document.removeEventListener("keydown", handleEscKey);
|
|
92
|
+
document.body.style.overflow = "";
|
|
93
|
+
};
|
|
94
|
+
}, [showModal, handleEscKey]);
|
|
95
|
+
|
|
96
|
+
const modalStyles: React.CSSProperties = {
|
|
97
|
+
transition: `opacity ${animationDuration}ms ease-in-out`,
|
|
98
|
+
opacity: showModal ? 1 : 0,
|
|
99
|
+
visibility: showModal ? "visible" : "hidden",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (!showModal) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
className={modalClass}
|
|
109
|
+
aria-labelledby={titleId}
|
|
110
|
+
role="dialog"
|
|
111
|
+
aria-modal="true"
|
|
112
|
+
onClick={handleOutsideClick}
|
|
113
|
+
style={modalStyles}
|
|
114
|
+
>
|
|
115
|
+
<div
|
|
116
|
+
ref={modalRef}
|
|
117
|
+
className={twMerge(
|
|
118
|
+
modalContentClass,
|
|
119
|
+
sizeClasses[size],
|
|
120
|
+
className,
|
|
121
|
+
classModalContent
|
|
122
|
+
)}
|
|
123
|
+
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
|
124
|
+
role="document"
|
|
125
|
+
tabIndex={-1}
|
|
126
|
+
>
|
|
127
|
+
<div className={twMerge(modalTitleClass, classModalTitle)}>
|
|
128
|
+
{showTitle && <h2 id={titleId}>{modalTitle}</h2>}
|
|
129
|
+
{showCloseButton && (
|
|
130
|
+
<button
|
|
131
|
+
onClick={handleClose}
|
|
132
|
+
className={defaultClasses.closeButton}
|
|
133
|
+
aria-label="Close modal"
|
|
134
|
+
type="button"
|
|
135
|
+
>
|
|
136
|
+
{closeButtonContent || (
|
|
137
|
+
<Icon elements={x} svgClass={defaultClasses.closeIcon} />
|
|
138
|
+
)}
|
|
139
|
+
</button>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
{showTitle && <hr className="border-slate-200 dark:border-slate-700" />}
|
|
143
|
+
<div className="max-h-[calc(90vh-73px)] overflow-y-auto p-5 text-sm text-slate-700 dark:text-slate-200">
|
|
144
|
+
{children}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
|
|
140
152
|
Modal.displayName = "Modal";
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
export type ModalProps = {
|
|
2
|
-
showModal?: boolean;
|
|
3
|
-
setShowModal: (show: boolean) => void;
|
|
4
|
-
modalTitle?: string;
|
|
5
|
-
modalClass?: string;
|
|
6
|
-
modalContentClass?: string;
|
|
7
|
-
classModalContent?: string;
|
|
8
|
-
modalTitleClass?: string;
|
|
9
|
-
classModalTitle?: string;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
export type ModalProps = {
|
|
2
|
+
showModal?: boolean;
|
|
3
|
+
setShowModal: (show: boolean) => void;
|
|
4
|
+
modalTitle?: string;
|
|
5
|
+
modalClass?: string;
|
|
6
|
+
modalContentClass?: string;
|
|
7
|
+
classModalContent?: string;
|
|
8
|
+
modalTitleClass?: string;
|
|
9
|
+
classModalTitle?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
size?: "sm" | "md" | "lg" | "xl";
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
showCloseButton?: boolean;
|
|
14
|
+
dismissible?: boolean;
|
|
15
|
+
titleId?: string;
|
|
16
|
+
closeButtonContent?: React.ReactNode;
|
|
17
|
+
animationDuration?: number;
|
|
18
|
+
showTitle?: boolean;
|
|
17
19
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
|
-
import { popUp } from "
|
|
3
|
+
import { popUp } from "../../globalStyle";
|
|
4
4
|
|
|
5
5
|
export const PortalDropdown = ({ targetRef, children, isVisible }: any) => {
|
|
6
6
|
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
|
|
@@ -41,3 +41,4 @@ export const PortalDropdown = ({ targetRef, children, isVisible }: any) => {
|
|
|
41
41
|
document.body
|
|
42
42
|
);
|
|
43
43
|
};
|
|
44
|
+
|
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
} from "@grampro/headless-helpers";
|
|
22
22
|
import Icon from "../icon/Icon";
|
|
23
23
|
import { check, search, upDown, x } from "../icon/iconPaths";
|
|
24
|
-
import { iconClass, primary } from "
|
|
25
|
-
import { selectStyle } from "
|
|
24
|
+
import { iconClass, primary } from "../../globalStyle";
|
|
25
|
+
import { selectStyle } from "../../globalStyle";
|
|
26
26
|
import { PortalDropdown } from "./PortalDropDown";
|
|
27
27
|
|
|
28
28
|
const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
|
|
@@ -266,3 +266,4 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
|
|
|
266
266
|
|
|
267
267
|
const MemoizedMultiSelect = memo(MultiSelect);
|
|
268
268
|
export { MemoizedMultiSelect as MultiSelect };
|
|
269
|
+
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useState, useCallback } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
|
-
import { popUp } from "
|
|
3
|
+
import { popUp } from "../../globalStyle";
|
|
4
4
|
|
|
5
5
|
export const PortalDropdown = ({ targetRef, children, isVisible }: any) => {
|
|
6
6
|
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
if (
|
|
8
|
+
const updatePosition = useCallback(() => {
|
|
9
|
+
if (targetRef && targetRef.current) {
|
|
10
10
|
const rect = targetRef.current.getBoundingClientRect();
|
|
11
11
|
const scrollTop =
|
|
12
12
|
window.pageYOffset || document.documentElement.scrollTop;
|
|
@@ -19,7 +19,23 @@ export const PortalDropdown = ({ targetRef, children, isVisible }: any) => {
|
|
|
19
19
|
width: rect.width,
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
-
}, [
|
|
22
|
+
}, [targetRef]);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (isVisible) {
|
|
26
|
+
updatePosition();
|
|
27
|
+
|
|
28
|
+
// Listen to scroll events anywhere in the document (use capture to catch scroll inside elements)
|
|
29
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
30
|
+
// Listen to window resizing
|
|
31
|
+
window.addEventListener("resize", updatePosition);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
36
|
+
window.removeEventListener("resize", updatePosition);
|
|
37
|
+
};
|
|
38
|
+
}, [isVisible, updatePosition]);
|
|
23
39
|
|
|
24
40
|
if (!isVisible) return null;
|
|
25
41
|
|
|
@@ -16,8 +16,8 @@ import React, {
|
|
|
16
16
|
import Icon from "../icon/Icon";
|
|
17
17
|
import { check, search, upDown, x } from "../icon/iconPaths";
|
|
18
18
|
import type { ItemsProps, SelectHandle, SelectProps } from "./types";
|
|
19
|
-
import { selectStyle } from "
|
|
20
|
-
import { iconClass, primary } from "
|
|
19
|
+
import { selectStyle } from "../../globalStyle";
|
|
20
|
+
import { iconClass, primary } from "../../globalStyle";
|
|
21
21
|
import {
|
|
22
22
|
useSelectState,
|
|
23
23
|
useSelectData,
|
|
@@ -40,6 +40,7 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
|
|
40
40
|
error = undefined,
|
|
41
41
|
onFiltering,
|
|
42
42
|
disabled = false,
|
|
43
|
+
className = "",
|
|
43
44
|
} = props;
|
|
44
45
|
|
|
45
46
|
applyScrollbarStyles();
|
|
@@ -141,7 +142,7 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
|
|
141
142
|
name="search"
|
|
142
143
|
id="search"
|
|
143
144
|
placeholder="Search a value"
|
|
144
|
-
className="w-full outline-none "
|
|
145
|
+
className="w-full bg-transparent outline-none text-slate-900 placeholder:text-slate-400 dark:text-slate-100"
|
|
145
146
|
ref={inputRef}
|
|
146
147
|
value={searchTerm}
|
|
147
148
|
onChange={(e) => {
|
|
@@ -163,7 +164,7 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
|
|
163
164
|
}
|
|
164
165
|
}}
|
|
165
166
|
className={`${selectStyle["filter-button"]} ${
|
|
166
|
-
focusedIndex === index ? "bg-
|
|
167
|
+
focusedIndex === index ? "bg-sky-50 text-sky-700 dark:bg-sky-950" : ""
|
|
167
168
|
}`}
|
|
168
169
|
onClick={() => handleSelect({ value, label })}
|
|
169
170
|
onMouseEnter={() => setFocusedIndex(index)}
|
|
@@ -171,7 +172,7 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
|
|
171
172
|
<Icon
|
|
172
173
|
elements={check}
|
|
173
174
|
svgClass={`h-4 w-4 fill-none ${
|
|
174
|
-
selectedItem === value ? "stroke-
|
|
175
|
+
selectedItem === value ? "stroke-sky-600" : ""
|
|
175
176
|
}`}
|
|
176
177
|
/>
|
|
177
178
|
{label.length > 20 ? `${label.substring(0, 20)}...` : label}
|
|
@@ -186,7 +187,7 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
|
|
186
187
|
|
|
187
188
|
return (
|
|
188
189
|
<div
|
|
189
|
-
className=
|
|
190
|
+
className={`relative w-full ${className}`}
|
|
190
191
|
ref={selectRef}
|
|
191
192
|
id={id}
|
|
192
193
|
onKeyDown={handleKeyDown}
|
|
@@ -245,3 +246,4 @@ Select.displayName = "Select";
|
|
|
245
246
|
|
|
246
247
|
const MemoizedSelect = memo(Select);
|
|
247
248
|
export { MemoizedSelect as Select };
|
|
249
|
+
|
|
@@ -15,6 +15,7 @@ export type SelectProps = {
|
|
|
15
15
|
selectedItem?: string;
|
|
16
16
|
onFiltering?: (searchTerm: string) => void;
|
|
17
17
|
disabled?: boolean;
|
|
18
|
+
className?: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export type SelectHandle = {
|
|
@@ -25,3 +26,4 @@ export type SelectHandle = {
|
|
|
25
26
|
selectedDisplay: string;
|
|
26
27
|
selected: string | undefined;
|
|
27
28
|
};
|
|
29
|
+
|
|
@@ -1,52 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Grampro Business Services and affiliates.
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the MIT license found in the
|
|
5
|
-
* LICENSE file in the root directory of this source tree.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useState } from "react";
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Grampro Business Services and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { twMerge } from "tailwind-merge";
|
|
10
|
+
import type { TabsProps } from "./types";
|
|
11
|
+
|
|
12
|
+
export function Tabs({
|
|
13
|
+
tabs,
|
|
14
|
+
defaultTabId,
|
|
15
|
+
renderContent,
|
|
16
|
+
onChange,
|
|
17
|
+
className = "",
|
|
18
|
+
variant = "line",
|
|
19
|
+
}: TabsProps) {
|
|
20
|
+
const [activeTabId, setActiveTabId] = useState<string>(
|
|
21
|
+
defaultTabId || (tabs.length > 0 ? tabs[0].id : "")
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const handleTabChange = (tabId: string) => {
|
|
25
|
+
setActiveTabId(tabId);
|
|
26
|
+
if (onChange) {
|
|
27
|
+
onChange(tabId);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isPills = variant === "pills";
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={twMerge("w-full", className)}>
|
|
35
|
+
<nav
|
|
36
|
+
className={twMerge(
|
|
37
|
+
"flex gap-1",
|
|
38
|
+
isPills
|
|
39
|
+
? "rounded-lg bg-slate-100 p-1 dark:bg-slate-900"
|
|
40
|
+
: "border-b border-slate-200 dark:border-slate-700"
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{tabs.map((tab) => {
|
|
44
|
+
const isActive = activeTabId === tab.id;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
key={tab.id}
|
|
49
|
+
type="button"
|
|
50
|
+
disabled={tab.disabled}
|
|
51
|
+
className={twMerge(
|
|
52
|
+
"min-h-9 px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500 disabled:cursor-not-allowed disabled:text-slate-300",
|
|
53
|
+
isPills ? "rounded-md" : "-mb-px border-b-2 border-transparent",
|
|
54
|
+
isActive && isPills
|
|
55
|
+
? "bg-white text-slate-950 shadow-sm dark:bg-slate-800 dark:text-white"
|
|
56
|
+
: "",
|
|
57
|
+
isActive && !isPills
|
|
58
|
+
? "border-sky-600 text-sky-700"
|
|
59
|
+
: "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100"
|
|
60
|
+
)}
|
|
61
|
+
onClick={() => !tab.disabled && handleTabChange(tab.id)}
|
|
62
|
+
>
|
|
63
|
+
{tab.label}
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</nav>
|
|
68
|
+
<div className="pt-4">{renderContent(activeTabId)}</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
52
71
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
export interface TabItem {
|
|
4
|
-
id: string;
|
|
5
|
-
label: string;
|
|
6
|
-
disabled?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface TabsProps {
|
|
10
|
-
tabs: TabItem[];
|
|
11
|
-
defaultTabId?: string;
|
|
12
|
-
renderContent: (activeTabId: string) => ReactNode;
|
|
13
|
-
onChange?: (tabId: string) => void;
|
|
14
|
-
className?: string;
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface TabItem {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TabsProps {
|
|
10
|
+
tabs: TabItem[];
|
|
11
|
+
defaultTabId?: string;
|
|
12
|
+
renderContent: (activeTabId: string) => ReactNode;
|
|
13
|
+
onChange?: (tabId: string) => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
variant?: "line" | "pills";
|
|
15
16
|
}
|