@crtobiasdelsud/portal-ui 1.0.5 → 1.0.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crtobiasdelsud/portal-ui",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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",
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+
3
+ import styles from './ArticleDetailView.module.scss'
4
+ import Standard from './Standard.jsx'
5
+ import Full from './Full.jsx'
6
+
7
+ /**
8
+ * ArticleDetailView — réplica portable de las screens `ArticleDetail` /
9
+ * `ArticleDetailFull` del portal (editor-template-front).
10
+ *
11
+ * Pensada para previews fuera del portal (ej. el CMS): es una *vista pura*,
12
+ * recibe el artículo ya resuelto y no hace data-fetching ni depende del
13
+ * registry de widgets. Reproduce el layout y los estilos de las screens, y
14
+ * elige la variante igual que el portal:
15
+ *
16
+ * article.tipoContenido === 'notaEspecial' → Full (ArticleDetailFull)
17
+ * resto → Standard (ArticleDetail)
18
+ *
19
+ * El root recrea el `<main>` del portal con `container-type: inline-size`
20
+ * para que las container-queries de los componentes portal-ui respondan al
21
+ * ancho real del contenedor — imprescindible al renderizar en un iframe.
22
+ *
23
+ * @param {object} props
24
+ * @param {object} props.article - Artículo resuelto. Shape esperado:
25
+ * { id, titulo, volanta, copete, imagen: { url, epigrafe }, imagenEpigrafe,
26
+ * focalPoint, categoria: { nombre, slug }, autor, publicarComoOrg,
27
+ * fechaPublicacion, tipoContenido, cuerpo (payload EditorJS) }
28
+ */
29
+ export default function ArticleDetailView({ article }) {
30
+ if (!article) return null
31
+
32
+ const isFull = article.tipoContenido === 'notaEspecial'
33
+
34
+ return (
35
+ <main className={styles.root}>
36
+ {isFull ? <Full article={article} /> : <Standard article={article} />}
37
+ </main>
38
+ )
39
+ }
@@ -0,0 +1,18 @@
1
+ // Root del preview — recrea el `<main>` del portal: es el container de las
2
+ // container-queries (`container-type: inline-size`) de los componentes
3
+ // portal-ui, para que el responsive responda al ancho real del contenedor
4
+ // (clave cuando se renderiza dentro de un iframe).
5
+ //
6
+ // NOTA: a diferencia del `<main>` real, NO se usa `min-height: 100dvh` —
7
+ // dentro de un iframe auto-dimensionado generaría un loop de altura.
8
+ .root {
9
+ overflow-x: clip;
10
+ display: flex;
11
+ flex-direction: column;
12
+ container-type: inline-size;
13
+ background: #fff;
14
+
15
+ // Fija la altura del hero "full" a un valor estable: dentro de un iframe
16
+ // auto-dimensionado, el `90vh/75vh` por defecto generaría un loop de altura.
17
+ --ahf-min-h: 620px;
18
+ }
@@ -0,0 +1,46 @@
1
+ 'use client'
2
+
3
+ import ArticleHeroFull from '../ArticleHeroFull/ArticleHeroFull.jsx'
4
+ import AuthorBlock from '../AuthorBlock/AuthorBlock.jsx'
5
+ import ShareBlock from '../ShareBlock/ShareBlock.jsx'
6
+ import EditorOutputFull from '../EditorOutputFull/EditorOutputFull.jsx'
7
+ import styles from './Full.module.scss'
8
+
9
+ /**
10
+ * Réplica de la screen `ArticleDetailFull` (camino non-AMP) del portal —
11
+ * la "nota especial": hero a sangre + cuerpo a una columna.
12
+ *
13
+ * Se omiten las piezas de infraestructura de la app (ArticleTracker y la
14
+ * zona post-body de widgets desde el registry).
15
+ */
16
+ export default function Full({ article }) {
17
+ const imagenEpigrafe = article.imagen?.epigrafe ?? article.imagenEpigrafe ?? null
18
+
19
+ return (
20
+ <>
21
+ <ArticleHeroFull
22
+ titulo={article.titulo}
23
+ copete={article.copete}
24
+ imagen={article.imagen?.url ?? null}
25
+ imagenEpigrafe={imagenEpigrafe}
26
+ focalPoint={article.focalPoint}
27
+ categoria={article.categoria ?? null}
28
+ />
29
+
30
+ <div className={styles.wrap}>
31
+ <div className={styles.author}>
32
+ <AuthorBlock
33
+ autor={article.autor}
34
+ publicarComoOrg={article.publicarComoOrg}
35
+ fechaPublicacion={article.fechaPublicacion}
36
+ />
37
+ <ShareBlock />
38
+ </div>
39
+
40
+ <div className={styles.body}>
41
+ <EditorOutputFull data={article.cuerpo} />
42
+ </div>
43
+ </div>
44
+ </>
45
+ )
46
+ }
@@ -0,0 +1,48 @@
1
+ // Réplica de ArticleDetailFull.module.scss del portal (editor-template-front).
2
+ @use "../../styles/index" as *;
3
+
4
+ .wrap {
5
+ width: 100%;
6
+ margin: 0 auto;
7
+ padding: 1.5rem 1rem;
8
+ box-sizing: border-box;
9
+
10
+ @include respond(tablet) {
11
+ max-width: 66.66667%;
12
+ min-width: 66.66667%;
13
+ margin-right: auto;
14
+ margin-left: auto;
15
+ padding: 0;
16
+ }
17
+
18
+ @include respond(laptop) {
19
+ max-width: 1250px;
20
+ }
21
+ }
22
+
23
+ .author {
24
+ display: flex;
25
+ align-items: stretch;
26
+ justify-content: space-between;
27
+ flex-direction: column;
28
+ margin: 1rem 0 1.5rem;
29
+ width: 100%;
30
+
31
+ @include respond(tablet) {
32
+ flex-direction: row;
33
+ align-items: center;
34
+ }
35
+ }
36
+
37
+ .heroImage {
38
+ width: 100%;
39
+ margin-bottom: 2rem;
40
+ }
41
+
42
+ .body {
43
+ width: 100%;
44
+ }
45
+
46
+ .postBody {
47
+ margin-top: 2rem;
48
+ }
@@ -0,0 +1,116 @@
1
+ 'use client'
2
+
3
+ import PageWrapper from '../UI/PageWrapper/PageWrapper.jsx'
4
+ import AspectImage from '../UI/AspectImage/AspectImage.jsx'
5
+ import EditorOutput from '../EditorOutput/EditorOutput.jsx'
6
+ import Breadcrumb from '../Breadcrumb/Breadcrumb.jsx'
7
+ import ArticleHero from '../ArticleHero/ArticleHero.jsx'
8
+ import AuthorBlock from '../AuthorBlock/AuthorBlock.jsx'
9
+ import ShareBlock from '../ShareBlock/ShareBlock.jsx'
10
+ import SpeechButton from '../SpeechButton/SpeechButton.jsx'
11
+ import ArticleSidebar from '../ArticleSidebar/ArticleSidebar.jsx'
12
+ import styles from './Standard.module.scss'
13
+
14
+ /**
15
+ * Réplica de la screen `ArticleDetail` (camino non-AMP) del portal.
16
+ *
17
+ * Reproduce el mismo árbol JSX y los mismos estilos. Se omiten las piezas
18
+ * que dependen de infraestructura de la app (ArticleTracker, zonas de
19
+ * widgets pre/post/in-body desde el registry, LoQueSeLee con data-fetching):
20
+ * el preview muestra el artículo en sí, no la config de layout del sitio.
21
+ */
22
+ export default function Standard({ article }) {
23
+ // Epígrafe de la imagen principal — normalizado en `imagen.epigrafe` y/o
24
+ // `imagenEpigrafe` a nivel raíz; puede venir null.
25
+ const imagenEpigrafe = article.imagen?.epigrafe ?? article.imagenEpigrafe ?? null
26
+
27
+ const breadcrumbItems = [
28
+ { label: 'Inicio', href: '/' },
29
+ { label: article.categoria?.nombre ?? '', href: `/${article.categoria?.slug ?? ''}` },
30
+ ]
31
+
32
+ return (
33
+ <PageWrapper>
34
+ <div className={styles.pageWrap}>
35
+ <div className={styles.articleWrap}>
36
+
37
+ {/* Row 2 — breadcrumb, hero, autor, speechbutton */}
38
+ <div className={styles.articleHeader}>
39
+ <Breadcrumb items={breadcrumbItems} />
40
+ <ArticleHero
41
+ titulo={article.titulo}
42
+ volanta={article.volanta}
43
+ copete={article.copete}
44
+ imagen={article.imagen?.url ?? null}
45
+ imagenEpigrafe={imagenEpigrafe}
46
+ focalPoint={article.focalPoint}
47
+ hideImageOnDesktop
48
+ extras={
49
+ <div className={styles.heroExtras}>
50
+ <AuthorBlock
51
+ autor={article.autor}
52
+ publicarComoOrg={article.publicarComoOrg}
53
+ fechaPublicacion={article.fechaPublicacion}
54
+ />
55
+ <ShareBlock />
56
+ </div>
57
+ }
58
+ />
59
+ {/* mobile-only: autor + share debajo del hero */}
60
+ <div className={styles.authorContainer}>
61
+ <AuthorBlock
62
+ autor={article.autor}
63
+ publicarComoOrg={article.publicarComoOrg}
64
+ fechaPublicacion={article.fechaPublicacion}
65
+ />
66
+ <ShareBlock />
67
+ </div>
68
+ {/* mobile-only: speechbutton debajo del hero */}
69
+ <div className={styles.speechMobile}>
70
+ <SpeechButton
71
+ titulo={article.titulo}
72
+ copete={article.copete}
73
+ cuerpo={article.cuerpo}
74
+ imagen={article.imagen?.url ?? null}
75
+ />
76
+ </div>
77
+ </div>
78
+
79
+ {/* Row 3 — body (2fr) + sidebar sticky (1fr) */}
80
+ <div className={styles.contentRow}>
81
+ <div className={styles.body}>
82
+ {article.imagen?.url && (
83
+ <figure className={styles.bodyImage}>
84
+ <AspectImage
85
+ src={article.imagen.url}
86
+ alt={article.titulo ?? ''}
87
+ aspect="16:9"
88
+ focalPoint={article.focalPoint}
89
+ />
90
+ {imagenEpigrafe && (
91
+ <figcaption className={styles.bodyImageCaption}>{imagenEpigrafe}</figcaption>
92
+ )}
93
+ </figure>
94
+ )}
95
+ <div className={styles.speechDesktop}>
96
+ <SpeechButton
97
+ titulo={article.titulo}
98
+ copete={article.copete}
99
+ cuerpo={article.cuerpo}
100
+ imagen={article.imagen?.url ?? null}
101
+ />
102
+ </div>
103
+ {article.cuerpo?.blocks?.length > 0 && (
104
+ <EditorOutput data={article.cuerpo} />
105
+ )}
106
+ </div>
107
+ <div className={styles.sidebarPlacement}>
108
+ <ArticleSidebar hasWidgets={false} />
109
+ </div>
110
+ </div>
111
+
112
+ </div>
113
+ </div>
114
+ </PageWrapper>
115
+ )
116
+ }
@@ -0,0 +1,179 @@
1
+ // Réplica de ArticleDetail.module.scss del portal (editor-template-front).
2
+ @use "../../styles/index" as *;
3
+
4
+ // ── Tokens ────────────────────────────────────────────────────────────────────
5
+ $page-max: 1376px;
6
+ $sidebar-w: 322px;
7
+ $col-gap: 30px;
8
+ $page-pad: 16px;
9
+
10
+ // ── Contenedor de ancho máximo ────────────────────────────────────────────────
11
+ .pageWrap {
12
+ width: 100%;
13
+ max-width: $page-max;
14
+ margin: 0 auto;
15
+ box-sizing: border-box;
16
+
17
+ @include respond(tablet) {
18
+ max-width: 720px;
19
+ }
20
+
21
+ @include respond(laptop) {
22
+ max-width: 960px;
23
+ }
24
+
25
+ @include respond(desktop) {
26
+ max-width: $page-max;
27
+ }
28
+ }
29
+
30
+ // ── Grid de 4 columnas iguales (igual que Home) ───────────────────────────────
31
+ .articleWrap {
32
+ @include respond(laptop) {
33
+ display: grid;
34
+ grid-template-columns: 1fr 1fr 1fr;
35
+ column-gap: $col-gap;
36
+ row-gap: 0;
37
+ align-items: start;
38
+ }
39
+
40
+ @include respond(desktop) {
41
+ display: grid;
42
+ grid-template-columns: 1fr 1fr 1fr 1fr;
43
+ column-gap: $col-gap;
44
+ row-gap: 0;
45
+ align-items: start;
46
+ }
47
+ }
48
+
49
+ // Breadcrumb + Hero + Author + SpeechButton — cols 1-3, row 2
50
+ .articleHeader {
51
+ min-width: 0;
52
+ padding-top: 1.5rem;
53
+
54
+ @include respond(laptop) {
55
+ grid-column: 1 / 3;
56
+ grid-row: 2;
57
+ }
58
+
59
+ @include respond(desktop) {
60
+ grid-column: 1 / 4;
61
+ grid-row: 2;
62
+ }
63
+ }
64
+
65
+ // Row 3 — grid item que agrupa body + sidebar.
66
+ .contentRow {
67
+ min-width: 0;
68
+
69
+ @include respond(laptop) {
70
+ grid-column: 1 / 4;
71
+ grid-row: 3;
72
+ display: flex;
73
+ align-items: stretch;
74
+ gap: $col-gap;
75
+ }
76
+
77
+ @include respond(desktop) {
78
+ grid-column: 1 / 4;
79
+ grid-row: 3;
80
+ display: flex;
81
+ align-items: stretch;
82
+ gap: $col-gap;
83
+ }
84
+ }
85
+
86
+ // Body — hijo flex del contentRow (2/3 del ancho)
87
+ .body {
88
+ min-width: 0;
89
+ flex: 2 1 0;
90
+ align-self: flex-start;
91
+ }
92
+
93
+ // Sidebar — ocupa el alto del body para que los sticky internos funcionen
94
+ .sidebarPlacement {
95
+ display: none;
96
+ container-type: inline-size;
97
+
98
+ @include respond(laptop) {
99
+ display: block;
100
+ flex: 1 0 0;
101
+ min-width: 0;
102
+ }
103
+
104
+ @include respond(desktop) {
105
+ display: block;
106
+ flex: 1 0 0;
107
+ min-width: 0;
108
+ }
109
+ }
110
+
111
+ // Widgets dentro del body — separación vertical entre widgets consecutivos
112
+ .inBodyWidget {
113
+ margin-top: 24px;
114
+ margin-bottom: 24px;
115
+ }
116
+
117
+ // ── Imagen del artículo en el body (tablet+) ─────────────────────────────────
118
+ .bodyImage {
119
+ display: none;
120
+ position: relative;
121
+ margin: 0;
122
+
123
+ @include respond(tablet) {
124
+ display: block;
125
+ margin: 0 0 1.5rem;
126
+ }
127
+ }
128
+
129
+ // ── Epígrafe — franja negra sobre el borde inferior de la imagen ─────────────
130
+ .bodyImageCaption {
131
+ position: absolute;
132
+ left: 0;
133
+ right: 0;
134
+ bottom: 0;
135
+ z-index: 1;
136
+ margin: 0;
137
+ padding: 10px 14px;
138
+ font-family: "Inter", "Helvetica", Arial, sans-serif;
139
+ font-size: 0.85rem;
140
+ line-height: 1.4;
141
+ color: #fff;
142
+ background: #000;
143
+ }
144
+
145
+ // ── SpeechButton mobile (en articleHeader) / desktop (en body tras imagen) ───
146
+ .speechMobile {
147
+ @include respond(laptop) {
148
+ display: none;
149
+ }
150
+ }
151
+
152
+ .speechDesktop {
153
+ display: none;
154
+
155
+ @include respond(laptop) {
156
+ display: block;
157
+ margin-bottom: 1rem;
158
+ }
159
+ }
160
+
161
+ // ── Author + share (mobile) ───────────────────────────────────────────────────
162
+ .authorContainer {
163
+ display: flex;
164
+ flex-direction: column;
165
+ margin-bottom: 0.5rem;
166
+
167
+ @include respond(laptop) {
168
+ display: none;
169
+ }
170
+ }
171
+
172
+ // Extras dentro del hero — visible solo en desktop (el ArticleHero lo oculta en mobile)
173
+ .heroExtras {
174
+ display: flex;
175
+ flex-direction: row;
176
+ justify-content: space-between;
177
+ align-items: center;
178
+ padding-top: 0.75rem;
179
+ }
@@ -14,7 +14,7 @@ import V0Desktop from './variants/V0Desktop/V0Desktop'
14
14
 
