Skip to content
Draft
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 .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
"dependencies": {
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@tailwindcss/vite": "^4.1.18",
"accessible-astro-components": "^5.1.1",
"animejs": "^4.2.2",
"astro": "^5.16.8",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
"eslint-plugin-jsx-a11y": "^6.10.2",
"prettier": "^3.7.4",
"prettier-plugin-astro": "^0.14.1",
"typescript": "^5.9.3"
Expand Down
1,644 changes: 1,644 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions src/content/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineCollection, z } from 'astro:content';

const menuCollection = defineCollection({
type: 'data',
schema: z.object({
items: z.array(
z.object({
label: z.string(),
href: z.string().optional(),
// optional
children: z.array(
z.object({
label: z.string(),
href: z.string(),
})
).optional(),
})
)
})
});

export const collections = {
'menu': menuCollection,
};
42 changes: 42 additions & 0 deletions src/content/menu/main.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"items": [
{
"label": "Inicio",
"href": "/"
},
{
"label": "La Conferencia",
"children": [
{
"label": "Speakers",
"href": "/speakers"
},
{
"label": "Agenda",
"href": "/agenda"
},
{
"label": "Sede",
"href": "/location"
}
]
},
{
"label": "Patrocinios",
"href": "/sponsorship"
},
{
"label": "Ediciones Anteriores",
"children": [
{
"label": "2025 (Sevilla)",
"href": "https://2025.es.pycon.org"
},
{
"label": "2024 (Vigo)",
"href": "https://2024.es.pycon.org"
}
]
}
]
}
10 changes: 8 additions & 2 deletions src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import '../style/global.css'
import '@fontsource-variable/jetbrains-mono'
import { ClientRouter } from 'astro:transitions'
import { SkipLink } from 'accessible-astro-components'
import Nav from './components/Nav.astro'

