diff --git a/src/pages/proyectos/[id].astro b/src/pages/proyectos/[id].astro index b6bbb02..c166dab 100644 --- a/src/pages/proyectos/[id].astro +++ b/src/pages/proyectos/[id].astro @@ -206,4 +206,14 @@ const breadcrumbLd = { + + diff --git a/src/scripts/proyecto-tabs.js b/src/scripts/proyecto-tabs.js new file mode 100644 index 0000000..21d0a2e --- /dev/null +++ b/src/scripts/proyecto-tabs.js @@ -0,0 +1,139 @@ +function slugify(value) { + return value + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); +} + +function ensureHeadingId(heading, usedIds) { + if (heading.id) { + usedIds.add(heading.id); + return heading.id; + } + + const base = slugify(heading.textContent || '') || 'apartado'; + let id = base; + let i = 2; + while (usedIds.has(id) || document.getElementById(id)) { + id = `${base}-${i++}`; + } + + heading.id = id; + usedIds.add(id); + return id; +} + +export function initProyectoTabs() { + const container = document.querySelector('.pd-contenido'); + if (!container) return; + + const topSections = Array.from(container.querySelectorAll(':scope > h2')); + if (topSections.length < 2) return; + + const tabsRoot = document.createElement('section'); + tabsRoot.className = 'pd-tabs'; + + const tabList = document.createElement('div'); + tabList.className = 'pd-tabs-list'; + tabList.setAttribute('role', 'tablist'); + tabList.setAttribute('aria-label', 'Apartados del proyecto'); + + const panels = document.createElement('div'); + panels.className = 'pd-tabs-panels'; + + const usedIds = new Set(Array.from(container.querySelectorAll('[id]'), (el) => el.id)); + const tabs = []; + const panelByHeadingId = new Map(); + + topSections.forEach((heading, index) => { + const headingId = ensureHeadingId(heading, usedIds); + const tabId = `pd-tab-${headingId}`; + const panelId = `pd-panel-${headingId}`; + + const tab = document.createElement('button'); + tab.type = 'button'; + tab.className = 'pd-tab-btn'; + tab.id = tabId; + tab.setAttribute('role', 'tab'); + tab.setAttribute('aria-controls', panelId); + tab.setAttribute('aria-selected', 'false'); + tab.setAttribute('tabindex', '-1'); + tab.textContent = heading.textContent?.trim() || `Apartado ${index + 1}`; + + const panel = document.createElement('section'); + panel.className = 'pd-tab-panel'; + panel.id = panelId; + panel.setAttribute('role', 'tabpanel'); + panel.setAttribute('aria-labelledby', tabId); + panel.hidden = true; + + let current = heading; + while (current) { + const next = current.nextElementSibling; + if (current !== heading && current.tagName === 'H2') break; + panel.appendChild(current); + current = next; + } + + panelByHeadingId.set(headingId, index); + tabs.push({ tab, panel, headingId }); + tabList.appendChild(tab); + panels.appendChild(panel); + }); + + tabsRoot.appendChild(tabList); + tabsRoot.appendChild(panels); + container.prepend(tabsRoot); + + const setActive = (index, { updateHash = false, focusTab = false } = {}) => { + tabs.forEach(({ tab, panel }, i) => { + const active = i === index; + tab.classList.toggle('is-active', active); + tab.setAttribute('aria-selected', active ? 'true' : 'false'); + tab.setAttribute('tabindex', active ? '0' : '-1'); + panel.hidden = !active; + panel.classList.toggle('is-active', active); + }); + + const activeTab = tabs[index]; + if (!activeTab) return; + + if (focusTab) activeTab.tab.focus(); + + if (updateHash && activeTab.headingId) { + history.replaceState(null, '', `#${activeTab.headingId}`); + } + }; + + tabs.forEach(({ tab }, index) => { + tab.addEventListener('click', () => setActive(index, { updateHash: true })); + }); + + tabList.addEventListener('keydown', (event) => { + const currentIndex = tabs.findIndex(({ tab }) => tab.getAttribute('aria-selected') === 'true'); + if (currentIndex === -1) return; + + let nextIndex = null; + if (event.key === 'ArrowRight') nextIndex = (currentIndex + 1) % tabs.length; + if (event.key === 'ArrowLeft') nextIndex = (currentIndex - 1 + tabs.length) % tabs.length; + if (event.key === 'Home') nextIndex = 0; + if (event.key === 'End') nextIndex = tabs.length - 1; + if (nextIndex === null) return; + + event.preventDefault(); + setActive(nextIndex, { updateHash: true, focusTab: true }); + }); + + const initialHash = window.location.hash.replace('#', ''); + const initialIndex = panelByHeadingId.get(initialHash) ?? 0; + setActive(initialIndex); + + window.addEventListener('hashchange', () => { + const hash = window.location.hash.replace('#', ''); + const index = panelByHeadingId.get(hash); + if (typeof index === 'number') setActive(index); + }); +} diff --git a/src/styles/proyecto-detalle.css b/src/styles/proyecto-detalle.css index b4fad82..43c91ef 100644 --- a/src/styles/proyecto-detalle.css +++ b/src/styles/proyecto-detalle.css @@ -377,6 +377,54 @@ body:has(.pd-page) { text-decoration-color: var(--pd-azul); } +/* Pestañas para apartados de primer nivel del markdown */ +.pd-tabs { + margin: 0 0 28px; +} + +.pd-tabs-list { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin: 0 0 24px; +} + +.pd-tab-btn { + appearance: none; + border: 1px solid rgba(147, 197, 253, 0.25); + background: rgba(147, 197, 253, 0.08); + color: rgba(212, 227, 255, 0.9); + border-radius: 999px; + padding: 10px 16px; + font: inherit; + font-size: 0.9rem; + font-weight: 600; + line-height: 1.2; + cursor: pointer; + transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease; +} + +.pd-tab-btn:hover { + background: rgba(147, 197, 253, 0.16); + border-color: rgba(147, 197, 253, 0.45); +} + +.pd-tab-btn:focus-visible { + outline: 2px solid rgba(147, 197, 253, 0.9); + outline-offset: 2px; +} + +.pd-tab-btn.is-active { + background: linear-gradient(135deg, rgba(147, 197, 253, 0.25), rgba(196, 181, 253, 0.2)); + border-color: rgba(147, 197, 253, 0.6); + color: #fff; + transform: translateY(-1px); +} + +.pd-tab-panel > h2:first-child { + margin-top: 0; +} + /* Secciones indentadas con línea de color por categoría */ .pd-contenido .pd-seccion { --pd-linea: rgba(255, 255, 255, 0.15); /* genérico, sin categoría */ @@ -582,6 +630,15 @@ body:has(.pd-page) { .pd-back { padding: 10px; } + + .pd-tabs-list { + gap: 8px; + } + + .pd-tab-btn { + padding: 9px 14px; + font-size: 0.85rem; + } } @media (max-width: 480px) {