feat: añadir funcionalidad de colapso y expansión en la barra lateral

This commit is contained in:
David Arranz 2026-10-08 17:16:59 +02:00
parent f37cec6d50
commit 1f21a605cc
5 changed files with 150 additions and 18 deletions

View File

@ -12,6 +12,7 @@ body {
:root { :root {
--desk-app-background: #000; --desk-app-background: #000;
--desk-content-background: #f7f9fb; --desk-content-background: #f7f9fb;
--desk-header-background: #fff;
--desk-sidebar-background: #0e282c; --desk-sidebar-background: #0e282c;
--desk-sidebar-top: #0c252a; --desk-sidebar-top: #0c252a;
--desk-sidebar-bottom: #102c30; --desk-sidebar-bottom: #102c30;
@ -27,6 +28,8 @@ body {
--desk-radius-md: 10px; --desk-radius-md: 10px;
--desk-radius-shell: 20px; --desk-radius-shell: 20px;
--desk-sidebar-width: 228px; --desk-sidebar-width: 228px;
--desk-sidebar-inset: 20px;
--desk-sidebar-collapsed-width: 72px;
--desk-header-height: 88px; --desk-header-height: 88px;
--desk-search-height: 41px; --desk-search-height: 41px;
--desk-nav-height: 48px; --desk-nav-height: 48px;
@ -59,9 +62,12 @@ body:has(.desk-workspace) {
background: var(--desk-app-background); background: var(--desk-app-background);
} }
.desk-sidebar { .desk-sidebar {
display: flex;
flex-direction: column;
min-height: 0;
width: var(--desk-sidebar-width); width: var(--desk-sidebar-width);
flex: 0 0 var(--desk-sidebar-width); flex: 0 0 var(--desk-sidebar-width);
overflow-y: auto; overflow: hidden;
background: linear-gradient( background: linear-gradient(
180deg, 180deg,
var(--desk-sidebar-top), var(--desk-sidebar-top),
@ -71,6 +77,7 @@ body:has(.desk-workspace) {
color: var(--desk-sidebar-text); color: var(--desk-sidebar-text);
} }
.desk-brand { .desk-brand {
flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 13px; gap: 13px;
@ -88,10 +95,13 @@ body:has(.desk-workspace) {
flex-shrink: 0; flex-shrink: 0;
} }
.desk-company-rail { .desk-company-rail {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 2px; gap: 2px;
padding: 28px 13px 24px 26px; padding: 28px var(--desk-sidebar-inset) 24px;
} }
.desk-space-button { .desk-space-button {
width: 100%; width: 100%;
@ -137,7 +147,7 @@ body:has(.desk-workspace) {
.desk-rail-separator { .desk-rail-separator {
height: 1px; height: 1px;
background: rgb(255 255 255 / 12%); background: rgb(255 255 255 / 12%);
margin: 26px -3px 25px -5px; margin: 26px 0 25px;
flex-shrink: 0; flex-shrink: 0;
} }
.desk-catalog-navigation { .desk-catalog-navigation {
@ -156,6 +166,58 @@ body:has(.desk-workspace) {
background: rgb(255 255 255 / 8%); background: rgb(255 255 255 / 8%);
color: white; color: white;
} }
.desk-sidebar-footer {
flex-shrink: 0;
display: grid;
gap: 2px;
padding: 12px var(--desk-sidebar-inset) 20px;
border-top: 1px solid rgb(255 255 255 / 12%);
}
.desk-sidebar-toggle[aria-expanded="true"] > svg {
transform: rotate(180deg);
}
.desk-catalog-navigation > button {
gap: 10px;
}
.desk-catalog-navigation > button > svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
@media (min-width: 768px) {
.desk-sidebar[data-collapsed="true"] {
width: var(--desk-sidebar-collapsed-width);
flex-basis: var(--desk-sidebar-collapsed-width);
}
.desk-sidebar[data-collapsed="true"] .desk-brand {
justify-content: center;
padding-left: 0;
}
.desk-sidebar[data-collapsed="true"] .desk-brand > span,
.desk-sidebar[data-collapsed="true"] .desk-space-button > span,
.desk-sidebar[data-collapsed="true"] .desk-nav-chevron,
.desk-sidebar[data-collapsed="true"] .desk-catalog-navigation > button > span {
display: none;
}
.desk-sidebar[data-collapsed="true"] .desk-company-rail {
padding: 28px 12px 12px;
}
.desk-sidebar[data-collapsed="true"] .desk-sidebar-footer {
padding: 12px 12px 20px;
}
.desk-sidebar[data-collapsed="true"] .desk-space-button {
justify-content: center;
gap: 0;
padding: 0;
}
.desk-sidebar[data-collapsed="true"] .desk-catalog-navigation {
padding-left: 0;
}
.desk-sidebar[data-collapsed="true"] .desk-catalog-navigation > button {
justify-content: center;
padding-inline: 0;
}
}
.desk-workspace-content { .desk-workspace-content {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@ -167,7 +229,9 @@ body:has(.desk-workspace) {
align-items: center; align-items: center;
gap: 28px; gap: 28px;
min-height: var(--desk-header-height); min-height: var(--desk-header-height);
padding: 31px 30px 16px 17px; padding: 15px 30px 15px 17px;
background: var(--desk-header-background);
border-bottom: 1px solid var(--desk-border);
color: var(--desk-navy); color: var(--desk-navy);
} }
.desk-search { .desk-search {
@ -561,7 +625,7 @@ body:has(.desk-workspace) {
flex-basis: auto; flex-basis: auto;
} }
.desk-brand { .desk-brand {
padding: 20px; padding: 2px;
} }
.desk-company-rail { .desk-company-rail {
flex-direction: row; flex-direction: row;
@ -645,3 +709,16 @@ body:has(.desk-workspace) {
padding: 12px; padding: 12px;
} }
} }
@media (max-width: 767px) {
.desk-company-rail {
flex: none;
overflow-y: visible;
}
.desk-sidebar-footer {
padding: 10px 16px;
}
.desk-sidebar-toggle {
display: none;
}
}

View File

@ -155,3 +155,13 @@ servicio de notificaciones. El perfil usa disclosure nativo con foco visible y
conserva las acciones de identidad y selección de idioma. No hay primitives Avatar conserva las acciones de identidad y selección de idioma. No hay primitives Avatar
ni DropdownMenu instalados; el avatar muestra iniciales del usuario real, sin ni DropdownMenu instalados; el avatar muestra iniciales del usuario real, sin
inventar foto ni cargo. Configuración abre un FormDialog con las acciones existentes. inventar foto ni cargo. Configuración abre un FormDialog con las acciones existentes.
La sidebar global permite contraerse en escritorio de 228px a 72px mediante
«Contraer menú» / «Expandir menú». Conserva iconos, nombres accesibles y títulos
de ayuda también para compañías y subcatálogos. La navegación se desplaza por
separado, manteniendo Configuración y el control de anchura en el pie visible.
En móvil se conserva la navegación horizontal existente.
El menú expandido y su pie comparten márgenes horizontales de 20px mediante
`--desk-sidebar-inset`. El header global usa `--desk-header-background` (#FFFFFF),
con un borde inferior de 1px en `--desk-border`, sobre el contenido #F7F9FB.

View File

@ -1,14 +1,19 @@
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { import {
BoxIcon,
Building2Icon, Building2Icon,
ChevronRightIcon, ChevronRightIcon,
DatabaseIcon, DatabaseIcon,
HomeIcon, HomeIcon,
KeyRoundIcon,
LayersIcon,
LayoutTemplateIcon,
SettingsIcon, SettingsIcon,
TicketIcon, TicketIcon,
WrenchIcon, WrenchIcon,
} from "@repo/shadcn-ui/lib/icons"; } from "@repo/shadcn-ui/lib/icons";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { WorkspaceContent } from "./workspace-content"; import type { WorkspaceContent } from "./workspace-content";
@ -35,9 +40,10 @@ export function WorkspaceSidebar({
const { t: resourceTranslation } = useTranslation("resources"); const { t: resourceTranslation } = useTranslation("resources");
const { t: assetTranslation } = useTranslation("assets"); const { t: assetTranslation } = useTranslation("assets");
const { t: accessTranslation } = useTranslation("access"); const { t: accessTranslation } = useTranslation("access");
const [collapsed, setCollapsed] = useState(false);
return ( return (
<aside className="desk-sidebar"> <aside className="desk-sidebar" data-collapsed={collapsed}>
<div className="desk-brand"> <div aria-label="Metis Desk" className="desk-brand" role="img">
<svg aria-hidden="true" viewBox="0 0 36 30"> <svg aria-hidden="true" viewBox="0 0 36 30">
<path <path
d="M5 24 13 7l9 17 9-18" d="M5 24 13 7l9 17 9-18"
@ -66,6 +72,7 @@ export function WorkspaceSidebar({
className="desk-space-button" className="desk-space-button"
key={area} key={area}
onClick={() => onNavigate({ area })} onClick={() => onNavigate({ area })}
title={t(label)}
variant="ghost" variant="ghost"
> >
<Icon aria-hidden="true" /> <Icon aria-hidden="true" />
@ -79,47 +86,58 @@ export function WorkspaceSidebar({
<nav aria-label={t("globalCatalogs")} className="desk-catalog-navigation"> <nav aria-label={t("globalCatalogs")} className="desk-catalog-navigation">
<> <>
<Button <Button
aria-label={accessTranslation("catalogTitle")}
aria-pressed={catalog === "access"} aria-pressed={catalog === "access"}
onClick={() => onSelectCatalog("access")} onClick={() => onSelectCatalog("access")}
title={accessTranslation("catalogTitle")}
variant="ghost" variant="ghost"
> >
{accessTranslation("catalogTitle")} <KeyRoundIcon aria-hidden="true" />
<span>{accessTranslation("catalogTitle")}</span>
</Button> </Button>
<Button <Button
aria-label={assetTranslation("title")}
aria-pressed={catalog === "assets"} aria-pressed={catalog === "assets"}
onClick={() => onSelectCatalog("assets")} onClick={() => onSelectCatalog("assets")}
title={assetTranslation("title")}
variant="ghost" variant="ghost"
> >
{assetTranslation("title")} <BoxIcon aria-hidden="true" />
<span>{assetTranslation("title")}</span>
</Button> </Button>
<Button <Button
aria-label={t("interventionTemplates.title")}
aria-pressed={catalog === "intervention-templates"} aria-pressed={catalog === "intervention-templates"}
onClick={() => onSelectCatalog("intervention-templates")} onClick={() => onSelectCatalog("intervention-templates")}
title={t("interventionTemplates.title")}
variant="ghost" variant="ghost"
> >
{t("interventionTemplates.title")} <LayoutTemplateIcon aria-hidden="true" />
<span>{t("interventionTemplates.title")}</span>
</Button> </Button>
<Button <Button
aria-label={assetTranslation("brands.title")}
aria-pressed={catalog === "brands"} aria-pressed={catalog === "brands"}
onClick={() => onSelectCatalog("brands")} onClick={() => onSelectCatalog("brands")}
title={assetTranslation("brands.title")}
variant="ghost" variant="ghost"
> >
{assetTranslation("brands.title")} <LayersIcon aria-hidden="true" />
<span>{assetTranslation("brands.title")}</span>
</Button> </Button>
<Button <Button
aria-label={resourceTranslation("title")}
aria-pressed={catalog === "resources"} aria-pressed={catalog === "resources"}
onClick={() => onSelectCatalog("resources")} onClick={() => onSelectCatalog("resources")}
title={resourceTranslation("title")}
variant="ghost" variant="ghost"
> >
{resourceTranslation("title")} <DatabaseIcon aria-hidden="true" />
<span>{resourceTranslation("title")}</span>
</Button> </Button>
</> </>
</nav> </nav>
)} )}
<Button className="desk-space-button" onClick={onSettings} variant="ghost">
<SettingsIcon aria-hidden="true" />
<span>{coreTranslation("shell.settings")}</span>
</Button>
<div className="desk-rail-separator" /> <div className="desk-rail-separator" />
{companies.map((company) => ( {companies.map((company) => (
<Button <Button
@ -136,6 +154,29 @@ export function WorkspaceSidebar({
</Button> </Button>
))} ))}
</nav> </nav>
<footer className="desk-sidebar-footer">
<Button
aria-label={coreTranslation("shell.settings")}
className="desk-space-button"
onClick={onSettings}
title={coreTranslation("shell.settings")}
variant="ghost"
>
<SettingsIcon aria-hidden="true" />
<span>{coreTranslation("shell.settings")}</span>
</Button>
<Button
aria-expanded={!collapsed}
aria-label={coreTranslation(collapsed ? "shell.expandSidebar" : "shell.collapseSidebar")}
className="desk-space-button desk-sidebar-toggle"
onClick={() => setCollapsed((value) => !value)}
title={coreTranslation(collapsed ? "shell.expandSidebar" : "shell.collapseSidebar")}
variant="ghost"
>
<ChevronRightIcon aria-hidden="true" />
<span>{coreTranslation("shell.collapseSidebar")}</span>
</Button>
</footer>
</aside> </aside>
); );
} }

View File

@ -24,6 +24,8 @@
"searchScope": "Search available companies and sections.", "searchScope": "Search available companies and sections.",
"notificationsUnavailable": "Notifications: service not yet available", "notificationsUnavailable": "Notifications: service not yet available",
"profile": "User menu", "profile": "User menu",
"supportWorkspace": "Support workspace" "supportWorkspace": "Support workspace",
"collapseSidebar": "Collapse menu",
"expandSidebar": "Expand menu"
} }
} }

View File

@ -24,6 +24,8 @@
"searchScope": "Buscar en compañías y secciones disponibles.", "searchScope": "Buscar en compañías y secciones disponibles.",
"notificationsUnavailable": "Notificaciones: servicio todavía no disponible", "notificationsUnavailable": "Notificaciones: servicio todavía no disponible",
"profile": "Menú de usuario", "profile": "Menú de usuario",
"supportWorkspace": "Espacio de soporte" "supportWorkspace": "Espacio de soporte",
"collapseSidebar": "Contraer menú",
"expandSidebar": "Expandir menú"
} }
} }