fix: sidebar — logo fijo en header y colapso de módulos funcional

Logo:
- Carga doc_logo_base64 y empresa_nombre desde lab_config
- Header muestra logo (filtro invert para fondo oscuro) o ícono fallback
- Nombre del laboratorio siempre visible debajo, nunca cambia por página
- Ícono de configuración reposicionado en esquina superior derecha

Colapso de módulos:
- Bug: transición CSS se disparaba durante init causando estado inconsistente
- Fix: inyecta <style> temporal con transition:none antes de aplicar estados,
  lo remueve en el siguiente frame para que las animaciones funcionen luego

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-23 23:55:20 -05:00
co-authored by Claude Sonnet 4.6
parent 86cfad6e68
commit 6b6389c583
2 changed files with 88 additions and 44 deletions
-13
View File
@@ -89,8 +89,6 @@ body {
/* ── Cabecera ─────────────────────────────────────────── */
.sidebar-header {
padding: 20px 20px 16px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.15);
backdrop-filter: blur(4px);
@@ -99,17 +97,6 @@ body {
flex-shrink: 0;
}
.sidebar-header h4 {
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.02em;
margin: 0;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── Lista de menú ────────────────────────────────────── */
.sidebar-menu {
list-style: none;
+88 -31
View File
@@ -26,23 +26,33 @@ if (!class_exists('ModuleRegistry', false)) {
}
}
// ── Brand color desde DB (si no fue inyectado ya por Layout.php) ─────────────
// ── Brand color + logo desde DB ──────────────────────────────────────────────
if (!defined('_SIDEBAR_BRAND_INJECTED')) {
define('_SIDEBAR_BRAND_INJECTED', true);
$_sb_brand = '#1565c0';
$_sb_brand_dark = '#0d47a1';
$_sb_logo = '';
$_sb_lab_nombre = '';
try {
if (class_exists('Database', false)) {
$_sb_row = Database::getInstance()->fetch("SELECT valor FROM lab_config WHERE clave = 'doc_color'");
if ($_sb_row && preg_match('/^#[0-9a-fA-F]{3,8}$/', $_sb_row['valor'])) {
$_sb_brand = $_sb_row['valor'];
$_sb_hex = ltrim($_sb_brand, '#');
$_sb_brand_dark = sprintf('#%02x%02x%02x',
max(0, hexdec(substr($_sb_hex, 0, 2)) - 40),
max(0, hexdec(substr($_sb_hex, 2, 2)) - 40),
max(0, hexdec(substr($_sb_hex, 4, 2)) - 40)
);
$__cfgs = Database::getInstance()->fetchAll(
"SELECT clave, valor FROM lab_config WHERE clave IN ('doc_color','doc_logo_base64','empresa_nombre')"
);
foreach ($__cfgs as $__r) {
if ($__r['clave'] === 'doc_color' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $__r['valor'])) {
$_sb_brand = $__r['valor'];
} elseif ($__r['clave'] === 'doc_logo_base64' && !empty($__r['valor'])) {
$_sb_logo = $__r['valor'];
} elseif ($__r['clave'] === 'empresa_nombre') {
$_sb_lab_nombre = $__r['valor'];
}
}
$_sb_hex = ltrim($_sb_brand, '#');
$_sb_brand_dark = sprintf('#%02x%02x%02x',
max(0, hexdec(substr($_sb_hex, 0, 2)) - 40),
max(0, hexdec(substr($_sb_hex, 2, 2)) - 40),
max(0, hexdec(substr($_sb_hex, 4, 2)) - 40)
);
}
} catch (\Throwable $_) {}
echo "<style>:root{--brand:{$_sb_brand};--brand-dark:{$_sb_brand_dark}}</style>\n";
@@ -75,9 +85,7 @@ $_isActive = function (string $route) use ($_current_script): string {
return $routeBase === $_current_script ? ' active' : '';
};
// Título del sidebar: prioridad al nombre definido por el módulo, luego el genérico
$_sidebar_title = $SIDEBAR_TITLE ?? $GLOBALS['_LAYOUT_TITLE'] ?? 'Panel ERP';
$_sidebar_icon = $SIDEBAR_ICON ?? $GLOBALS['_LAYOUT_ICON'] ?? 'fas fa-th-large';
$_sb_display_name = $_sb_lab_nombre ?: 'Panel ERP';
// Nombre del usuario para el pie del sidebar
$_user_name = $_SESSION['admin_user']['full_name']
@@ -103,10 +111,15 @@ $_base = defined('APP_URL') ? rtrim(APP_URL, '/') : '';
<nav class="sidebar" id="erp-sidebar">
<div class="sidebar-header">
<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>
<?php if ($_sb_logo): ?>
<img src="<?= htmlspecialchars($_sb_logo, ENT_QUOTES, 'UTF-8') ?>"
alt="Logo" class="sb-logo-img">
<?php else: ?>
<div class="sb-logo-fallback">
<i class="fas fa-flask"></i>
</div>
<?php endif; ?>
<div class="sb-brand-name"><?= htmlspecialchars($_sb_display_name, ENT_QUOTES, 'UTF-8') ?></div>
<a href="<?= $_base ?>/lab_configuracion.php" class="sb-config-btn" title="Configuración">
<i class="fas fa-gear"></i>
</a>
@@ -223,15 +236,50 @@ if (class_exists('ModuleRegistry', false)) {
</nav>
<style>
/* ── Botón config en header ─────────────────────────── */
.sidebar-header { display:flex; align-items:center; gap:10px; }
/* ── Header con logo ────────────────────────────────── */
.sidebar-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 18px 16px 14px;
position: relative;
}
.sb-logo-img {
max-width: 120px;
max-height: 60px;
object-fit: contain;
filter: brightness(0) invert(1);
opacity: .92;
}
.sb-logo-fallback {
width: 52px; height: 52px;
border-radius: 14px;
background: rgba(255,255,255,.18);
display: flex; align-items: center; justify-content: center;
font-size: 1.5rem; color: #fff;
}
.sb-brand-name {
font-size: .72rem;
font-weight: 700;
letter-spacing: .04em;
color: rgba(255,255,255,.80);
text-align: center;
line-height: 1.3;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* config en esquina superior derecha */
.sb-config-btn {
color: rgba(255,255,255,.65);
font-size: 1rem;
padding: 4px 6px;
position: absolute;
top: 10px; right: 10px;
color: rgba(255,255,255,.55);
font-size: .9rem;
padding: 5px 7px;
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); }
@@ -387,23 +435,32 @@ if (class_exists('ModuleRegistry', false)) {
});
};
// Inicializar al cargar
// Inicializar al cargar — sin animación para evitar flash
document.addEventListener('DOMContentLoaded', function () {
var collapsed = getCollapsed();
// Deshabilitar transición durante init
var style = document.createElement('style');
style.id = '_sb_notrans';
style.textContent = '.sb-group { transition: none !important; }';
document.head.appendChild(style);
document.querySelectorAll('.sb-group').forEach(function(group) {
var catId = group.id;
var section = document.querySelector('[data-cat="' + catId + '"]');
var catId = group.id;
var section = document.querySelector('[data-cat="' + catId + '"]');
if (!section) return;
var hasActive = !!group.querySelector('.nav-link.active');
var hasActive = !!group.querySelector('.nav-link.active');
var isCollapsed = !hasActive && collapsed.indexOf(catId) !== -1;
// Medir altura real antes de colapsar
// Altura real (funciona incluso con overflow:hidden)
group.style.maxHeight = group.scrollHeight + 'px';
group.style.opacity = '1';
applyState(group, section, isCollapsed, false);
});
// Reactivar transición en el próximo frame
requestAnimationFrame(function() {
var el = document.getElementById('_sb_notrans');
if (el) el.remove();
});
});
})();
</script>