@crtobiasdelsud/portal-ui 1.1.13 → 1.1.14
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/ArticleHero/ArticleHero.jsx +4 -1
- package/src/components/EditorOutput/EditorOutput.jsx +6 -1
- package/src/components/Footers/FooterSimple/FooterSimple.jsx +66 -4
- package/src/components/Headers/HeaderSimple/HeaderSimpleAmp/HeaderSimpleAmp.jsx +77 -47
- package/src/utils/sanitizeHtml.js +21 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crtobiasdelsud/portal-ui",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
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",
|
|
@@ -43,7 +43,10 @@ export default function ArticleHero({ titulo, volanta, copete, imagen, imagenes,
|
|
|
43
43
|
alt={titulo ?? ''}
|
|
44
44
|
class="article-hero__img"
|
|
45
45
|
layout="fill"
|
|
46
|
-
|
|
46
|
+
>
|
|
47
|
+
{/* Placeholder si la imagen falla (AMP no deja colapsar el hueco). */}
|
|
48
|
+
<div fallback="" className="article-hero__img-fallback">Imagen no disponible</div>
|
|
49
|
+
</amp-img>
|
|
47
50
|
)
|
|
48
51
|
} else if (slides.length > 1) {
|
|
49
52
|
ImgEl = (
|
|
@@ -285,7 +285,12 @@ function Block({ block, cls, isAmp }) {
|
|
|
285
285
|
width={w || 1200}
|
|
286
286
|
height={h || 675}
|
|
287
287
|
layout="responsive"
|
|
288
|
-
|
|
288
|
+
>
|
|
289
|
+
{/* AMP no permite JS para colapsar el hueco si la imagen
|
|
290
|
+
falla; el `fallback` es lo válido: muestra un placeholder
|
|
291
|
+
prolijo en lugar de un recuadro vacío. */}
|
|
292
|
+
<div fallback="" className="eo-image-fallback">Imagen no disponible</div>
|
|
293
|
+
</amp-img>
|
|
289
294
|
: <img
|
|
290
295
|
src={imgSrc}
|
|
291
296
|
alt={alt}
|
|
@@ -137,11 +137,73 @@ export default function FooterSimple({ isAmp = false }) {
|
|
|
137
137
|
)
|
|
138
138
|
|
|
139
139
|
if (isAmp) {
|
|
140
|
+
// Markup AMP dedicado con clases fijas `footer-simple__*` (las del partial
|
|
141
|
+
// SCSS compilado a amp-custom) y <amp-img> — NO clases de CSS module, que en
|
|
142
|
+
// AMP no existen. Misma estructura que el footer real: fila de 2 columnas
|
|
143
|
+
// (izq logo+slogan+legal / der categorías + redes+links) y barra inferior.
|
|
144
|
+
const catHref = (slug) => (slug ? (slug.startsWith('/') ? slug : `/${slug}`) : '#')
|
|
145
|
+
// Sin `style` inline: AMP lo prohíbe. Los colores del footer llegan por las
|
|
146
|
+
// vars globales --footer-bg/--footer-text/--footer-accent (ver _document.jsx).
|
|
140
147
|
return (
|
|
141
|
-
<footer className="footer-simple"
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
<footer className="footer-simple">
|
|
149
|
+
<div className="footer-simple__main">
|
|
150
|
+
|
|
151
|
+
<div className="footer-simple__left">
|
|
152
|
+
{(logoUrl || iconUrl) && (
|
|
153
|
+
<div className="footer-simple__logo-wrap">
|
|
154
|
+
<a href="/" aria-label={`Ir a inicio - ${siteName ?? ''}`}>
|
|
155
|
+
<amp-img src={logoUrl || iconUrl} alt={logoAlt || siteName} class="footer-simple__logo-img" layout="fixed-height" height="53" />
|
|
156
|
+
</a>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
{slogan && <p className="footer-simple__slogan">{slogan}</p>}
|
|
160
|
+
<div className="footer-simple__legal">
|
|
161
|
+
{legal.owner && <p className="footer-simple__legal-row">Propietario: <strong>{legal.owner}</strong></p>}
|
|
162
|
+
{legal.director && <p className="footer-simple__legal-row">Director: <strong>{legal.director}</strong></p>}
|
|
163
|
+
{legal.edition && <p className="footer-simple__legal-row">Edición: <strong>{legal.edition}</strong></p>}
|
|
164
|
+
<p className="footer-simple__legal-row">Fecha: <strong>{formatFecha()}</strong></p>
|
|
165
|
+
{legal.dnda && <p className="footer-simple__legal-row">DNDA: <strong>{legal.dnda}</strong></p>}
|
|
166
|
+
{legal.address && <p className="footer-simple__legal-row">Domicilio legal: <strong>{legal.address}</strong></p>}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div className="footer-simple__right">
|
|
171
|
+
{categories.length > 0 && (
|
|
172
|
+
<nav className="footer-simple__nav">
|
|
173
|
+
<ul className="footer-simple__nav-list">
|
|
174
|
+
{categories.map((cat) => (
|
|
175
|
+
<li key={cat.id}>
|
|
176
|
+
<a href={catHref(cat.slug)} className="footer-simple__nav-link">{cat.label}</a>
|
|
177
|
+
</li>
|
|
178
|
+
))}
|
|
179
|
+
</ul>
|
|
180
|
+
</nav>
|
|
181
|
+
)}
|
|
182
|
+
<div className="footer-simple__social-row">
|
|
183
|
+
<div className="footer-simple__social">
|
|
184
|
+
{social.facebook && <a href={social.facebook} aria-label="Facebook" className="footer-simple__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/facebook.svg" alt="Facebook" width="22" height="22" layout="fixed" /></a>}
|
|
185
|
+
{social.instagram && <a href={social.instagram} aria-label="Instagram" className="footer-simple__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/instagram.svg" alt="Instagram" width="22" height="22" layout="fixed" /></a>}
|
|
186
|
+
{social.tiktok && <a href={social.tiktok} aria-label="TikTok" className="footer-simple__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/tiktok.svg" alt="TikTok" width="22" height="22" layout="fixed" /></a>}
|
|
187
|
+
{social.youtube && <a href={social.youtube} aria-label="YouTube" className="footer-simple__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/youtube.svg" alt="YouTube" width="22" height="22" layout="fixed" /></a>}
|
|
188
|
+
</div>
|
|
189
|
+
{allLinks.length > 0 && (
|
|
190
|
+
<div className="footer-simple__links">
|
|
191
|
+
{allLinks.map(({ label, href }, i) => (
|
|
192
|
+
<span key={href} className="footer-simple__link-item">
|
|
193
|
+
{i > 0 && <span className="footer-simple__link-sep">/</span>}
|
|
194
|
+
<a href={href} className="footer-simple__link">{label}</a>
|
|
195
|
+
</span>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{bottomBarContent && (
|
|
205
|
+
<div className="footer-simple__bottom">{bottomBarContent}</div>
|
|
206
|
+
)}
|
|
145
207
|
</footer>
|
|
146
208
|
)
|
|
147
209
|
}
|
|
@@ -19,17 +19,26 @@ export default function HeaderSimpleAmp({ settings = {} }) {
|
|
|
19
19
|
<div className="header-simple__brand">
|
|
20
20
|
{resolvedLogo ? (
|
|
21
21
|
<a href="/" aria-label={`Ir a inicio - ${siteName}`}>
|
|
22
|
-
<amp-img src={resolvedLogo.src ?? resolvedLogo} alt={logoAlt || siteName} class="header-simple__logo-img" layout="fixed-height" height="
|
|
22
|
+
<amp-img src={resolvedLogo.src ?? resolvedLogo} alt={logoAlt || siteName} class="header-simple__logo-img" layout="fixed-height" height="44" />
|
|
23
23
|
</a>
|
|
24
24
|
) : logoUrl ? (
|
|
25
25
|
<a href="/" aria-label={`Ir a inicio - ${siteName}`}>
|
|
26
|
-
<amp-img src={logoUrl} alt={logoAlt || siteName} class="header-simple__logo-img" layout="fixed-height" height="
|
|
26
|
+
<amp-img src={logoUrl} alt={logoAlt || siteName} class="header-simple__logo-img" layout="fixed-height" height="44" />
|
|
27
27
|
</a>
|
|
28
28
|
) : (
|
|
29
29
|
<span className="header-simple__logo">{siteName}</span>
|
|
30
30
|
)}
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
|
+
{/* Desktop: redes a la derecha (en mobile se usa la hamburguesa). */}
|
|
34
|
+
<div className="header-simple__social-desktop">
|
|
35
|
+
{social.facebook && <a href={social.facebook} aria-label="Facebook" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/facebook.svg" alt="Facebook" width="22" height="22" layout="fixed" /></a>}
|
|
36
|
+
{social.twitter && <a href={social.twitter} aria-label="X" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/x.svg" alt="X" width="22" height="22" layout="fixed" /></a>}
|
|
37
|
+
{social.instagram && <a href={social.instagram} aria-label="Instagram" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/instagram.svg" alt="Instagram" width="22" height="22" layout="fixed" /></a>}
|
|
38
|
+
{social.tiktok && <a href={social.tiktok} aria-label="TikTok" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/tiktok.svg" alt="TikTok" width="22" height="22" layout="fixed" /></a>}
|
|
39
|
+
{social.youtube && <a href={social.youtube} aria-label="YouTube" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/youtube.svg" alt="YouTube" width="22" height="22" layout="fixed" /></a>}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
33
42
|
<button
|
|
34
43
|
className="header-simple__burger"
|
|
35
44
|
on="tap:amp-nav-sidebar.toggle"
|
|
@@ -40,56 +49,77 @@ export default function HeaderSimpleAmp({ settings = {} }) {
|
|
|
40
49
|
</svg>
|
|
41
50
|
</button>
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
<div className="amp-sidebar__header">
|
|
45
|
-
<button on="tap:amp-nav-sidebar.close" className="amp-sidebar__close" aria-label="Cerrar menú">✕</button>
|
|
46
|
-
</div>
|
|
52
|
+
</div>
|
|
47
53
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
{/* Desktop: barra de categorías horizontal — mismo look que CategoriesBar
|
|
55
|
+
del portal (links en mayúscula separados por puntos), pero SIN el
|
|
56
|
+
carrusel con flechas (AMP no permite el JS de scroll). */}
|
|
57
|
+
{categories.length > 0 && (
|
|
58
|
+
<nav className="header-simple__cats" aria-label="Categorías">
|
|
59
|
+
<ul className="header-simple__cats-list">
|
|
60
|
+
{categories.map((cat) => (
|
|
61
|
+
<li key={cat.id} className="header-simple__cats-item">
|
|
62
|
+
<a
|
|
63
|
+
href={cat.slug ? (cat.slug.startsWith('/') ? cat.slug : `/${cat.slug}`) : '#'}
|
|
64
|
+
className="header-simple__cats-link"
|
|
65
|
+
>
|
|
66
|
+
{(cat.label ?? '').toUpperCase()}
|
|
67
|
+
</a>
|
|
68
|
+
</li>
|
|
69
|
+
))}
|
|
70
|
+
</ul>
|
|
71
|
+
</nav>
|
|
72
|
+
)}
|
|
53
73
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{cat.subcategories?.length ? (
|
|
59
|
-
<details className="amp-sidebar__sub-menu">
|
|
60
|
-
<summary className="amp-sidebar__nav-link-summary">
|
|
61
|
-
{cat.label}
|
|
62
|
-
<span className="amp-sidebar__arrow">›</span>
|
|
63
|
-
</summary>
|
|
64
|
-
<ul className="amp-sidebar__sub-list">
|
|
65
|
-
{cat.subcategories.map((sub) => (
|
|
66
|
-
<li key={sub.id}>
|
|
67
|
-
<a href={sub.slug ? (sub.slug.startsWith('/') ? sub.slug : `/${sub.slug}`) : '#'} className="amp-sidebar__sub-link">{sub.label}</a>
|
|
68
|
-
</li>
|
|
69
|
-
))}
|
|
70
|
-
</ul>
|
|
71
|
-
</details>
|
|
72
|
-
) : (
|
|
73
|
-
<a href={cat.slug ? (cat.slug.startsWith('/') ? cat.slug : `/${cat.slug}`) : '#'} className="amp-sidebar__nav-link">{cat.label}</a>
|
|
74
|
-
)}
|
|
75
|
-
</li>
|
|
76
|
-
))}
|
|
77
|
-
</ul>
|
|
78
|
-
</nav>
|
|
74
|
+
<amp-sidebar id="amp-nav-sidebar" layout="nodisplay" side="left">
|
|
75
|
+
<div className="amp-sidebar__header">
|
|
76
|
+
<button on="tap:amp-nav-sidebar.close" className="amp-sidebar__close" aria-label="Cerrar menú">✕</button>
|
|
77
|
+
</div>
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<
|
|
83
|
-
{social.facebook && <a href={social.facebook} aria-label="Facebook" className="amp-sidebar__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/facebook.svg" alt="Facebook" width="24" height="24" layout="fixed" /></a>}
|
|
84
|
-
{social.twitter && <a href={social.twitter} aria-label="X" className="amp-sidebar__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/x.svg" alt="X" width="24" height="24" layout="fixed" /></a>}
|
|
85
|
-
{social.instagram && <a href={social.instagram} aria-label="Instagram" className="amp-sidebar__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/instagram.svg" alt="Instagram" width="24" height="24" layout="fixed" /></a>}
|
|
86
|
-
{social.youtube && <a href={social.youtube} aria-label="YouTube" className="amp-sidebar__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/youtube.svg" alt="YouTube" width="24" height="24" layout="fixed" /></a>}
|
|
87
|
-
{social.tiktok && <a href={social.tiktok} aria-label="TikTok" className="amp-sidebar__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/tiktok.svg" alt="TikTok" width="24" height="24" layout="fixed" /></a>}
|
|
88
|
-
</div>
|
|
79
|
+
{searchEnabled && (
|
|
80
|
+
<div className="amp-sidebar__search">
|
|
81
|
+
<input type="search" placeholder="Buscar..." className="amp-sidebar__search-input" />
|
|
89
82
|
</div>
|
|
90
|
-
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<nav>
|
|
86
|
+
<ul className="amp-sidebar__nav">
|
|
87
|
+
{categories.map((cat) => (
|
|
88
|
+
<li key={cat.id} className="amp-sidebar__nav-item">
|
|
89
|
+
{cat.subcategories?.length ? (
|
|
90
|
+
<details className="amp-sidebar__sub-menu">
|
|
91
|
+
<summary className="amp-sidebar__nav-link-summary">
|
|
92
|
+
{cat.label}
|
|
93
|
+
<span className="amp-sidebar__arrow">›</span>
|
|
94
|
+
</summary>
|
|
95
|
+
<ul className="amp-sidebar__sub-list">
|
|
96
|
+
{cat.subcategories.map((sub) => (
|
|
97
|
+
<li key={sub.id}>
|
|
98
|
+
<a href={sub.slug ? (sub.slug.startsWith('/') ? sub.slug : `/${sub.slug}`) : '#'} className="amp-sidebar__sub-link">{sub.label}</a>
|
|
99
|
+
</li>
|
|
100
|
+
))}
|
|
101
|
+
</ul>
|
|
102
|
+
</details>
|
|
103
|
+
) : (
|
|
104
|
+
<a href={cat.slug ? (cat.slug.startsWith('/') ? cat.slug : `/${cat.slug}`) : '#'} className="amp-sidebar__nav-link">{cat.label}</a>
|
|
105
|
+
)}
|
|
106
|
+
</li>
|
|
107
|
+
))}
|
|
108
|
+
</ul>
|
|
109
|
+
</nav>
|
|
110
|
+
|
|
111
|
+
<div className="amp-sidebar__social">
|
|
112
|
+
<span className="amp-sidebar__social-label">Seguinos en:</span>
|
|
113
|
+
<div className="amp-sidebar__social-icons">
|
|
114
|
+
{social.facebook && <a href={social.facebook} aria-label="Facebook" className="amp-sidebar__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/facebook.svg" alt="Facebook" width="24" height="24" layout="fixed" /></a>}
|
|
115
|
+
{social.twitter && <a href={social.twitter} aria-label="X" className="amp-sidebar__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/x.svg" alt="X" width="24" height="24" layout="fixed" /></a>}
|
|
116
|
+
{social.instagram && <a href={social.instagram} aria-label="Instagram" className="amp-sidebar__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/instagram.svg" alt="Instagram" width="24" height="24" layout="fixed" /></a>}
|
|
117
|
+
{social.youtube && <a href={social.youtube} aria-label="YouTube" className="amp-sidebar__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/youtube.svg" alt="YouTube" width="24" height="24" layout="fixed" /></a>}
|
|
118
|
+
{social.tiktok && <a href={social.tiktok} aria-label="TikTok" className="amp-sidebar__social-link" target="_blank" rel="noopener noreferrer"><amp-img src="/icons/tiktok.svg" alt="TikTok" width="24" height="24" layout="fixed" /></a>}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</amp-sidebar>
|
|
91
122
|
|
|
92
|
-
</div>
|
|
93
123
|
</header>
|
|
94
124
|
)
|
|
95
125
|
}
|
|
@@ -122,16 +122,32 @@ function sanitizeUrl(value, attrName) {
|
|
|
122
122
|
.toLowerCase()
|
|
123
123
|
|
|
124
124
|
if (!normalized) return null
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
|
|
126
|
+
// URL devuelta SIN espacios, caracteres de control ni invisibles Unicode
|
|
127
|
+
// (zero-width U+200B–U+200D, word-joiner U+2060, BOM U+FEFF). El editor a
|
|
128
|
+
// veces los pega al final de la URL: la validación de protocolo ya los ignora,
|
|
129
|
+
// pero si se devolvían tal cual rompían la carga del recurso —p. ej. <amp-img>
|
|
130
|
+
// tiraba "Failed to load: …png". Una URL válida no lleva whitespace, así que
|
|
131
|
+
// quitarlo es seguro y no altera la ruta real.
|
|
132
|
+
const safe = Array.from(trimmed).filter((ch) => {
|
|
133
|
+
const c = ch.codePointAt(0)
|
|
134
|
+
if (c <= 0x1f || c === 0x7f) return false // control
|
|
135
|
+
if (c === 0x200b || c === 0x200c || c === 0x200d) return false // zero-width
|
|
136
|
+
if (c === 0x2060 || c === 0xfeff) return false // word-joiner, BOM
|
|
137
|
+
return !/\s/.test(ch) // cualquier whitespace
|
|
138
|
+
}).join('')
|
|
139
|
+
if (!safe) return null
|
|
140
|
+
|
|
141
|
+
if (normalized.startsWith('#')) return safe
|
|
142
|
+
if (normalized.startsWith('//')) return safe
|
|
143
|
+
if (/^(\/|\.\.?\/|\?)/.test(normalized)) return safe
|
|
128
144
|
|
|
129
145
|
const protocolMatch = normalized.match(/^([a-z][a-z0-9+.-]*:)/)
|
|
130
|
-
if (!protocolMatch) return
|
|
146
|
+
if (!protocolMatch) return safe
|
|
131
147
|
if (!SAFE_PROTOCOLS.has(protocolMatch[1])) return null
|
|
132
148
|
if (attrName === 'src' && !/^https?:/.test(protocolMatch[1])) return null
|
|
133
149
|
|
|
134
|
-
return
|
|
150
|
+
return safe
|
|
135
151
|
}
|
|
136
152
|
|
|
137
153
|
function sanitizeNumeric(value) {
|