@axzydev/axzy_ui_system 1.4.8 → 1.5.1

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.
Files changed (223) hide show
  1. package/README.md +2 -11
  2. package/dist/index.cjs +1826 -1351
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +1 -1
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.d.cts +115 -14
  7. package/dist/index.d.ts +115 -14
  8. package/dist/index.js +1481 -1010
  9. package/dist/index.js.map +1 -1
  10. package/dist/layered.css +1 -1
  11. package/package.json +3 -16
  12. package/src/App.tsx +90 -73
  13. package/src/components/atoms/badget/badget.tsx +23 -5
  14. package/src/components/atoms/chip/chip.tsx +3 -1
  15. package/src/components/atoms/image/image.tsx +3 -1
  16. package/src/components/atoms/input/input.tsx +8 -5
  17. package/src/components/atoms/loader/loader.tsx +3 -1
  18. package/src/components/atoms/slide/slide.props.ts +12 -0
  19. package/src/components/atoms/slide/slide.tsx +3 -0
  20. package/src/components/molecules/alert/alert.tsx +3 -1
  21. package/src/components/molecules/breadcrumbs/breadcrumbs.tsx +3 -1
  22. package/src/components/molecules/calendar/calendar.tsx +9 -6
  23. package/src/components/molecules/card/card.tsx +5 -2
  24. package/src/components/molecules/chip-input/chip-input.tsx +9 -7
  25. package/src/components/molecules/date-picker/datePicker.tsx +9 -9
  26. package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +1 -1
  27. package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +6 -3
  28. package/src/components/molecules/field/field.tsx +3 -1
  29. package/src/components/molecules/form-header/form-header.tsx +3 -1
  30. package/src/components/molecules/line-bar-chart/line-bar-chart.props.ts +2 -2
  31. package/src/components/molecules/line-bar-chart/line-bar-chart.tsx +20 -15
  32. package/src/components/molecules/masked-input/masked-input.tsx +3 -1
  33. package/src/components/molecules/multi-select/multi-select.tsx +12 -9
  34. package/src/components/molecules/pagination/pagination.tsx +11 -5
  35. package/src/components/molecules/search-select/search-select.tsx +11 -9
  36. package/src/components/molecules/select/select.tsx +6 -4
  37. package/src/components/molecules/stepper/stepper.tsx +6 -4
  38. package/src/components/molecules/table/table.tsx +30 -28
  39. package/src/components/molecules/time-picker/timePicker.tsx +8 -6
  40. package/src/components/molecules/wysiwyg/wysiwyg.tsx +23 -12
  41. package/src/components/organisms/confirm-dialog/confirm-dialog.props.ts +4 -4
  42. package/src/components/organisms/confirm-dialog/confirm-dialog.tsx +16 -8
  43. package/src/components/organisms/data-table/dataTable.tsx +31 -25
  44. package/src/components/organisms/dialog/dialog.tsx +3 -0
  45. package/src/components/organisms/drawer/drawer.tsx +3 -0
  46. package/src/components/organisms/dropfile/dropfile.tsx +19 -17
  47. package/src/components/organisms/form-builder/fieldRenderer.tsx +4 -2
  48. package/src/components/organisms/navbar/navbar.tsx +4 -2
  49. package/src/components/organisms/page-header/page-header.tsx +3 -1
  50. package/src/components/organisms/searchTable/components/PaginationControls.tsx +8 -6
  51. package/src/components/organisms/searchTable/components/PaginationInfo.tsx +3 -1
  52. package/src/components/organisms/searchTable/components/SearchAndSortBar.tsx +7 -5
  53. package/src/components/organisms/searchTable/components/SearchInput.tsx +4 -2
  54. package/src/components/organisms/searchTable/components/SortButton.tsx +13 -11
  55. package/src/components/organisms/searchTable/components/TableEmptyState.tsx +4 -2
  56. package/src/components/organisms/searchTable/components/TableRow.tsx +6 -4
  57. package/src/components/organisms/searchTable/searchTable.tsx +4 -2
  58. package/src/components/organisms/sidebar/sidebar.props.ts +1 -1
  59. package/src/components/organisms/sidebar/sidebar.tsx +9 -4
  60. package/src/components/organisms/toast/toast.tsx +3 -1
  61. package/src/components/organisms/topbar/topbar.tsx +4 -2
  62. package/src/components/templates/page/page.props.ts +4 -4
  63. package/src/components/templates/page/page.tsx +6 -4
  64. package/src/components/theme-provider/themeProvider.tsx +28 -17
  65. package/src/dev.css +61 -2
  66. package/src/global.d.ts +10 -0
  67. package/src/i18n/context.ts +9 -0
  68. package/src/i18n/index.ts +22 -0
  69. package/src/i18n/locales/en.ts +163 -0
  70. package/src/i18n/locales/es.ts +165 -0
  71. package/src/i18n/provider.tsx +44 -0
  72. package/src/i18n/translator.ts +82 -0
  73. package/src/i18n/types.ts +54 -0
  74. package/src/i18n/useITTranslation.ts +20 -0
  75. package/src/index.css +0 -1
  76. package/src/index.ts +7 -1
  77. package/src/sandbox/LandingShowcase.tsx +224 -580
  78. package/src/sandbox/NotFoundShowcase.tsx +51 -36
  79. package/src/sandbox/TopbarSearch.tsx +6 -4
  80. package/src/sandbox/controls.tsx +180 -0
  81. package/src/sandbox/i18n.ts +1939 -0
  82. package/src/sandbox/icons.tsx +25 -0
  83. package/src/sandbox/locale.tsx +72 -0
  84. package/src/sandbox/ui.tsx +474 -0
  85. package/src/sandbox/useHashRoute.ts +46 -10
  86. package/src/showcases/DataShowcases.tsx +103 -87
  87. package/src/showcases/FeedbackShowcases.tsx +37 -30
  88. package/src/showcases/FormShowcases.tsx +303 -215
  89. package/src/showcases/GettingStartedShowcase.tsx +151 -145
  90. package/src/showcases/HelpShowcase.tsx +126 -128
  91. package/src/showcases/HomeShowcase.tsx +678 -482
  92. package/src/showcases/LayoutPrimitivesShowcases.tsx +129 -114
  93. package/src/showcases/NavigationShowcases.tsx +45 -37
  94. package/src/showcases/PageShowcases.tsx +171 -152
  95. package/src/showcases/ProductoDetalle.tsx +272 -129
  96. package/src/showcases/ShowcaseLayout.tsx +256 -219
  97. package/src/showcases/SizesShowcase.tsx +123 -64
  98. package/src/showcases/StructureShowcases.tsx +203 -138
  99. package/src/showcases/portfolioUi.tsx +153 -0
  100. package/src/showcases/techIcons.ts +3 -0
  101. package/src/utils/table.utils.ts +10 -2
  102. package/src/components/atoms/avatar/avatar.doc.mdx +0 -48
  103. package/src/components/atoms/avatar/avatar.stories.tsx +0 -64
  104. package/src/components/atoms/badget/badget.doc.mdx +0 -46
  105. package/src/components/atoms/badget/badget.stories.tsx +0 -75
  106. package/src/components/atoms/button/button.doc.mdx +0 -59
  107. package/src/components/atoms/button/button.stories.tsx +0 -173
  108. package/src/components/atoms/checkbox/checkbox.doc.mdx +0 -54
  109. package/src/components/atoms/checkbox/checkbox.stories.tsx +0 -53
  110. package/src/components/atoms/chip/chip.doc.mdx +0 -47
  111. package/src/components/atoms/chip/chip.stories.tsx +0 -64
  112. package/src/components/atoms/divider/divider.doc.mdx +0 -42
  113. package/src/components/atoms/divider/divider.stories.tsx +0 -33
  114. package/src/components/atoms/flex/flex.doc.mdx +0 -59
  115. package/src/components/atoms/flex/flex.stories.tsx +0 -70
  116. package/src/components/atoms/grid/grid.doc.mdx +0 -54
  117. package/src/components/atoms/grid/grid.stories.tsx +0 -71
  118. package/src/components/atoms/image/image.doc.mdx +0 -44
  119. package/src/components/atoms/image/image.stories.tsx +0 -48
  120. package/src/components/atoms/input/input.doc.mdx +0 -70
  121. package/src/components/atoms/input/input.stories.tsx +0 -113
  122. package/src/components/atoms/input-number/input-number.doc.mdx +0 -74
  123. package/src/components/atoms/input-number/input-number.stories.tsx +0 -111
  124. package/src/components/atoms/loader/loader.doc.mdx +0 -43
  125. package/src/components/atoms/loader/loader.stories.tsx +0 -104
  126. package/src/components/atoms/popover/popover.doc.mdx +0 -53
  127. package/src/components/atoms/popover/popover.stories.tsx +0 -24
  128. package/src/components/atoms/progress/progress.doc.mdx +0 -48
  129. package/src/components/atoms/progress/progress.stories.tsx +0 -39
  130. package/src/components/atoms/radio/radio.doc.mdx +0 -62
  131. package/src/components/atoms/radio/radio.stories.tsx +0 -48
  132. package/src/components/atoms/segmented-control/segmented-control.doc.mdx +0 -60
  133. package/src/components/atoms/segmented-control/segmented-control.stories.tsx +0 -62
  134. package/src/components/atoms/skeleton/skeleton.doc.mdx +0 -43
  135. package/src/components/atoms/skeleton/skeleton.stories.tsx +0 -29
  136. package/src/components/atoms/slide/slide.doc.mdx +0 -46
  137. package/src/components/atoms/slide/slide.stories.tsx +0 -120
  138. package/src/components/atoms/slider/slider.doc.mdx +0 -49
  139. package/src/components/atoms/slider/slider.stories.tsx +0 -29
  140. package/src/components/atoms/stack/stack.doc.mdx +0 -126
  141. package/src/components/atoms/stack/stack.stories.tsx +0 -283
  142. package/src/components/atoms/text/text.doc.mdx +0 -41
  143. package/src/components/atoms/text/text.stories.tsx +0 -66
  144. package/src/components/atoms/textarea/textarea.doc.mdx +0 -64
  145. package/src/components/atoms/textarea/textarea.stories.tsx +0 -26
  146. package/src/components/atoms/tooltip/tooltip.doc.mdx +0 -47
  147. package/src/components/atoms/tooltip/tooltip.stories.tsx +0 -19
  148. package/src/components/isolation/isolation.stories.tsx +0 -202
  149. package/src/components/molecules/accordion/accordion.doc.mdx +0 -47
  150. package/src/components/molecules/accordion/accordion.stories.tsx +0 -68
  151. package/src/components/molecules/alert/alert.doc.mdx +0 -48
  152. package/src/components/molecules/alert/alert.stories.tsx +0 -40
  153. package/src/components/molecules/breadcrumbs/breadcrumbs.doc.mdx +0 -57
  154. package/src/components/molecules/breadcrumbs/breadcrumbs.stories.tsx +0 -20
  155. package/src/components/molecules/calendar/calendar.doc.mdx +0 -76
  156. package/src/components/molecules/calendar/calendar.stories.tsx +0 -90
  157. package/src/components/molecules/card/card.doc.mdx +0 -54
  158. package/src/components/molecules/card/card.stories.tsx +0 -57
  159. package/src/components/molecules/chip-input/chip-input.doc.mdx +0 -44
  160. package/src/components/molecules/chip-input/chip-input.stories.tsx +0 -52
  161. package/src/components/molecules/date-picker/date-picker.doc.mdx +0 -65
  162. package/src/components/molecules/date-picker/date-picker.stories.tsx +0 -88
  163. package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +0 -46
  164. package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +0 -41
  165. package/src/components/molecules/empty-state/empty-state.doc.mdx +0 -50
  166. package/src/components/molecules/empty-state/empty-state.stories.tsx +0 -19
  167. package/src/components/molecules/field/field.doc.mdx +0 -43
  168. package/src/components/molecules/field/field.stories.tsx +0 -42
  169. package/src/components/molecules/form-header/form-header.doc.mdx +0 -38
  170. package/src/components/molecules/form-header/form-header.stories.tsx +0 -52
  171. package/src/components/molecules/masked-input/masked-input.doc.mdx +0 -78
  172. package/src/components/molecules/masked-input/masked-input.stories.tsx +0 -126
  173. package/src/components/molecules/multi-select/multi-select.doc.mdx +0 -55
  174. package/src/components/molecules/multi-select/multi-select.stories.tsx +0 -59
  175. package/src/components/molecules/pagination/pagination.doc.mdx +0 -62
  176. package/src/components/molecules/pagination/pagination.stories.tsx +0 -110
  177. package/src/components/molecules/search-select/search-select.doc.mdx +0 -77
  178. package/src/components/molecules/search-select/search-select.stories.tsx +0 -146
  179. package/src/components/molecules/select/select.doc.mdx +0 -75
  180. package/src/components/molecules/select/select.stories.tsx +0 -85
  181. package/src/components/molecules/stat-card/stat-card.doc.mdx +0 -48
  182. package/src/components/molecules/stat-card/stat-card.stories.tsx +0 -40
  183. package/src/components/molecules/stepper/stepper.doc.mdx +0 -54
  184. package/src/components/molecules/stepper/stepper.stories.tsx +0 -154
  185. package/src/components/molecules/table/table.doc.mdx +0 -140
  186. package/src/components/molecules/table/table.stories.tsx +0 -402
  187. package/src/components/molecules/tabs/tabs.doc.mdx +0 -52
  188. package/src/components/molecules/tabs/tabs.stories.tsx +0 -31
  189. package/src/components/molecules/time-picker/timePicker.doc.mdx +0 -68
  190. package/src/components/molecules/time-picker/timePicker.stories.tsx +0 -130
  191. package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +0 -95
  192. package/src/components/organisms/confirm-dialog/confirm-dialog.doc.mdx +0 -60
  193. package/src/components/organisms/confirm-dialog/confirm-dialog.stories.tsx +0 -32
  194. package/src/components/organisms/data-table/ITDataTable.doc.mdx +0 -132
  195. package/src/components/organisms/data-table/ITDataTable.stories.tsx +0 -353
  196. package/src/components/organisms/dialog/dialog.doc.mdx +0 -54
  197. package/src/components/organisms/dialog/dialog.stories.tsx +0 -126
  198. package/src/components/organisms/drawer/drawer.doc.mdx +0 -59
  199. package/src/components/organisms/drawer/drawer.stories.tsx +0 -40
  200. package/src/components/organisms/dropfile/dropfile.doc.mdx +0 -74
  201. package/src/components/organisms/dropfile/dropfile.stories.tsx +0 -93
  202. package/src/components/organisms/form-builder/formBuilder.doc.mdx +0 -251
  203. package/src/components/organisms/form-builder/formBuilder.stories.tsx +0 -914
  204. package/src/components/organisms/navbar/navbar.doc.mdx +0 -77
  205. package/src/components/organisms/navbar/navbar.stories.tsx +0 -83
  206. package/src/components/organisms/page-header/page-header.doc.mdx +0 -54
  207. package/src/components/organisms/page-header/page-header.stories.tsx +0 -74
  208. package/src/components/organisms/searchTable/searchTable.doc.mdx +0 -79
  209. package/src/components/organisms/searchTable/searchTable.stories.tsx +0 -73
  210. package/src/components/organisms/sidebar/sidebar.doc.mdx +0 -176
  211. package/src/components/organisms/sidebar/sidebar.stories.tsx +0 -234
  212. package/src/components/organisms/toast/toast.doc.mdx +0 -50
  213. package/src/components/organisms/toast/toast.stories.tsx +0 -137
  214. package/src/components/organisms/topbar/topbar.doc.mdx +0 -72
  215. package/src/components/organisms/topbar/topbar.stories.tsx +0 -79
  216. package/src/components/organisms/triple-filter/tripleFilter.doc.mdx +0 -62
  217. package/src/components/organisms/triple-filter/tripleFilter.stories.tsx +0 -31
  218. package/src/components/templates/layout/layout.doc.mdx +0 -53
  219. package/src/components/templates/layout/layout.stories.tsx +0 -113
  220. package/src/components/templates/page/page.doc.mdx +0 -86
  221. package/src/components/templates/page/page.stories.tsx +0 -97
  222. package/src/components/theme-provider/themeProvider.doc.mdx +0 -85
  223. package/src/components/theme-provider/themeProvider.stories.tsx +0 -58
