libreria-astro-lefebvre 0.1.83 → 0.1.85

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": "libreria-astro-lefebvre",
3
- "version": "0.1.83",
3
+ "version": "0.1.85",
4
4
  "description": "Librería de componentes Astro, React y Vue para Lefebvre",
5
5
  "author": "Equipo web desarrollo Lefebvre",
6
6
  "type": "module",
@@ -42,4 +42,3 @@
42
42
  "typescript": "^5.8.3"
43
43
  }
44
44
  }
45
-
@@ -36,7 +36,7 @@ export const metadata: ComponentMetadata = {
36
36
  {
37
37
  name: 'imageAlt',
38
38
  type: 'text',
39
- help: 'Texto alternativo de la imagen de fondo para accesibilidad. Si la imagen es puramente decorativa, puede dejarse vacío',
39
+ help: 'Texto alternativo de la imagen de fondo. Al ser una imagen decorativa detrás del texto, lo recomendable es dejarlo VACÍO (el lector de pantalla la ignorará). Rellenar solo si la imagen aporta información que no está en el título ni en la descripción',
40
40
  label: 'Alt de la imagen',
41
41
  mandatory: false,
42
42
  example_value: 'Dos profesionales revisando documentación en una tablet'
@@ -44,7 +44,7 @@ export const metadata: ComponentMetadata = {
44
44
  {
45
45
  name: 'ctaText',
46
46
  type: 'text',
47
- help: 'Texto del botón CTA. El botón SOLO se muestra en móvil (en escritorio el diseño no lo incluye). Si se deja vacío, no aparece ningún botón',
47
+ help: 'Texto del botón CTA. El botón SOLO se muestra en móvil (en escritorio el diseño no lo incluye). Si se deja vacío el texto o la URL, no aparece ningún botón',
48
48
  label: 'Texto del botón (solo móvil)',
49
49
  mandatory: false,
50
50
  example_value: 'Pruébalo ya'
@@ -52,7 +52,7 @@ export const metadata: ComponentMetadata = {
52
52
  {
53
53
  name: 'ctaUrl',
54
54
  type: 'text',
55
- help: 'URL de destino del botón CTA móvil. Solo tiene efecto si se ha informado el texto del botón',
55
+ help: 'URL de destino del botón CTA móvil. Obligatoria para que el botón se muestre: sin URL no se renderiza el botón aunque tenga texto',
56
56
  label: 'URL del botón',
57
57
  mandatory: false,
58
58
  example_value: 'https://www.lefebvre.es/tienda/apuntes-y-consejos'
@@ -16,13 +16,18 @@ const {
16
16
  fontSize = 'base',
17
17
  alignItems = 'center',
18
18
  h2Size = 'default',
19
+ h2SizeBreakpoint = 'lg',
19
20
  h2MarginBottom = 'mb-8',
20
21
  h2AlignClass = 'text-center lg:text-left',
21
22
  h2TrackingClass = '',
22
- imageAspectClass = '',
23
+ imageAspectClass = 'h-auto',
23
24
  imageSizeClass = 'md:max-w-xl w-full md:w-[75%]',
24
25
  columnGapClass = 'gap-6',
25
26
  contentPaddingClass = 'p-4 pr-4',
27
+ rowBreakpoint = 'md',
28
+ contentWidthBreakpoint = 'lg',
29
+ outerPaddingClass = '',
30
+ rowMaxWidthClass = '',
26
31
  loading = 'lazy' as 'lazy' | 'eager',
27
32
 
28
33
  showBtn = false,
@@ -41,6 +46,23 @@ const {
41
46
  const imageModalUrl = extractImageUrl(imageModal);
42
47
  const randomId = Math.floor(Math.random() * 1000);
43
48
 
49
+ // Tailwind necesita ver cada clase completa de forma literal en el código para
50
+ // generarla — no puede resolver un prefijo de breakpoint interpolado en runtime
51
+ // (`${breakpoint}:utilidad`). Por eso aquí se enumeran las dos combinaciones
52
+ // posibles (md/lg) en vez de construir el prefijo dinámicamente.
53
+ const rowFlexClass = rowBreakpoint === 'lg' ? 'lg:flex' : 'md:flex';
54
+ const rowWidthClass = rowBreakpoint === 'lg' ? 'lg:w-4/5' : 'md:w-4/5';
55
+ const rowFlex1Class = rowBreakpoint === 'lg' ? 'lg:flex-1' : 'md:flex-1';
56
+ const rowMinW0Class = rowBreakpoint === 'lg' ? 'lg:min-w-0' : 'md:min-w-0';
57
+ const contentFlex1Class = contentWidthBreakpoint === 'lg' ? 'lg:flex-1' : 'md:flex-1';
58
+ const contentMinW0Class = contentWidthBreakpoint === 'lg' ? 'lg:min-w-0' : 'md:min-w-0';
59
+ const h2AltSizeClass = h2SizeBreakpoint === 'lg' ? 'lg:text-[40px]' : 'md:text-[40px]';
60
+ const h2AltLeadingClass = h2SizeBreakpoint === 'lg' ? 'lg:leading-[48px]' : 'md:leading-[48px]';
61
+ const h2IcoSizeClass = h2SizeBreakpoint === 'lg' ? 'lg:text-[46px]' : 'md:text-[46px]';
62
+ const h2IcoLeadingClass = h2SizeBreakpoint === 'lg' ? 'lg:leading-[54px]' : 'md:leading-[54px]';
63
+ const h2LargeSizeClass = h2SizeBreakpoint === 'lg' ? 'lg:text-[56px]' : 'md:text-[56px]';
64
+ const h2LargeLeadingClass = h2SizeBreakpoint === 'lg' ? 'lg:leading-[64px]' : 'md:leading-[64px]';
65
+
44
66
  // Extraer todos los crops definidos en el carbin (base, skyscraper)
45
67
  // Funciona igual con JSON de PageBuilder o con objeto simple { base: "url", skyscraper: "url" }
46
68
  const crops = extractAllCrops(image) as Record<string, string>;
@@ -63,7 +85,7 @@ const videoUrl = iframeSrc || '';
63
85
  <section class="w-full flex items-center justify-center">
64
86
  {showImage ? (
65
87
  <article class={`w-${width} flex flex-col items-center justify-center py-6`}>
66
- <div class={`flex-col md:flex ${orientation === 'left' ? 'flex-row' : 'flex-row-reverse'} w-full md:w-4/5 items-${alignItems} ${columnGapClass}`}>
88
+ <div class={`flex-col ${rowFlexClass} ${orientation === 'left' ? 'flex-row' : 'flex-row-reverse'} w-full ${rowWidthClass} ${rowMaxWidthClass} items-${alignItems} ${columnGapClass} ${outerPaddingClass}`}>
67
89
  {showIco ? (
68
90
  <div class={`flex ${flexOrientation === 'row' ? 'flex-row' : 'flex-col'} w-full md:w-1/2 gap-6 p-4`}>
69
91
  <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-[30px]">
@@ -76,7 +98,7 @@ const videoUrl = iframeSrc || '';
76
98
  </svg>
77
99
  <div class={`w-full ${orientation === 'left' ? `pl-[${paddingLeft}px]` : `pr-[${paddingLeft}px]`}`}>
78
100
  {title && <h1 class="font-poppins text-[#262626] text-center lg:text-left text-base font-normal leading-6 rounded bg-[#F2F3F8] mb-8 w-fit py-2 px-3 mx-auto lg:mx-0" set:html={title}></h1>}
79
- <h2 class={`font-poppins text-[#262626] font-normal ${h2AlignClass} ${h2TrackingClass} pr-6 ${h2MarginBottom} mx-auto lg:mx-0 text-[36px] ${h2Size === '40px' ? 'lg:text-[40px] leading-[44px] lg:leading-[48px]' : 'lg:text-[46px] leading-[44px] lg:leading-[54px]'}`} set:html={subtitle}></h2>
101
+ <h2 class={`font-poppins text-[#262626] font-normal ${h2AlignClass} ${h2TrackingClass} pr-6 ${h2MarginBottom} mx-auto lg:mx-0 text-[36px] ${h2Size === '40px' ? `${h2AltSizeClass} leading-[44px] ${h2AltLeadingClass}` : `${h2IcoSizeClass} leading-[44px] ${h2IcoLeadingClass}`}`} set:html={subtitle}></h2>
80
102
 
81
103
  <div
82
104
  class={`text-[#363942] font-inter font-normal text-${fontSize} not-italic leading-6 flex flex-col gap-4 mb-8`}
@@ -110,10 +132,10 @@ const videoUrl = iframeSrc || '';
110
132
  }
111
133
  }
112
134
  </style>
113
- <div class={`w-full lg:w-1/2 ${contentPaddingClass} ${orientation === 'left' ? 'css-mq-pl-767' : 'pr-[var(--padding-left)] pl-0' }`}
135
+ <div class={`w-full ${contentFlex1Class} ${contentMinW0Class} ${contentPaddingClass} ${orientation === 'left' ? 'css-mq-pl-767' : 'pr-[var(--padding-left)] pl-0' }`}
114
136
  style={{ 'padding-left': `${paddingLeft}px` }}>
115
137
  {title && <h1 class="font-poppins text-[#262626] text-center lg:text-left text-base font-normal leading-6 rounded bg-[#F2F3F8] mb-8 w-fit py-2 px-3 mx-auto lg:mx-0" set:html={title}></h1>}
116
- <h2 class={`font-poppins text-[#262626] font-normal ${h2AlignClass} ${h2TrackingClass} pr-6 ${h2MarginBottom} mx-auto lg:mx-0 text-[36px] ${h2Size === '40px' ? 'lg:text-[40px] leading-[44px] lg:leading-[48px]' : 'lg:text-[56px] leading-[44px] lg:leading-[64px]'}`} set:html={subtitle}></h2>
138
+ <h2 class={`font-poppins text-[#262626] font-normal ${h2AlignClass} ${h2TrackingClass} pr-6 ${h2MarginBottom} mx-auto lg:mx-0 text-[36px] ${h2Size === '40px' ? `${h2AltSizeClass} leading-[44px] ${h2AltLeadingClass}` : `${h2LargeSizeClass} leading-[44px] ${h2LargeLeadingClass}`}`} set:html={subtitle}></h2>
117
139
  <div
118
140
  class={`text-[#363942] font-inter font-normal text-${fontSize} not-italic leading-6 flex flex-col gap-4 mb-8`}
119
141
  set:html={description}
@@ -134,13 +156,13 @@ const videoUrl = iframeSrc || '';
134
156
 
135
157
  )}
136
158
 
137
- <div class={`w-full md:w-1/2 p-4 flex items-center ${orientation === 'left' ? 'justify-end' : 'justify-start'} aspect-auto`}>
159
+ <div class={`w-full ${rowFlex1Class} ${rowMinW0Class} p-4 flex items-center ${orientation === 'left' ? 'justify-end' : 'justify-start'} aspect-auto`}>
138
160
  {videoUrl ? (
139
161
  <video autoplay loop muted playsinline preload="metadata" width="600" height="450" class="md:max-w-xl w-full md:w-[75%] h-auto object-cover rounded-lg m-auto">
140
162
  <source src={videoUrl} type="video/mp4" />
141
163
  </video>
142
164
  ) : (
143
- <img src={imageUrl} alt={title} width="600" height="450" class={`${imageSizeClass} h-auto object-cover rounded-lg m-auto ${imageAspectClass}`} loading={loading} />
165
+ <img src={imageUrl} alt={title} width="600" height="450" class={`${imageSizeClass} object-cover rounded-lg m-auto ${imageAspectClass}`} loading={loading} />
144
166
  )}
145
167
  </div>
146
168
 
@@ -165,7 +187,7 @@ const videoUrl = iframeSrc || '';
165
187
 
166
188
  <div class="w-1/2 p-4 flex flex-col flex-wrap ">
167
189
  {title && <h1 class="font-poppins text-[#262626] text-center lg:text-left text-base font-normal leading-6 rounded bg-[#F2F3F8] mb-8 w-fit py-2 px-3 mx-auto lg:mx-0" set:html={title}></h1>}
168
- <h2 class={`font-poppins text-[#262626] font-normal ${h2AlignClass} ${h2TrackingClass} pr-6 ${h2MarginBottom} mx-auto lg:mx-0 text-[36px] ${h2Size === '40px' ? 'lg:text-[40px] leading-[44px] lg:leading-[48px]' : 'lg:text-[56px] leading-[44px] lg:leading-[64px]'}`} set:html={subtitle}></h2>
190
+ <h2 class={`font-poppins text-[#262626] font-normal ${h2AlignClass} ${h2TrackingClass} pr-6 ${h2MarginBottom} mx-auto lg:mx-0 text-[36px] ${h2Size === '40px' ? `${h2AltSizeClass} leading-[44px] ${h2AltLeadingClass}` : `${h2LargeSizeClass} leading-[44px] ${h2LargeLeadingClass}`}`} set:html={subtitle}></h2>
169
191
  <div
170
192
  class="text-[#363942] font-inter font-normal text-base not-italic leading-6 flex flex-col gap-4 mb-8"
171
193
  set:html={description}
@@ -188,7 +210,7 @@ const videoUrl = iframeSrc || '';
188
210
  ) : (
189
211
  <div class="w-1/2 p-4 flex flex-col flex-wrap ">
190
212
  {title && <h1 class="font-poppins text-[#262626] text-center lg:text-left text-base font-normal leading-6 rounded bg-[#F2F3F8] mb-8 w-fit py-2 px-3 mx-auto lg:mx-0" set:html={title}></h1>}
191
- <h2 class={`font-poppins text-[#262626] font-normal ${h2AlignClass} ${h2TrackingClass} pr-6 ${h2MarginBottom} mx-auto lg:mx-0 text-[36px] ${h2Size === '40px' ? 'lg:text-[40px] leading-[44px] lg:leading-[48px]' : 'lg:text-[56px] leading-[44px] lg:leading-[64px]'}`} set:html={subtitle}></h2>
213
+ <h2 class={`font-poppins text-[#262626] font-normal ${h2AlignClass} ${h2TrackingClass} pr-6 ${h2MarginBottom} mx-auto lg:mx-0 text-[36px] ${h2Size === '40px' ? `${h2AltSizeClass} leading-[44px] ${h2AltLeadingClass}` : `${h2LargeSizeClass} leading-[44px] ${h2LargeLeadingClass}`}`} set:html={subtitle}></h2>
192
214
  <div
193
215
  class="text-[#363942] font-inter font-normal text-base not-italic leading-6 flex flex-col gap-4 mb-8"
194
216
  set:html={description}
@@ -5,33 +5,42 @@ const {
5
5
  title,
6
6
  description,
7
7
  image,
8
- imageAlt = '',
8
+ imageAlt,
9
9
  ctaText,
10
- ctaUrl = '#'
10
+ ctaUrl
11
11
  } = Astro.props;
12
+
13
+ // ACCESIBILIDAD Imagen de fondo: si no llega alt (undefined o null desde Mecano) forzamos alt="" para que el lector de pantalla no anuncie el nombre del fichero.
14
+ const safeImageAlt = imageAlt ?? '';
15
+
16
+ // ACCESIBILIDAD El CTA solo se renderiza si hay texto Y URL; evita enlaces "#" que solo saltan al inicio de la página.
17
+ const showCta = Boolean(ctaText && ctaUrl);
18
+
19
+ // ACCESIBILIDAD Id único por instancia para enlazar la sección con su h1 (aria-labelledby) sin colisionar si hay varios heros.
20
+ const titleId = 'hero-title-' + Math.random().toString(36).substring(2, 10);
12
21
  ---
13
22
 
14
- <section class="relative w-full overflow-hidden bg-gray-100">
23
+ <section class="relative w-full overflow-hidden bg-gray-100" aria-labelledby={title ? titleId : undefined}>
15
24
  <LimboImage
16
25
  src={image}
17
- alt={imageAlt}
26
+ alt={safeImageAlt}
18
27
  class="absolute inset-0 w-full h-full object-cover object-left md:object-center"
19
28
  loading="eager"
20
29
  />
21
30
  <div class="relative max-w-7xl w-full mx-auto flex items-center min-h-[600px] px-8 md:px-0">
22
- <div class="w-full md:w-1/2 flex flex-col items-start gap-8 py-8 md:py-20 md:pr-16">
31
+ <div class="w-full md:w-1/2 flex flex-col items-start gap-8 pl-8 py-8 md:pl-6 md:py-20 md:pr-16">
23
32
  <div class="flex flex-col gap-4 w-full">
24
33
  {title && (
25
- <h1 class="font-poppins text-[#262626] text-4xl leading-[44px] tracking-[-0.5px] font-semibold md:text-[56px] md:leading-[64px] md:tracking-[-1px] md:font-normal" set:html={title}></h1>
34
+ <h1 id={titleId} class="font-poppins text-[#262626] text-4xl leading-[44px] tracking-[-0.5px] font-semibold md:text-[56px] md:leading-[64px] md:tracking-[-1px] md:font-normal" set:html={title}></h1>
26
35
  )}
27
36
  {description && (
28
37
  <p class="font-poppins text-[#262626] text-[28px] leading-9 md:text-[32px] md:leading-10 font-normal" set:html={description}></p>
29
38
  )}
30
39
  </div>
31
- {ctaText && (
40
+ {showCta && (
32
41
  <a
33
42
  href={ctaUrl}
34
- class="md:hidden inline-flex items-center justify-center w-fit h-10 px-4 rounded-lg bg-[#00092CE5] text-white font-inter text-base font-semibold leading-6 shadow-md"
43
+ class="md:hidden inline-flex items-center justify-center w-fit min-h-11 h-11 px-4 rounded-lg bg-[#00092CE5] text-white font-inter text-base font-semibold leading-6 shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-[#00092C]"
35
44
  >
36
45
  {ctaText}
37
46
  </a>