portal_publico/static/css/layout.css

470 lines
12 KiB
CSS

.app-shell {
display: grid;
grid-template-columns: var(--sidebar-width) 1fr;
min-height: 100vh;
transition: grid-template-columns var(--transition-base);
}
.app-shell.is-collapsed {
grid-template-columns: var(--sidebar-width-collapsed) 1fr;
}
/* Sidebar */
.sidebar {
/* Fundo congelado escuro nos dois temas: a barra de rolagem acompanha. */
color-scheme: dark;
background: var(--bg-sidebar);
border-right: 1px solid var(--sidebar-border);
padding: var(--space-5) var(--space-3);
display: flex;
flex-direction: column;
gap: var(--space-4);
overflow-y: auto;
overflow-x: hidden;
/* Duração/curva própria (não var(--transition-base)) só pra entrada em
portal.html vinda do login (ver .pid-entering-sidebar abaixo) — um
slide precisa de mais tempo e uma curva com "chegada" suave pra ler
como um movimento de verdade, não um fade genérico de microinteração. */
transition: opacity 420ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
}
/* Antes de access.js aplicar a visibilidade real (união de permissões, vinda
de /api/me/, assíncrona), todo item de menu gated por permissão começa
escondido — "fail closed". Sem isso, o menu inteiro (todas as seções/
aplicações) ficava visível por um instante em qualquer carregamento de
página — inclusive pra quem não tem acesso a todas —, já que o HTML de
cada shell já vem com todos os itens no DOM e só é filtrado depois que o
fetch de /api/me/ resolve. `pidApplyAccessVisibility` (access.js) só marca
`.sidebar` como `.is-ready` depois de já ter escondido cada item que o
usuário não pode ver. */
.sidebar:not(.is-ready) [data-section],
.sidebar:not(.is-ready) [data-app] {
display: none !important;
}
.sidebar__brand {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 92px;
padding: var(--space-2) 0 var(--space-3);
text-decoration: none;
cursor: pointer;
}
.sidebar__brand:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* Duas marcas em cima uma da outra (não uma só redimensionada) — a horizontal
tem texto (ilegível encolhida), o ícone é só o símbolo quadrado. O
cross-fade (opacity + scale) entre elas, em vez de trocar o `src` na hora,
é o que deixa a transição expandir/recolher fluida em vez de brusca. */
.sidebar__logo {
position: absolute;
top: 50%;
left: 50%;
object-fit: contain;
transition: opacity var(--transition-base), transform var(--transition-base);
}
.sidebar__logo--full {
width: 250px;
max-width: 96%;
height: auto;
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.sidebar__logo--icon {
width: 40px;
height: 40px;
opacity: 0;
transform: translate(-50%, -50%) scale(0.85);
}
.sidebar__nav {
flex: 1;
}
.nav-list {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.nav-item {
display: flex;
align-items: center;
gap: var(--space-3);
width: 100%;
padding: var(--space-3);
border-radius: var(--radius-sm);
color: var(--sidebar-text-secondary);
font-size: 0.88rem;
font-weight: 500;
white-space: nowrap;
text-align: left;
transition: background var(--transition-fast), color var(--transition-fast);
}
.nav-item svg {
color: var(--sidebar-text-muted);
transition: color var(--transition-fast);
}
.nav-item:hover {
background: var(--sidebar-hover-bg);
color: var(--sidebar-text-primary);
}
.nav-item:hover svg {
color: var(--accent);
}
.nav-item.is-active {
background: var(--accent);
color: var(--accent-contrast);
}
.nav-item.is-active svg {
color: var(--accent-contrast);
}
.nav-item__label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-item__chevron {
transition: transform var(--transition-fast);
color: var(--sidebar-text-muted);
}
.nav-group.is-open > .nav-group__toggle .nav-item__chevron {
transform: rotate(90deg);
}
.nav-group__list {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: var(--space-1) 0 var(--space-2) var(--space-6);
max-height: 0;
overflow: hidden;
visibility: hidden;
pointer-events: none;
transition: max-height var(--transition-base);
}
.nav-group.is-open > .nav-group__list {
visibility: visible;
pointer-events: auto;
max-height: 340px;
}
.nav-subitem {
display: block;
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
color: var(--sidebar-text-muted);
font-size: 0.82rem;
text-align: left;
}
.nav-subitem:hover {
background: var(--sidebar-hover-bg);
color: var(--sidebar-text-primary);
}
.nav-subitem.is-active {
color: var(--accent);
font-weight: 700;
}
/* Subgrupo dentro de um módulo (3º nível: módulo > subgrupo > ferramenta) */
.nav-subgroup__toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
width: 100%;
font-weight: 700;
border-bottom: 1px solid var(--sidebar-border);
border-radius: 0;
}
.nav-subgroup.is-open > .nav-subgroup__toggle .nav-item__chevron {
transform: rotate(90deg);
}
.nav-subgroup__list {
display: flex;
flex-direction: column;
gap: var(--space-1);
padding: var(--space-1) 0 var(--space-2) var(--space-4);
max-height: 0;
overflow: hidden;
visibility: hidden;
pointer-events: none;
transition: max-height var(--transition-base);
}
.nav-subgroup.is-open > .nav-subgroup__list {
visibility: visible;
pointer-events: auto;
max-height: 160px;
}
.nav-subitem--tool {
display: flex;
align-items: center;
gap: var(--space-2);
}
.nav-subitem--tool svg {
color: var(--sidebar-text-muted);
flex-shrink: 0;
}
/* Linha com o item de navegação + botão de favoritar. padding-right aqui
repõe o padding-right que o .nav-item perde visualmente ao ser encurtado
por flex:1 — sem isso, o .fav-toggle fica encostado na borda da sidebar,
enquanto itens sem estrela (ex.: toggles de grupo) mantêm o respiro do
próprio padding do .nav-item. */
.nav-item-row {
display: flex;
align-items: center;
gap: var(--space-1);
padding-right: var(--space-3);
}
.nav-item-row > .nav-item,
.nav-item-row > .nav-subitem {
flex: 1;
min-width: 0;
}
.fav-toggle {
flex-shrink: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
color: var(--sidebar-text-muted);
border-radius: var(--radius-sm);
transition: color var(--transition-fast), background var(--transition-fast);
}
.fav-toggle:hover {
color: var(--gold-400);
background: var(--sidebar-hover-bg);
}
.fav-toggle.is-favorite {
color: var(--gold-400);
}
.fav-toggle.is-favorite svg {
fill: currentColor;
}
.badge-restricted {
font-size: 0.62rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--gold-400);
border: 1px solid var(--gold-400);
border-radius: var(--radius-pill);
padding: 1px 6px;
}
/* Sidebar colapsada: esconde texto, mantém ícones */
.app-shell.is-collapsed .nav-item__label,
.app-shell.is-collapsed .nav-item__chevron,
.app-shell.is-collapsed .nav-group__list,
.app-shell.is-collapsed .badge-restricted,
.app-shell.is-collapsed .fav-toggle {
display: none;
}
.app-shell.is-collapsed .nav-item {
justify-content: center;
}
.app-shell.is-collapsed .sidebar__logo--full {
opacity: 0;
transform: translate(-50%, -50%) scale(0.85);
}
.app-shell.is-collapsed .sidebar__logo--icon {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.app-shell.is-collapsed .nav-item-row {
padding-right: 0;
}
/* Tooltip com o nome da ferramenta, mostrado ao passar o mouse sobre um
ícone da sidebar colapsada. Fica em document.body (não dentro de
.sidebar, que tem overflow-x:hidden e cortaria um elemento posicionado
fora da faixa da sidebar), posicionado via JS com getBoundingClientRect. */
.sidebar-tooltip {
position: fixed;
transform: translateY(-50%);
z-index: 60;
padding: var(--space-1) var(--space-3);
background: var(--bg-sidebar);
color: var(--sidebar-text-primary);
border: 1px solid var(--sidebar-border);
border-radius: var(--radius-sm);
font-size: 0.82rem;
font-weight: 500;
white-space: nowrap;
box-shadow: var(--shadow-md);
pointer-events: none;
animation: pidFadeIn var(--transition-fast);
}
/* Topbar */
.topbar {
height: var(--topbar-height);
background: var(--bg-topbar);
border-bottom: 1px solid var(--border-subtle);
display: flex;
align-items: center;
gap: var(--space-4);
padding: 0 var(--space-5);
position: sticky;
top: 0;
z-index: 20;
}
.topbar__title {
font-size: 1rem;
font-weight: 700;
letter-spacing: 0.03em;
color: var(--text-primary);
}
.topbar__actions {
margin-left: auto;
display: flex;
align-items: center;
gap: var(--space-2);
}
.main-content {
/* `isolation:isolate` garante um contexto de empilhamento próprio — sem
isso, `position:relative` sozinho (z-index auto) não isola nada, e a
teia de aranha sazonal (z-index:-1, ver halloween-cobweb.css) escaparia
pro contexto raiz da página em vez de ficar só atrás do conteúdo desta
área. Mesmo raciocínio de `isolation` em `.login-page` (login.css). */
position: relative;
isolation: isolate;
min-width: 0;
transition: opacity var(--transition-base);
}
/* Ligado só em portal.html, só quando se chega ali logo depois da animação
de intro do login (ver portal-reveal.js) — duas classes, removidas em
sequência pelo JS: primeiro a sidebar desliza de fora da tela (esquerda
pra direita, `translateX(-100%)` — toda a largura dela, não um deslize
sutil de poucos pixels, pra realmente ler como "a barra lateral surgindo"
em vez de só um fade) até a posição final; só depois o resto do app
aparece num fade (`pid-entering`), em vez das duas coisas surgirem ao
mesmo tempo. */
html.pid-entering-sidebar .sidebar {
opacity: 0;
transform: translateX(-100%);
}
html.pid-entering .main-content {
opacity: 0;
}
.page-content {
padding: var(--space-5);
max-width: 1200px;
margin: 0 auto;
animation: pidFadeSlideUp 180ms ease;
}
/* Telas de aplicação (tudo exceto o "Principal"/portal.html) aproveitam melhor
o espaço entre a sidebar e a borda da tela — a home continua com o limite
padrão de 1200px, mais confortável de leitura pra uma grade de favoritos. */
.page-content.page-content--wide {
max-width: 1600px;
}
@media (max-width: 1024px) {
.app-shell {
grid-template-columns: var(--sidebar-width-collapsed) 1fr;
}
.app-shell .nav-item__label,
.app-shell .nav-item__chevron,
.app-shell .nav-group__list,
.app-shell .badge-restricted,
.app-shell .fav-toggle {
display: none;
}
.app-shell .nav-item {
justify-content: center;
}
.app-shell .nav-item-row {
padding-right: 0;
}
.app-shell .sidebar__logo--full {
opacity: 0;
transform: translate(-50%, -50%) scale(0.85);
}
.app-shell .sidebar__logo--icon {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.app-shell.is-expanded-mobile {
grid-template-columns: var(--sidebar-width) 1fr;
}
.app-shell.is-expanded-mobile .sidebar__logo--full {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.app-shell.is-expanded-mobile .sidebar__logo--icon {
opacity: 0;
transform: translate(-50%, -50%) scale(0.85);
}
.app-shell.is-expanded-mobile .nav-item__label,
.app-shell.is-expanded-mobile .nav-item__chevron,
.app-shell.is-expanded-mobile .badge-restricted {
display: inline;
}
.app-shell.is-expanded-mobile .fav-toggle {
display: flex;
}
.app-shell.is-expanded-mobile .nav-item-row {
padding-right: var(--space-3);
}
.app-shell.is-expanded-mobile .nav-group.is-open .nav-group__list {
display: flex;
}
}