@crtobiasdelsud/portal-ui 1.0.3 → 1.0.5

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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased] — 2026-05-18
4
+
5
+ ### Fixed
6
+
7
+ - `EditorOutput` + `EditorOutputFull`: `@editorjs/quote` guarda `caption` como HTML via `innerHTML`; se renderizaba como texto plano mostrando tags HTML crudos en el autor de la cita. Corregido con `dangerouslySetInnerHTML`.
8
+
9
+ ---
10
+
11
+ ## 1.0.2 — 2026-05-15
12
+
13
+ Patch release. Fixes menores sobre la migracion 1.0.0; sin breaking changes
14
+ en API publica. Detalle de cambios: ver `git log v1.0.1..v1.0.2 -- src/`.
15
+
16
+ ## 1.0.1 — 2026-05-14
17
+
18
+ Patch release post-1.0.0. Sin breaking changes en API publica.
19
+ Detalle de cambios: ver `git log v1.0.0..v1.0.1 -- src/`.
20
+
3
21
  ## 1.0.0 — 2026-05-14
4
22
 
5
23
  Migración 100% completa. Paridad total entre portal y CMS — todos los
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crtobiasdelsud/portal-ui",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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",
@@ -14,6 +14,7 @@ import Carrusel from './variants/Carrusel/Carrusel.jsx'
14
14
  import LeeAdemas from './variants/LeeAdemas/LeeAdemas.jsx'
15
15
  import LoQueSeLee from './variants/LoQueSeLee/LoQueSeLee.jsx'
16
16
  import SeguiLeyendo from './variants/SeguiLeyendo/SeguiLeyendo.jsx'
17
+ import Etiquetas from './variants/Etiquetas/Etiquetas.jsx'
17
18
 