15
15
  const VARIANTS = { '0': V0, '1': V1, '2': V2, '3': V3, '4': V4, '5': V5 }
16
16
 
17
- export default function ArticleHero({ titulo, volanta, copete, imagen, focalPoint, isAmp = false, extras = null, hideImageOnDesktop = false }) {
17
+ export default function ArticleHero({ titulo, volanta, copete, imagen, imagenEpigrafe, focalPoint, isAmp = false, extras = null, hideImageOnDesktop = false }) {
18
18
  const theme = useTheme()
19
19
  const variant = String(theme.articleHero ?? 1)
20
20
 
@@ -27,10 +27,21 @@ export default function ArticleHero({ titulo, volanta, copete, imagen, focalPoin
27
27
 
28
28
  const ExtrasEl = (!isAmp && extras) ? <div className={styles.extras}>{extras}</div> : null
29
29
 
30
+ // Epígrafe: overlay sutil sobre el borde inferior de la imagen del hero.
31
+ // Solo non-amp (en amp no se aplican los CSS modules).
32
+ const EpigrafeEl = (!isAmp && imagen && imagenEpigrafe)
33
+ ? <p className={styles.epigrafe}>{imagenEpigrafe}</p>
34
+ : null
35
+
30
36
  const ImgEl = imagen
31
37
  ? isAmp
32
38
  ? <img src={imagen} alt={titulo ?? ''} className="article-hero__img" />
33
- : <AspectImage src={imagen} alt={titulo ?? ''} aspect="16:9" fill={true} focalPoint={focalPoint} />
39
+ : (
40
+ <>
41
+ <AspectImage src={imagen} alt={titulo ?? ''} aspect="16:9" fill={true} focalPoint={focalPoint} />
42
+ {EpigrafeEl}
43
+ </>
44
+ )
34
45
  : null
35
46
 
36
47
  const imgWrapClass = isAmp
@@ -87,3 +87,19 @@
87
87
  max-height: none;
88
88
  }
89
89
  }
90
+
91
+ // ── Epígrafe de la imagen — franja negra sobre el borde inferior de la foto ──
92
+ .epigrafe {
93
+ position: absolute;
94
+ left: 0;
95
+ right: 0;
96
+ bottom: 0;
97
+ z-index: 1;
98
+ margin: 0;
99
+ padding: 10px 14px;
100
+ font-family: "Inter", "Helvetica", Arial, sans-serif;
101
+ font-size: 0.8rem;
102
+ line-height: 1.35;
103
+ color: #fff;
104
+ background: #000;
105
+ }
@@ -3,7 +3,7 @@
3
3
  import styles from './ArticleHeroFull.module.scss'
4
4
  import { useSiteConfig } from '../../context/SiteConfigContext.jsx'
5
5
 
6
- export default function ArticleHeroFull({ titulo, copete, imagen, focalPoint, categoria }) {
6
+ export default function ArticleHeroFull({ titulo, copete, imagen, imagenEpigrafe, focalPoint, categoria }) {
7
7
  const { config } = useSiteConfig()
8
8
  const siteName = config?.slots?.header?.settings?.siteName ?? ''
9
9
 
@@ -22,6 +22,9 @@ export default function ArticleHeroFull({ titulo, copete, imagen, focalPoint, ca
22
22
  />
23
23
  )}
24
24
  <div className={styles.gradient} />
25
+ {imagen && imagenEpigrafe && (
26
+ <p className={styles.epigrafe}>{imagenEpigrafe}</p>
27
+ )}
25
28
  <div className={styles.content}>
26
29
  {categoria && (
27
30
  <div className={styles.breadcrumb}>
@@ -3,17 +3,20 @@
3
3
  .hero {
4
4
  position: relative;
5
5
  width: 100%;
6
- min-height: 90vh;
6
+ // Altura via custom property: el portal usa el default en `vh`; un preview
7
+ // dentro de un iframe puede fijar `--ahf-min-h` a un valor estable y evitar
8
+ // el loop de altura (vh → alto del iframe → scrollHeight → vh…).
9
+ min-height: var(--ahf-min-h, 90vh);
7
10
  overflow: hidden;
8
11
  display: flex;
9
12
  align-items: flex-end;
10
13
 
11
14
  @include respond(tablet) {
12
- min-height: 75vh;
15
+ min-height: var(--ahf-min-h, 75vh);
13
16
  }
14
17
 
15
18
  @include respond(desktop) {
16
- min-height: 75vh;
19
+ min-height: var(--ahf-min-h, 75vh);
17
20
  }
18
21
  }
19
22
 
@@ -113,4 +116,24 @@
113
116
  line-height: 1.3em;
114
117
  padding-top: 1.875rem;
115
118
  }
119
+ }
120
+
121
+ // ── Epígrafe de la imagen — crédito sutil en el borde inferior del hero ──────
122
+ .epigrafe {
123
+ position: absolute;
124
+ right: 0;
125
+ bottom: 0;
126
+ left: 0;
127
+ z-index: 1;
128
+ margin: 0;
129
+ padding: 0 5% 0.5rem;
130
+ font-family: var(--font-inter), Inter, sans-serif;
131
+ font-size: 0.78rem;
132
+ line-height: 1.35;
133
+ text-align: right;
134
+ color: rgb(255 255 255 / 0.75);
135
+
136
+ @include respond(tablet) {
137
+ padding: 0 10% 0.6rem;
138
+ }
116
139
  }
@@ -15,7 +15,7 @@ function formatDate(fechaPublicacion, useLongDate) {
15
15
  const fecha = date.toLocaleDateString('es-AR', useLongDate
16
16
  ? { day: '2-digit', month: 'long', year: 'numeric' }
17
17
  : { day: '2-digit', month: '2-digit', year: 'numeric' })
18
- const hora = date.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit' })
18
+ const hora = date.toLocaleTimeString('es-AR', { hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
19
19
  return `${fecha} - ${hora}`
20
20
  }
21
21
 
@@ -225,22 +225,27 @@ function Block({ block, cls, isAmp }) {
225
225
  return <div className={cls.raw} dangerouslySetInnerHTML={{ __html: block.data.html }} suppressHydrationWarning />
226
226
 
227
227
  case "pullquote": {
228
- const { variant, text } = block.data
228
+ const { variant, text, color, align } = block.data
229
229
  const hasClose = variant !== "2"
230
+ // color === '' → el CSS cae a var(--primary-color, #af0437).
231
+ // align ausente (bloques viejos) → default 'center'. Sólo afecta al
232
+ // texto; las comillas quedan fijas (apertura izq, cierre der).
233
+ const pqStyle = { "--pq-text-align": align || "center" }
234
+ if (color) pqStyle["--eo-pullquote-color"] = color
230
235
  if (isAmp) {
231
236
  return (
232
- <div className={cls.pullquote}>
237
+ <div className={cls.pullquote} style={pqStyle}>
233
238
  <span className={cls.pullquoteOpen}>&ldquo;</span>
234
- <p>{text}</p>
239
+ <p dangerouslySetInnerHTML={{ __html: text }} suppressHydrationWarning />
235
240
  {hasClose && <span className={cls.pullquoteClose}>&rdquo;</span>}
236
241
  </div>
237
242
  )
238
243
  }
239
244
  const pullCls = [cls.pullquote, cls[`pullquoteV${variant}`]].filter(Boolean).join(" ")
240
245
  return (
241
- <div className={pullCls}>
246
+ <div className={pullCls} style={pqStyle}>
242
247
  <span className={cls.pullquoteOpen}>&ldquo;</span>
243
- <p>{text}</p>
248
+ <p dangerouslySetInnerHTML={{ __html: text }} suppressHydrationWarning />
244
249
  {hasClose && <span className={cls.pullquoteClose}>&rdquo;</span>}
245
250
  </div>
246
251
  )
@@ -209,6 +209,53 @@
209
209
  line-height: 1.6;
210
210
  }
211
211
 
212
+ /* ── Pullquote / Cita destacada ──
213
+ El color usa `--eo-pullquote-color` (parámetro elegido en el CMS);
214
+ si no se setea, cae al color del portal: var(--primary-color, #af0437). */
215
+ .pullquote {
216
+ --pq-color: var(--eo-pullquote-color, var(--primary-color, #af0437));
217
+ margin: 2.25rem 0;
218
+ padding: 0.25rem 0;
219
+ }
220
+
221
+ .pullquoteOpen,
222
+ .pullquoteClose {
223
+ display: block;
224
+ font-family: Georgia, "Times New Roman", serif;
225
+ font-weight: 700;
226
+ font-size: 3.75rem;
227
+ line-height: 0.1;
228
+ color: var(--pq-color);
229
+ }
230
+
231
+ /* Comillas fijas: apertura a la izquierda, cierre a la derecha. */
232
+ .pullquoteOpen { margin-bottom: 1.5rem; text-align: left; }
233
+ .pullquoteClose { margin-top: 1.25rem; text-align: right; }
234
+
235
+ .pullquote p {
236
+ margin: 0;
237
+ font-family: Georgia, "Times New Roman", serif;
238
+ font-size: 1.6rem;
239
+ font-weight: 700;
240
+ line-height: 1.4;
241
+ color: var(--pq-color);
242
+ /* La alineación elegida en el CMS sólo afecta al texto. */
243
+ text-align: var(--pq-text-align, center);
244
+ }
245
+
246
+ /* Variante 3 — comillas en color, texto en negro. */
247
+ .pullquoteV3 p { color: #111; }
248
+
249
+ /* Variante 2 — fondo oscuro, texto blanco, sólo comilla de apertura. */
250
+ .pullquoteV2 {
251
+ background: #333;
252
+ border-radius: 6px;
253
+ padding: 1.5rem 1.75rem 2rem;
254
+
255
+ p { color: #fff; }
256
+ .pullquoteClose { display: none; }
257
+ }
258
+
212
259
  .table {
213
260
  width: 100%;
214
261
  border-collapse: collapse;
@@ -225,22 +225,27 @@ function Block({ block, cls, isAmp }) {
225
225
  return <div className={cls.raw} dangerouslySetInnerHTML={{ __html: block.data.html }} />
226
226
 
227
227
  case "pullquote": {
228
- const { variant, text } = block.data
228
+ const { variant, text, color, align } = block.data
229
229
  const hasClose = variant !== "2"
230
+ // color === '' → el CSS cae a var(--primary-color, #af0437).
231
+ // align ausente (bloques viejos) → default 'center'. Sólo afecta al
232
+ // texto; las comillas quedan fijas (apertura izq, cierre der).
233
+ const pqStyle = { "--pq-text-align": align || "center" }
234
+ if (color) pqStyle["--eo-pullquote-color"] = color
230
235
  if (isAmp) {
231
236
  return (
232
- <div className={cls.pullquote}>
237
+ <div className={cls.pullquote} style={pqStyle}>
233
238
  <span className={cls.pullquoteOpen}>&ldquo;</span>
234
- <p>{text}</p>
239
+ <p dangerouslySetInnerHTML={{ __html: text }} />
235
240
  {hasClose && <span className={cls.pullquoteClose}>&rdquo;</span>}
236
241
  </div>
237
242
  )
238
243
  }
239
244
  const pullCls = [cls.pullquote, cls[`pullquoteV${variant}`]].filter(Boolean).join(" ")
240
245
  return (
241
- <div className={pullCls}>
246
+ <div className={pullCls} style={pqStyle}>
242
247
  <span className={cls.pullquoteOpen}>&ldquo;</span>
243
- <p>{text}</p>
248
+ <p dangerouslySetInnerHTML={{ __html: text }} />
244
249
  {hasClose && <span className={cls.pullquoteClose}>&rdquo;</span>}
245
250
  </div>
246
251
  )
@@ -216,6 +216,53 @@
216
216
  line-height: 1.6;
217
217
  }
218
218
 
219
+ /* ── Pullquote / Cita destacada ──
220
+ El color usa `--eo-pullquote-color` (parámetro elegido en el CMS);
221
+ si no se setea, cae al color del portal: var(--primary-color, #af0437). */
222
+ .pullquote {
223
+ --pq-color: var(--eo-pullquote-color, var(--primary-color, #af0437));
224
+ margin: 2.25rem 0;
225
+ padding: 0.25rem 0;
226
+ }
227
+
228
+ .pullquoteOpen,
229
+ .pullquoteClose {
230
+ display: block;
231
+ font-family: Georgia, "Times New Roman", serif;
232
+ font-weight: 700;
233
+ font-size: 3.75rem;
234
+ line-height: 0.1;
235
+ color: var(--pq-color);
236
+ }
237
+
238
+ /* Comillas fijas: apertura a la izquierda, cierre a la derecha. */
239
+ .pullquoteOpen { margin-bottom: 1.5rem; text-align: left; }
240
+ .pullquoteClose { margin-top: 1.25rem; text-align: right; }
241
+
242
+ .pullquote p {
243
+ margin: 0;
244
+ font-family: Georgia, "Times New Roman", serif;
245
+ font-size: 1.6rem;
246
+ font-weight: 700;
247
+ line-height: 1.4;
248
+ color: var(--pq-color);
249
+ /* La alineación elegida en el CMS sólo afecta al texto. */
250
+ text-align: var(--pq-text-align, center);
251
+ }
252
+
253
+ /* Variante 3 — comillas en color, texto en negro. */
254
+ .pullquoteV3 p { color: #111; }
255
+
256
+ /* Variante 2 — fondo oscuro, texto blanco, sólo comilla de apertura. */
257
+ .pullquoteV2 {
258
+ background: #333;
259
+ border-radius: 6px;
260
+ padding: 1.5rem 1.75rem 2rem;
261
+
262
+ p { color: #fff; }
263
+ .pullquoteClose { display: none; }
264
+ }
265
+
219
266
  .table {
220
267
  width: 100%;
221
268
  border-collapse: collapse;
@@ -33,13 +33,19 @@ export default function FooterSimple({ isAmp = false }) {
33
33
  } = s
34
34
  const social = { ...headerSocial, ...(s.social ?? {}) }
35
35
 
36
- const safeTextColor = footerTextColor ?? theme.textColor ?? '#ffffff'
36
+ // Colores propios del footer (footer.settings) con fallback al theme global
37
+ const footerBg = s.backgroundColor ?? theme.secondary ?? '#0D1333'
38
+ const footerText = s.textColor ?? footerTextColor ?? theme.textColor ?? '#ffffff'
39
+ const footerAccent = s.primaryColor ?? theme.primary ?? footerBg
40
+ const footerFont = s.fontFamily
41
+
37
42
  const inlineStyle = {
38
- color: safeTextColor,
39
- '--primary-color': theme.primary,
40
- '--secondary-color': theme.secondary,
41
- '--text-color': safeTextColor,
42
- '--social-hover-filter': theme.primary ? hexToCssFilter(theme.primary) : 'none',
43
+ color: footerText,
44
+ ...(footerFont && { fontFamily: footerFont }),
45
+ '--primary-color': footerAccent,
46
+ '--secondary-color': footerBg,
47
+ '--text-color': footerText,
48
+ '--social-hover-filter': footerAccent ? hexToCssFilter(footerAccent) : 'none',
43
49
  }
44
50
 
45
51
  const logoEl = (logoUrl || iconUrl) && (
@@ -140,8 +146,8 @@ export default function FooterSimple({ isAmp = false }) {
140
146
  }
141
147
 
142
148
  return (
143
- <div className={styles.fullcontainer}>
144
- <footer className={styles.container} style={inlineStyle}>
149
+ <div className={styles.fullcontainer} style={inlineStyle}>
150
+ <footer className={styles.container}>
145
151
 
146
152
  {/* ── MAIN: izq (logo+slogan+legal) | der (nav + social+links) ── */}
147
153
  <div className={styles.mainRow}>
@@ -14,13 +14,13 @@
14
14
  display: flex;
15
15
  align-items: center;
16
16
  padding-left: 15px;
17
-
18
- color: white;
17
+
18
+ color: var(--text-color, #fff);
19
19
  font-family: var(--font-inter, Inter, sans-serif);
20
20
  }
21
21
 
22
22
  .fullcontainer{
23
- color: white;
23
+ color: var(--text-color, #fff);
24
24
 
25
25
  }
26
26
 
@@ -34,7 +34,7 @@
34
34
  box-sizing: border-box;
35
35
  margin-top: auto;
36
36
  min-height: 268px;
37
- color: white;
37
+ color: var(--text-color, #fff);
38
38
  }
39
39
 
40
40
  // ── MAIN ROW (2 columns) ──────────────────────────────────────────────────────
@@ -91,7 +91,7 @@
91
91
  width: 100%;
92
92
 
93
93
  @media (min-width: $tablet) {
94
- border-top: 1px solid white;
94
+ border-top: 1px solid var(--text-color, #fff);
95
95
  padding-top: 20px;
96
96
  flex-direction: row;
97
97
  align-items: center;
@@ -155,10 +155,9 @@
155
155
  text-transform: uppercase;
156
156
  color: inherit;
157
157
  opacity: 1;
158
- transition: opacity 0.15s;
159
-
158
+ transition: color 0.15s;
160
159
 
161
- &:hover { opacity: 1; }
160
+ &:hover { color: var(--primary-color, #B1043F); }
162
161
  }
163
162
 
164
163
  // ── Divider ───────────────────────────────────────────────────────────────────
@@ -209,7 +208,7 @@
209
208
  }
210
209
 
211
210
  a {
212
- color: white;
211
+ color: var(--text-color, #fff);
213
212
  }
214
213
  }
215
214
 
@@ -246,11 +245,11 @@
246
245
  color: inherit;
247
246
  text-decoration: none;
248
247
  opacity: 1;
249
- transition: opacity 0.15s;
248
+ transition: color 0.15s;
250
249
  white-space: nowrap;
251
250
 
252
251
  &:hover {
253
- opacity: 1;
252
+ color: var(--primary-color, #B1043F);
254
253
  text-decoration: underline;
255
254
  }
256
255
  }
package/src/index.js CHANGED
@@ -57,6 +57,9 @@ export { default as ArticleHero } from './components/ArticleHero/ArticleHero
57
57
  export { default as ArticleHeroFull } from './components/ArticleHeroFull/ArticleHeroFull.jsx'
58
58
  export { default as ArticleSidebar } from './components/ArticleSidebar/ArticleSidebar.jsx'
59
59
 
60
+ // === Screens / vistas de detalle de artículo (réplica portable para previews) ===
61
+ export { default as ArticleDetailView } from './components/ArticleDetailView/ArticleDetailView.jsx'
62
+
60
63
  // === Headers ===
61
64
  export { default as HeaderSimpleSwitch } from './components/Headers/HeaderSimple/HeaderSimpleSwitch/HeaderSimpleSwitch.jsx'
62
65
  export { default as HeaderSimpleDesktop } from './components/Headers/HeaderSimple/HeaderSimpleDesktop/HeaderSimpleDesktop.jsx'