@crtobiasdelsud/portal-ui 1.0.32 → 1.1.2

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 (58) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +2 -1
  3. package/src/components/ArticleHero/variants/V0/V0.jsx +2 -1
  4. package/src/components/ArticleHero/variants/V0Desktop/V0Desktop.jsx +2 -1
  5. package/src/components/ArticleHero/variants/V0Tablet/V0Tablet.jsx +2 -1
  6. package/src/components/ArticleHero/variants/V1/V1.jsx +2 -1
  7. package/src/components/ArticleHeroFull/ArticleHeroFull.jsx +2 -1
  8. package/src/components/Blocks/BlockColumns/BlockColumns.module.scss +5 -5
  9. package/src/components/Cabezal/CardCabezal/variants/Amp/Amp.jsx +3 -1
  10. package/src/components/Cabezal/CardCabezal/variants/Carrusel/Carrusel.jsx +7 -5
  11. package/src/components/Cabezal/CardCabezal/variants/Compact/Compact.jsx +6 -3
  12. package/src/components/Cabezal/CardCabezal/variants/Default/Default.jsx +6 -3
  13. package/src/components/Cabezal/CardCabezal/variants/Featured/Featured.jsx +7 -4
  14. package/src/components/Cabezal/CardCabezal/variants/FeaturedDuo/FeaturedDuo.jsx +8 -4
  15. package/src/components/Cabezal/CardCabezal/variants/FeaturedHorizontal/FeaturedHorizontal.jsx +8 -4
  16. package/src/components/Cabezal/CardCabezal/variants/Medium/Medium.jsx +6 -3
  17. package/src/components/Cabezal/CardCabezal/variants/Ranked/Ranked.jsx +2 -1
  18. package/src/components/Cabezal/variants/Carrusel/Carrusel.jsx +1 -1
  19. package/src/components/Cabezal/variants/Categoria/Categoria.jsx +1 -1
  20. package/src/components/Cabezal/variants/CategoriaDos/CategoriaDos.jsx +1 -1
  21. package/src/components/Cabezal/variants/Compact/Compact.jsx +1 -1
  22. package/src/components/Cabezal/variants/Default/Default.jsx +1 -1
  23. package/src/components/Cabezal/variants/Desktop/Desktop.jsx +1 -1
  24. package/src/components/Cabezal/variants/Duo/Duo.jsx +1 -1
  25. package/src/components/Cabezal/variants/DuoSinCopete/DuoSinCopete.jsx +1 -1
  26. package/src/components/Cabezal/variants/Horizontal/Horizontal.jsx +1 -1
  27. package/src/components/Cabezal/variants/LeeAdemas/LeeAdemas.jsx +2 -1
  28. package/src/components/Cabezal/variants/LoQueSeLee/LoQueSeLee.jsx +9 -4
  29. package/src/components/Cabezal/variants/Medium/Medium.jsx +1 -1
  30. package/src/components/Cabezal/variants/Mobile/Mobile.jsx +2 -2
  31. package/src/components/Cabezal/variants/Ranking/Ranking.jsx +1 -1
  32. package/src/components/Cabezal/variants/Tablet/Tablet.jsx +1 -1
  33. package/src/components/Cabezal/variants/Tres/Tres.jsx +1 -1
  34. package/src/components/Cabezal/variants/UnaDetallada/UnaDetallada.jsx +1 -1
  35. package/src/components/Cards/ArticleCard/ArticleCard.jsx +20 -14
  36. package/src/components/Cards/Bajada/variants/V1/V1.jsx +2 -1
  37. package/src/components/Cards/Bajada/variants/V2/V2.jsx +2 -1
  38. package/src/components/Carousel/Carousel.jsx +246 -68
  39. package/src/components/Carousel/Carousel.module.scss +30 -2
  40. package/src/components/Clima/ClimaView.jsx +1 -1
  41. package/src/components/EditorOutput/EditorOutput.jsx +35 -22
  42. package/src/components/EditorOutputFull/EditorOutputFull.jsx +32 -19
  43. package/src/components/Feed/variants/V1/V1.jsx +1 -1
  44. package/src/components/Footers/FooterSimple/FooterSimple.jsx +2 -2
  45. package/src/components/Headers/HeaderSimple/CategoriesBar/CategoriesBar.jsx +1 -1
  46. package/src/components/Headers/HeaderSimple/HeaderSimpleAmp/HeaderSimpleAmp.jsx +2 -2
  47. package/src/components/Headers/HeaderSimple/MenuDrawer/MenuDrawer.jsx +2 -2
  48. package/src/components/Hero/variants/V1/V1.jsx +14 -7
  49. package/src/components/Hero/variants/V2/V2.jsx +15 -8
  50. package/src/components/Hero/variants/V2/V2.module.scss +16 -34
  51. package/src/components/Hero/variants/V3/V3.jsx +15 -6
  52. package/src/components/Hero/variants/V3/V3.module.scss +10 -4
  53. package/src/utils/authorDisplay.js +48 -0
  54. package/src/utils/imageVariants.js +7 -2
  55. package/src/utils/imageVariants.test.js +22 -0
  56. package/src/utils/sanitizeHtml.js +275 -0
  57. package/src/utils/sanitizeHtml.test.js +53 -0
  58. package/src/utils/volanta.js +12 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,76 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased] — 2026-06-04
