analytica-frontend-lib 1.0.68 → 1.0.70

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.
@@ -0,0 +1,166 @@
1
+ // src/components/Auth/Auth.tsx
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useState,
7
+ useCallback,
8
+ useMemo
9
+ } from "react";
10
+ import { useLocation, Navigate } from "react-router-dom";
11
+ import { Fragment, jsx } from "react/jsx-runtime";
12
+ var AuthContext = createContext(void 0);
13
+ var AuthProvider = ({
14
+ children,
15
+ checkAuthFn,
16
+ signOutFn,
17
+ initialAuthState = {},
18
+ getUserFn,
19
+ getSessionFn,
20
+ getTokensFn
21
+ }) => {
22
+ const [authState, setAuthState] = useState({
23
+ isAuthenticated: false,
24
+ isLoading: true,
25
+ ...initialAuthState
26
+ });
27
+ const checkAuth = useCallback(async () => {
28
+ try {
29
+ setAuthState((prev) => ({ ...prev, isLoading: true }));
30
+ if (!checkAuthFn) {
31
+ setAuthState((prev) => ({
32
+ ...prev,
33
+ isAuthenticated: false,
34
+ isLoading: false
35
+ }));
36
+ return false;
37
+ }
38
+ const isAuth = await checkAuthFn();
39
+ setAuthState((prev) => ({
40
+ ...prev,
41
+ isAuthenticated: isAuth,
42
+ isLoading: false,
43
+ user: getUserFn ? getUserFn() : prev.user,
44
+ sessionInfo: getSessionFn ? getSessionFn() : prev.sessionInfo,
45
+ tokens: getTokensFn ? getTokensFn() : prev.tokens
46
+ }));
47
+ return isAuth;
48
+ } catch (error) {
49
+ console.error("Erro ao verificar autentica\xE7\xE3o:", error);
50
+ setAuthState((prev) => ({
51
+ ...prev,
52
+ isAuthenticated: false,
53
+ isLoading: false
54
+ }));
55
+ return false;
56
+ }
57
+ }, [checkAuthFn, getUserFn, getSessionFn, getTokensFn]);
58
+ const signOut = useCallback(() => {
59
+ if (signOutFn) {
60
+ signOutFn();
61
+ }
62
+ setAuthState((prev) => ({
63
+ ...prev,
64
+ isAuthenticated: false,
65
+ user: void 0,
66
+ sessionInfo: void 0,
67
+ tokens: void 0
68
+ }));
69
+ }, [signOutFn]);
70
+ useEffect(() => {
71
+ checkAuth();
72
+ }, [checkAuth]);
73
+ const contextValue = useMemo(
74
+ () => ({
75
+ ...authState,
76
+ checkAuth,
77
+ signOut
78
+ }),
79
+ [authState, checkAuth, signOut]
80
+ );
81
+ return /* @__PURE__ */ jsx(AuthContext.Provider, { value: contextValue, children });
82
+ };
83
+ var useAuth = () => {
84
+ const context = useContext(AuthContext);
85
+ if (context === void 0) {
86
+ throw new Error("useAuth deve ser usado dentro de um AuthProvider");
87
+ }
88
+ return context;
89
+ };
90
+ var ProtectedRoute = ({
91
+ children,
92
+ redirectTo = "/",
93
+ loadingComponent,
94
+ additionalCheck
95
+ }) => {
96
+ const { isAuthenticated, isLoading, ...authState } = useAuth();
97
+ const defaultLoadingComponent = /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center min-h-screen", children: /* @__PURE__ */ jsx("div", { className: "text-text-950 text-lg", children: "Carregando..." }) });
98
+ if (isLoading) {
99
+ return /* @__PURE__ */ jsx(Fragment, { children: loadingComponent || defaultLoadingComponent });
100
+ }
101
+ if (!isAuthenticated) {
102
+ return /* @__PURE__ */ jsx(Navigate, { to: redirectTo, replace: true });
103
+ }
104
+ if (additionalCheck && !additionalCheck({ isAuthenticated, isLoading, ...authState })) {
105
+ return /* @__PURE__ */ jsx(Navigate, { to: redirectTo, replace: true });
106
+ }
107
+ return /* @__PURE__ */ jsx(Fragment, { children });
108
+ };
109
+ var PublicRoute = ({
110
+ children,
111
+ redirectTo = "/painel",
112
+ redirectIfAuthenticated = false,
113
+ checkAuthBeforeRender = false
114
+ }) => {
115
+ const { isAuthenticated, isLoading } = useAuth();
116
+ if (checkAuthBeforeRender && isLoading) {
117
+ return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center min-h-screen", children: /* @__PURE__ */ jsx("div", { className: "text-text-950 text-lg", children: "Carregando..." }) });
118
+ }
119
+ if (isAuthenticated && redirectIfAuthenticated) {
120
+ return /* @__PURE__ */ jsx(Navigate, { to: redirectTo, replace: true });
121
+ }
122
+ return /* @__PURE__ */ jsx(Fragment, { children });
123
+ };
124
+ var withAuth = (Component, options = {}) => {
125
+ return (props) => /* @__PURE__ */ jsx(ProtectedRoute, { ...options, children: /* @__PURE__ */ jsx(Component, { ...props }) });
126
+ };
127
+ var useAuthGuard = (options = {}) => {
128
+ const authState = useAuth();
129
+ const { requireAuth = true, customCheck } = options;
130
+ const canAccess = !authState.isLoading && (requireAuth ? authState.isAuthenticated && (!customCheck || customCheck(authState)) : !authState.isAuthenticated || !customCheck || customCheck(authState));
131
+ return {
132
+ canAccess,
133
+ isLoading: authState.isLoading,
134
+ authState
135
+ };
136
+ };
137
+ var useRouteAuth = (fallbackPath = "/") => {
138
+ const { isAuthenticated, isLoading } = useAuth();
139
+ const location = useLocation();
140
+ const redirectToLogin = () => /* @__PURE__ */ jsx(Navigate, { to: fallbackPath, state: { from: location }, replace: true });
141
+ return {
142
+ isAuthenticated,
143
+ isLoading,
144
+ redirectToLogin
145
+ };
146
+ };
147
+ var Auth_default = {
148
+ AuthProvider,
149
+ ProtectedRoute,
150
+ PublicRoute,
151
+ withAuth,
152
+ useAuth,
153
+ useAuthGuard,
154
+ useRouteAuth
155
+ };
156
+ export {
157
+ AuthProvider,
158
+ ProtectedRoute,
159
+ PublicRoute,
160
+ Auth_default as default,
161
+ useAuth,
162
+ useAuthGuard,
163
+ useRouteAuth,
164
+ withAuth
165
+ };
166
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/Auth/Auth.tsx"],"sourcesContent":["import {\n createContext,\n useContext,\n useEffect,\n useState,\n ReactNode,\n ComponentType,\n useCallback,\n useMemo,\n} from 'react';\nimport { useLocation, Navigate } from 'react-router-dom';\n\n/**\n * Interface básica para tokens de autenticação\n */\nexport interface AuthTokens {\n token: string;\n refreshToken: string;\n [key: string]: unknown;\n}\n\n/**\n * Interface básica para usuário\n */\nexport interface AuthUser {\n id: string;\n name?: string;\n email?: string;\n [key: string]: unknown;\n}\n\n/**\n * Interface básica para informações de sessão\n */\nexport interface SessionInfo {\n institutionId?: string;\n profileId?: string;\n schoolId?: string;\n schoolYearId?: string;\n classId?: string;\n [key: string]: unknown;\n}\n\n/**\n * Interface para o estado de autenticação\n */\nexport interface AuthState {\n isAuthenticated: boolean;\n isLoading: boolean;\n user?: AuthUser | null;\n sessionInfo?: SessionInfo | null;\n tokens?: AuthTokens | null;\n}\n\n/**\n * Interface para as funções de autenticação\n */\nexport interface AuthContextType extends AuthState {\n checkAuth: () => Promise<boolean>;\n signOut: () => void;\n}\n\n/**\n * Context de autenticação\n */\nconst AuthContext = createContext<AuthContextType | undefined>(undefined);\n\n/**\n * Props do AuthProvider\n */\nexport interface AuthProviderProps {\n children: ReactNode;\n /**\n * Função para verificar se o usuário está autenticado\n * Deve retornar uma Promise<boolean>\n */\n checkAuthFn?: () => Promise<boolean> | boolean;\n /**\n * Função para fazer logout\n */\n signOutFn?: () => void;\n /**\n * Estado de autenticação inicial\n */\n initialAuthState?: Partial<AuthState>;\n /**\n * Função para obter dados do usuário (opcional)\n */\n getUserFn?: () => AuthUser | null | undefined;\n /**\n * Função para obter informações da sessão (opcional)\n */\n getSessionFn?: () => SessionInfo | null | undefined;\n /**\n * Função para obter tokens (opcional)\n */\n getTokensFn?: () => AuthTokens | null | undefined;\n}\n\n/**\n * Provider de autenticação que gerencia o estado global de auth\n * Compatível com qualquer store (Zustand, Redux, Context, etc.)\n */\nexport const AuthProvider = ({\n children,\n checkAuthFn,\n signOutFn,\n initialAuthState = {},\n getUserFn,\n getSessionFn,\n getTokensFn,\n}: AuthProviderProps) => {\n const [authState, setAuthState] = useState<AuthState>({\n isAuthenticated: false,\n isLoading: true,\n ...initialAuthState,\n });\n\n const checkAuth = useCallback(async (): Promise<boolean> => {\n try {\n setAuthState((prev) => ({ ...prev, isLoading: true }));\n\n // Se não há função de verificação, assume como não autenticado\n if (!checkAuthFn) {\n setAuthState((prev) => ({\n ...prev,\n isAuthenticated: false,\n isLoading: false,\n }));\n return false;\n }\n\n const isAuth = await checkAuthFn();\n\n setAuthState((prev) => ({\n ...prev,\n isAuthenticated: isAuth,\n isLoading: false,\n user: getUserFn ? getUserFn() : prev.user,\n sessionInfo: getSessionFn ? getSessionFn() : prev.sessionInfo,\n tokens: getTokensFn ? getTokensFn() : prev.tokens,\n }));\n\n return isAuth;\n } catch (error) {\n console.error('Erro ao verificar autenticação:', error);\n setAuthState((prev) => ({\n ...prev,\n isAuthenticated: false,\n isLoading: false,\n }));\n return false;\n }\n }, [checkAuthFn, getUserFn, getSessionFn, getTokensFn]);\n\n const signOut = useCallback(() => {\n if (signOutFn) {\n signOutFn();\n }\n setAuthState((prev) => ({\n ...prev,\n isAuthenticated: false,\n user: undefined,\n sessionInfo: undefined,\n tokens: undefined,\n }));\n }, [signOutFn]);\n\n useEffect(() => {\n checkAuth();\n }, [checkAuth]);\n\n const contextValue = useMemo(\n (): AuthContextType => ({\n ...authState,\n checkAuth,\n signOut,\n }),\n [authState, checkAuth, signOut]\n );\n\n return (\n <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>\n );\n};\n\n/**\n * Hook para usar o contexto de autenticação\n */\nexport const useAuth = (): AuthContextType => {\n const context = useContext(AuthContext);\n if (context === undefined) {\n throw new Error('useAuth deve ser usado dentro de um AuthProvider');\n }\n return context;\n};\n\n/**\n * Props do ProtectedRoute\n */\nexport interface ProtectedRouteProps {\n children: ReactNode;\n /**\n * Path para redirecionamento quando não autenticado\n */\n redirectTo?: string;\n /**\n * Componente de loading personalizado\n */\n loadingComponent?: ReactNode;\n /**\n * Função adicional de verificação (ex: verificar permissões específicas)\n */\n additionalCheck?: (authState: AuthState) => boolean;\n}\n\n/**\n * Componente para proteger rotas que requerem autenticação\n *\n * @example\n * ```tsx\n * <ProtectedRoute redirectTo=\"/login\">\n * <PainelPage />\n * </ProtectedRoute>\n * ```\n */\nexport const ProtectedRoute = ({\n children,\n redirectTo = '/',\n loadingComponent,\n additionalCheck,\n}: ProtectedRouteProps) => {\n const { isAuthenticated, isLoading, ...authState } = useAuth();\n\n // Componente de loading padrão\n const defaultLoadingComponent = (\n <div className=\"flex items-center justify-center min-h-screen\">\n <div className=\"text-text-950 text-lg\">Carregando...</div>\n </div>\n );\n\n // Mostrar loading enquanto verifica autenticação\n if (isLoading) {\n return <>{loadingComponent || defaultLoadingComponent}</>;\n }\n\n // Verificar autenticação básica\n if (!isAuthenticated) {\n return <Navigate to={redirectTo} replace />;\n }\n\n // Verificação adicional (ex: permissões)\n if (\n additionalCheck &&\n !additionalCheck({ isAuthenticated, isLoading, ...authState })\n ) {\n return <Navigate to={redirectTo} replace />;\n }\n\n return <>{children}</>;\n};\n\n/**\n * Props do PublicRoute\n */\nexport interface PublicRouteProps {\n children: ReactNode;\n /**\n * Path para redirecionamento\n */\n redirectTo?: string;\n /**\n * Se deve redirecionar quando usuário estiver autenticado\n */\n redirectIfAuthenticated?: boolean;\n /**\n * Se deve verificar autenticação antes de renderizar\n */\n checkAuthBeforeRender?: boolean;\n}\n\n/**\n * Componente para rotas públicas (login, recuperação de senha, etc.)\n * Opcionalmente redireciona se o usuário já estiver autenticado\n *\n * @example\n * ```tsx\n * <PublicRoute redirectTo=\"/painel\" redirectIfAuthenticated={true}>\n * <LoginPage />\n * </PublicRoute>\n * ```\n */\nexport const PublicRoute = ({\n children,\n redirectTo = '/painel',\n redirectIfAuthenticated = false,\n checkAuthBeforeRender = false,\n}: PublicRouteProps) => {\n const { isAuthenticated, isLoading } = useAuth();\n\n // Se deve aguardar verificação de auth antes de renderizar\n if (checkAuthBeforeRender && isLoading) {\n return (\n <div className=\"flex items-center justify-center min-h-screen\">\n <div className=\"text-text-950 text-lg\">Carregando...</div>\n </div>\n );\n }\n\n // Redirecionar se já autenticado e configurado para isso\n if (isAuthenticated && redirectIfAuthenticated) {\n return <Navigate to={redirectTo} replace />;\n }\n\n return <>{children}</>;\n};\n\n/**\n * HOC para proteger componentes com autenticação\n *\n * @example\n * ```tsx\n * const ProtectedComponent = withAuth(MyComponent, {\n * fallback: \"/login\",\n * loadingComponent: <CustomSpinner />\n * });\n * ```\n */\nexport const withAuth = <P extends object>(\n Component: ComponentType<P>,\n options: Omit<ProtectedRouteProps, 'children'> = {}\n) => {\n return (props: P) => (\n <ProtectedRoute {...options}>\n <Component {...props} />\n </ProtectedRoute>\n );\n};\n\n/**\n * Hook para guard de autenticação com verificações customizadas\n *\n * @example\n * ```tsx\n * const { canAccess, isLoading } = useAuthGuard({\n * requireAuth: true,\n * customCheck: (user) => user?.role === 'admin'\n * });\n * ```\n */\nexport const useAuthGuard = (\n options: {\n requireAuth?: boolean;\n customCheck?: (authState: AuthState) => boolean;\n } = {}\n) => {\n const authState = useAuth();\n const { requireAuth = true, customCheck } = options;\n\n const canAccess =\n !authState.isLoading &&\n (requireAuth\n ? authState.isAuthenticated && (!customCheck || customCheck(authState))\n : !authState.isAuthenticated || !customCheck || customCheck(authState));\n\n return {\n canAccess,\n isLoading: authState.isLoading,\n authState,\n };\n};\n\n/**\n * Hook para verificar autenticação em rotas específicas\n * Útil para verificações condicionais dentro de componentes\n *\n * @example\n * ```tsx\n * const { isAuthenticated, redirectToLogin } = useRouteAuth();\n *\n * if (!isAuthenticated) {\n * return redirectToLogin();\n * }\n * ```\n */\nexport const useRouteAuth = (fallbackPath = '/') => {\n const { isAuthenticated, isLoading } = useAuth();\n const location = useLocation();\n\n const redirectToLogin = () => (\n <Navigate to={fallbackPath} state={{ from: location }} replace />\n );\n\n return {\n isAuthenticated,\n isLoading,\n redirectToLogin,\n };\n};\n\nexport default {\n AuthProvider,\n ProtectedRoute,\n PublicRoute,\n withAuth,\n useAuth,\n useAuthGuard,\n useRouteAuth,\n};\n"],"mappings":"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","names":[]}
@@ -0,0 +1,37 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes } from 'react';
3
+
4
+ /**
5
+ * Individual tab item interface
6
+ */
7
+ interface TabItem {
8
+ /** Unique identifier for the tab */
9
+ id: string;
10
+ /** Label text for the tab */
11
+ label: string;
12
+ /** Alternative label for mobile (optional) */
13
+ mobileLabel?: string;
14
+ /** Whether the tab is disabled */
15
+ disabled?: boolean;
16
+ }
17
+ /**
18
+ * Tab component props interface
19
+ */
20
+ interface TabProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
21
+ /** Array of tab items */
22
+ tabs: TabItem[];
23
+ /** Currently active tab ID */
24
+ activeTab: string;
25
+ /** Callback when tab changes */
26
+ onTabChange: (tabId: string) => void;
27
+ /** Size variant of the tabs */
28
+ size?: 'small' | 'medium' | 'large';
29
+ /** Whether to enable responsive behavior */
30
+ responsive?: boolean;
31
+ }
32
+ /**
33
+ * Tab component following the established architecture patterns
34
+ */
35
+ declare const Tab: react.ForwardRefExoticComponent<TabProps & react.RefAttributes<HTMLDivElement>>;
36
+
37
+ export { type TabItem, type TabProps, Tab as default };
@@ -0,0 +1,37 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes } from 'react';
3
+
4
+ /**
5
+ * Individual tab item interface
6
+ */
7
+ interface TabItem {
8
+ /** Unique identifier for the tab */
9
+ id: string;
10
+ /** Label text for the tab */
11
+ label: string;
12
+ /** Alternative label for mobile (optional) */
13
+ mobileLabel?: string;
14
+ /** Whether the tab is disabled */
15
+ disabled?: boolean;
16
+ }
17
+ /**
18
+ * Tab component props interface
19
+ */
20
+ interface TabProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
21
+ /** Array of tab items */
22
+ tabs: TabItem[];
23
+ /** Currently active tab ID */
24
+ activeTab: string;
25
+ /** Callback when tab changes */
26
+ onTabChange: (tabId: string) => void;
27
+ /** Size variant of the tabs */
28
+ size?: 'small' | 'medium' | 'large';
29
+ /** Whether to enable responsive behavior */
30
+ responsive?: boolean;
31
+ }
32
+ /**
33
+ * Tab component following the established architecture patterns
34
+ */
35
+ declare const Tab: react.ForwardRefExoticComponent<TabProps & react.RefAttributes<HTMLDivElement>>;
36
+
37
+ export { type TabItem, type TabProps, Tab as default };
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/components/Tab/Tab.tsx
21
+ var Tab_exports = {};
22
+ __export(Tab_exports, {
23
+ default: () => Tab_default
24
+ });
25
+ module.exports = __toCommonJS(Tab_exports);
26
+ var import_react = require("react");
27
+ var import_jsx_runtime = require("react/jsx-runtime");
28
+ var TAB_SIZE_CLASSES = {
29
+ small: {
30
+ container: "h-10 gap-1",
31
+ tab: "px-3 py-2 text-sm",
32
+ indicator: "h-0.5"
33
+ },
34
+ medium: {
35
+ container: "h-12 gap-2",
36
+ tab: "px-4 py-4 text-sm",
37
+ indicator: "h-1"
38
+ },
39
+ large: {
40
+ container: "h-14 gap-2",
41
+ tab: "px-6 py-4 text-base",
42
+ indicator: "h-1"
43
+ }
44
+ };
45
+ var RESPONSIVE_WIDTH_CLASSES = {
46
+ twoTabs: "w-[115px] sm:w-[204px]",
47
+ threeTabs: "w-[100px] sm:w-[160px]",
48
+ fourTabs: "w-[80px] sm:w-[140px]",
49
+ fiveTabs: "w-[70px] sm:w-[120px]",
50
+ default: "flex-1"
51
+ };
52
+ var Tab = (0, import_react.forwardRef)(
53
+ ({
54
+ tabs,
55
+ activeTab,
56
+ onTabChange,
57
+ size = "medium",
58
+ responsive = true,
59
+ className = "",
60
+ ...props
61
+ }, ref) => {
62
+ const sizeClasses = TAB_SIZE_CLASSES[size];
63
+ const getResponsiveWidthClass = (tabCount) => {
64
+ if (!responsive) return RESPONSIVE_WIDTH_CLASSES.default;
65
+ switch (tabCount) {
66
+ case 2:
67
+ return RESPONSIVE_WIDTH_CLASSES.twoTabs;
68
+ case 3:
69
+ return RESPONSIVE_WIDTH_CLASSES.threeTabs;
70
+ case 4:
71
+ return RESPONSIVE_WIDTH_CLASSES.fourTabs;
72
+ case 5:
73
+ return RESPONSIVE_WIDTH_CLASSES.fiveTabs;
74
+ default:
75
+ return RESPONSIVE_WIDTH_CLASSES.default;
76
+ }
77
+ };
78
+ const handleTabClick = (tabId) => {
79
+ const tab = tabs.find((t) => t.id === tabId);
80
+ if (tab && !tab.disabled) {
81
+ onTabChange(tabId);
82
+ }
83
+ };
84
+ const wrapAroundIndex = (index, maxLength) => {
85
+ if (index < 0) return maxLength - 1;
86
+ if (index >= maxLength) return 0;
87
+ return index;
88
+ };
89
+ const findNextValidTab = (startIndex, direction) => {
90
+ let nextIndex = wrapAroundIndex(startIndex + direction, tabs.length);
91
+ let attempts = 0;
92
+ while (tabs[nextIndex]?.disabled && attempts < tabs.length) {
93
+ nextIndex = wrapAroundIndex(nextIndex + direction, tabs.length);
94
+ attempts++;
95
+ }
96
+ return nextIndex;
97
+ };
98
+ const handleArrowNavigation = (direction) => {
99
+ const currentIndex = tabs.findIndex((tab) => tab.id === activeTab);
100
+ const nextIndex = findNextValidTab(currentIndex, direction);
101
+ if (!tabs[nextIndex]?.disabled && nextIndex !== currentIndex) {
102
+ handleTabClick(tabs[nextIndex].id);
103
+ }
104
+ };
105
+ const handleKeyDown = (event, tabId) => {
106
+ if (event.key === "Enter" || event.key === " ") {
107
+ event.preventDefault();
108
+ handleTabClick(tabId);
109
+ return;
110
+ }
111
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
112
+ event.preventDefault();
113
+ const direction = event.key === "ArrowLeft" ? -1 : 1;
114
+ handleArrowNavigation(direction);
115
+ }
116
+ };
117
+ const getTabClassNames = (isDisabled, isActive) => {
118
+ if (isDisabled) {
119
+ return "text-text-400 cursor-not-allowed opacity-50";
120
+ }
121
+ if (isActive) {
122
+ return "text-text-950";
123
+ }
124
+ return "text-text-700 hover:text-text-800";
125
+ };
126
+ const tabWidthClass = getResponsiveWidthClass(tabs.length);
127
+ const containerWidth = responsive && tabs.length <= 2 ? "w-[240px] sm:w-[416px]" : "w-full";
128
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
129
+ "div",
130
+ {
131
+ ref,
132
+ className: `flex flex-row items-start ${sizeClasses.container} ${containerWidth} ${className}`,
133
+ role: "tablist",
134
+ ...props,
135
+ children: tabs.map((tab) => {
136
+ const isActive = tab.id === activeTab;
137
+ const isDisabled = Boolean(tab.disabled);
138
+ const tabClassNames = getTabClassNames(isDisabled, isActive);
139
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
140
+ "button",
141
+ {
142
+ type: "button",
143
+ role: "tab",
144
+ "aria-selected": isActive,
145
+ "aria-disabled": isDisabled,
146
+ tabIndex: isActive ? 0 : -1,
147
+ className: `
148
+ relative flex flex-row justify-center items-center gap-2 rounded transition-colors isolate
149
+ ${sizeClasses.tab}
150
+ ${tabWidthClass}
151
+ ${tabClassNames}
152
+ ${!isDisabled && !isActive ? "hover:bg-background-50" : ""}
153
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2
154
+ `,
155
+ onClick: () => handleTabClick(tab.id),
156
+ onKeyDown: (e) => handleKeyDown(e, tab.id),
157
+ disabled: isDisabled,
158
+ "data-testid": `tab-${tab.id}`,
159
+ children: [
160
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-bold leading-4 tracking-[0.2px] truncate", children: responsive && tab.mobileLabel ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
161
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "sm:hidden", children: tab.mobileLabel }),
162
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "hidden sm:inline", children: tab.label })
163
+ ] }) : tab.label }),
164
+ isActive && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
165
+ "div",
166
+ {
167
+ className: `absolute bottom-0 left-2 right-2 bg-primary-700 rounded-lg z-[2] ${sizeClasses.indicator}`,
168
+ "data-testid": "active-indicator"
169
+ }
170
+ )
171
+ ]
172
+ },
173
+ tab.id
174
+ );
175
+ })
176
+ }
177
+ );
178
+ }
179
+ );
180
+ Tab.displayName = "Tab";
181
+ var Tab_default = Tab;
182
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/Tab/Tab.tsx"],"sourcesContent":["import { forwardRef, HTMLAttributes, KeyboardEvent } from 'react';\n\n/**\n * Individual tab item interface\n */\nexport interface TabItem {\n /** Unique identifier for the tab */\n id: string;\n /** Label text for the tab */\n label: string;\n /** Alternative label for mobile (optional) */\n mobileLabel?: string;\n /** Whether the tab is disabled */\n disabled?: boolean;\n}\n\n/**\n * Tab component props interface\n */\nexport interface TabProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /** Array of tab items */\n tabs: TabItem[];\n /** Currently active tab ID */\n activeTab: string;\n /** Callback when tab changes */\n onTabChange: (tabId: string) => void;\n /** Size variant of the tabs */\n size?: 'small' | 'medium' | 'large';\n /** Whether to enable responsive behavior */\n responsive?: boolean;\n}\n\n/**\n * Size configuration lookup table\n */\nconst TAB_SIZE_CLASSES = {\n small: {\n container: 'h-10 gap-1',\n tab: 'px-3 py-2 text-sm',\n indicator: 'h-0.5',\n },\n medium: {\n container: 'h-12 gap-2',\n tab: 'px-4 py-4 text-sm',\n indicator: 'h-1',\n },\n large: {\n container: 'h-14 gap-2',\n tab: 'px-6 py-4 text-base',\n indicator: 'h-1',\n },\n} as const;\n\n/**\n * Responsive width classes for tabs\n */\nconst RESPONSIVE_WIDTH_CLASSES = {\n twoTabs: 'w-[115px] sm:w-[204px]',\n threeTabs: 'w-[100px] sm:w-[160px]',\n fourTabs: 'w-[80px] sm:w-[140px]',\n fiveTabs: 'w-[70px] sm:w-[120px]',\n default: 'flex-1',\n} as const;\n\n/**\n * Tab component following the established architecture patterns\n */\nconst Tab = forwardRef<HTMLDivElement, TabProps>(\n (\n {\n tabs,\n activeTab,\n onTabChange,\n size = 'medium',\n responsive = true,\n className = '',\n ...props\n },\n ref\n ) => {\n const sizeClasses = TAB_SIZE_CLASSES[size];\n\n /**\n * Get responsive width class based on number of tabs\n */\n const getResponsiveWidthClass = (tabCount: number): string => {\n if (!responsive) return RESPONSIVE_WIDTH_CLASSES.default;\n\n switch (tabCount) {\n case 2:\n return RESPONSIVE_WIDTH_CLASSES.twoTabs;\n case 3:\n return RESPONSIVE_WIDTH_CLASSES.threeTabs;\n case 4:\n return RESPONSIVE_WIDTH_CLASSES.fourTabs;\n case 5:\n return RESPONSIVE_WIDTH_CLASSES.fiveTabs;\n default:\n return RESPONSIVE_WIDTH_CLASSES.default;\n }\n };\n\n /**\n * Handle tab click\n */\n const handleTabClick = (tabId: string) => {\n const tab = tabs.find((t) => t.id === tabId);\n if (tab && !tab.disabled) {\n onTabChange(tabId);\n }\n };\n\n /**\n * Wrap index around array bounds\n */\n const wrapAroundIndex = (index: number, maxLength: number): number => {\n if (index < 0) return maxLength - 1;\n if (index >= maxLength) return 0;\n return index;\n };\n\n /**\n * Find next valid (non-disabled) tab index\n */\n const findNextValidTab = (\n startIndex: number,\n direction: number\n ): number => {\n let nextIndex = wrapAroundIndex(startIndex + direction, tabs.length);\n let attempts = 0;\n\n while (tabs[nextIndex]?.disabled && attempts < tabs.length) {\n nextIndex = wrapAroundIndex(nextIndex + direction, tabs.length);\n attempts++;\n }\n\n return nextIndex;\n };\n\n /**\n * Handle arrow key navigation\n */\n const handleArrowNavigation = (direction: number): void => {\n const currentIndex = tabs.findIndex((tab) => tab.id === activeTab);\n const nextIndex = findNextValidTab(currentIndex, direction);\n\n if (!tabs[nextIndex]?.disabled && nextIndex !== currentIndex) {\n handleTabClick(tabs[nextIndex].id);\n }\n };\n\n /**\n * Handle keyboard navigation\n */\n const handleKeyDown = (\n event: KeyboardEvent<HTMLButtonElement>,\n tabId: string\n ) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n handleTabClick(tabId);\n return;\n }\n\n if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {\n event.preventDefault();\n const direction = event.key === 'ArrowLeft' ? -1 : 1;\n handleArrowNavigation(direction);\n }\n };\n\n /**\n * Get tab text and interaction classes based on state\n */\n const getTabClassNames = (\n isDisabled: boolean,\n isActive: boolean\n ): string => {\n if (isDisabled) {\n return 'text-text-400 cursor-not-allowed opacity-50';\n }\n\n if (isActive) {\n return 'text-text-950';\n }\n\n return 'text-text-700 hover:text-text-800';\n };\n\n const tabWidthClass = getResponsiveWidthClass(tabs.length);\n const containerWidth =\n responsive && tabs.length <= 2 ? 'w-[240px] sm:w-[416px]' : 'w-full';\n\n return (\n <div\n ref={ref}\n className={`flex flex-row items-start ${sizeClasses.container} ${containerWidth} ${className}`}\n role=\"tablist\"\n {...props}\n >\n {tabs.map((tab) => {\n const isActive = tab.id === activeTab;\n const isDisabled = Boolean(tab.disabled);\n const tabClassNames = getTabClassNames(isDisabled, isActive);\n\n return (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n aria-selected={isActive}\n aria-disabled={isDisabled}\n tabIndex={isActive ? 0 : -1}\n className={`\n relative flex flex-row justify-center items-center gap-2 rounded transition-colors isolate\n ${sizeClasses.tab}\n ${tabWidthClass}\n ${tabClassNames}\n ${!isDisabled && !isActive ? 'hover:bg-background-50' : ''}\n focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2\n `}\n onClick={() => handleTabClick(tab.id)}\n onKeyDown={(e) => handleKeyDown(e, tab.id)}\n disabled={isDisabled}\n data-testid={`tab-${tab.id}`}\n >\n <span className=\"font-bold leading-4 tracking-[0.2px] truncate\">\n {responsive && tab.mobileLabel ? (\n <>\n <span className=\"sm:hidden\">{tab.mobileLabel}</span>\n <span className=\"hidden sm:inline\">{tab.label}</span>\n </>\n ) : (\n tab.label\n )}\n </span>\n {isActive && (\n <div\n className={`absolute bottom-0 left-2 right-2 bg-primary-700 rounded-lg z-[2] ${sizeClasses.indicator}`}\n data-testid=\"active-indicator\"\n />\n )}\n </button>\n );\n })}\n </div>\n );\n }\n);\n\nTab.displayName = 'Tab';\n\nexport default Tab;\n"],"mappings":"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","names":[]}
@@ -0,0 +1,161 @@
1
+ // src/components/Tab/Tab.tsx
2
+ import { forwardRef } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ var TAB_SIZE_CLASSES = {
5
+ small: {
6
+ container: "h-10 gap-1",
7
+ tab: "px-3 py-2 text-sm",
8
+ indicator: "h-0.5"
9
+ },
10
+ medium: {
11
+ container: "h-12 gap-2",
12
+ tab: "px-4 py-4 text-sm",
13
+ indicator: "h-1"
14
+ },
15
+ large: {
16
+ container: "h-14 gap-2",
17
+ tab: "px-6 py-4 text-base",
18
+ indicator: "h-1"
19
+ }
20
+ };
21
+ var RESPONSIVE_WIDTH_CLASSES = {
22
+ twoTabs: "w-[115px] sm:w-[204px]",
23
+ threeTabs: "w-[100px] sm:w-[160px]",
24
+ fourTabs: "w-[80px] sm:w-[140px]",
25
+ fiveTabs: "w-[70px] sm:w-[120px]",
26
+ default: "flex-1"
27
+ };
28
+ var Tab = forwardRef(
29
+ ({
30
+ tabs,
31
+ activeTab,
32
+ onTabChange,
33
+ size = "medium",
34
+ responsive = true,
35
+ className = "",
36
+ ...props
37
+ }, ref) => {
38
+ const sizeClasses = TAB_SIZE_CLASSES[size];
39
+ const getResponsiveWidthClass = (tabCount) => {
40
+ if (!responsive) return RESPONSIVE_WIDTH_CLASSES.default;
41
+ switch (tabCount) {
42
+ case 2:
43
+ return RESPONSIVE_WIDTH_CLASSES.twoTabs;
44
+ case 3:
45
+ return RESPONSIVE_WIDTH_CLASSES.threeTabs;
46
+ case 4:
47
+ return RESPONSIVE_WIDTH_CLASSES.fourTabs;
48
+ case 5:
49
+ return RESPONSIVE_WIDTH_CLASSES.fiveTabs;
50
+ default:
51
+ return RESPONSIVE_WIDTH_CLASSES.default;
52
+ }
53
+ };
54
+ const handleTabClick = (tabId) => {
55
+ const tab = tabs.find((t) => t.id === tabId);
56
+ if (tab && !tab.disabled) {
57
+ onTabChange(tabId);
58
+ }
59
+ };
60
+ const wrapAroundIndex = (index, maxLength) => {
61
+ if (index < 0) return maxLength - 1;
62
+ if (index >= maxLength) return 0;
63
+ return index;
64
+ };
65
+ const findNextValidTab = (startIndex, direction) => {
66
+ let nextIndex = wrapAroundIndex(startIndex + direction, tabs.length);
67
+ let attempts = 0;
68
+ while (tabs[nextIndex]?.disabled && attempts < tabs.length) {
69
+ nextIndex = wrapAroundIndex(nextIndex + direction, tabs.length);
70
+ attempts++;
71
+ }
72
+ return nextIndex;
73
+ };
74
+ const handleArrowNavigation = (direction) => {
75
+ const currentIndex = tabs.findIndex((tab) => tab.id === activeTab);
76
+ const nextIndex = findNextValidTab(currentIndex, direction);
77
+ if (!tabs[nextIndex]?.disabled && nextIndex !== currentIndex) {
78
+ handleTabClick(tabs[nextIndex].id);
79
+ }
80
+ };
81
+ const handleKeyDown = (event, tabId) => {
82
+ if (event.key === "Enter" || event.key === " ") {
83
+ event.preventDefault();
84
+ handleTabClick(tabId);
85
+ return;
86
+ }
87
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
88
+ event.preventDefault();
89
+ const direction = event.key === "ArrowLeft" ? -1 : 1;
90
+ handleArrowNavigation(direction);
91
+ }
92
+ };
93
+ const getTabClassNames = (isDisabled, isActive) => {
94
+ if (isDisabled) {
95
+ return "text-text-400 cursor-not-allowed opacity-50";
96
+ }
97
+ if (isActive) {
98
+ return "text-text-950";
99
+ }
100
+ return "text-text-700 hover:text-text-800";
101
+ };
102
+ const tabWidthClass = getResponsiveWidthClass(tabs.length);
103
+ const containerWidth = responsive && tabs.length <= 2 ? "w-[240px] sm:w-[416px]" : "w-full";
104
+ return /* @__PURE__ */ jsx(
105
+ "div",
106
+ {
107
+ ref,
108
+ className: `flex flex-row items-start ${sizeClasses.container} ${containerWidth} ${className}`,
109
+ role: "tablist",
110
+ ...props,
111
+ children: tabs.map((tab) => {
112
+ const isActive = tab.id === activeTab;
113
+ const isDisabled = Boolean(tab.disabled);
114
+ const tabClassNames = getTabClassNames(isDisabled, isActive);
115
+ return /* @__PURE__ */ jsxs(
116
+ "button",
117
+ {
118
+ type: "button",
119
+ role: "tab",
120
+ "aria-selected": isActive,
121
+ "aria-disabled": isDisabled,
122
+ tabIndex: isActive ? 0 : -1,
123
+ className: `
124
+ relative flex flex-row justify-center items-center gap-2 rounded transition-colors isolate
125
+ ${sizeClasses.tab}
126
+ ${tabWidthClass}
127
+ ${tabClassNames}
128
+ ${!isDisabled && !isActive ? "hover:bg-background-50" : ""}
129
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2
130
+ `,
131
+ onClick: () => handleTabClick(tab.id),
132
+ onKeyDown: (e) => handleKeyDown(e, tab.id),
133
+ disabled: isDisabled,
134
+ "data-testid": `tab-${tab.id}`,
135
+ children: [
136
+ /* @__PURE__ */ jsx("span", { className: "font-bold leading-4 tracking-[0.2px] truncate", children: responsive && tab.mobileLabel ? /* @__PURE__ */ jsxs(Fragment, { children: [
137
+ /* @__PURE__ */ jsx("span", { className: "sm:hidden", children: tab.mobileLabel }),
138
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: tab.label })
139
+ ] }) : tab.label }),
140
+ isActive && /* @__PURE__ */ jsx(
141
+ "div",
142
+ {
143
+ className: `absolute bottom-0 left-2 right-2 bg-primary-700 rounded-lg z-[2] ${sizeClasses.indicator}`,
144
+ "data-testid": "active-indicator"
145
+ }
146
+ )
147
+ ]
148
+ },
149
+ tab.id
150
+ );
151
+ })
152
+ }
153
+ );
154
+ }
155
+ );
156
+ Tab.displayName = "Tab";
157
+ var Tab_default = Tab;
158
+ export {
159
+ Tab_default as default
160
+ };
161
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/Tab/Tab.tsx"],"sourcesContent":["import { forwardRef, HTMLAttributes, KeyboardEvent } from 'react';\n\n/**\n * Individual tab item interface\n */\nexport interface TabItem {\n /** Unique identifier for the tab */\n id: string;\n /** Label text for the tab */\n label: string;\n /** Alternative label for mobile (optional) */\n mobileLabel?: string;\n /** Whether the tab is disabled */\n disabled?: boolean;\n}\n\n/**\n * Tab component props interface\n */\nexport interface TabProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {\n /** Array of tab items */\n tabs: TabItem[];\n /** Currently active tab ID */\n activeTab: string;\n /** Callback when tab changes */\n onTabChange: (tabId: string) => void;\n /** Size variant of the tabs */\n size?: 'small' | 'medium' | 'large';\n /** Whether to enable responsive behavior */\n responsive?: boolean;\n}\n\n/**\n * Size configuration lookup table\n */\nconst TAB_SIZE_CLASSES = {\n small: {\n container: 'h-10 gap-1',\n tab: 'px-3 py-2 text-sm',\n indicator: 'h-0.5',\n },\n medium: {\n container: 'h-12 gap-2',\n tab: 'px-4 py-4 text-sm',\n indicator: 'h-1',\n },\n large: {\n container: 'h-14 gap-2',\n tab: 'px-6 py-4 text-base',\n indicator: 'h-1',\n },\n} as const;\n\n/**\n * Responsive width classes for tabs\n */\nconst RESPONSIVE_WIDTH_CLASSES = {\n twoTabs: 'w-[115px] sm:w-[204px]',\n threeTabs: 'w-[100px] sm:w-[160px]',\n fourTabs: 'w-[80px] sm:w-[140px]',\n fiveTabs: 'w-[70px] sm:w-[120px]',\n default: 'flex-1',\n} as const;\n\n/**\n * Tab component following the established architecture patterns\n */\nconst Tab = forwardRef<HTMLDivElement, TabProps>(\n (\n {\n tabs,\n activeTab,\n onTabChange,\n size = 'medium',\n responsive = true,\n className = '',\n ...props\n },\n ref\n ) => {\n const sizeClasses = TAB_SIZE_CLASSES[size];\n\n /**\n * Get responsive width class based on number of tabs\n */\n const getResponsiveWidthClass = (tabCount: number): string => {\n if (!responsive) return RESPONSIVE_WIDTH_CLASSES.default;\n\n switch (tabCount) {\n case 2:\n return RESPONSIVE_WIDTH_CLASSES.twoTabs;\n case 3:\n return RESPONSIVE_WIDTH_CLASSES.threeTabs;\n case 4:\n return RESPONSIVE_WIDTH_CLASSES.fourTabs;\n case 5:\n return RESPONSIVE_WIDTH_CLASSES.fiveTabs;\n default:\n return RESPONSIVE_WIDTH_CLASSES.default;\n }\n };\n\n /**\n * Handle tab click\n */\n const handleTabClick = (tabId: string) => {\n const tab = tabs.find((t) => t.id === tabId);\n if (tab && !tab.disabled) {\n onTabChange(tabId);\n }\n };\n\n /**\n * Wrap index around array bounds\n */\n const wrapAroundIndex = (index: number, maxLength: number): number => {\n if (index < 0) return maxLength - 1;\n if (index >= maxLength) return 0;\n return index;\n };\n\n /**\n * Find next valid (non-disabled) tab index\n */\n const findNextValidTab = (\n startIndex: number,\n direction: number\n ): number => {\n let nextIndex = wrapAroundIndex(startIndex + direction, tabs.length);\n let attempts = 0;\n\n while (tabs[nextIndex]?.disabled && attempts < tabs.length) {\n nextIndex = wrapAroundIndex(nextIndex + direction, tabs.length);\n attempts++;\n }\n\n return nextIndex;\n };\n\n /**\n * Handle arrow key navigation\n */\n const handleArrowNavigation = (direction: number): void => {\n const currentIndex = tabs.findIndex((tab) => tab.id === activeTab);\n const nextIndex = findNextValidTab(currentIndex, direction);\n\n if (!tabs[nextIndex]?.disabled && nextIndex !== currentIndex) {\n handleTabClick(tabs[nextIndex].id);\n }\n };\n\n /**\n * Handle keyboard navigation\n */\n const handleKeyDown = (\n event: KeyboardEvent<HTMLButtonElement>,\n tabId: string\n ) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n handleTabClick(tabId);\n return;\n }\n\n if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {\n event.preventDefault();\n const direction = event.key === 'ArrowLeft' ? -1 : 1;\n handleArrowNavigation(direction);\n }\n };\n\n /**\n * Get tab text and interaction classes based on state\n */\n const getTabClassNames = (\n isDisabled: boolean,\n isActive: boolean\n ): string => {\n if (isDisabled) {\n return 'text-text-400 cursor-not-allowed opacity-50';\n }\n\n if (isActive) {\n return 'text-text-950';\n }\n\n return 'text-text-700 hover:text-text-800';\n };\n\n const tabWidthClass = getResponsiveWidthClass(tabs.length);\n const containerWidth =\n responsive && tabs.length <= 2 ? 'w-[240px] sm:w-[416px]' : 'w-full';\n\n return (\n <div\n ref={ref}\n className={`flex flex-row items-start ${sizeClasses.container} ${containerWidth} ${className}`}\n role=\"tablist\"\n {...props}\n >\n {tabs.map((tab) => {\n const isActive = tab.id === activeTab;\n const isDisabled = Boolean(tab.disabled);\n const tabClassNames = getTabClassNames(isDisabled, isActive);\n\n return (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n aria-selected={isActive}\n aria-disabled={isDisabled}\n tabIndex={isActive ? 0 : -1}\n className={`\n relative flex flex-row justify-center items-center gap-2 rounded transition-colors isolate\n ${sizeClasses.tab}\n ${tabWidthClass}\n ${tabClassNames}\n ${!isDisabled && !isActive ? 'hover:bg-background-50' : ''}\n focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2\n `}\n onClick={() => handleTabClick(tab.id)}\n onKeyDown={(e) => handleKeyDown(e, tab.id)}\n disabled={isDisabled}\n data-testid={`tab-${tab.id}`}\n >\n <span className=\"font-bold leading-4 tracking-[0.2px] truncate\">\n {responsive && tab.mobileLabel ? (\n <>\n <span className=\"sm:hidden\">{tab.mobileLabel}</span>\n <span className=\"hidden sm:inline\">{tab.label}</span>\n </>\n ) : (\n tab.label\n )}\n </span>\n {isActive && (\n <div\n className={`absolute bottom-0 left-2 right-2 bg-primary-700 rounded-lg z-[2] ${sizeClasses.indicator}`}\n data-testid=\"active-indicator\"\n />\n )}\n </button>\n );\n })}\n </div>\n );\n }\n);\n\nTab.displayName = 'Tab';\n\nexport default Tab;\n"],"mappings":"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","names":[]}