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) {