@crtobiasdelsud/portal-ui 1.0.0

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 (219) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +829 -0
  3. package/package.json +43 -0
  4. package/src/adapters/AdaptersContext.jsx +28 -0
  5. package/src/components/ArticleHero/ArticleHero.jsx +73 -0
  6. package/src/components/ArticleHero/ArticleHero.module.scss +89 -0
  7. package/src/components/ArticleHero/variants/V0/V0.jsx +29 -0
  8. package/src/components/ArticleHero/variants/V0/V0.module.scss +51 -0
  9. package/src/components/ArticleHero/variants/V0Desktop/V0Desktop.jsx +29 -0
  10. package/src/components/ArticleHero/variants/V0Desktop/V0Desktop.module.scss +54 -0
  11. package/src/components/ArticleHero/variants/V0Tablet/V0Tablet.jsx +29 -0
  12. package/src/components/ArticleHero/variants/V0Tablet/V0Tablet.module.scss +49 -0
  13. package/src/components/ArticleHero/variants/V1/V1.jsx +29 -0
  14. package/src/components/ArticleHero/variants/V1/V1.module.scss +40 -0
  15. package/src/components/ArticleHero/variants/V2/V2.jsx +29 -0
  16. package/src/components/ArticleHero/variants/V2/V2.module.scss +44 -0
  17. package/src/components/ArticleHero/variants/V3/V3.jsx +25 -0
  18. package/src/components/ArticleHero/variants/V3/V3.module.scss +41 -0
  19. package/src/components/ArticleHero/variants/V4/V4.jsx +25 -0
  20. package/src/components/ArticleHero/variants/V4/V4.module.scss +36 -0
  21. package/src/components/ArticleHero/variants/V5/V5.jsx +25 -0
  22. package/src/components/ArticleHero/variants/V5/V5.module.scss +33 -0
  23. package/src/components/ArticleHeroFull/ArticleHeroFull.jsx +38 -0
  24. package/src/components/ArticleHeroFull/ArticleHeroFull.module.scss +116 -0
  25. package/src/components/ArticleSidebar/ArticleSidebar.jsx +28 -0
  26. package/src/components/ArticleSidebar/ArticleSidebar.module.scss +41 -0
  27. package/src/components/AuthorBlock/AuthorBlock.jsx +67 -0
  28. package/src/components/AuthorBlock/AuthorBlock.module.scss +35 -0
  29. package/src/components/AuthorBlock/variants/V1/V1.jsx +38 -0
  30. package/src/components/AuthorBlock/variants/V1/V1.module.scss +25 -0
  31. package/src/components/AuthorBlock/variants/V2/V2.jsx +38 -0
  32. package/src/components/AuthorBlock/variants/V2/V2.module.scss +10 -0
  33. package/src/components/AuthorBlock/variants/V3/V3.jsx +30 -0
  34. package/src/components/AuthorBlock/variants/V3/V3.module.scss +10 -0
  35. package/src/components/AuthorBlock/variants/V4/V4.jsx +30 -0
  36. package/src/components/AuthorBlock/variants/V4/V4.module.scss +10 -0
  37. package/src/components/Banner/Banner.module.scss +70 -0
  38. package/src/components/Banner/BannerDisplay.jsx +118 -0
  39. package/src/components/Banner/BannerView.jsx +35 -0
  40. package/src/components/Blocks/BlockColumns/BlockColumns.jsx +37 -0
  41. package/src/components/Blocks/BlockColumns/BlockColumns.module.scss +64 -0
  42. package/src/components/Blocks/BlockColumnsBajada/BlockColumnsBajada.jsx +38 -0
  43. package/src/components/Blocks/BlockColumnsBajada/BlockColumnsBajada.module.scss +64 -0
  44. package/src/components/Blocks/BlockMain/BlockMain.jsx +18 -0
  45. package/src/components/Blocks/BlockMain/BlockMain.module.scss +9 -0
  46. package/src/components/Blocks/BlockMainNarrow/BlockMainNarrow.jsx +18 -0
  47. package/src/components/Blocks/BlockMainNarrow/BlockMainNarrow.module.scss +9 -0
  48. package/src/components/Blocks/BlockMainSidebar/BlockMainSidebar.jsx +33 -0
  49. package/src/components/Blocks/BlockMainSidebar/BlockMainSidebar.module.scss +44 -0
  50. package/src/components/Blocks/BlockStack/BlockStack.jsx +8 -0
  51. package/src/components/Blocks/BlockStack/BlockStack.module.scss +18 -0
  52. package/src/components/Blocks/WidgetErrorBoundary.jsx +43 -0
  53. package/src/components/Breadcrumb/Breadcrumb.jsx +24 -0
  54. package/src/components/Breadcrumb/Breadcrumb.module.scss +33 -0
  55. package/src/components/Breadcrumb/variants/V1/V1.jsx +39 -0
  56. package/src/components/Breadcrumb/variants/V1/V1.module.scss +3 -0
  57. package/src/components/Breadcrumb/variants/V2/V2.jsx +28 -0
  58. package/src/components/Breadcrumb/variants/V2/V2.module.scss +3 -0
  59. package/src/components/Breadcrumb/variants/V3/V3.jsx +28 -0
  60. package/src/components/Breadcrumb/variants/V3/V3.module.scss +3 -0
  61. package/src/components/Breadcrumb/variants/V4/V4.jsx +28 -0
  62. package/src/components/Breadcrumb/variants/V4/V4.module.scss +3 -0
  63. package/src/components/Breadcrumb/variants/V5/V5.jsx +28 -0
  64. package/src/components/Breadcrumb/variants/V5/V5.module.scss +4 -0
  65. package/src/components/Cabezal/Cabezal.module.scss +82 -0
  66. package/src/components/Cabezal/CabezalView.jsx +87 -0
  67. package/src/components/Cabezal/CardCabezal/CardCabezal.module.scss +403 -0
  68. package/src/components/Cabezal/CardCabezal/index.jsx +25 -0
  69. package/src/components/Cabezal/CardCabezal/variants/Amp/Amp.jsx +20 -0
  70. package/src/components/Cabezal/CardCabezal/variants/Carrusel/Carrusel.jsx +39 -0
  71. package/src/components/Cabezal/CardCabezal/variants/Carrusel/Carrusel.module.scss +87 -0
  72. package/src/components/Cabezal/CardCabezal/variants/Compact/Compact.jsx +36 -0
  73. package/src/components/Cabezal/CardCabezal/variants/Default/Default.jsx +35 -0
  74. package/src/components/Cabezal/CardCabezal/variants/Featured/Featured.jsx +36 -0
  75. package/src/components/Cabezal/CardCabezal/variants/FeaturedDuo/FeaturedDuo.jsx +36 -0
  76. package/src/components/Cabezal/CardCabezal/variants/FeaturedHorizontal/FeaturedHorizontal.jsx +36 -0
  77. package/src/components/Cabezal/CardCabezal/variants/Medium/Medium.jsx +35 -0
  78. package/src/components/Cabezal/CardCabezal/variants/Ranked/Ranked.jsx +36 -0
  79. package/src/components/Cabezal/variants/Carrusel/Carrusel.jsx +167 -0
  80. package/src/components/Cabezal/variants/Carrusel/Carrusel.module.scss +145 -0
  81. package/src/components/Cabezal/variants/Categoria/Categoria.jsx +29 -0
  82. package/src/components/Cabezal/variants/Categoria/Categoria.module.scss +82 -0
  83. package/src/components/Cabezal/variants/CategoriaDos/CategoriaDos.jsx +29 -0
  84. package/src/components/Cabezal/variants/CategoriaDos/CategoriaDos.module.scss +99 -0
  85. package/src/components/Cabezal/variants/Compact/Compact.jsx +24 -0
  86. package/src/components/Cabezal/variants/Compact/Compact.module.scss +73 -0
  87. package/src/components/Cabezal/variants/Default/Default.jsx +24 -0
  88. package/src/components/Cabezal/variants/Default/Default.module.scss +71 -0
  89. package/src/components/Cabezal/variants/Desktop/Desktop.jsx +45 -0
  90. package/src/components/Cabezal/variants/Desktop/Desktop.module.scss +113 -0
  91. package/src/components/Cabezal/variants/Duo/Duo.jsx +24 -0
  92. package/src/components/Cabezal/variants/Duo/Duo.module.scss +72 -0
  93. package/src/components/Cabezal/variants/DuoSinCopete/DuoSinCopete.jsx +24 -0
  94. package/src/components/Cabezal/variants/DuoSinCopete/DuoSinCopete.module.scss +72 -0
  95. package/src/components/Cabezal/variants/Horizontal/Horizontal.jsx +24 -0
  96. package/src/components/Cabezal/variants/Horizontal/Horizontal.module.scss +64 -0
  97. package/src/components/Cabezal/variants/LeeAdemas/LeeAdemas.jsx +53 -0
  98. package/src/components/Cabezal/variants/LeeAdemas/LeeAdemas.module.scss +103 -0
  99. package/src/components/Cabezal/variants/LoQueSeLee/LoQueSeLee.jsx +48 -0
  100. package/src/components/Cabezal/variants/LoQueSeLee/LoQueSeLee.module.scss +103 -0
  101. package/src/components/Cabezal/variants/LoQueSeLee/LoQueSeLeeSkeleton.jsx +21 -0
  102. package/src/components/Cabezal/variants/LoQueSeLee/LoQueSeLeeSkeleton.module.scss +34 -0
  103. package/src/components/Cabezal/variants/Medium/Medium.jsx +24 -0
  104. package/src/components/Cabezal/variants/Medium/Medium.module.scss +78 -0
  105. package/src/components/Cabezal/variants/Mobile/Mobile.jsx +48 -0
  106. package/src/components/Cabezal/variants/Mobile/Mobile.module.scss +64 -0
  107. package/src/components/Cabezal/variants/Ranking/Ranking.jsx +24 -0
  108. package/src/components/Cabezal/variants/Ranking/Ranking.module.scss +79 -0
  109. package/src/components/Cabezal/variants/SeguiLeyendo/SeguiLeyendo.jsx +38 -0
  110. package/src/components/Cabezal/variants/SeguiLeyendo/SeguiLeyendo.module.scss +52 -0
  111. package/src/components/Cabezal/variants/Tablet/Tablet.jsx +45 -0
  112. package/src/components/Cabezal/variants/Tablet/Tablet.module.scss +113 -0
  113. package/src/components/Cabezal/variants/Tres/Tres.jsx +24 -0
  114. package/src/components/Cabezal/variants/Tres/Tres.module.scss +74 -0
  115. package/src/components/Cabezal/variants/UnaDetallada/UnaDetallada.jsx +29 -0
  116. package/src/components/Cabezal/variants/UnaDetallada/UnaDetallada.module.scss +86 -0
  117. package/src/components/Cards/ArticleBody/ArticleBody.module.scss +9 -0
  118. package/src/components/Cards/ArticleBody/ArticleBodyView.jsx +23 -0
  119. package/src/components/Cards/ArticleCard/ArticleCard.jsx +111 -0
  120. package/src/components/Cards/ArticleCard/ArticleCard.module.scss +129 -0
  121. package/src/components/Cards/Bajada/Bajada.jsx +41 -0
  122. package/src/components/Cards/Bajada/Bajada.module.scss +113 -0
  123. package/src/components/Cards/Bajada/variants/V1/V1.jsx +20 -0
  124. package/src/components/Cards/Bajada/variants/V1/V1.module.scss +2 -0
  125. package/src/components/Cards/Bajada/variants/V2/V2.jsx +20 -0
  126. package/src/components/Cards/Bajada/variants/V2/V2.module.scss +2 -0
  127. package/src/components/Clima/Clima.module.scss +93 -0
  128. package/src/components/Clima/ClimaView.jsx +45 -0
  129. package/src/components/DateTime/DateTime.jsx +26 -0
  130. package/src/components/DateTime/DateTime.module.scss +29 -0
  131. package/src/components/DolarTicker/DolarTicker.jsx +137 -0
  132. package/src/components/DolarTicker/DolarTicker.module.scss +249 -0
  133. package/src/components/DolarTicker/arrow_back_ios_black.svg +3 -0
  134. package/src/components/DolarTicker/arrow_back_ios_white.svg +3 -0
  135. package/src/components/DolarTicker/arrow_downward.svg +3 -0
  136. package/src/components/DolarTicker/arrow_upward.svg +3 -0
  137. package/src/components/DolarTickerOriginal/DolarTickerOriginal.jsx +137 -0
  138. package/src/components/DolarTickerOriginal/DolarTickerOriginal.module.scss +235 -0
  139. package/src/components/DolarTickerOriginal/arrow_back_ios_black.svg +3 -0
  140. package/src/components/DolarTickerOriginal/arrow_back_ios_white.svg +3 -0
  141. package/src/components/DolarTickerOriginal/arrow_downward.svg +3 -0
  142. package/src/components/DolarTickerOriginal/arrow_upward.svg +3 -0
  143. package/src/components/EditorOutput/EditorOutput.jsx +303 -0
  144. package/src/components/EditorOutput/EditorOutput.module.scss +310 -0
  145. package/src/components/EditorOutputFull/EditorOutputFull.jsx +303 -0
  146. package/src/components/EditorOutputFull/EditorOutputFull.module.scss +317 -0
  147. package/src/components/Feed/Feed.module.scss +77 -0
  148. package/src/components/Feed/FeedView.jsx +25 -0
  149. package/src/components/Feed/variants/V1/V1.jsx +34 -0
  150. package/src/components/Feed/variants/V1/V1.module.scss +39 -0
  151. package/src/components/Feed/variants/V2/V2.jsx +36 -0
  152. package/src/components/Feed/variants/V2/V2.module.scss +72 -0
  153. package/src/components/Footers/FooterSimple/FooterSimple.jsx +170 -0
  154. package/src/components/Footers/FooterSimple/FooterSimple.module.scss +256 -0
  155. package/src/components/Headers/HeaderSimple/CategoriesBar/CategoriesBar.jsx +104 -0
  156. package/src/components/Headers/HeaderSimple/CategoriesBar/CategoriesBar.module.scss +112 -0
  157. package/src/components/Headers/HeaderSimple/DrawerContext/DrawerContext.jsx +44 -0
  158. package/src/components/Headers/HeaderSimple/HeaderSimpleAmp/HeaderSimpleAmp.jsx +95 -0
  159. package/src/components/Headers/HeaderSimple/HeaderSimpleAmp/HeaderSimpleAmp.module.scss +2 -0
  160. package/src/components/Headers/HeaderSimple/HeaderSimpleDesktop/HeaderSimpleDesktop.jsx +93 -0
  161. package/src/components/Headers/HeaderSimple/HeaderSimpleDesktop/HeaderSimpleDesktop.module.scss +121 -0
  162. package/src/components/Headers/HeaderSimple/HeaderSimpleDesktopCompact/HeaderSimpleDesktopCompact.jsx +96 -0
  163. package/src/components/Headers/HeaderSimple/HeaderSimpleDesktopCompact/HeaderSimpleDesktopCompact.module.scss +137 -0
  164. package/src/components/Headers/HeaderSimple/HeaderSimpleDesktopCompact/SearchTrigger.jsx +17 -0
  165. package/src/components/Headers/HeaderSimple/HeaderSimpleMobile/HeaderSimpleMobile.jsx +89 -0
  166. package/src/components/Headers/HeaderSimple/HeaderSimpleMobile/HeaderSimpleMobile.module.scss +76 -0
  167. package/src/components/Headers/HeaderSimple/HeaderSimpleSwitch/HeaderSimpleSwitch.jsx +47 -0
  168. package/src/components/Headers/HeaderSimple/HeaderSimpleSwitch/HeaderSwitch.jsx +32 -0
  169. package/src/components/Headers/HeaderSimple/HeaderSimpleSwitch/HeaderSwitch.module.scss +46 -0
  170. package/src/components/Headers/HeaderSimple/LiveBanner/LiveBanner.jsx +79 -0
  171. package/src/components/Headers/HeaderSimple/LiveBanner/LiveBanner.module.scss +102 -0
  172. package/src/components/Headers/HeaderSimple/MenuDrawer/MenuDrawer.jsx +217 -0
  173. package/src/components/Headers/HeaderSimple/MenuDrawer/MenuDrawer.module.scss +243 -0
  174. package/src/components/Headers/HeaderSimple/_headerUtils.js +48 -0
  175. package/src/components/Headers/HeaderSimple/_logos/mendoza-claro.svg +43 -0
  176. package/src/components/Headers/HeaderSimple/_logos/mendoza-oscuro.svg +43 -0
  177. package/src/components/Hero/Hero.module.scss +131 -0
  178. package/src/components/Hero/HeroView.jsx +48 -0
  179. package/src/components/Recommended/Recommended.module.scss +119 -0
  180. package/src/components/Recommended/RecommendedView.jsx +23 -0
  181. package/src/components/ShareBlock/ShareBlock.jsx +75 -0
  182. package/src/components/ShareBlock/ShareBlock.module.scss +60 -0
  183. package/src/components/ShareBlock/variants/V1/V1.jsx +27 -0
  184. package/src/components/ShareBlock/variants/V1/V1.module.scss +2 -0
  185. package/src/components/ShareBlock/variants/V2/V2.jsx +27 -0
  186. package/src/components/ShareBlock/variants/V2/V2.module.scss +3 -0
  187. package/src/components/SpeechButton/SpeechButton.jsx +60 -0
  188. package/src/components/SpeechButton/SpeechButton.module.scss +49 -0
  189. package/src/components/SpeechPlayerBar/SpeechPlayerBar.jsx +92 -0
  190. package/src/components/SpeechPlayerBar/SpeechPlayerBar.module.scss +180 -0
  191. package/src/components/SpeechProviderWrapper/SpeechProviderWrapper.jsx +13 -0
  192. package/src/components/TextWrap/TextWrap.module.scss +72 -0
  193. package/src/components/TextWrap/TextWrapView.jsx +23 -0
  194. package/src/components/UI/AspectImage/AspectImage.jsx +53 -0
  195. package/src/components/UI/FocalImage/FocalImage.jsx +36 -0
  196. package/src/components/UI/Icon/Icon.jsx +40 -0
  197. package/src/components/UI/Icon/Icon.module.scss +82 -0
  198. package/src/components/UI/IconSmall/IconSmall.jsx +40 -0
  199. package/src/components/UI/IconSmall/IconSmall.module.scss +82 -0
  200. package/src/components/UI/PageWrapper/PageWrapper.jsx +9 -0
  201. package/src/components/UI/PageWrapper/PageWrapper.module.scss +5 -0
  202. package/src/components/UI/ToolTip/ToolTip.jsx +49 -0
  203. package/src/components/UI/ToolTip/ToolTip.module.scss +26 -0
  204. package/src/components/UI/index.js +6 -0
  205. package/src/constants/imageSizes.js +10 -0
  206. package/src/context/SiteConfigContext.jsx +79 -0
  207. package/src/context/SpeechContext.jsx +138 -0
  208. package/src/data/ArticlePoolContext.jsx +83 -0
  209. package/src/data/index.js +7 -0
  210. package/src/data/useArticles.js +67 -0
  211. package/src/index.js +101 -0
  212. package/src/styles/index.scss +3 -0
  213. package/src/styles/mixins/_helpers.scss +6 -0
  214. package/src/styles/mixins/_media.scss +30 -0
  215. package/src/styles/variables/_breakpoint.scss +4 -0
  216. package/src/styles/variables/_colors.scss +4 -0
  217. package/src/styles/variables/_spacing.scss +0 -0
  218. package/src/utils/colorContrast.js +64 -0
  219. package/src/utils/fechaHora.js +26 -0
