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.
@@ -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 overflow-y-auto inset-0 flex items-center justify-center bg-[#2b2426bd]",
17
- modalContent: "bg-white m-10 md:w-[80vh] rounded-xl relative",
18
- modalTitle:
19
- "p-4 text-lg leading-6 font-medium text-gray-900 flex justify-between items-center",
20
- closeButton:
21
- "absolute top-2 right-2 p-2 hover:bg-gray-100 rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-gray-300",
22
- closeIcon: "h-5 w-5 stroke-gray-500 fill-none",
23
- };
24
-
25
- export const Modal = memo(
26
- ({
27
- showModal = false,
28
- setShowModal,
29
- modalTitle = "Modal Title",
30
- modalClass = defaultClasses.modal,
31
- modalContentClass = defaultClasses.modalContent,
32
- classModalContent = "",
33
- modalTitleClass = defaultClasses.modalTitle,
34
- classModalTitle = "",
35
- children,
36
- showCloseButton = false,
37
- dismissible = false,
38
- titleId = "modal-title",
39
- closeButtonContent,
40
- animationDuration = 200,
41
- showTitle = true,
42
- }: ModalProps) => {
43
- const modalRef = useRef<HTMLDivElement>(null);
44
- const previousActiveElement = useRef<HTMLElement | null>(null);
45
-
46
- // Handle ESC key press to close modal
47
- const handleEscKey = useCallback(
48
- (event: KeyboardEvent) => {
49
- if (event.key === "Escape" && showModal && dismissible) {
50
- setShowModal(false);
51
- }
52
- },
53
- [showModal, dismissible, setShowModal]
54
- );
55
-
56
- // Handle click outside modal to close
57
- const handleOutsideClick = useCallback(
58
- (e: React.MouseEvent<HTMLDivElement>) => {
59
- if (e.target === e.currentTarget && dismissible) {
60
- setShowModal(false);
61
- }
62
- },
63
- [dismissible, setShowModal]
64
- );
65
-
66
- // Handle close button click
67
- const handleClose = useCallback(() => {
68
- setShowModal(false);
69
- }, [setShowModal]);
70
-
71
- // Manage focus trap and keyboard events
72
- useEffect(() => {
73
- if (showModal) {
74
- previousActiveElement.current = document.activeElement as HTMLElement;
75
- document.addEventListener("keydown", handleEscKey);
76
- document.body.style.overflow = "hidden";
77
- modalRef.current?.focus();
78
- } else {
79
- document.removeEventListener("keydown", handleEscKey);
80
- document.body.style.overflow = "";
81
- previousActiveElement.current?.focus();
82
- }
83
-
84
- return () => {
85
- document.removeEventListener("keydown", handleEscKey);
86
- document.body.style.overflow = "";
87
- };
88
- }, [showModal, handleEscKey]);
89
-
90
- // Animation styles
91
- const modalStyles: React.CSSProperties = {
92
- transition: `opacity ${animationDuration}ms ease-in-out`,
93
- opacity: showModal ? 1 : 0,
94
- visibility: showModal ? "visible" : "hidden",
95
- };
96
-
97
- if (!showModal) {
98
- return null;
99
- }
100
-
101
- return (
102
- <div
103
- className={modalClass}
104
- aria-labelledby={titleId}
105
- role="dialog"
106
- aria-modal="true"
107
- onClick={handleOutsideClick}
108
- style={modalStyles}
109
- >
110
- <div
111
- ref={modalRef}
112
- className={twMerge(modalContentClass, classModalContent)}
113
- onClick={(e: React.MouseEvent) => e.stopPropagation()}
114
- role="document"
115
- tabIndex={-1}
116
- >
117
- <div className={twMerge(modalTitleClass, classModalTitle)}>
118
- {showTitle && <h2 id={titleId}>{modalTitle}</h2>}
119
- {showCloseButton && (
120
- <button
121
- onClick={handleClose}
122
- className={defaultClasses.closeButton}
123
- aria-label="Close modal"
124
- type="button"
125
- >
126
- {closeButtonContent || (
127
- <Icon elements={x} svgClass={defaultClasses.closeIcon} />
128
- )}
129
- </button>
130
- )}
131
- </div>
132
- {showTitle && <hr className="border-gray-200" />}
133
- <div className="p-4">{children}</div>
134
- </div>
135
- </div>
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
- children: React.ReactNode;
11
- showCloseButton?: boolean;
12
- dismissible?: boolean;
13
- titleId?: string;
14
- closeButtonContent?: React.ReactNode;
15
- animationDuration?: number;
16
- showTitle?: boolean;
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 "../globalStyle";
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 "../globalStyle";
25
- import { selectStyle } from "../globalStyle";
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 "../globalStyle";
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
- useEffect(() => {
9
- if (isVisible && targetRef.current) {
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
- }, [isVisible, targetRef]);
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 "../globalStyle";
20
- import { iconClass, primary } from "../globalStyle";
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-gray-100 dark:bg-gray-700" : ""
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-gray-500" : ""
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="relative w-full"
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 type { TabsProps } from "./types";
10
-
11
- export function Tabs({
12
- tabs,
13
- defaultTabId,
14
- renderContent,
15
- onChange,
16
- className = "",
17
- }: TabsProps) {
18
- const [activeTabId, setActiveTabId] = useState<string>(
19
- defaultTabId || (tabs.length > 0 ? tabs[0].id : "")
20
- );
21
-
22
- const handleTabChange = (tabId: string) => {
23
- setActiveTabId(tabId);
24
- if (onChange) {
25
- onChange(tabId);
26
- }
27
- };
28
-
29
- return (
30
- <div className={`w-full ${className}`}>
31
- <nav className="flex border-b border-gray-200">
32
- {tabs.map((tab) => (
33
- <button
34
- key={tab.id}
35
- disabled={tab.disabled}
36
- className={`px-4 py-2 text-sm font-medium transition-colors ${
37
- tab.disabled
38
- ? "text-gray-300 cursor-not-allowed"
39
- : activeTabId === tab.id
40
- ? "border-b-2 border-black text-black"
41
- : "text-gray-500 hover:text-gray-700 hover:border-gray-300"
42
- }`}
43
- onClick={() => !tab.disabled && handleTabChange(tab.id)}
44
- >
45
- {tab.label}
46
- </button>
47
- ))}
48
- </nav>
49
- <div className="pt-4">{renderContent(activeTabId)}</div>
50
- </div>
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
  }