@@ -1,284 +1,359 @@
1
- import { useState, type ReactNode } from "react";
1
+ import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
2
2
  import {
3
- FaBoxOpen,
4
- FaCloud,
5
- FaCode,
6
- FaDatabase,
3
+ FaArrowRight,
4
+ FaCheck,
7
5
  FaDownload,
8
6
  FaExternalLinkAlt,
9
7
  FaGithub,
10
- FaLayerGroup,
11
8
  FaLinkedin,
12
- FaMapMarkerAlt,
13
- FaMedium,
14
- FaMobileAlt,
15
9
  FaSearch,
16
- FaServer,
17
10
  } from "react-icons/fa";
18
- import ITBadget from "../components/atoms/badget/badget";
19
- import ITButton from "../components/atoms/button/button";
20
- import ITDivider from "../components/atoms/divider/divider";
21
11
  import ITFlex from "../components/atoms/flex/flex";
22
12
  import ITGrid from "../components/atoms/grid/grid";
23
- import ITStack from "../components/atoms/stack/stack";
24
13
  import ITText from "../components/atoms/text/text";
25
14
  import { techIcon, type TechIconEntry } from "./techIcons";
26
15
  import { PRODUCTOS, type Producto } from "./productos";
27
- import ProductoDetalle from "./ProductoDetalle";
28
16
  import ProductoIcono from "./ProductoIcono";
17
+ import {
18
+ BarraProgreso,
19
+ CARD,
20
+ ControlesPortafolio,
21
+ CARD_HOVER,
22
+ CHIP,
23
+ EYEBROW,
24
+ EnlaceBoton,
25
+ Etiqueta,
26
+ SaltarContenido,
27
+ VolverArriba,
28
+ } from "./portfolioUi";
29
+ import { useITTranslation } from "@/i18n";
29
30
 
30
31
  const CV_URL = "/Martin_Amaro_CV_ES_2026.pdf";
31
32
  const GITHUB_URL = "https://github.com/maag070208";
32
33
  const LINKEDIN_URL = "https://www.linkedin.com/in/maag070208/";
33
34
  const MEDIUM_URL = "https://medium.com/@axzydev";
35
+ const SANDBOX_URL = "#ui-system";
36
+
37
+ /** Producto que ocupa el bloque destacado del portafolio. */
38
+ const DESTACADO = "checkapp";
39
+
40
+ /** Ancho de la hoja: una sola columna de lectura, sin cajas flotantes. */
41
+ const HOJA = "mx-auto w-full max-w-[1180px] px-5 sm:px-8 lg:px-10";
42
+
43
+ /** Anchor ids of the portfolio sections, in document order (drives the scroll-spy). */
44
+ const SECTION_IDS = ["sobre-mi", "stack", "proyectos", "contacto"] as const;
45
+
46
+ /** Top navigation entries: anchor id → i18n key. El orden es el número de sección. */
47
+ const NAV_ITEMS: { id: (typeof SECTION_IDS)[number]; key: string }[] = [
48
+ { id: "sobre-mi", key: "showcase.home.navAbout" },
49
+ { id: "stack", key: "showcase.home.navStack" },
50
+ { id: "proyectos", key: "showcase.home.navProjects" },
51
+ { id: "contacto", key: "showcase.home.navContact" },
52
+ ];
53
+
54
+ /** Cabecera sticky: en móvil ocupa dos filas, así que el ancla deja más aire. */
55
+ const ANCLA = "scroll-mt-[6.75rem] md:scroll-mt-[4.5rem]";
56
+
57
+ /** Shared stagger offset for the hero entrance cascade (`it-reveal`). */
58
+ const reveal = (delay: number): CSSProperties =>
59
+ ({ "--it-delay": `${delay}ms` }) as CSSProperties;
60
+
61
+ /** Filete de 1px: la separación entre bloques (no hay tarjetas contenedoras). */
62
+ const FILETE = "border-slate-200 dark:border-slate-800";
63
+
64
+ /** Índice de sección en mono: `01`, `02`… */
65
+ const numero = (index: number): string => String(index + 1).padStart(2, "0");
34
66
 