4
+
5
+ ### Added
6
+
7
+ Soporte de "Publicar como organización" en los bylines de tarjetas, heros y
8
+ carruseles. Hasta ahora solo `AuthorBlock` (detalle) contemplaba `publicarComoOrg`;
9
+ el resto mostraba el byline desde `autor.nombre` y quedaba **vacío** en las notas
10
+ publicadas como organización (el backend manda `autor: null`).
11
+
12
+ - **`utils/authorDisplay.js`** (nuevo): helper `resolveAuthorDisplay()` (puro) +
13
+ hook `useAuthorDisplay(autor, publicarComoOrg)`. Centraliza la lógica de
14
+ `AuthorBlock`: con organización (o sin autor) devuelve el nombre del sitio como
15
+ `displayName` y su logo como `avatarSrc`, sin enlazar a `/autor/`. Lee
16
+ `siteName`/`iconUrl` de `SiteConfigContext` (con default seguro si no hay provider).
17
+ - **Bylines consistentes**: `Cards/ArticleCard`, `Cabezal/CardCabezal/variants/`
18
+ `{Default, Compact, Featured, FeaturedDuo, FeaturedHorizontal, Medium, Carrusel}`,
19
+ `Cabezal/variants/LoQueSeLee` y `Hero/variants/{V1, V2, V3}` ahora muestran
20
+ "Por {nombre del sitio}" (y el logo como avatar donde corresponde) en notas de
21
+ organización, en vez de no mostrar autor. `Ranked`/`Amp` (sin byline) no cambian.
22
+ - **Sin cambios en la API pública**: los componentes ya recibían el `article`
23
+ (con `publicarComoOrg`); no cambian props ni shape. `AuthorBlock` no se tocó.
24
+
25
+ ### Fixed
26
+
27
+ Correcciones de marcado semántico, accesibilidad y validez de enlaces (HTML
28
+ correcto para indexación e intérpretes de pantalla). Sin cambios en la API
29
+ pública (mismos exports, mismos nombres y shape de props); sólo cambia el
30
+ markup/atributos que renderizan los componentes. Las clases de estilo se
31
+ preservan en todos los casos.
32
+
33
+ - **Headings del Hero**: `Hero/variants/V1` renderizaba el título como `<p>` y
34
+ `Hero/variants/V2` como `<span>`; ahora ambos usan `<h2>` (igual que V3),
35
+ conservando las clases `headline`/`headlineWrap`.
36
+ - **`alt` de imágenes de contenido**: en `EditorOutput` y `EditorOutputFull`, las
37
+ imágenes del cuerpo caen al epígrafe como texto alternativo antes de quedar con
38
+ `alt=""`, evitando dejar imágenes informativas sin descripción.
39
+ - **Enlace de `ArticleCard`**: se eliminó el atributo `rel="canonical"` del `<a>`
40
+ de navegación (inválido en anclas; el canonical sólo corresponde al `<link>` del
41
+ head).
42
+ - **Rutas de enlaces**: `ArticleCard`, `Feed/variants/V1`, `HeaderSimpleAmp` y
43
+ `MenuDrawer` ahora normalizan el `href` a `/${slug}` (con guardia a `#` cuando no
44
+ hay slug), alineándose con el patrón del resto de la librería.
45
+ - **"Lee además" como encabezado**: en `EditorOutput` y `EditorOutputFull` el
46
+ título de relacionados pasó de `<p>` a `<h3>`.
47
+ - **Enlace "VER MÁS" descriptivo**: las 15 variantes de `Cabezal` agregan
48
+ `aria-label="Ver más de <título de sección>"` al enlace, manteniendo el texto
49
+ visible "VER MÁS".
50
+ - **`alt` del clima**: los iconos horarios de `ClimaView` usaban la URL/código del
51
+ icono como `alt`; ahora usan la condición meteorológica (o `""` si no hay).
52
+ - **AMP `CardCabezal`**: el título se envuelve en `<h3>` (manteniendo el `<a>` y su
53
+ clase) y el copete se renderiza como texto plano vía `stripHtml` en lugar de
54
+ inyectar HTML.
55
+
56
+ ### Performance
57
+
58
+ Optimización de LCP del destacado de portada. Sin cambios en la API pública
59
+ (mismos exports y shape de props); sólo se pasan props que `AspectImage` ya
60
+ acepta.
61
+
62
+ - **Imágenes responsive en `Hero`**: `Hero/variants/V1`, `V2` y `V3` ahora pasan
63
+ `variants={article.imagen?.variants ?? null}` y `sizes="(max-width: 768px) 100vw, 66vw"`
64
+ a `AspectImage`, de modo que el destacado de portada sirve `srcset` y el
65
+ navegador baja la resolución justa según el viewport (antes bajaba siempre la
66
+ variante `large`).
67
+ - **Prioridad de carga condicionada**: las tres variantes usan
68
+ `priority={important}` para emitir `fetchpriority=high` sólo en el destacado
69
+ principal. `V1` no tenía prioridad (ahora la recibe cuando es `important`); `V2`
70
+ y `V3` emitían `priority` fijo (siempre `true`), lo que producía múltiples
71
+ `fetchpriority=high` en una misma página con varios Hero; ahora queda acotado al
72
+ destacado principal.
73
+
3
74
  ## [Unreleased] — 2026-05-18
