flysoft-react-ui 0.1.8 → 0.1.10
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 +152 -1
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +12 -2
- package/dist/components/form-controls/Button.d.ts +1 -1
- package/dist/components/form-controls/Button.d.ts.map +1 -1
- package/dist/components/form-controls/Button.js +4 -4
- package/dist/components/layout/AppLayout.d.ts +9 -0
- package/dist/components/layout/AppLayout.d.ts.map +1 -0
- package/dist/components/layout/AppLayout.js +89 -0
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/components/layout/index.js +1 -0
- package/dist/contexts/ThemeContext.d.ts.map +1 -1
- package/dist/contexts/ThemeContext.js +7 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/useBreakpoint.d.ts +14 -0
- package/dist/hooks/useBreakpoint.d.ts.map +1 -0
- package/dist/hooks/useBreakpoint.js +59 -0
- package/dist/hooks/useElementScroll.d.ts +5 -0
- package/dist/hooks/useElementScroll.d.ts.map +1 -0
- package/dist/hooks/useElementScroll.js +28 -0
- package/dist/hooks/useGlobalThemeStyles.d.ts +6 -0
- package/dist/hooks/useGlobalThemeStyles.d.ts.map +1 -0
- package/dist/hooks/useGlobalThemeStyles.js +40 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/templates/forms/ContactForm.d.ts +27 -0
- package/dist/templates/forms/ContactForm.d.ts.map +1 -0
- package/dist/templates/forms/ContactForm.js +58 -0
- package/dist/templates/forms/LoginForm.d.ts +24 -0
- package/dist/templates/forms/LoginForm.d.ts.map +1 -0
- package/dist/templates/forms/LoginForm.js +36 -0
- package/dist/templates/forms/RegistrationForm.d.ts +27 -0
- package/dist/templates/forms/RegistrationForm.d.ts.map +1 -0
- package/dist/templates/forms/RegistrationForm.js +54 -0
- package/dist/templates/layouts/DashboardLayout.d.ts +38 -0
- package/dist/templates/layouts/DashboardLayout.d.ts.map +1 -0
- package/dist/templates/layouts/DashboardLayout.js +26 -0
- package/dist/templates/layouts/SidebarLayout.d.ts +48 -0
- package/dist/templates/layouts/SidebarLayout.d.ts.map +1 -0
- package/dist/templates/layouts/SidebarLayout.js +27 -0
- package/dist/templates/patterns/FormPattern.d.ts +54 -0
- package/dist/templates/patterns/FormPattern.d.ts.map +1 -0
- package/dist/templates/patterns/FormPattern.js +67 -0
- package/package.json +20 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Flysoft React UI
|
|
2
2
|
|
|
3
|
-
Una biblioteca de componentes React moderna y accesible construida con TypeScript, Tailwind CSS y FontAwesome.
|
|
3
|
+
Una biblioteca de componentes React moderna y accesible construida con TypeScript, Tailwind CSS y FontAwesome 5. Incluye formularios, layouts, temas y templates para desarrollo rápido.
|
|
4
4
|
|
|
5
5
|
## 🚀 Características
|
|
6
6
|
|
|
@@ -11,6 +11,8 @@ Una biblioteca de componentes React moderna y accesible construida con TypeScrip
|
|
|
11
11
|
- **Personalizable**: Fácil de personalizar con clases de Tailwind
|
|
12
12
|
- **Tree-shakable**: Solo importa los componentes que uses
|
|
13
13
|
- **🎨 Sistema de Temas**: Sistema completo de temas personalizables con Context API
|
|
14
|
+
- **📋 Templates Listos**: Formularios y layouts pre-construidos
|
|
15
|
+
- **🤖 Cursor AI Ready**: Optimizado para uso con Cursor AI
|
|
14
16
|
|
|
15
17
|
## 📦 Instalación
|
|
16
18
|
|
|
@@ -18,6 +20,94 @@ Una biblioteca de componentes React moderna y accesible construida con TypeScrip
|
|
|
18
20
|
npm install flysoft-react-ui
|
|
19
21
|
```
|
|
20
22
|
|
|
23
|
+
## ⚡ Quick Start
|
|
24
|
+
|
|
25
|
+
### 1. Configuración Básica
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { ThemeProvider } from "flysoft-react-ui";
|
|
29
|
+
import "flysoft-react-ui/styles";
|
|
30
|
+
|
|
31
|
+
function App() {
|
|
32
|
+
return (
|
|
33
|
+
<ThemeProvider initialTheme="light">
|
|
34
|
+
{/* Tu aplicación aquí */}
|
|
35
|
+
</ThemeProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Formulario de Login Rápido
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { LoginForm } from "flysoft-react-ui";
|
|
44
|
+
|
|
45
|
+
function LoginPage() {
|
|
46
|
+
const handleLogin = (data) => {
|
|
47
|
+
console.log("Login data:", data);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return <LoginForm onSubmit={handleLogin} />;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3. Dashboard Básico
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { DashboardLayout } from "flysoft-react-ui";
|
|
58
|
+
|
|
59
|
+
function Dashboard() {
|
|
60
|
+
const stats = [
|
|
61
|
+
{
|
|
62
|
+
title: "Usuarios",
|
|
63
|
+
value: "1,234",
|
|
64
|
+
change: "+12%",
|
|
65
|
+
changeType: "positive",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
title: "Ventas",
|
|
69
|
+
value: "$45,678",
|
|
70
|
+
change: "+8%",
|
|
71
|
+
changeType: "positive",
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<DashboardLayout title="Mi Dashboard" stats={stats}>
|
|
77
|
+
<div>Contenido del dashboard</div>
|
|
78
|
+
</DashboardLayout>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 4. Integración con Cursor AI
|
|
84
|
+
|
|
85
|
+
Para que Cursor AI priorice automáticamente estos componentes, crea un archivo `.cursorrules` en tu proyecto:
|
|
86
|
+
|
|
87
|
+
```markdown
|
|
88
|
+
# Priorizar flysoft-react-ui
|
|
89
|
+
|
|
90
|
+
SIEMPRE usa los componentes de flysoft-react-ui antes de crear nuevos:
|
|
91
|
+
|
|
92
|
+
## Componentes Disponibles:
|
|
93
|
+
|
|
94
|
+
- Button, Input, Card, Badge, ThemeSwitcher
|
|
95
|
+
- LoginForm, RegistrationForm, ContactForm
|
|
96
|
+
- DashboardLayout, SidebarLayout, FormPattern
|
|
97
|
+
|
|
98
|
+
## Para formularios:
|
|
99
|
+
|
|
100
|
+
- SIEMPRE usar Input y Button de flysoft-react-ui
|
|
101
|
+
- SIEMPRE usar Card como contenedor
|
|
102
|
+
- SIEMPRE usar FontAwesome 5 para iconos (fa fa-\*)
|
|
103
|
+
|
|
104
|
+
## Importación requerida:
|
|
105
|
+
|
|
106
|
+
import "flysoft-react-ui/styles";
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Ver [INTEGRATION_GUIDE.md](./INTEGRATION_GUIDE.md) para configuración completa.
|
|
110
|
+
|
|
21
111
|
## 🔧 Configuración
|
|
22
112
|
|
|
23
113
|
### Tailwind CSS
|
|
@@ -155,6 +245,46 @@ function App() {
|
|
|
155
245
|
}
|
|
156
246
|
```
|
|
157
247
|
|
|
248
|
+
## 📋 Templates Disponibles
|
|
249
|
+
|
|
250
|
+
### Formularios
|
|
251
|
+
|
|
252
|
+
- **LoginForm**: Formulario de login completo con validación
|
|
253
|
+
- **RegistrationForm**: Formulario de registro con validación de contraseñas
|
|
254
|
+
- **ContactForm**: Formulario de contacto con textarea y validación
|
|
255
|
+
|
|
256
|
+
### Layouts
|
|
257
|
+
|
|
258
|
+
- **DashboardLayout**: Layout de dashboard con estadísticas y métricas
|
|
259
|
+
- **SidebarLayout**: Layout con sidebar de navegación y contenido principal
|
|
260
|
+
|
|
261
|
+
### Patrones
|
|
262
|
+
|
|
263
|
+
- **FormPattern**: Patrón reutilizable para cualquier formulario
|
|
264
|
+
|
|
265
|
+
### Ejemplo de Uso de Templates
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
import { LoginForm, DashboardLayout, SidebarLayout } from "flysoft-react-ui";
|
|
269
|
+
|
|
270
|
+
// Formulario de login
|
|
271
|
+
<LoginForm onSubmit={handleLogin} loading={isLoading} />
|
|
272
|
+
|
|
273
|
+
// Dashboard con estadísticas
|
|
274
|
+
<DashboardLayout title="Mi App" stats={stats}>
|
|
275
|
+
<div>Contenido del dashboard</div>
|
|
276
|
+
</DashboardLayout>
|
|
277
|
+
|
|
278
|
+
// Layout con sidebar
|
|
279
|
+
<SidebarLayout
|
|
280
|
+
title="Mi App"
|
|
281
|
+
menuItems={menuItems}
|
|
282
|
+
user={user}
|
|
283
|
+
>
|
|
284
|
+
<div>Contenido principal</div>
|
|
285
|
+
</SidebarLayout>
|
|
286
|
+
```
|
|
287
|
+
|
|
158
288
|
## 🧩 Componentes
|
|
159
289
|
|
|
160
290
|
### Button
|
|
@@ -305,6 +435,27 @@ MIT
|
|
|
305
435
|
|
|
306
436
|
Las contribuciones son bienvenidas. Por favor, abre un issue o un pull request.
|
|
307
437
|
|
|
438
|
+
## 📚 Recursos Adicionales
|
|
439
|
+
|
|
440
|
+
- **[INTEGRATION_GUIDE.md](./INTEGRATION_GUIDE.md)**: Guía completa de integración con Cursor AI
|
|
441
|
+
- **[THEME_SYSTEM.md](./THEME_SYSTEM.md)**: Documentación detallada del sistema de temas
|
|
442
|
+
- **[examples/common-patterns.tsx](./examples/common-patterns.tsx)**: Ejemplos completos de uso
|
|
443
|
+
- **[flysoft-ui.config.ts](./flysoft-ui.config.ts)**: Configuración centralizada de la librería
|
|
444
|
+
- **[docs/component-metadata.json](./docs/component-metadata.json)**: Metadatos de todos los componentes
|
|
445
|
+
|
|
446
|
+
## 🔧 Scripts de Mantenimiento
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
# Actualizar documentación automáticamente
|
|
450
|
+
npm run update-docs
|
|
451
|
+
|
|
452
|
+
# Validar que toda la documentación esté sincronizada
|
|
453
|
+
npm run validate-docs
|
|
454
|
+
|
|
455
|
+
# Ver ejemplos completos
|
|
456
|
+
npm run dev
|
|
457
|
+
```
|
|
458
|
+
|
|
308
459
|
## 📞 Soporte
|
|
309
460
|
|
|
310
461
|
Si tienes alguna pregunta o necesitas ayuda, abre un issue en el repositorio.
|
package/dist/App.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAWA,OAAO,aAAa,CAAC;AA6CrB,iBAAS,GAAG,4CA+MX;AAED,eAAe,GAAG,CAAC"}
|
package/dist/App.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { AppLayout } from "./components";
|
|
3
|
+
import { Button, Input, Card, Badge, ThemeProvider, ThemeSwitcher, useTheme, useGlobalThemeStyles, } from "./index";
|
|
3
4
|
import "./index.css";
|
|
4
5
|
// Componente para probar la funcionalidad de resetToDefault
|
|
5
6
|
const ResetToDefaultButton = () => {
|
|
6
7
|
const { resetToDefault, theme } = useTheme();
|
|
7
8
|
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("p", { className: "text-sm", style: { color: "var(--flysoft-text-secondary)" }, children: ["Tema actual: ", _jsx("strong", { children: theme.name })] }), _jsx(Button, { onClick: resetToDefault, variant: "outline", icon: "fa-undo", children: "Resetear al Tema Inicial" })] }));
|
|
8
9
|
};
|
|
10
|
+
// Componente que demuestra el uso de useGlobalThemeStyles
|
|
11
|
+
const GlobalThemeDemo = () => {
|
|
12
|
+
// Este hook aplica automáticamente los estilos del tema al body/html
|
|
13
|
+
useGlobalThemeStyles();
|
|
14
|
+
return (_jsxs("div", { className: "p-4 rounded-lg border", style: {
|
|
15
|
+
borderColor: "var(--flysoft-border-default)",
|
|
16
|
+
backgroundColor: "var(--flysoft-bg-secondary)",
|
|
17
|
+
}, children: [_jsx("h3", { className: "text-lg font-semibold mb-2", style: { color: "var(--flysoft-text-primary)" }, children: "Hook useGlobalThemeStyles" }), _jsx("p", { className: "text-sm", style: { color: "var(--flysoft-text-secondary)" }, children: "Este componente usa el hook useGlobalThemeStyles que aplica autom\u00E1ticamente los estilos del tema actual al body y html." })] }));
|
|
18
|
+
};
|
|
9
19
|
function App() {
|
|
10
|
-
return (_jsx(ThemeProvider, { initialTheme: "light", forceInitialTheme: false, children: _jsx("div", {
|
|
20
|
+
return (_jsx(ThemeProvider, { initialTheme: "light", forceInitialTheme: false, children: _jsx(AppLayout, { navBarDrawer: _jsx("div", { children: _jsx("h2", { children: "Flysoft React UI" }) }), leftDrawer: _jsx("div", { children: "Left Drawer" }), children: _jsx("div", { children: _jsxs("div", { className: "max-w-4xl mx-auto", children: [_jsx("div", { className: "text-center mb-12", children: _jsx("p", { className: "text-xl", style: { color: "var(--flysoft-text-secondary)" }, children: "Biblioteca de componentes React moderna con Tailwind CSS, FontAwesome y sistema de temas personalizable" }) }), _jsx("div", { className: "mb-8", children: _jsx(ThemeSwitcher, {}) }), _jsx("div", { className: "mb-8 text-center", children: _jsx(ResetToDefaultButton, {}) }), _jsx("div", { className: "mb-8", children: _jsx(GlobalThemeDemo, {}) }), _jsx("div", { className: "mb-8", children: _jsx(Card, { title: "Form Controls", className: "mb-6", children: _jsxs("div", { className: "space-y-6", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-4", style: { color: "var(--flysoft-text-primary)" }, children: "Botones" }), _jsxs("div", { className: "flex flex-wrap gap-4", children: [_jsx(Button, { variant: "primary", icon: "fa-save", children: "Primary" }), _jsx(Button, { variant: "outline", icon: "fa-edit", children: "Outline" }), _jsx(Button, { variant: "ghost", icon: "fa-trash", children: "Ghost" }), _jsx(Button, { size: "sm", variant: "primary", children: "Small" }), _jsx(Button, { size: "lg", variant: "outline", children: "Large" })] })] }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-4", style: { color: "var(--flysoft-text-primary)" }, children: "Campos de Entrada" }), _jsxs("div", { className: "space-y-4", children: [_jsx(Input, { label: "Nombre", placeholder: "Ingresa tu nombre", icon: "fa-user" }), _jsx(Input, { label: "Email", type: "email", placeholder: "tu@email.com", icon: "fa-envelope" }), _jsx(Input, { label: "Contrase\u00F1a", type: "password", placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", icon: "fa-lock" }), _jsx(Input, { label: "Mensaje", placeholder: "Escribe tu mensaje aqu\u00ED...", icon: "fa-comment" })] })] })] }) }) }), _jsx("div", { className: "mb-8", children: _jsx(Card, { title: "Utility Components", children: _jsx("div", { className: "space-y-6", children: _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-4", style: { color: "var(--flysoft-text-primary)" }, children: "Badges" }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Badge, { variant: "primary", children: "Primary" }), _jsx(Badge, { variant: "secondary", children: "Secondary" }), _jsx(Badge, { variant: "success", children: "Success" }), _jsx(Badge, { variant: "warning", children: "Warning" }), _jsx(Badge, { variant: "danger", children: "Danger" }), _jsx(Badge, { variant: "info", children: "Info" })] })] }) }) }) }), _jsx("div", { className: "mb-8", children: _jsx(Card, { title: "Layout Components", children: _jsx("div", { className: "space-y-6", children: _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-4", style: { color: "var(--flysoft-text-primary)" }, children: "Cards con Diferentes Variantes" }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-6", children: [_jsx(Card, { variant: "default", title: "Card Default", children: _jsx("p", { style: { color: "var(--flysoft-text-secondary)" }, children: "Esta es una tarjeta con variante por defecto." }) }), _jsx(Card, { variant: "elevated", title: "Card Elevated", children: _jsx("p", { style: { color: "var(--flysoft-text-secondary)" }, children: "Esta es una tarjeta con sombra elevada." }) }), _jsx(Card, { variant: "outlined", title: "Card Outlined", children: _jsx("p", { style: { color: "var(--flysoft-text-secondary)" }, children: "Esta es una tarjeta con borde destacado." }) }), _jsx(Card, { title: "Card con Footer", footer: _jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { size: "sm", variant: "outline", children: "Cancelar" }), _jsx(Button, { size: "sm", variant: "primary", icon: "fa-save", children: "Guardar" })] }), children: _jsx("p", { style: { color: "var(--flysoft-text-secondary)" }, children: "Tarjeta con footer personalizado y botones." }) })] })] }) }) }) }), _jsxs("div", { className: "mt-8 space-y-8", children: [_jsxs("div", { className: "h-96 bg-blue-100 rounded p-4", children: [_jsx("h3", { className: "text-lg font-bold mb-4", children: "Secci\u00F3n 1" }), _jsx("p", { children: "Contenido adicional para hacer scroll..." })] }), _jsxs("div", { className: "h-96 bg-green-100 rounded p-4", children: [_jsx("h3", { className: "text-lg font-bold mb-4", children: "Secci\u00F3n 2" }), _jsx("p", { children: "M\u00E1s contenido para probar el comportamiento del navbar..." })] }), _jsxs("div", { className: "h-96 bg-yellow-100 rounded p-4", children: [_jsx("h3", { className: "text-lg font-bold mb-4", children: "Secci\u00F3n 3" }), _jsx("p", { children: "Contenido final para asegurar que hay suficiente altura..." })] }), _jsxs("div", { className: "h-96 bg-red-100 rounded p-4", children: [_jsx("h3", { className: "text-lg font-bold mb-4", children: "Secci\u00F3n 4" }), _jsx("p", { children: "\u00DAltima secci\u00F3n para completar la prueba de scroll..." })] })] })] }) }) }) }));
|
|
11
21
|
}
|
|
12
22
|
export default App;
|
|
@@ -5,7 +5,7 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
|
|
|
5
5
|
icon?: string;
|
|
6
6
|
iconPosition?: "left" | "right";
|
|
7
7
|
loading?: boolean;
|
|
8
|
-
children
|
|
8
|
+
children?: React.ReactNode;
|
|
9
9
|
}
|
|
10
10
|
export declare const Button: React.FC<ButtonProps>;
|
|
11
11
|
//# sourceMappingURL=Button.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../src/components/form-controls/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC;IACrD,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IAC1C,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../../src/components/form-controls/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,WAAW,WACf,SAAQ,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC;IACrD,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IAC1C,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAgExC,CAAC"}
|
|
@@ -22,16 +22,16 @@ export const Button = ({ variant = "primary", size = "md", icon, iconPosition =
|
|
|
22
22
|
`,
|
|
23
23
|
};
|
|
24
24
|
const sizeClasses = {
|
|
25
|
-
sm: "px-3 py-1.5 text-sm
|
|
26
|
-
md: "px-4 py-2 text-base
|
|
27
|
-
lg: "px-6 py-3 text-lg
|
|
25
|
+
sm: `${children ? "px-3 py-1.5" : "p-1.5"} text-sm`,
|
|
26
|
+
md: `${children ? "px-4 py-2" : "p-2"} text-base`,
|
|
27
|
+
lg: `${children ? "px-6 py-3" : "p-3"} text-lg`,
|
|
28
28
|
};
|
|
29
29
|
const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`;
|
|
30
30
|
const renderIcon = () => {
|
|
31
31
|
if (!icon)
|
|
32
32
|
return null;
|
|
33
33
|
const iconClasses = size === "sm" ? "w-4 h-4" : size === "md" ? "w-5 h-5" : "w-6 h-6";
|
|
34
|
-
return (_jsx("i", { className: `fa ${icon} ${iconClasses} ${iconPosition === "right" ? "ml-2" : "mr-2"}` }));
|
|
34
|
+
return (_jsx("i", { className: `fa ${icon} ${iconClasses} ${children ? (iconPosition === "right" ? "ml-2" : "mr-2") : ""}` }));
|
|
35
35
|
};
|
|
36
36
|
return (_jsxs("button", { className: classes, disabled: disabled || loading, ...props, children: [loading && _jsx("i", { className: "fa fa-spinner fa-spin mr-2" }), icon && iconPosition === "left" && !loading && renderIcon(), children, icon && iconPosition === "right" && !loading && renderIcon()] }));
|
|
37
37
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface AppLayoutProps {
|
|
3
|
+
navBarDrawer?: React.ReactNode;
|
|
4
|
+
leftDrawer?: React.ReactNode;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const AppLayout: React.FC<AppLayoutProps>;
|
|
9
|
+
//# sourceMappingURL=AppLayout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AppLayout.d.ts","sourceRoot":"","sources":["../../../src/components/layout/AppLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2B,MAAM,OAAO,CAAC;AAKhD,MAAM,WAAW,cAAc;IAC7B,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC/B,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC7B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAoK9C,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState, useRef } from "react";
|
|
3
|
+
import { useBreakpoint } from "../../hooks";
|
|
4
|
+
import { useElementScroll } from "../../hooks/useElementScroll";
|
|
5
|
+
import { Button } from "../form-controls";
|
|
6
|
+
export const AppLayout = ({ navBarDrawer, leftDrawer, children, className = "", }) => {
|
|
7
|
+
const { isMobile, isTablet } = useBreakpoint();
|
|
8
|
+
const contentRef = useRef(null);
|
|
9
|
+
const { scrollY, scrollDirection } = useElementScroll(contentRef);
|
|
10
|
+
const [isMobileDrawerOpen, setIsMobileDrawerOpen] = useState(false);
|
|
11
|
+
const [isNavbarVisible, setIsNavbarVisible] = useState(true);
|
|
12
|
+
const shouldShowMobileDrawer = isMobile || isTablet;
|
|
13
|
+
const shouldShowDesktopDrawer = !shouldShowMobileDrawer && leftDrawer;
|
|
14
|
+
// Controlar visibilidad del navbar basado en scroll
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
if (scrollY < 100) {
|
|
17
|
+
// Siempre mostrar navbar cerca del top
|
|
18
|
+
setIsNavbarVisible(true);
|
|
19
|
+
}
|
|
20
|
+
else if (scrollDirection === "down" && scrollY > 100) {
|
|
21
|
+
// Ocultar navbar al hacer scroll hacia abajo
|
|
22
|
+
setIsNavbarVisible(false);
|
|
23
|
+
}
|
|
24
|
+
else if (scrollDirection === "up" && scrollY > 100) {
|
|
25
|
+
// Mostrar navbar al hacer scroll hacia arriba
|
|
26
|
+
setIsNavbarVisible(true);
|
|
27
|
+
}
|
|
28
|
+
}, [scrollDirection, scrollY, isNavbarVisible]);
|
|
29
|
+
const handleMobileDrawerToggle = () => {
|
|
30
|
+
setIsMobileDrawerOpen(!isMobileDrawerOpen);
|
|
31
|
+
};
|
|
32
|
+
const handleOverlayClick = () => {
|
|
33
|
+
setIsMobileDrawerOpen(false);
|
|
34
|
+
};
|
|
35
|
+
// Clases base del layout
|
|
36
|
+
const layoutClasses = `
|
|
37
|
+
flex flex-col h-screen w-full
|
|
38
|
+
font-[var(--font-default)]
|
|
39
|
+
${className}
|
|
40
|
+
`;
|
|
41
|
+
// Clases del navbar
|
|
42
|
+
const navbarClasses = `
|
|
43
|
+
bg-[var(--color-bg-default)] border-b border-[var(--color-border-default)]
|
|
44
|
+
z-[1000] fixed top-0 left-0 right-0
|
|
45
|
+
transform transition-transform duration-300 ease-in-out
|
|
46
|
+
${isNavbarVisible ? "translate-y-0" : "-translate-y-full"}
|
|
47
|
+
`;
|
|
48
|
+
// Estilos inline para debug
|
|
49
|
+
const navbarStyle = {
|
|
50
|
+
transform: isNavbarVisible ? "translateY(0)" : "translateY(-100%)",
|
|
51
|
+
transition: "transform 300ms ease-in-out",
|
|
52
|
+
};
|
|
53
|
+
const navbarContentClasses = `
|
|
54
|
+
flex items-center pr-4 lg:px-4 h-16 gap-2
|
|
55
|
+
md:px-3
|
|
56
|
+
`;
|
|
57
|
+
const navbarDrawerClasses = `flex-1`;
|
|
58
|
+
// Clases del contenido principal
|
|
59
|
+
const mainClasses = `
|
|
60
|
+
flex flex-1 overflow-hidden
|
|
61
|
+
transition-all duration-300 ease-in-out
|
|
62
|
+
${navBarDrawer && isNavbarVisible ? "pt-16" : ""}
|
|
63
|
+
`;
|
|
64
|
+
const leftDrawerClasses = `
|
|
65
|
+
w-64 bg-[var(--color-bg-default)]
|
|
66
|
+
overflow-y-auto flex-shrink-0 p-4
|
|
67
|
+
transition-all duration-300 ease-in-out
|
|
68
|
+
${navBarDrawer && isNavbarVisible ? "pt-20" : "pt-4"}
|
|
69
|
+
`;
|
|
70
|
+
const contentClasses = `
|
|
71
|
+
flex-1 overflow-y-auto px-2 py-4 lg:px-6
|
|
72
|
+
`;
|
|
73
|
+
// Clases del overlay móvil
|
|
74
|
+
const overlayClasses = `
|
|
75
|
+
fixed inset-0 bg-black/50 backdrop-blur-sm z-[1998]
|
|
76
|
+
`;
|
|
77
|
+
// Clases del drawer móvil
|
|
78
|
+
const mobileDrawerBaseClasses = `
|
|
79
|
+
fixed top-0 left-0 h-screen w-64 max-w-[80vw]
|
|
80
|
+
bg-[var(--color-bg-default)] shadow-[var(--shadow-xl)]
|
|
81
|
+
transform -translate-x-full transition-transform duration-300 ease-in-out
|
|
82
|
+
z-[1999] flex flex-col
|
|
83
|
+
`;
|
|
84
|
+
const mobileDrawerOpenClasses = `translate-x-0`;
|
|
85
|
+
const mobileDrawerContentClasses = `
|
|
86
|
+
flex-1 overflow-y-auto p-4
|
|
87
|
+
`;
|
|
88
|
+
return (_jsxs("div", { className: layoutClasses, children: [navBarDrawer && (_jsx("nav", { className: navbarClasses, style: navbarStyle, children: _jsxs("div", { className: navbarContentClasses, children: [shouldShowMobileDrawer && leftDrawer && (_jsx(Button, { variant: "ghost", icon: "fa-bars", onClick: handleMobileDrawerToggle, "aria-label": "Abrir men\u00FA" })), _jsx("div", { className: navbarDrawerClasses, children: navBarDrawer })] }) })), _jsxs("div", { className: mainClasses, children: [shouldShowDesktopDrawer && (_jsx("aside", { className: leftDrawerClasses, children: leftDrawer })), _jsx("main", { ref: contentRef, className: contentClasses, children: children })] }), shouldShowMobileDrawer && leftDrawer && isMobileDrawerOpen && (_jsx("div", { className: overlayClasses, onClick: handleOverlayClick })), shouldShowMobileDrawer && leftDrawer && (_jsxs("aside", { className: `${mobileDrawerBaseClasses} ${isMobileDrawerOpen ? mobileDrawerOpenClasses : ""}`, children: [_jsx("div", { className: "text-right", children: _jsx(Button, { variant: "ghost", icon: "fa-times", onClick: handleMobileDrawerToggle, "aria-label": "Cerrar men\u00FA" }) }), _jsx("div", { className: mobileDrawerContentClasses, children: leftDrawer })] }))] }));
|
|
89
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/layout/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,YAAY,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/layout/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,YAAY,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeContext.d.ts","sourceRoot":"","sources":["../../src/contexts/ThemeContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAKZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAKvD,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,SAAS,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,
|
|
1
|
+
{"version":3,"file":"ThemeContext.d.ts","sourceRoot":"","sources":["../../src/contexts/ThemeContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAKZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAKvD,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,SAAS,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAmJtD,CAAC;AAGF,eAAO,MAAM,QAAQ,QAAO,gBAM3B,CAAC;AAGF,eAAO,MAAM,eAAe,eAG3B,CAAC"}
|
|
@@ -72,6 +72,13 @@ export const ThemeProvider = ({ children, initialTheme = "light", storageKey = "
|
|
|
72
72
|
});
|
|
73
73
|
// Set theme name as data attribute for CSS targeting
|
|
74
74
|
root.setAttribute("data-theme", theme.name);
|
|
75
|
+
// Apply background and text colors to body for better integration
|
|
76
|
+
const body = document.body;
|
|
77
|
+
if (body) {
|
|
78
|
+
body.style.backgroundColor = theme.colors.bgDefault;
|
|
79
|
+
body.style.color = theme.colors.textPrimary;
|
|
80
|
+
body.style.fontFamily = theme.fonts.default;
|
|
81
|
+
}
|
|
75
82
|
};
|
|
76
83
|
// Function to set theme
|
|
77
84
|
const setTheme = (theme) => {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { useThemeOverride, useTemporaryOverride } from "./useThemeOverride";
|
|
2
|
+
export { useGlobalThemeStyles } from "./useGlobalThemeStyles";
|
|
3
|
+
export { useBreakpoint } from "./useBreakpoint";
|
|
4
|
+
export type { Breakpoint, WindowSize, BreakpointInfo } from "./useBreakpoint";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
|
2
|
+
export interface WindowSize {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
}
|
|
6
|
+
export interface BreakpointInfo {
|
|
7
|
+
breakpoint: Breakpoint;
|
|
8
|
+
windowSize: WindowSize;
|
|
9
|
+
isMobile: boolean;
|
|
10
|
+
isTablet: boolean;
|
|
11
|
+
isDesktop: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const useBreakpoint: () => BreakpointInfo;
|
|
14
|
+
//# sourceMappingURL=useBreakpoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBreakpoint.d.ts","sourceRoot":"","sources":["../../src/hooks/useBreakpoint.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;CACpB;AAYD,eAAO,MAAM,aAAa,QAAO,cAqDhC,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
// Breakpoints basados en Tailwind CSS
|
|
3
|
+
const breakpoints = {
|
|
4
|
+
xs: 0,
|
|
5
|
+
sm: 640,
|
|
6
|
+
md: 768,
|
|
7
|
+
lg: 1024,
|
|
8
|
+
xl: 1280,
|
|
9
|
+
"2xl": 1536,
|
|
10
|
+
};
|
|
11
|
+
export const useBreakpoint = () => {
|
|
12
|
+
const [windowSize, setWindowSize] = useState({
|
|
13
|
+
width: typeof window !== "undefined" ? window.innerWidth : 1024,
|
|
14
|
+
height: typeof window !== "undefined" ? window.innerHeight : 768,
|
|
15
|
+
});
|
|
16
|
+
const [breakpoint, setBreakpoint] = useState("lg");
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const handleResize = () => {
|
|
19
|
+
const width = window.innerWidth;
|
|
20
|
+
const height = window.innerHeight;
|
|
21
|
+
setWindowSize({ width, height });
|
|
22
|
+
// Determinar el breakpoint actual
|
|
23
|
+
if (width >= breakpoints["2xl"]) {
|
|
24
|
+
setBreakpoint("2xl");
|
|
25
|
+
}
|
|
26
|
+
else if (width >= breakpoints.xl) {
|
|
27
|
+
setBreakpoint("xl");
|
|
28
|
+
}
|
|
29
|
+
else if (width >= breakpoints.lg) {
|
|
30
|
+
setBreakpoint("lg");
|
|
31
|
+
}
|
|
32
|
+
else if (width >= breakpoints.md) {
|
|
33
|
+
setBreakpoint("md");
|
|
34
|
+
}
|
|
35
|
+
else if (width >= breakpoints.sm) {
|
|
36
|
+
setBreakpoint("sm");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
setBreakpoint("xs");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
// Ejecutar una vez al montar
|
|
43
|
+
handleResize();
|
|
44
|
+
// Agregar listener para cambios de tamaño
|
|
45
|
+
window.addEventListener("resize", handleResize);
|
|
46
|
+
// Cleanup
|
|
47
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
48
|
+
}, []);
|
|
49
|
+
const isMobile = breakpoint === "xs" || breakpoint === "sm";
|
|
50
|
+
const isTablet = breakpoint === "md";
|
|
51
|
+
const isDesktop = breakpoint === "lg" || breakpoint === "xl" || breakpoint === "2xl";
|
|
52
|
+
return {
|
|
53
|
+
breakpoint,
|
|
54
|
+
windowSize,
|
|
55
|
+
isMobile,
|
|
56
|
+
isTablet,
|
|
57
|
+
isDesktop,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useElementScroll.d.ts","sourceRoot":"","sources":["../../src/hooks/useElementScroll.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,gBAAgB,GAC3B,YAAY,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC;;;CAoChD,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
export const useElementScroll = (elementRef) => {
|
|
3
|
+
const [scrollY, setScrollY] = useState(0);
|
|
4
|
+
const [scrollDirection, setScrollDirection] = useState(null);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
if (!elementRef.current)
|
|
7
|
+
return;
|
|
8
|
+
const element = elementRef.current;
|
|
9
|
+
let lastScrollY = element.scrollTop;
|
|
10
|
+
const handleScroll = () => {
|
|
11
|
+
const currentScrollY = element.scrollTop;
|
|
12
|
+
setScrollY(currentScrollY);
|
|
13
|
+
if (currentScrollY > lastScrollY && currentScrollY > 10) {
|
|
14
|
+
setScrollDirection("down");
|
|
15
|
+
}
|
|
16
|
+
else if (currentScrollY < lastScrollY) {
|
|
17
|
+
setScrollDirection("up");
|
|
18
|
+
}
|
|
19
|
+
lastScrollY = currentScrollY;
|
|
20
|
+
};
|
|
21
|
+
// Agregar el listener al elemento
|
|
22
|
+
element.addEventListener("scroll", handleScroll, { passive: true });
|
|
23
|
+
return () => {
|
|
24
|
+
element.removeEventListener("scroll", handleScroll);
|
|
25
|
+
};
|
|
26
|
+
}, [elementRef]);
|
|
27
|
+
return { scrollY, scrollDirection };
|
|
28
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGlobalThemeStyles.d.ts","sourceRoot":"","sources":["../../src/hooks/useGlobalThemeStyles.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,eAAO,MAAM,oBAAoB,YAqChC,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { useTheme } from "../contexts/ThemeContext";
|
|
3
|
+
/**
|
|
4
|
+
* Hook que aplica estilos globales del tema al body y html
|
|
5
|
+
* Útil para aplicaciones host que quieren que el tema afecte toda la página
|
|
6
|
+
*/
|
|
7
|
+
export const useGlobalThemeStyles = () => {
|
|
8
|
+
const { theme } = useTheme();
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const body = document.body;
|
|
11
|
+
const html = document.documentElement;
|
|
12
|
+
if (body) {
|
|
13
|
+
// Aplicar estilos al body
|
|
14
|
+
body.style.backgroundColor = theme.colors.bgDefault;
|
|
15
|
+
body.style.color = theme.colors.textPrimary;
|
|
16
|
+
body.style.fontFamily = theme.fonts.default;
|
|
17
|
+
body.style.margin = "0";
|
|
18
|
+
body.style.padding = "0";
|
|
19
|
+
}
|
|
20
|
+
if (html) {
|
|
21
|
+
// Aplicar estilos al html
|
|
22
|
+
html.style.backgroundColor = theme.colors.bgDefault;
|
|
23
|
+
html.style.color = theme.colors.textPrimary;
|
|
24
|
+
}
|
|
25
|
+
// Cleanup function para restaurar estilos originales
|
|
26
|
+
return () => {
|
|
27
|
+
if (body) {
|
|
28
|
+
body.style.backgroundColor = "";
|
|
29
|
+
body.style.color = "";
|
|
30
|
+
body.style.fontFamily = "";
|
|
31
|
+
body.style.margin = "";
|
|
32
|
+
body.style.padding = "";
|
|
33
|
+
}
|
|
34
|
+
if (html) {
|
|
35
|
+
html.style.backgroundColor = "";
|
|
36
|
+
html.style.color = "";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}, [theme]);
|
|
40
|
+
};
|