Skip to content
Merged
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
8 changes: 6 additions & 2 deletions 2026/agenda.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha384-iw3OoTErCYJJB9mCa8LNS2hbsQ7M3C0EpIsO/H5+EGAkPGc6rk+V8i04oW/K5xq0" crossorigin="anonymous">
<link href="css/variables.css" rel="stylesheet">
<link href="css/base.css" rel="stylesheet">
<link href="css/style.css?v=20261012" rel="stylesheet">
<link href="css/style.css?v=20261015" rel="stylesheet">
<link href="css/responsive.css" rel="stylesheet">
</head>

Expand Down Expand Up @@ -140,6 +140,10 @@ <h2 id="agenda-detail-title"></h2>
<section class="agenda-dialog-speaker" id="agenda-detail-speaker"></section>
</div>
</dialog>
<dialog class="agenda-dialog agenda-photo-dialog" id="agenda-photo-dialog" aria-label="Foto del conferencista ampliada">
<form method="dialog"><button class="agenda-dialog-close" aria-label="Cerrar foto ampliada"><i class="fa-solid fa-xmark" aria-hidden="true"></i></button></form>
<img class="agenda-photo-dialog-image" id="agenda-photo-dialog-image" alt="">
</dialog>
</div>
</section>
</main>
Expand All @@ -159,7 +163,7 @@ <h2 id="agenda-detail-title"></h2>
</footer>

<script src="js/main.js"></script>
<script src="js/agenda.js?v=20261014"></script>
<script src="js/agenda.js?v=20261015"></script>
</body>

</html>
5 changes: 5 additions & 0 deletions 2026/css/responsive.css
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@
width: 48px;
height: 48px;
}

.speaker-profile .speaker-avatar-trigger {
width: 48px;
height: 48px;
}
}


Expand Down
53 changes: 53 additions & 0 deletions 2026/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,35 @@ button[type="submit"], .btn {
margin-left: -10px;
}

.speaker-avatar-trigger {
display: block;
width: 40px;
height: 40px;
flex: none;
overflow: hidden;
padding: 0;
border: 0;
border-radius: 50%;
background: transparent;
cursor: zoom-in;
}

.speaker-avatar-trigger .speaker-avatar {
display: block;
width: 100%;
height: 100%;
border-radius: inherit;
}

.speaker-avatar-trigger:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 3px;
}

.speaker-avatar-trigger + .speaker-avatar-trigger {
margin-left: -10px;
}