interface Props {
title: string
Expand All @@ -16,7 +18,7 @@ const { title, description = 'PyconES 2026' } = Astro.props
<head>
<meta charset="UTF-8" />
<meta name="description" content={description} />
<meta name="viewport" content="width=device-width" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/svg+xml" href="/2026.es.pycon.org/favicon.svg" />
<meta name="generator" content={Astro.generator} />

Expand All @@ -28,7 +30,11 @@ const { title, description = 'PyconES 2026' } = Astro.props
<body
class="bg-slate-900 text-white antialiased h-screen flex flex-col justify-center bg-linear-to-r from-gray to-blue-900 bg-radial"
>
<slot />
<SkipLink />
<Nav />
<main id="main-content" class="flex-grow">
<slot />
</main>
</body>
</html>

Expand Down
94 changes: 94 additions & 0 deletions src/layouts/components/Nav.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
import { getEntry } from 'astro:content'
import NavMobile from './NavMobile.astro'
import NavDesktop from './NavDesktop.astro'

// load menu data
const menuData = await getEntry('menu', 'main')

if (!menuData) {
throw new Error('No se encontró el archivo src/content/menu/main.json')
}

const { items } = menuData.data
---

<header class="fixed top-0 w-full z-50 bg-slate-900/90 backdrop-blur-md border-b border-white/10">
<div class="container mx-auto px-4 h-20 flex items-center justify-between">
<a
href="/"
class="text-2xl font-bold font-mono text-white focus-visible:ring-2 focus-visible:ring-blue-400 rounded outline-none"
>
PyCon<span class="text-blue-500">ES</span>
</a>

<button
id="mobile-menu-btn"
class="lg:hidden text-white p-2 rounded focus-visible:ring-2 focus-visible:ring-blue-400 outline-none"
aria-label="Abrir menú principal"
aria-expanded="false"
aria-controls="nav-menu"
>
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
class="menu-icon-path"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16m-7 6h7"></path>
</svg>
</button>

<nav
id="nav-menu"
class="hidden absolute top-20 left-0 w-full bg-slate-900 lg:static lg:w-auto lg:bg-transparent lg:flex lg:items-center border-b border-white/10 lg:border-none shadow-xl lg:shadow-none overflow-y-auto max-h-[calc(100vh-5rem)] lg:overflow-visible lg:max-h-full"
>
{/* Mobile Menu Structure using Accordion */}
<NavMobile items={items} />

{/* Desktop Menu Structure */}
<NavDesktop items={items} />
</nav>
</div>
</header>

<script>
// Simple logic for the mobile hamburger button toggle
// The dropdown logic is now handled internally by Accordion components
document.addEventListener('astro:page-load', () => {
const mobileBtn = document.getElementById('mobile-menu-btn')
const navMenu = document.getElementById('nav-menu')

mobileBtn?.addEventListener('click', (e) => {
e.stopPropagation()
const isHidden = navMenu?.classList.contains('hidden')
navMenu?.classList.toggle('hidden')
// If it WAS hidden, it is NOW expanded (true).
// If it WAS NOT hidden, it is NOW closed (false).
const isExpanded = isHidden

mobileBtn.setAttribute('aria-expanded', String(isExpanded))
mobileBtn.setAttribute('aria-label', isExpanded ? 'Cerrar menú' : 'Abrir menú')

const path = mobileBtn.querySelector('path')
if (isExpanded) {
path?.setAttribute('d', 'M6 18L18 6M6 6l12 12')
} else {
path?.setAttribute('d', 'M4 6h16M4 12h16m-7 6h7')
}
})

// Click Outside to close mobile menu
document.addEventListener('click', (e) => {
const target = e.target as Node
if (
window.innerWidth < 1024 &&
!navMenu?.contains(target) &&
!mobileBtn?.contains(target) &&
!navMenu?.classList.contains('hidden')
) {
mobileBtn?.click()
}
})
})
</script>
10 changes: 10 additions & 0 deletions src/layouts/components/NavDesktop.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
import NavDropdown from './NavDropdown.astro'
import NavItem from './NavItem.astro'

const { items } = Astro.props
---

<ul class="hidden lg:flex lg:flex-row gap-2 lg:gap-8 items-center">
{items.map((item: any) => (item.children ? <NavDropdown item={item} /> : <NavItem item={item} />))}
</ul>
121 changes: 121 additions & 0 deletions src/layouts/components/NavDropdown.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
import { AccordionItem, Link } from 'accessible-astro-components'

const { item } = Astro.props
---

<AccordionItem header={item.label} variant="chevron" title={item.label} class="nav-dropdown group relative">
<ul class="dropdown-content flex flex-col gap-2 p-2">
{
item.children.map((child: any) => (
<li>
<Link
href={child.href}
class="block px-4 py-1 rounded-md text-white font-medium hover:bg-white/5 focus-visible:bg-blue-600 focus-visible:text-white outline-none"
aria-current={Astro.url.pathname === child.href ? 'page' : undefined}
>
{child.label}
{child.description && (
<span class="block text-sm text-slate-400 mt-0.5">{child.description}</span>
)}
</Link>
</li>
))
}
</ul>
</AccordionItem>

<script>
// Close dropdowns when clicking outside (Desktop only)
document.addEventListener('click', (event) => {
// Only run on desktop where we treat these as dropdowns
if (window.innerWidth < 1024) return

const target = event.target as HTMLElement
const dropdowns = document.querySelectorAll('.nav-dropdown details[open]')

dropdowns.forEach((details) => {
if (!details.contains(target)) {
details.removeAttribute('open')
}
})
})
</script>

<style is:global>
/* Fix spacing on mobile */
.nav-dropdown details {
margin-bottom: 0 !important;
border: none !important;
}

.nav-dropdown summary {
padding-left: 0 !important;
padding-right: 0 !important;
}

/*
Desktop Override Styles for AccordionItem
This makes it float like a dropdown on large screens
while behaving like a normal accordion on mobile.
*/
@media (min-width: 1024px) {
.nav-dropdown .accordion__header {
background: transparent;
border: none;
padding: 0;
color: #cbd5e1; /* text-slate-300 */
font-weight: 500;
width: 100%;
cursor: pointer;
transition: color 0.15s ease-in-out;
}

/* Active state (when open) AND Hover state AND Focus state */
.nav-dropdown details[open] > summary .heading,
.nav-dropdown details summary:focus .heading,
.nav-dropdown details summary:focus-visible .heading,
.nav-dropdown details summary:hover .heading,
.nav-dropdown details[open] > summary .accordion__header,
.nav-dropdown .accordion__header:hover,
.nav-dropdown .accordion__header:focus,
.nav-dropdown .accordion__header:focus-visible {
color: white !important;
}

/* Hide the default background of accordion content */
.nav-dropdown .content {
position: absolute;
top: 100%;
left: 0;
min-width: 16rem; /* w-64 */
background-color: #0f172a; /* bg-slate-900 */
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
z-index: 50;
padding: 0;
margin-top: 1rem;
display: none; /* Key fix: hide by default in absolute mode */
}

/* Ensure content is visible when expanded */
.nav-dropdown details[open] .content {
display: block;
animation: fadeIn 0.1s ease-in-out;
}
.content {
margin-top: none;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
</style>
18 changes: 18 additions & 0 deletions src/layouts/components/NavItem.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
import { Link } from 'accessible-astro-components'

const { item, class: className } = Astro.props
---

<li class="relative">
<Link
href={item.href}
class:list={[
'block text-slate-300 hover:text-white font-medium transition-colors rounded outline-none focus-visible:text-blue-400 focus-visible:underline decoration-2 underline-offset-4',
className,
]}
aria-current={Astro.url.pathname === item.href ? 'page' : undefined}
>
{item.label}
</Link>
</li>
21 changes: 21 additions & 0 deletions src/layouts/components/NavMobile.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
import { Accordion } from 'accessible-astro-components'
import NavDropdown from './NavDropdown.astro'
import NavItem from './NavItem.astro'

const { items } = Astro.props
---

<div class="block lg:hidden w-full px-4 pb-4">
<Accordion>
{
items.map((item: any) =>
item.children ? (
<NavDropdown item={item} />
) : (
<NavItem item={item} class="text-lg font-medium text-slate-200 hover:text-white py-2" />
),
)
}
</Accordion>
</div>
1 change: 1 addition & 0 deletions src/pages/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const SPONSORS_EMAIL = 'sponsors@2026.es.pycon.org'
export const SPONSORS_SUBJECT = 'Interés de patrocinio para PyConES2026'
Loading