35
67
  /**
36
- * Degradados literales para los íconos de las métricas. Tailwind v4 no ve las
37
- * clases construidas en runtime, así que van escritas completas y se eligen por
38
- * posición.
68
+ * Tracks which anchored section is currently in view (scroll-spy) so the top
69
+ * navigation can highlight the active link.
39
70
  */
40
- const ACCENTS = [
41
- "from-primary-500 to-info-500",
42
- "from-purple-500 to-primary-500",
43
- "from-primary-600 to-purple-600",
44
- "from-info-500 to-purple-500",
45
- ];
71
+ function useActiveSection(ids: readonly string[]): string {
72
+ const [active, setActive] = useState(ids[0]);
73
+ useEffect(() => {
74
+ const elements = ids
75
+ .map((id) => document.getElementById(id))
76
+ .filter((el): el is HTMLElement => Boolean(el));
77
+ if (elements.length === 0) return;
78
+
79
+ const observer = new IntersectionObserver(
80
+ (entries) => {
81
+ const visible = entries
82
+ .filter((entry) => entry.isIntersecting)
83
+ .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
84
+ if (visible[0]) setActive(visible[0].target.id);
85
+ },
86
+ { rootMargin: "-35% 0px -55% 0px", threshold: 0 }
87
+ );
88
+
89
+ elements.forEach((el) => observer.observe(el));
90
+ return () => observer.disconnect();
91
+ }, [ids]);
92
+ return active;
93
+ }
46
94
 
47
95
  /**
48
- * Encabezado de sección: eyebrow en mayúsculas, título y subtítulo opcional.
49
- * Mismo patrón que el landing del sistema (`#ui-system`).
96
+ * Cabecera: marca, índice de secciones con scroll-spy, progreso de lectura y el
97
+ * único punto de entrada al sandbox de componentes (`#ui-system`).
98
+ */
99
+ const PortfolioNav = () => {
100
+ const { t } = useITTranslation();
101
+ const active = useActiveSection(SECTION_IDS);
102
+
103
+ const linkClass = (id: string) =>
104
+ `inline-flex items-baseline gap-1.5 whitespace-nowrap rounded-[6px] px-2.5 py-1.5 font-mono !text-[11px] uppercase !tracking-[0.14em] transition-colors ${
105
+ active === id
106
+ ? "text-primary-700 dark:text-primary-500"
107
+ : "text-slate-500 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
108
+ }`;
109
+
110
+ const enlaces = NAV_ITEMS.map((item, index) => (
111
+ <li key={item.id}>
112
+ <a
113
+ href={`#${item.id}`}
114
+ aria-current={active === item.id ? "location" : undefined}
115
+ className={linkClass(item.id)}
116
+ >
117
+ <span className="tabular-nums">{numero(index)}</span>
118
+ {t(item.key)}
119
+ </a>
120
+ </li>
121
+ ));
122
+
123
+ return (
124
+ <header className="sticky top-0 z-[100] border-b border-slate-200 bg-white/85 backdrop-blur-md dark:border-slate-800 dark:bg-[#0b0d10]/85">
125
+ <nav aria-label={t("showcase.home.sectionNav")} className={HOJA}>
126
+ <div className="flex h-14 items-center gap-4">
127
+ <a href="#sobre-mi" className="flex items-center gap-2.5">
128
+ <span className="grid h-6 w-6 place-items-center rounded-[6px] bg-primary-700 font-mono !text-[10px] !font-bold text-white">
129
+ AA
130
+ </span>
131
+ <ITText
132
+ as="span"
133
+ className="hidden font-display !text-[13px] !font-bold -tracking-[0.01em] text-slate-900 dark:text-white sm:block"
134
+ >
135
+ Asael Amaro
136
+ </ITText>
137
+ </a>
138
+
139
+ <ul className="ml-auto hidden items-center gap-1 md:flex">{enlaces}</ul>
140
+
141
+ <div className="ml-auto flex items-center gap-2 md:ml-3">
142
+ <ControlesPortafolio />
143
+ <EnlaceBoton
144
+ href={SANDBOX_URL}
145
+ variante="outlined"
146
+ tamano="sm"
147
+ icono={<FaArrowRight size={10} />}
148
+ >
149
+ {t("showcase.home.navUiSystem")}
150
+ </EnlaceBoton>
151
+ </div>
152
+ </div>
153
+
154
+ {/* Móvil: el mismo índice en una fila desplazable */}
155
+ <ul className="flex items-center gap-1 overflow-x-auto pb-2 md:hidden">{enlaces}</ul>
156
+ </nav>
157
+
158
+ <BarraProgreso />
159
+ </header>
160
+ );
161
+ };
162
+
163
+ /**
164
+ * Encabezado de sección: número mono + título display + apoyo, con acciones a la
165
+ * derecha. El bloque lo separa el filete superior de cada sección.
50
166
  */
51
167
  const SectionHeading = ({
52
- eyebrow,
168
+ index,
169
+ label,
53
170
  title,
54
171
  subtitle,
55
- className = "",
172
+ actions,
56
173
  }: {
57
- /** Texto chico en mayúsculas sobre el título. */
58
- eyebrow: string;
59
- /** Título de la sección. */
174
+ /** Índice de sección (0-based: se pinta con dos dígitos). */
175
+ index: number;
176
+ /** Etiqueta mono corta. */
177
+ label: string;
178
+ /** Título display. */
60
179
  title: string;
61
- /** Frase de apoyo debajo del título. */
180
+ /** Frase de apoyo. */
62
181
  subtitle?: string;
63
- /** Clases extra del contenedor (márgenes). */
64
- className?: string;
182
+ /** Contenido alineado a la derecha (filtros). */
183
+ actions?: ReactNode;
65
184
  }) => (
66
- <ITStack spacing={1} className={className}>
67
- <ITText as="p" className="!text-[11px] !font-bold uppercase !tracking-wider text-primary-600 dark:text-primary-400">
68
- {eyebrow}
69
- </ITText>
70
- <ITText as="h2" className="!text-xl !font-bold !tracking-tight text-slate-900 dark:text-white">
71
- {title}
72
- </ITText>
73
- {subtitle && (
74
- <ITText as="p" className="!text-sm text-slate-500 dark:text-slate-400">
75
- {subtitle}
185
+ <div className="flex flex-col gap-5 pb-7 md:flex-row md:items-end md:justify-between">
186
+ <div className="min-w-0">
187
+ <ITText as="p" className={EYEBROW}>
188
+ <span className="tabular-nums">{numero(index)}</span>
189
+ {` — ${label}`}
76
190
  </ITText>
77
- )}
78
- </ITStack>
191
+ <ITText
192
+ as="h2"
193
+ className="font-display !mt-2.5 !text-[26px] !font-bold -tracking-[0.02em] text-slate-900 dark:text-white sm:!text-[32px]"
194
+ >
195
+ {title}
196
+ </ITText>
197
+ {subtitle && (
198
+ <ITText
199
+ as="p"
200
+ className="!mt-2 max-w-[62ch] !text-[15px] !leading-relaxed text-slate-500 dark:text-slate-400"
201
+ >
202
+ {subtitle}
203
+ </ITText>
204
+ )}
205
+ </div>
206
+ {actions && <div className="shrink-0">{actions}</div>}
207
+ </div>
79
208
  );
80
209
 
81
210
  /**
82
- * Ícono de tecnología local, invertido en modo oscuro cuando la marca es
83
- * monocromática (GitHub, Express).
211
+ * Ícono de tecnología local. Es decorativo (el nombre va al lado), así que se
212
+ * oculta a lectores de pantalla y solo conserva el `title` nativo.
84
213
  */
