libreria-astro-lefebvre 0.0.185 → 0.0.187

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.0.185",
3
+ "version": "0.0.187",
4
4
  "description": "Librería de componentes Astro, React y Vue para Lefebvre",
5
5
  "author": "Equipo web desarrollo Lefebvre",
6
6
  "type": "module",
@@ -4,34 +4,48 @@ export const metadata: ComponentMetadata = {
4
4
  component_name: 'Cabecera_2025_Barcelona',
5
5
  category: 'Header',
6
6
  name: 'Breadcrumb 2025',
7
- description: 'Breadcrumb de 2 niveles con icono de home y enlace apuntando al subdirectorio raíz, seguido del título de la página actual. Incluye structured data schema.org (BreadcrumbList) con URLs absolutas para SEO.',
7
+ description: 'Breadcrumb reutilizable con icono de home configurable, niveles intermedios con sus propias URLs y último nivel sin enlace. Incluye structured data schema.org (BreadcrumbList) con URLs absolutas para SEO.',
8
8
  framework: 'Astro',
9
9
  priority: 1,
10
10
  tags: ['breadcrumb', 'navegacion', 'seo'],
11
11
  fields: [
12
- {
12
+ {
13
+ name: 'homeLabel',
14
+ type: 'text',
15
+ help: 'Texto del primer nivel del breadcrumb (home). Admite HTML.',
16
+ label: 'Etiqueta del home',
17
+ mandatory: true,
18
+ example_value: 'GenIA-L'
19
+ },
20
+ {
21
+ name: 'items',
22
+ type: 'list',
23
+ help: 'Niveles intermedios del breadcrumb. Cada item tiene su propio enlace. El último nivel NO se incluye aquí.',
24
+ label: 'Niveles intermedios',
25
+ mandatory: false,
26
+ items: {
27
+ type: 'object',
28
+ fields: [
29
+ { name: 'text', type: 'text', help: 'Texto del nivel intermedio. Admite HTML.', label: 'Texto', example_value: 'Categoría' },
30
+ { name: 'url', type: 'text', help: 'URL de destino de este nivel intermedio.', label: 'URL', example_value: '/genia-l/categoria' }
31
+ ]
32
+ }
33
+ },
34
+ {
13
35
  name: 'text',
14
36
  type: 'text',
15
- help: 'Texto del último nivel del breadcrumb (el actual). Admite HTML. Si se deja vacío, el nivel aparecerá sin texto',
16
- label: 'Texto del breadcrumb',
37
+ help: 'Texto del último nivel del breadcrumb (la página actual). Admite HTML. No tiene enlace.',
38
+ label: 'Texto del último nivel',
17
39
  mandatory: false,
18
40
  example_value: 'Guía de fiscalidad para autónomos'
19
41
  },
20
- {
42
+ {
21
43
  name: 'subdirectory',
22
44
  type: 'text',
23
45
  help: 'Ruta base del sitio (p. ej. "/genia-l"). Se usa para construir el enlace al inicio y la URL absoluta del breadcrumb en el structured data SEO. Déjalo vacío si el breadcrumb parte de la raíz',
24
46
  label: 'Subdirectorio',
25
47
  mandatory: false,
26
48
  example_value: '/genia-l'
27
- },
28
- {
29
- name: 'link',
30
- type: 'text',
31
- help: 'URL de destino del último nivel del breadcrumb. Por defecto "#" (sin destino)',
32
- label: 'Enlace del breadcrumb',
33
- mandatory: false,
34
- example_value: '/genia-l/guia-fiscalidad-autonomos'
35
49
  }
36
50
  ]
37
51
  };
@@ -4,7 +4,7 @@ export const metadata: ComponentMetadata = {
4
4
  component_name: 'FAQ_2025_Hiroshima',
5
5
  category: 'Contenido con listas',
6
6
  name: 'Listado de preguntas frecuentes en acordeón exclusivo 2025',
7
- description: 'Listado de preguntas y respuestas en acordeón exclusivo: al abrir una pregunta se cierran automáticamente las demás (solo una respuesta visible a la vez). Un segundo click sobre la pregunta abierta la cierra. Incluye título h2 con valor por defecto "Preguntas frecuentes". Las respuestas se renderizan como texto plano dentro de un <p> no admiten HTML',
7
+ description: 'Listado de preguntas y respuestas en acordeón exclusivo: al abrir una pregunta se cierran automáticamente las demás (solo una respuesta visible a la vez). Un segundo click sobre la pregunta abierta la cierra. Incluye título h2 opcional. Permite configurar el estado inicial: todos plegados (por defecto), todos desplegados, o solo el primero desplegado. Las respuestas admiten HTML básico: strong, b, em, i, ul, ol, li. Incluye datos estructurados FAQPage.',
8
8
  framework: 'Astro',
9
9
  priority: 1,
10
10
  tags: ['texto', 'faq', 'acordeon', 'preguntas', 'interactivo'],
@@ -12,11 +12,21 @@ export const metadata: ComponentMetadata = {
12
12
  {
13
13
  name: 'title',
14
14
  type: 'text',
15
- help: 'Título de la sección de FAQ (h2). Si se deja vacío, por defecto muestra "Preguntas frecuentes"',
15
+ help: 'Título de la sección de FAQ (h2). Si se deja vacío, no se muestra el título',
16
16
  label: 'Texto del título',
17
17
  mandatory: false,
18
18
  example_value: 'Preguntas frecuentes sobre GenIA-L'
19
19
  },
20
+ {
21
+ name: 'initialState',
22
+ type: 'select',
23
+ help: 'Estado inicial de los elementos del acordeón. "all-closed": todos plegados (por defecto). "all-open": todos desplegados. "first-open": solo el primero desplegado',
24
+ label: 'Estado inicial del acordeón',
25
+ mandatory: false,
26
+ options: ['all-closed', 'all-open', 'first-open'],
27
+ options_labels: ['Todos plegados (por defecto)', 'Todos desplegados', 'Solo el primero desplegado'],
28
+ example_value: 'all-closed'
29
+ },
20
30
  {
21
31
  name: 'items',
22
32
  type: 'list',
@@ -27,7 +37,7 @@ export const metadata: ComponentMetadata = {
27
37
  type: 'object',
28
38
  fields: [
29
39
  { name: 'question', type: 'text', help: 'Texto de la pregunta (aparece como botón). No admite HTML', label: 'Texto de la pregunta', example_value: '¿Qué es GenIA-L y para qué sirve?' },
30
- { name: 'answer', type: 'text', help: 'Texto de la respuesta que aparece al abrir la pregunta. NO admite HTML (se renderiza como texto plano dentro de un <p>)', label: 'Texto de la respuesta', example_value: 'GenIA-L es la plataforma de inteligencia artificial jurídica de Lefebvre que automatiza la redacción de contratos, la búsqueda de jurisprudencia y el análisis de documentos legales' }
40
+ { name: 'answer', type: 'text', help: 'Texto de la respuesta que aparece al abrir la pregunta. Admite HTML básico: <strong>, <b>, <em>, <i>, <ul>, <ol>, <li>. Evitar etiquetas de bloque complejas o scripts', label: 'Texto de la respuesta', example_value: 'GenIA-L es la plataforma de inteligencia artificial jurídica de Lefebvre que automatiza la redacción de contratos, la búsqueda de jurisprudencia y el análisis de documentos legales' }
31
41
  ]
32
42
  }
33
43
  }
@@ -0,0 +1,75 @@
1
+ import type { ComponentMetadata } from '../interfaces/types';
2
+
3
+ export const metadata: ComponentMetadata = {
4
+ component_name: 'Tabla_2026_Cadiz',
5
+ category: 'Contenido con listas',
6
+ name: 'Tabla de dos columnas independientes con título centrado 2026',
7
+ description: 'Tabla con título h2 centrado, descripción HTML y dos columnas independientes (izquierda y derecha) con sus títulos e ítems respectivos (strings planos sin HTML). Las columnas no están relacionadas entre sí: cada una muestra sus propios ítems sin sincronizar filas ni flecha separadora. Sin fondo en el contenedor. Layout responsive: apilado en móvil, grid de 2 columnas en desktop. TERNARIO: si AMBAS columnas tienen contenido → grid de 2 columnas; si solo UNA tiene contenido → se renderiza una única tabla con esa columna. Incluye structured data schema.org (WebPageElement con dos ItemList independientes) para SEO.',
8
+ framework: 'Astro',
9
+ priority: 1,
10
+ tags: ['texto', 'tabla', 'columnas', 'lista'],
11
+ fields: [
12
+ {
13
+ name: 'title',
14
+ type: 'text',
15
+ help: 'Título superior de la tabla (h2), centrado. Solo se muestra si tiene valor; si se deja vacío, NO aparece',
16
+ label: 'Texto del título',
17
+ mandatory: false,
18
+ example_value: 'Nuestros servicios'
19
+ },
20
+ {
21
+ name: 'description',
22
+ type: 'text',
23
+ help: 'Descripción debajo del título. Admite HTML. Solo se muestra si tiene valor',
24
+ label: 'Texto de la descripción',
25
+ mandatory: false,
26
+ example_value: 'Todo lo que necesitas para tu despacho'
27
+ },
28
+ {
29
+ name: 'leftTitle',
30
+ type: 'text',
31
+ help: 'Título de la columna izquierda',
32
+ label: 'Título columna izquierda',
33
+ mandatory: false,
34
+ example_value: 'Servicios incluidos'
35
+ },
36
+ {
37
+ name: 'leftItems',
38
+ type: 'list',
39
+ help: 'Ítems de la columna izquierda. Cada ítem tiene un texto obligatorio y un enlace opcional. Si se rellena "link", el texto se renderiza como <a>; si se deja vacío, como texto plano',
40
+ label: 'Ítems columna izquierda',
41
+ mandatory: false,
42
+ items: {
43
+ type: 'object',
44
+ fields: [
45
+ { name: 'text', type: 'text', help: 'Texto visible del ítem', label: 'Texto del ítem', example_value: 'Asesoría fiscal' },
46
+ { name: 'link', type: 'text', help: 'URL de destino (opcional). Si se rellena, el ítem se convierte en enlace', label: 'Enlace (opcional)', example_value: '' },
47
+ { name: 'target', type: 'select', help: 'Cómo se abre el enlace: misma pestaña o nueva pestaña', label: 'Apertura del enlace', options: ['_self', '_blank'], options_labels: ['Misma pestaña', 'Nueva pestaña'], example_value: '_self' },
48
+ ]
49
+ }
50
+ },
51
+ {
52
+ name: 'rightTitle',
53
+ type: 'text',
54
+ help: 'Título de la columna derecha',
55
+ label: 'Título columna derecha',
56
+ mandatory: false,
57
+ example_value: 'Servicios adicionales'
58
+ },
59
+ {
60
+ name: 'rightItems',
61
+ type: 'list',
62
+ help: 'Ítems de la columna derecha. Cada ítem tiene un texto obligatorio y un enlace opcional. Si se rellena "link", el texto se renderiza como <a>; si se deja vacío, como texto plano',
63
+ label: 'Ítems columna derecha',
64
+ mandatory: false,
65
+ items: {
66
+ type: 'object',
67
+ fields: [
68
+ { name: 'text', type: 'text', help: 'Texto visible del ítem', label: 'Texto del ítem', example_value: 'Formación online' },
69
+ { name: 'link', type: 'text', help: 'URL de destino (opcional). Si se rellena, el ítem se convierte en enlace', label: 'Enlace (opcional)', example_value: '' },
70
+ { name: 'target', type: 'select', help: 'Cómo se abre el enlace: misma pestaña o nueva pestaña', label: 'Apertura del enlace', options: ['_self', '_blank'], options_labels: ['Misma pestaña', 'Nueva pestaña'], example_value: '_self' },
71
+ ]
72
+ }
73
+ }
74
+ ]
75
+ };
@@ -1,37 +1,32 @@
1
1
  ---
2
2
  const {
3
+ homeLabel,
4
+ items = [],
3
5
  text,
4
- link = "#",
5
6
  subdirectory = "",
6
7
  } = Astro.props;
7
8
 
8
- const currentUrl = subdirectory
9
- ? new URL(subdirectory + Astro.url.pathname, Astro.url.origin).href
9
+ const currentUrl = subdirectory
10
+ ? new URL(subdirectory + Astro.url.pathname, Astro.url.origin).href
10
11
  : Astro.url.href;
11
-
12
+
13
+ const homeUrl = new URL(subdirectory + '/', Astro.url.origin.endsWith('/') ? Astro.url.origin : Astro.url.origin + '/').href;
14
+
15
+ const allBreadcrumbItems = [
16
+ { name: homeLabel, id: homeUrl },
17
+ ...items.map((item: { text: string; url: string }) => ({ name: item.text, id: item.url || null })),
18
+ { name: text, id: currentUrl },
19
+ ];
20
+
12
21
  const breadcrumbStructuredData = {
13
22
  "@context": "https://schema.org",
14
23
  "@type": "BreadcrumbList",
15
- "itemListElement": [
16
- {
17
- "@type": "ListItem",
18
- "position": 1,
19
- "name": "Inicio",
20
- "item": {
21
- "@type": "WebPage",
22
- "@id": new URL(subdirectory + '/', Astro.url.origin.endsWith('/') ? Astro.url.origin : Astro.url.origin + '/').href
23
- }
24
- },
25
- {
26
- "@type": "ListItem",
27
- "position": 2,
28
- "name": text,
29
- "item": {
30
- "@type": "WebPage",
31
- "@id": currentUrl
32
- }
33
- }
34
- ]
24
+ "itemListElement": allBreadcrumbItems.map((item, i) => ({
25
+ "@type": "ListItem",
26
+ "position": i + 1,
27
+ "name": item.name,
28
+ ...(item.id ? { "item": { "@type": "WebPage", "@id": item.id } } : {})
29
+ }))
35
30
  };
36
31
 
37
32
  const breadcrumbScript = `<script type="application/ld+json">${JSON.stringify(breadcrumbStructuredData)}</script>`;
@@ -47,8 +42,14 @@ const {
47
42
  <path fill-rule="evenodd" clip-rule="evenodd" d="M11.607 2.26475C11.1639 2.32582 10.6509 2.48899 10.1624 2.72428C9.60055 2.99492 9.00859 3.34706 6.66084 4.80718C4.31852 6.26394 3.93572 6.53182 3.40193 7.08788C2.90464 7.60592 2.64374 8.07514 2.46575 8.77158C2.28122 9.49354 2.26758 9.74391 2.2535 12.6665C2.24172 15.1104 2.25893 16.3111 2.31649 17.061C2.44485 18.7333 2.80188 19.7175 3.55237 20.4678C4.28974 21.2051 5.2235 21.5501 6.83418 21.6806C7.50612 21.735 8.61888 21.75 11.9999 21.75C15.3809 21.75 16.4937 21.735 17.1656 21.6806C18.7575 21.5516 19.699 21.2081 20.4254 20.4911C20.9976 19.9264 21.341 19.2288 21.5307 18.2464C21.724 17.2457 21.7637 16.2126 21.7463 12.6447C21.7321 9.7458 21.7182 9.49232 21.534 8.77158C21.356 8.07514 21.0951 7.60592 20.5979 7.08788C20.0641 6.53182 19.6813 6.26394 17.3389 4.80718C14.9942 3.34893 14.4021 2.99663 13.8362 2.72324C13.4707 2.54669 13.003 2.3831 12.6586 2.3114C12.3947 2.25643 11.8444 2.23204 11.607 2.26475ZM11.6269 3.73267C11.5426 3.74947 11.3777 3.79614 11.2605 3.83636C10.6997 4.02896 10.2326 4.29571 7.66624 5.88901C5.52292 7.21964 5.04514 7.53681 4.65608 7.88712C3.98123 8.49478 3.78607 9.00003 3.72514 10.2972C3.69616 10.9139 3.69657 15.5359 3.72565 16.2378C3.80342 18.1147 4.03097 18.9336 4.63227 19.501C5.0201 19.867 5.59984 20.0807 6.49905 20.189C7.24869 20.2793 7.46553 20.283 11.9999 20.283C16.5343 20.283 16.7511 20.2793 17.5007 20.189C19.2668 19.9762 19.925 19.3378 20.1684 17.6016C20.2815 16.7945 20.3028 15.9639 20.291 12.8283C20.284 10.9976 20.272 10.0331 20.254 9.86956C20.1456 8.88351 19.9347 8.41609 19.3684 7.90674C18.9506 7.53089 18.535 7.25522 16.2312 5.82558C13.7693 4.29779 13.2896 4.02434 12.7476 3.83985C12.3354 3.69954 11.9691 3.6645 11.6269 3.73267Z" fill="#363942"/>
48
43
  <path fill-rule="evenodd" clip-rule="evenodd" d="M10.5553 14.1632C10.1564 14.1632 9.83307 14.4866 9.83307 14.8856V20.6644C9.83307 21.0634 9.50969 21.3868 9.11079 21.3868C8.71189 21.3868 8.38852 21.0634 8.38852 20.6644V14.8856C8.38852 13.6888 9.35864 12.7185 10.5553 12.7185H13.4444C14.6411 12.7185 15.6113 13.6888 15.6113 14.8856V20.6644C15.6113 21.0634 15.2879 21.3868 14.889 21.3868C14.4901 21.3868 14.1667 21.0634 14.1667 20.6644V14.8856C14.1667 14.4866 13.8433 14.1632 13.4444 14.1632H10.5553Z" fill="#363942"/>
49
44
  </svg>
50
- <span>GenIA-L</span>
51
- </a> / <a href={link}><span class="text-[#363942] font-normal" set:html={text}></span></a>
45
+ <span set:html={homeLabel}></span>
46
+ </a>
47
+ {items.map((item: { text: string; url: string }) => (
48
+ item.url
49
+ ? <> / <a href={item.url} class="underline"><span class="text-[#363942] font-normal" set:html={item.text}></span></a></>
50
+ : <> / <span class="text-[#363942] font-normal" set:html={item.text}></span></>
51
+ ))}
52
+ / <span class="text-[#363942] font-normal" set:html={text}></span>
52
53
  </div>
53
54
  </section>
54
55
 
@@ -1,49 +1,82 @@
1
1
  ---
2
2
  const {
3
- title = "Preguntas frecuentes",
4
- items = []
3
+ title,
4
+ items = [],
5
+ initialState = "all-closed"
5
6
  } = Astro.props;
6
7
 
8
+ const allOpen = initialState === 'all-open';
9
+
10
+ const stripHtml = (html: string) => html.replace(/<[^>]*>/g, '');
11
+
12
+ const structuredData = items.length > 0 ? `<script type="application/ld+json">
13
+ {
14
+ "@context": "https://schema.org",
15
+ "@type": "FAQPage",
16
+ "mainEntity": [
17
+ ${items.map((item: { question: string; answer: string }) => `{
18
+ "@type": "Question",
19
+ "name": ${JSON.stringify(item.question)},
20
+ "acceptedAnswer": {
21
+ "@type": "Answer",
22
+ "text": ${JSON.stringify(stripHtml(item.answer))}
23
+ }
24
+ }`).join(',\n ')}
25
+ ]
26
+ }
27
+ </script>` : '';
7
28
  ---
8
29
 
9
30
  <div class="w-full flex flex-col items-start gap-6 px-8 md:px-0 py-12">
10
- <h2 class="font-poppins text-[#262626] text-2xl font-semibold leading-8">{title}</h2>
11
- <div class="w-full flex flex-col">
12
- {items.map((item) => (
13
- <div class="faq-item w-full border-b border-[#E0E0E0]">
14
- <button class="faq-toggle flex items-center justify-between w-full py-4 cursor-pointer bg-transparent border-none font-inter text-[#262626] text-base font-semibold leading-6 hover:bg-[rgba(0,25,120,0.04)] transition-all text-left px-4">
15
- <span>{item.question}</span>
16
- <svg class="faq-arrow w-5 h-5 shrink-0 transition-transform duration-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#262626">
17
- <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
18
- </svg>
19
- </button>
20
- <div class="faq-answer hidden pb-4 text-[#363942] font-inter text-base font-normal leading-6 p-4">
21
- <p>{item.answer}</p>
31
+ {title && <h2 class="font-poppins text-[#262626] text-2xl font-semibold leading-8">{title}</h2>}
32
+ <div class="w-full flex flex-col faq-container" data-initial-state={initialState}>
33
+ {items.map((item: { question: string; answer: string }, index: number) => {
34
+ const isOpen = allOpen || (initialState === 'first-open' && index === 0);
35
+ return (
36
+ <div class="faq-item w-full border-b border-[#E0E0E0]">
37
+ <button class="faq-toggle flex items-center justify-between w-full py-4 cursor-pointer bg-transparent border-none font-inter text-[#262626] text-base font-semibold leading-6 hover:bg-[rgba(0,25,120,0.04)] transition-all text-left px-4">
38
+ <span>{item.question}</span>
39
+ <svg class={`faq-arrow w-5 h-5 shrink-0 transition-transform duration-300${isOpen ? ' rotate-180' : ''}`} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#262626">
40
+ <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
41
+ </svg>
42
+ </button>
43
+ <div class={`faq-answer pb-4 text-[#363942] font-inter text-base font-normal leading-6 p-4 prose prose-sm max-w-none${isOpen ? '' : ' hidden'}`} set:html={item.answer} />
22
44
  </div>
23
- </div>
24
- ))}
45
+ );
46
+ })}
25
47
  </div>
26
48
  </div>
27
49
 
50
+ <Fragment set:html={structuredData} />
51
+
28
52
  <script>
29
- document.querySelectorAll('.faq-toggle').forEach((button) => {
30
- button.addEventListener('click', () => {
31
- const item = button.closest('.faq-item');
32
- const answer = item?.querySelector('.faq-answer');
33
- const arrow = item?.querySelector('.faq-arrow');
34
- const isOpen = !answer?.classList.contains('hidden');
35
-
36
- // Cerrar todos
37
- document.querySelectorAll('.faq-item').forEach((otherItem) => {
38
- otherItem.querySelector('.faq-answer')?.classList.add('hidden');
39
- otherItem.querySelector('.faq-arrow')?.classList.remove('rotate-180');
40
- });
53
+ document.querySelectorAll('.faq-container').forEach((container) => {
54
+ const exclusive = container.getAttribute('data-initial-state') !== 'all-open';
41
55
 
42
- // Si no estaba abierto, abrirlo
43
- if (!isOpen) {
44
- answer?.classList.remove('hidden');
45
- arrow?.classList.add('rotate-180');
46
- }
56
+ container.querySelectorAll('.faq-toggle').forEach((button) => {
57
+ button.addEventListener('click', () => {
58
+ const item = button.closest('.faq-item');
59
+ const answer = item?.querySelector('.faq-answer');
60
+ const arrow = item?.querySelector('.faq-arrow');
61
+ const isOpen = !answer?.classList.contains('hidden');
62
+
63
+ if (exclusive) {
64
+ // Cerrar todos los del mismo contenedor
65
+ container.querySelectorAll('.faq-item').forEach((otherItem) => {
66
+ otherItem.querySelector('.faq-answer')?.classList.add('hidden');
67
+ otherItem.querySelector('.faq-arrow')?.classList.remove('rotate-180');
68
+ });
69
+ // Si no estaba abierto, abrirlo
70
+ if (!isOpen) {
71
+ answer?.classList.remove('hidden');
72
+ arrow?.classList.add('rotate-180');
73
+ }
74
+ } else {
75
+ // Toggle independiente
76
+ answer?.classList.toggle('hidden');
77
+ arrow?.classList.toggle('rotate-180');
78
+ }
79
+ });
47
80
  });
48
81
  });
49
82
  </script>
@@ -0,0 +1,141 @@
1
+ ---
2
+ const {
3
+ title = "",
4
+ description = "",
5
+ leftTitle = "",
6
+ leftItems = [],
7
+ rightTitle = "",
8
+ rightItems = [],
9
+ } = Astro.props;
10
+
11
+ const hasLeft = leftTitle || leftItems.length > 0;
12
+ const hasRight = rightTitle || rightItems.length > 0;
13
+ const hasBoth = hasLeft && hasRight;
14
+
15
+ type Item = { text: string; link?: string; target?: string };
16
+
17
+ const structuredData = `<script type="application/ld+json">
18
+ {
19
+ "@context": "https://schema.org",
20
+ "@type": "WebPageElement",
21
+ "name": "${title.replace(/"/g, '\\"')}",
22
+ "description": "${description ? description.replace(/<[^>]*>/g, '').replace(/"/g, '\\"') : ''}",
23
+ "hasPart": [
24
+ ${hasLeft ? `{
25
+ "@type": "ItemList",
26
+ "name": "${leftTitle.replace(/"/g, '\\"')}",
27
+ "numberOfItems": ${leftItems.length},
28
+ "itemListElement": [
29
+ ${leftItems.map((item: Item, index: number) => `{
30
+ "@type": "ListItem",
31
+ "position": ${index + 1},
32
+ "name": "${item.text.replace(/"/g, '\\"')}"${item.link ? `,
33
+ "url": "${item.link.replace(/"/g, '\\"')}"` : ''}
34
+ }`).join(',\n ')}
35
+ ]
36
+ }` : ''}${hasLeft && hasRight ? ',' : ''}
37
+ ${hasRight ? `{
38
+ "@type": "ItemList",
39
+ "name": "${rightTitle.replace(/"/g, '\\"')}",
40
+ "numberOfItems": ${rightItems.length},
41
+ "itemListElement": [
42
+ ${rightItems.map((item: Item, index: number) => `{
43
+ "@type": "ListItem",
44
+ "position": ${index + 1},
45
+ "name": "${item.text.replace(/"/g, '\\"')}"${item.link ? `,
46
+ "url": "${item.link.replace(/"/g, '\\"')}"` : ''}
47
+ }`).join(',\n ')}
48
+ ]
49
+ }` : ''}
50
+ ]
51
+ }
52
+ </script>`;
53
+ ---
54
+
55
+ <Fragment set:html={structuredData} />
56
+ <div class="w-full my-8 p-6 rounded-xl">
57
+ {title && <h2 class="text-[#262626] font-poppins text-2xl font-semibold leading-8 mb-4 text-center">{title}</h2>}
58
+ {description && <p class="text-[#363942] font-inter text-base font-normal leading-6 mb-4" set:html={description}></p>}
59
+ {hasBoth ? (
60
+ <>
61
+ {/* Mobile: tablas apiladas */}
62
+ <div class="flex flex-col gap-4 md:hidden">
63
+ <div class="w-full flex flex-col rounded-2xl bg-white">
64
+ <div class="p-4 border-b border-[#E0E0E0]">
65
+ <span class="font-poppins text-[#262626] text-base font-semibold leading-6">{leftTitle}</span>
66
+ </div>
67
+ {leftItems.map((item: Item) => (
68
+ <div class="p-4">
69
+ {item.link ? (
70
+ <a href={item.link} target={item.target ?? '_self'} class="font-inter text-[#363942] text-sm font-normal leading-5 underline">{item.text}</a>
71
+ ) : (
72
+ <span class="font-inter text-[#363942] text-sm font-normal leading-5">{item.text}</span>
73
+ )}
74
+ </div>
75
+ ))}
76
+ </div>
77
+ <div class="w-full flex flex-col rounded-2xl bg-white">
78
+ <div class="p-4 border-b border-[#E0E0E0]">
79
+ <span class="font-poppins text-[#262626] text-base font-semibold leading-6">{rightTitle}</span>
80
+ </div>
81
+ {rightItems.map((item: Item) => (
82
+ <div class="p-4">
83
+ {item.link ? (
84
+ <a href={item.link} target={item.target ?? '_self'} class="font-inter text-[#363942] text-sm font-normal leading-5 underline">{item.text}</a>
85
+ ) : (
86
+ <span class="font-inter text-[#363942] text-sm font-normal leading-5">{item.text}</span>
87
+ )}
88
+ </div>
89
+ ))}
90
+ </div>
91
+ </div>
92
+
93
+ {/* Desktop: grid de 2 columnas independientes */}
94
+ <div class="hidden md:grid gap-4 w-full" style="grid-template-columns: 1fr 1fr;">
95
+ <div class="flex flex-col rounded-2xl bg-white">
96
+ <div class="p-4 border-b border-[#E0E0E0]">
97
+ <span class="font-poppins text-[#262626] text-base font-semibold leading-6">{leftTitle}</span>
98
+ </div>
99
+ {leftItems.map((item: Item) => (
100
+ <div class="p-4">
101
+ {item.link ? (
102
+ <a href={item.link} target={item.target ?? '_self'} class="font-inter text-[#363942] text-sm font-normal leading-5 underline">{item.text}</a>
103
+ ) : (
104
+ <span class="font-inter text-[#363942] text-sm font-normal leading-5">{item.text}</span>
105
+ )}
106
+ </div>
107
+ ))}
108
+ </div>
109
+ <div class="flex flex-col rounded-2xl bg-white">
110
+ <div class="p-4 border-b border-[#E0E0E0]">
111
+ <span class="font-poppins text-[#262626] text-base font-semibold leading-6">{rightTitle}</span>
112
+ </div>
113
+ {rightItems.map((item: Item) => (
114
+ <div class="p-4">
115
+ {item.link ? (
116
+ <a href={item.link} target={item.target ?? '_self'} class="font-inter text-[#363942] text-sm font-normal leading-5 underline">{item.text}</a>
117
+ ) : (
118
+ <span class="font-inter text-[#363942] text-sm font-normal leading-5">{item.text}</span>
119
+ )}
120
+ </div>
121
+ ))}
122
+ </div>
123
+ </div>
124
+ </>
125
+ ) : (
126
+ <div class="w-full flex flex-col rounded-2xl bg-white">
127
+ <div class="p-4 border-b border-[#E0E0E0]">
128
+ <span class="font-poppins text-[#262626] text-base font-semibold leading-6">{hasLeft ? leftTitle : rightTitle}</span>
129
+ </div>
130
+ {(hasLeft ? leftItems : rightItems).map((item: Item) => (
131
+ <div class="p-4">
132
+ {item.link ? (
133
+ <a href={item.link} target={item.target ?? '_self'} class="font-inter text-[#363942] text-sm font-normal leading-5 underline">{item.text}</a>
134
+ ) : (
135
+ <span class="font-inter text-[#363942] text-sm font-normal leading-5">{item.text}</span>
136
+ )}
137
+ </div>
138
+ ))}
139
+ </div>
140
+ )}
141
+ </div>
@@ -59,6 +59,7 @@ import * as Share_2025_Florencia from '../carbins/Share_2025_Florencia.ts';
59
59
  import * as SpectrumSeparator from '../carbins/SpectrumSeparator.ts';
60
60
  import * as Sumario_2025_Beijing from '../carbins/Sumario_2025_Beijing.ts';
61
61
  import * as Tabla_2025_Fuenlabrada from '../carbins/Tabla_2025_Fuenlabrada.ts';
62
+ import * as Tabla_2026_Cadiz from '../carbins/Tabla_2026_Cadiz.ts';
62
63
  import * as Tag_2025_Bolonia from '../carbins/Tag_2025_Bolonia.ts';
63
64
  import * as TestPadre from '../carbins/TestPadre.ts';
64
65
  import * as Test_2026_Gaza from '../carbins/Test_2026_Gaza.ts';
@@ -138,6 +139,7 @@ export const components = [
138
139
  {component: SpectrumSeparator},
139
140
  {component: Sumario_2025_Beijing},
140
141
  {component: Tabla_2025_Fuenlabrada},
142
+ {component: Tabla_2026_Cadiz},
141
143
  {component: Tag_2025_Bolonia},
142
144
  {component: TestPadre},
143
145
  {component: Test_2026_Gaza},
package/src/index.ts CHANGED
@@ -65,6 +65,7 @@ import Share_2025_Florencia from './components/Astro/Share_2025_Florencia.astro'
65
65
  import SpectrumSeparator from './components/Astro/SpectrumSeparator.astro';
66
66
  import Sumario_2025_Beijing from './components/Astro/Sumario_2025_Beijing.astro';
67
67
  import Tabla_2025_Fuenlabrada from './components/Astro/Tabla_2025_Fuenlabrada.astro';
68
+ import Tabla_2026_Cadiz from './components/Astro/Tabla_2026_Cadiz.astro';
68
69
  import Tag_2025_Bolonia from './components/Astro/Tag_2025_Bolonia.astro';
69
70
  import TestHijo from './components/Astro/TestHijo.astro';
70
71
  import TestPadre from './components/Astro/TestPadre.astro';
@@ -87,7 +88,7 @@ import ReactButton from './components/React/ReactButton.jsx';
87
88
  // Exporta todos los componentes uno a uno para que puedan ser usados directamente.
88
89
 
89
90
 
90
- export { VueButton, Author_2025_Algarve, Button, CTA_2025_Formentera, Cabecera_2025_Barcelona, Cabecera_2025_Madrid, Cabecera_2026_Bilbao, Cabecera_2026_Madrid, Card_2025_Malta, Contenido_2025_Alcorcon, Contenido_2025_Cordoba, Contenido_2025_Granada, Contenido_2025_Malaga, Contenido_2025_Montevideo, Contenido_2026_Cabra, Contenido_2026_Dubai, Contenido_2026_Estocolmo, Contenido_2026_Jaen, Contenido_2026_Leon, Contenido_2026_Mallorca, Contenido_2026_Marruecos, Contenido_2026_Menorca, Contenido_2026_Michigan, Contenido_2026_Mostoles, Contenido_2026_Orcasitas, Contenido_2026_Oslo, Contenido_2026_Quito, Contenido_2026_Seattle, Contenido_2026_Sevilla, Contenido_2026_Tokyo, Contenido_2026_Ubeda, Contenido_2026_Yakarta, CorpFooter, CorpHero, CorpNavigation, Enlace_2025_Venecia, FAQ_2025_Hiroshima, FooterCorporativo, Footer_2025_Napoles, Formulario_2025_Nara, Formulario_2025_Seul, Formulario_2025_Teruel, Formulario_2026_Wichita, GeometricShape, GeometricShapeCard, HeaderCorporativo, Hero_2025_Benidorm, Hero_2026_Benidorm, ImageTextSimple, Imagen_2025_Bogota, Imagen_2025_Fukushima, Indice_2025_Taiwan, Modal_2025_Sagunto, Paginacion_2025_Paris, RRSS_2025_Pisa, SEO_Head_Section, Separador_2025_Reinosa, Separador_2025_Toledo, Share_2025_Florencia, SpectrumSeparator, Sumario_2025_Beijing, Tabla_2025_Fuenlabrada, Tag_2025_Bolonia, TestHijo, TestPadre, Test_2026_Gaza, TextBox, TextImageBackground, TextImageBlock, TextImageCard, TextImageHeader, Texto_2025_Kyoto, Tiempo_2025_Londres, Titulo_2025_Algeciras, Titulo_2025_Santorini, VideoAutoplay, Video_2025_Polop, Video_2025_Valencia, Video_2026_Andujar, ReactButton };
91
+ export { VueButton, Author_2025_Algarve, Button, CTA_2025_Formentera, Cabecera_2025_Barcelona, Cabecera_2025_Madrid, Cabecera_2026_Bilbao, Cabecera_2026_Madrid, Card_2025_Malta, Contenido_2025_Alcorcon, Contenido_2025_Cordoba, Contenido_2025_Granada, Contenido_2025_Malaga, Contenido_2025_Montevideo, Contenido_2026_Cabra, Contenido_2026_Dubai, Contenido_2026_Estocolmo, Contenido_2026_Jaen, Contenido_2026_Leon, Contenido_2026_Mallorca, Contenido_2026_Marruecos, Contenido_2026_Menorca, Contenido_2026_Michigan, Contenido_2026_Mostoles, Contenido_2026_Orcasitas, Contenido_2026_Oslo, Contenido_2026_Quito, Contenido_2026_Seattle, Contenido_2026_Sevilla, Contenido_2026_Tokyo, Contenido_2026_Ubeda, Contenido_2026_Yakarta, CorpFooter, CorpHero, CorpNavigation, Enlace_2025_Venecia, FAQ_2025_Hiroshima, FooterCorporativo, Footer_2025_Napoles, Formulario_2025_Nara, Formulario_2025_Seul, Formulario_2025_Teruel, Formulario_2026_Wichita, GeometricShape, GeometricShapeCard, HeaderCorporativo, Hero_2025_Benidorm, Hero_2026_Benidorm, ImageTextSimple, Imagen_2025_Bogota, Imagen_2025_Fukushima, Indice_2025_Taiwan, Modal_2025_Sagunto, Paginacion_2025_Paris, RRSS_2025_Pisa, SEO_Head_Section, Separador_2025_Reinosa, Separador_2025_Toledo, Share_2025_Florencia, SpectrumSeparator, Sumario_2025_Beijing, Tabla_2025_Fuenlabrada, Tabla_2026_Cadiz, Tag_2025_Bolonia, TestHijo, TestPadre, Test_2026_Gaza, TextBox, TextImageBackground, TextImageBlock, TextImageCard, TextImageHeader, Texto_2025_Kyoto, Tiempo_2025_Londres, Titulo_2025_Algeciras, Titulo_2025_Santorini, VideoAutoplay, Video_2025_Polop, Video_2025_Valencia, Video_2026_Andujar, ReactButton };
91
92
 
92
93
 
93
94
  // Exporta la función listComponents para que sea usado en el Pagebuilder en Vue.
@@ -164,6 +165,7 @@ export const components = {
164
165
  SpectrumSeparator: SpectrumSeparator,
165
166
  Sumario_2025_Beijing: Sumario_2025_Beijing,
166
167
  Tabla_2025_Fuenlabrada: Tabla_2025_Fuenlabrada,
168
+ Tabla_2026_Cadiz: Tabla_2026_Cadiz,
167
169
  Tag_2025_Bolonia: Tag_2025_Bolonia,
168
170
  TestHijo: TestHijo,
169
171
  TestPadre: TestPadre,