feat: refine menu and module visual hierarchy

Daniel Silva committed Aug 26, 2026 at 23:56 UTC 4585b6c57194325d604cfb6f93ecdac9a345897b
6 files changed +59 -9
frontend/css/circulos.css
+4 -1
@@ -1,6 +1,9 @@
1 /* MAPS Connect - Círculos de estudio */
2
3 .circulos-container { width: 100%; max-width: 1050px; margin: 0 auto; }
4 +.module-heading { margin-bottom: 1.5rem; }
5 +.module-heading h1 { color: var(--sidebar-bg); font-size: 1.75rem; }
6 +.module-heading p { margin-top: 0.25rem; color: var(--gray-600); }
7 .circulos-container .card, .circulos-container .btn, .circulos-container .badge, .circulos-container input { border-radius: 0; }
8 .top-action-bar { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
9 .search-input { flex: 1; min-width: 250px; padding: 0.625rem 0.75rem; border: 1px solid var(--gray-300); background: white; font: inherit; font-size: 0.9rem; }
@@ -10,7 +13,7 @@
13 .circulos-container .btn-clean { display: inline-block; border: 1px solid var(--sidebar-bg); background: transparent; padding: 0.45rem 0.875rem; color: var(--sidebar-bg); font: inherit; font-size: 0.85rem; font-weight: 600; cursor: pointer; text-align: center; }
14 .circulos-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr); gap: 1.25rem; }
15 .section-title { margin-bottom: 0.875rem; font-size: 1.05rem; }
13 -.session-card { border-left: 4px solid var(--sidebar-bg); }
16 +.session-card { margin-bottom: 1.5rem; border-left: 4px solid var(--sidebar-bg); }
17 .session-card.live-session { border-left-color: var(--danger-color); }
18 .session-header, .session-footer { display: flex; justify-content: space-between; align-items: flex-start; gap: 0.75rem; }
19 .session-header { margin-bottom: 0.5rem; }
frontend/css/empresarial.css
+4 -1
@@ -1,11 +1,14 @@
1 /* MAPS Connect - Directorio de Semestre Empresarial */
2
3 .empresarial-container { width: 100%; max-width: 1050px; margin: 0 auto; }
4 +.module-heading { margin-bottom: 1.5rem; }
5 +.module-heading h1 { color: var(--sidebar-bg); font-size: 1.75rem; }
6 +.module-heading p { margin-top: 0.25rem; color: var(--gray-600); }
7 .empresarial-container .card, .empresarial-container .btn, .empresarial-container .badge, .empresarial-container input, .empresarial-container select { border-radius: 0; }
8 .filter-bar { display: grid; grid-template-columns: 2fr 1fr 1fr auto; gap: 0.625rem; margin-bottom: 1.5rem; }
9 .input-search, .select-filter { min-width: 0; padding: 0.625rem 0.75rem; border: 1px solid var(--gray-300); background: white; font: inherit; font-size: 0.9rem; }
10 .input-search:focus, .select-filter:focus { outline: none; border-color: var(--sidebar-bg); }
8 -.company-card { margin-bottom: 1.25rem; padding: 1.25rem; }
11 +.company-card { margin-bottom: 1.75rem; padding: 1.25rem; }
12 .company-header, .company-footer { display: flex; justify-content: space-between; gap: 1rem; }
13 .company-header { align-items: flex-start; margin-bottom: 0.75rem; }
14 .company-name { font-size: 1.2rem; }
frontend/css/layout.css
+33 -1
@@ -82,6 +82,11 @@
82 display: flex;
83 flex-direction: column;
84 min-width: 0;
85 + transition: margin-left 0.3s ease;
86 +}
87 +
88 +.app-layout.menu-open .main-wrapper {
89 + margin-left: var(--sidebar-width);
90 }
91
92 .topbar {
@@ -92,7 +97,7 @@
97 position: fixed;
98 top: 1rem;
99 left: 1rem;
95 - z-index: 90;
100 + z-index: 110;
101 width: 2.75rem;
102 height: 2.75rem;
103 border: none;
@@ -102,6 +107,25 @@
107 font-size: 1.25rem;
108 cursor: pointer;
109 box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.2);
110 + transition: left 0.3s ease;
111 +}
112 +
113 +.app-layout.menu-open .menu-toggle {
114 + left: calc(var(--sidebar-width) + 1rem);
115 +}
116 +
117 +.menu-backdrop {
118 + display: none;
119 + position: fixed;
120 + inset: 0;
121 + z-index: 80;
122 + border: 0;
123 + background: rgba(0, 0, 0, 0.35);
124 + cursor: pointer;
125 +}
126 +
127 +.app-layout.menu-open .menu-backdrop {
128 + display: block;
129 }
130
131 .main-content {
@@ -192,6 +216,14 @@
216 }
217
218 @media (max-width: 768px) {
219 + .app-layout.menu-open .main-wrapper {
220 + margin-left: 0;
221 + }
222 +
223 + .app-layout.menu-open .menu-toggle {
224 + left: 1rem;
225 + }
226 +
227 .main-content {
228 padding: 4.75rem 1rem 1rem;
229 }
frontend/js/layout.js
+12 -2
@@ -51,6 +51,7 @@ const Layout = {
51 if (!topbar) return;
52
53 topbar.innerHTML = `
54 + <button class="menu-backdrop" id="menu-backdrop" type="button" aria-label="Cerrar menú"></button>
55 <button class="menu-toggle" id="sidebar-toggle" type="button"
56 aria-label="Abrir menú" aria-controls="sidebar" aria-expanded="false">
57 <span aria-hidden="true">☰</span>
@@ -62,11 +63,20 @@ const Layout = {
63 bindToggle() {
64 const toggle = document.getElementById('sidebar-toggle');
65 const sidebar = document.getElementById('sidebar');
66 + const layout = document.querySelector('.app-layout');
67 + const backdrop = document.getElementById('menu-backdrop');
68
66 - toggle?.addEventListener('click', () => {
67 - const isOpen = sidebar?.classList.toggle('open') ?? false;
69 + const setMenuState = isOpen => {
70 + sidebar?.classList.toggle('open', isOpen);
71 + layout?.classList.toggle('menu-open', isOpen);
72 toggle.setAttribute('aria-expanded', String(isOpen));
73 toggle.setAttribute('aria-label', isOpen ? 'Cerrar menú' : 'Abrir menú');
74 + };
75 +
76 + toggle?.addEventListener('click', () => setMenuState(!sidebar?.classList.contains('open')));
77 + backdrop?.addEventListener('click', () => setMenuState(false));
78 + document.addEventListener('keydown', event => {
79 + if (event.key === 'Escape' && sidebar?.classList.contains('open')) setMenuState(false);
80 });
81 },
82
frontend/js/pages/circulos.js
+3 -2
@@ -31,14 +31,15 @@ async function loadCircles(content, search = '') {
31 function renderCircles(content, sessions, groups) {
32 content.innerHTML = `
33 <div class="circulos-container">
34 + <header class="module-heading"><h1>Círculos de estudio</h1><p>Encuentra sesiones de repaso y comunidades para aprender en conjunto.</p></header>
35 <form class="top-action-bar" id="circle-search-form">
36 <label class="visually-hidden" for="circle-search">Buscar círculo o sesión</label>
37 <input class="search-input" id="circle-search" name="busqueda" type="search" placeholder="Buscar círculo o sesión por materia...">
38 <button class="btn-solid" type="button" id="create-circle">+ Crear nuevo círculo</button>
39 </form>
40 <div class="circulos-grid">
40 - <section><h1 class="section-title">Próximas sesiones de repaso</h1>${sessions.length ? sessions.map(renderSession).join('') : renderEmptySessions()}</section>
41 - <aside><h1 class="section-title">Tus grupos permanentes</h1>${groups.length ? `<section class="card">${groups.map(renderGroup).join('')}</section>` : ''}<section class="card rules-card"><h2>Normas del círculo</h2><p>Los círculos son espacios colaborativos. Comparte material libre de plagio y mantén el respeto en las salas de videollamada.</p></section></aside>
41 + <section><h2 class="section-title">Próximas sesiones de repaso</h2>${sessions.length ? sessions.map(renderSession).join('') : renderEmptySessions()}</section>
42 + <aside><h2 class="section-title">Tus grupos permanentes</h2>${groups.length ? `<section class="card">${groups.map(renderGroup).join('')}</section>` : ''}<section class="card rules-card"><h2>Normas del círculo</h2><p>Los círculos son espacios colaborativos. Comparte material libre de plagio y mantén el respeto en las salas de videollamada.</p></section></aside>
43 </div>
44 </div>
45 `;
frontend/js/pages/empresarial.js
+3 -2
@@ -35,6 +35,7 @@ async function loadCompanies(content, filters = {}) {
35 function renderCompanies(content, companies) {
36 content.innerHTML = `
37 <div class="empresarial-container">
38 + <header class="module-heading"><h1>Semestre Empresarial</h1><p>Explora empresas vinculadas y experiencias compartidas por la comunidad.</p></header>
39 <form class="filter-bar" id="company-filter-form">
40 <label class="visually-hidden" for="company-search">Buscar empresa</label>
41 <input class="input-search" id="company-search" name="busqueda" type="search" placeholder="Buscar por empresa, tecnología o proyecto...">
@@ -64,7 +65,7 @@ function renderCompany(company) {
65 return `
66 <article class="card company-card">
67 <header class="company-header">
67 - <div><h1 class="company-name">${escapeHtml(company.nombre || 'Empresa')}</h1>${locationAndMode ? `<span class="company-location">${escapeHtml(locationAndMode)}</span>` : ''}</div>
68 + <div><h2 class="company-name">${escapeHtml(company.nombre || 'Empresa')}</h2>${locationAndMode ? `<span class="company-location">${escapeHtml(locationAndMode)}</span>` : ''}</div>
69 ${hasRating ? `<div class="rating-box"><span class="stars" aria-label="${rating} de 5 estrellas">${renderStars(rating)}</span><strong class="rating-number">${rating.toFixed(1)}</strong>${Number.isFinite(Number(company.totalResenas)) ? `<span class="rating-count">(${Number(company.totalResenas)} reseñas)</span>` : ''}</div>` : ''}
70 </header>
71 ${company.descripcion ? `<p class="company-desc"><strong>¿Qué hacen?</strong> ${escapeHtml(company.descripcion)}</p>` : ''}
@@ -81,7 +82,7 @@ function renderExperience(experience) {
82 }
83
84 function renderEmptyCompanies() {
84 - return '<section class="card empty-companies"><h1>Aún no hay empresas disponibles</h1><p>Las empresas vinculadas y las experiencias de estudiantes aparecerán aquí cuando se registren.</p></section>';
85 + return '<section class="card empty-companies"><h2>Aún no hay empresas disponibles</h2><p>Las empresas vinculadas y las experiencias de estudiantes aparecerán aquí cuando se registren.</p></section>';
86 }
87
88 function renderStars(rating) {