Agrupar el menú principal y acercar el ribbon al mockup.

El menú queda en una sola fila con iconos, desplegables por módulo y
sin wrap; el ribbon se compacta como tarjeta de acciones.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
This commit is contained in:
Cursor Agent 2026-09-08 16:41:33 +00:00
parent 496273b41c
commit 1e1c3b21b8
No known key found for this signature in database
4 changed files with 256 additions and 23 deletions

View file

@ -27,6 +27,7 @@ html, body, #__nuxt {
min-height: 44px;
padding: 0 8px !important;
flex-shrink: 0;
flex-wrap: nowrap;
}
.menubar-link {
display: flex;
@ -50,9 +51,8 @@ html, body, #__nuxt {
box-shadow: inset 0 -2px 0 #1c4a7a;
}
.ribbon.p-toolbar {
border-radius: 0 !important;
border: none !important;
border-bottom: 1px solid #d5dde6 !important;
border-radius: 8px !important;
border: 1px solid #e1e6ed !important;
background: #fff !important;
padding: 4px 10px 6px !important;
flex-shrink: 0;

View file

@ -782,7 +782,7 @@ html, body, #__nuxt {
padding-inline: 16px;
font-size: 13px;
}
.menubar-link > i { display: none; }
.menubar-link > i { display: inline-flex; }
.menubar-link.is-active {
box-shadow: inset 0 -3px 0 #2f6fe4;
}
@ -2236,3 +2236,188 @@ html.dark .p-dialog .p-dialog-header-icon {
.cost-warn { background: #fff8e1 !important; color: #e65100 !important; }
.cost-critical { background: #ffebee !important; color: #b42318 !important; }
.cost-critical-text { color: #b42318; font-weight: 600; }
/* Grouped menubar: single row, icons, card-style submenus. */
.menubar.p-menubar {
display: flex;
flex-wrap: nowrap;
align-items: center;
min-height: 52px;
height: 52px;
overflow: visible;
}
.brand-lockup {
height: 52px;
min-width: 148px;
padding-right: 18px;
}
.menubar .p-menubar-start,
.menubar .p-menubar-end {
flex: 0 0 auto;
}
.menubar .p-menubar-root-list {
display: flex;
flex-wrap: nowrap !important;
align-items: stretch;
min-width: 0;
flex: 1 1 auto;
gap: 2px;
margin: 0;
overflow: visible;
}
.menubar .p-menubar-item {
flex: 0 0 auto;
}
.menubar-link {
min-height: 52px;
height: 52px;
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: 7px;
padding-inline: 11px;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
color: #243044;
}
.menubar-link > i {
display: inline-flex !important;
font-size: 13px;
color: #4b6380;
}
.menubar-link .menubar-chevron {
margin-left: 1px;
font-size: 10px;
opacity: .7;
}
.menubar-link.is-active {
color: #1f5fd1;
box-shadow: inset 0 -3px 0 #2f6fe4;
}
.menubar-link.is-active > i,
.menubar-link.is-active .menubar-chevron {
color: #2f6fe4;
}
.menubar .p-menubar-submenu {
min-width: 228px;
padding: 6px;
margin-top: 0;
border: 1px solid #e1e6ed;
border-radius: 8px;
background: #fff;
box-shadow: 0 10px 28px rgba(23, 32, 51, .12);
}
.menubar .p-menubar-submenu .p-menubar-item-content {
border-radius: 6px;
}
.menubar .p-menubar-submenu .menubar-link {
width: 100%;
min-height: 38px;
height: 38px;
padding: 0 10px;
font-weight: 500;
border-radius: 6px;
box-shadow: none !important;
}
.menubar .p-menubar-submenu .menubar-link:hover {
background: #f4f7fb;
}
.menubar .p-menubar-submenu .menubar-link.is-active {
background: #eaf2ff;
color: #174fba;
box-shadow: none !important;
}
/* Ribbon as compact action card (mockup). */
.ribbon.p-toolbar {
display: flex;
align-items: stretch;
min-height: 88px;
margin: 8px 8px 10px;
padding: 8px 12px 6px !important;
overflow-x: auto;
overflow-y: hidden;
border: 1px solid var(--shell-border, #e1e6ed) !important;
border-radius: 8px !important;
background: #fff !important;
box-shadow: var(--shell-shadow, 0 2px 9px rgba(23, 32, 51, .055));
}
.ribbon .p-toolbar-start {
display: flex;
flex-wrap: nowrap;
align-items: stretch;
gap: 0;
min-width: 0;
}
.ribbon-cluster {
flex: 0 0 auto;
align-items: stretch;
}
.ribbon-group {
gap: 4px;
}
.ribbon-group-items {
display: flex;
flex-wrap: nowrap;
gap: 4px;
}
.ribbon-group-label {
text-transform: none;
letter-spacing: .02em;
font-size: 10px;
color: #748095;
}
.ribbon .ribbon-action {
min-width: 64px;
min-height: 64px;
padding: 8px 10px 6px;
border-radius: 6px;
}
.ribbon .ribbon-action .p-button-icon {
width: 22px;
height: 22px;
margin-bottom: 4px;
font-size: 20px !important;
line-height: 22px;
color: #4b6380;
}
.ribbon .ribbon-action .p-button-label {
font-size: 12px;
font-weight: 500;
line-height: 1.2;
}
.ribbon .ribbon-action--alta .p-button-icon {
color: #2f6fe4;
}
.ribbon .ribbon-action:not(:disabled):hover {
background: #f4f7fb;
}
.ribbon .p-divider-vertical {
min-height: 64px;
margin-inline: 8px;
}
html.dark .menubar .p-menubar-submenu {
background: #111b2e !important;
border-color: #243044 !important;
box-shadow: 0 10px 28px rgba(0, 0, 0, .4);
}
html.dark .menubar .p-menubar-submenu .menubar-link {
color: #f1f5f9 !important;
}
html.dark .menubar .p-menubar-submenu .menubar-link:hover {
background: rgba(47, 111, 228, 0.14) !important;
}
html.dark .menubar .p-menubar-submenu .menubar-link.is-active {
background: rgba(47, 111, 228, 0.22) !important;
color: #93c5fd !important;
}
html.dark .menubar-link > i,
html.dark .menubar-link .menubar-chevron {
color: #94a3b8;
}
html.dark .menubar-link.is-active > i,
html.dark .menubar-link.is-active .menubar-chevron {
color: #93c5fd;
}

View file

@ -1,5 +1,5 @@
<template>
<Toolbar class="ribbon">
<Toolbar v-if="ribbon.length" class="ribbon">
<template #start>
<div v-for="(group, i) in ribbon" :key="group.key" class="ribbon-cluster">
<Divider v-if="i > 0" layout="vertical" />

View file

@ -1,6 +1,6 @@
<template>
<div class="shell">
<Menubar class="menubar" :model="menu">
<Menubar class="menubar" :model="menu" breakpoint="1100px">
<template #start>
<button type="button" class="brand-lockup" aria-label="Ir al inicio" @click="navigateTo('/')">
<span class="brand-mark" aria-hidden="true">
@ -9,10 +9,19 @@
<span class="brand-word">{{ brand }}</span>
</button>
</template>
<template #item="{ item, props }">
<a v-bind="props.action" class="menubar-link" :class="{ 'is-active': isActive(String(item.to || '')) }">
<i v-if="item.icon" :class="item.icon" />
<template #item="{ item, props, hasSubmenu, root }">
<a
v-bind="props.action"
class="menubar-link"
:class="{
'is-active': itemIsActive(item),
'is-root': root !== false,
'has-submenu': Boolean(hasSubmenu || item.items?.length),
}"
>
<i v-if="item.icon" :class="item.icon" aria-hidden="true" />
<span>{{ item.label }}</span>
<i v-if="hasSubmenu || item.items?.length" class="pi pi-angle-down menubar-chevron" aria-hidden="true" />
</a>
</template>
<template #end>
@ -140,20 +149,53 @@ const profileItems = computed(() => [
const brand = computed(() => "PANELS");
const menu = [
{ label: "Inicio", icon: "pi pi-home", to: "/", command: () => navigateTo("/") },
{ label: "Proyectos", icon: "pi pi-building", to: "/proyectos", command: () => navigateTo("/proyectos") },
{ label: "Presupuesto", icon: "pi pi-calculator", to: "/presupuesto", command: () => navigateTo("/presupuesto") },
{ label: "Programa de obra", icon: "pi pi-calendar", to: "/programa-obra", command: () => navigateTo("/programa-obra") },
{ label: "Control costos", icon: "pi pi-chart-bar", to: "/control-presupuesto", command: () => navigateTo("/control-presupuesto") },
{ label: "Gastos", icon: "pi pi-money-bill", to: "/gastos", command: () => navigateTo("/gastos") },
{ label: "Almacén", icon: "pi pi-box", to: "/almacen", command: () => navigateTo("/almacen") },
{ label: "Padrón", icon: "pi pi-users", to: "/padron", command: () => navigateTo("/padron") },
{ label: "Kanban", icon: "pi pi-th-large", to: "/kanban", command: () => navigateTo("/kanban") },
{ label: "Gafetes", icon: "pi pi-id-card", to: "/gafetes", command: () => navigateTo("/gafetes") },
{ label: "Nómina", icon: "pi pi-wallet", to: "/nomina", command: () => navigateTo("/nomina") },
{ label: "Usuarios", icon: "pi pi-shield", to: "/usuarios", command: () => navigateTo("/usuarios") },
{ label: "Ayuda", icon: "pi pi-question-circle", command: showHelp },
type NavItem = {
label: string;
icon?: string;
to?: string;
command?: () => void;
items?: NavItem[];
};
function nav(label: string, icon: string, to: string): NavItem {
return { label, icon, to, command: () => navigateTo(to) };
}
const menu: NavItem[] = [
nav("Inicio", "pi pi-home", "/"),
{
label: "Obra",
icon: "pi pi-building",
items: [
nav("Proyectos", "pi pi-building", "/proyectos"),
nav("Presupuesto", "pi pi-calculator", "/presupuesto"),
nav("Programa de obra", "pi pi-calendar", "/programa-obra"),
nav("Control costos", "pi pi-chart-bar", "/control-presupuesto"),
],
},
{
label: "Operación",
icon: "pi pi-briefcase",
items: [
nav("Gastos", "pi pi-money-bill", "/gastos"),
nav("Almacén", "pi pi-box", "/almacen"),
nav("Kanban", "pi pi-th-large", "/kanban"),
],
},
{
label: "Personal",
icon: "pi pi-users",
items: [
nav("Padrón", "pi pi-users", "/padron"),
nav("Gafetes", "pi pi-id-card", "/gafetes"),
nav("Nómina", "pi pi-wallet", "/nomina"),
],
},
{
label: "Administración",
icon: "pi pi-shield",
items: [nav("Usuarios", "pi pi-shield", "/usuarios")],
},
];
function isActive(to: string) {
@ -162,6 +204,12 @@ function isActive(to: string) {
return route.path === to || route.path.startsWith(`${to}/`);
}
function itemIsActive(item: NavItem | undefined): boolean {
if (!item) return false;
if (item.to && isActive(item.to)) return true;
return Boolean(item.items?.some((child) => itemIsActive(child)));
}
function goSearch() {
navigateTo("/padron");
}