85
- const TechIconImg = ({
86
- icon,
87
- alt,
88
- size = "w-7 h-7",
89
- }: {
90
- /** Ícono resuelto por `techIcons`. */
91
- icon: TechIconEntry;
92
- /** Texto alternativo. */
93
- alt: string;
94
- /** Clases de tamaño. @default "w-7 h-7" */
95
- size?: string;
96
- }) => (
214
+ const TechIconImg = ({ icon, alt }: { icon: TechIconEntry; alt: string }) => (
97
215
  <img
98
216
  src={icon.src}
99
- alt={alt}
100
- className={`${size} object-contain ${icon.mono ? "dark:invert" : ""}`}
217
+ alt=""
218
+ aria-hidden
219
+ title={alt}
220
+ className={`h-4 w-4 object-contain ${icon.mono ? "dark:invert" : ""}`}
101
221
  loading="lazy"
102
222
  />
103
223
  );
104
224
 
105
- /** Tarjeta de una tecnología: ícono en caja blanca + nombre. */
106
- const TechTile = ({ icon, label }: { icon: TechIconEntry; label: string }) => (
107
- <div className="group flex h-full flex-col items-center justify-center gap-2 rounded-xl border border-slate-100 bg-slate-50/70 p-3 transition-all duration-200 hover:-translate-y-0.5 hover:border-primary-300 hover:bg-white hover:shadow-md dark:border-slate-800 dark:bg-slate-950/40 dark:hover:border-primary-700 dark:hover:bg-slate-900">
108
- <div className="flex h-11 w-11 items-center justify-center rounded-lg bg-white shadow-sm dark:bg-slate-900">
109
- <TechIconImg icon={icon} alt={label} size="w-7 h-7" />
110
- </div>
111
- <ITText as="span" className="w-full truncate text-center text-[11px] font-semibold text-slate-600 dark:text-slate-300">
112
- {label}
113
- </ITText>
114
- </div>
115
- );
116
-
117
- /** Métrica del hero: valor grande, ícono con degradado y texto de apoyo. */
118
- const StatCard = ({ stat, index }: { stat: { label: string; value: string; hint: string; icon: ReactNode }; index: number }) => (
119
- <div className="flex h-full flex-col gap-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition-all duration-300 hover:-translate-y-0.5 hover:border-primary-300 hover:shadow-lg dark:border-slate-800 dark:bg-slate-900 dark:hover:border-primary-700">
120
- <div className="flex items-start justify-between gap-3">
121
- <ITText as="p" className="!text-3xl !font-extrabold !leading-none !tracking-tight tabular-nums text-slate-900 dark:text-white">
122
- {stat.value}
123
- </ITText>
124
- <span className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-[13px] text-white shadow-sm ${ACCENTS[index % ACCENTS.length]}`}>
125
- {stat.icon}
126
- </span>
127
- </div>
128
-
129
- <div className="mt-auto">
130
- <ITText as="p" className="!text-sm !font-semibold text-primary-600 dark:text-primary-400">
131
- {stat.label}
132
- </ITText>
133
- <ITText as="p" className="!mt-0.5 !text-[11px] text-slate-400 dark:text-slate-500">
134
- {stat.hint}
135
- </ITText>
136
- </div>
137
- </div>
138
- );
139
-
140
225
  /**
141
- * Tarjeta de producto, compacta: portada (captura real o degradado), nombre,
142
- * descripción corta, stack y acceso al detalle.
226
+ * Tarjeta de producto de la rejilla: portada, índice + tipo, nombre, descripción
227
+ * acotada, stack y acciones.
143
228
  */
144
- const TarjetaProducto = ({
145
- producto,
146
- onVerDetalle,
147
- }: {
148
- producto: Producto;
149
- onVerDetalle: (producto: Producto) => void;
150
- }) => {
229
+ const TarjetaProducto = ({ producto, index }: { producto: Producto; index: number }) => {
230
+ const { t } = useITTranslation();
151
231
  const capturas = producto.capturas ?? [];
152
232
  const portada = capturas[0];
153
233
  const tieneDetalle = capturas.length > 0;
154
234
  const enlaceInterno = producto.enlace?.startsWith("#");
235
+ const stackVisible = producto.stack.slice(0, 3);
236
+ const stackResto = producto.stack.length - stackVisible.length;
155
237
 
156
238
  return (
157
- <div className="group flex h-full flex-col gap-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-primary-300 hover:shadow-xl dark:border-slate-800 dark:bg-slate-900 dark:hover:border-primary-700">
158
- {/* Portada */}
159
- <button
160
- type="button"
161
- disabled={!tieneDetalle}
162
- onClick={() => onVerDetalle(producto)}
163
- aria-label={tieneDetalle ? `Ver detalle de ${producto.nombre}` : producto.nombre}
164
- className="relative block aspect-[16/10] w-full overflow-hidden rounded-xl border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-950/60"
165
- >
239
+ <article className={`group flex h-full flex-col ${CARD} ${CARD_HOVER}`}>
240
+ <div className="relative flex aspect-[16/10] w-full items-center justify-center overflow-hidden border-b border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-950">
166
241
  {portada ? (
167
- <img
168
- src={portada.src}
169
- alt={`${producto.nombre}: ${portada.titulo}`}
170
- className="h-full w-full object-cover object-top transition-transform duration-300 group-hover:scale-[1.02]"
171
- loading="lazy"
172
- />
242
+ <a
243
+ href={`#proyecto/${producto.id}`}
244
+ aria-label={t("showcase.home.viewDetailOf", { name: producto.nombre })}
245
+ className="absolute inset-0"
246
+ >
247
+ <img
248
+ src={portada.src}
249
+ alt={`${producto.nombre}: ${portada.titulo}`}
250
+ className="h-full w-full object-cover object-top transition-transform duration-500 group-hover:scale-[1.02]"
251
+ loading="lazy"
252
+ />
253
+ </a>
173
254
  ) : (
174
- <div className={`flex h-full w-full items-center justify-center bg-gradient-to-br ${producto.gradiente} text-white`}>
175
- <ProductoIcono icono={producto.icono} size={44} />
176
- </div>
177
- )}
178
-
179
- {capturas.length > 1 && (
180
- <span className="absolute bottom-2 right-2 rounded-full bg-slate-900/70 px-2.5 py-1 text-[10px] font-semibold text-white backdrop-blur-sm">
181
- {capturas.length} capturas
255
+ <span className="flex flex-col items-center gap-2 text-primary-700 dark:text-primary-500">
256
+ <ProductoIcono icono={producto.icono} size={30} />
257
+ <ITText as="span" className={EYEBROW}>
258
+ {producto.tipo}
259
+ </ITText>
182
260
  </span>
183
261
  )}
184
- </button>
262
+ </div>
185
263
 
186
- {/* Encabezado */}
187
- <ITFlex align="center" justify="between" gap={3}>
188
- <ITFlex align="center" gap={2.5}>
189
- <div className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br ${producto.gradiente} text-white shadow-sm`}>
190
- <ProductoIcono icono={producto.icono} size={16} />
191
- </div>
192
- <ITText as="h3" className="!text-base !font-bold !tracking-tight text-slate-900 dark:text-white">
193
- {producto.nombre}
264
+ <div className="flex flex-1 flex-col p-5">
265
+ <div className="flex items-center justify-between gap-3">
266
+ <ITText as="span" className={`${EYEBROW} tabular-nums`}>
267
+ {numero(index)}
194
268
  </ITText>
195
- </ITFlex>
196
- <ITBadget label={producto.tipo} color="primary" variant="outlined" />
197
- </ITFlex>
269
+ <Etiqueta>{producto.tipo}</Etiqueta>
270
+ </div>
198
271
 
199
- <ITText as="p" className="!text-sm !leading-relaxed text-slate-600 dark:text-slate-300">
200
- {producto.descripcion}
201
- </ITText>
272
+ <ITText
273
+ as="h3"
274
+ className="font-display !mt-3 !text-[19px] !font-bold -tracking-[0.02em] text-slate-900 dark:text-white"
275
+ >
276
+ {producto.nombre}
277
+ </ITText>
202
278
 
203
- <ITFlex gap={1.5} wrap="wrap">
204
- {producto.stack.map((tecnologia) => (
205
- <span
206
- key={tecnologia}
207
- className="rounded-md bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-500 dark:bg-slate-800 dark:text-slate-400"
208
- >
209
- {tecnologia}
210
- </span>
211
- ))}
212
- </ITFlex>
213
-
214
- {/* Acciones */}
215
- <ITFlex align="center" justify="between" gap={3} wrap="wrap" className="mt-auto pt-2">
216
- {tieneDetalle ? (
217
- <ITButton variant="filled" color="primary" size="sm" onClick={() => onVerDetalle(producto)}>
218
- Ver detalle
219
- </ITButton>
220
- ) : (
221
- <ITText as="span" />
222
- )}
279
+ <ITText
280
+ as="p"
281
+ className="line-clamp-3 !mt-2 !text-[13.5px] !leading-relaxed text-slate-600 dark:text-slate-300"
282
+ >
283
+ {producto.descripcion}
284
+ </ITText>
223
285
 
224
- {producto.enlace && (
225
- <a
226
- href={producto.enlace}
227
- {...(enlaceInterno ? {} : { target: "_blank", rel: "noopener noreferrer" })}
228
- className="!text-xs !font-semibold !text-primary-600 dark:!text-primary-400 inline-flex items-center gap-1.5"
229
- >
230
- {!enlaceInterno && <FaExternalLinkAlt size={11} />}
231
- {producto.enlaceTexto ?? "Ver el repositorio"}
232
- </a>
233
- )}
234
- </ITFlex>
235
- </div>
286
+ <ITFlex gap={1.5} wrap="wrap" className="!mt-4">
287
+ {stackVisible.map((tecnologia) => (
288
+ <span key={tecnologia} className={CHIP}>
289
+ {tecnologia}
290
+ </span>
291
+ ))}
292
+ {stackResto > 0 && (
293
+ <span className={CHIP}>{t("showcase.home.moreStack", { count: stackResto })}</span>
294
+ )}
295
+ </ITFlex>
296
+
297
+ <div className="mt-auto flex items-center justify-between gap-3 pt-5">
298
+ {tieneDetalle ? (
299
+ <EnlaceBoton href={`#proyecto/${producto.id}`} tamano="sm">
300
+ {t("showcase.home.viewDetail")}
301
+ </EnlaceBoton>
302
+ ) : (
303
+ <ITText as="span" className={EYEBROW}>
304
+ {t("showcase.home.noPreview")}
305
+ </ITText>
306
+ )}
307
+
308
+ {producto.enlace && (
309
+ <a
310
+ href={producto.enlace}
311
+ {...(enlaceInterno ? {} : { target: "_blank", rel: "noopener noreferrer" })}
312
+ className="inline-flex items-center gap-1.5 !text-xs !font-semibold !text-primary-700 dark:!text-primary-500"
313
+ >
314
+ {!enlaceInterno && <FaExternalLinkAlt size={10} />}
315
+ {producto.enlaceTexto ?? t("showcase.home.viewRepo")}
316
+ </a>
317
+ )}
318
+ </div>
319
+ </div>
320
+ </article>
236
321
  );
