@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
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Íconos de las familias del catálogo, compartidos por la barra lateral
3
+ * (`App.tsx`) y la landing (`LandingShowcase`) para que la misma familia se vea
4
+ * igual en los dos sitios.
5
+ */
6
+
7
+ import type { ReactNode } from "react";
8
+ import {
9
+ FaBell,
10
+ FaCreditCard,
11
+ FaHome,
12
+ FaKeyboard,
13
+ FaSlidersH,
14
+ FaTable,
15
+ } from "react-icons/fa";
16
+
17
+ /** Ícono por id de grupo (`navigation.ts`). */
18
+ export const GROUP_ICONS: Record<string, ReactNode> = {
19
+ general: <FaHome />,
20
+ struc: <FaCreditCard />,
21
+ forms: <FaKeyboard />,
22
+ data: <FaTable />,
23
+ nav: <FaSlidersH />,
24
+ feed: <FaBell />,
25
+ };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Idioma del sandbox, compartido entre el chrome (ITTopBar) y las páginas del
3
+ * portafolio.
4
+ *
5
+ * La preferencia vivía solo en `App`, así que el portafolio no podía cambiarla:
6
+ * ahora se expone por contexto para que el selector del header del portafolio y
7
+ * el de la barra del sandbox muevan el mismo estado (y persistan en
8
+ * `localStorage`).
9
+ */
10
+
11
+ import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
12
+ import type { ITLocale } from "@/i18n";
13
+
14
+ /** Valor del contexto de idioma del sandbox. */
15
+ export interface SandboxLocaleValue {
16
+ /** Idioma activo. */
17
+ locale: ITLocale;
18
+ /** Cambia el idioma y lo persiste. */
19
+ setLocale: (locale: ITLocale) => void;
20
+ }
21
+
22
+ /** Clave de `localStorage` donde se recuerda la preferencia. */
23
+ const STORAGE_KEY = "it-locale";
24
+
25
+ const SandboxLocaleContext = createContext<SandboxLocaleValue | undefined>(undefined);
26
+
27
+ /**
28
+ * Lee la preferencia guardada. Cualquier valor distinto de `"en"` cae en `"es"`,
29
+ * que es el idioma por defecto del sandbox.
30
+ */
31
+ function readStoredLocale(): ITLocale {
32
+ if (typeof window === "undefined") return "es";
33
+ return window.localStorage.getItem(STORAGE_KEY) === "en" ? "en" : "es";
34
+ }
35
+
36
+ /**
37
+ * Provee el idioma del sandbox a todo el árbol (chrome + páginas).
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * <SandboxLocaleProvider>
42
+ * <ITI18nProvider locale={locale} messages={sandboxMessages}>…</ITI18nProvider>
43
+ * </SandboxLocaleProvider>
44
+ * ```
45
+ */
46
+ export function SandboxLocaleProvider({ children }: { children: ReactNode }) {
47
+ const [locale, setLocale] = useState<ITLocale>(readStoredLocale);
48
+
49
+ useEffect(() => {
50
+ window.localStorage.setItem(STORAGE_KEY, locale);
51
+ }, [locale]);
52
+
53
+ return (
54
+ <SandboxLocaleContext.Provider value={{ locale, setLocale }}>
55
+ {children}
56
+ </SandboxLocaleContext.Provider>
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Acceso al idioma del sandbox. Fuera del provider devuelve español y un cambio
62
+ * de idioma sin efecto, para que una página se pueda renderizar aislada (tests,
63
+ * Storybook futuro) sin explotar.
64
+ */
65
+ export function useSandboxLocale(): SandboxLocaleValue {
66
+ return (
67
+ useContext(SandboxLocaleContext) ?? {
68
+ locale: "es",
69
+ setLocale: () => undefined,
70
+ }
71
+ );
72
+ }
@@ -0,0 +1,474 @@
1
+ /**
2
+ * Kit de UI de las superficies dev (sandbox `#ui-system` + portafolio `/`).
3
+ *
4
+ * Son las piezas que **no** se usan de la librería publicada porque esta no
5
+ * garantiza contraste AA con la paleta por defecto: `ITButton` relleno queda en
6
+ * 2.4:1, `ITButton color="secondary"` en 3.7:1, `ITBadget` outlined en 2.6:1 y
7
+ * `ITSegmentedControl` en 4.34:1. Aquí el acento se usa en `primary-700`
8
+ * (claro) / `primary-500` (oscuro), que sí pasan.
9
+ *
10
+ * Vive en `src/sandbox/` a propósito: es dev-only y nunca entra en `dist/`.
11
+ * Sigue `DESIGN.md` (AXZY Console): superficies planas con borde de 1px,
12
+ * metadatos en mono, filetes para separar y sombra solo al hover.
13
+ */
14
+
15
+ import { useState, type ReactNode } from "react";
16
+ import { FaCheck, FaCode, FaCopy } from "react-icons/fa";
17
+ import { useITTranslation } from "@/i18n";
18
+
19
+ /** Eyebrow mono apagado: encabezados de sección y etiquetas de metadato. */
20
+ export const EYEBROW =
21
+ "font-mono !text-[11px] !font-medium uppercase !tracking-[0.14em] text-slate-500 dark:text-slate-400";
22
+
23
+ /** Filete de 1px: la separación entre bloques. */
24
+ export const FILETE = "border-slate-200 dark:border-slate-800";
25
+
26
+ /** Superficie de tarjeta: borde 1px, radio 14px, sin sombra en reposo. */
27
+ export const CARD =
28
+ "rounded-[14px] border border-slate-200 bg-white transition-all duration-200 dark:border-slate-800 dark:bg-slate-900";
29
+
30
+ /** Refuerzo al hover de las tarjetas: borde acento, lift de 2px y sombra suave. */
31
+ export const CARD_HOVER =
32
+ "hover:-translate-y-0.5 hover:border-primary-400 hover:shadow-[0_8px_30px_-12px_rgba(15,23,42,0.25)] dark:hover:border-primary-500";
33
+
34
+ /** Chip mono chico para stack y metadatos. */
35
+ export const CHIP =
36
+ "rounded-[6px] border border-slate-200 bg-slate-50 px-2 py-0.5 font-mono text-[10px] text-slate-500 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-400";
37
+
38
+ /**
39
+ * Variantes visuales de los CTA. `outlined` es el borde de acento (segundo CTA
40
+ * de un bloque) y `neutral` el ghost de tinta para acciones que no compiten
41
+ * (volver, cerrar).
42
+ */
43
+ export type VarianteCta = "filled" | "outlined" | "neutral" | "text";
44
+
45
+ /** Tamaños de los CTA (mismos paddings y tipografía que `ITButton`). */
46
+ export type TamanoCta = "sm" | "md" | "lg";
47
+
48
+ /** Padding y tipografía por tamaño (idénticos a `ITButton`). */
49
+ export const TAMANO_CTA: Record<TamanoCta, string> = {
50
+ sm: "text-xs px-2.5 py-1",
51
+ md: "text-[13px] px-3.5 py-1.5",
52
+ lg: "text-sm px-4 py-2",
53
+ };
54
+
55
+ /**
56
+ * Color por variante. El relleno usa `primary-700` (5.05:1 con blanco) porque
57
+ * el `primary-600` de la paleta resuelta se queda en 3.3:1.
58
+ */
59
+ export const VARIANTE_CTA: Record<VarianteCta, string> = {
60
+ filled:
61
+ "border border-transparent bg-primary-700 text-white shadow-sm hover:bg-primary-800",
62
+ outlined:
63
+ "border border-primary-700 text-primary-700 hover:bg-primary-700/10 dark:border-primary-500 dark:text-primary-500",
64
+ neutral:
65
+ "border border-slate-300 text-slate-700 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800",
66
+ text: "border border-transparent text-primary-700 hover:bg-primary-700/10 dark:text-primary-500",
67
+ };
68
+
69
+ /** Foco compartido por todos los CTA (anillo del acento, offset del lienzo). */
70
+ export const FOCO_CTA =
71
+ "focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/60 focus-visible:ring-offset-2 focus-visible:ring-offset-[#fafafa] dark:focus-visible:ring-offset-[#0b0d10]";
72
+
73
+ /** Props de `EnlaceBoton`. */
74
+ export interface EnlaceBotonProps {
75
+ /** Destino. Interno (`#...`, `/...`) o externo (`https://...`). */
76
+ href: string;
77
+ /** Texto visible del enlace. */
78
+ children: ReactNode;
79
+ /** Estilo visual. @default "filled" */
80
+ variante?: VarianteCta;
81
+ /** Tamaño. @default "md" */
82
+ tamano?: TamanoCta;
83
+ /** Ícono a la izquierda del texto. */
84
+ icono?: ReactNode;
85
+ /** Fuerza abrir en pestaña nueva. Por defecto se deduce de `href`. */
86
+ externo?: boolean;
87
+ /** Marca la descarga (`download`) para archivos propios. */
88
+ descargar?: boolean;
89
+ /** Clases extra. */
90
+ className?: string;
91
+ /** Nombre accesible cuando el texto visible no alcanza. */
92
+ ariaLabel?: string;
93
+ }
94
+
95
+ /**
96
+ * CTA que **es un enlace de verdad**: `ITButton` renderiza un `<button>`, así que
97
+ * envolverlo en `<a>` producía interactivos anidados (HTML inválido y fallo
98
+ * `nested-interactive` en axe). Esta pieza conserva el aspecto del botón, pero
99
+ * mantiene la semántica de enlace: clic central, "abrir en pestaña nueva", menú
100
+ * contextual y URL en la barra de estado.
101
+ *
102
+ * @example
103
+ * ```tsx
104
+ * <EnlaceBoton href={GITHUB_URL} variante="outlined" icono={<FaGithub size={12} />}>
105
+ * GitHub
106
+ * </EnlaceBoton>
107
+ * ```
108
+ */
109
+ export const EnlaceBoton = ({
110
+ href,
111
+ children,
112
+ variante = "filled",
113
+ tamano = "md",
114
+ icono,
115
+ externo,
116
+ descargar = false,
117
+ className = "",
118
+ ariaLabel,
119
+ }: EnlaceBotonProps) => {
120
+ const esExterno = externo ?? /^https?:\/\//.test(href);
121
+
122
+ return (
123
+ <a
124
+ href={href}
125
+ {...(esExterno ? { target: "_blank", rel: "noopener noreferrer" } : {})}
126
+ {...(descargar ? { download: "" } : {})}
127
+ aria-label={ariaLabel}
128
+ className={`inline-flex shrink-0 items-center justify-center gap-1.5 rounded-[10px] font-semibold transition-colors duration-200 ${FOCO_CTA} ${TAMANO_CTA[tamano]} ${VARIANTE_CTA[variante]} ${className}`}
129
+ >
130
+ {icono && <span className="flex-shrink-0">{icono}</span>}
131
+ <span>{children}</span>
132
+ </a>
133
+ );
134
+ };
135
+
136
+ /** Props de `BotonAccion`. */
137
+ export interface BotonAccionProps {
138
+ /** Acción al pulsar. */
139
+ onClick: () => void;
140
+ /** Texto visible. */
141
+ children: ReactNode;
142
+ /** Estilo visual. @default "neutral" */
143
+ variante?: VarianteCta;
144
+ /** Tamaño. @default "md" */
145
+ tamano?: TamanoCta;
146
+ /** Ícono a la izquierda. */
147
+ icono?: ReactNode;
148
+ /** Clases extra. */
149
+ className?: string;
150
+ }
151
+
152
+ /**
153
+ * Acción que **no** navega (volver, abrir una pestaña interna): mismo aspecto que
154
+ * `EnlaceBoton` pero con semántica de botón.
155
+ *
156
+ * @example
157
+ * ```tsx
158
+ * <BotonAccion onClick={onBack} icono={<FaArrowLeft size={12} />}>Volver</BotonAccion>
159
+ * ```
160
+ */
161
+ export const BotonAccion = ({
162
+ onClick,
163
+ children,
164
+ variante = "neutral",
165
+ tamano = "md",
166
+ icono,
167
+ className = "",
168
+ }: BotonAccionProps) => (
169
+ <button
170
+ type="button"
171
+ onClick={onClick}
172
+ className={`inline-flex shrink-0 items-center justify-center gap-1.5 rounded-[10px] font-semibold transition-colors duration-200 ${FOCO_CTA} ${TAMANO_CTA[tamano]} ${VARIANTE_CTA[variante]} ${className}`}
173
+ >
174
+ {icono && <span className="flex-shrink-0">{icono}</span>}
175
+ <span>{children}</span>
176
+ </button>
177
+ );
178
+
179
+ /** Props de `Etiqueta`. */
180
+ export interface EtiquetaProps {
181
+ /** Texto de la etiqueta (tipo, estado…). */
182
+ children: ReactNode;
183
+ /** Clases extra. */
184
+ className?: string;
185
+ }
186
+
187
+ /**
188
+ * Píldora de metadato con el acento del proyecto.
189
+ *
190
+ * Sustituye a `ITBadget`: el badge del tema resuelve su color desde la paleta y
191
+ * con la paleta por defecto queda en 2.6:1 sobre las tarjetas oscuras.
192
+ *
193
+ * @example
194
+ * ```tsx
195
+ * <Etiqueta>Librería</Etiqueta>
196
+ * ```
197
+ */
198
+ export const Etiqueta = ({ children, className = "" }: EtiquetaProps) => (
199
+ <span
200
+ className={`shrink-0 rounded-full border border-primary-800/45 px-2 py-[3px] text-[11px] font-semibold text-primary-800 dark:border-primary-500/45 dark:text-primary-500 ${className}`}
201
+ >
202
+ {children}
203
+ </span>
204
+ );
205
+
206
+ /**
207
+ * Encabezado de sección de la hoja: `NN — ETIQUETA`, título display y apoyo,
208
+ * con acciones opcionales a la derecha.
209
+ *
210
+ * @example
211
+ * ```tsx
212
+ * <SectionHeading index={2} label="Catálogo" title="Explora por familia" subtitle="…" />
213
+ * ```
214
+ */
215
+ export const SectionHeading = ({
216
+ index,
217
+ label,
218
+ title,
219
+ subtitle,
220
+ actions,
221
+ className = "",
222
+ }: {
223
+ /** Índice de sección (0-based; se pinta con dos dígitos). */
224
+ index: number;
225
+ /** Etiqueta mono corta. */
226
+ label: string;
227
+ /** Título display. */
228
+ title: string;
229
+ /** Frase de apoyo. */
230
+ subtitle?: string;
231
+ /** Contenido alineado a la derecha. */
232
+ actions?: ReactNode;
233
+ /** Clases extra. */
234
+ className?: string;
235
+ }) => (
236
+ <div
237
+ className={`flex flex-col gap-5 md:flex-row md:items-end md:justify-between ${className}`}
238
+ >
239
+ <div className="min-w-0">
240
+ <p className={EYEBROW}>
241
+ <span className="tabular-nums">{String(index + 1).padStart(2, "0")}</span>
242
+ {` — ${label}`}
243
+ </p>
244
+ <h2 className="mt-2.5 font-display text-[26px] font-bold tracking-[-0.02em] text-slate-900 dark:text-white sm:text-[32px]">
245
+ {title}
246
+ </h2>
247
+ {subtitle && (
248
+ <p className="mt-2 max-w-[62ch] text-[15px] leading-relaxed text-slate-500 dark:text-slate-400">
249
+ {subtitle}
250
+ </p>
251
+ )}
252
+ </div>
253
+ {actions && <div className="shrink-0">{actions}</div>}
254
+ </div>
255
+ );
256
+
257
+ /**
258
+ * Enlace externo mono en línea (GitHub, npm, docs). Mantiene el acento AA y se
259
+ * subraya al hover.
260
+ *
261
+ * @example
262
+ * ```tsx
263
+ * <EnlaceMono href={GITHUB_URL} icono={<FaGithub size={11} />}>GitHub</EnlaceMono>
264
+ * ```
265
+ */
266
+ export const EnlaceMono = ({
267
+ href,
268
+ children,
269
+ icono,
270
+ className = "",
271
+ }: {
272
+ /** Destino (normalmente externo). */
273
+ href: string;
274
+ /** Texto visible. */
275
+ children: ReactNode;
276
+ /** Ícono a la izquierda. */
277
+ icono?: ReactNode;
278
+ /** Clases extra. */
279
+ className?: string;
280
+ }) => (
281
+ <a
282
+ href={href}
283
+ {...(/^https?:\/\//.test(href) ? { target: "_blank", rel: "noopener noreferrer" } : {})}
284
+ className={`inline-flex items-center gap-1.5 font-mono !text-[11px] uppercase !tracking-[0.14em] !text-primary-700 transition-colors hover:underline dark:!text-primary-500 ${className}`}
285
+ >
286
+ {icono}
287
+ {children}
288
+ </a>
289
+ );
290
+
291
+ /**
292
+ * Botón mono de «copiar al portapapeles» con confirmación de dos segundos.
293
+ *
294
+ * @example
295
+ * ```tsx
296
+ * <BotonCopiar texto="pnpm add @axzydev/axzy_ui_system" />
297
+ * ```
298
+ */
299
+ export const BotonCopiar = ({
300
+ texto,
301
+ className = "",
302
+ }: {
303
+ /** Texto que se copia. */
304
+ texto: string;
305
+ /** Clases extra. */
306
+ className?: string;
307
+ }) => {
308
+ const { t } = useITTranslation();
309
+ const [copiado, setCopiado] = useState(false);
310
+
311
+ const copiar = () => {
312
+ navigator.clipboard?.writeText(texto);
313
+ setCopiado(true);
314
+ window.setTimeout(() => setCopiado(false), 2000);
315
+ };
316
+
317
+ return (
318
+ <button
319
+ type="button"
320
+ onClick={copiar}
321
+ className={`inline-flex items-center gap-1.5 rounded-[8px] px-2.5 py-1 font-mono text-[10px] uppercase tracking-[0.12em] text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/60 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white ${className}`}
322
+ >
323
+ {copiado ? (
324
+ <FaCheck size={10} className="text-primary-700 dark:text-primary-500" />
325
+ ) : (
326
+ <FaCopy size={10} />
327
+ )}
328
+ {copiado ? t("showcase.layout.copied") : t("showcase.layout.copy")}
329
+ </button>
330
+ );
331
+ };
332
+
333
+ /**
334
+ * Bloque de código: fondo tinta, mono, etiqueta del archivo y botón de copiar.
335
+ * El acento de señal (`cyan-400`) solo se usa aquí, como manda el spec.
336
+ *
337
+ * @example
338
+ * ```tsx
339
+ * <BloqueCodigo filename="vite.config.ts" code={`export default defineConfig({})`} />
340
+ * ```
341
+ */
342
+ export const BloqueCodigo = ({
343
+ code,
344
+ filename,
345
+ compact = false,
346
+ className = "",
347
+ }: {
348
+ /** Código a mostrar. */
349
+ code: string;
350
+ /** Etiqueta mono de la esquina (archivo o lenguaje). */
351
+ filename?: string;
352
+ /** Limita la altura con scroll vertical. */
353
+ compact?: boolean;
354
+ /** Clases extra. */
355
+ className?: string;
356
+ }) => (
357
+ <div
358
+ className={`overflow-hidden rounded-[10px] border border-slate-800 bg-slate-950 ${className}`}
359
+ >
360
+ <div className="flex items-center justify-between gap-3 border-b border-slate-800 px-4 py-2.5">
361
+ <span className="font-mono text-[10px] uppercase tracking-[0.14em] text-cyan-400">
362
+ {filename ?? "code"}
363
+ </span>
364
+ <BotonCopiar
365
+ texto={code}
366
+ className="text-slate-400 hover:bg-slate-800 hover:text-white dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
367
+ />
368
+ </div>
369
+ <pre
370
+ tabIndex={0}
371
+ className={`overflow-x-auto whitespace-pre-wrap p-4 font-mono text-xs leading-relaxed text-slate-200 ${
372
+ compact ? "max-h-[420px] overflow-y-auto" : ""
373
+ }`}
374
+ >
375
+ <code>{code}</code>
376
+ </pre>
377
+ </div>
378
+ );
379
+
380
+ /** Props de `EjemploContenedor`. */
381
+ export interface EjemploContenedorProps {
382
+ /** Número del ejemplo (`01`, `02`…). Opcional. */
383
+ numero?: string;
384
+ /** Título del ejemplo (mono, mayúsculas). */
385
+ titulo: string;
386
+ /** El demo. */
387
+ children: ReactNode;
388
+ /** Código fuente del ejemplo: si se pasa, aparece el botón «Código». */
389
+ codigo?: string;
390
+ /** Nota al pie, bajo el demo. */
391
+ nota?: string;
392
+ /** Superficie del demo sin padding (layouts completos). */
393
+ sangrado?: boolean;
394
+ /** Clases extra de la superficie del demo. */
395
+ className?: string;
396
+ }
397
+
398
+ /**
399
+ * Contenedor de un ejemplo del sandbox: cabecera con número, título y la acción
400
+ * de ver el código (siempre visible), superficie del demo y nota opcional. El
401
+ * código se despliega **dentro** del mismo contenedor, no en una pestaña aparte.
402
+ *
403
+ * @example
404
+ * ```tsx
405
+ * <EjemploContenedor numero="01" titulo="Con icono" codigo={code}>
406
+ * <ITButton icon={<FaSave />}>Guardar</ITButton>
407
+ * </EjemploContenedor>
408
+ * ```
409
+ */
410
+ export const EjemploContenedor = ({
411
+ numero,
412
+ titulo,
413
+ children,
414
+ codigo,
415
+ nota,
416
+ sangrado = false,
417
+ className = "",
418
+ }: EjemploContenedorProps) => {
419
+ const { t } = useITTranslation();
420
+ const [verCodigo, setVerCodigo] = useState(false);
421
+
422
+ return (
423
+ <section className={`overflow-hidden rounded-[10px] border ${FILETE}`}>
424
+ <header
425
+ className={`flex flex-wrap items-center gap-x-3 gap-y-2 border-b bg-slate-50/70 px-4 py-2.5 dark:bg-slate-950/40 ${FILETE}`}
426
+ >
427
+ {numero && (
428
+ <span className="font-mono text-[10px] tabular-nums text-slate-400 dark:text-slate-500">
429
+ {numero}
430
+ </span>
431
+ )}
432
+ <h2 className="min-w-0 flex-1 truncate font-mono text-[11px] font-medium uppercase tracking-[0.14em] text-slate-700 dark:text-slate-200">
433
+ {titulo}
434
+ </h2>
435
+
436
+ {codigo && (
437
+ <button
438
+ type="button"
439
+ onClick={() => setVerCodigo(!verCodigo)}
440
+ aria-expanded={verCodigo}
441
+ className={`inline-flex items-center gap-1.5 rounded-[8px] border px-2.5 py-1 font-mono text-[10px] uppercase tracking-[0.12em] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/60 ${
442
+ verCodigo
443
+ ? "border-primary-700 text-primary-700 dark:border-primary-500 dark:text-primary-500"
444
+ : "border-slate-200 text-slate-500 hover:text-slate-900 dark:border-slate-700 dark:text-slate-400 dark:hover:text-white"
445
+ }`}
446
+ >
447
+ <FaCode size={10} />
448
+ {verCodigo ? t("showcase.layout.hideCode") : t("showcase.layout.viewCode")}
449
+ </button>
450
+ )}
451
+ </header>
452
+
453
+ <div
454
+ className={`bg-white dark:bg-slate-950 ${sangrado ? "" : "p-5 sm:p-6"} ${className}`}
455
+ >
456
+ {children}
457
+ </div>
458
+
459
+ {nota && (
460
+ <p
461
+ className={`border-t px-4 py-2.5 font-mono text-[11px] leading-relaxed text-slate-500 dark:text-slate-400 ${FILETE}`}
462
+ >
463
+ {nota}
464
+ </p>
465
+ )}
466
+
467
+ {codigo && verCodigo && (
468
+ <div className={`border-t p-3 ${FILETE}`}>
469
+ <BloqueCodigo code={codigo} compact />
470
+ </div>
471
+ )}
472
+ </section>
473
+ );
474
+ };