feat: sidebar con buscador de módulos e icono de configuración

- Icono de engranaje (⚙) en cabecera del sidebar → lab_configuracion.php
- Buscador de texto que filtra ítems del menú en tiempo real
- Al borrar la búsqueda restaura el estado colapsado guardado
- CSS integrado en el mismo componente

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-23 23:07:58 -05:00
co-authored by Claude Sonnet 4.6
parent 9c9acbed70
commit 892b01493a
+104 -2
View File
@@ -103,8 +103,20 @@ $_base = defined('APP_URL') ? rtrim(APP_URL, '/') : '';
<nav class="sidebar" id="erp-sidebar">
<div class="sidebar-header">
<h4><i class="<?= htmlspecialchars($_sidebar_icon, ENT_QUOTES, 'UTF-8') ?>"></i>
<?= htmlspecialchars($_sidebar_title, ENT_QUOTES, 'UTF-8') ?></h4>
<h4 style="flex:1;min-width:0">
<i class="<?= htmlspecialchars($_sidebar_icon, ENT_QUOTES, 'UTF-8') ?>"></i>
<?= htmlspecialchars($_sidebar_title, ENT_QUOTES, 'UTF-8') ?>
</h4>
<a href="<?= $_base ?>/lab_configuracion.php" class="sb-config-btn" title="Configuración">
<i class="fas fa-gear"></i>
</a>
</div>
<!-- Buscador de menú -->
<div class="sb-search-wrap">
<i class="fas fa-search sb-search-icon"></i>
<input type="text" id="sbSearch" class="sb-search" placeholder="Buscar módulo…"
oninput="sbFilter(this.value)" autocomplete="off">
</div>
<ul class="sidebar-menu" id="sb-menu">
@@ -211,6 +223,48 @@ if (class_exists('ModuleRegistry', false)) {
</nav>
<style>
/* ── Botón config en header ─────────────────────────── */
.sidebar-header { display:flex; align-items:center; gap:10px; }
.sb-config-btn {
color: rgba(255,255,255,.65);
font-size: 1rem;
padding: 4px 6px;
border-radius: 6px;
transition: color .15s, background .15s;
flex-shrink: 0;
line-height: 1;
}
.sb-config-btn:hover { color:#fff; background:rgba(255,255,255,.15); }
/* ── Buscador de menú ───────────────────────────────── */
.sb-search-wrap {
position: relative;
padding: 10px 14px 6px;
flex-shrink: 0;
}
.sb-search-icon {
position: absolute;
left: 24px;
top: 50%;
transform: translateY(-30%);
color: rgba(255,255,255,.45);
font-size: .75rem;
pointer-events: none;
}
.sb-search {
width: 100%;
background: rgba(255,255,255,.12);
border: 1px solid rgba(255,255,255,.18);
border-radius: 8px;
color: #fff;
padding: 6px 10px 6px 28px;
font-size: .8rem;
outline: none;
transition: background .2s, border-color .2s;
}
.sb-search::placeholder { color: rgba(255,255,255,.45); }
.sb-search:focus { background: rgba(255,255,255,.2); border-color: rgba(255,255,255,.4); }
/* ── Acordeón de categorías ─────────────────────────── */
.sidebar-section {
display: flex;
@@ -285,6 +339,54 @@ if (class_exists('ModuleRegistry', false)) {
setCollapsed(arr);
};
// ── Buscador de menú ─────────────────────────────────────────
window.sbFilter = function(q) {
q = q.trim().toLowerCase();
var menu = document.getElementById('sb-menu');
if (!menu) return;
if (!q) {
// Restaurar estado guardado
menu.querySelectorAll('.sb-group').forEach(function(g) {
g.style.display = '';
});
menu.querySelectorAll('.sidebar-section').forEach(function(s) {
s.style.display = '';
});
// Re-aplicar estado colapsado
var collapsed = getCollapsed();
menu.querySelectorAll('.sb-group').forEach(function(group) {
var section = document.querySelector('[data-cat="' + group.id + '"]');
var hasActive = !!group.querySelector('.nav-link.active');
var isCol = !hasActive && collapsed.indexOf(group.id) !== -1;
if (section) applyState(group, section, isCol, false);
});
return;
}
// Mostrar todos expandidos y filtrar por texto
menu.querySelectorAll('.sb-group').forEach(function(g) {
var links = g.querySelectorAll('.nav-link');
var anyMatch = false;
links.forEach(function(a) {
var txt = a.textContent.toLowerCase();
var show = txt.includes(q);
a.parentElement.style.display = show ? '' : 'none';
if (show) anyMatch = true;
});
// Mostrar/ocultar grupo completo
g.style.display = anyMatch ? '' : 'none';
g.style.maxHeight = '2000px';
g.style.opacity = '1';
g.classList.remove('collapsed');
var section = document.querySelector('[data-cat="' + g.id + '"]');
if (section) {
section.style.display = anyMatch ? '' : 'none';
section.classList.remove('collapsed');
}
});
};
// Inicializar al cargar
document.addEventListener('DOMContentLoaded', function () {
var collapsed = getCollapsed();