feat: convert sidebar to collapsible menu

Daniel Silva committed Aug 19, 2026 at 15:25 UTC f15d0e2f514d59ebfbccaaec8c5275bc55a4ce77
2 files changed +70 -38
frontend/css/layout.css
+52 -30
@@ -9,8 +9,20 @@
9 width: var(--sidebar-width);
10 background: var(--sidebar-bg);
11 color: var(--sidebar-text-hover);
12 - flex-shrink: 0;
12 + position: fixed;
13 + top: 0;
14 + left: 0;
15 + height: 100vh;
16 + z-index: 100;
17 + display: flex;
18 + flex-direction: column;
19 + transform: translateX(-100%);
20 transition: transform 0.3s ease;
21 + box-shadow: 0 0 1.25rem rgba(0, 0, 0, 0.2);
22 +}
23 +
24 +.sidebar.open {
25 + transform: translateX(0);
26 }
27
28 .sidebar-header {
@@ -31,6 +43,8 @@
43 .sidebar-nav {
44 list-style: none;
45 padding: 1rem 0;
46 + overflow-y: auto;
47 + flex: 1;
48 }
49
50 .sidebar-nav a {
@@ -54,6 +68,15 @@
68 text-decoration: none;
69 }
70
71 +.sidebar-footer {
72 + padding: 1rem 1.25rem;
73 + border-top: 1px solid var(--sidebar-border);
74 +}
75 +
76 +.sidebar-footer .btn {
77 + width: 100%;
78 +}
79 +
80 .main-wrapper {
81 flex: 1;
82 display: flex;
@@ -62,31 +85,43 @@
85 }
86
87 .topbar {
65 - height: var(--header-height);
66 - background: var(--topbar-bg);
67 - color: var(--topbar-text);
68 - border-bottom: 1px solid var(--gray-200);
69 - display: flex;
70 - align-items: center;
71 - justify-content: space-between;
72 - padding: 0 1.5rem;
88 + height: 0;
89 }
90
75 -.topbar-toggle {
76 - display: none;
77 - background: none;
91 +.menu-toggle {
92 + position: fixed;
93 + top: 1rem;
94 + left: 1rem;
95 + z-index: 90;
96 + width: 2.75rem;
97 + height: 2.75rem;
98 border: none;
79 - font-size: 1.5rem;
99 + border-radius: 50%;
100 + background: var(--sidebar-bg);
101 + color: var(--sidebar-text-hover);
102 + font-size: 1.25rem;
103 cursor: pointer;
81 - color: var(--topbar-text);
104 + box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.2);
105 }
106
107 .main-content {
108 flex: 1;
86 - padding: 1.5rem;
109 + padding: 4.75rem 1.5rem 1.5rem;
110 overflow-y: auto;
111 }
112
113 +.visually-hidden {
114 + position: absolute;
115 + width: 1px;
116 + height: 1px;
117 + padding: 0;
118 + margin: -1px;
119 + overflow: hidden;
120 + clip: rect(0, 0, 0, 0);
121 + white-space: nowrap;
122 + border: 0;
123 +}
124 +
125 .auth-layout {
126 min-height: 100vh;
127 display: flex;
@@ -157,20 +192,7 @@
192 }
193
194 @media (max-width: 768px) {
160 - .sidebar {
161 - position: fixed;
162 - top: 0;
163 - left: 0;
164 - height: 100vh;
165 - z-index: 100;
166 - transform: translateX(-100%);
167 - }
168 -
169 - .sidebar.open {
170 - transform: translateX(0);
171 - }
172 -
173 - .topbar-toggle {
174 - display: block;
195 + .main-content {
196 + padding: 4.75rem 1rem 1rem;
197 }
198 }
frontend/js/layout.js
+18 -8
@@ -1,5 +1,5 @@
1 /**
2 - * MAPS Connect - Layout compartido (sidebar + topbar)
2 + * MAPS Connect - Layout compartido (menú lateral plegable)
3 */
4
5 const NAV_ITEMS = [
@@ -38,7 +38,12 @@ const Layout = {
38 <small>${user?.nombre || 'Usuario'}</small>
39 </div>
40 <ul class="sidebar-nav">${navLinks}</ul>
41 + <div class="sidebar-footer">
42 + <button class="btn btn-sm btn-outline" id="btn-logout">Cerrar sesión</button>
43 + </div>
44 `;
45 +
46 + document.getElementById('btn-logout')?.addEventListener('click', () => Auth.logout());
47 },
48
49 renderTopbar() {
@@ -46,17 +51,22 @@ const Layout = {
51 if (!topbar) return;
52
53 topbar.innerHTML = `
49 - <button class="topbar-toggle" id="sidebar-toggle" aria-label="Menú">Menú</button>
50 - <span id="topbar-title"></span>
51 - <button class="btn btn-sm btn-outline" id="btn-logout">Cerrar sesión</button>
54 + <button class="menu-toggle" id="sidebar-toggle" type="button"
55 + aria-label="Abrir menú" aria-controls="sidebar" aria-expanded="false">
56 + <span aria-hidden="true">☰</span>
57 + </button>
58 + <span id="topbar-title" class="visually-hidden"></span>
59 `;
53 -
54 - document.getElementById('btn-logout')?.addEventListener('click', () => Auth.logout());
60 },
61
62 bindToggle() {
58 - document.getElementById('sidebar-toggle')?.addEventListener('click', () => {
59 - document.getElementById('sidebar')?.classList.toggle('open');
63 + const toggle = document.getElementById('sidebar-toggle');
64 + const sidebar = document.getElementById('sidebar');
65 +
66 + toggle?.addEventListener('click', () => {
67 + const isOpen = sidebar?.classList.toggle('open') ?? false;
68 + toggle.setAttribute('aria-expanded', String(isOpen));
69 + toggle.setAttribute('aria-label', isOpen ? 'Cerrar menú' : 'Abrir menú');
70 });
71 },
72