feat: enhance layout and component structure for dashboard, including responsive sidebar and header actions, and update footer styling

This commit is contained in:
2025-12-12 16:05:28 -03:00
parent b771322b24
commit 4f238022cf
6 changed files with 350 additions and 465 deletions

View File

@@ -4,8 +4,8 @@
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
</script> </script>
<footer class="bg-base-200 text-base-content border-base-300 border-t"> <footer class="bg-base-200 text-base-content border-base-300 mt-16 border-t">
<div class="container mx-auto px-4 py-10"> <div class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 gap-8 text-center md:grid-cols-3 md:text-left"> <div class="grid grid-cols-1 gap-8 text-center md:grid-cols-3 md:text-left">
<div> <div>
<h3 class="text-primary mb-4 text-lg font-bold">SGSE</h3> <h3 class="text-primary mb-4 text-lg font-bold">SGSE</h3>

View File

@@ -1,20 +1,43 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import logo from '$lib/assets/logo_governo_PE.png'; import logo from '$lib/assets/logo_governo_PE.png';
import { page } from '$app/state'; import type { Snippet } from 'svelte';
type HeaderProps = {
left?: Snippet;
right?: Snippet;
};
const { left, right }: HeaderProps = $props();
</script> </script>
<header class="sticky top-0 z-50 w-full border-b backdrop-blur-md bg-base-100/90 border-base-200 shadow-sm transition-all duration-300"> <header
<div class="container mx-auto px-4 h-16 flex items-center justify-between"> class="bg-base-200 border-base-100 sticky top-0 z-50 w-full border-b shadow-sm backdrop-blur-md transition-all duration-300"
<a href="/" class="flex items-center gap-3 group transition-transform hover:scale-[1.02]"> >
<div class=" flex h-16 w-full items-center justify-between px-4">
<div class="flex items-center gap-3">
{#if left}
{@render left()}
{/if}
<a
href={resolve('/')}
class="group flex items-center gap-3 transition-transform hover:scale-[1.02]"
>
<img src={logo} alt="Logo Governo PE" class="h-10 w-auto object-contain drop-shadow-sm" /> <img src={logo} alt="Logo Governo PE" class="h-10 w-auto object-contain drop-shadow-sm" />
<div class="flex flex-col hidden sm:flex"> <div class="hidden flex-col sm:flex">
<span class="text-xs font-bold text-primary tracking-wider uppercase">Governo de</span> <span class="text-primary text-xs font-bold tracking-wider uppercase">Governo de</span>
<span class="text-lg font-extrabold -mt-1 tracking-tight text-base-content leading-none">Pernambuco</span> <span class="text-base-content -mt-1 text-lg leading-none font-extrabold tracking-tight"
>Pernambuco</span
>
</div> </div>
</a> </a>
</div>
<nav class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Links can be added here based on auth state or specific requirements --> {#if right}
</nav> {@render right()}
{/if}
</div>
</div> </div>
</header> </header>

View File

@@ -3,39 +3,19 @@
import { useQuery } from 'convex-svelte'; import { useQuery } from 'convex-svelte';
import type { FunctionReference } from 'convex/server'; import type { FunctionReference } from 'convex/server';
import { import {
Home, ChevronDown,
User,
UserPlus,
XCircle,
Users,
DollarSign,
ClipboardCheck, ClipboardCheck,
FileText, FileText,
ShoppingCart,
Scale,
Megaphone,
Trophy,
Briefcase,
UserCog,
Monitor,
ChevronDown,
GitMerge, GitMerge,
Home,
Settings, Settings,
Check, Tag,
LogIn, Users,
Menu, Briefcase,
Plus, UserPlus
Tag
} from 'lucide-svelte'; } from 'lucide-svelte';
import type { Snippet } from 'svelte';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { page } from '$app/state'; import { page } from '$app/state';
import logo from '$lib/assets/logo_governo_PE.png';
import { authClient } from '$lib/auth';
import ChatWidget from '$lib/components/chat/ChatWidget.svelte';
import NotificationBell from '$lib/components/chat/NotificationBell.svelte';
import PresenceManager from '$lib/components/chat/PresenceManager.svelte';
interface MenuItemPermission { interface MenuItemPermission {
recurso: string; recurso: string;
@@ -198,12 +178,13 @@
type IconType = typeof Home; type IconType = typeof Home;
const { children }: { children: Snippet } = $props(); type SidebarProps = {
onNavigate?: () => void;
};
const { onNavigate }: SidebarProps = $props();
let currentPath = $derived(page.url.pathname); let currentPath = $derived(page.url.pathname);
let showAboutModal = $state(false);
const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>, {});
const permissionsQuery = useQuery(api.menu.getUserPermissions as FunctionReference<'query'>, {}); const permissionsQuery = useQuery(api.menu.getUserPermissions as FunctionReference<'query'>, {});
// Filtrar menu baseado nas permissões do usuário // Filtrar menu baseado nas permissões do usuário
@@ -267,27 +248,14 @@
const iconMap: Record<string, IconType> = { const iconMap: Record<string, IconType> = {
Home, Home,
User,
UserPlus, UserPlus,
XCircle,
Users, Users,
DollarSign,
ClipboardCheck, ClipboardCheck,
FileText, FileText,
ShoppingCart,
Scale,
Megaphone,
Trophy,
Briefcase, Briefcase,
UserCog,
Monitor,
ChevronDown, ChevronDown,
GitMerge, GitMerge,
Settings, Settings,
Check,
LogIn,
Menu,
Plus,
Tag Tag
}; };
@@ -330,202 +298,11 @@
function getSolicitarClasses(active: boolean) { function getSolicitarClasses(active: boolean) {
return getMenuClasses(active); return getMenuClasses(active);
} }
// Função para obter a URL do avatar/foto do usuário
let avatarUrlDoUsuario = $derived.by(() => {
if (!currentUser.data) return null;
// Prioridade: fotoPerfilUrl > avatar > fallback com nome
if (currentUser.data.fotoPerfilUrl) {
return currentUser.data.fotoPerfilUrl;
}
if (currentUser.data.avatar) {
return currentUser.data.avatar;
}
// Fallback: retornar null para usar o ícone User do Lucide
return null;
});
function goToLogin(redirectTo?: string) {
const target = redirectTo || currentPath || '/';
goto(`${resolve('/login')}?redirect=${encodeURIComponent(target)}`);
}
function openAboutModal() {
showAboutModal = true;
}
function closeAboutModal() {
showAboutModal = false;
}
async function handleLogout() {
const result = await authClient.signOut();
if (result.error) {
console.error('Sign out error:', result.error);
}
// Resetar tema para padrão ao fazer logout
const { aplicarTemaPadrao } = await import('$lib/utils/temas');
aplicarTemaPadrao();
goto(resolve('/'));
}
</script> </script>
<!-- Header Fixo acima de tudo --> <nav
<!-- Header Fixo Minimalista & Premium --> class="menu text-base-content bg-base-200 border-base-100 h-[calc(100vh-64px)] w-full flex-col gap-2 overflow-y-auto p-4"
<div
class="navbar border-primary/10 from-primary/10 via-primary/5 to-primary/10 fixed top-0 right-0 left-0 z-50 h-16 border-b bg-linear-to-r px-4 shadow-sm"
> >
<div class="flex-none lg:hidden">
<label for="my-drawer-3" class="btn btn-square btn-ghost btn-sm" aria-label="Abrir menu">
<Menu class="h-5 w-5" />
</label>
</div>
<div class="flex flex-1 items-center gap-4 lg:gap-6">
<!-- Logo Visível e Ajustado -->
<div class="flex items-center gap-3">
<div class="relative h-10 overflow-hidden rounded-lg shadow-sm lg:h-14">
<img src={logo} alt="Logo do Governo de PE" class="h-full w-full object-contain p-1" />
</div>
<div class="flex flex-col justify-center">
<h1 class="text-base-content text-xl leading-none font-bold tracking-tight lg:text-2xl">
SGSE
</h1>
<span class="text-base-content/60 font-medium tracking-wider uppercase"
>Secretaria de Esportes</span
>
</div>
</div>
</div>
<div class="ml-auto flex flex-none items-center gap-3 lg:gap-5">
{#if currentUser.data}
<!-- Nome e Perfil -->
<div class="hidden flex-col items-end lg:flex">
<span class="text-base-content text-sm leading-tight font-semibold"
>{currentUser.data.nome}</span
>
<span class="text-base-content/60 text-xs leading-tight">{currentUser.data.role?.nome}</span
>
</div>
<div class="dropdown dropdown-end">
<!-- Botão de Perfil com Avatar -->
<button
type="button"
tabindex="0"
class="btn avatar ring-base-200 hover:ring-primary/50 h-12 w-12 p-0 ring-2 ring-offset-2 transition-all"
aria-label="Menu do usuário"
>
<div class="h-full w-full overflow-hidden rounded-full">
{#if avatarUrlDoUsuario}
<img
src={avatarUrlDoUsuario}
alt={currentUser.data?.nome || 'Usuário'}
class="h-full w-full object-cover"
/>
{:else}
<div
class="bg-primary/10 text-primary flex h-full w-full items-center justify-center"
>
<User class="h-6 w-6" />
</div>
{/if}
</div>
</button>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box ring-base-content/5 z-1 mt-3 w-56 p-2 shadow-xl ring-1"
>
<li class="menu-title border-base-200 mb-2 border-b px-4 py-2">
<span class="text-base-content font-bold">{currentUser.data?.nome}</span>
<span class="text-base-content/60 text-xs font-normal">{currentUser.data.email}</span>
</li>
<li>
<a href={resolve('/perfil')} class="active:bg-primary/10 active:text-primary"
><UserCog class="mr-2 h-4 w-4" /> Meu Perfil</a
>
</li>
<li>
<a href={resolve('/alterar-senha')} class="active:bg-primary/10 active:text-primary"
><Settings class="mr-2 h-4 w-4" /> Alterar Senha</a
>
</li>
<div class="divider my-1"></div>
<li>
<button type="button" onclick={handleLogout} class="text-error hover:bg-error/10"
><LogIn class="mr-2 h-4 w-4 rotate-180" /> Sair</button
>
</li>
</ul>
</div>
<!-- Sino de notificações -->
<div class="relative">
<NotificationBell />
</div>
{:else}
<button
type="button"
class="btn btn-primary btn-sm rounded-full px-6"
onclick={() => goToLogin()}
>
Entrar
</button>
{/if}
</div>
</div>
<div class="drawer lg:drawer-open" style="margin-top: 64px;">
<input id="my-drawer-3" type="checkbox" class="drawer-toggle" />
<div
class="drawer-content flex flex-col transition-all duration-300 lg:ml-72"
style="min-height: calc(100vh - 64px);"
>
<!-- Page content -->
<div class="flex-1 overflow-y-auto p-6">
{@render children?.()}
</div>
<!-- Footer Minimalista -->
<footer
class="footer footer-center text-base-content/60 border-base-200 bg-base-100/50 border-t p-4 text-xs backdrop-blur-sm"
>
<div class="flex flex-wrap justify-center gap-6">
<button
type="button"
class="hover:text-primary font-medium transition-colors"
onclick={() => openAboutModal()}>Sobre</button
>
<a href={resolve('/')} class="hover:text-primary font-medium transition-colors">Contato</a>
<a href={resolve('/abrir-chamado')} class="hover:text-primary font-medium transition-colors"
>Suporte</a
>
<span class="text-base-content/30"></span>
<a
href={resolve('/abrir-chamado')}
class="link link-hover hover:text-primary transition-colors">Suporte</a
>
<span class="text-base-content/30"></span>
<a
href={resolve('/privacidade')}
class="link link-hover hover:text-primary transition-colors">Privacidade</a
>
</div>
<div class="mt-2 opacity-70">
<p>© {new Date().getFullYear()} Governo de Pernambuco - Secretaria de Esportes</p>
</div>
</footer>
</div>
<div class="drawer-side fixed z-40" style="margin-top: 64px;">
<label for="my-drawer-3" aria-label="close sidebar" class="drawer-overlay"></label>
<div
class="menu text-base-content border-base-300 from-primary/10 via-primary/5 to-primary/10 h-[calc(100vh-64px)] w-72 flex-col gap-2 overflow-y-auto border-r-2 bg-linear-to-b p-4 backdrop-blur-sm"
>
<!-- Sidebar menu items -->
<!-- Sidebar menu items -->
{#snippet menuItem(item: MenuItem)} {#snippet menuItem(item: MenuItem)}
{@const Icon = getIconComponent(item.icon)} {@const Icon = getIconComponent(item.icon)}
{@const isActive = isRouteActive(item.link, { {@const isActive = isRouteActive(item.link, {
@@ -560,7 +337,11 @@
exact: sub.exact exact: sub.exact
})} })}
<li> <li>
<a href={resolve(sub.link as any)} class={getMenuClasses(isSubActive, true)}> <a
href={resolve(sub.link as any)}
class={getMenuClasses(isSubActive, true)}
onclick={() => onNavigate?.()}
>
<span>{sub.label}</span> <span>{sub.label}</span>
</a> </a>
</li> </li>
@@ -573,6 +354,7 @@
href={resolve(item.link as any)} href={resolve(item.link as any)}
aria-current={isActive ? 'page' : undefined} aria-current={isActive ? 'page' : undefined}
class={getMenuClasses(isActive)} class={getMenuClasses(isActive)}
onclick={() => onNavigate?.()}
> >
<Icon class="h-5 w-5" strokeWidth={2} /> <Icon class="h-5 w-5" strokeWidth={2} />
<span>{item.label}</span> <span>{item.label}</span>
@@ -602,147 +384,16 @@
<a <a
href={resolve('/abrir-chamado')} href={resolve('/abrir-chamado')}
class={getSolicitarClasses(currentPath === '/abrir-chamado')} class={getSolicitarClasses(currentPath === '/abrir-chamado')}
onclick={() => onNavigate?.()}
> >
<UserPlus class="h-5 w-5" strokeWidth={2} /> <UserPlus class="h-5 w-5" strokeWidth={2} />
<span>Abrir Chamado</span> <span>Abrir Chamado</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </nav>
</div>
</div>
<!-- Modal Sobre -->
{#if showAboutModal}
<dialog class="modal modal-open">
<div
class="modal-box from-base-100 to-base-200 relative max-w-md overflow-hidden bg-linear-to-br shadow-xl"
>
<button
type="button"
class="btn btn-sm btn-circle btn-ghost hover:bg-base-300 absolute top-2 right-2 z-10"
onclick={closeAboutModal}
>
</button>
<div class="space-y-5 px-6 py-6 text-center">
<!-- Logo e Título -->
<div class="flex flex-col items-center gap-3">
<div class="avatar">
<div class="ring-primary/20 w-20 rounded-xl bg-white p-3 shadow-lg ring-2">
<img src={logo} alt="Logo SGSE" class="h-full w-full object-contain" />
</div>
</div>
<div class="space-y-1">
<h3 class="text-primary text-2xl font-bold tracking-tight">SGSE</h3>
<p class="text-base-content/70 text-sm font-medium">
Sistema de Gerenciamento de Secretaria
</p>
</div>
</div>
<!-- Divider -->
<div class="divider my-1"></div>
<!-- Informações de Versão -->
<div
class="from-primary/10 to-primary/5 border-primary/10 space-y-2 rounded-xl border bg-linear-to-br p-4 shadow-sm"
>
<div class="flex items-center justify-center gap-2">
<Tag class="text-primary h-4 w-4" strokeWidth={2} />
<p class="text-base-content/60 text-xs font-medium tracking-wide uppercase">Versão</p>
</div>
<p class="text-primary text-2xl font-bold tracking-tight">1.0 11_2025</p>
<div class="badge badge-warning badge-sm gap-1.5 px-3 py-1.5 text-xs">
<Plus class="h-3.5 w-3.5" strokeWidth={2} />
Em Desenvolvimento
</div>
</div>
<!-- Desenvolvido por -->
<div class="space-y-1.5">
<p class="text-base-content/50 text-xs font-medium tracking-wide uppercase">
Desenvolvido por
</p>
<p class="text-primary text-sm font-semibold">Secretaria de Esportes de Pernambuco</p>
</div>
<!-- Divider -->
<div class="divider my-1"></div>
<!-- Informações Adicionais -->
<div class="grid grid-cols-2 gap-3">
<div
class="bg-base-200/60 border-base-300/50 rounded-lg border p-3 shadow-sm transition-all hover:shadow-md"
>
<p class="text-primary mb-1 text-xs font-semibold tracking-wide uppercase">Governo</p>
<p class="text-base-content/60 text-xs font-medium">Estado de Pernambuco</p>
</div>
<div
class="bg-base-200/60 border-base-300/50 rounded-lg border p-3 shadow-sm transition-all hover:shadow-md"
>
<p class="text-primary mb-1 text-xs font-semibold tracking-wide uppercase">Ano</p>
<p class="text-base-content/60 text-xs font-medium">2025</p>
</div>
</div>
<!-- Botão OK -->
<div class="pt-3">
<button
type="button"
class="btn btn-primary btn-sm mx-auto w-full max-w-xs shadow-md transition-all duration-200 hover:shadow-lg"
onclick={closeAboutModal}
>
<Check class="h-4 w-4" strokeWidth={2} />
OK
</button>
</div>
</div>
</div>
<div
class="modal-backdrop"
onclick={closeAboutModal}
role="button"
tabindex="0"
onkeydown={(e) => e.key === 'Escape' && closeAboutModal()}
></div>
</dialog>
{/if}
<!-- Componentes de Chat (apenas se autenticado) -->
{#if currentUser.data}
<PresenceManager />
<ChatWidget />
{/if}
<style> <style>
/* Animação de pulso sutil para o anel do botão de perfil */
@keyframes pulse-ring-subtle {
0%,
100% {
opacity: 0.1;
transform: scale(1);
}
50% {
opacity: 0.3;
transform: scale(1.05);
}
}
/* Animação de pulso para o badge de status online */
@keyframes pulse-dot {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.8;
transform: scale(1.1);
}
}
/* Remove default details marker */ /* Remove default details marker */
details > summary { details > summary {
list-style: none; list-style: none;

View File

@@ -0,0 +1,124 @@
<script lang="ts">
import { api } from '@sgse-app/backend/convex/_generated/api';
import { useQuery } from 'convex-svelte';
import type { FunctionReference } from 'convex/server';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { LogIn, Settings, User, UserCog } from 'lucide-svelte';
import { authClient } from '$lib/auth';
import NotificationBell from '$lib/components/chat/NotificationBell.svelte';
let currentPath = $derived(page.url.pathname);
const currentUser = useQuery(api.auth.getCurrentUser as FunctionReference<'query'>, {});
// Função para obter a URL do avatar/foto do usuário
let avatarUrlDoUsuario = $derived.by(() => {
if (!currentUser.data) return null;
// Prioridade: fotoPerfilUrl > avatar > fallback com nome
if (currentUser.data.fotoPerfilUrl) {
return currentUser.data.fotoPerfilUrl;
}
if (currentUser.data.avatar) {
return currentUser.data.avatar;
}
// Fallback: retornar null para usar o ícone User do Lucide
return null;
});
function goToLogin(redirectTo?: string) {
const target = redirectTo || currentPath || '/';
goto(`${resolve('/login')}?redirect=${encodeURIComponent(target)}`);
}
async function handleLogout() {
const result = await authClient.signOut();
if (result.error) {
console.error('Sign out error:', result.error);
}
// Resetar tema para padrão ao fazer logout
const { aplicarTemaPadrao } = await import('$lib/utils/temas');
aplicarTemaPadrao();
goto(resolve('/home'));
}
</script>
<div class="flex items-center gap-3">
{#if currentUser.data}
<!-- Nome e Perfil -->
<div class="hidden flex-col items-end lg:flex">
<span class="text-base-content text-sm leading-tight font-semibold"
>{currentUser.data.nome}</span
>
<span class="text-base-content/60 text-xs leading-tight">{currentUser.data.role?.nome}</span>
</div>
<div class="dropdown dropdown-end">
<!-- Botão de Perfil com Avatar -->
<button
type="button"
tabindex="0"
class="btn avatar ring-base-200 hover:ring-primary/50 h-10 w-10 p-0 ring-2 ring-offset-2 transition-all"
aria-label="Menu do usuário"
>
<div class="h-full w-full overflow-hidden rounded-full">
{#if avatarUrlDoUsuario}
<img
src={avatarUrlDoUsuario}
alt={currentUser.data?.nome || 'Usuário'}
class="h-full w-full object-cover"
/>
{:else}
<div class="bg-primary/10 text-primary flex h-full w-full items-center justify-center">
<User class="h-5 w-5" />
</div>
{/if}
</div>
</button>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box ring-base-content/5 z-1 mt-3 w-56 p-2 shadow-xl ring-1"
>
<li class="menu-title border-base-200 mb-2 border-b px-4 py-2">
<span class="text-base-content font-bold">{currentUser.data?.nome}</span>
<span class="text-base-content/60 text-xs font-normal">{currentUser.data.email}</span>
</li>
<li>
<a href={resolve('/perfil')} class="active:bg-primary/10 active:text-primary"
><UserCog class="mr-2 h-4 w-4" /> Meu Perfil</a
>
</li>
<li>
<a href={resolve('/alterar-senha')} class="active:bg-primary/10 active:text-primary"
><Settings class="mr-2 h-4 w-4" /> Alterar Senha</a
>
</li>
<div class="divider my-1"></div>
<li>
<button type="button" onclick={handleLogout} class="text-error hover:bg-error/10"
><LogIn class="mr-2 h-4 w-4 rotate-180" /> Sair</button
>
</li>
</ul>
</div>
<!-- Sino de notificações -->
<div class="relative">
<NotificationBell />
</div>
{:else}
<button
type="button"
class="btn btn-primary btn-sm rounded-full px-6"
onclick={() => goToLogin()}
>
Entrar
</button>
{/if}
</div>

View File

@@ -1,16 +1,92 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import { Toaster } from 'svelte-sonner'; import { Toaster } from 'svelte-sonner';
import PushNotificationManager from '$lib/components/PushNotificationManager.svelte'; import PushNotificationManager from '$lib/components/PushNotificationManager.svelte';
import Footer from '$lib/components/Footer.svelte';
import Header from '$lib/components/Header.svelte';
import Sidebar from '$lib/components/Sidebar.svelte'; import Sidebar from '$lib/components/Sidebar.svelte';
import DashboardHeaderActions from '$lib/components/dashboard/DashboardHeaderActions.svelte';
import ChatWidget from '$lib/components/chat/ChatWidget.svelte';
import PresenceManager from '$lib/components/chat/PresenceManager.svelte';
import { Menu, X } from 'lucide-svelte';
const { children } = $props(); const { children } = $props();
let sidebarOpen = $state(false);
const toggleSidebar = () => (sidebarOpen = !sidebarOpen);
const closeSidebar = () => (sidebarOpen = false);
// No desktop, abrir por padrão; no mobile, começar fechado
onMount(() => {
sidebarOpen = window.matchMedia('(min-width: 1024px)').matches;
});
</script> </script>
<div class="flex flex-col"> <div
<Sidebar> class="bg-base-100 text-base-content selection:bg-primary selection:text-primary-content flex min-h-screen flex-col font-sans"
<main id="container-central" class="w-full max-w-none px-3 py-4 lg:px-4"> >
<Header>
{#snippet left()}
<button
type="button"
class="btn btn-ghost btn-sm"
aria-label={sidebarOpen ? 'Fechar menu' : 'Abrir menu'}
onclick={toggleSidebar}
>
{#if sidebarOpen}
<X class="h-5 w-5" />
{:else}
<Menu class="h-5 w-5" />
{/if}
</button>
{/snippet}
{#snippet right()}
<DashboardHeaderActions />
{/snippet}
</Header>
<div class="relative flex min-h-[calc(100vh-4rem)] flex-1">
<!-- Overlay (mobile) -->
{#if sidebarOpen}
<div
class="fixed inset-0 z-40 bg-black/30 backdrop-blur-[1px] lg:hidden"
role="button"
tabindex="0"
aria-label="Fechar menu"
onclick={closeSidebar}
onkeydown={(e) => e.key === 'Escape' && closeSidebar()}
></div>
{/if}
<!-- Sidebar -->
<aside
class="bg-base-100 border-base-200 fixed top-16 bottom-0 left-0 z-50 w-72 border-r shadow-sm transition-transform duration-200"
class:translate-x-0={sidebarOpen}
class:-translate-x-full={!sidebarOpen}
>
<div class="h-full overflow-y-auto">
<Sidebar onNavigate={closeSidebar} />
</div>
</aside>
<!-- Conteúdo -->
<div
class="min-w-0 flex-1 transition-[padding] duration-200 {sidebarOpen
? 'lg:pl-72'
: 'lg:pl-0'}"
>
<div id="container-central" class="w-full max-w-none px-3 py-4 lg:px-4">
{@render children()} {@render children()}
</main> </div>
</Sidebar>
<Footer />
</div>
</div>
<!-- Componentes de Chat (gerenciam auth internamente) -->
<PresenceManager />
<ChatWidget />
</div> </div>
<!-- Toast Notifications (Sonner) --> <!-- Toast Notifications (Sonner) -->
@@ -18,3 +94,14 @@
<!-- Push Notification Manager (registra subscription automaticamente) --> <!-- Push Notification Manager (registra subscription automaticamente) -->
<PushNotificationManager /> <PushNotificationManager />
<style>
/* Evita “corredor” quando páginas usam `container mx-auto` */
#container-central :global(.container) {
max-width: none !important;
}
#container-central :global(.container.mx-auto) {
margin-left: 0 !important;
margin-right: 0 !important;
}
</style>

View File

@@ -140,7 +140,7 @@
</script> </script>
<ProtectedRoute> <ProtectedRoute>
<main class="container mx-auto px-4 py-4"> <main class="w-full px-4 py-4">
<!-- Alerta de Acesso Negado / Autenticação --> <!-- Alerta de Acesso Negado / Autenticação -->
{#if showAlert} {#if showAlert}
{@const alertData = getAlertMessage()} {@const alertData = getAlertMessage()}