athlefi-ui 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "athlefi-ui",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Librería UI centralizada del ecosistema AthleFi basada en Componentes Astro",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -1,120 +1,41 @@
1
1
  ---
2
- /**
3
- * Copyright (c) 2025 Grupo DedSec S.A. de C.V.
4
- * Este código fuente es propiedad exclusiva de Grupo DedSec S.A. de C.V.
5
- * Se prohíbe su uso, reproducción, distribución o modificación, total o parcial, sin autorización expresa por escrito.
6
- * No se permite el uso comercial ni la redistribución pública del mismo.
7
- * Para licenciamiento, contactar a: admin@dedsec.com.mx
8
- */
2
+ import NavLink from "../atoms/NavLink.astro";
9
3
 
10
- /**
11
- * Átomo: TimelineStep
12
- * @description Paso individual de una línea de tiempo con ícono, título y descripción
13
- */
14
-
15
- export interface Props {
16
- icon: string;
17
- title: string;
18
- description: string;
19
- stepNumber: number;
20
- isVertical?: boolean;
21
- animationDelay?: number;
4
+ export interface Props {
22
5
  class?: string;
6
+ isMobile?: boolean;
23
7
  }
24
8
 
25
- const {
26
- icon,
27
- title,
28
- description,
29
- stepNumber,
30
- isVertical = false,
31
- animationDelay = 0,
32
- class: className = ""
33
- } = Astro.props;
34
-
35
- // Importar los iconos SVG desde assets
36
- import shieldIcon from '@assets/shield-checkmark-outline.svg?raw';
37
- import searchIcon from '@assets/search-outline.svg?raw';
38
- import cardIcon from '@assets/card-outline.svg?raw';
39
- import chartIcon from '@assets/bar-chart-outline.svg?raw';
40
- import trophyIcon from '@assets/trophy-outline.svg?raw';
41
- import userIcon from '@assets/person-outline.svg?raw';
42
- import documentIcon from '@assets/document-text-outline.svg?raw';
43
- import laptopIcon from '@assets/laptop-outline.svg?raw';
44
- import cashIcon from '@assets/cash-outline.svg?raw';
45
- import pieIcon from '@assets/pie-chart-outline.svg?raw';
46
-
47
- // Mapeo de iconos disponibles
48
- const iconMap: Record<string, string> = {
49
- shield: shieldIcon,
50
- search: searchIcon,
51
- card: cardIcon,
52
- chart: chartIcon,
53
- trophy: trophyIcon,
54
- user: userIcon,
55
- 'document-text': documentIcon,
56
- laptop: laptopIcon,
57
- cash: cashIcon,
58
- pie: pieIcon
59
- };
9
+ const { class: className = "", isMobile = false } = Astro.props;
60
10
 
61
- const iconSvg = iconMap[icon] || iconMap.shield;
11
+ const navigationItems = [
12
+ { href: "#atleta", label: "Atleta" },
13
+ { href: "/us", label: "Nosotros" },
14
+ { href: "/how-work", label: "Cómo trabajamos" },
15
+ { href: "/contact", label: "Contacto" },
16
+ ];
62
17
  ---
63
18
 
64
- <div class={`timeline-step ${isVertical ? 'vertical' : 'horizontal'} ${className}`}>
65
- <div class="timeline-icon-container relative">
66
- <!-- Ícono principal -->
67
- <div
68
- class="w-12 h-12 bg-primary-60 rounded-full flex items-center justify-center relative z-10 mb-4"
69
- set:html={iconSvg.replace('<svg', `<svg width="${isVertical ? '20' : '24'}" height="${isVertical ? '20' : '24'}" class="text-primary-90"`)}
70
- ></div>
71
-
72
- <!-- Efecto pulse animado -->
73
- <div
74
- class="absolute -top-2 -left-2 w-16 h-16 border-2 border-primary-60/30 rounded-full animate-pulse"
75
- style={`animation-delay: ${animationDelay}s;`}
76
- ></div>
77
-
78
- <!-- Conector vertical (solo para timeline vertical) -->
79
- {isVertical && stepNumber < 5 && (
80
- <div class="absolute top-12 left-1/2 transform -translate-x-1/2 w-0.5 h-16 bg-primary-60/30"></div>
81
- )}
82
- </div>
83
-
84
- <!-- Contenido del paso -->
85
- <div class={`timeline-content ${isVertical ? 'flex-1 ml-4' : 'text-center'}`}>
86
- <h3 class={`subtitle-m-style text-white ${isVertical ? ' ' : 'mb-2 mt-4'}`}>
87
- {stepNumber}. {title}
88
- </h3>
89
- <p class={`body-s-style text-gray-300 ${!isVertical ? 'max-w-48' : ''}`}>
90
- {description}
91
- </p>
92
- </div>
93
- </div>
19
+ <nav class={`${className}`} aria-label="Navegación principal">
20
+ <ul
21
+ class={`
22
+ ${
23
+ isMobile
24
+ ? "flex flex-col space-y-4 py-4"
25
+ : "hidden lg:flex lg:items-center lg:space-x-8"
26
+ }
27
+ `}
28
+ role="menubar"
29
+ >
30
+ {
31
+ navigationItems.map((item) => (
32
+ <li role="none">
33
+ <NavLink href={item.href} class={isMobile ? "w-full text-left" : ""}>
34
+ {item.label}
35
+ </NavLink>
36
+ </li>
37
+ ))
38
+ }
39
+ </ul>
40
+ </nav>
94
41
 
95
- <style>
96
- .timeline-step.horizontal {
97
- @apply flex flex-col items-center w-1/5 relative;
98
- }
99
-
100
- .timeline-step.vertical {
101
- @apply flex items-start;
102
- }
103
-
104
- .timeline-step.vertical .timeline-icon-container {
105
- @apply flex-shrink-0 relative;
106
- }
107
-
108
- .timeline-step.horizontal .timeline-icon-container {
109
- @apply relative;
110
- }
111
-
112
- @keyframes pulse {
113
- 0%, 100% { opacity: 0.3; }
114
- 50% { opacity: 0.1; }
115
- }
116
-
117
- .animate-pulse {
118
- animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
119
- }
120
- </style>
@@ -0,0 +1,70 @@
1
+ ---
2
+ import FooterLink from "../atoms/FooterLink.astro";
3
+
4
+ export interface Props {
5
+ title: string;
6
+ links: Array<{
7
+ href: string;
8
+ label: string;
9
+ external?: boolean;
10
+ ariaLabel?: string;
11
+ }>;
12
+ class?: string;
13
+ }
14
+
15
+ const { title, links, class: className = "" } = Astro.props;
16
+ ---
17
+
18
+ <div class={`link-group ${className}`}>
19
+ <h3 class="link-group__title">
20
+ {title}
21
+ </h3>
22
+
23
+ <nav aria-label={`${title} navigation`}>
24
+ <ul class="link-group__list" role="list">
25
+ {
26
+ links.map((link) => (
27
+ <li class="link-group__item">
28
+ <FooterLink
29
+ href={link.href}
30
+ external={link.external || false}
31
+ ariaLabel={link.ariaLabel}
32
+ >
33
+ {link.label}
34
+ </FooterLink>
35
+ </li>
36
+ ))
37
+ }
38
+ </ul>
39
+ </nav>
40
+ </div>
41
+
42
+ <style>
43
+ .link-group {
44
+ /* Base styles */
45
+ }
46
+
47
+ .link-group__title {
48
+ font-family: var(--font-family-manrope);
49
+ font-size: var(--font-size-h6);
50
+ font-weight: var(--font-weight-h6);
51
+ line-height: var(--line-height-h6);
52
+ letter-spacing: var(--letter-spacing-h6);
53
+ color: var(--color-white);
54
+ margin-bottom: 16px;
55
+ }
56
+
57
+ .link-group__list {
58
+ list-style: none;
59
+ margin: 0;
60
+ padding: 0;
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 12px;
64
+ }
65
+
66
+ .link-group__item {
67
+ /* Item styles */
68
+ }
69
+ </style>
70
+
@@ -0,0 +1,79 @@
1
+ ---
2
+ import NavLink from "../atoms/NavLink.astro";
3
+
4
+ export interface Props {
5
+ class?: string;
6
+ isMobile?: boolean;
7
+ }
8
+
9
+ const { class: className = "", isMobile = false } = Astro.props;
10
+
11
+ const navigationItems = [
12
+ { href: "#atleta", label: "Atleta" },
13
+ { href: "/us", label: "Nosotros" },
14
+ { href: "/how-work", label: "Cómo trabajamos" },
15
+ { href: "/contact", label: "Contacto" },
16
+ ];
17
+ ---
18
+
19
+ <nav class={`nav-menu ${className}`} aria-label="Navegación principal">
20
+ <ul
21
+ class={`nav-menu__list ${isMobile ? 'nav-menu__list--mobile' : 'nav-menu__list--desktop'}`}
22
+ role="menubar"
23
+ >
24
+ {
25
+ navigationItems.map((item) => (
26
+ <li role="none" class="nav-menu__item">
27
+ <NavLink href={item.href} class={isMobile ? "nav-menu__link--mobile" : ""}>
28
+ {item.label}
29
+ </NavLink>
30
+ </li>
31
+ ))
32
+ }
33
+ </ul>
34
+ </nav>
35
+
36
+ <style>
37
+ .nav-menu {
38
+ /* Base styles */
39
+ }
40
+
41
+ .nav-menu__list {
42
+ list-style: none;
43
+ margin: 0;
44
+ padding: 0;
45
+ }
46
+
47
+ /* Desktop menu */
48
+ .nav-menu__list--desktop {
49
+ display: none;
50
+ }
51
+
52
+ @media (min-width: 1024px) {
53
+ .nav-menu__list--desktop {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 32px;
57
+ }
58
+ }
59
+
60
+ /* Mobile menu */
61
+ .nav-menu__list--mobile {
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: 16px;
65
+ padding: 16px 0;
66
+ }
67
+
68
+ .nav-menu__item {
69
+ /* Item styles */
70
+ }
71
+
72
+ /* Mobile link full width */
73
+ .nav-menu__list--mobile :global(.nav-menu__link--mobile) {
74
+ display: block;
75
+ width: 100%;
76
+ text-align: left;
77
+ }
78
+ </style>
79
+
@@ -0,0 +1,64 @@
1
+ ---
2
+ import SocialIcon from "../atoms/SocialIcon.astro";
3
+
4
+ export interface Props {
5
+ title: string;
6
+ socialLinks: Array<{
7
+ platform: "youtube" | "facebook" | "twitter" | "instagram";
8
+ href: string;
9
+ }>;
10
+ class?: string;
11
+ }
12
+
13
+ const { title, socialLinks, class: className = "" } = Astro.props;
14
+ ---
15
+
16
+ <div class={`social-group ${className}`}>
17
+ <h3 class="social-group__title">
18
+ {title}
19
+ </h3>
20
+
21
+ <nav aria-label="Redes sociales">
22
+ <ul class="social-group__list" role="list">
23
+ {
24
+ socialLinks.map((social) => (
25
+ <li class="social-group__item">
26
+ <SocialIcon platform={social.platform} href={social.href} />
27
+ </li>
28
+ ))
29
+ }
30
+ </ul>
31
+ </nav>
32
+ </div>
33
+
34
+ <style>
35
+ .social-group {
36
+ /* Base styles */
37
+ }
38
+
39
+ .social-group__title {
40
+ font-family: var(--font-family-manrope);
41
+ font-size: var(--font-size-h6);
42
+ font-weight: var(--font-weight-h6);
43
+ line-height: var(--line-height-h6);
44
+ letter-spacing: var(--letter-spacing-h6);
45
+ color: var(--color-white);
46
+ text-align: center;
47
+ margin-bottom: 16px;
48
+ }
49
+
50
+ .social-group__list {
51
+ list-style: none;
52
+ margin: 0;
53
+ padding: 0;
54
+ display: flex;
55
+ justify-content: center;
56
+ align-items: center;
57
+ gap: 16px;
58
+ }
59
+
60
+ .social-group__item {
61
+ /* Item styles */
62
+ }
63
+ </style>
64
+
@@ -0,0 +1,214 @@
1
+ ---
2
+ import Logo from "../atoms/Logo.astro";
3
+ import LinkGroup from "../molecules/LinkGroup.astro";
4
+ import SocialGroup from "../molecules/SocialGroup.astro";
5
+
6
+ export interface Props {
7
+ class?: string;
8
+ }
9
+
10
+ const { class: className = "" } = Astro.props;
11
+
12
+ // Configuración de enlaces del sitio
13
+ const siteMapLinks = [
14
+ { href: "#atletas", label: "Atletas" },
15
+ { href: "#nosotros", label: "Nosotros" },
16
+ { href: "#como-trabajamos", label: "Cómo trabajamos" },
17
+ { href: "#contacto", label: "Contacto" },
18
+ { href: "#ayuda", label: "Ayuda" },
19
+ { href: "#faq", label: "FAQ" },
20
+ ];
21
+
22
+ // Configuración de enlaces legales
23
+ const legalLinks = [
24
+ { href: "/privacy-policy", label: "Políticas de Privacidad" },
25
+ { href: "/terms-conditions", label: "Términos y Condiciones" },
26
+ { href: "/cookie-policy", label: "Políticas de Cookies" },
27
+ ];
28
+
29
+ // Configuración de redes sociales
30
+ const socialLinks = [
31
+ { platform: "youtube" as const, href: "https://youtube.com/@athlefi" },
32
+ { platform: "facebook" as const, href: "https://facebook.com/athlefi" },
33
+ { platform: "twitter" as const, href: "https://twitter.com/athlefi" },
34
+ { platform: "instagram" as const, href: "https://instagram.com/athlefi" },
35
+ ];
36
+
37
+ const currentYear = new Date().getFullYear();
38
+ ---
39
+
40
+ <footer
41
+ class={`footer ${className}`}
42
+ role="contentinfo"
43
+ aria-label="Información del sitio y enlaces adicionales"
44
+ >
45
+ <div class="footer__container">
46
+ <!-- Secciones principales del footer -->
47
+ <div class="footer__grid">
48
+ <!-- Logo -->
49
+ <div class="footer__logo">
50
+ <div class="footer__logo-wrapper">
51
+ <Logo size="lg" class="footer__logo-image" />
52
+ </div>
53
+ </div>
54
+
55
+ <!-- Separador mobile -->
56
+ <div class="footer__separator footer__separator--mobile"></div>
57
+
58
+ <!-- Mapa del sitio -->
59
+ <div class="footer__sitemap">
60
+ <LinkGroup title="Mapa del sitio" links={siteMapLinks} />
61
+ </div>
62
+
63
+ <!-- Legales -->
64
+ <div class="footer__legal">
65
+ <LinkGroup title="Legales" links={legalLinks} />
66
+ </div>
67
+
68
+ <!-- Redes sociales -->
69
+ <div class="footer__social">
70
+ <SocialGroup title="Síguenos" socialLinks={socialLinks} />
71
+ </div>
72
+ </div>
73
+
74
+ <!-- Separador -->
75
+ <div class="footer__separator"></div>
76
+
77
+ <!-- Sección inferior con copyright -->
78
+ <div class="footer__copyright">
79
+ <div class="footer__copyright-content">
80
+ <p class="footer__copyright-text">
81
+ © {currentYear} Athlefi. Todos los derechos reservados.
82
+ </p>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </footer>
87
+
88
+ <style>
89
+ .footer {
90
+ background-color: var(--color-primary-90);
91
+ border-top: 1px solid rgb(31, 41, 55);
92
+ }
93
+
94
+ .footer__container {
95
+ max-width: 1280px;
96
+ margin: 0 auto;
97
+ padding: 0 1rem;
98
+ }
99
+
100
+ @media (min-width: 640px) {
101
+ .footer__container {
102
+ padding: 0 1.5rem;
103
+ }
104
+ }
105
+
106
+ @media (min-width: 1024px) {
107
+ .footer__container {
108
+ padding: 0 2rem;
109
+ }
110
+ }
111
+
112
+ /* Grid Layout */
113
+ .footer__grid {
114
+ display: grid;
115
+ grid-template-columns: 1fr;
116
+ gap: 2rem;
117
+ padding: 3rem 0;
118
+ }
119
+
120
+ @media (min-width: 1024px) {
121
+ .footer__grid {
122
+ grid-template-columns: repeat(16, 1fr);
123
+ gap: 1.5rem;
124
+ }
125
+ }
126
+
127
+ /* Logo Section */
128
+ .footer__logo {
129
+ grid-column: 1;
130
+ }
131
+
132
+ @media (min-width: 1024px) {
133
+ .footer__logo {
134
+ grid-column: 1 / span 4;
135
+ }
136
+ }
137
+
138
+ .footer__logo-wrapper {
139
+ display: flex;
140
+ flex-direction: column;
141
+ align-items: flex-start;
142
+ }
143
+
144
+ .footer__logo-image {
145
+ margin-bottom: 1rem;
146
+ }
147
+
148
+ /* Sitemap Section */
149
+ .footer__sitemap {
150
+ grid-column: 1;
151
+ }
152
+
153
+ @media (min-width: 1024px) {
154
+ .footer__sitemap {
155
+ grid-column: 5 / span 4;
156
+ }
157
+ }
158
+
159
+ /* Legal Section */
160
+ .footer__legal {
161
+ grid-column: 1;
162
+ }
163
+
164
+ @media (min-width: 1024px) {
165
+ .footer__legal {
166
+ grid-column: 9 / span 4;
167
+ }
168
+ }
169
+
170
+ /* Social Section */
171
+ .footer__social {
172
+ grid-column: 1;
173
+ }
174
+
175
+ @media (min-width: 1024px) {
176
+ .footer__social {
177
+ grid-column: 13 / span 4;
178
+ }
179
+ }
180
+
181
+ /* Separators */
182
+ .footer__separator {
183
+ border-top: 1px solid var(--color-gray-medium);
184
+ }
185
+
186
+ .footer__separator--mobile {
187
+ display: block;
188
+ }
189
+
190
+ @media (min-width: 1024px) {
191
+ .footer__separator--mobile {
192
+ display: none;
193
+ }
194
+ }
195
+
196
+ /* Copyright Section */
197
+ .footer__copyright {
198
+ padding: 1.5rem 0;
199
+ }
200
+
201
+ .footer__copyright-content {
202
+ text-align: center;
203
+ }
204
+
205
+ .footer__copyright-text {
206
+ font-family: var(--font-family-manrope);
207
+ font-size: var(--font-size-body-s);
208
+ font-weight: var(--font-weight-body-s);
209
+ line-height: var(--line-height-body-s);
210
+ letter-spacing: var(--letter-spacing-body-s);
211
+ color: rgb(156, 163, 175);
212
+ }
213
+ </style>
214
+
@@ -0,0 +1,407 @@
1
+ ---
2
+ import Logo from "../atoms/Logo.astro";
3
+ import NavMenu from "../molecules/NavMenu.astro";
4
+
5
+ export interface Props {
6
+ class?: string;
7
+ }
8
+
9
+ const { class: className = "" } = Astro.props;
10
+ ---
11
+
12
+ <header
13
+ class={`header ${className}`}
14
+ role="banner"
15
+ >
16
+ <div class="header__container">
17
+ <!-- Distribución con grid para centrado perfecto -->
18
+ <div class="header__grid">
19
+ <!-- Logo -->
20
+ <div class="header__logo">
21
+ <Logo />
22
+ </div>
23
+
24
+ <!-- Navegación Desktop -->
25
+ <div class="header__nav-desktop">
26
+ <NavMenu class="header__nav-center" />
27
+ </div>
28
+
29
+ <!-- Botones Auth Desktop - Slot -->
30
+ <div class="header__auth-desktop">
31
+ <slot name="auth-buttons" />
32
+ </div>
33
+
34
+ <!-- Botón Hamburguesa Mobile -->
35
+ <div class="header__mobile-toggle">
36
+ <button
37
+ id="mobile-menu-button"
38
+ type="button"
39
+ class="header__hamburger"
40
+ aria-controls="mobile-menu"
41
+ aria-expanded="false"
42
+ aria-label="Abrir menú de navegación"
43
+ >
44
+ <span class="sr-only">Abrir menú principal</span>
45
+ <!-- Hamburger Icon -->
46
+ <svg
47
+ id="hamburger-icon"
48
+ class="header__icon header__icon--visible"
49
+ xmlns="http://www.w3.org/2000/svg"
50
+ fill="none"
51
+ viewBox="0 0 24 24"
52
+ stroke="currentColor"
53
+ aria-hidden="true"
54
+ >
55
+ <path
56
+ stroke-linecap="round"
57
+ stroke-linejoin="round"
58
+ stroke-width="2"
59
+ d="M4 6h16M4 12h16M4 18h16"></path>
60
+ </svg>
61
+ <!-- Close Icon -->
62
+ <svg
63
+ id="close-icon"
64
+ class="header__icon header__icon--hidden"
65
+ xmlns="http://www.w3.org/2000/svg"
66
+ fill="none"
67
+ viewBox="0 0 24 24"
68
+ stroke="currentColor"
69
+ aria-hidden="true"
70
+ >
71
+ <path
72
+ stroke-linecap="round"
73
+ stroke-linejoin="round"
74
+ stroke-width="2"
75
+ d="M6 18L18 6M6 6l12 12"></path>
76
+ </svg>
77
+ </button>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Mobile Menu -->
83
+ <div
84
+ id="mobile-menu"
85
+ class="header__mobile-menu header__mobile-menu--hidden"
86
+ role="navigation"
87
+ aria-label="Menú de navegación móvil"
88
+ >
89
+ <div class="header__mobile-content">
90
+ <!-- Mobile Navigation -->
91
+ <NavMenu isMobile={true} />
92
+
93
+ <!-- Mobile Auth Buttons - Slot -->
94
+ <div class="header__mobile-auth">
95
+ <slot name="auth-buttons-mobile">
96
+ <slot name="auth-buttons" />
97
+ </slot>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </header>
102
+
103
+ <style>
104
+ .header {
105
+ background-color: var(--color-primary-90);
106
+ border-bottom: 1px solid var(--color-gray-medium);
107
+ }
108
+
109
+ .header__container {
110
+ max-width: 1280px;
111
+ margin: 0 auto;
112
+ padding: 0 1rem;
113
+ }
114
+
115
+ @media (min-width: 640px) {
116
+ .header__container {
117
+ padding: 0 1.5rem;
118
+ }
119
+ }
120
+
121
+ @media (min-width: 1024px) {
122
+ .header__container {
123
+ padding: 0 2rem;
124
+ }
125
+ }
126
+
127
+ /* Grid Layout */
128
+ .header__grid {
129
+ display: grid;
130
+ grid-template-columns: repeat(4, 1fr);
131
+ align-items: center;
132
+ gap: 1rem;
133
+ padding: 1rem 0;
134
+ }
135
+
136
+ @media (min-width: 1024px) {
137
+ .header__grid {
138
+ grid-template-columns: repeat(16, 1fr);
139
+ }
140
+ }
141
+
142
+ /* Logo */
143
+ .header__logo {
144
+ grid-column: span 2;
145
+ }
146
+
147
+ @media (min-width: 1024px) {
148
+ .header__logo {
149
+ grid-column: 1 / span 3;
150
+ }
151
+ }
152
+
153
+ /* Desktop Navigation */
154
+ .header__nav-desktop {
155
+ display: none;
156
+ }
157
+
158
+ @media (min-width: 1024px) {
159
+ .header__nav-desktop {
160
+ display: block;
161
+ grid-column: 5 / span 8;
162
+ margin-left: 5rem;
163
+ }
164
+ }
165
+
166
+ .header__nav-center {
167
+ display: flex;
168
+ justify-content: center;
169
+ }
170
+
171
+ /* Desktop Auth Buttons */
172
+ .header__auth-desktop {
173
+ display: none;
174
+ }
175
+
176
+ @media (min-width: 1024px) {
177
+ .header__auth-desktop {
178
+ display: block;
179
+ grid-column: 14 / span 3;
180
+ }
181
+ }
182
+
183
+ /* Mobile Toggle */
184
+ .header__mobile-toggle {
185
+ grid-column: span 2;
186
+ display: flex;
187
+ justify-content: flex-end;
188
+ }
189
+
190
+ @media (min-width: 1024px) {
191
+ .header__mobile-toggle {
192
+ display: none;
193
+ }
194
+ }
195
+
196
+ /* Hamburger Button */
197
+ .header__hamburger {
198
+ display: inline-flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ padding: 0.5rem;
202
+ color: var(--color-white);
203
+ background-color: transparent;
204
+ border: none;
205
+ border-radius: 6px;
206
+ cursor: pointer;
207
+ transition: background-color 0.2s ease, color 0.2s ease;
208
+ }
209
+
210
+ .header__hamburger:hover {
211
+ background-color: rgb(31, 41, 55);
212
+ color: var(--color-primary-60);
213
+ }
214
+
215
+ .header__hamburger:focus-visible {
216
+ outline: 2px solid var(--color-primary-60);
217
+ outline-offset: 2px;
218
+ }
219
+
220
+ /* Icons */
221
+ .header__icon {
222
+ width: 24px;
223
+ height: 24px;
224
+ }
225
+
226
+ .header__icon--visible {
227
+ display: block;
228
+ }
229
+
230
+ .header__icon--hidden {
231
+ display: none;
232
+ }
233
+
234
+ /* Screen reader only */
235
+ .sr-only {
236
+ position: absolute;
237
+ width: 1px;
238
+ height: 1px;
239
+ padding: 0;
240
+ margin: -1px;
241
+ overflow: hidden;
242
+ clip: rect(0, 0, 0, 0);
243
+ white-space: nowrap;
244
+ border-width: 0;
245
+ }
246
+
247
+ /* Mobile Menu */
248
+ .header__mobile-menu {
249
+ border-top: 1px solid var(--color-primary-60);
250
+ }
251
+
252
+ @media (min-width: 1024px) {
253
+ .header__mobile-menu {
254
+ display: none !important;
255
+ }
256
+ }
257
+
258
+ .header__mobile-menu--hidden {
259
+ display: none;
260
+ }
261
+
262
+ .header__mobile-menu--visible {
263
+ display: block;
264
+ }
265
+
266
+ .header__mobile-content {
267
+ max-width: 1280px;
268
+ margin: 0 auto;
269
+ padding: 1rem;
270
+ display: flex;
271
+ flex-direction: column;
272
+ gap: 1rem;
273
+ }
274
+
275
+ @media (min-width: 640px) {
276
+ .header__mobile-content {
277
+ padding: 1rem 1.5rem;
278
+ }
279
+ }
280
+
281
+ /* Mobile Auth */
282
+ .header__mobile-auth {
283
+ border-top: 1px solid var(--color-primary-60);
284
+ padding-top: 1rem;
285
+ }
286
+ </style>
287
+
288
+ <script>
289
+ // Mobile menu toggle functionality
290
+ document.addEventListener("DOMContentLoaded", function () {
291
+ // Get DOM elements
292
+ const mobileMenuButton = document.getElementById("mobile-menu-button");
293
+ const mobileMenu = document.getElementById("mobile-menu");
294
+ const hamburgerIcon = document.getElementById("hamburger-icon");
295
+ const closeIcon = document.getElementById("close-icon");
296
+
297
+ // Early return if any required element is missing
298
+ if (!mobileMenuButton || !mobileMenu || !hamburgerIcon || !closeIcon) {
299
+ console.warn("Mobile menu: Required DOM elements not found");
300
+ return;
301
+ }
302
+
303
+ let isMenuOpen = false;
304
+
305
+ /**
306
+ * Toggles the mobile menu visibility and updates UI states
307
+ */
308
+ function toggleMenu(): void {
309
+ try {
310
+ isMenuOpen = !isMenuOpen;
311
+
312
+ // Toggle menu visibility
313
+ if (isMenuOpen) {
314
+ mobileMenu!.classList.remove("header__mobile-menu--hidden");
315
+ mobileMenu!.classList.add("header__mobile-menu--visible");
316
+ } else {
317
+ mobileMenu!.classList.remove("header__mobile-menu--visible");
318
+ mobileMenu!.classList.add("header__mobile-menu--hidden");
319
+ }
320
+
321
+ // Toggle icons visibility
322
+ if (isMenuOpen) {
323
+ hamburgerIcon!.classList.remove("header__icon--visible");
324
+ hamburgerIcon!.classList.add("header__icon--hidden");
325
+ closeIcon!.classList.remove("header__icon--hidden");
326
+ closeIcon!.classList.add("header__icon--visible");
327
+ } else {
328
+ hamburgerIcon!.classList.remove("header__icon--hidden");
329
+ hamburgerIcon!.classList.add("header__icon--visible");
330
+ closeIcon!.classList.remove("header__icon--visible");
331
+ closeIcon!.classList.add("header__icon--hidden");
332
+ }
333
+
334
+ // Update ARIA attributes for accessibility
335
+ mobileMenuButton!.setAttribute("aria-expanded", isMenuOpen.toString());
336
+ mobileMenuButton!.setAttribute(
337
+ "aria-label",
338
+ isMenuOpen ? "Cerrar menú de navegación" : "Abrir menú de navegación",
339
+ );
340
+
341
+ // Manage body scroll and focus
342
+ if (isMenuOpen) {
343
+ document.body.style.overflow = "hidden";
344
+ // Focus first focusable element in mobile menu for keyboard navigation
345
+ const firstFocusableElement = mobileMenu!.querySelector(
346
+ 'a, button, input, [tabindex]:not([tabindex="-1"])',
347
+ ) as HTMLElement;
348
+ firstFocusableElement?.focus();
349
+ } else {
350
+ document.body.style.overflow = "";
351
+ // Return focus to menu button when closing
352
+ mobileMenuButton!.focus();
353
+ }
354
+ } catch (error) {
355
+ console.error("Error toggling mobile menu:", error);
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Closes the menu if it's currently open
361
+ */
362
+ function closeMenu(): void {
363
+ if (isMenuOpen) {
364
+ toggleMenu();
365
+ }
366
+ }
367
+
368
+ // Event Listeners
369
+
370
+ // Toggle menu on button click
371
+ mobileMenuButton.addEventListener("click", toggleMenu);
372
+
373
+ // Close menu when clicking outside
374
+ document.addEventListener("click", function (event: Event) {
375
+ const target = event.target as Node;
376
+ if (
377
+ isMenuOpen &&
378
+ !mobileMenu!.contains(target) &&
379
+ !mobileMenuButton!.contains(target)
380
+ ) {
381
+ closeMenu();
382
+ }
383
+ });
384
+
385
+ // Close menu on escape key
386
+ document.addEventListener("keydown", function (event: KeyboardEvent) {
387
+ if (event.key === "Escape" && isMenuOpen) {
388
+ closeMenu();
389
+ }
390
+ });
391
+
392
+ // Close menu when window is resized to desktop size
393
+ let resizeTimeout: number;
394
+ window.addEventListener("resize", function () {
395
+ clearTimeout(resizeTimeout);
396
+ resizeTimeout = window.setTimeout(() => {
397
+ if (window.innerWidth >= 1024 && isMenuOpen) {
398
+ closeMenu();
399
+ }
400
+ }, 150);
401
+ });
402
+
403
+ // Close menu on page navigation (for SPAs)
404
+ window.addEventListener("beforeunload", closeMenu);
405
+ });
406
+ </script>
407
+
package/src/index.ts CHANGED
@@ -21,14 +21,17 @@ export { default as TimelineStep } from "./components/atoms/TimelineStep.astro";
21
21
  // ============================================
22
22
  // MOLÉCULAS
23
23
  // ============================================
24
+ export { default as LinkGroup } from "./components/molecules/LinkGroup.astro";
25
+ export { default as NavMenu } from "./components/molecules/NavMenu.astro";
26
+ export { default as SocialGroup } from "./components/molecules/SocialGroup.astro";
24
27
  // export { default as SearchBar } from "./components/molecules/SearchBar.astro";
25
28
  // export { default as Card } from "./components/molecules/Card.astro";
26
29
 
27
30
  // ============================================
28
31
  // ORGANISMOS
29
32
  // ============================================
30
- // export { default as Header } from "./components/organisms/Header.astro";
31
- // export { default as Footer } from "./components/organisms/Footer.astro";
33
+ export { default as Header } from "./components/organisms/Header.astro";
34
+ export { default as Footer } from "./components/organisms/Footer.astro";
32
35
 
33
36
  // ============================================
34
37
  // TEMPLATES