237
322
  };
238
323
 
324
+ /**
325
+ * Portafolio (`/`): hoja técnica con masthead tipográfico, banda de métricas,
326
+ * stack en formato datasheet y productos (uno destacado + rejilla).
327
+ *
328
+ * No recibe props: cada tarjeta enlaza a `#proyecto/<id>` con un `<a>` real, así
329
+ * que el router por hash hace la navegación (y el detalle es compartible).
330
+ */
239
331
  export const HomeShowcase = () => {
332
+ const { t } = useITTranslation();
240
333
  const [techSearch, setTechSearch] = useState("");
241
- const [selectedCategory, setSelectedCategory] = useState("all");
242
- const [detalle, setDetalle] = useState<Producto | null>(null);
243
-
244
- const LANGUAGES = [
245
- { name: "TypeScript", icon: techIcon("typescript") },
246
- { name: "JavaScript", icon: techIcon("javascript") },
247
- { name: "C#", icon: techIcon("csharp") },
248
- { name: "Python", icon: techIcon("python") },
249
- { name: "Kotlin", icon: techIcon("kotlin") },
250
- { name: "Dart", icon: techIcon("dart") },
251
- ];
252
334
 
253
- const FRAMEWORKS = [
254
- { name: "Angular", icon: techIcon("angular") },
255
- { name: "React", icon: techIcon("react") },
256
- { name: "Ionic", icon: techIcon("ionic") },
257
- { name: ".NET", icon: techIcon("dotnet") },
258
- { name: "Node.js", icon: techIcon("nodejs") },
259
- { name: "Flutter", icon: techIcon("flutter") },
260
- { name: "Express", icon: techIcon("express") },
261
- { name: "Electron", icon: techIcon("electron") },
262
- ];
263
-
264
- const TECH_STACK = [
265
- { name: "TypeScript", icon: techIcon("typescript"), category: "frontend" },
266
- { name: "JavaScript", icon: techIcon("javascript"), category: "frontend" },
335
+ /** Stack completo: única fuente de verdad (el datasheet lo agrupa por categoría). */
336
+ const TECH_STACK: { name: string; icon: TechIconEntry; category: string }[] = [
337
+ { name: "TypeScript", icon: techIcon("typescript"), category: "languages" },
338
+ { name: "JavaScript", icon: techIcon("javascript"), category: "languages" },
339
+ { name: "C#", icon: techIcon("csharp"), category: "languages" },
340
+ { name: "Python", icon: techIcon("python"), category: "languages" },
341
+ { name: "Kotlin", icon: techIcon("kotlin"), category: "languages" },
342
+ { name: "Dart", icon: techIcon("dart"), category: "languages" },
267
343
  { name: "Angular", icon: techIcon("angular"), category: "frontend" },
268
344
  { name: "React", icon: techIcon("react"), category: "frontend" },
269
345
  { name: "Redux", icon: techIcon("redux"), category: "frontend" },
270
346
  { name: "HTML5", icon: techIcon("html5"), category: "frontend" },
271
347
  { name: "CSS3", icon: techIcon("css3"), category: "frontend" },
272
348
  { name: "Sass", icon: techIcon("sass"), category: "frontend" },
273
- { name: "C#", icon: techIcon("csharp"), category: "backend" },
274
- { name: "Python", icon: techIcon("python"), category: "backend" },
275
349
  { name: "Node.js", icon: techIcon("nodejs"), category: "backend" },
276
350
  { name: "Express", icon: techIcon("express"), category: "backend" },
277
351
  { name: ".NET Core", icon: techIcon("dotnet"), category: "backend" },
352
+ { name: "Spring Boot", icon: techIcon("springboot"), category: "backend" },
278
353
  { name: "MongoDB", icon: techIcon("mongodb"), category: "database" },
279
354
  { name: "MySQL", icon: techIcon("mysql"), category: "database" },
355
+ { name: "PostgreSQL", icon: techIcon("postgresql"), category: "database" },
280
356
  { name: "Flutter", icon: techIcon("flutter"), category: "mobile" },
281
- { name: "Kotlin", icon: techIcon("kotlin"), category: "mobile" },
282
357
  { name: "Android", icon: techIcon("android"), category: "mobile" },
283
358
  { name: "Ionic", icon: techIcon("ionic"), category: "mobile" },
284
359
  { name: "Docker", icon: techIcon("docker"), category: "devops" },
@@ -289,329 +364,450 @@ export const HomeShowcase = () => {
289
364
  { name: "Electron", icon: techIcon("electron"), category: "desktop" },
290
365
  { name: "Visual Studio", icon: techIcon("visualstudio"), category: "tools" },
291
366
  { name: "Jest", icon: techIcon("jest"), category: "testing" },
367
+ { name: "Appium", icon: techIcon("appium"), category: "testing" },
292
368
  ];
293
369
 
294
- const STATS = [
295
- { label: "Experiencia", value: "5+", hint: "años como fullstack", icon: <FaCode /> },
296
- { label: "Tecnologías", value: `${TECH_STACK.length}`, hint: "en el stack", icon: <FaLayerGroup /> },
297
- { label: "Productos", value: `${PRODUCTOS.length}`, hint: "propios y en uso", icon: <FaBoxOpen /> },
298
- { label: "Modalidad", value: "Remoto", hint: "disponible ya", icon: <FaMapMarkerAlt /> },
370
+ /** Orden y etiqueta i18n de cada fila del datasheet. */
371
+ const FILAS: { category: string; key: string }[] = [
372
+ { category: "languages", key: "showcase.home.catLanguages" },
373
+ { category: "frontend", key: "showcase.home.catFrontend" },
374
+ { category: "backend", key: "showcase.home.catBackend" },
375
+ { category: "database", key: "showcase.home.catDatabase" },
376
+ { category: "mobile", key: "showcase.home.catMobile" },
377
+ { category: "devops", key: "showcase.home.catDevops" },
378
+ { category: "desktop", key: "showcase.home.catDesktop" },
379
+ { category: "tools", key: "showcase.home.catTools" },
380
+ { category: "testing", key: "showcase.home.catTesting" },
299
381
  ];
300
382
 
301
- const categoriesList = [
302
- { id: "all", name: "Todos", icon: <FaLayerGroup size={10} /> },
303
- { id: "frontend", name: "Frontend", icon: <FaCode size={10} /> },
304
- { id: "backend", name: "Backend", icon: <FaServer size={10} /> },
305
- { id: "mobile", name: "Móvil", icon: <FaMobileAlt size={10} /> },
306
- { id: "database", name: "Bases de Datos", icon: <FaDatabase size={10} /> },
307
- { id: "devops", name: "DevOps", icon: <FaCloud size={10} /> },
383
+ const termino = techSearch.trim().toLowerCase();
384
+ const filas = FILAS.map((fila) => ({
385
+ ...fila,
386
+ items: TECH_STACK.filter(
387
+ (tech) => tech.category === fila.category && tech.name.toLowerCase().includes(termino)
388
+ ),
389
+ })).filter((fila) => fila.items.length > 0);
390
+ const visibles = filas.reduce((total, fila) => total + fila.items.length, 0);
391
+
392
+ /** Métricas del perfil: una banda de datos con filetes, no cuatro tarjetas. */
393
+ const METRICAS = [
394
+ { label: t("showcase.home.statExperience"), value: "5+" },
395
+ { label: t("showcase.home.statProducts"), value: `${PRODUCTOS.length}` },
396
+ { label: t("showcase.home.statTech"), value: `${TECH_STACK.length}` },
397
+ { label: t("showcase.home.statMode"), value: t("showcase.home.statModeValue") },
308
398
  ];
309
399
 
310
- const filteredTech = TECH_STACK.filter((tech) => {
311
- const matchesSearch = tech.name.toLowerCase().includes(techSearch.toLowerCase());
312
- const matchesCategory = selectedCategory === "all" || tech.category === selectedCategory;
313
- return matchesSearch && matchesCategory;
314
- });
400
+ const destacado = PRODUCTOS.find((producto) => producto.id === DESTACADO) ?? PRODUCTOS[0];
401
+ const portadaDestacada = destacado.capturas?.[0];
402
+ const resto = PRODUCTOS.filter((producto) => producto.id !== destacado.id);
403
+ const enlaceDestacadoExterno = Boolean(destacado.enlace && !destacado.enlace.startsWith("#"));
315
404
 
316
405
  return (
317
- <ITStack spacing={10} className="animate-fadeIn overflow-x-clip pb-5">
318
- {/* ─── HERO ─── */}
319
- <section
320
- aria-labelledby="home-hero-title"
321
- className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-primary-50 via-white to-purple-50 px-8 py-14 dark:from-primary-950/30 dark:via-slate-900 dark:to-purple-950/30 sm:px-14"
322
- >
323
- <div aria-hidden className="it-landing-float pointer-events-none absolute -top-24 -right-16 h-72 w-72 rounded-full bg-primary-400/20 blur-3xl" />
324
- <div
325
- aria-hidden
326
- className="it-landing-float pointer-events-none absolute -bottom-24 -left-16 h-72 w-72 rounded-full bg-purple-400/20 blur-3xl"
327
- style={{ animationDelay: "1.5s" }}
328
- />
329
- <div aria-hidden className="it-landing-grid pointer-events-none absolute inset-0 text-slate-400/50 dark:text-slate-600/40" />
330
-
331
- <div className="relative z-10 flex flex-col gap-12 xl:flex-row xl:items-center xl:gap-14">
332
- <div className="min-w-0 max-w-3xl flex-1 xl:max-w-2xl">
333
- <div className="it-landing-rise">
334
- <ITBadget label="Portafolio · 2026" color="primary" variant="outlined" className="w-fit" />
335
- </div>
336
-
337
- <ITText
338
- as="h1"
339
- id="home-hero-title"
340
- className="it-landing-rise !mt-5 !text-4xl !font-extrabold !tracking-tight text-slate-900 dark:text-white sm:!text-5xl"
341
- style={{ animationDelay: "70ms" }}
342
- >
343
- Hola, soy{" "}
344
- <span className="bg-gradient-to-r from-primary-600 via-primary-500 to-purple-500 bg-clip-text text-transparent">
345
- Asael Amaro
346
- </span>
347
- </ITText>
348
-
349
- <ITText
350
- as="p"
351
- className="it-landing-rise !mt-4 !text-base !leading-relaxed max-w-xl text-slate-600 dark:text-slate-300"
352
- style={{ animationDelay: "140ms" }}
353
- >
354
- Fullstack Developer con 4+ años creando aplicaciones web modernas.
355
- Especializado en TypeScript, Angular y React, con foco en arquitectura
356
- limpia y en productos que la gente usa todos los días.
357
- </ITText>
358
-
406
+ <>
407
+ <SaltarContenido />
408
+ <PortfolioNav />
409
+
410
+ <main id="contenido" className="animate-fadeIn">
411
+ {/* ─── 01 · MASTHEAD ─── */}
412
+ <section id="sobre-mi" aria-labelledby="home-hero-title" className={ANCLA}>
413
+ <div className={`${HOJA} pb-10 pt-10 sm:pb-12 sm:pt-14`}>
359
414
  <ITFlex
360
- gap={3}
361
- wrap="wrap"
362
415
  align="center"
363
- className="it-landing-rise !mt-7"
364
- style={{ animationDelay: "210ms" }}
365
- >
366
- <a href={CV_URL} download>
367
- <ITButton variant="filled" color="primary" size="lg" icon={<FaDownload size={13} />}>
368
- Descargar CV
369
- </ITButton>
370
- </a>
371
- <a href={GITHUB_URL} target="_blank" rel="noopener noreferrer">
372
- <ITButton variant="outlined" color="secondary" size="lg" icon={<FaGithub size={13} />}>
373
- GitHub
374
- </ITButton>
375
- </a>
376
- </ITFlex>
377
-
378
- <div
379
- className="it-landing-rise mt-7 inline-flex flex-wrap items-center gap-x-3 gap-y-2 rounded-2xl border border-primary-200 bg-gradient-to-r from-primary-50 to-purple-50 px-4 py-2 shadow-sm dark:border-primary-800/50 dark:from-primary-950/30 dark:to-purple-950/30"
380
- style={{ animationDelay: "280ms" }}
416
+ justify="between"
417
+ gap={4}
418
+ wrap="wrap"
419
+ className="it-reveal border-b border-slate-200 pb-4 dark:border-slate-800"
381
420
  >
382
- <span className="relative flex h-2 w-2 shrink-0">
383
- <span className="absolute inline-flex h-full w-full rounded-full bg-success-400 opacity-75" />
384
- <span className="relative inline-flex h-2 w-2 rounded-full bg-success-500" />
421
+ <span className="inline-flex items-center gap-2.5">
422
+ <span className="relative flex h-1.5 w-1.5 shrink-0">
423
+ <span className="absolute inline-flex h-full w-full rounded-full bg-success-500 opacity-75" />
424
+ <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-success-500" />
425
+ </span>
426
+ <ITText
427
+ as="span"
428
+ className="font-mono !text-[11px] uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400"
429
+ >
430
+ {`${t("showcase.home.availableFullstack")} · ${t("showcase.home.location")}`}
431
+ </ITText>
385
432
  </span>
386
- <code className="text-sm font-medium text-slate-800 dark:text-slate-200">
387
- Disponible para proyectos fullstack
388
- </code>
389
433
  <a
390
434
  href={LINKEDIN_URL}
391
435
  target="_blank"
392
436
  rel="noopener noreferrer"
393
- className="inline-flex items-center gap-1.5 text-xs font-semibold text-primary-600 dark:text-primary-400"
437
+ className="inline-flex items-center gap-1.5 font-mono !text-[11px] uppercase !tracking-[0.14em] !text-primary-700 dark:!text-primary-500"
394
438
  >
395
- <FaLinkedin size={12} />
439
+ <FaLinkedin size={11} />
396
440
  LinkedIn
397
441
  </a>
398
- </div>
399
- </div>
442
+ </ITFlex>
400
443
 
401
- {/* Foto en tarjeta flotante */}
402
- <div className="flex w-full shrink-0 justify-center xl:w-[300px] xl:justify-end">
403
- <div
404
- className="it-landing-float w-full max-w-[300px] overflow-hidden rounded-2xl border border-slate-200/80 bg-white/85 shadow-xl backdrop-blur-sm dark:border-slate-700/70 dark:bg-slate-900/85"
405
- style={{ animationDelay: "0.8s" }}
406
- >
407
- <img
408
- src="/personalFoto.jpg"
409
- alt="Asael Amaro"
410
- className="aspect-[4/5] w-full object-cover"
411
- />
412
- <div className="flex items-center justify-between gap-3 border-t border-slate-200/80 px-4 py-3.5 dark:border-slate-700/70">
413
- <div>
414
- <ITText as="p" className="!text-xs !font-bold text-slate-900 dark:text-white">
415
- Fullstack Developer
444
+ <div className="mt-10 grid gap-10 lg:grid-cols-[minmax(0,1fr)_auto] lg:gap-14">
445
+ <div className="min-w-0">
446
+ <ITText
447
+ as="h1"
448
+ id="home-hero-title"
449
+ className="it-reveal font-display !text-[38px] !font-bold !leading-[1.06] -tracking-[0.015em] text-slate-900 dark:text-white sm:!text-[54px] lg:!text-[62px]"
450
+ style={reveal(60)}
451
+ >
452
+ {t("showcase.home.heroLead")}{" "}
453
+ <span className="text-primary-600 dark:text-primary-400">
454
+ {t("showcase.home.heroAccent")}
455
+ </span>
456
+ </ITText>
457
+
458
+ <ITText
459
+ as="p"
460
+ className="it-reveal !mt-6 max-w-[62ch] !text-[15.5px] !leading-[1.75] text-slate-600 dark:text-slate-300 sm:!text-[17px]"
461
+ style={reveal(130)}
462
+ >
463
+ {t("showcase.home.bio")}
464
+ </ITText>
465
+
466
+ <ITFlex
467
+ gap={3}
468
+ wrap="wrap"
469
+ align="center"
470
+ className="it-reveal !mt-8"
471
+ style={reveal(200)}
472
+ >
473
+ <EnlaceBoton href={CV_URL} descargar icono={<FaDownload size={12} />}>
474
+ {t("showcase.home.downloadCv")}
475
+ </EnlaceBoton>
476
+ <EnlaceBoton href={GITHUB_URL} variante="text" icono={<FaGithub size={13} />}>
477
+ GitHub
478
+ </EnlaceBoton>
479
+ <EnlaceBoton href={SANDBOX_URL} variante="text" icono={<FaArrowRight size={12} />}>
480
+ {t("showcase.home.navUiSystem")}
481
+ </EnlaceBoton>
482
+ </ITFlex>
483
+ </div>
484
+
485
+ {/* Retrato como documento: chico y con filete, sin tarjeta contenedora */}
486
+ <div
487
+ className="it-reveal flex items-start gap-4 lg:flex-col lg:gap-3 lg:self-end"
488
+ style={reveal(260)}
489
+ >
490
+ <img
491
+ src="/personalFoto.jpg"
492
+ alt="Asael Amaro"
493
+ className="h-[150px] w-[120px] rounded-[10px] border border-slate-200 object-cover dark:border-slate-800 lg:h-[210px] lg:w-[168px]"
494
+ />
495
+ <div className="pt-0.5">
496
+ <ITText
497
+ as="p"
498
+ className="font-mono !text-[10px] uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400"
499
+ >
500
+ {t("showcase.home.role")}
416
501
  </ITText>
417
- <ITText as="p" className="!text-[11px] text-slate-400 dark:text-slate-500">
418
- Remoto · México
502
+ <ITText as="p" className="!mt-1 !text-[11px] text-slate-500 dark:text-slate-400">
503
+ {t("showcase.home.location")}
419
504
  </ITText>
420
505
  </div>
421
- <span className="inline-flex shrink-0 items-center gap-1.5 rounded-full border border-success-500/40 px-2.5 py-1 text-[10px] font-semibold text-success-600 dark:text-success-400">
422
- <span className="h-1.5 w-1.5 rounded-full bg-success-500" />
423
- Disponible
424
- </span>
425
506
  </div>
426
507
  </div>
427
508
  </div>
428
- </div>
429
- </section>
430
-
431
- {/* ─── MÉTRICAS ─── */}
432
- <section aria-label="Métricas del perfil">
433
- <ITGrid container spacing={4}>
434
- {STATS.map((stat, index) => (
435
- <ITGrid item xs={6} lg={3} key={stat.label}>
436
- <StatCard stat={stat} index={index} />
437
- </ITGrid>
438
- ))}
439
- </ITGrid>
440
- </section>
441
-
442
- {/* ─── LENGUAJES Y FRAMEWORKS ─── */}
443
- <section>
444
- <SectionHeading
445
- eyebrow="Base técnica"
446
- title="Lenguajes y frameworks"
447
- subtitle="Con los que trabajo todos los días."
448
- className="!mb-5"
449
- />
450
-
451
- <ITGrid container spacing={5}>
452
- <ITGrid item xs={12} lg={6}>
453
- <div className="h-full rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
454
- <ITText as="h3" className="!text-[11px] !font-bold uppercase !tracking-wider text-slate-400 dark:text-slate-500">
455
- Lenguajes
456
- </ITText>
457
- <div className="mt-4 grid grid-cols-3 gap-3 sm:grid-cols-6 lg:grid-cols-3">
458
- {LANGUAGES.map((lang) => (
459
- <TechTile key={lang.name} icon={lang.icon} label={lang.name} />
460
- ))}
461
- </div>
462
- </div>
463
- </ITGrid>
464
509
 
465
- <ITGrid item xs={12} lg={6}>
466
- <div className="h-full rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
467
- <ITText as="h3" className="!text-[11px] !font-bold uppercase !tracking-wider text-slate-400 dark:text-slate-500">
468
- Frameworks
469
- </ITText>
470
- <div className="mt-4 grid grid-cols-3 gap-3 sm:grid-cols-4 lg:grid-cols-4">
471
- {FRAMEWORKS.map((fw) => (
472
- <TechTile key={fw.name} icon={fw.icon} label={fw.name} />
510
+ {/* Banda de métricas */}
511
+ <div className={`border-y ${FILETE}`}>
512
+ <dl
513
+ aria-label={t("showcase.home.ariaStats")}
514
+ className={`${HOJA} grid grid-cols-2 sm:grid-cols-4`}
515
+ >
516
+ {METRICAS.map((metrica, indice) => (
517
+ <div
518
+ key={metrica.label}
519
+ className={`py-5 ${indice > 0 ? "sm:border-l sm:pl-6 " : ""}${
520
+ indice % 2 === 1 ? "border-l pl-6 " : ""
521
+ }border-slate-200 dark:border-slate-800`}
522
+ >
523
+ <dt className={EYEBROW}>{metrica.label}</dt>
524
+ <dd className="mt-2 font-display text-[30px] font-bold leading-none tracking-[-0.02em] tabular-nums text-slate-900 dark:text-white">
525
+ {metrica.value}
526
+ </dd>
527
+ </div>
528
+ ))}
529
+ </dl>
530
+ </div>
531
+ </section>
532
+
533
+ {/* ─── 02 · STACK (datasheet) ─── */}
534
+ <section id="stack" className={ANCLA}>
535
+ <div className={`${HOJA} py-12 sm:py-16`}>
536
+ <SectionHeading
537
+ index={1}
538
+ label={t("showcase.home.eyebrowStack")}
539
+ title={t("showcase.home.titleStack")}
540
+ subtitle={t("showcase.home.subtitleStack")}
541
+ actions={
542
+ <div className="relative w-full sm:w-[260px]">
543
+ <FaSearch
544
+ className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
545
+ size={11}
546
+ />
547
+ <input
548
+ type="search"
549
+ placeholder={t("showcase.home.searchTech")}
550
+ aria-label={t("showcase.home.searchTechAria")}
551
+ value={techSearch}
552
+ onChange={(e) => setTechSearch(e.target.value)}
553
+ className="w-full rounded-[10px] border border-slate-200 bg-white py-2 pl-8 pr-3 font-mono text-xs text-slate-800 outline-none transition-colors focus:border-primary-600 dark:border-slate-800 dark:bg-slate-900 dark:text-white"
554
+ />
555
+ </div>
556
+ }
557
+ />
558
+
559
+ {filas.length > 0 ? (
560
+ <dl className={`border-t ${FILETE}`}>
561
+ {filas.map((fila) => (
562
+ <div
563
+ key={fila.category}
564
+ className={`grid gap-2 border-b py-4 sm:grid-cols-[150px_minmax(0,1fr)] sm:gap-6 ${FILETE}`}
565
+ >
566
+ <dt className="font-mono text-[11px] uppercase leading-6 tracking-[0.12em] text-slate-500 dark:text-slate-400">
567
+ {t(fila.key)}
568
+ </dt>
569
+ <dd className="flex flex-wrap items-center gap-x-5 gap-y-2.5">
570
+ {fila.items.map((tech) => (
571
+ <span
572
+ key={tech.name}
573
+ className="inline-flex items-center gap-2 text-[14px] text-slate-700 dark:text-slate-200"
574
+ >
575
+ <TechIconImg icon={tech.icon} alt={tech.name} />
576
+ {tech.name}
577
+ </span>
578
+ ))}
579
+ </dd>
580
+ </div>
473
581
  ))}
474
- </div>
475
- </div>
476
- </ITGrid>
477
- </ITGrid>
478
- </section>
479
-
480
- {/* ─── STACK TECNOLÓGICO ─── */}
481
- <section>
482
- <SectionHeading
483
- eyebrow="Stack"
484
- title="Tecnologías que uso"
485
- subtitle="Filtra por área o busca por nombre."
486
- className="!mb-5"
487
- />
488
-
489
- <ITFlex align="center" justify="between" wrap="wrap" gap={4} className="!mb-6">
490
- <ITFlex gap={2} wrap="wrap">
491
- {categoriesList.map((cat) => (
492
- <button
493
- key={cat.id}
494
- type="button"
495
- onClick={() => setSelectedCategory(cat.id)}
496
- className={`inline-flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-xs font-semibold transition-all ${
497
- selectedCategory === cat.id
498
- ? "bg-primary-500 text-white shadow-sm"
499
- : "bg-slate-100 text-slate-500 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
500
- }`}
582
+ </dl>
583
+ ) : (
584
+ <ITText
585
+ as="p"
586
+ className={`border-y py-10 text-center !text-sm text-slate-500 dark:text-slate-400 ${FILETE}`}
501
587
  >
502
- {cat.icon}
503
- {cat.name}
504
- </button>
505
- ))}
506
- </ITFlex>
507
-
508
- <div className="relative w-full sm:w-64">
509
- <FaSearch className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={12} />
510
- <input
511
- type="text"
512
- placeholder="Buscar tecnología..."
513
- value={techSearch}
514
- onChange={(e) => setTechSearch(e.target.value)}
515
- className="w-full rounded-xl border border-slate-200 bg-white py-2 pl-8 pr-4 text-xs text-slate-800 outline-none transition-all focus:border-primary-400 focus:ring-2 focus:ring-primary-500/30 dark:border-slate-700 dark:bg-slate-900 dark:text-white"
516
- />
517
- </div>
518
- </ITFlex>
588
+ {t("showcase.home.noTech", { term: techSearch })}
589
+ </ITText>
590
+ )}
519
591
 
520
- {filteredTech.length > 0 ? (
521
- <ITGrid container spacing={3}>
522
- {filteredTech.map((tech, index) => (
523
- <ITGrid item xs={4} sm={3} md={2} lg={2} key={`${tech.name}-${index}`}>
524
- <TechTile icon={tech.icon} label={tech.name} />
525
- </ITGrid>
526
- ))}
527
- </ITGrid>
528
- ) : (
529
- <div className="rounded-2xl border border-slate-200 bg-white py-12 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900">
530
- <ITText as="p" className="!text-sm text-slate-500 dark:text-slate-400">
531
- No se encontraron tecnologías para “{techSearch}”.
592
+ <ITText
593
+ as="p"
594
+ className="mt-4 font-mono !text-[11px] uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400"
595
+ >
596
+ {t("showcase.home.techCount", { shown: visibles, total: TECH_STACK.length })}
532
597
  </ITText>
533
598
  </div>
534
- )}
599
+ </section>
535
600
 
536
- <ITDivider className="!my-6" />
537
- <ITText as="p" className="!text-xs text-center text-slate-400 dark:text-slate-500">
538
- {filteredTech.length} de {TECH_STACK.length} tecnologías
539
- </ITText>
540
- </section>
541
-
542
- {/* ─── PRODUCTOS ─── */}
543
- <section>
544
- <SectionHeading
545
- eyebrow="Portafolio"
546
- title="Productos que construí"
547
- subtitle="Los que tienen capturas se abren en su pantalla de detalle."
548
- className="!mb-5"
549
- />
550
-
551
- <ITGrid container spacing={5}>
552
- {PRODUCTOS.map((producto) => (
553
- <ITGrid item xs={12} md={6} key={producto.id}>
554
- <TarjetaProducto producto={producto} onVerDetalle={setDetalle} />
555
- </ITGrid>
556
- ))}
557
- </ITGrid>
558
- </section>
559
-
560
- {/* ─── CIERRE ─── */}
561
- <section className="relative overflow-hidden rounded-3xl border border-primary-200 bg-gradient-to-br from-primary-50 via-white to-purple-50 p-6 shadow-sm dark:border-primary-900/40 dark:from-primary-950/30 dark:via-slate-900 dark:to-purple-950/30 sm:p-10">
562
- <div aria-hidden className="it-landing-float pointer-events-none absolute -top-20 -right-16 h-56 w-56 rounded-full bg-primary-400/20 blur-3xl" />
563
- <div aria-hidden className="it-landing-grid pointer-events-none absolute inset-0 text-slate-400/40 dark:text-slate-600/30" />
564
-
565
- <div className="relative z-10">
566
- <div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between lg:gap-10">
601
+ {/* ─── 03 · PRODUCTOS ─── */}
602
+ <section id="proyectos" className={ANCLA}>
603
+ <div className={`${HOJA} border-t pb-14 pt-12 sm:pt-16 ${FILETE}`}>
567
604
  <SectionHeading
568
- eyebrow="Siguientes pasos"
569
- title="¿Trabajamos juntos?"
570
- subtitle="Estoy disponible para proyectos fullstack, de producto o de diseño de sistemas."
605
+ index={2}
606
+ label={t("showcase.home.eyebrowPortfolio")}
607
+ title={t("showcase.home.titleProducts")}
608
+ subtitle={t("showcase.home.subtitleProducts")}
571
609
  />
572
610
 
573
- <ITFlex gap={3} wrap="wrap" align="center" className="shrink-0">
574
- <a href={CV_URL} download>
575
- <ITButton variant="filled" color="primary" icon={<FaDownload size={13} />}>
576
- Descargar CV
577
- </ITButton>
578
- </a>
579
- <a href={LINKEDIN_URL} target="_blank" rel="noopener noreferrer">
580
- <ITButton variant="outlined" color="secondary" icon={<FaLinkedin size={13} />}>
611
+ {/* Destacado: copy a la izquierda, captura a la derecha */}
612
+ <article className={`${CARD} overflow-hidden`}>
613
+ <div className="grid lg:grid-cols-12">
614
+ <div className="flex flex-col p-6 sm:p-8 lg:col-span-5">
615
+ <ITFlex align="center" gap={2.5} wrap="wrap">
616
+ <ITText as="span" className={EYEBROW}>
617
+ <span className="tabular-nums">
618
+ {numero(PRODUCTOS.findIndex((p) => p.id === destacado.id))}
619
+ </span>
620
+ {` — ${t("showcase.home.featuredLabel")}`}
621
+ </ITText>
622
+ <Etiqueta>{destacado.tipo}</Etiqueta>
623
+ </ITFlex>
624
+
625
+ <ITText
626
+ as="h3"
627
+ className="font-display !mt-4 !text-[26px] !font-bold -tracking-[0.02em] text-slate-900 dark:text-white"
628
+ >
629
+ {destacado.nombre}
630
+ </ITText>
631
+
632
+ <ITText
633
+ as="p"
634
+ className="!mt-3 !text-[14px] !leading-relaxed text-slate-600 dark:text-slate-300"
635
+ >
636
+ {destacado.descripcion}
637
+ </ITText>
638
+
639
+ {!!destacado.caracteristicas?.length && (
640
+ <ul className="mt-5 space-y-2.5">
641
+ {destacado.caracteristicas.slice(0, 3).map((caracteristica) => (
642
+ <li key={caracteristica} className="flex gap-2.5">
643
+ <FaCheck
644
+ size={10}
645
+ className="mt-1.5 shrink-0 text-primary-700 dark:text-primary-500"
646
+ />
647
+ <ITText
648
+ as="span"
649
+ className="!text-[13px] !leading-relaxed text-slate-600 dark:text-slate-300"
650
+ >
651
+ {caracteristica}
652
+ </ITText>
653
+ </li>
654
+ ))}
655
+ </ul>
656
+ )}
657
+
658
+ <ITFlex gap={1.5} wrap="wrap" className="!mt-5">
659
+ {destacado.stack.map((tecnologia) => (
660
+ <span key={tecnologia} className={CHIP}>
661
+ {tecnologia}
662
+ </span>
663
+ ))}
664
+ </ITFlex>
665
+
666
+ <ITFlex gap={3} wrap="wrap" align="center" className="!mt-6">
667
+ <EnlaceBoton href={`#proyecto/${destacado.id}`} tamano="sm">
668
+ {t("showcase.home.viewDetail")}
669
+ </EnlaceBoton>
670
+ {destacado.enlace && (
671
+ <a
672
+ href={destacado.enlace}
673
+ {...(enlaceDestacadoExterno
674
+ ? { target: "_blank", rel: "noopener noreferrer" }
675
+ : {})}
676
+ className="inline-flex items-center gap-1.5 !text-xs !font-semibold !text-primary-700 dark:!text-primary-500"
677
+ >
678
+ {enlaceDestacadoExterno && <FaExternalLinkAlt size={10} />}
679
+ {destacado.enlaceTexto ?? t("showcase.home.viewRepo")}
680
+ </a>
681
+ )}
682
+ </ITFlex>
683
+ </div>
684
+
685
+ {portadaDestacada && (
686
+ <figure className="border-t border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-950 lg:col-span-7 lg:border-l lg:border-t-0">
687
+ <div className="p-3 sm:p-5">
688
+ <img
689
+ src={portadaDestacada.src}
690
+ alt={`${destacado.nombre}: ${portadaDestacada.titulo}`}
691
+ className="w-full rounded-[6px] border border-slate-200 object-cover object-top dark:border-slate-800"
692
+ />
693
+ </div>
694
+ <figcaption className="px-3 pb-4 font-mono text-[11px] leading-relaxed text-slate-500 dark:text-slate-400 sm:px-5">
695
+ {portadaDestacada.titulo}
696
+ </figcaption>
697
+ </figure>
698
+ )}
699
+ </div>
700
+ </article>
701
+
702
+ {/* Resto: rejilla de tres */}
703
+ <ITGrid container spacing={5} className="!mt-6">
704
+ {resto.map((producto) => (
705
+ <ITGrid item xs={12} md={6} lg={4} key={producto.id}>
706
+ <TarjetaProducto
707
+ producto={producto}
708
+ index={PRODUCTOS.findIndex((p) => p.id === producto.id)}
709
+ />
710
+ </ITGrid>
711
+ ))}
712
+ </ITGrid>
713
+ </div>
714
+ </section>
715
+
716
+ {/* ─── 04 · CONTACTO ─── */}
717
+ <section id="contacto" className={ANCLA}>
718
+ <div className={`${HOJA} border-t py-14 sm:py-16 ${FILETE}`}>
719
+ <div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end">
720
+ <div className="min-w-0">
721
+ <ITText as="p" className={EYEBROW}>
722
+ <span className="tabular-nums">04</span>
723
+ {` — ${t("showcase.home.navContact")}`}
724
+ </ITText>
725
+ <ITText
726
+ as="h2"
727
+ className="font-display !mt-3 !text-[32px] !font-bold !leading-[1.08] -tracking-[0.02em] text-slate-900 dark:text-white sm:!text-[44px]"
728
+ >
729
+ {t("showcase.home.titleTogether")}
730
+ </ITText>
731
+ <ITText
732
+ as="p"
733
+ className="!mt-4 max-w-[58ch] !text-[15px] !leading-relaxed text-slate-600 dark:text-slate-300"
734
+ >
735
+ {t("showcase.home.subtitleTogether")}
736
+ </ITText>
737
+ </div>
738
+
739
+ <ITFlex gap={3} wrap="wrap" align="center">
740
+ <EnlaceBoton href={CV_URL} descargar icono={<FaDownload size={12} />}>
741
+ {t("showcase.home.downloadCv")}
742
+ </EnlaceBoton>
743
+ <EnlaceBoton
744
+ href={LINKEDIN_URL}
745
+ variante="outlined"
746
+ icono={<FaLinkedin size={12} />}
747
+ >
581
748
  LinkedIn
582
- </ITButton>
749
+ </EnlaceBoton>
750
+ </ITFlex>
751
+ </div>
752
+
753
+ <ITText
754
+ as="p"
755
+ className="!mt-8 font-mono !text-[11px] !tracking-[0.06em] text-slate-500 dark:text-slate-400"
756
+ >
757
+ {t("showcase.home.writingPrefix")}{" "}
758
+ <a
759
+ href={MEDIUM_URL}
760
+ target="_blank"
761
+ rel="noopener noreferrer"
762
+ className="!font-semibold !text-primary-700 dark:!text-primary-500"
763
+ >
764
+ Medium
765
+ </a>{" "}
766
+ {t("showcase.home.codeOn")}{" "}
767
+ <a
768
+ href={GITHUB_URL}
769
+ target="_blank"
770
+ rel="noopener noreferrer"
771
+ className="!font-semibold !text-primary-700 dark:!text-primary-500"
772
+ >
773
+ GitHub
583
774
  </a>
584
- </ITFlex>
775
+ .
776
+ </ITText>
585
777
  </div>
778
+ </section>
779
+ </main>
586
780
 
587
- <ITText as="p" className="!mt-6 !text-xs text-slate-500 dark:text-slate-400">
588
- También escribo en{" "}
589
- <a
590
- href={MEDIUM_URL}
591
- target="_blank"
592
- rel="noopener noreferrer"
593
- className="inline-flex items-center gap-1 font-semibold text-primary-600 dark:text-primary-400"
781
+ {/* ─── PIE ─── */}
782
+ <footer className={`border-t ${FILETE}`}>
783
+ <div
784
+ className={`${HOJA} flex flex-col items-start justify-between gap-3 py-7 sm:flex-row sm:items-center`}
785
+ >
786
+ <ITText
787
+ as="p"
788
+ className="font-mono !text-[11px] uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400"
789
+ >
790
+ {`Asael Amaro · ${t("showcase.home.role")}`}
791
+ </ITText>
792
+ <div className="flex flex-wrap items-center gap-x-5 gap-y-2">
793
+ <ITText
794
+ as="span"
795
+ className="font-mono !text-[11px] uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400"
594
796
  >
595
- <FaMedium size={11} />
596
- Medium
597
- </a>{" "}
598
- y el código de mis productos está en{" "}
797
+ {t("showcase.home.badge")}
798
+ </ITText>
599
799
  <a
600
- href={GITHUB_URL}
601
- target="_blank"
602
- rel="noopener noreferrer"
603
- className="inline-flex items-center gap-1 font-semibold text-primary-600 dark:text-primary-400"
800
+ href={SANDBOX_URL}
801
+ className="inline-flex items-center gap-1.5 font-mono !text-[11px] uppercase !tracking-[0.14em] text-slate-500 transition-colors hover:text-primary-700 dark:text-slate-400 dark:hover:text-primary-500"
604
802
  >
605
- <FaGithub size={11} />
606
- GitHub
803
+ {t("showcase.home.footerBuiltWith")}
804
+ <FaArrowRight size={10} />
607
805
  </a>
608
- .
609
- </ITText>
806
+ </div>
610
807
  </div>
611
- </section>
808
+ </footer>
612
809
 
613
- {/* Pantalla de detalle del producto elegido */}
614
- {detalle && <ProductoDetalle producto={detalle} onClose={() => setDetalle(null)} />}
615
- </ITStack>
810
+ <VolverArriba />
811
+ </>
616
812
  );
617
813
  };