18
19
  const VARIANTS = {
19
20
  default: Default,
@@ -41,6 +42,7 @@ const VARIANTS = {
41
42
  loqueselee: LoQueSeLee,
42
43
  seguiLeyendo: SeguiLeyendo,
43
44
  seguileyendo: SeguiLeyendo,
45
+ etiquetas: Etiquetas,
44
46
  }
45
47
 
46
48
  /**
@@ -53,8 +55,9 @@ const VARIANTS = {
53
55
  * @param {string} [props.titulo]
54
56
  * @param {string} [props.tipo='default']
55
57
  * @param {string} [props.verMasUrl]
56
- * @param {Array<object>} [props.articles=[]] — todos los tipos excepto loQueSeLee
58
+ * @param {Array<object>} [props.articles=[]] — todos los tipos excepto loQueSeLee/etiquetas
57
59
  * @param {object} [props.article] — solo para loQueSeLee
60
+ * @param {Array<object>} [props.tags] — solo para etiquetas (tags del artículo actual)
58
61
  * @param {boolean} [props.isAmp=false]
59
62
  */
60
63
  export default function CabezalView({
@@ -63,6 +66,7 @@ export default function CabezalView({
63
66
  verMasUrl,
64
67
  articles = [],
65
68
  article,
69
+ tags,
66
70
  isAmp = false,
67
71
  }) {
68
72
  const isLoQueSeLee = tipo === 'loQueSeLee' || tipo === 'loqueselee'
@@ -72,6 +76,11 @@ export default function CabezalView({
72
76
  return <LoQueSeLee article={article} />
73
77
  }
74
78
 
79
+ // `etiquetas` recibe los tags del artículo en curso, no un array de artículos.
80
+ if (tipo === 'etiquetas') {
81
+ return <Etiquetas tags={tags} />
82
+ }
83
+
75
84
  if (!titulo && !articles.length) return null
76
85
 
77
86
  const Component = VARIANTS[tipo] ?? Default
@@ -47,6 +47,9 @@
47
47
  font-weight: 500;
48
48
  line-height: 150%;
49
49
  letter-spacing: -2%;
50
+ min-width: 0;
51
+ overflow-wrap: break-word;
52
+ word-break: break-word;
50
53
  }
51
54
 
52
55
  .volanta {
@@ -0,0 +1,52 @@
1
+ import { useAdapters } from '../../../../adapters/AdaptersContext.jsx'
2
+ import styles from './Etiquetas.module.scss'
3
+
4
+ // View pura: muestra las etiquetas/tags del artículo en curso.
5
+ // A diferencia de LoQueSeLee, no hay fetch: la data layer le pasa los `tags`
6
+ // que ya vienen resueltos en el artículo (`/api/portal/articles/:id` los incluye).
7
+ // Cada tag enlaza a la pantalla de etiqueta ya existente: /etiqueta/{slug}.
8
+ export default function Etiquetas({ tags = [] }) {
9
+ const { Link } = useAdapters()
10
+
11
+ // Acepta los dos shapes del backend:
12
+ // - article.tags → { id, name, slug }
13
+ // - article.etiquetas → { slug, nombre }
14
+ const items = (Array.isArray(tags) ? tags : [])
15
+ .map((t) => ({ slug: t?.slug, name: t?.name ?? t?.nombre }))
16
+ .filter((t) => t.slug && t.name)
17
+ if (items.length === 0) return null
18
+
19
+ return (
20
+ <section className={styles.container} aria-label="Temas del artículo">
21
+ <span className={styles.label}>
22
+ <svg
23
+ className={styles.icon}
24
+ xmlns="http://www.w3.org/2000/svg"
25
+ width="19"
26
+ height="19"
27
+ viewBox="0 0 19 19"
28
+ aria-hidden="true"
29
+ >
30
+ <path
31
+ fill="currentColor"
32
+ d="M17.1 0H1.9A1.906 1.906 0 0 0 0 1.9V19l3.8-3.8h13.3a1.906 1.906 0 0 0 1.9-1.9V1.9A1.906 1.906 0 0 0 17.1 0Zm0 13.3H3.8l-1.9 1.9V1.9h15.2Z"
33
+ />
34
+ <path
35
+ fill="currentColor"
36
+ d="m11.87 6.826-.194 1.64H13.1v1.469h-1.606l-.216 1.7H9.729l.216-1.7h-1.4l-.216 1.7H6.78l.216-1.7H5.561V8.466h1.617l.194-1.64H5.937V5.36h1.617l.216-1.7h1.549l-.216 1.7h1.4l.216-1.7h1.55l-.217 1.7h1.437v1.469Zm-1.549 0h-1.4l-.194 1.64h1.4Z"
37
+ />
38
+ </svg>
39
+ Temas
40
+ </span>
41
+ <ul className={styles.list}>
42
+ {items.map((tag) => (
43
+ <li key={tag.id ?? tag.slug} className={styles.item}>
44
+ <Link href={`/etiqueta/${tag.slug}`} className={styles.tag}>
45
+ {tag.name}
46
+ </Link>
47
+ </li>
48
+ ))}
49
+ </ul>
50
+ </section>
51
+ )
52
+ }
@@ -0,0 +1,70 @@
1
+ @use "../../../../styles/index" as *;
2
+
3
+ .container {
4
+ display: flex;
5
+ align-items: flex-start;
6
+ flex-wrap: wrap;
7
+ gap: 12px 16px;
8
+ width: 100%;
9
+ box-sizing: border-box;
10
+ padding: 16px 0;
11
+ }
12
+
13
+ // ── Badge "# TEMAS" ─────────────────────────────────────────────────────────
14
+
15
+ .label {
16
+ display: inline-flex;
17
+ align-items: center;
18
+ gap: 6px;
19
+ flex-shrink: 0;
20
+ padding: 6px 10px;
21
+ border: 1px solid var(--primary-color, #af0437);
22
+ color: var(--primary-color, #af0437);
23
+ font-family: var(--font-inter, Inter, sans-serif);
24
+ font-size: 16px;
25
+ font-weight: 700;
26
+ line-height: 1;
27
+ text-transform: uppercase;
28
+ letter-spacing: 0.04em;
29
+ }
30
+
31
+ .icon {
32
+ width: 15px;
33
+ height: 15px;
34
+ flex-shrink: 0;
35
+ color: var(--primary-color, #af0437);
36
+ }
37
+
38
+ // ── Lista de tags ───────────────────────────────────────────────────────────
39
+
40
+ .list {
41
+ display: flex;
42
+ flex: 1;
43
+ min-width: 0;
44
+ flex-wrap: wrap;
45
+ gap: 8px 20px;
46
+ margin: 0;
47
+ padding: 0;
48
+ list-style: none;
49
+ }
50
+
51
+ .item {
52
+ display: inline-flex;
53
+ }
54
+
55
+ .tag {
56
+ font-family: var(--font-inter, Inter, sans-serif);
57
+ font-size: 16px;
58
+ font-weight: 600;
59
+ line-height: 20px;
60
+ text-transform: uppercase;
61
+ letter-spacing: 0.02em;
62
+ color: var(--text-color, #252523);
63
+ text-decoration: none;
64
+ transition: color 0.15s ease;
65
+
66
+ &:hover {
67
+ color: var(--primary-color, #af0437);
68
+ text-decoration: underline;
69
+ }
70
+ }
@@ -60,8 +60,15 @@
60
60
  flex-direction: column;
61
61
  gap: 16px;
62
62
 
63
- @include respond(tablet) {
63
+ // Intermedio (~480–768px): 2 columnas
64
+ @include respond(mobile) {
64
65
  display: grid;
66
+ grid-template-columns: repeat(2, 1fr);
67
+ gap: 20px;
68
+ }
69
+
70
+ // Ancho (≥768px): 3 columnas
71
+ @include respond(tablet) {
65
72
  grid-template-columns: repeat(3, 1fr);
66
73
  gap: 30px;
67
74
  }
@@ -109,7 +109,7 @@ function Block({ block, cls, isAmp }) {
109
109
  return (
110
110
  <blockquote className={cls.quote} suppressHydrationWarning>
111
111
  <p dangerouslySetInnerHTML={{ __html: block.data.text }} suppressHydrationWarning />
112
- {block.data.caption && <cite>{block.data.caption}</cite>}
112
+ {block.data.caption && <cite dangerouslySetInnerHTML={{ __html: block.data.caption }} suppressHydrationWarning />}
113
113
  </blockquote>
114
114
  )
115
115
 
@@ -109,7 +109,7 @@ function Block({ block, cls, isAmp }) {
109
109
  return (
110
110
  <blockquote className={cls.quote}>
111
111
  <p dangerouslySetInnerHTML={{ __html: block.data.text }} />
112
- {block.data.caption && <cite>{block.data.caption}</cite>}
112
+ {block.data.caption && <cite dangerouslySetInnerHTML={{ __html: block.data.caption }} />}
113
113
  </blockquote>
114
114
  )
115
115
 
@@ -125,7 +125,7 @@
125
125
  altísima (~774px). La topamos para que sea un banner ancho y entre
126
126
  en pantalla. En mobile la imagen es chica y este tope nunca se activa. */
127
127
  .media {
128
- max-height: 400px;
128
+ max-height: 500px;
129
129
  }
130
130
 
131
131
  .headline {
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'
4
4
  import { useTheme } from '../../context/SiteConfigContext.jsx'
5
5
  import V1 from './variants/V1/V1'
6
6
  import V2 from './variants/V2/V2'
7
+ import { NETWORK_ICONS } from './icons/index.js'
7
8
 
8
9
  const VARIANTS = { '1': V1, '2': V2 }
9
10
 
@@ -12,42 +13,42 @@ function buildNetworks(url) {
12
13
  return [
13
14
  {
14
15
  key: 'x',
15
- src: '/icons/x.svg',
16
+ Glyph: NETWORK_ICONS.x,
16
17
  label: 'X',
17
18
  tooltip: 'Compartir en X',
18
19
  href: `https://twitter.com/intent/tweet?url=${encoded}`,
19
20
  },
20
21
  {
21
22
  key: 'facebook',
22
- src: '/icons/facebook.svg',
23
+ Glyph: NETWORK_ICONS.facebook,
23
24
  label: 'Facebook',
24
25
  tooltip: 'Compartir en Facebook',
25
26
  href: `https://www.facebook.com/sharer/sharer.php?u=${encoded}`,
26
27
  },
27
28
  {
28
29
  key: 'linkedin',
29
- src: '/icons/linkedin.svg',
30
+ Glyph: NETWORK_ICONS.linkedin,
30
31
  label: 'LinkedIn',
31
32
  tooltip: 'Compartir en LinkedIn',
32
33
  href: `https://www.linkedin.com/shareArticle?mini=true&url=${encoded}`,
33
34
  },
34
35
  {
35
36
  key: 'telegram',
36
- src: '/icons/telegram.svg',
37
+ Glyph: NETWORK_ICONS.telegram,
37
38
  label: 'Telegram',
38
39
  tooltip: 'Compartir en Telegram',
39
40
  href: `https://t.me/share/url?url=${encoded}`,
40
41
  },
41
42
  {
42
43
  key: 'whatsapp',
43
- src: '/icons/whatsapp.svg',
44
+ Glyph: NETWORK_ICONS.whatsapp,
44
45
  label: 'WhatsApp',
45
46
  tooltip: 'Compartir en WhatsApp',
46
47
  href: `https://wa.me/?text=${encoded}`,
47
48
  },
48
49
  {
49
50
  key: 'email',
50
- src: '/icons/email.svg',
51
+ Glyph: NETWORK_ICONS.email,
51
52
  label: 'Email',
52
53
  tooltip: 'Compartir por Email',
53
54
  href: `mailto:?body=${encoded}`,
@@ -55,7 +56,7 @@ function buildNetworks(url) {
55
56
  ]
56
57
  }
57
58
 
58
- export default function ShareBlock({ isAmp = false }) {
59
+ export default function ShareBlock({ isAmp = false, settings = {} }) {
59
60
  const theme = useTheme()
60
61
  const [networks, setNetworks] = useState(() => buildNetworks(''))
61
62
 
@@ -66,10 +67,22 @@ export default function ShareBlock({ isAmp = false }) {
66
67
  const v = String(theme.shareBlock ?? 1)
67
68
  const Variant = VARIANTS[v] ?? V1
68
69
 
70
+ // `borderLeft`: barra lateral con el color primario. Se usa cuando el
71
+ // ShareBlock va dentro del cuerpo del artículo (lo setea el widget del CMS).
72
+ const borderLeft = settings?.borderLeft ?? false
73
+
69
74
  const inlineStyle = isAmp ? {} : {
70
75
  '--primary-color': theme.primary,
71
76
  '--surface-color': theme.surface,
72
77
  }
73
78
 
74
- return <Variant isAmp={isAmp} inlineStyle={inlineStyle} networks={networks} v={v} />
79
+ return (
80
+ <Variant
81
+ isAmp={isAmp}
82
+ inlineStyle={inlineStyle}
83
+ networks={networks}
84
+ v={v}
85
+ borderLeft={borderLeft}
86
+ />
87
+ )
75
88
  }
@@ -18,6 +18,17 @@
18
18
  }
19
19
  }
20
20
 
21
+ // Barra lateral con el color primario. Se activa con la prop `borderLeft`
22
+ // (la setea el widget del CMS cuando el ShareBlock va en el cuerpo del artículo).
23
+ .borderLeft {
24
+ border-left: 4px solid var(--primary-color, #B1043F);
25
+ padding-left: 12px;
26
+
27
+ @include respond(desktop) {
28
+ padding-left: 16px;
29
+ }
30
+ }
31
+
21
32
  .label {
22
33
  font-size: 14px;
23
34
  line-height: 14px;
@@ -0,0 +1,21 @@
1
+ // Ícono Email — vector monocromo. Recolorea vía `currentColor`.
2
+ export default function Email(props) {
3
+ return (
4
+ <svg viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
5
+ <rect
6
+ x="0.448591"
7
+ y="0.448591"
8
+ width="31.2056"
9
+ height="31.2056"
10
+ rx="15.6028"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth="0.897182"
14
+ />
15
+ <path
16
+ fill="currentColor"
17
+ d="M25.0227 10.6694C25.0227 9.68246 24.2153 8.875 23.2284 8.875H8.87347C7.88657 8.875 7.0791 9.68246 7.0791 10.6694V21.4356C7.0791 22.4225 7.88657 23.2299 8.87347 23.2299H23.2284C24.2153 23.2299 25.0227 22.4225 25.0227 21.4356V10.6694ZM23.2284 10.6694L16.0509 15.1553L8.87347 10.6694H23.2284ZM23.2284 21.4356H8.87347V12.4637L16.0509 16.9496L23.2284 12.4637V21.4356Z"
18
+ />
19
+ </svg>
20
+ )
21
+ }
@@ -0,0 +1,15 @@
1
+ // Ícono Facebook — vector monocromo. Recolorea vía `fill="currentColor"`.
2
+ export default function Facebook(props) {
3
+ return (
4
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
5
+ <path
6
+ fill="currentColor"
7
+ d="M11.7645 23.3479H11.7132C5.29228 23.3479 0.0683594 18.1224 0.0683594 11.6996V11.6482C0.0683594 5.22548 5.29228 0 11.7132 0H11.7645C18.1854 0 23.4093 5.22548 23.4093 11.6482V11.6996C23.4093 18.1224 18.1854 23.3479 11.7645 23.3479ZM11.7132 0.790298C5.72761 0.790298 0.858423 5.66093 0.858423 11.6482V11.6996C0.858423 17.6869 5.72761 22.5575 11.7132 22.5575H11.7645C17.7501 22.5575 22.6193 17.6869 22.6193 11.6996V11.6482C22.6193 5.66093 17.7501 0.790298 11.7645 0.790298H11.7132Z"
8
+ />
9
+ <path
10
+ fill="currentColor"
11
+ d="M13.3092 9.0559V11.5129H16.3478L15.8667 14.8227H13.3092V22.4483C12.7964 22.5195 12.2718 22.5566 11.7393 22.5566C11.1246 22.5566 10.5211 22.5076 9.93321 22.4128V14.8227H7.13086V11.5129H9.93321V8.50666C9.93321 6.64154 11.4446 5.12891 13.31 5.12891V5.13049C13.3155 5.13049 13.3203 5.12891 13.3258 5.12891H16.3486V7.99139H14.3734C13.7864 7.99139 13.31 8.4679 13.31 9.0551L13.3092 9.0559Z"
12
+ />
13
+ </svg>
14
+ )
15
+ }
@@ -0,0 +1,12 @@
1
+ // Ícono LinkedIn — vector monocromo. Recolorea vía `currentColor`.
2
+ export default function Linkedin(props) {
3
+ return (
4
+ <svg viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
5
+ <rect x="0.5" y="0.5" width="22" height="22" rx="11" fill="none" stroke="currentColor" />
6
+ <path
7
+ fill="currentColor"
8
+ d="M8.33525 6.17784C8.33508 6.54495 8.19718 6.89695 7.9519 7.15641C7.70662 7.41586 7.37404 7.56152 7.02733 7.56133C6.68061 7.56115 6.34817 7.41514 6.10313 7.15543C5.85809 6.89571 5.72053 6.54357 5.7207 6.17646C5.72088 5.80935 5.85877 5.45735 6.10406 5.1979C6.34934 4.93844 6.68192 4.79279 7.02863 4.79297C7.37534 4.79315 7.70779 4.93916 7.95282 5.19888C8.19786 5.45859 8.33543 5.81074 8.33525 6.17784ZM8.37447 8.58632H5.75992V17.2513H8.37447V8.58632ZM12.5055 8.58632H9.90398V17.2513H12.4793V12.7043C12.4793 10.1712 15.5972 9.9359 15.5972 12.7043V17.2513H18.179V11.763C18.179 7.49282 13.5644 7.652 12.4793 9.74903L12.5055 8.58632Z"
9
+ />
10
+ </svg>
11
+ )
12
+ }
@@ -0,0 +1,21 @@
1
+ // Ícono Telegram — vector monocromo. Recolorea vía `currentColor`.
2
+ export default function Telegram(props) {
3
+ return (
4
+ <svg viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
5
+ <rect
6
+ x="0.448591"
7
+ y="0.448591"
8
+ width="31.2056"
9
+ height="31.2056"
10
+ rx="15.6028"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth="0.897182"
14
+ />
15
+ <path
16
+ fill="currentColor"
17
+ d="M24.0141 8.23267L7.16884 14.7622C6.49096 15.0662 6.26168 15.6751 7.005 16.0056L11.3265 17.3861L21.7754 10.8951C22.3459 10.4876 22.93 10.5962 22.4274 11.0445L13.4532 19.212L13.1713 22.6685C13.4324 23.2022 13.9105 23.2047 14.2155 22.9394L16.6983 20.578L20.9506 23.7786C21.9382 24.3663 22.4756 23.9871 22.6881 22.9098L25.4772 9.63482C25.7668 8.30888 25.273 7.72465 24.0141 8.23267Z"
18
+ />
19
+ </svg>
20
+ )
21
+ }
@@ -0,0 +1,12 @@
1
+ // Ícono WhatsApp — vector monocromo. Recolorea vía `currentColor`.
2
+ export default function Whatsapp(props) {
3
+ return (
4
+ <svg viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
5
+ <rect x="0.5" y="0.5" width="32" height="32" rx="16" fill="none" stroke="currentColor" />
6
+ <path
7
+ fill="currentColor"
8
+ d="M23.3769 9.61904C22.5475 8.78574 21.5597 8.12502 20.4711 7.67537C19.3825 7.22573 18.2148 6.99616 17.0362 7.00005C12.0975 7.00005 8.07236 11.005 8.07236 15.919C8.07236 17.494 8.48844 19.024 9.26633 20.374L8 25L12.7487 23.758C14.0603 24.469 15.5347 24.847 17.0362 24.847C21.9749 24.847 26 20.842 26 15.928C26 13.543 25.0683 11.302 23.3769 9.61904ZM17.0362 23.335C15.6975 23.335 14.3859 22.975 13.2372 22.3L12.9658 22.138L10.1437 22.876L10.8945 20.14L10.7136 19.861C9.96964 18.6794 9.57471 17.3134 9.57387 15.919C9.57387 11.833 12.9206 8.50304 17.0271 8.50304C19.0171 8.50304 20.8894 9.27704 22.2915 10.681C22.9858 11.3685 23.536 12.1863 23.9102 13.087C24.2844 13.9877 24.4752 14.9534 24.4714 15.928C24.4894 20.014 21.1427 23.335 17.0362 23.335ZM21.1246 17.791C20.8985 17.683 19.795 17.143 19.596 17.062C19.3879 16.99 19.2432 16.954 19.0894 17.17C18.9357 17.395 18.5106 17.899 18.3839 18.043C18.2573 18.196 18.1216 18.214 17.8955 18.097C17.6693 17.989 16.9457 17.746 16.0955 16.99C15.4261 16.396 14.9829 15.667 14.8472 15.442C14.7206 15.217 14.8291 15.1 14.9467 14.983C15.0462 14.884 15.1729 14.722 15.2814 14.596C15.3899 14.47 15.4352 14.371 15.5075 14.227C15.5799 14.074 15.5437 13.948 15.4894 13.84C15.4352 13.732 14.9829 12.634 14.802 12.184C14.6211 11.752 14.4312 11.806 14.2955 11.797H13.8613C13.7075 11.797 13.4724 11.851 13.2643 12.076C13.0653 12.301 12.4864 12.841 12.4864 13.939C12.4864 15.037 13.2915 16.099 13.4 16.243C13.5085 16.396 14.9829 18.646 17.2261 19.609C17.7598 19.843 18.1759 19.978 18.5015 20.077C19.0352 20.248 19.5236 20.221 19.9126 20.167C20.3467 20.104 21.2422 19.627 21.4231 19.105C21.6131 18.583 21.6131 18.142 21.5497 18.043C21.4864 17.944 21.3508 17.899 21.1246 17.791Z"
9
+ />
10
+ </svg>
11
+ )
12
+ }
@@ -0,0 +1,15 @@
1
+ // Ícono X (Twitter) — vector monocromo. Recolorea vía `fill="currentColor"`.
2
+ export default function X(props) {
3
+ return (
4
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
5
+ <path
6
+ fill="currentColor"
7
+ d="M11.6961 23.3479H11.6448C5.22393 23.3479 0 18.1224 0 11.6996V11.6482C0 5.22545 5.22393 0 11.6448 0H11.6961C18.117 0 23.3409 5.22545 23.3409 11.6482V11.6996C23.3409 18.1224 18.117 23.3479 11.6961 23.3479ZM11.6448 0.790298C5.65925 0.790298 0.790065 5.66091 0.790065 11.6482V11.6996C0.790065 17.6869 5.65925 22.5575 11.6448 22.5575H11.6961C17.6817 22.5575 22.5509 17.6869 22.5509 11.6996V11.6482C22.5509 5.66091 17.6817 0.790298 11.6961 0.790298H11.6448Z"
8
+ />
9
+ <path
10
+ fill="currentColor"
11
+ d="M4.96892 5.50391L10.1691 12.4586L4.93652 18.1131H6.11452L10.6961 13.1627L14.3976 18.1131H18.4056L12.913 10.7673L17.7838 5.50391H16.6058L12.3869 10.0632L8.9777 5.50391H4.96969H4.96892ZM6.70073 6.37167H8.54163L16.6722 17.2454H14.8313L6.70073 6.37167Z"
12
+ />
13
+ </svg>
14
+ )
15
+ }
@@ -0,0 +1,18 @@
1
+ // Íconos de redes del ShareBlock — componentes SVG inline, monocromos.
2
+ // Cada uno usa `fill="currentColor"`, así el color (y el hover) lo controla
3
+ // el CSS del componente `Icon` (ver Icon.module.scss).
4
+ import X from './X.jsx'
5
+ import Facebook from './Facebook.jsx'
6
+ import Linkedin from './Linkedin.jsx'
7
+ import Telegram from './Telegram.jsx'
8
+ import Whatsapp from './Whatsapp.jsx'
9
+ import Email from './Email.jsx'
10
+
11
+ export const NETWORK_ICONS = {
12
+ x: X,
13
+ facebook: Facebook,
14
+ linkedin: Linkedin,
15
+ telegram: Telegram,
16
+ whatsapp: Whatsapp,
17
+ email: Email,
18
+ }
@@ -2,8 +2,10 @@ import Icon from '../../../UI/Icon/Icon.jsx'
2
2
  import shared from '../../ShareBlock.module.scss'
3
3
  import s from './V1.module.scss'
4
4
 
5
- export default function V1({ isAmp, inlineStyle, networks, v }) {
6
- const containerCls = isAmp ? `share-block share-block--${v}` : `${shared.container} ${s.root}`
5
+ export default function V1({ isAmp, inlineStyle, networks, v, borderLeft = false }) {
6
+ const containerCls = isAmp
7
+ ? `share-block share-block--${v}${borderLeft ? ' share-block--bordered' : ''}`
8
+ : `${shared.container} ${s.root}${borderLeft ? ` ${shared.borderLeft}` : ''}`
7
9
 
8
10
  return (
9
11
  <div className={containerCls} style={inlineStyle}>
@@ -11,16 +13,24 @@ export default function V1({ isAmp, inlineStyle, networks, v }) {
11
13
  Compartí esta nota:
12
14
  </span>
13
15
  <div className={isAmp ? 'share-block__icons' : shared.icons}>
14
- {networks.map(({ key, src, label, href, tooltip }) => (
15
- isAmp
16
+ {networks.map(({ key, Glyph, label, href, tooltip }) => {
17
+ // Links externos (http) → pestaña nueva. `mailto:` queda en la misma.
18
+ const external = (href ?? '').startsWith('http')
19
+ return isAmp
16
20
  ? (
17
- <a key={key} href={href ?? '#'} className="share-block__icon-link" aria-label={label}>
18
- <img src={src} alt={label} className="share-block__icon" />
21
+ <a
22
+ key={key}
23
+ href={href ?? '#'}
24
+ className="share-block__icon-link"
25
+ aria-label={label}
26
+ {...(external && { target: '_blank', rel: 'noopener noreferrer' })}
27
+ >
28
+ <Glyph className="share-block__icon" />
19
29
  </a>
20
30
  ) : (
21
- <Icon key={key} src={src} label={label} href={href ?? '#'} tooltipText={tooltip} />
31
+ <Icon key={key} glyph={<Glyph />} label={label} href={href ?? '#'} tooltipText={tooltip} newTab={external} />
22
32
  )
23
- ))}
33
+ })}
24
34
  </div>
25
35
  </div>
26
36
  )
@@ -2,8 +2,10 @@ import Icon from '../../../UI/Icon/Icon.jsx'
2
2
  import shared from '../../ShareBlock.module.scss'
3
3
  import s from './V2.module.scss'
4
4
 
5
- export default function V2({ isAmp, inlineStyle, networks, v }) {
6
- const containerCls = isAmp ? `share-block share-block--${v}` : `${shared.container} ${s.root}`
5
+ export default function V2({ isAmp, inlineStyle, networks, v, borderLeft = false }) {
6
+ const containerCls = isAmp
7
+ ? `share-block share-block--${v}${borderLeft ? ' share-block--bordered' : ''}`
8
+ : `${shared.container} ${s.root}${borderLeft ? ` ${shared.borderLeft}` : ''}`
7
9
 
8
10
  return (
9
11
  <div className={containerCls} style={inlineStyle}>
@@ -11,16 +13,24 @@ export default function V2({ isAmp, inlineStyle, networks, v }) {
11
13
  Compartí esta nota:
12
14
  </span>
13
15
  <div className={isAmp ? 'share-block__icons' : shared.icons}>
14
- {networks.map(({ key, src, label, href, tooltip }) => (
15
- isAmp
16
+ {networks.map(({ key, Glyph, label, href, tooltip }) => {
17
+ // Links externos (http) → pestaña nueva. `mailto:` queda en la misma.
18
+ const external = (href ?? '').startsWith('http')
19
+ return isAmp
16
20
  ? (
17
- <a key={key} href={href ?? '#'} className="share-block__icon-link" aria-label={label}>
18
- <img src={src} alt={label} className="share-block__icon" />
21
+ <a
22
+ key={key}
23
+ href={href ?? '#'}
24
+ className="share-block__icon-link"
25
+ aria-label={label}
26
+ {...(external && { target: '_blank', rel: 'noopener noreferrer' })}
27
+ >
28
+ <Glyph className="share-block__icon" />
19
29
  </a>
20
30
  ) : (
21
- <Icon key={key} src={src} label={label} href={href ?? '#'} tooltipText={tooltip} />
31
+ <Icon key={key} glyph={<Glyph />} label={label} href={href ?? '#'} tooltipText={tooltip} newTab={external} />
22
32
  )
23
- ))}
33
+ })}
24
34
  </div>
25
35
  </div>
26
36
  )
@@ -10,13 +10,17 @@ function buildTooltip(label, href) {
10
10
  }
11
11
  }
12
12
 
13
- export default function Icon({ src, label, href, tooltipText }) {
14
- const maskStyle = {
15
- maskImage: `url(${src})`,
16
- WebkitMaskImage: `url(${src})`,
17
- }
18
-
19
- const icon = <span style={maskStyle} aria-hidden="true" className={style.icon} />
13
+ export default function Icon({ src, glyph, label, href, tooltipText, newTab = false }) {
14
+ // Dos modos de render del ícono:
15
+ // - `glyph`: SVG inline (React node). Recolorea vía `fill="currentColor"`.
16
+ // - `src`: ruta a un .svg, usado como `mask-image` (modo legacy).
17
+ const icon = glyph
18
+ ? <span aria-hidden="true" className={style.glyph}>{glyph}</span>
19
+ : <span
20
+ aria-hidden="true"
21
+ className={style.icon}
22
+ style={{ maskImage: `url(${src})`, WebkitMaskImage: `url(${src})` }}
23
+ />
20
24
 
21
25
  if (href) {
22
26
  return (
@@ -25,6 +29,7 @@ export default function Icon({ src, label, href, tooltipText }) {
25
29
  className={style.container}
26
30
  href={href}
27
31
  aria-label={label}
32
+ {...(newTab && { target: '_blank', rel: 'noopener noreferrer' })}
28
33
  >
29
34
  {icon}
30
35
 
@@ -27,6 +27,26 @@
27
27
  background-color: var(--primary-color, #4bac48);
28
28
  }
29
29
 
30
+ /* Modo glifo: SVG inline. El color (y su hover) se aplica vía `currentColor`
31
+ — el <svg> del ícono usa fill="currentColor". */
32
+ .glyph{
33
+ display: block;
34
+ height: 32px;
35
+ width: 32px;
36
+ color: currentColor;
37
+ transition: color 0.3s;
38
+ }
39
+
40
+ .glyph svg{
41
+ display: block;
42
+ height: 100%;
43
+ width: 100%;
44
+ }
45
+
46
+ .container:hover .glyph{
47
+ color: var(--primary-color, #4bac48);
48
+ }
49
+
30
50
  .container[data-tooltip] {
31
51
  position: relative;
32
52
  }
@@ -1,6 +1,15 @@
1
1
  @use "../variables/breakpoint" as *;
2
2
 
3
3
  @mixin respond($breakpoint) {
4
+ @if $breakpoint == mobile {
5
+ @supports (container-type: inline-size) {
6
+ @container (min-width: #{$mobile}) { @content; }
7
+ }
8
+ @supports not (container-type: inline-size) {
9
+ @media (min-width: $mobile) { @content; }
10
+ }
11
+ }
12
+
4
13
  @if $breakpoint == tablet {
5
14
  @supports (container-type: inline-size) {
6
15
  @container (min-width: #{$tablet}) { @content; }