Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/pages/proyectos/[id].astro
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,14 @@ const breadcrumbLd = {
</article>
</main>

<script>
import { initProyectoTabs } from '../../scripts/proyecto-tabs.js';

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initProyectoTabs, { once: true });
} else {
initProyectoTabs();
}
</script>

</Layout>
139 changes: 139 additions & 0 deletions src/scripts/proyecto-tabs.js
Original file line number Diff line number Diff line change
@@ -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);
});
}
57 changes: 57 additions & 0 deletions src/styles/proyecto-detalle.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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) {
Expand Down
Loading