@crtobiasdelsud/portal-ui 1.1.13 → 1.1.15

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.1.13",
3
+ "version": "1.1.15",
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 = (
@@ -15,7 +15,9 @@ export default function BlockColumns({ widgets, registry, settings = {} }) {
15
15
  const fantasmaArticleId = importantHero?.settings?.fantasmaArticleId
16
16
  const layout = settings.layout ?? 'default'
17
17
 
18
- const gridClass = layout === 'categoriaDos'
18
+ const gridClass = layout === 'heroIzquierda'
19
+ ? style.gridHeroLeft
20
+ : layout === 'categoriaDos'
19
21
  ? style.gridCategoriaDos
20
22
  : hasImportant ? style.gridFeatured : style.gridNormal
21
23
 
@@ -56,6 +56,20 @@
56
56
  }
57
57
  }
58
58
 
59
+ /* Espejo de gridNormal: hero ancho (2 cols) a la IZQUIERDA + los 2 widgets
60
+ acompañantes (1 col c/u) a la derecha. Apilado en mobile como el resto. */
61
+ .gridHeroLeft {
62
+ grid-template-areas:
63
+ "hero"
64
+ "recommended"
65
+ "feed";
66
+
67
+ @include respond(desktop) {
68
+ grid-template-columns: repeat(4, 1fr);
69
+ grid-template-areas: "hero hero recommended feed";
70
+ }
71
+ }
72
+
59
73
  .item {
60
74
  min-width: 0;
61
75
  overflow: hidden;
@@ -0,0 +1,12 @@
1
+ import BlockColumns from "../BlockColumns/BlockColumns"
2
+
3
+ /**
4
+ * Espejo de BlockColumns con el Hero ancho (2 cols) a la IZQUIERDA y los 2
5
+ * widgets acompañantes (1 col c/u) a la derecha. No tiene grilla propia: solo
6
+ * fuerza `layout: 'heroIzquierda'`, que BlockColumns traduce a la clase
7
+ * `.gridHeroLeft` ("hero hero recommended feed"). Mismo patrón que el par
8
+ * BlockHeroTrio / BlockHeroTrioLeft.
9
+ */
10
+ export default function BlockColumnsHeroLeft({ settings = {}, ...props }) {
11
+ return <BlockColumns {...props} settings={{ ...settings, layout: 'heroIzquierda' }} />
12
+ }
@@ -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" style={inlineStyle}>
142
- {logoEl}{sloganEl}{socialEl}{navEl}
143
- <hr className="footer-simple__divider" />
144
- {legalEl}{staticLinksEl}{bottomEl}
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="30" />
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="30" />
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
- <amp-sidebar id="amp-nav-sidebar" layout="nodisplay" side="left">
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
- {searchEnabled && (
49
- <div className="amp-sidebar__search">
50
- <input type="search" placeholder="Buscar..." className="amp-sidebar__search-input" />
51
- </div>
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
- <nav>
55
- <ul className="amp-sidebar__nav">
56
- {categories.map((cat) => (
57
- <li key={cat.id} className="amp-sidebar__nav-item">
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
- <div className="amp-sidebar__social">
81
- <span className="amp-sidebar__social-label">Seguinos en:</span>
82
- <div className="amp-sidebar__social-icons">
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
- </amp-sidebar>
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
  }
package/src/index.js CHANGED
@@ -74,6 +74,7 @@ export { default as FooterSimple } from './components/Footers/FooterSimple/Foote
74
74
 
75
75
  // === Blocks (containers que iteran widgets via registry) ===
76
76
  export { default as BlockColumns } from './components/Blocks/BlockColumns/BlockColumns.jsx'
77
+ export { default as BlockColumnsHeroLeft } from './components/Blocks/BlockColumnsHeroLeft/BlockColumnsHeroLeft.jsx'
77
78
  export { default as BlockColumnsBajada } from './components/Blocks/BlockColumnsBajada/BlockColumnsBajada.jsx'
78
79
  export { default as BlockMain } from './components/Blocks/BlockMain/BlockMain.jsx'
79
80
  export { default as BlockMainNarrow } from './components/Blocks/BlockMainNarrow/BlockMainNarrow.jsx'
@@ -122,16 +122,32 @@ function sanitizeUrl(value, attrName) {
122
122
  .toLowerCase()
123
123
 
124
124
  if (!normalized) return null
125
- if (normalized.startsWith('#')) return trimmed
126
- if (normalized.startsWith('//')) return trimmed
127
- if (/^(\/|\.\.?\/|\?)/.test(normalized)) return trimmed
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 trimmed
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 trimmed
150
+ return safe
135
151
  }
136
152
 
137
153
  function sanitizeNumeric(value) {