@crtobiasdelsud/portal-ui 1.1.1 → 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.
- package/package.json +1 -1
- package/src/components/Blocks/BlockColumns/BlockColumns.module.scss +5 -5
- package/src/components/Cabezal/CardCabezal/variants/Amp/Amp.jsx +4 -10
- package/src/components/Cabezal/CardCabezal/variants/Compact/Compact.jsx +2 -1
- package/src/components/Cabezal/CardCabezal/variants/Default/Default.jsx +2 -1
- package/src/components/Cabezal/CardCabezal/variants/Featured/Featured.jsx +2 -1
- package/src/components/Cabezal/CardCabezal/variants/FeaturedDuo/FeaturedDuo.jsx +2 -1
- package/src/components/Cabezal/CardCabezal/variants/FeaturedHorizontal/FeaturedHorizontal.jsx +2 -1
- package/src/components/Cabezal/CardCabezal/variants/Medium/Medium.jsx +2 -1
- package/src/components/Cabezal/CardCabezal/variants/Ranked/Ranked.jsx +2 -1
- package/src/components/Cabezal/variants/LeeAdemas/LeeAdemas.jsx +2 -1
- package/src/components/Cabezal/variants/LoQueSeLee/LoQueSeLee.jsx +2 -1
- package/src/components/Cards/ArticleCard/ArticleCard.jsx +7 -4
- package/src/components/Cards/Bajada/variants/V1/V1.jsx +2 -1
- package/src/components/Cards/Bajada/variants/V2/V2.jsx +2 -1
- package/src/components/Carousel/Carousel.jsx +246 -68
- package/src/components/Carousel/Carousel.module.scss +30 -2
- package/src/components/Hero/variants/V1/V1.jsx +2 -1
- package/src/components/Hero/variants/V2/V2.jsx +7 -4
- package/src/components/Hero/variants/V2/V2.module.scss +16 -35
- package/src/components/Hero/variants/V3/V3.jsx +7 -2
- package/src/components/Hero/variants/V3/V3.module.scss +10 -4
- package/src/utils/volanta.js +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crtobiasdelsud/portal-ui",
|
|
3
|
-
"version": "1.1.
|
|
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",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
display: grid;
|
|
10
10
|
gap: 20px;
|
|
11
11
|
|
|
12
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
66
|
+
@include respond(desktop) {
|
|
67
67
|
padding: 0;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { volantaWithStop } from '../../../../../utils/volanta.js'
|
|
2
2
|
|
|
3
3
|
export default function Amp({ article, rank }) {
|
|
4
4
|
const { titulo, volanta, copete, imagen, slug } = article
|
|
@@ -12,15 +12,9 @@ export default function Amp({ article, rank }) {
|
|
|
12
12
|
</a>
|
|
13
13
|
)}
|
|
14
14
|
<div className="card-cabezal__body">
|
|
15
|
-
{volanta && <span className="card-cabezal__volanta">{volanta}
|
|
16
|
-
{titulo &&
|
|
17
|
-
|
|
18
|
-
// margin:0 inline anula el margin por defecto del <h3> (AMP no permite <style> extra).
|
|
19
|
-
<h3 className="card-cabezal__titulo-heading" style={{ margin: 0 }}>
|
|
20
|
-
<a href={href} className="card-cabezal__titulo">{titulo}</a>
|
|
21
|
-
</h3>
|
|
22
|
-
)}
|
|
23
|
-
{copete && <div className="card-cabezal__copete">{stripHtml(copete)}</div>}
|
|
15
|
+
{volanta && <span className="card-cabezal__volanta">{volantaWithStop(volanta)}</span>}
|
|
16
|
+
{titulo && <a href={href} className="card-cabezal__titulo">{titulo}</a>}
|
|
17
|
+
{copete && <div className="card-cabezal__copete" dangerouslySetInnerHTML={{ __html: copete }} />}
|
|
24
18
|
</div>
|
|
25
19
|
{rank != null && <span className="card-cabezal__rank">{rank}</span>}
|
|
26
20
|
</article>
|
|
@@ -3,6 +3,7 @@ import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
|
|
|
3
3
|
import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
|
|
4
4
|
import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
|
|
5
5
|
import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
|
|
6
|
+
import { volantaWithStop } from '../../../../../utils/volanta.js'
|
|
6
7
|
|
|
7
8
|
export default function Compact({ article }) {
|
|
8
9
|
|
|
@@ -27,7 +28,7 @@ export default function Compact({ article }) {
|
|
|
27
28
|
)}
|
|
28
29
|
<div className={styles.body}>
|
|
29
30
|
<div className={styles.header}>
|
|
30
|
-
{volanta && <span className={styles.volanta}>{volanta}
|
|
31
|
+
{volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
|
|
31
32
|
{titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
|
|
32
33
|
</div>
|
|
33
34
|
{displayName && <span className={styles.autor}>Por {displayName}</span>}
|
|
@@ -3,6 +3,7 @@ import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
|
|
|
3
3
|
import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
|
|
4
4
|
import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
|
|
5
5
|
import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
|
|
6
|
+
import { volantaWithStop } from '../../../../../utils/volanta.js'
|
|
6
7
|
|
|
7
8
|
export default function Default({ article }) {
|
|
8
9
|
|
|
@@ -26,7 +27,7 @@ export default function Default({ article }) {
|
|
|
26
27
|
)}
|
|
27
28
|
<div className={styles.body}>
|
|
28
29
|
<div className={styles.header}>
|
|
29
|
-
{volanta && <span className={styles.volanta}>{volanta}
|
|
30
|
+
{volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
|
|
30
31
|
{titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
|
|
31
32
|
</div>
|
|
32
33
|
{displayName && <span className={styles.autor}>Por {displayName}</span>}
|
|
@@ -3,6 +3,7 @@ import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
|
|
|
3
3
|
import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
|
|
4
4
|
import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
|
|
5
5
|
import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
|
|
6
|
+
import { volantaWithStop } from '../../../../../utils/volanta.js'
|
|
6
7
|
|
|
7
8
|
export default function Featured({ article, large }) {
|
|
8
9
|
|
|
@@ -27,7 +28,7 @@ export default function Featured({ article, large }) {
|
|
|
27
28
|
)}
|
|
28
29
|
<div className={styles.body}>
|
|
29
30
|
<div className={styles.header}>
|
|
30
|
-
{volanta && <span className={styles.volanta}>{volanta}
|
|
31
|
+
{volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
|
|
31
32
|
{titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
|
|
32
33
|
</div>
|
|
33
34
|
{displayName && <span className={styles.autor}>Por {displayName}</span>}
|
|
@@ -4,6 +4,7 @@ import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
|
|
|
4
4
|
import { sanitizeInlineHtml } from '../../../../../utils/sanitizeHtml.js'
|
|
5
5
|
import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
|
|
6
6
|
import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
|
|
7
|
+
import { volantaWithStop } from '../../../../../utils/volanta.js'
|
|
7
8
|
|
|
8
9
|
export default function FeaturedDuo({ article }) {
|
|
9
10
|
|
|
@@ -27,7 +28,7 @@ export default function FeaturedDuo({ article }) {
|
|
|
27
28
|
)}
|
|
28
29
|
<div className={styles.body}>
|
|
29
30
|
<div className={styles.header}>
|
|
30
|
-
{volanta && <span className={styles.volanta}>{volanta}
|
|
31
|
+
{volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
|
|
31
32
|
{titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
|
|
32
33
|
</div>
|
|
33
34
|
{copete && <div className={styles.copete} dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(copete) }} />}
|
package/src/components/Cabezal/CardCabezal/variants/FeaturedHorizontal/FeaturedHorizontal.jsx
CHANGED
|
@@ -4,6 +4,7 @@ import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
|
|
|
4
4
|
import { sanitizeInlineHtml } from '../../../../../utils/sanitizeHtml.js'
|
|
5
5
|
import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
|
|
6
6
|
import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
|
|
7
|
+
import { volantaWithStop } from '../../../../../utils/volanta.js'
|
|
7
8
|
|
|
8
9
|
export default function FeaturedHorizontal({ article }) {
|
|
9
10
|
|
|
@@ -27,7 +28,7 @@ export default function FeaturedHorizontal({ article }) {
|
|
|
27
28
|
)}
|
|
28
29
|
<div className={styles.body}>
|
|
29
30
|
<div className={styles.header}>
|
|
30
|
-
{volanta && <span className={styles.volanta}>{volanta}
|
|
31
|
+
{volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
|
|
31
32
|
{titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
|
|
32
33
|
</div>
|
|
33
34
|
{copete && <div className={styles.copete} dangerouslySetInnerHTML={{ __html: sanitizeInlineHtml(copete) }} />}
|
|
@@ -3,6 +3,7 @@ import { useAdapters } from '../../../../../adapters/AdaptersContext.jsx'
|
|
|
3
3
|
import { useAuthorDisplay } from '../../../../../utils/authorDisplay.js'
|
|
4
4
|
import AspectImage from '../../../../UI/AspectImage/AspectImage.jsx'
|
|
5
5
|
import Tooltip from '../../../../UI/ToolTip/ToolTip.jsx'
|
|
6
|
+
import { volantaWithStop } from '../../../../../utils/volanta.js'
|
|
6
7
|
|
|
7
8
|
export default function Medium({ article }) {
|
|
8
9
|
|
|
@@ -26,7 +27,7 @@ export default function Medium({ article }) {
|
|
|
26
27
|
)}
|
|
27
28
|
<div className={styles.body}>
|
|
28
29
|
<div className={styles.header}>
|
|
29
|
-
{volanta && <span className={styles.volanta}>{volanta}
|
|
30
|
+
{volanta && <span className={styles.volanta}>{volantaWithStop(volanta)}</span>}
|
|
30
31
|
{titulo && <Link href={href} className={styles.titulo}>{titulo}</Link>}
|
|
31
32
|
</div>
|
|
32
33
|
{displayName && <span className={styles.autor}>Por {displayName}</span>}
|
|
@@ -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}
|
|
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>}
|
|
@@ -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}
|
|
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,6 +1,7 @@
|
|
|
1
1
|
import { useAdapters } from '../../../../adapters/AdaptersContext.jsx'
|
|
2
2
|
import { useAuthorDisplay } from '../../../../utils/authorDisplay.js'
|
|
3
3
|
import AspectImage from '../../../UI/AspectImage/AspectImage.jsx'
|
|
4
|
+
import { volantaWithStop } from '../../../../utils/volanta.js'
|
|
4
5
|
import styles from './LoQueSeLee.module.scss'
|
|
5
6
|
|
|
6
7
|
// View pura: recibe el artículo ya resuelto.
|
|
@@ -40,7 +41,7 @@ export default function LoQueSeLee({ article }) {
|
|
|
40
41
|
|
|
41
42
|
<div className={styles.body}>
|
|
42
43
|
<Link href={href} className={styles.textLink}>
|
|
43
|
-
{volanta && <span className={styles.volanta}>{volanta}
|
|
44
|
+
{volanta && <span className={styles.volanta}>{volantaWithStop(volanta)} </span>}
|
|
44
45
|
{titulo && <span className={styles.titulo}>{titulo}</span>}
|
|
45
46
|
</Link>
|
|
46
47
|
{displayName && (
|
|
@@ -7,6 +7,7 @@ import { useAdapters } from "../../../adapters/AdaptersContext.jsx"
|
|
|
7
7
|
import { useAuthorDisplay } from "../../../utils/authorDisplay.js"
|
|
8
8
|
import AspectImage from "../../UI/AspectImage/AspectImage.jsx"
|
|
9
9
|
import Tooltip from "../../UI/ToolTip/ToolTip.jsx"
|
|
10
|
+
import { volantaWithStop } from "../../../utils/volanta.js"
|
|
10
11
|
|
|
11
12
|
function buildTooltip(article, authorName) {
|
|
12
13
|
const author = authorName
|
|
@@ -76,10 +77,12 @@ export default function ArticleCard({
|
|
|
76
77
|
}`}
|
|
77
78
|
>
|
|
78
79
|
<p className={styles.headline}>
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
{article.volanta && (
|
|
81
|
+
<span className={styles.category}>
|
|
82
|
+
{volantaWithStop(article.volanta)}
|
|
83
|
+
</span>
|
|
84
|
+
)}
|
|
85
|
+
{article.volanta && ' '}
|
|
83
86
|
{article.titulo}
|
|
84
87
|
</p>
|
|
85
88
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import shared from '../../Bajada.module.scss'
|
|
2
2
|
import { stripHtml } from '../../../../../utils/stripHtml'
|
|
3
|
+
import { volantaWithStop } from '../../../../../utils/volanta.js'
|
|
3
4
|
import s from './V1.module.scss'
|
|
4
5
|
|
|
5
6
|
export default function V1({ isAmp, inlineStyle, volanta, title, copete, authorId, vDesktop }) {
|
|
@@ -11,7 +12,7 @@ export default function V1({ isAmp, inlineStyle, volanta, title, copete, authorI
|
|
|
11
12
|
return (
|
|
12
13
|
<div className={className} style={inlineStyle}>
|
|
13
14
|
<h2 className={isAmp ? 'bajada__headline' : shared.headline}>
|
|
14
|
-
<span className={isAmp ? 'bajada__volanta' : shared.volanta}>{volanta}
|
|
15
|
+
<span className={isAmp ? 'bajada__volanta' : shared.volanta}>{volantaWithStop(volanta)} </span>
|
|
15
16
|
{title}
|
|
16
17
|
</h2>
|
|
17
18
|
{copete && <p className={isAmp ? 'bajada__copete' : shared.copete}>{stripHtml(copete)}</p>}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import shared from '../../Bajada.module.scss'
|
|
2
|
+
import { volantaWithStop } from '../../../../../utils/volanta.js'
|
|
2
3
|
import s from './V2.module.scss'
|
|
3
4
|
|
|
4
5
|
export default function V2({ isAmp, inlineStyle, volanta, title, authorId, vDesktop }) {
|
|
@@ -10,7 +11,7 @@ export default function V2({ isAmp, inlineStyle, volanta, title, authorId, vDesk
|
|
|
10
11
|
return (
|
|
11
12
|
<div className={className} style={inlineStyle}>
|
|
12
13
|
<h2 className={isAmp ? 'bajada__headline' : shared.headline}>
|
|
13
|
-
<span className={isAmp ? 'bajada__volanta' : shared.volanta}>{volanta}
|
|
14
|
+
<span className={isAmp ? 'bajada__volanta' : shared.volanta}>{volantaWithStop(volanta)} </span>
|
|
14
15
|
{title}
|
|
15
16
|
</h2>
|
|
16
17
|
{authorId && <span className={isAmp ? 'bajada__autor' : shared.autor}>Por {authorId}</span>}
|
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react'
|
|
3
|
+
import { useState, useRef, useCallback, useEffect } from 'react'
|
|
4
4
|
import AspectImage from '../UI/AspectImage/AspectImage.jsx'
|
|
5
5
|
import styles from './Carousel.module.scss'
|
|
6
6
|
|
|
7
|
+
const RATIOS = { '16:9': '16/9', '4:3': '4/3' }
|
|
8
|
+
|
|
9
|
+
// Una sola "copia fantasma" a cada lado alcanza cuando se ve 1 slide a la vez.
|
|
10
|
+
// Mismo principio que Cabezal/Carrusel: al llegar al clon teleportamos al real
|
|
11
|
+
// equivalente sin que el usuario lo perciba.
|
|
12
|
+
const CLONE_COUNT = 1
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
|
-
* Carrusel de imágenes con
|
|
15
|
+
* Carrusel de imágenes con scroll-snap nativo + flechas + puntos.
|
|
9
16
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
17
|
+
* El track usa `overflow-x: scroll` con `scroll-snap-type: x mandatory`, lo
|
|
18
|
+
* que da swipe nativo en iOS/Android sin librerías externas. Las flechas y
|
|
19
|
+
* los puntos disparan `scrollBy` y `setScrollLeft` sobre ese mismo track.
|
|
12
20
|
*
|
|
13
21
|
* @param {object} props
|
|
14
22
|
* @param {{ url: string, alt?: string, epigrafe?: string, variants?: object }[]} props.images
|
|
15
23
|
* @param {object} [props.focalPoint] - { x, y, zoom } aplicado a todas.
|
|
16
24
|
* @param {string} [props.aspect] - '16:9' | '4:3'.
|
|
17
|
-
* @param {string} [props.sizes] - hint de tamaño para el srcset
|
|
18
|
-
* @param {boolean} [props.fill] - true → ocupa el contenedor
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* @param {boolean} [props.showEpigrafe] - false → oculta la franja de epígrafe.
|
|
25
|
+
* @param {string} [props.sizes] - hint de tamaño para el srcset.
|
|
26
|
+
* @param {boolean} [props.fill] - true → ocupa el contenedor padre
|
|
27
|
+
* posicionado (uso en el hero).
|
|
28
|
+
* @param {boolean} [props.showEpigrafe] - false → oculta el epígrafe.
|
|
22
29
|
* @param {'bottom'|'top'} [props.dotsPosition]
|
|
23
|
-
* @param {boolean} [props.priority]
|
|
24
|
-
* prioridad alta (LCP). El resto carga al navegar a su slide.
|
|
30
|
+
* @param {boolean} [props.priority] - true → la primera imagen es LCP.
|
|
25
31
|
*/
|
|
26
32
|
export default function Carousel({
|
|
27
33
|
images = [],
|
|
@@ -33,74 +39,246 @@ export default function Carousel({
|
|
|
33
39
|
priority = false,
|
|
34
40
|
sizes,
|
|
35
41
|
}) {
|
|
36
|
-
const
|
|
42
|
+
const total = images.length
|
|
43
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
44
|
+
const trackRef = useRef(null)
|
|
45
|
+
const isJumping = useRef(false)
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
const getRealIndex = useCallback((childIdx) => {
|
|
48
|
+
if (!total) return 0
|
|
49
|
+
const raw = childIdx - CLONE_COUNT
|
|
50
|
+
return ((raw % total) + total) % total
|
|
51
|
+
}, [total])
|
|
39
52
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
const getSnappedIndex = (track) => {
|
|
54
|
+
const trackLeft = track.getBoundingClientRect().left
|
|
55
|
+
let best = 0
|
|
56
|
+
let bestDist = Infinity
|
|
57
|
+
Array.from(track.children).forEach((child, i) => {
|
|
58
|
+
const dist = Math.abs(child.getBoundingClientRect().left - trackLeft)
|
|
59
|
+
if (dist < bestDist) { bestDist = dist; best = i }
|
|
60
|
+
})
|
|
61
|
+
return best
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Teleportar de un clon al real equivalente, sin que el usuario lo note.
|
|
65
|
+
// En WebKit (iOS) el snap mandatory y el scroll-behavior:smooth pelean con
|
|
66
|
+
// scrollBy({behavior:'instant'}) → glitch. Los apagamos durante el salto.
|
|
67
|
+
const wrapIfNeeded = useCallback(() => {
|
|
68
|
+
if (isJumping.current) return
|
|
69
|
+
const track = trackRef.current
|
|
70
|
+
if (!track || total < 2) return
|
|
71
|
+
|
|
72
|
+
const snapped = getSnappedIndex(track)
|
|
73
|
+
let target = null
|
|
74
|
+
if (snapped < CLONE_COUNT) {
|
|
75
|
+
target = track.children[snapped + total]
|
|
76
|
+
} else if (snapped >= CLONE_COUNT + total) {
|
|
77
|
+
target = track.children[snapped - total]
|
|
78
|
+
}
|
|
79
|
+
if (!target) return
|
|
80
|
+
|
|
81
|
+
const delta =
|
|
82
|
+
target.getBoundingClientRect().left -
|
|
83
|
+
track.children[snapped].getBoundingClientRect().left
|
|
84
|
+
|
|
85
|
+
isJumping.current = true
|
|
86
|
+
const prevSnap = track.style.scrollSnapType
|
|
87
|
+
const prevBehavior = track.style.scrollBehavior
|
|
88
|
+
track.style.scrollSnapType = 'none'
|
|
89
|
+
track.style.scrollBehavior = 'auto'
|
|
90
|
+
track.scrollLeft += delta
|
|
91
|
+
void track.offsetWidth
|
|
92
|
+
track.style.scrollSnapType = prevSnap
|
|
93
|
+
void track.offsetWidth
|
|
94
|
+
track.style.scrollBehavior = prevBehavior
|
|
95
|
+
requestAnimationFrame(() => { isJumping.current = false })
|
|
96
|
+
}, [total])
|
|
97
|
+
|
|
98
|
+
// Al montar: posicionarse en el primer slide real (saltar el leading clone).
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (total < 2) return
|
|
101
|
+
requestAnimationFrame(() => {
|
|
102
|
+
const track = trackRef.current
|
|
103
|
+
const firstReal = track?.children[CLONE_COUNT]
|
|
104
|
+
if (!firstReal) return
|
|
105
|
+
const delta =
|
|
106
|
+
firstReal.getBoundingClientRect().left -
|
|
107
|
+
track.getBoundingClientRect().left
|
|
108
|
+
const prevBehavior = track.style.scrollBehavior
|
|
109
|
+
track.style.scrollBehavior = 'auto'
|
|
110
|
+
track.scrollLeft += delta
|
|
111
|
+
track.style.scrollBehavior = prevBehavior
|
|
112
|
+
})
|
|
113
|
+
}, [total])
|
|
114
|
+
|
|
115
|
+
// scrollend dispara el wrap recién cuando el scroll se asienta. Fallback con
|
|
116
|
+
// debounce para navegadores que aún no lo implementan.
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (total < 2) return
|
|
119
|
+
const track = trackRef.current
|
|
120
|
+
if (!track) return
|
|
121
|
+
|
|
122
|
+
if ('onscrollend' in HTMLElement.prototype) {
|
|
123
|
+
track.addEventListener('scrollend', wrapIfNeeded)
|
|
124
|
+
return () => track.removeEventListener('scrollend', wrapIfNeeded)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let timer
|
|
128
|
+
const onScroll = () => {
|
|
129
|
+
clearTimeout(timer)
|
|
130
|
+
timer = setTimeout(wrapIfNeeded, 150)
|
|
131
|
+
}
|
|
132
|
+
track.addEventListener('scroll', onScroll)
|
|
133
|
+
return () => {
|
|
134
|
+
track.removeEventListener('scroll', onScroll)
|
|
135
|
+
clearTimeout(timer)
|
|
136
|
+
}
|
|
137
|
+
}, [wrapIfNeeded, total])
|
|
138
|
+
|
|
139
|
+
const handleScroll = useCallback(() => {
|
|
140
|
+
if (isJumping.current) return
|
|
141
|
+
const track = trackRef.current
|
|
142
|
+
if (!track) return
|
|
143
|
+
setActiveIndex(getRealIndex(getSnappedIndex(track)))
|
|
144
|
+
}, [getRealIndex])
|
|
145
|
+
|
|
146
|
+
const getSlideWidth = () => {
|
|
147
|
+
const s = trackRef.current?.children[0]
|
|
148
|
+
return s ? s.getBoundingClientRect().width : 0
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const prev = () => {
|
|
152
|
+
const sw = getSlideWidth()
|
|
153
|
+
trackRef.current?.scrollBy({ left: -sw, behavior: 'smooth' })
|
|
154
|
+
}
|
|
155
|
+
const next = () => {
|
|
156
|
+
const sw = getSlideWidth()
|
|
157
|
+
trackRef.current?.scrollBy({ left: sw, behavior: 'smooth' })
|
|
158
|
+
}
|
|
159
|
+
const goToIndex = (idx) => {
|
|
160
|
+
const track = trackRef.current
|
|
161
|
+
if (!track) return
|
|
162
|
+
const target = track.children[CLONE_COUNT + idx]
|
|
163
|
+
if (!target) return
|
|
164
|
+
const delta =
|
|
165
|
+
target.getBoundingClientRect().left -
|
|
166
|
+
track.getBoundingClientRect().left
|
|
167
|
+
track.scrollBy({ left: delta, behavior: 'smooth' })
|
|
168
|
+
setActiveIndex(idx)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!total) return null
|
|
44
172
|
|
|
45
173
|
const rootClass = fill ? `${styles.carousel} ${styles.fill}` : styles.carousel
|
|
46
174
|
const dotsClass = dotsPosition === 'top'
|
|
47
175
|
? `${styles.dots} ${styles.dotsTop}`
|
|
48
176
|
: styles.dots
|
|
49
177
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
178
|
+
// En modo fill el padre define el alto. Sin fill, el viewport pone su propio
|
|
179
|
+
// aspect-ratio (cada slide hereda height: 100%).
|
|
180
|
+
const viewportStyle = fill
|
|
181
|
+
? undefined
|
|
182
|
+
: { aspectRatio: RATIOS[aspect] ?? RATIOS['16:9'] }
|
|
183
|
+
|
|
184
|
+
// Caso 1 imagen: sin scroll, sin clones, sin controles. Salida temprana.
|
|
185
|
+
if (total === 1) {
|
|
186
|
+
const only = images[0]
|
|
187
|
+
return (
|
|
188
|
+
<div className={rootClass}>
|
|
189
|
+
<div className={styles.viewport} style={viewportStyle}>
|
|
190
|
+
<AspectImage
|
|
191
|
+
src={only.url}
|
|
192
|
+
alt={only.alt ?? ''}
|
|
193
|
+
aspect={aspect}
|
|
194
|
+
fill
|
|
195
|
+
focalPoint={focalPoint}
|
|
196
|
+
variants={only.variants ?? null}
|
|
197
|
+
sizes={sizes}
|
|
198
|
+
priority={priority}
|
|
199
|
+
/>
|
|
200
|
+
{showEpigrafe && only.epigrafe && (
|
|
201
|
+
<p className={styles.epigrafe}>{only.epigrafe}</p>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
66
204
|
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const leadClones = images.slice(-CLONE_COUNT)
|
|
209
|
+
const tailClones = images.slice(0, CLONE_COUNT)
|
|
67
210
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
<div className={dotsClass}>
|
|
87
|
-
{images.map((_, i) => (
|
|
88
|
-
<button
|
|
89
|
-
key={i}
|
|
90
|
-
type="button"
|
|
91
|
-
className={
|
|
92
|
-
i === safeIndex
|
|
93
|
-
? `${styles.dot} ${styles.dotActive}`
|
|
94
|
-
: styles.dot
|
|
95
|
-
}
|
|
96
|
-
onClick={() => setIndex(i)}
|
|
97
|
-
aria-label={`Ir a la imagen ${i + 1}`}
|
|
98
|
-
aria-current={i === safeIndex}
|
|
99
|
-
/>
|
|
100
|
-
))}
|
|
101
|
-
</div>
|
|
102
|
-
</>
|
|
211
|
+
const renderSlide = (img, key, isPriority, isClone) => (
|
|
212
|
+
<div
|
|
213
|
+
key={key}
|
|
214
|
+
className={styles.slide}
|
|
215
|
+
aria-hidden={isClone ? 'true' : undefined}
|
|
216
|
+
>
|
|
217
|
+
<AspectImage
|
|
218
|
+
src={img.url}
|
|
219
|
+
alt={img.alt ?? ''}
|
|
220
|
+
aspect={aspect}
|
|
221
|
+
fill
|
|
222
|
+
focalPoint={focalPoint}
|
|
223
|
+
variants={img.variants ?? null}
|
|
224
|
+
sizes={sizes}
|
|
225
|
+
priority={isPriority}
|
|
226
|
+
/>
|
|
227
|
+
{showEpigrafe && img.epigrafe && (
|
|
228
|
+
<p className={styles.epigrafe}>{img.epigrafe}</p>
|
|
103
229
|
)}
|
|
104
230
|
</div>
|
|
105
231
|
)
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div className={rootClass}>
|
|
235
|
+
<div className={styles.viewport} style={viewportStyle}>
|
|
236
|
+
<div className={styles.track} ref={trackRef} onScroll={handleScroll}>
|
|
237
|
+
{leadClones.map((img, i) =>
|
|
238
|
+
renderSlide(img, `clone-start-${i}`, false, true),
|
|
239
|
+
)}
|
|
240
|
+
{images.map((img, i) =>
|
|
241
|
+
renderSlide(img, img.url ?? `slide-${i}`, priority && i === 0, false),
|
|
242
|
+
)}
|
|
243
|
+
{tailClones.map((img, i) =>
|
|
244
|
+
renderSlide(img, `clone-end-${i}`, false, true),
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<button
|
|
249
|
+
type="button"
|
|
250
|
+
className={`${styles.arrow} ${styles.arrowPrev}`}
|
|
251
|
+
onClick={prev}
|
|
252
|
+
aria-label="Imagen anterior"
|
|
253
|
+
>
|
|
254
|
+
<span aria-hidden="true">‹</span>
|
|
255
|
+
</button>
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
className={`${styles.arrow} ${styles.arrowNext}`}
|
|
259
|
+
onClick={next}
|
|
260
|
+
aria-label="Imagen siguiente"
|
|
261
|
+
>
|
|
262
|
+
<span aria-hidden="true">›</span>
|
|
263
|
+
</button>
|
|
264
|
+
|
|
265
|
+
<div className={dotsClass}>
|
|
266
|
+
{images.map((_, i) => (
|
|
267
|
+
<button
|
|
268
|
+
key={i}
|
|
269
|
+
type="button"
|
|
270
|
+
className={
|
|
271
|
+
i === activeIndex
|
|
272
|
+
? `${styles.dot} ${styles.dotActive}`
|
|
273
|
+
: styles.dot
|
|
274
|
+
}
|
|
275
|
+
onClick={() => goToIndex(i)}
|
|
276
|
+
aria-label={`Ir a la imagen ${i + 1}`}
|
|
277
|
+
aria-current={i === activeIndex}
|
|
278
|
+
/>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
)
|
|
106
284
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Carrusel de imágenes — navegación
|
|
2
|
-
//
|
|
1
|
+
// Carrusel de imágenes — navegación nativa con scroll-snap. Las flechas y
|
|
2
|
+
// los puntos disparan scrollBy sobre el mismo track. Swipe nativo en iOS.
|
|
3
3
|
.carousel {
|
|
4
4
|
position: relative;
|
|
5
5
|
width: 100%;
|
|
@@ -16,6 +16,34 @@
|
|
|
16
16
|
position: relative;
|
|
17
17
|
width: 100%;
|
|
18
18
|
height: 100%;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Track horizontal scrollable con snap obligatorio: el navegador maneja el
|
|
23
|
+
// swipe en mobile sin necesidad de listeners de touch.
|
|
24
|
+
.track {
|
|
25
|
+
display: flex;
|
|
26
|
+
width: 100%;
|
|
27
|
+
height: 100%;
|
|
28
|
+
overflow-x: scroll;
|
|
29
|
+
overflow-y: hidden;
|
|
30
|
+
scroll-snap-type: x mandatory;
|
|
31
|
+
scroll-behavior: smooth;
|
|
32
|
+
scrollbar-width: none;
|
|
33
|
+
-webkit-overflow-scrolling: touch;
|
|
34
|
+
|
|
35
|
+
&::-webkit-scrollbar { display: none; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.slide {
|
|
39
|
+
flex: 0 0 100%;
|
|
40
|
+
width: 100%;
|
|
41
|
+
height: 100%;
|
|
42
|
+
position: relative;
|
|
43
|
+
scroll-snap-align: start;
|
|
44
|
+
// Fuerza un slide por gesto incluso en swipes rápidos — mantiene la lógica
|
|
45
|
+
// de clones simple (sólo necesitamos 1 clon por lado).
|
|
46
|
+
scroll-snap-stop: always;
|
|
19
47
|
}
|
|
20
48
|
|
|
21
49
|
// Epígrafe — franja negra de altura fija (38px) sobre el borde inferior.
|
|
@@ -2,6 +2,7 @@ import { useAdapters } from '../../../../adapters/AdaptersContext.jsx'
|
|
|
2
2
|
import { useAuthorDisplay } from '../../../../utils/authorDisplay.js'
|
|
3
3
|
import { sanitizeInlineHtml } from '../../../../utils/sanitizeHtml.js'
|
|
4
4
|
import AspectImage from '../../../UI/AspectImage/AspectImage.jsx'
|
|
5
|
+
import { volantaWithStop } from '../../../../utils/volanta.js'
|
|
5
6
|
import styles from '../../Hero.module.scss'
|
|
6
7
|
|
|
7
8
|
export default function V1({ article, important = false, inlineStyle }) {
|
|
@@ -25,7 +26,7 @@ export default function V1({ article, important = false, inlineStyle }) {
|
|
|
25
26
|
)}
|
|
26
27
|
<div className={styles.body}>
|
|
27
28
|
<h2 className={styles.headline}>
|
|
28
|
-
{article.volanta && <span className={styles.category}>{article.volanta}
|
|
29
|
+
{article.volanta && <span className={styles.category}>{volantaWithStop(article.volanta)} </span>}
|
|
29
30
|
{article.titulo}
|
|
30
31
|
</h2>
|
|
31
32
|
{article.copete && (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useAdapters } from '../../../../adapters/AdaptersContext.jsx'
|
|
2
2
|
import { useAuthorDisplay } from '../../../../utils/authorDisplay.js'
|
|
3
3
|
import AspectImage from '../../../UI/AspectImage/AspectImage.jsx'
|
|
4
|
+
import { volantaWithStop } from '../../../../utils/volanta.js'
|
|
4
5
|
import s from './V2.module.scss'
|
|
5
6
|
|
|
6
7
|
export default function V2({ article, important = false, inlineStyle }) {
|
|
@@ -25,10 +26,12 @@ export default function V2({ article, important = false, inlineStyle }) {
|
|
|
25
26
|
</div>
|
|
26
27
|
)}
|
|
27
28
|
<div className={s.overlay}>
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
<h2 className={s.headline}>
|
|
30
|
+
{article.volanta && (
|
|
31
|
+
<span className={s.volanta}>{volantaWithStop(article.volanta)} </span>
|
|
32
|
+
)}
|
|
33
|
+
{article.titulo}
|
|
34
|
+
</h2>
|
|
32
35
|
</div>
|
|
33
36
|
</div>
|
|
34
37
|
|
|
@@ -53,55 +53,27 @@
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// ── Overlay SOBRE la imagen en todos los anchos (cajas blancas por línea) ──
|
|
56
|
+
// Sin flex: queremos que volanta y headline fluyan inline en la misma línea
|
|
57
|
+
// (cuando entran). Con flex-column eran filas separadas obligadas.
|
|
56
58
|
.overlay {
|
|
57
59
|
position: absolute;
|
|
58
60
|
left: 0;
|
|
59
61
|
right: 0;
|
|
60
62
|
bottom: 14px;
|
|
61
63
|
padding: 0 14px;
|
|
62
|
-
display: flex;
|
|
63
|
-
flex-direction: column;
|
|
64
|
-
align-items: flex-start;
|
|
65
64
|
font-family: var(--font-family);
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.headlineWrap {
|
|
75
|
-
display: block;
|
|
76
|
-
max-width: 100%;
|
|
77
|
-
margin: 0; // el headline ahora es <h2>: anula el margin por defecto del UA para no romper el overlay
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Cajas con fondo blanco que se clonan en cada línea de texto
|
|
81
|
-
.volanta,
|
|
67
|
+
// Una sola caja blanca contiene volanta + título: usamos el background y el
|
|
68
|
+
// box-decoration-break solo en .headline. La .volanta queda como modificador
|
|
69
|
+
// inline (color + peso) DENTRO de la misma caja, así no se ve corte entre
|
|
70
|
+
// las dos partes del texto.
|
|
82
71
|
.headline {
|
|
83
72
|
-webkit-box-decoration-break: clone;
|
|
84
73
|
box-decoration-break: clone;
|
|
85
74
|
background: #fff;
|
|
86
75
|
padding: 3px 7px;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.volanta {
|
|
90
|
-
color: var(--primary-color, #4bac48);
|
|
91
|
-
font-size: 18px;
|
|
92
|
-
font-weight: 700;
|
|
93
|
-
line-height: 1.5;
|
|
94
|
-
|
|
95
|
-
@include respond(tablet) {
|
|
96
|
-
font-size: 26px;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
@include respond(desktop) {
|
|
100
|
-
font-size: 28px;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.headline {
|
|
76
|
+
margin: 0; // h2 default UA margin desplaza el overlay anclado por bottom
|
|
105
77
|
color: var(--text-color, #252523);
|
|
106
78
|
font-size: 20px;
|
|
107
79
|
font-weight: 800;
|
|
@@ -117,6 +89,11 @@
|
|
|
117
89
|
}
|
|
118
90
|
}
|
|
119
91
|
|
|
92
|
+
.volanta {
|
|
93
|
+
color: var(--primary-color, #4bac48);
|
|
94
|
+
font-weight: 700;
|
|
95
|
+
}
|
|
96
|
+
|
|
120
97
|
// ── Copete debajo de la imagen (si existe) ───────────────────────────────
|
|
121
98
|
.copete {
|
|
122
99
|
font-size: 14px;
|
|
@@ -153,8 +130,12 @@
|
|
|
153
130
|
@include respond(tablet) {
|
|
154
131
|
aspect-ratio: 16 / 7;
|
|
155
132
|
}
|
|
133
|
+
// Desktop: V1 cap la imagen importante a 500px (sin esto el hero ocupa
|
|
134
|
+
// ~774px y rompe el ratio respecto a V1). Replicamos acá para que V2 y
|
|
135
|
+
// V1 tengan la misma altura en portada de escritorio.
|
|
156
136
|
@include respond(desktop) {
|
|
157
137
|
aspect-ratio: unset;
|
|
138
|
+
max-height: 500px;
|
|
158
139
|
}
|
|
159
140
|
}
|
|
160
141
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useAdapters } from '../../../../adapters/AdaptersContext.jsx'
|
|
2
2
|
import { useAuthorDisplay } from '../../../../utils/authorDisplay.js'
|
|
3
3
|
import AspectImage from '../../../UI/AspectImage/AspectImage.jsx'
|
|
4
|
+
import { volantaWithStop } from '../../../../utils/volanta.js'
|
|
4
5
|
import s from './V3.module.scss'
|
|
5
6
|
|
|
6
7
|
export default function V3({ article, important = false, inlineStyle }) {
|
|
@@ -26,8 +27,12 @@ export default function V3({ article, important = false, inlineStyle }) {
|
|
|
26
27
|
)}
|
|
27
28
|
<div className={s.gradient} aria-hidden="true" />
|
|
28
29
|
<div className={s.overlay}>
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
<h2 className={s.headline}>
|
|
31
|
+
{article.volanta && (
|
|
32
|
+
<span className={s.volanta}>{volantaWithStop(article.volanta)} </span>
|
|
33
|
+
)}
|
|
34
|
+
{article.titulo}
|
|
35
|
+
</h2>
|
|
31
36
|
</div>
|
|
32
37
|
</div>
|
|
33
38
|
|
|
@@ -78,20 +78,22 @@
|
|
|
78
78
|
font-family: var(--font-family);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Volanta inline dentro del h2: misma línea que el título, color primario
|
|
82
|
+
// uppercase. El tamaño chico la hace funcionar como "etiqueta de sección".
|
|
81
83
|
.volanta {
|
|
82
|
-
margin: 0 0 6px;
|
|
83
84
|
color: var(--primary-color, #4bac48);
|
|
84
|
-
font-size:
|
|
85
|
+
font-size: 14px;
|
|
85
86
|
font-weight: 700;
|
|
86
87
|
text-transform: uppercase;
|
|
87
88
|
letter-spacing: 0.04em;
|
|
89
|
+
white-space: nowrap;
|
|
88
90
|
|
|
89
91
|
@include respond(tablet) {
|
|
90
|
-
font-size:
|
|
92
|
+
font-size: 18px;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
@include respond(desktop) {
|
|
94
|
-
font-size:
|
|
96
|
+
font-size: 20px;
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
|
|
@@ -148,8 +150,12 @@
|
|
|
148
150
|
@include respond(tablet) {
|
|
149
151
|
aspect-ratio: 16 / 7;
|
|
150
152
|
}
|
|
153
|
+
// Desktop: V1 cap la imagen importante a 500px (sin esto el hero ocupa
|
|
154
|
+
// ~774px y rompe el ratio respecto a V1). Replicamos acá para que V3 y
|
|
155
|
+
// V1 tengan la misma altura en portada de escritorio.
|
|
151
156
|
@include respond(desktop) {
|
|
152
157
|
aspect-ratio: unset;
|
|
158
|
+
max-height: 500px;
|
|
153
159
|
}
|
|
154
160
|
}
|
|
155
161
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Helper compartido por cabezales, hero, bajada, etc. La volanta se renderiza
|
|
2
|
+
// inline con el título separada por ". " (en cabezales el espacio lo da el
|
|
3
|
+
// `margin-right` del CSS; en bajada/leeAdemas/loQueSeLee es un literal en JSX).
|
|
4
|
+
// Si quien edita la nota ya cerró la volanta con un signo (".", "!", "?", "…"),
|
|
5
|
+
// no agregamos otro punto — devolvemos el texto tal cual.
|
|
6
|
+
|
|
7
|
+
const SENTENCE_END = /[.!?…]\s*$/
|
|
8
|
+
|
|
9
|
+
export function volantaWithStop(volanta) {
|
|
10
|
+
if (!volanta) return ''
|
|
11
|
+
return SENTENCE_END.test(volanta) ? volanta : `${volanta}.`
|
|
12
|
+
}
|