/* Tema y nivel: información secundaria */
.session-meta {
display: flex;
Expand Down Expand Up @@ -1034,6 +1063,21 @@ button[type="submit"], .btn {
box-shadow: var(--shadow-lg);
}

.agenda-photo-dialog {
width: min(900px, calc(100% - 32px));
max-height: 92dvh;
overflow: auto;
}

.agenda-photo-dialog-image {
display: block;
width: auto;
max-width: 100%;
max-height: calc(92dvh - 72px);
margin: 0 auto var(--space-4);
object-fit: contain;
}

.agenda-dialog::backdrop {
background: rgba(10, 61, 104, 0.55);
}
Expand Down Expand Up @@ -1128,10 +1172,19 @@ button[type="submit"], .btn {
font-size: var(--fs-base);
}

.speaker-profile .speaker-avatar-trigger {
width: 56px;
height: 56px;
}

.speaker-profile .speaker-avatar + .speaker-avatar {
margin-left: -16px;
}

.speaker-profile .speaker-avatar-trigger + .speaker-avatar-trigger {
margin-left: -16px;
}

.speaker-profile .session-speaker {
font-size: var(--fs-md);
font-weight: 700;
Expand Down
8 changes: 8 additions & 0 deletions 2026/img/flags/gt.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 29 additions & 7 deletions 2026/js/agenda.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ document.addEventListener('DOMContentLoaded', function () {
const detailDescription = document.getElementById('agenda-detail-description');
const detailSpeaker = document.getElementById('agenda-detail-speaker');
const detailMeta = document.getElementById('agenda-detail-meta');
/* Campos opcionales por perfil: role, org, country y countryCode (valor o lista), photo (ruta o lista de rutas) y links.
const photoDialog = document.getElementById('agenda-photo-dialog');
const photoDialogImage = document.getElementById('agenda-photo-dialog-image');
/* Campos opcionales por perfil: role, org, country y countryCode (valor o lista), photo (ruta o lista de rutas),
photoPosition (posición del encuadre), y links.
Lo que no exista simplemente no se muestra. */
const speakerProfiles = {
abdel: { name: 'Abdel Gadiel Martínez Lassonde', role: 'Director de Soluciones y Servicios Tecnológicos', org: 'Solusoft, S.A.', country: 'Panamá', countryCode: 'PA', bio: 'Abdel Gadiel Martínez Lassonde es Director de Soluciones y Servicios Tecnológicos en Solusoft, empresa panameña de servicios de ingeniería de datos con más de 30 años de trayectoria. Lidera equipos de consultores que dan soporte a clientes de banca, gobierno, salud, retail y telecomunicaciones, y es el arquitecto principal de Holos, la plataforma de observabilidad de Solusoft. Tiene amplia experiencia como DBA, ingeniero DevOps y arquitecto cloud, y cuenta con múltiples certificaciones en todas estas tecnologías. Es vicepresidente de la Fundación de Tecnologías Libres de Panamá y docente en la Universidad Interamericana de Panamá, UNICyT y CEAGRO AIP, donde comparte su experiencia con nuevas generaciones de profesionales.', photo: 'img/conferencistas/AbdelMartinez.webp', links: [{ label: 'LinkedIn', url: 'https://linkedin.com/in/abdelgmartinezl' }, { label: 'Sitio web', url: 'https://www.solusoft.com' }, { label: 'Holos', url: 'https://www.holosmonitor.com' }] },
http3: { name: 'Stuti Jain y Palak Jain', org: 'Adobe · HSBC', country: 'India', countryCode: 'IN', bio: 'Stuti Jain es SDE II en Adobe y ha presentado en PyCon Indonesia y PyCon Korea. Palak Jain es ingeniera de software en HSBC, con experiencia en sistemas backend y aplicaciones distribuidas con Python; también ha presentado en PyCon Indonesia y PyCon Korea.', photo: ['img/conferencistas/Stuti Jain.webp', 'img/conferencistas/Palak Jain.webp'], links: [{ label: 'Stuti Jain en LinkedIn', url: 'https://linkedin.com/in/stuti-jain-98630a197' }, { label: 'Palak Jain en LinkedIn', url: 'https://linkedin.com/in/palak-jain-980622145' }] },
uv: { name: 'David Jonathan Sol Llaven', role: 'Cloud Architect', org: 'Caylent', country: 'México', countryCode: 'MX', bio: 'Cloud Architect en Caylent; ayuda a organizar PythonCDMX y Ajolotes en la Nube (AWS).', photo: 'img/conferencistas/DavidSolCaylent.webp', links: [{ label: 'LinkedIn', url: 'https://linkedin.com/in/soldavidcloud' }] },
pypi: { name: 'Renzo Caceres Rossi', role: 'Especialista en R y Python', country: 'Perú', countryCode: 'PE', bio: 'Especialista en R y Python, creador de más de 30 librerías en R y 10 en Python para ciencia de datos. Ha sido ponente en PyDay Chile 2022 y PyCon Bolivia 2022.', photo: 'img/conferencistas/RENZOCACERESROSSI.webp', links: [{ label: 'LinkedIn', url: 'https://linkedin.com/in/andrescaceresrossi' }] },
'ai-ready': { name: 'Valery C. Briz', role: 'Senior Data Engineer', org: 'Mitek Systems', country: 'México', countryCode: 'MX', bio: 'Senior Data Engineer en Mitek Systems, con más de 10 años de experiencia. Excoorganizadora de PyLadies Madrid y PyLadies CDMX, fundadora de Python Guatemala e instructora en OpenWebinars.', photo: 'img/conferencistas/ValeriaCalderonBriz.webp', links: [{ label: 'LinkedIn', url: 'https://linkedin.com/in/valerybriz' }] },
'ai-ready': { name: 'Valery C. Briz', role: 'Senior Data Engineer', org: 'Mitek Systems', country: 'Guatemala', countryCode: 'GT', bio: 'Senior Data Engineer en Mitek Systems, con más de 10 años de experiencia. Excoorganizadora de PyLadies Madrid y PyLadies CDMX, fundadora de Python Guatemala e instructora en OpenWebinars.', photo: 'img/conferencistas/ValeriaCalderonBriz.webp', links: [{ label: 'LinkedIn', url: 'https://linkedin.com/in/valerybriz' }] },
streamlit: { name: 'Maria Clara Sanchez de Mira', role: 'Ingeniera de software backend', org: 'CI&T', country: 'Brasil', countryCode: 'BR', bio: 'Soy ingeniera informática con más de 4 años de experiencia en desarrollo de software, trabajando principalmente con Python. Actualmente, me desempeño como ingeniera de software backend en CI&T, atendiendo a un cliente del sector financiero. Estoy cursando una maestría en Ciencias de la Computación en el ICMC-USP, con enfoque en sistemas distribuidos y programación concurrente.', photo: 'img/conferencistas/MaríaClaraSanchez.webp', links: [{ label: 'LinkedIn', url: 'https://linkedin.com/in/maasanchez' }, { label: 'Sitio web', url: 'https://maria-sanchez.netlify.app' }] },
'educacion-ia': { name: 'Juan Camilo Infante', role: 'Educador, cofundador de Perspektiva 360 y Truvadur', country: ['Colombia', 'Costa Rica'], countryCode: ['CO', 'CR'], bio: 'Educador, apasionado por cómo la tecnología puede solucionar los problemas del mundo. Cofundó Perspektiva 360, empresa de software que durante 11 años creó productos para clientes como el Programa de las Naciones Unidas para el Desarrollo (PNUD) y UNICEF. Hoy construye productos de IA por voz con Python en Truvadur, como Bjørn, un tutor de idiomas por voz en Telegram. Ingeniero industrial de la Universidad de los Andes, MSc en Technology-based Entrepreneurship de KTH Royal Institute of Technology (Suecia) y Machine Learning Specialization de DeepLearning.AI. Fellow de la Young Leaders of the Americas Initiative (YLAI) y becario del Swedish Institute (SISGP).', photo: 'img/conferencistas/JuanCamiloInfante.webp', links: [{ label: 'LinkedIn', url: 'https://linkedin.com/in/juan-camilo-infante' }, { label: 'Sitio web', url: 'https://www.truvadur.com' }] },
gil: { name: 'Alberto Castillo', role: 'Líder de infraestructura', country: 'Panamá', countryCode: 'PA', bio: 'Más de 8 años de experiencia en desarrollo e infraestructura. Lidera infraestructura en una empresa de tecnología y forma parte de FLOSSPA desde 2016.', links: [{ label: 'Sitio web', url: 'https://www.betoissues.com' }] },
Expand Down Expand Up @@ -129,11 +132,11 @@ document.addEventListener('DOMContentLoaded', function () {
return [profile.role, profile.org, [].concat(profile.country || []).join(' y ')].filter(Boolean).join(' · ');
}

function createAvatars(profile) {
function createAvatars(profile, zoomable) {
const group = document.createElement('span');
const photos = [].concat(profile.photo || []);
group.className = 'speaker-avatars';
group.setAttribute('aria-hidden', 'true');
if (!zoomable) group.setAttribute('aria-hidden', 'true');
profile.name.split(' y ').forEach(function (name, index) {
let avatar;
if (photos[index]) {
Expand All @@ -142,6 +145,21 @@ document.addEventListener('DOMContentLoaded', function () {
avatar.alt = '';
avatar.loading = 'lazy';
avatar.decoding = 'async';
if (profile.photoPosition) avatar.style.objectPosition = profile.photoPosition;
if (zoomable) {
const trigger = document.createElement('button');
trigger.type = 'button';
trigger.className = 'speaker-avatar-trigger';
trigger.setAttribute('aria-label', 'Ampliar foto de ' + name);
trigger.append(avatar);
trigger.addEventListener('click', function () {
photoDialogImage.src = avatar.src;
photoDialogImage.alt = 'Foto ampliada de ' + name;
photoDialog.showModal();
});
group.append(trigger);
return;
}
} else {
avatar = document.createElement('span');
avatar.textContent = name.split(/\s+/).filter(Boolean).slice(0, 2).map(function (word) { return word[0]; }).join('').toUpperCase();
Expand Down Expand Up @@ -170,7 +188,7 @@ document.addEventListener('DOMContentLoaded', function () {
}).filter(Boolean);
}

function createSpeakerBlock(profile) {
function createSpeakerBlock(profile, zoomable) {
const block = document.createElement('div');
const text = document.createElement('div');
const name = document.createElement('p');
Expand All @@ -188,7 +206,7 @@ document.addEventListener('DOMContentLoaded', function () {
role.textContent = subtitle;
text.append(role);
}
block.append(createAvatars(profile), text);
block.append(createAvatars(profile, zoomable), text);
return block;
}

Expand Down Expand Up @@ -220,7 +238,7 @@ document.addEventListener('DOMContentLoaded', function () {
detailSpeaker.hidden = !(showSpeaker && profile);
if (showSpeaker && profile) {
const label = document.createElement('h3');
const card = createSpeakerBlock(profile);
const card = createSpeakerBlock(profile, true);
label.className = 'agenda-dialog-label';
label.textContent = profile.name.includes(' y ') ? 'Conferencistas' : 'Conferencista';
card.classList.add('speaker-profile');
Expand Down Expand Up @@ -311,6 +329,10 @@ document.addEventListener('DOMContentLoaded', function () {
if (event.target === dialog) dialog.close();
});

photoDialog.addEventListener('click', function (event) {
if (event.target === photoDialog) photoDialog.close();
});

addDayFilterControls();
setDayFilter('all');
});
9 changes: 6 additions & 3 deletions tests/agenda-page.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ test('la agenda es una sola vista con filtro por día y ficha de detalle', () =>
/http3: \{[^}]*countryCode: 'IN'/,
/uv: \{[^}]*countryCode: 'MX'/,
/pypi: \{[^}]*countryCode: 'PE'/,
/'ai-ready': \{[^}]*countryCode: 'MX'/,
/'ai-ready': \{[^}]*country: 'Guatemala'[^}]*countryCode: 'GT'/,
/streamlit: \{[^}]*countryCode: 'BR'/,
/'educacion-ia': \{[^}]*countryCode: \['CO', 'CR'\]/,
/carlos: \{[^}]*countryCode: 'CO'/,
Expand Down Expand Up @@ -88,12 +88,15 @@ test('las fotos de conferencistas aparecen en la agenda y en sus biografías', (
assert.ok(fs.statSync('2026/' + photo).size < 100 * 1024, 'la foto pesa menos de 100 KB: ' + photo);
});
assert.match(script, /photo: \['img\/conferencistas\/Stuti Jain\.webp', 'img\/conferencistas\/Palak Jain\.webp'\]/);
assert.match(script, /createAvatars\(profile\)/);
assert.match(script, /createAvatars\(profile, zoomable\)/);
assert.match(script, /speaker-profile-bio/);
assert.match(script, /Ampliar foto de '/);
assert.match(script, /photoDialog\.showModal\(\)/);
assert.match(page, /id="agenda-photo-dialog"/);
});

test('las banderas nacionales se cargan desde SVG locales', () => {
['br', 'co', 'in', 'mx', 'pa', 'pe'].forEach((country) => {
['br', 'co', 'gt', 'in', 'mx', 'pa', 'pe'].forEach((country) => {
assert.ok(fs.existsSync('2026/img/flags/' + country + '.svg'), 'existe la bandera ' + country);
});
assert.ok(fs.existsSync('2026/img/flags/LICENSE'), 'se incluye la licencia de las banderas');
Expand Down
Loading