From e0beabf4839d2d4428204e4b264cf8079ee69fce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:39:34 +0000 Subject: [PATCH 1/3] Add tabbed top-level markdown sections on project detail pages --- src/pages/proyectos/[id].astro | 10 +++ src/scripts/proyecto-tabs.js | 139 ++++++++++++++++++++++++++++++++ src/styles/proyecto-detalle.css | 57 +++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 src/scripts/proyecto-tabs.js 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) { From 83da82ed06ece6bc053512b593eddc37708b2f15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:02:47 +0000 Subject: [PATCH 2/3] Restyle project tabs as true tab bar --- src/styles/proyecto-detalle.css | 93 ++++++++++++++++++++++++++------- 1 file changed, 73 insertions(+), 20 deletions(-) diff --git a/src/styles/proyecto-detalle.css b/src/styles/proyecto-detalle.css index 43c91ef..106bd0b 100644 --- a/src/styles/proyecto-detalle.css +++ b/src/styles/proyecto-detalle.css @@ -379,46 +379,98 @@ body:has(.pd-page) { /* Pestañas para apartados de primer nivel del markdown */ .pd-tabs { - margin: 0 0 28px; + margin: 0 0 30px; + border: 1px solid rgba(102, 134, 255, 0.18); + border-radius: 14px; + background: + linear-gradient(180deg, rgba(12, 16, 32, 0.88), rgba(8, 10, 22, 0.88)), + radial-gradient(120% 140% at 50% 0%, rgba(147, 197, 253, 0.12), rgba(147, 197, 253, 0)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.06), + 0 8px 24px rgba(0, 0, 0, 0.25); + overflow: hidden; } .pd-tabs-list { display: flex; - gap: 10px; - flex-wrap: wrap; - margin: 0 0 24px; + align-items: stretch; + gap: 0; + margin: 0; + border-bottom: 1px solid rgba(147, 197, 253, 0.15); + overflow-x: auto; + scrollbar-width: thin; } .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; + position: relative; + flex: 1 1 auto; + border: none; + border-right: 1px solid rgba(147, 197, 253, 0.1); + background: transparent; + color: rgba(225, 232, 255, 0.82); + padding: 14px 18px 13px; font: inherit; - font-size: 0.9rem; + font-size: 1rem; 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; + text-align: center; + white-space: nowrap; + transition: color 0.2s ease, background 0.2s ease; +} + +.pd-tab-btn:last-child { + border-right: none; } .pd-tab-btn:hover { - background: rgba(147, 197, 253, 0.16); - border-color: rgba(147, 197, 253, 0.45); + color: #fff; + background: rgba(147, 197, 253, 0.06); } .pd-tab-btn:focus-visible { - outline: 2px solid rgba(147, 197, 253, 0.9); - outline-offset: 2px; + outline: none; + box-shadow: inset 0 0 0 2px rgba(147, 197, 253, 0.85); } .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); + background: rgba(147, 197, 253, 0.08); + color: #c6a6ff; +} + +.pd-tab-btn.is-active::after { + content: ''; + position: absolute; + left: 12px; + right: 12px; + bottom: -1px; + height: 3px; + border-radius: 999px; + background: linear-gradient(90deg, #8a6bff, #b592ff); +} + +.pd-tabs-panels { + padding-top: 24px; +} + +.pd-tab-panel { + animation: pd-tab-fade 0.25s ease; +} + +@keyframes pd-tab-fade { + from { + opacity: 0; + transform: translateY(4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.pd-tab-panel.is-active { color: #fff; - transform: translateY(-1px); } .pd-tab-panel > h2:first-child { @@ -632,11 +684,12 @@ body:has(.pd-page) { } .pd-tabs-list { - gap: 8px; + -webkit-overflow-scrolling: touch; } .pd-tab-btn { - padding: 9px 14px; + flex: 0 0 auto; + padding: 12px 14px; font-size: 0.85rem; } } From f525829fb08df94cd426331ecdc0e6ac19416966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Garc=C3=ADa=20Santamar=C3=ADa?= <69808296+PGSCOM@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:14:32 +0200 Subject: [PATCH 3/3] Revert "Restyle project tabs as true tab bar" This reverts commit 83da82ed06ece6bc053512b593eddc37708b2f15. --- src/styles/proyecto-detalle.css | 93 +++++++-------------------------- 1 file changed, 20 insertions(+), 73 deletions(-) diff --git a/src/styles/proyecto-detalle.css b/src/styles/proyecto-detalle.css index 106bd0b..43c91ef 100644 --- a/src/styles/proyecto-detalle.css +++ b/src/styles/proyecto-detalle.css @@ -379,98 +379,46 @@ body:has(.pd-page) { /* Pestañas para apartados de primer nivel del markdown */ .pd-tabs { - margin: 0 0 30px; - border: 1px solid rgba(102, 134, 255, 0.18); - border-radius: 14px; - background: - linear-gradient(180deg, rgba(12, 16, 32, 0.88), rgba(8, 10, 22, 0.88)), - radial-gradient(120% 140% at 50% 0%, rgba(147, 197, 253, 0.12), rgba(147, 197, 253, 0)); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.06), - 0 8px 24px rgba(0, 0, 0, 0.25); - overflow: hidden; + margin: 0 0 28px; } .pd-tabs-list { display: flex; - align-items: stretch; - gap: 0; - margin: 0; - border-bottom: 1px solid rgba(147, 197, 253, 0.15); - overflow-x: auto; - scrollbar-width: thin; + gap: 10px; + flex-wrap: wrap; + margin: 0 0 24px; } .pd-tab-btn { appearance: none; - position: relative; - flex: 1 1 auto; - border: none; - border-right: 1px solid rgba(147, 197, 253, 0.1); - background: transparent; - color: rgba(225, 232, 255, 0.82); - padding: 14px 18px 13px; + 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: 1rem; + font-size: 0.9rem; font-weight: 600; line-height: 1.2; cursor: pointer; - text-align: center; - white-space: nowrap; - transition: color 0.2s ease, background 0.2s ease; -} - -.pd-tab-btn:last-child { - border-right: none; + transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease; } .pd-tab-btn:hover { - color: #fff; - background: rgba(147, 197, 253, 0.06); + background: rgba(147, 197, 253, 0.16); + border-color: rgba(147, 197, 253, 0.45); } .pd-tab-btn:focus-visible { - outline: none; - box-shadow: inset 0 0 0 2px rgba(147, 197, 253, 0.85); + outline: 2px solid rgba(147, 197, 253, 0.9); + outline-offset: 2px; } .pd-tab-btn.is-active { - background: rgba(147, 197, 253, 0.08); - color: #c6a6ff; -} - -.pd-tab-btn.is-active::after { - content: ''; - position: absolute; - left: 12px; - right: 12px; - bottom: -1px; - height: 3px; - border-radius: 999px; - background: linear-gradient(90deg, #8a6bff, #b592ff); -} - -.pd-tabs-panels { - padding-top: 24px; -} - -.pd-tab-panel { - animation: pd-tab-fade 0.25s ease; -} - -@keyframes pd-tab-fade { - from { - opacity: 0; - transform: translateY(4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.pd-tab-panel.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 { @@ -684,12 +632,11 @@ body:has(.pd-page) { } .pd-tabs-list { - -webkit-overflow-scrolling: touch; + gap: 8px; } .pd-tab-btn { - flex: 0 0 auto; - padding: 12px 14px; + padding: 9px 14px; font-size: 0.85rem; } }