4
75
 
5
76
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crtobiasdelsud/portal-ui",
3
- "version": "1.0.32",
3
+ "version": "1.1.2",
4
4
  "description": "Componentes compartidos entre el portal (Next) y el CMS (Vite) — widgets, views, providers para adapters y article pool.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -16,6 +16,7 @@
16
16
  "CHANGELOG.md"
17
17
  ],
18
18
  "scripts": {
19
+ "test": "node --test src/**/*.test.js",
19
20
  "release:patch": "npm version patch && npm publish --access public",
20
21
  "release:minor": "npm version minor && npm publish --access public",
21
22
  "release:major": "npm version major && npm publish --access public"
@@ -1,5 +1,6 @@
1
1
  import shared from '../../ArticleHero.module.scss'
2
2
  import s from './V0.module.scss'
3
+ import { sanitizeInlineHtml } from '../../../../utils/sanitizeHtml.js'
3
4
 
4
5
  export default function V0({ isAmp, inlineStyle, titulo, volanta, copete, ImgEl, ExtrasEl, imgWrapClass, noImgMod }) {
5
6
  const VolantaEl = volanta
@@ -9,7 +10,7 @@ export default function V0({ isAmp, inlineStyle, titulo, volanta, copete, ImgEl,
9
10
  const CopeteEl = copete
10
11
  ? <div
11
12
  className={isAmp ? 'article-hero__copete' : `${shared.copete} ${s.copete}`}
12
- dangerouslySetInnerHTML={{ __html: copete }}
13
+ dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(copete) }}
13
14
  />
14
15
  : null
15
16
 
@@ -1,5 +1,6 @@
1
1
  import shared from '../../ArticleHero.module.scss'
2
2
  import s from './V0Desktop.module.scss'
3
+ import { sanitizeInlineHtml } from '../../../../utils/sanitizeHtml.js'
3
4
 
4
5
  export default function V0Desktop({ isAmp, inlineStyle, titulo, volanta, copete, ImgEl, ExtrasEl, imgWrapClass, noImgMod }) {
5
6
  const VolantaEl = volanta
@@ -9,7 +10,7 @@ export default function V0Desktop({ isAmp, inlineStyle, titulo, volanta, copete,
9
10
  const CopeteEl = copete
10
11
  ? <div
11
12
  className={isAmp ? 'article-hero__copete' : `${shared.copete} ${s.copete}`}
12
- dangerouslySetInnerHTML={{ __html: copete }}
13
+ dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(copete) }}
13
14
  />
14
15
  : null
15
16
 
@@ -1,5 +1,6 @@
1
1
  import shared from '../../ArticleHero.module.scss'
2
2
  import s from './V0Tablet.module.scss'
3
+ import { sanitizeInlineHtml } from '../../../../utils/sanitizeHtml.js'
3
4
 
4
5
  export default function V0Tablet({ isAmp, inlineStyle, titulo, volanta, copete, ImgEl, ExtrasEl, imgWrapClass, noImgMod }) {
5
6
  const VolantaEl = volanta
@@ -9,7 +10,7 @@ export default function V0Tablet({ isAmp, inlineStyle, titulo, volanta, copete,
9
10
  const CopeteEl = copete
10
11
  ? <div
11
12
  className={isAmp ? 'article-hero__copete' : `${shared.copete} ${s.copete}`}
12
- dangerouslySetInnerHTML={{ __html: copete }}
13
+ dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(copete) }}
13
14
  />
14
15
  : null
15
16
 
@@ -1,5 +1,6 @@
1
1
  import shared from '../../ArticleHero.module.scss'
2
2
  import s from './V1.module.scss'
3
+ import { sanitizeInlineHtml } from '../../../../utils/sanitizeHtml.js'
3
4
 
4
5
  export default function V1({ isAmp, inlineStyle, titulo, volanta, copete, ImgEl, ExtrasEl, imgWrapClass, noImgMod }) {
5
6
  const VolantaEl = volanta
@@ -9,7 +10,7 @@ export default function V1({ isAmp, inlineStyle, titulo, volanta, copete, ImgEl,
9
10
  const CopeteEl = copete
10
11
  ? <div
11
12
  className={isAmp ? 'article-hero__copete' : `${shared.copete} ${s.copete}`}
12
- dangerouslySetInnerHTML={{ __html: copete }}
13
+ dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(copete) }}
13
14
  />
14
15
  : null
15
16
 
@@ -4,6 +4,7 @@ import styles from './ArticleHeroFull.module.scss'
4
4
  import { useSiteConfig } from '../../context/SiteConfigContext.jsx'
5
5
  import Carousel from '../Carousel/Carousel.jsx'
6
6
  import AspectImage from '../UI/AspectImage/AspectImage.jsx'
7
+ import { sanitizeInlineHtml } from '../../utils/sanitizeHtml.js'
7
8
 
8
9
  export default function ArticleHeroFull({ titulo, copete, imagen, imagenes, imagenEpigrafe, focalPoint, categoria }) {
9
10
  const { config } = useSiteConfig()
@@ -65,7 +66,7 @@ export default function ArticleHeroFull({ titulo, copete, imagen, imagenes, imag
65
66
  </div>
66
67
  )}
67
68
  <h1 className={styles.titulo}>{titulo}</h1>
68
- {copete && <div className={styles.copete} dangerouslySetInnerHTML={{ __html: copete }} />}
69
+ {copete && <div className={styles.copete} dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(copete) }} />}
69
70
  </div>
70
71
  </div>
71
72
  )
@@ -9,7 +9,7 @@
9
9
  display: grid;
10
10
  gap: 20px;
11
11
 
12
- @media (min-width: 1000px) {
12
+ @include respond(desktop) {
13
13
  gap: 30px;
14
14
  }
15
15
  }
@@ -20,7 +20,7 @@
20
20
  "recommended"
21
21
  "feed";
22
22
 
23
- @media (min-width: 1000px) {
23
+ @include respond(desktop) {
24
24
  grid-template-columns: repeat(4, 1fr);
25
25
  grid-template-areas: "recommended hero hero feed";
26
26
  }
@@ -35,7 +35,7 @@
35
35
  "fantasma"
36
36
  "feed";
37
37
 
38
- @media (min-width: 1000px) {
38
+ @include respond(desktop) {
39
39
  grid-template-columns: repeat(4, 1fr);
40
40
  grid-template-areas:
41
41
  "hero hero hero hero"
@@ -49,7 +49,7 @@
49
49
  "recommended"
50
50
  "feed";
51
51
 
52
- @media (min-width: 1000px) {
52
+ @include respond(desktop) {
53
53
  gap: 15px;
54
54
  grid-template-columns: repeat(4, 1fr);
55
55
  grid-template-areas: "recommended feed hero hero";
@@ -63,7 +63,7 @@
63
63
  flex-direction: column;
64
64
  padding: 10px 0;
65
65
 
66
- @media (min-width: 1000px) {
66
+ @include respond(desktop) {
67
67
  padding: 0;
68
68
  }
69
69
  }
@@ -1,3 +1,5 @@
1
+ import { volantaWithStop } from '../../../../../utils/volanta.js'
2
+
1
3
  export default function Amp({ article, rank }) {
2
4
  const { titulo, volanta, copete, imagen, slug } = article
3
5
  const href = slug ? `/${slug}` : '#'
@@ -10,7 +12,7 @@ export default function Amp({ article, rank }) {
10
12
  </a>
11
13
  )}
12
14
  <div className="card-cabezal__body">
13
- {volanta && <span className="card-cabezal__volanta">{volanta}.</span>}
15
+ {volanta && <span className="card-cabezal__volanta">{volantaWithStop(volanta)}</span>}
14
16
  {titulo && <a href={href} className="card-cabezal__titulo">{titulo}</a>}
15
17
  {copete && <div className="card-cabezal__copete" dangerouslySetInnerHTML={{ __html: copete }} />}
16
18
  </div>
@@ -1,12 +1,14 @@
1
1
  import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
2
+ import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
2
3
  import styles from './Carrusel.module.scss'
3
4
  import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
4
5
 
5
6
  export default function CarruselCard({ article }) {
6
7
 
7
8
  const { Link } = useAdapters()
8
- const { titulo, imagen, slug, autor } = article
9
+ const { titulo, imagen, slug, autor, publicarComoOrg } = article
9
10
  const href = slug ? `/${slug}` : '#'
11
+ const { displayName, avatarSrc } = useAuthorDisplay(autor, publicarComoOrg)
10
12
 
11
13
  return (
12
14
  <Tooltip text={titulo}>
@@ -22,13 +24,13 @@ export default function CarruselCard({ article }) {
22
24
  )}
23
25
  <div className={styles.body}>
24
26
  {titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
25
- {autor?.nombre && <span className={styles.autor}>Por {autor.nombre}</span>}
27
+ {displayName && <span className={styles.autor}>Por {displayName}</span>}
26
28
  </div>
27
- {autor?.avatar && (
29
+ {avatarSrc && (
28
30
  <div className={styles.authorAvatarWrap}>
29
31
  <img
30
- src={autor.avatar}
31
- alt={autor.nombre ?? ''}
32
+ src={avatarSrc}
33
+ alt={displayName ?? ''}
32
34
  className={styles.authorAvatar}
33
35
  />
34
36
  </div>
@@ -1,13 +1,16 @@
1
1
  import styles from '../../CardCabezal.module.scss'
2
2
  import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
3
+ import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
3
4
  import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
4
5
  import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
6
+ import { volantaWithStop } from '../../../../../utils/volanta.js'
5
7
 
6
8
  export default function Compact({ article }) {
7
9
 
8
10
  const { Link } = useAdapters()
9
- const { titulo, volanta, imagen, slug, autor, focalPoint } = article
11
+ const { titulo, volanta, imagen, slug, autor, publicarComoOrg, focalPoint } = article
10
12
  const href = slug ? `/${slug}` : '#'
13
+ const { displayName } = useAuthorDisplay(autor, publicarComoOrg)
11
14
 
12
15
  return (
13
16
  <Tooltip text={titulo}>
@@ -25,10 +28,10 @@ export default function Compact({ article }) {
25
28
  )}
26
29
  <div className={styles.body}>
27
30
  <div className={styles.header}>
28
- {volanta && <span className={styles.volanta}>{volanta}.</span>}
31
+ {volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
29
32
  {titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
30
33
  </div>
31
- {autor?.nombre && <span className={styles.autor}>Por {autor.nombre}</span>}
34
+ {displayName && <span className={styles.autor}>Por {displayName}</span>}
32
35
  </div>
33
36
  </article>
34
37
  </Tooltip>
@@ -1,13 +1,16 @@
1
1
  import styles from '../../CardCabezal.module.scss'
2
2
  import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
3
+ import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
3
4
  import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
4
5
  import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
6
+ import { volantaWithStop } from '../../../../../utils/volanta.js'
5
7
 
6
8
  export default function Default({ article }) {
7
9
 
8
10
  const { Link } = useAdapters()
9
- const { titulo, volanta, imagen, slug, autor, focalPoint } = article
11
+ const { titulo, volanta, imagen, slug, autor, publicarComoOrg, focalPoint } = article
10
12
  const href = slug ? `/${slug}` : '#'
13
+ const { displayName } = useAuthorDisplay(autor, publicarComoOrg)
11
14
 
12
15
  return (
13
16
  <Tooltip text={titulo}>
@@ -24,10 +27,10 @@ export default function Default({ article }) {
24
27
  )}
25
28
  <div className={styles.body}>
26
29
  <div className={styles.header}>
27
- {volanta && <span className={styles.volanta}>{volanta}.</span>}
30
+ {volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
28
31
  {titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
29
32
  </div>
30
- {autor?.nombre && <span className={styles.autor}>Por {autor.nombre}</span>}
33
+ {displayName && <span className={styles.autor}>Por {displayName}</span>}
31
34
  </div>
32
35
  </article>
33
36
  </Tooltip>
@@ -1,13 +1,16 @@
1
1
  import styles from '../../CardCabezal.module.scss'
2
2
  import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
3
+ import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
3
4
  import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
4
5
  import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
6
+ import { volantaWithStop } from '../../../../../utils/volanta.js'
5
7
 
6
8
  export default function Featured({ article, large }) {
7
9
 
8
10
  const { Link } = useAdapters()
9
- const { titulo, volanta, copete, imagen, slug, autor, focalPoint } = article
11
+ const { titulo, volanta, copete, imagen, slug, autor, publicarComoOrg, focalPoint } = article
10
12
  const href = slug ? `/${slug}` : '#'
13
+ const { displayName } = useAuthorDisplay(autor, publicarComoOrg)
11
14
 
12
15
  return (
13
16
  <Tooltip text={titulo}>
@@ -25,10 +28,10 @@ export default function Featured({ article, large }) {
25
28
  )}
26
29
  <div className={styles.body}>
27
30
  <div className={styles.header}>
28
- {volanta && <span className={styles.volanta}>{volanta}.</span>}
31
+ {volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
29
32
  {titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
30
- </div>
31
- {autor?.nombre && <span className={styles.autor}>Por {autor.nombre}</span>}
33
+ </div>
34
+ {displayName && <span className={styles.autor}>Por {displayName}</span>}
32
35
  </div>
33
36
  </article>
34
37
  </Tooltip>
@@ -1,13 +1,17 @@
1
1
  import styles from '../../CardCabezal.module.scss'
2
2
  import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
3
+ import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
4
+ import { sanitizeInlineHtml } from '../../../../../utils/sanitizeHtml.js'
3
5
  import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
4
6
  import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
7
+ import { volantaWithStop } from '../../../../../utils/volanta.js'
5
8
 
6
9
  export default function FeaturedDuo({ article }) {
7
10
 
8
11
  const { Link } = useAdapters()
9
- const { titulo, volanta, copete, imagen, slug, autor, focalPoint } = article
12
+ const { titulo, volanta, copete, imagen, slug, autor, publicarComoOrg, focalPoint } = article
10
13
  const href = slug ? `/${slug}` : '#'
14
+ const { displayName } = useAuthorDisplay(autor, publicarComoOrg)
11
15
 
12
16
  return (
13
17
  <Tooltip text={titulo}>
@@ -24,11 +28,11 @@ export default function FeaturedDuo({ article }) {
24
28
  )}
25
29
  <div className={styles.body}>
26
30
  <div className={styles.header}>
27
- {volanta && <span className={styles.volanta}>{volanta}.</span>}
31
+ {volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
28
32
  {titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
29
33
  </div>
30
- {copete && <div className={styles.copete} dangerouslySetInnerHTML={{ __html: copete }} />}
31
- {autor?.nombre && <span className={styles.autor}>Por {autor.nombre}</span>}
34
+ {copete && <div className={styles.copete} dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(copete) }} />}
35
+ {displayName && <span className={styles.autor}>Por {displayName}</span>}
32
36
  </div>
33
37
  </article>
34
38
  </Tooltip>
@@ -1,13 +1,17 @@
1
1
  import styles from '../../CardCabezal.module.scss'
2
2
  import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
3
+ import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
4
+ import { sanitizeInlineHtml } from '../../../../../utils/sanitizeHtml.js'
3
5
  import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
4
6
  import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
7
+ import { volantaWithStop } from '../../../../../utils/volanta.js'
5
8
 
6
9
  export default function FeaturedHorizontal({ article }) {
7
10
 
8
11
  const { Link } = useAdapters()
9
- const { titulo, volanta, copete, imagen, slug, autor, focalPoint } = article
12
+ const { titulo, volanta, copete, imagen, slug, autor, publicarComoOrg, focalPoint } = article
10
13
  const href = slug ? `/${slug}` : '#'
14
+ const { displayName } = useAuthorDisplay(autor, publicarComoOrg)
11
15
 
12
16
  return (
13
17
  <Tooltip text={titulo}>
@@ -24,11 +28,11 @@ export default function FeaturedHorizontal({ article }) {
24
28
  )}
25
29
  <div className={styles.body}>
26
30
  <div className={styles.header}>
27
- {volanta && <span className={styles.volanta}>{volanta}.</span>}
31
+ {volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
28
32
  {titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
29
33
  </div>
30
- {copete && <div className={styles.copete} dangerouslySetInnerHTML={{ __html: copete }} />}
31
- {autor?.nombre && <span className={styles.autor}>Por {autor.nombre}</span>}
34
+ {copete && <div className={styles.copete} dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(copete) }} />}
35
+ {displayName && <span className={styles.autor}>Por {displayName}</span>}
32
36
  </div>
33
37
  </article>
34
38
  </Tooltip>
@@ -1,13 +1,16 @@
1
1
  import styles from '../../CardCabezal.module.scss'
2
2
  import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
3
+ import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
3
4
  import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
4
5
  import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
6
+ import { volantaWithStop } from '../../../../../utils/volanta.js'
5
7
 
6
8
  export default function Medium({ article }) {
7
9
 
8
10
  const { Link } = useAdapters()
9
- const { titulo, volanta, imagen, slug, autor, focalPoint } = article
11
+ const { titulo, volanta, imagen, slug, autor, publicarComoOrg, focalPoint } = article
10
12
  const href = slug ? `/${slug}` : '#'
13
+ const { displayName } = useAuthorDisplay(autor, publicarComoOrg)
11
14
 
12
15
  return (
13
16
  <Tooltip text={titulo}>
@@ -24,10 +27,10 @@ export default function Medium({ article }) {
24
27
  )}
25
28
  <div className={styles.body}>
26
29
  <div className={styles.header}>
27
- {volanta && <span className={styles.volanta}>{volanta}.</span>}
30
+ {volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
28
31
  {titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
29
32
  </div>
30
- {autor?.nombre && <span className={styles.autor}>Por {autor.nombre}</span>}
33
+ {displayName && <span className={styles.autor}>Por {displayName}</span>}
31
34
  </div>
32
35
  </article>
33
36
  </Tooltip>
@@ -2,6 +2,7 @@ import styles from '../../CardCabezal.module.scss'
2
2
  import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
3
3
  import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
4
4
  import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
5
+ import { volantaWithStop } from '../../../../../utils/volanta.js'
5
6
 
6
7
  export default function Ranked({ article, rank, rankVariant }) {
7
8
 
@@ -25,7 +26,7 @@ export default function Ranked({ article, rank, rankVariant }) {
25
26
  )}
26
27
  <div className={styles.body}>
27
28
  <div className={styles.header}>
28
- {volanta && <span className={styles.volanta}>{volanta}.</span>}
29
+ {volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
29
30
  {titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
30
31
  </div>
31
32
  {rank != null && <span className={styles.rank}>{rank}</span>}
@@ -130,7 +130,7 @@ export default function Carrusel({ titulo, verMasUrl, articles, getSlotProps })
130
130
  {titulo && (
131
131
  <div className={styles.header}>
132
132
  <h2 className={styles.titulo}>{titulo}</h2>
133
- {verMasUrl && <a href={verMasUrl} className={styles.verMas}>VER MÁS</a>}
133
+ {verMasUrl && <a href={verMasUrl} className={styles.verMas} aria-label={`Ver más de ${titulo}`}>VER MÁS</a>}
134
134
  </div>
135
135
  )}
136
136
 
@@ -12,7 +12,7 @@ export default function Categoria({ titulo, verMasUrl, articles, getSlotProps })
12
12
  {titulo && (
13
13
  <div className={styles.header}>
14
14
  <h2 className={styles.titulo}>{titulo}</h2>
15
- {verMasUrl && <a href={verMasUrl} className={styles.verMas}>VER MÁS</a>}
15
+ {verMasUrl && <a href={verMasUrl} className={styles.verMas} aria-label={`Ver más de ${titulo}`}>VER MÁS</a>}
16
16
  </div>
17
17
  )}
18
18
  {articles.length > 0 && (
@@ -12,7 +12,7 @@ export default function CategoriaDos({ titulo, verMasUrl, articles, getSlotProps
12
12
  {titulo && (
13
13
  <div className={styles.header}>
14
14
  <h2 className={styles.titulo}>{titulo}</h2>
15
- {verMasUrl && <a href={verMasUrl} className={styles.verMas}>VER MÁS</a>}
15
+ {verMasUrl && <a href={verMasUrl} className={styles.verMas} aria-label={`Ver más de ${titulo}`}>VER MÁS</a>}
16
16
  </div>
17
17
  )}
18
18
  {articles.length > 0 && (
@@ -7,7 +7,7 @@ export default function Compact({ titulo, verMasUrl, articles, getSlotProps }) {
7
7
  {titulo && (
8
8
  <div className={styles.header}>
9
9
  <h2 className={styles.titulo}>{titulo}</h2>
10
- {verMasUrl && <a href={verMasUrl} className={styles.verMas}>VER MÁS</a>}
10
+ {verMasUrl && <a href={verMasUrl} className={styles.verMas} aria-label={`Ver más de ${titulo}`}>VER MÁS</a>}
11
11
  </div>
12
12
  )}
13
13
  {articles.length > 0 && (
@@ -7,7 +7,7 @@ export default function Default({ titulo, verMasUrl, articles, tipo, getSlotProp
7
7
  {titulo && (
8
8
  <div className={styles.header}>
9
9
  <h2 className={styles.titulo}>{titulo}</h2>
10
- {verMasUrl && <a href={verMasUrl} className={styles.verMas}>VER MÁS</a>}
10
+ {verMasUrl && <a href={verMasUrl} className={styles.verMas} aria-label={`Ver más de ${titulo}`}>VER MÁS</a>}
11
11
  </div>
12
12
  )}
13
13
  {articles.length > 0 && (
@@ -28,7 +28,7 @@ export default function Desktop({ titulo, verMasUrl, articles, tipo }) {
28
28
  {titulo && (
29
29
  <div className={styles.header}>
30
30
  <h2 className={styles.titulo}>{titulo}</h2>
31
- {verMasUrl && <a href={verMasUrl} className={styles.verMas}>VER MÁS</a>}
31
+ {verMasUrl && <a href={verMasUrl} className={styles.verMas} aria-label={`Ver más de ${titulo}`}>VER MÁS</a>}
32
32
  </div>
33
33
  )}
34
34
  {articles.length > 0 && (
@@ -7,7 +7,7 @@ export default function Duo({ titulo, verMasUrl, articles, getSlotProps }) {
7
7
  {titulo && (
8
8
  <div className={styles.header}>
9
9
  <h2 className={styles.titulo}>{titulo}</h2>
10
- {verMasUrl && <a href={verMasUrl} className={styles.verMas}>VER MÁS</a>}
10
+ {verMasUrl && <a href={verMasUrl} className={styles.verMas} aria-label={`Ver más de ${titulo}`}>VER MÁS</a>}
11
11
  </div>
12
12
  )}
13
13
  {articles.length > 0 && (
@@ -7,7 +7,7 @@ export default function DuoSinCopete({ titulo, verMasUrl, articles, getSlotProps
7
7
  {titulo && (
8
8
  <div className={styles.header}>
9
9
  <h2 className={styles.titulo}>{titulo}</h2>
10
- {verMasUrl && <a href={verMasUrl} className={styles.verMas}>VER MÁS</a>}
10
+ {verMasUrl && <a href={verMasUrl} className={styles.verMas} aria-label={`Ver más de ${titulo}`}>VER MÁS</a>}
11
11
  </div>
12
12
  )}
13
13
  {articles.length > 0 && (
@@ -7,7 +7,7 @@ export default function Horizontal({ titulo, verMasUrl, articles, getSlotProps }
7
7
  {titulo && (
8
8
  <div className={styles.header}>
9
9
  <h2 className={styles.titulo}>{titulo}</h2>
10
- {verMasUrl && <a href={verMasUrl} className={styles.verMas}>VER MÁS</a>}
10
+ {verMasUrl && <a href={verMasUrl} className={styles.verMas} aria-label={`Ver más de ${titulo}`}>VER MÁS</a>}
11
11
  </div>
12
12
  )}
13
13
  {articles.length > 0 && (
@@ -1,5 +1,6 @@
1
1
  import { useAdapters } from '../../../../adapters/AdaptersContext.jsx'
2
2
  import AspectImage from '../../../UI/AspectImage/AspectImage.jsx'
3
+ import { volantaWithStop } from '../../../../utils/volanta.js'
3
4
  import styles from './LeeAdemas.module.scss'
4
5
 
5
6
  function LeeAdemasCard({ article }) {
@@ -22,7 +23,7 @@ function LeeAdemasCard({ article }) {
22
23
  )}
23
24
  <div className={styles.body}>
24
25
  <Link href={href} className={styles.header}>
25
- {volanta && <span className={styles.volanta}>{volanta}. </span>}
26
+ {volanta && <span className={styles.volanta}>{volantaWithStop(volanta)} </span>}
26
27
  {titulo && <span className={styles.titulo}>{titulo}</span>}
27
28
  </Link>
28
29
  </div>
@@ -1,5 +1,7 @@
1
1
  import { useAdapters } from '../../../../adapters/AdaptersContext.jsx'
2
+ import { useAuthorDisplay } from '../../../../utils/authorDisplay.js'
2
3
  import AspectImage from '../../../UI/AspectImage/AspectImage.jsx'
4
+ import { volantaWithStop } from '../../../../utils/volanta.js'
3
5
  import styles from './LoQueSeLee.module.scss'
4
6
 
5
7
  // View pura: recibe el artículo ya resuelto.
@@ -7,10 +9,13 @@ import styles from './LoQueSeLee.module.scss'
7
9
  // /api/portal/articles/trending?categoria=X y filtrar por excludeId.
8
10
  export default function LoQueSeLee({ article }) {
9
11
  const { Link } = useAdapters()
12
+ // El hook va antes del early-return para no violar las reglas de hooks; null-safe
13
+ // porque `article` puede ser null (ver guard debajo).
14
+ const { displayName } = useAuthorDisplay(article?.autor, article?.publicarComoOrg)
10
15
 
11
16
  if (!article) return null
12
17
 
13
- const { titulo, volanta, imagen, slug, autor, focalPoint } = article
18
+ const { titulo, volanta, imagen, slug, focalPoint } = article
14
19
  const href = slug ? `/${slug}` : '#'
15
20
 
16
21
  return (
@@ -36,11 +41,11 @@ export default function LoQueSeLee({ article }) {
36
41
 
37
42
  <div className={styles.body}>
38
43
  <Link href={href} className={styles.textLink}>
39
- {volanta && <span className={styles.volanta}>{volanta}. </span>}
44
+ {volanta && <span className={styles.volanta}>{volantaWithStop(volanta)} </span>}
40
45
  {titulo && <span className={styles.titulo}>{titulo}</span>}
41
46
  </Link>
42
- {autor?.nombre && (
43
- <span className={styles.autor}>Por {autor.nombre}</span>
47
+ {displayName && (
48
+ <span className={styles.autor}>Por {displayName}</span>
44
49
  )}
45
50
  </div>
46
51
  </article>