package/README.md ADDED
@@ -0,0 +1,829 @@
1
+ # @crtobias/portal-ui
2
+
3
+ Librería de componentes compartida entre el portal público (Next 15) y el
4
+ editor CMS (Vite). Provee:
5
+
6
+ - **Componentes UI** — Header, Footer, Hero, Feed, Cabezal, Cards, AuthorBlock,
7
+ Breadcrumb, ShareBlock, EditorOutput, Speech*, Banner, Clima, DolarTicker,
8
+ Blocks, ArticleHero, etc.
9
+ - **Providers** — adapters (`Image`/`Link`/`fetcher`), site config,
10
+ article pool, speech.
11
+ - **Hooks** — `useTheme`, `useSiteConfig`, `useCategories`, `useBanners`,
12
+ `useArticlePool`, `useArticles`, `useSpeech`, etc.
13
+ - **Utils** — `getFechaHora`, `contrastRatio`, `hexToCssFilter`, `ensureContrast`.
14
+
15
+ > El paquete ship-ea `.jsx` + `.scss` crudo. No hay build step propio: cada app
16
+ > los compila en su bundle (Next via `transpilePackages`, Vite out-of-the-box).
17
+
18
+ ---
19
+
20
+ ## Índice
21
+
22
+ 1. [Instalación](#instalación)
23
+ 2. [Setup mínimo](#setup-mínimo)
24
+ 3. [El adapter pattern — cómo funciona Next ↔ Vite](#el-adapter-pattern--cómo-funciona-next--vite)
25
+ 4. [Workflow de desarrollo](#workflow-de-desarrollo)
26
+ 5. [Cómo agregar un componente nuevo](#cómo-agregar-un-componente-nuevo)
27
+ 6. [Publicar a npm](#publicar-a-npm)
28
+ 7. [Trabajar entre varios devs](#trabajar-entre-varios-devs)
29
+ 8. [Qué NO va en el paquete](#qué-no-va-en-el-paquete)
30
+ 9. [Estructura](#estructura)
31
+ 10. [API exportada](#api-exportada)
32
+
33
+ ---
34
+
35
+ ## Instalación
36
+
37
+ ```bash
38
+ npm install @crtobias/portal-ui
39
+ ```
40
+
41
+ `peerDependencies`:
42
+ - `react` ^19
43
+ - `react-dom` ^19
44
+ - `sass` ^1.98
45
+
46
+ **Next 15:** agregar a `transpilePackages` en `next.config.mjs`:
47
+
48
+ ```js
49
+ const nextConfig = {
50
+ transpilePackages: ['@crtobias/portal-ui'],
51
+ // ...
52
+ }
53
+ ```
54
+
55
+ **Vite:** funciona out-of-the-box.
56
+
57
+ ---
58
+
59
+ ## Setup mínimo
60
+
61
+ ### Next (App Router)
62
+
63
+ ```jsx
64
+ // src/app/PortalUIProviders.jsx
65
+ 'use client'
66
+ import Image from 'next/image'
67
+ import Link from 'next/link'
68
+ import { AdaptersProvider } from '@crtobias/portal-ui'
69
+ import { clientFetch } from '@/lib/clientFetch' // tu fetcher con BASE_URL + tenant
70
+
71
+ export default function PortalUIProviders({ children }) {
72
+ return (
73
+ <AdaptersProvider value={{ Image, Link, fetcher: clientFetch }}>
74
+ {children}
75
+ </AdaptersProvider>
76
+ )
77
+ }
78
+ ```
79
+
80
+ ```jsx
81
+ // src/app/layout.jsx (server component)
82
+ import PortalUIProviders from './PortalUIProviders'
83
+ import { SiteConfigProvider } from '@crtobias/portal-ui'
84
+
85
+ export default async function RootLayout({ children }) {
86
+ const siteData = await fetchSiteConfig() // tu lógica
87
+ return (
88
+ <html>
89
+ <body>
90
+ <PortalUIProviders>
91
+ <SiteConfigProvider value={siteData}>
92
+ {children}
93
+ </SiteConfigProvider>
94
+ </PortalUIProviders>
95
+ </body>
96
+ </html>
97
+ )
98
+ }
99
+ ```
100
+
101
+ ### Vite (CMS)
102
+
103
+ ```jsx
104
+ // src/PortalUIProviders.jsx
105
+ import { AdaptersProvider } from '@crtobias/portal-ui'
106
+ import ImageShim from './shims/Image' // <img> plano
107
+ import LinkShim from './shims/Link' // <a> plano
108
+ import { backendFetch } from './lib/backendClient'
109
+
110
+ export default function PortalUIProviders({ children }) {
111
+ return (
112
+ <AdaptersProvider value={{ Image: ImageShim, Link: LinkShim, fetcher: backendFetch }}>
113
+ {children}
114
+ </AdaptersProvider>
115
+ )
116
+ }
117
+ ```
118
+
119
+ ```jsx
120
+ // src/main.jsx
121
+ <PortalUIProviders><App /></PortalUIProviders>
122
+ ```
123
+
124
+ ---
125
+
126
+ ## El adapter pattern — cómo funciona Next ↔ Vite
127
+
128
+ ### El problema
129
+
130
+ El portal usa **Next 15**, el CMS usa **Vite**. Ambos consumen los mismos
131
+ componentes. Si dentro de `portal-ui` hacemos:
132
+
133
+ ```jsx
134
+ import Link from 'next/link'
135
+ import Image from 'next/image'
136
+ ```
137
+
138
+ …rompe en el CMS, porque Vite no resuelve esos paquetes. Y si hacemos:
139
+
140
+ ```jsx
141
+ import { Link } from 'react-router-dom'
142
+ ```
143
+
144
+ …rompe en el portal, porque Next no usa react-router.
145
+
146
+ Tampoco queremos `if (process.env...)` ramificando código. Ni hacer dos copias
147
+ del paquete. Queremos **el mismo componente** funcionando en los dos lados.
148
+
149
+ ### La solución: inyección de dependencias por contexto
150
+
151
+ `portal-ui` define una "interfaz" — tres cosas que cualquier app tiene que
152
+ proveer:
153
+
154
+ ```jsx
155
+ // portal-ui/src/adapters/AdaptersContext.jsx
156
+ const AdaptersContext = createContext(null)
157
+
158
+ export function AdaptersProvider({ value, children }) {
159
+ return (
160
+ <AdaptersContext.Provider value={value}>
161
+ {children}
162
+ </AdaptersContext.Provider>
163
+ )
164
+ }
165
+
166
+ export function useAdapters() {
167
+ const a = useContext(AdaptersContext)
168
+ if (!a) throw new Error('AdaptersProvider missing')
169
+ return a
170
+ }
171
+ ```
172
+
173
+ La interfaz es: `{ Image, Link, fetcher }`.
174
+
175
+ ### Cada app implementa la interfaz
176
+
177
+ **Portal (Next):**
178
+ ```jsx
179
+ import Image from 'next/image' // optimización Next + lazy loading
180
+ import Link from 'next/link' // client-side navigation
181
+ import { clientFetch } from '@/lib/clientFetch' // fetch con BASE_URL + X-Tenant-ID
182
+
183
+ <AdaptersProvider value={{ Image, Link, fetcher: clientFetch }}>
184
+ ```
185
+
186
+ **CMS (Vite):**
187
+ ```jsx
188
+ // shims/Image.jsx
189
+ export default function Image({ src, alt, width, height, ...rest }) {
190
+ return <img src={src} alt={alt} width={width} height={height} {...rest} />
191
+ }
192
+
193
+ // shims/Link.jsx
194
+ export default function Link({ href, children, ...rest }) {
195
+ return <a href={href} {...rest}>{children}</a>
196
+ }
197
+
198
+ <AdaptersProvider value={{ Image: ImageShim, Link: LinkShim, fetcher: backendFetch }}>
199
+ ```
200
+
201
+ ### Dentro del paquete
202
+
203
+ ```jsx
204
+ // portal-ui/src/components/ArticleCard/ArticleCard.jsx
205
+ 'use client'
206
+ import { useAdapters } from '../../adapters/AdaptersContext.jsx'
207
+
208
+ export default function ArticleCard({ article }) {
209
+ const { Image, Link } = useAdapters() // ← traduce a Next o Vite según la app
210
+ return (
211
+ <Link href={article.slug}>
212
+ <Image src={article.imagen.url} alt={article.titulo} width={400} height={250} />
213
+ </Link>
214
+ )
215
+ }
216
+ ```
217
+
218
+ **El componente no sabe** si está corriendo en Next o en Vite. Solo sabe que
219
+ hay un `Link` y un `Image` con cierta API.
220
+
221
+ ### Diagrama
222
+
223
+ ```
224
+ ┌─────────────────────────────────────────┐
225
+ │ PORTAL (Next 15) │
226
+ │ ┌─────────────────────────────────────┐ │
227
+ │ │ AdaptersProvider │ │
228
+ │ │ value={{ │ │
229
+ │ │ Image: next/image, ─────┐ │ │
230
+ │ │ Link: next/link, ─────┤ │ │
231
+ │ │ fetcher: clientFetch ─────┤ │ │
232
+ │ │ }} │ │ │
233
+ │ │ ┌─────────────────────────┐ │ │ │
234
+ │ │ │ <ArticleCard> │ │ │ │
235
+ │ │ │ useAdapters() ────────┴────┘ │ │
236
+ │ │ │ → next/image │ │ │
237
+ │ │ │ → next/link │ │ │
238
+ │ │ │ → clientFetch │ │ │
239
+ │ │ └─────────────────────────┘ │ │
240
+ │ └─────────────────────────────────────┘ │
241
+ └─────────────────────────────────────────┘
242
+
243
+ ┌─────────────────────────────────────────┐
244
+ │ CMS (Vite) │
245
+ │ ┌─────────────────────────────────────┐ │
246
+ │ │ AdaptersProvider │ │
247
+ │ │ value={{ │ │
248
+ │ │ Image: ImgShim, ─────┐ │ │
249
+ │ │ Link: LinkShim, ─────┤ │ │
250
+ │ │ fetcher: backendFetch ─────┤ │ │
251
+ │ │ }} │ │ │
252
+ │ │ ┌─────────────────────────┐ │ │ │
253
+ │ │ │ <ArticleCard> (MISMO) │ │ │ │
254
+ │ │ │ useAdapters() ────────┴────┘ │ │
255
+ │ │ │ → <img> │ │ │
256
+ │ │ │ → <a> │ │ │
257
+ │ │ │ → backendFetch │ │ │
258
+ │ │ └─────────────────────────┘ │ │
259
+ │ └─────────────────────────────────────┘ │
260
+ └─────────────────────────────────────────┘
261
+ ```
262
+
263
+ ### Por qué funciona
264
+
265
+ 1. **Sin acoplamiento.** El paquete no importa nada de Next ni de Vite. Solo
266
+ importa `useAdapters` de su propio contexto. Cero dependencias en
267
+ `package.json` aparte de las peer deps.
268
+
269
+ 2. **Sin if/else.** No hay ramas tipo `if (isNext) ... else ...`. El
270
+ componente usa una sola API uniforme.
271
+
272
+ 3. **Extensible.** Si mañana hace falta otro adapter (ej. `navigate` para
273
+ programmatic navigation), se agrega al objeto del provider. Las dos apps
274
+ lo implementan a su modo.
275
+
276
+ 4. **SSR-friendly.** En Next, `next/image` se renderiza server-side con
277
+ `<picture>` y srcset. En Vite/CMS, `<img>` plano alcanza para preview.
278
+ Ningún componente del paquete tiene que saberlo.
279
+
280
+ ### Otros adapters internos del paquete
281
+
282
+ Además del context principal, el paquete tiene **3 contexts más** para
283
+ inyectar "data" en lugar de "primitives":
284
+
285
+ | Context | Qué provee la app | Ejemplo Next | Ejemplo Vite |
286
+ |---|---|---|---|
287
+ | `AdaptersContext` | `Image`, `Link`, `fetcher` | `next/image`, `next/link`, `clientFetch` | `<img>`, `<a>`, `backendFetch` |
288
+ | `SiteConfigContext` | `theme`, `slots`, `categories`, `banners` | desde `layout.jsx` async fetch | desde `EditableHomePreview` state |
289
+ | `ArticlePoolContext` | tracker de dedup de artículos | sólo en `<Home>` (request-scoped) | sólo en modo Lector del CMS |
290
+ | `SpeechContext` | estado de Web Speech API | montado una vez en layout | montado una vez en App |
291
+
292
+ Todos siguen el mismo patrón: `XxxProvider` arriba en el árbol, `useXxx()`
293
+ adentro de los componentes.
294
+
295
+ ### Casos donde el adapter no alcanza
296
+
297
+ | Caso | Solución |
298
+ |---|---|
299
+ | Router programático (`router.push`) | Convertir a `<form action="...">` nativo o aceptar `onNavigate` como prop. Ya lo hicimos en `MenuDrawer`. |
300
+ | `next/font/google` | Cargar fonts en CSS global de cada app, no en el componente |
301
+ | `next/dynamic` lazy load | Cada app decide cuándo lazy-loadear el componente |
302
+ | Web Speech API | Wraperar en context propio (`SpeechProvider`) que sea opcional |
303
+
304
+ ### Resumen
305
+
306
+ El paquete es **agnóstico al framework**. Las "diferencias", entre Next y Vite
307
+ viven en 5 líneas de JSX en cada `PortalUIProviders.jsx` de cada app. Todo lo
308
+ demás (componentes, hooks, styles) es código portable.
309
+
310
+ ---
311
+
312
+ ## Workflow de desarrollo
313
+
314
+ ### Local (varios escenarios)
315
+
316
+ #### A. `file:` link (recomendado para dev de día a día)
317
+
318
+ En el `package.json` de cada app:
319
+
320
+ ```json
321
+ "@crtobias/portal-ui": "file:../portal-ui"
322
+ ```
323
+
324
+ `npm install` crea un symlink: `node_modules/@crtobias/portal-ui` → `../portal-ui/`. **Cualquier cambio en `portal-ui/src/` aparece al toque** en las apps con HMR. **No tenés que publicar nada.**
325
+
326
+ Cuando hagas un cambio que rompa, ambas apps lo ven en el siguiente reload. Cuando estás contento, lo publicás.
327
+
328
+ > Ojo Vite: a veces no invalida el cache de file: deps con HMR. Si parece "viejo":
329
+ > ```bash
330
+ > rm -rf node_modules/.vite && npm run dev
331
+ > ```
332
+
333
+ #### B. `npm link`
334
+
335
+ Mismo efecto que `file:`, pero global a tu máquina:
336
+
337
+ ```bash
338
+ cd portal-ui && npm link
339
+
340
+ cd ../editor-template-front && npm link @crtobias/portal-ui
341
+ cd ../cms-editor-front && npm link @crtobias/portal-ui
342
+ ```
343
+
344
+ Para desconectar:
345
+
346
+ ```bash
347
+ cd editor-template-front && npm unlink @crtobias/portal-ui && npm install
348
+ ```
349
+
350
+ #### C. Versión publicada (producción / otro dev sin acceso al disco)
351
+
352
+ ```json
353
+ "@crtobias/portal-ui": "^1.0.0"
354
+ ```
355
+
356
+ ```bash
357
+ npm install
358
+ ```
359
+
360
+ En este modo NO se ven cambios locales. Hay que publicar.
361
+
362
+ ### ¿Cuándo necesito publicar?
363
+
364
+ | Acción | Local con `file:` / `npm link` | Producción / otro dev |
365
+ |---|---|---|
366
+ | Editar un .scss / .jsx | ✅ instantáneo | ❌ publicar nueva versión |
367
+ | Agregar un componente | ✅ instantáneo | ❌ publicar nueva versión |
368
+ | Cambiar API (props, exports) | ✅ instantáneo | ❌ publicar major / minor |
369
+ | Bug fix urgente en prod | — | ❌ publicar patch |
370
+
371
+ **Regla:** publicás cuando alguien (incluyendo CI) que no tiene tu `~/Desktop/portal-ui` necesita el cambio.
372
+
373
+ ---
374
+
375
+ ## Cómo agregar un componente nuevo
376
+
377
+ Te tomo un caso concreto: querés agregar un componente `Newsletter` para
378
+ mostrar un formulario de suscripción.
379
+
380
+ ### 1. Crear los archivos en `portal-ui`
381
+
382
+ ```
383
+ src/components/Newsletter/
384
+ Newsletter.jsx
385
+ Newsletter.module.scss
386
+ ```
387
+
388
+ ```jsx
389
+ // src/components/Newsletter/Newsletter.jsx
390
+ 'use client'
391
+
392
+ import { useState } from 'react'
393
+ import styles from './Newsletter.module.scss'
394
+ import { useAdapters } from '../../adapters/AdaptersContext.jsx'
395
+ import { useTheme } from '../../context/SiteConfigContext.jsx'
396
+
397
+ export default function Newsletter({ titulo = 'Suscribite' }) {
398
+ const { fetcher } = useAdapters()
399
+ const theme = useTheme()
400
+ const [email, setEmail] = useState('')
401
+
402
+ const handleSubmit = async (e) => {
403
+ e.preventDefault()
404
+ await fetcher('/api/portal/newsletter', {
405
+ method: 'POST',
406
+ body: JSON.stringify({ email }),
407
+ })
408
+ }
409
+
410
+ return (
411
+ <form
412
+ className={styles.container}
413
+ style={{ '--primary': theme.primary }}
414
+ onSubmit={handleSubmit}
415
+ >
416
+ <h3 className={styles.titulo}>{titulo}</h3>
417
+ <input
418
+ type="email"
419
+ value={email}
420
+ onChange={e => setEmail(e.target.value)}
421
+ placeholder="tu@email.com"
422
+ />
423
+ <button type="submit">Suscribirme</button>
424
+ </form>
425
+ )
426
+ }
427
+ ```
428
+
429
+ ```scss
430
+ /* src/components/Newsletter/Newsletter.module.scss */
431
+ @use "../../styles/index" as *;
432
+
433
+ .container { /* ... */ }
434
+ .titulo { color: var(--primary); }
435
+ ```
436
+
437
+ ### 2. Reglas para el código del componente
438
+
439
+ | ✅ Hacer | ❌ Evitar |
440
+ |---|---|
441
+ | `import { useAdapters }` para `Image`, `Link`, `fetcher` | `import Link from 'next/link'` |
442
+ | `import { useTheme }` para colores/fonts del site | acceder a `process.env` |
443
+ | Paths relativos: `../../adapters/AdaptersContext.jsx` | Path aliases: `@/components/...` |
444
+ | `'use client'` si usa hooks o estado | `async function` si va a usar hooks (ver split data/view abajo) |
445
+ | Para componentes que necesitan data: aceptar `articles`/`article` como prop | Hacer `await fetch(...)` dentro del componente del paquete |
446
+ | Para tracking client: `useAdapters().fetcher` | `process.env.NEXT_PUBLIC_X` |
447
+
448
+ ### 3. Si tu componente necesita fetchear (split data/view)
449
+
450
+ Patrón: el paquete expone el **View**, cada app implementa la **data layer**.
451
+
452
+ ```jsx
453
+ // portal-ui/src/components/Newsletter/NewsletterView.jsx
454
+ 'use client'
455
+ export default function NewsletterView({ subscribers, onSubmit }) {
456
+ return (
457
+ <div>
458
+ <span>{subscribers.length} suscriptos</span>
459
+ <button onClick={onSubmit}>Suscribirme</button>
460
+ </div>
461
+ )
462
+ }
463
+ ```
464
+
465
+ ```jsx
466
+ // editor-template-front/src/components/Newsletter/Newsletter.jsx (data layer)
467
+ import { backendFetch } from '@/lib/backendClient'
468
+ import { NewsletterView } from '@crtobias/portal-ui'
469
+
470
+ export default async function Newsletter({ settings }) {
471
+ const subs = await fetchSubscribers(backendFetch)
472
+ return <NewsletterView subscribers={subs} />
473
+ }
474
+ ```
475
+
476
+ ```jsx
477
+ // cms-editor-front/src/previewHome/components/Newsletter/Newsletter.jsx (data layer)
478
+ import { useEffect, useState } from 'react'
479
+ import { backendFetch } from '@/lib/backendClient'
480
+ import { NewsletterView } from '@crtobias/portal-ui'
481
+
482
+ export default function Newsletter({ settings }) {
483
+ const [subs, setSubs] = useState([])
484
+ useEffect(() => { fetchSubscribers(backendFetch).then(setSubs) }, [])
485
+ return <NewsletterView subscribers={subs} />
486
+ }
487
+ ```
488
+
489
+ Mismos componentes que ya hicieron split data/view: `Feed`, `Hero`, `Recommended`,
490
+ `Cabezal`, `Banner`, `Clima`, `TextWrap`, `ArticleBody`.
491
+
492
+ ### 4. Agregar al barrel
493
+
494
+ ```js
495
+ // src/index.js
496
+ export { default as Newsletter } from './components/Newsletter/Newsletter.jsx'
497
+ // o si es split data/view:
498
+ export { default as NewsletterView } from './components/Newsletter/NewsletterView.jsx'
499
+ ```
500
+
501
+ ### 5. (Opcional) Shim en las apps para no tocar callers
502
+
503
+ Si ya hay código importando `@/components/Newsletter/Newsletter`, mantené ese
504
+ path con un re-export:
505
+
506
+ ```jsx
507
+ // editor-template-front/src/components/Newsletter/Newsletter.jsx
508
+ 'use client'
509
+ export { Newsletter as default } from '@crtobias/portal-ui'
510
+ ```
511
+
512
+ Así no hay que tocar 20 callers — el caller sigue importando como antes, pero
513
+ termina yendo al paquete.
514
+
515
+ ### 6. Probar local
516
+
517
+ Con `file:` link o `npm link` activo, las dos apps levantan con el componente
518
+ nuevo sin publicar:
519
+
520
+ ```bash
521
+ cd ~/Desktop/editor-template-front && npm run dev # http://localhost:3000
522
+ cd ~/Desktop/cms-editor-front && npm run dev # http://localhost:5173
523
+ ```
524
+
525
+ ### 7. Cuando funcione, publicar
526
+
527
+ ```bash
528
+ cd ~/Desktop/portal-ui
529
+ npm run release:minor # componente nuevo = minor
530
+ git push --follow-tags
531
+ ```
532
+
533
+ (Si CI hace publish automático, basta con `git push`.)
534
+
535
+ ### 8. En las apps, traer la versión nueva
536
+
537
+ ```bash
538
+ cd ~/Desktop/editor-template-front && npm install @crtobias/portal-ui@latest
539
+ cd ~/Desktop/cms-editor-front && npm install @crtobias/portal-ui@latest
540
+ ```
541
+
542
+ ---
543
+
544
+ ## Publicar a npm
545
+
546
+ ### Setup una vez por máquina
547
+
548
+ ```bash
549
+ npm login # con tu cuenta crtobiasdev
550
+ ```
551
+
552
+ ### Publicar
553
+
554
+ ```bash
555
+ cd ~/Desktop/portal-ui
556
+
557
+ npm run release:patch # 1.0.0 → 1.0.1 bug fix
558
+ npm run release:minor # 1.0.0 → 1.1.0 componente nuevo / feature
559
+ npm run release:major # 1.0.0 → 2.0.0 breaking change (rename, remove prop)
560
+ ```
561
+
562
+ Los scripts hacen `npm version <bump>` (commit + tag automáticos) y
563
+ `npm publish --access public` en una sola pasada.
564
+
565
+ Después:
566
+
567
+ ```bash
568
+ git push --follow-tags
569
+ ```
570
+
571
+ ---
572
+
573
+ ## Trabajar entre varios devs
574
+
575
+ ### El problema
576
+
577
+ Si sos el único que publica, hay que decidir cómo:
578
+ - Los otros devs **proponen** cambios al paquete
579
+ - Quién **autoriza** y **publica**
580
+
581
+ ### Modelo recomendado: CI publica al merge
582
+
583
+ **Setup:**
584
+
585
+ 1. **Cada dev tiene cuenta de GitHub propia** (no compartir la tuya)
586
+ 2. **Sumalos como collaborators** al repo `portal-ui` en GitHub
587
+ (Settings → Collaborators)
588
+ 3. **La cuenta de npm queda SOLO tuya** — los devs no necesitan acceso a npm
589
+ 4. **GitHub Actions publica automático** cuando vos mergeás a `main`
590
+
591
+ **Workflow para un dev nuevo:**
592
+
593
+ ```bash
594
+ # Una vez
595
+ git clone git@github.com:crtobias/portal-ui.git
596
+ git clone git@github.com:crtobias/editor-template-front.git
597
+ git clone git@github.com:crtobias/cms-editor-front.git
598
+
599
+ # Asegurarse que las apps usen el paquete local
600
+ cd portal-ui && npm link
601
+ cd ../editor-template-front && npm link @crtobias/portal-ui
602
+ cd ../cms-editor-front && npm link @crtobias/portal-ui
603
+ ```
604
+
605
+ **Para cada cambio:**
606
+
607
+ ```bash
608
+ cd portal-ui
609
+ git checkout -b mi-feature
610
+ # ...editar...
611
+ git commit -am "feat: agregar Newsletter"
612
+ git push origin mi-feature
613
+ # Crear PR en GitHub
614
+ ```
615
+
616
+ **Vos (mantenedor):**
617
+ 1. Revisás el PR
618
+ 2. Mergeás a `main`
619
+ 3. CI corre `npm publish` automático
620
+ 4. Avisás en Slack/Discord: "Nueva versión 1.1.0 — bumpean en las apps"
621
+
622
+ ### GitHub Actions — workflow de auto-publish
623
+
624
+ Crear `.github/workflows/publish.yml`:
625
+
626
+ ```yaml
627
+ name: Publish to npm
628
+
629
+ on:
630
+ push:
631
+ branches: [main]
632
+ paths:
633
+ - 'src/**'
634
+ - 'package.json'
635
+
636
+ jobs:
637
+ publish:
638
+ runs-on: ubuntu-latest
639
+ steps:
640
+ - uses: actions/checkout@v4
641
+ with:
642
+ fetch-depth: 0
643
+ token: ${{ secrets.GITHUB_TOKEN }}
644
+
645
+ - uses: actions/setup-node@v4
646
+ with:
647
+ node-version: '20'
648
+ registry-url: 'https://registry.npmjs.org'
649
+
650
+ - name: Bump patch version
651
+ run: |
652
+ git config user.name "github-actions[bot]"
653
+ git config user.email "github-actions[bot]@users.noreply.github.com"
654
+ npm version patch -m "chore: release %s [skip ci]"
655
+ git push --follow-tags
656
+
657
+ - name: Publish
658
+ run: npm publish --access public
659
+ env:
660
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
661
+ ```
662
+
663
+ **Setup del token:**
664
+ 1. En npmjs.com → Profile → Access Tokens → "Generate New Token"
665
+ (tipo "Automation")
666
+ 2. Copiá el token
667
+ 3. En GitHub repo → Settings → Secrets and variables → Actions → New secret
668
+ - Name: `NPM_TOKEN`
669
+ - Value: pegá el token
670
+
671
+ > Este workflow bumpea **patch** cada merge a main. Para minor/major, los devs
672
+ > ponen en el commit `[minor]` o `[major]` y agregás lógica al workflow para
673
+ > parsear eso. O publicás manualmente para versiones grandes.
674
+
675
+ ### Si NO querés CI
676
+
677
+ Alternativa simple: los devs proponen PRs, vos publicás manualmente al mergear:
678
+
679
+ ```bash
680
+ git checkout main && git pull
681
+ npm run release:minor
682
+ git push --follow-tags
683
+ ```
684
+
685
+ ### Permisos a tener en cuenta
686
+
687
+ | Recurso | A quién dar acceso | Cómo |
688
+ |---|---|---|
689
+ | Repo de GitHub `portal-ui` | Todos los devs | Settings → Collaborators (write para mergear, read para PRs) |
690
+ | Repos de las apps | Todos los devs | Igual |
691
+ | Cuenta npm `crtobiasdev` | Solo vos | — |
692
+ | `NPM_TOKEN` en GitHub Secrets | Generado por vos, accesible solo por CI | — |
693
+ | Bump de versión | Solo CI (o vos) | Workflow / manual |
694
+
695
+ ---
696
+
697
+ ## Qué NO va en el paquete
698
+
699
+ | ❌ NO | Por qué | Solución |
700
+ |---|---|---|
701
+ | `next/*` (Image, Link, font, navigation) | Acopla a Next | Inyectar via `useAdapters()` |
702
+ | `@mui/*` | Solo CMS lo usa | Vive en `cms-editor-front` |
703
+ | `@dnd-kit/*` | Solo el editor lo usa | Vive en `cms-editor-front` |
704
+ | `fs`, env del server, secrets | No es código de cliente | Cada app |
705
+ | `React.cache()` | Acopla a Next runtime | Cada app lo usa con `createArticlePool` |
706
+ | Path alias `@/` | El paquete no controla resolver | Imports relativos |
707
+ | `process.env.X` | No portable | Settings via prop o context |
708
+
709
+ ---
710
+
711
+ ## Estructura
712
+
713
+ ```
714
+ src/
715
+ ├── adapters/
716
+ │ └── AdaptersContext.jsx AdaptersProvider, useAdapters
717
+ ├── context/
718
+ │ ├── SiteConfigContext.jsx SiteConfigProvider + hooks
719
+ │ └── SpeechContext.jsx SpeechProvider, useSpeech
720
+ ├── data/
721
+ │ ├── ArticlePoolContext.jsx createArticlePool, useArticlePool
722
+ │ ├── useArticles.js hook universal
723
+ │ └── index.js
724
+ ├── constants/imageSizes.js
725
+ ├── utils/
726
+ │ ├── fechaHora.js getFechaHora
727
+ │ └── colorContrast.js contrastRatio, hexToCssFilter, ensureContrast
728
+ ├── styles/ SCSS partials compartidos
729
+ │ ├── index.scss
730
+ │ ├── mixins/
731
+ │ └── variables/
732
+ └── components/
733
+ ├── UI/ AspectImage, FocalImage, Icon, IconSmall,
734
+ │ PageWrapper, ToolTip
735
+ ├── DateTime/
736
+ ├── AuthorBlock/ 4 variants
737
+ ├── Breadcrumb/ 5 variants
738
+ ├── ShareBlock/ 2 variants
739
+ ├── Cards/
740
+ │ ├── ArticleCard/
741
+ │ ├── Bajada/ 2 variants
742
+ │ └── ArticleBody/ View only (data layer en cada app)
743
+ ├── Headers/HeaderSimple/ HeaderSimpleSwitch (+ forceMode para CMS)
744
+ │ Desktop / Mobile / Compact / Amp
745
+ │ + sub-componentes (CategoriesBar,
746
+ │ LiveBanner, MenuDrawer, etc.)
747
+ ├── Footers/FooterSimple/
748
+ ├── Blocks/ Containers que iteran widgets via registry
749
+ ├── ArticleHero/ 8 variants
750
+ ├── ArticleHeroFull/
751
+ ├── ArticleSidebar/
752
+ ├── EditorOutput/ Renderer de Editor.js (con AMP)
753
+ ├── EditorOutputFull/
754
+ ├── SpeechButton/
755
+ ├── SpeechPlayerBar/
756
+ ├── SpeechProviderWrapper/
757
+ ├── Feed/ FeedView (data layer en cada app)
758
+ ├── Hero/ HeroView
759
+ ├── Recommended/ RecommendedView
760
+ ├── Cabezal/ CabezalView + 18 variants + 9 CardCabezal
761
+ ├── Banner/ BannerView + BannerDisplay (tracking)
762
+ ├── Clima/ ClimaView
763
+ ├── TextWrap/ TextWrapView
764
+ ├── DolarTicker/ Self-fetching client
765
+ └── DolarTickerOriginal/
766
+ ```
767
+
768
+ ---
769
+
770
+ ## API exportada
771
+
772
+ ```js
773
+ import {
774
+ // Adapters
775
+ AdaptersProvider, useAdapters, useOptionalAdapters,
776
+
777
+ // Site config
778
+ SiteConfigProvider, PreviewThemeProvider,
779
+ useSiteConfig, useTheme, useRawConfig,
780
+ useCategories, useBanners, useComputed, useInfoPages,
781
+
782
+ // Article pool
783
+ ArticlePoolProvider, useArticlePool, createArticlePool,
784
+ useArticles,
785
+
786
+ // Speech
787
+ SpeechProvider, useSpeech,
788
+
789
+ // Utils
790
+ getFechaHora,
791
+ contrastRatio, hexToCssFilter, ensureContrast,
792
+
793
+ // Constants
794
+ IMAGE_SIZES,
795
+
796
+ // UI primitives
797
+ AspectImage, FocalImage, Icon, IconSmall, PageWrapper, ToolTip,
798
+
799
+ // Componentes
800
+ DateTime, AuthorBlock, Breadcrumb, ShareBlock,
801
+ ArticleCard, Bajada,
802
+ ArticleHero, ArticleHeroFull, ArticleSidebar,
803
+ HeaderSimpleSwitch, HeaderSimpleDesktop, HeaderSimpleDesktopCompact,
804
+ HeaderSimpleMobile, HeaderSimpleAmp,
805
+ FooterSimple,
806
+ BlockColumns, BlockColumnsBajada, BlockMain, BlockMainNarrow,
807
+ BlockMainSidebar, BlockStack, WidgetErrorBoundary,
808
+ EditorOutput, EditorBlocks, EditorOutputFull, EditorBlocksFull,
809
+ SpeechButton, SpeechPlayerBar, SpeechProviderWrapper,
810
+ DolarTicker, DolarTickerOriginal,
811
+
812
+ // Views (data layer en cada app)
813
+ FeedView, HeroView, RecommendedView, CabezalView,
814
+ BannerView, BannerDisplay, ClimaView,
815
+ TextWrapView, ArticleBodyView,
816
+ } from '@crtobias/portal-ui'
817
+ ```
818
+
819
+ ---
820
+
821
+ ## Convención de versiones
822
+
823
+ - **patch** (`1.0.0 → 1.0.1`): bug fix sin cambios de API
824
+ - **minor** (`1.0.0 → 1.1.0`): componente nuevo, prop opcional nueva
825
+ - **major** (`1.0.0 → 2.0.0`): breaking change (rename, remove prop,
826
+ cambio de shape, cambio de signature de hook)
827
+
828
+ Pinear con `^` para auto-update minor/patch en las apps. Para producción
829
+ estable, pinear exact (`"@crtobias/portal-ui": "1.1.2"`).