Componentes de UI para Layout e Navegação
Esta skill define os componentes Blade específicos para layout e navegação no Easy Budget, seguindo o padrão de components estabelecido no sistema.
Estrutura de Components
resources/views/components/
├── layout/
│ ├── app.blade.php # Layout principal do sistema
│ ├── admin.blade.php # Layout para área administrativa
│ ├── auth.blade.php # Layout para páginas de autenticação
│ ├── dashboard.blade.php # Layout para dashboard
│ ├── sidebar.blade.php # Barra lateral de navegação
│ ├── navbar.blade.php # Barra de navegação superior
│ ├── breadcrumbs.blade.php # Breadcrumb de navegação
│ ├── footer.blade.php # Rodapé do sistema
│ ├── theme-switcher.blade.php # Troca de tema
│ └── notifications.blade.php # Sistema de notificações
└── ...
1. App Layout Component
Componente para layout principal do sistema.
Uso Básico
<x-layout.app :title="$title" :sidebar="$sidebar" :navbar="$navbar" />
Parâmetros
| Parâmetro | Tipo | Descrição | Padrão |
|---|---|---|---|
title |
string |
Título da página | Obrigatório |
sidebar |
bool |
Exibir barra lateral | true |
navbar |
bool |
Exibir barra superior | true |
theme |
string |
Tema do sistema | auto |
Estrutura
@props([
'title',
'sidebar' => true,
'navbar' => true,
'theme' => 'auto'
])
<!DOCTYPE html>
<html lang="pt-BR" data-theme="{{ $theme }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title }} - {{ tenant('name') ?? 'Easy Budget' }}</title>
<!-- Favicon -->
<link rel="icon" href="{{ asset('favicon.ico') }}" type="image/x-icon">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
<!-- Custom Styles -->
@stack('styles')
<!-- Meta Tags -->
<meta name="description" content="Sistema de gestão empresarial completo">
<meta name="keywords" content="gestão, orçamentos, faturas, clientes, estoque">
<meta name="author" content="Easy Budget">
<!-- Open Graph -->
<meta property="og:title" content="{{ $title }}">
<meta property="og:description" content="Sistema de gestão empresarial">
<meta property="og:type" content="website">
<!-- CSRF Token -->
<script>
window.Laravel = {
csrfToken: '{{ csrf_token() }}',
baseUrl: '{{ url('/') }}'
};
</script>
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
@if($navbar)
<x-layout.navbar />
@endif
<div class="flex">
@if($sidebar)
<x-layout.sidebar />
@endif
<main class="flex-1 overflow-x-hidden overflow-y-auto">
<div class="container mx-auto px-6 py-8">
{{ $slot }}
</div>
</main>
</div>
</div>
<!-- Flash Messages -->
@if(session('success'))
<div id="flash-message" class="fixed top-4 right-4 z-50" data-message="{{ session('success') }}" data-type="success"></div>
@elseif(session('error'))
<div id="flash-message" class="fixed top-4 right-4 z-50" data-message="{{ session('error') }}" data-type="error"></div>
@endif
<!-- Scripts -->
@stack('scripts')
<!-- Flash Message Script -->
<script>
document.addEventListener('DOMContentLoaded', function() {
const flashMessage = document.getElementById('flash-message');
if (flashMessage) {
const message = flashMessage.getAttribute('data-message');
const type = flashMessage.getAttribute('data-type');
showFlashMessage(message, type);
}
});
</script>
</body>
</html>
2. Admin Layout Component
Componente para layout da área administrativa.
Uso Básico
<x-layout.admin :title="$title" :breadcrumbs="$breadcrumbs" />
Parâmetros
| Parâmetro | Tipo | Descrição | Padrão |
|---|---|---|---|
title |
string |
Título da página | Obrigatório |
breadcrumbs |
array |
Breadcrumb da página | [] |
sidebar |
bool |
Exibir barra lateral | true |
Estrutura
@props([
'title',
'breadcrumbs' => [],
'sidebar' => true
])
<!DOCTYPE html>
<html lang="pt-BR" data-theme="admin">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title }} - Administração - {{ tenant('name') ?? 'Easy Budget' }}</title>
<!-- Favicon -->
<link rel="icon" href="{{ asset('favicon.ico') }}" type="image/x-icon">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Admin Styles -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom Admin Styles -->
<style>
:root {
--admin-bg: #f8f9fa;
--admin-sidebar: #2c3e50;
--admin-sidebar-active: #34495e;
--admin-primary: #3498db;
--admin-danger: #e74c3c;
--admin-warning: #f39c12;
--admin-success: #27ae60;
}
.admin-layout {
min-height: 100vh;
background-color: var(--admin-bg);
}
.admin-sidebar {
background-color: var(--admin-sidebar);
min-height: 100vh;
border-right: 1px solid #dee2e6;
}
.admin-sidebar .nav-link {
color: #bdc3c7;
border-radius: 0;
padding: 1rem 1.5rem;
transition: all 0.3s ease;
}
.admin-sidebar .nav-link:hover {
background-color: var(--admin-sidebar-active);
color: #fff;
}
.admin-sidebar .nav-link.active {
background-color: var(--admin-primary);
color: #fff;
border-left: 4px solid #fff;
}
.admin-header {
background-color: #fff;
border-bottom: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.admin-content {
padding: 2rem;
}
.admin-card {
background: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.admin-stat-card {
background: linear-gradient(135deg, var(--admin-primary), #2980b9);
color: white;
border: none;
}
.admin-stat-card .stat-value {
font-size: 2rem;
font-weight: bold;
}
.admin-stat-card .stat-label {
font-size: 0.9rem;
opacity: 0.8;
}
</style>
<!-- Scripts -->
@stack('styles')
</head>
<body class="admin-layout">
<!-- Header -->
<header class="admin-header">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col-md-6">
<h1 class="h4 mb-0 text-dark">{{ $title }}</h1>
</div>
<div class="col-md-6 text-end">
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle me-2"></i>{{ auth()->user()->name }}
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ route('profile.edit') }}"><i class="bi bi-person me-2"></i>Perfil</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="dropdown-item"><i class="bi bi-box-arrow-right me-2"></i>Sair</button>
</form>
</li>
</ul>
</div>
</div>
</div>
</div>
</header>
<div class="container-fluid">
<div class="row">
@if($sidebar)
<x-layout.sidebar />
@endif
<main class="col {{ $sidebar ? 'col-md-9' : 'col-md-12' }} ms-sm-auto px-md-4">
<div class="admin-content">
<!-- Breadcrumb -->
@if(!empty($breadcrumbs))
<x-layout.breadcrumbs :breadcrumbs="$breadcrumbs" />
@endif
<!-- Content -->
{{ $slot }}
</div>
</main>
</div>
</div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
@stack('scripts')
</body>
</html>
3. Auth Layout Component
Componente para layout de páginas de autenticação.
Uso Básico
<x-layout.auth :title="$title" :showLogo="$showLogo" />
Parâmetros
| Parâmetro | Tipo | Descrição | Padrão |
|---|---|---|---|
title |
string |
Título da página | Obrigatório |
showLogo |
bool |
Exibir logo da empresa | true |
background |
string |
Imagem de fundo | null |
Estrutura
@props([
'title',
'showLogo' => true,
'background' => null
])
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title }} - {{ tenant('name') ?? 'Easy Budget' }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Styles -->
<style>
:root {
--auth-bg: #f8f9fa;
--auth-card: #ffffff;
--auth-border: #e9ecef;
--auth-primary: #0d6efd;
--auth-text: #333333;
--auth-muted: #6c757d;
}
body {
background-color: var(--auth-bg);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
}
.auth-container {
width: 100%;
max-width: 400px;
margin: 0 auto;
padding: 2rem;
}
.auth-card {
background: var(--auth-card);
border: 1px solid var(--auth-border);
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 2rem;
position: relative;
overflow: hidden;
}
.auth-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--auth-primary), #6610f2);
}
.auth-header {
text-align: center;
margin-bottom: 2rem;
}
.auth-logo {
margin-bottom: 1rem;
}
.auth-logo img {
max-height: 60px;
}
.auth-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--auth-text);
margin: 0;
}
.auth-subtitle {
color: var(--auth-muted);
font-size: 0.9rem;
margin-top: 0.5rem;
}
.auth-form {
margin-top: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--auth-text);
font-size: 0.875rem;
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--auth-border);
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
background-color: #fff;
}
.form-control:focus {
border-color: var(--auth-primary);
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
outline: none;
}
.form-control.is-invalid {
border-color: #dc3545;
}
.invalid-feedback {
display: block;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875em;
color: #dc3545;
}
.btn-auth {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.btn-primary {
background-color: var(--auth-primary);
color: white;
}
.btn-primary:hover {
background-color: #0b5ed7;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(13, 110, 253, 0.3);
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #5c636a;
transform: translateY(-1px);
}
.auth-links {
text-align: center;
margin-top: 1.5rem;
font-size: 0.9rem;
color: var(--auth-muted);
}
.auth-links a {
color: var(--auth-primary);
text-decoration: none;
font-weight: 500;
}
.auth-links a:hover {
text-decoration: underline;
}
.auth-footer {
text-align: center;
margin-top: 2rem;
font-size: 0.8rem;
color: var(--auth-muted);
border-top: 1px solid var(--auth-border);
padding-top: 1.5rem;
}
/* Responsive */
@media (max-width: 480px) {
.auth-container {
padding: 1rem;
}
.auth-card {
padding: 1.5rem;
}
}
</style>
@stack('styles')
</head>
<body>
<div class="auth-container">
<div class="auth-card">
<!-- Header -->
<div class="auth-header">
@if($showLogo && tenant('logo'))
<div class="auth-logo">
<img src="{{ asset('storage/' . tenant('logo')) }}" alt="{{ tenant('name') }}">
</div>
@endif
<h1 class="auth-title">{{ $title }}</h1>
<p class="auth-subtitle">Bem-vindo ao {{ tenant('name') ?? 'Easy Budget' }}</p>
</div>
<!-- Content -->
<div class="auth-form">
{{ $slot }}
</div>
<!-- Footer -->
<div class="auth-footer">
<p>© {{ date('Y') }} {{ tenant('name') ?? 'Easy Budget' }}. Todos os direitos reservados.</p>
</div>
</div>
</div>
<!-- Scripts -->
@stack('scripts')
</body>
</html>
4. Dashboard Layout Component
Componente para layout de dashboard.
Uso Básico
<x-layout.dashboard :title="$title" :stats="$stats" :charts="$charts" />
Parâmetros
| Parâmetro | Tipo | Descrição | Padrão |
|---|---|---|---|
title |
string |
Título do dashboard | Obrigatório |
stats |
array |
Estatísticas do dashboard | [] |
charts |
array |
Gráficos do dashboard | [] |
filters |
bool |
Exibir filtros | true |
Estrutura
@props([
'title',
'stats' => [],
'charts' => [],
'filters' => true
])
<x-layout.app :title="$title">
<div class="dashboard-container">
<!-- Header -->
<div class="dashboard-header">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h2 mb-0">{{ $title }}</h1>
<p class="text-muted mb-0">Visão geral do seu negócio</p>
</div>
@if($filters)
<div class="dashboard-filters">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary btn-sm active" data-filter="today">Hoje</button>
<button type="button" class="btn btn-outline-primary btn-sm" data-filter="week">Semana</button>
<button type="button" class="btn btn-outline-primary btn-sm" data-filter="month">Mês</button>
<button type="button" class="btn btn-outline-primary btn-sm" data-filter="year">Ano</button>
</div>
</div>
@endif
</div>
</div>
<!-- Stats -->
@if(!empty($stats))
<div class="dashboard-stats">
<div class="row g-4">
@foreach($stats as $stat)
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon {{ $stat['icon_color'] ?? 'bg-primary' }}">
<i class="bi {{ $stat['icon'] }}"></i>
</div>
<div class="stat-content">
<div class="stat-value">{{ $stat['value'] }}</div>
<div class="stat-label">{{ $stat['label'] }}</div>
@if(isset($stat['trend']))
<div class="stat-trend {{ $stat['trend'] >= 0 ? 'trend-up' : 'trend-down' }}">
<i class="bi {{ $stat['trend'] >= 0 ? 'bi-arrow-up' : 'bi-arrow-down' }}"></i>
{{ abs($stat['trend']) }}%
</div>
@endif
</div>
</div>
</div>
@endforeach
</div>
</div>
@endif
<!-- Charts -->
@if(!empty($charts))
<div class="dashboard-charts">
<div class="row g-4">
@foreach($charts as $chart)
<div class="col-md-6">
<div class="chart-card">
<div class="chart-header">
<h5 class="mb-0">{{ $chart['title'] }}</h5>
</div>
<div class="chart-body">
<canvas id="{{ $chart['id'] }}" width="400" height="200"></canvas>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endif
<!-- Content -->
<div class="dashboard-content">
{{ $slot }}
</div>
</div>
@push('scripts')
<script>
// Dashboard interactivity
document.addEventListener('DOMContentLoaded', function() {
const filterButtons = document.querySelectorAll('.dashboard-filters .btn');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
filterButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
// Trigger filter change
const filter = this.getAttribute('data-filter');
applyDashboardFilter(filter);
});
});
// Initialize charts
@foreach($charts as $chart)
initChart('{{ $chart['id'] }}', @json($chart['data']));
@endforeach
});
function applyDashboardFilter(filter) {
// Implement filter logic
console.log('Applying filter:', filter);
}
function initChart(canvasId, data) {
const ctx = document.getElementById(canvasId).getContext('2d');
new Chart(ctx, {
type: data.type || 'line',
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
}
}
}
});
}
</script>
@endpush
</x-layout.app>
5. Sidebar Component
Componente para barra lateral de navegação.
Uso Básico
<x-layout.sidebar :menu="$menu" :active="$active" />
Parâmetros
| Parâmetro | Tipo | Descrição | Padrão |
|---|---|---|---|
menu |
array |
Estrutura do menu | [] |
active |
string |
Item ativo | '' |
user |
User |
Usuário logado | null |
Estrutura
@props([
'menu' => [],
'active' => '',
'user' => null
])
<aside class="sidebar" id="sidebar">
<!-- Logo -->
<div class="sidebar-header">
<div class="sidebar-logo">
@if(tenant('logo'))
<img src="{{ asset('storage/' . tenant('logo')) }}" alt="{{ tenant('name') }}" class="logo-img">
@else
<div class="logo-text">{{ tenant('name') ?? 'Easy Budget' }}</div>
@endif
</div>
<button class="sidebar-toggle" id="sidebarToggle">
<i class="bi bi-list"></i>
</button>
</div>
<!-- User Info -->
@if($user)
<div class="sidebar-user">
<div class="user-avatar">
@if($user->avatar)
<img src="{{ asset('storage/' . $user->avatar) }}" alt="{{ $user->name }}">
@else
<div class="avatar-placeholder">{{ substr($user->name, 0, 2) }}</div>
@endif
</div>
<div class="user-info">
<div class="user-name">{{ $user->name }}</div>
<div class="user-role">{{ $user->role ?? 'Usuário' }}</div>
</div>
</div>
@endif
<!-- Navigation -->
<nav class="sidebar-nav">
<ul class="nav flex-column">
@foreach($menu as $item)
@if(isset($item['divider']))
<li class="nav-divider">{{ $item['divider'] }}</li>
@elseif(isset($item['header']))
<li class="nav-header">{{ $item['header'] }}</li>
@else
<li class="nav-item">
@if(isset($item['children']))
<a class="nav-link nav-dropdown {{ in_array($active, $item['children']) ? 'active' : '' }}" href="#" data-bs-toggle="collapse" data-bs-target="#collapse-{{ Str::slug($item['label']) }}">
<i class="bi {{ $item['icon'] }}"></i>
<span>{{ $item['label'] }}</span>
<i class="bi bi-chevron-down dropdown-icon"></i>
</a>
<div class="collapse {{ in_array($active, $item['children']) ? 'show' : '' }}" id="collapse-{{ Str::slug($item['label']) }}">
<ul class="nav flex-column sub-menu">
@foreach($item['children'] as $child)
<li class="nav-item">
<a class="nav-link {{ $active === $child ? 'active' : '' }}" href="{{ route($child) }}">
<i class="bi bi-circle"></i>
<span>{{ $item['labels'][$child] ?? $child }}</span>
</a>
</li>
@endforeach
</ul>
</div>
@else
<a class="nav-link {{ $active === $item['route'] ? 'active' : '' }}" href="{{ route($item['route']) }}">
<i class="bi {{ $item['icon'] }}"></i>
<span>{{ $item['label'] }}</span>
</a>
@endif
</li>
@endif
@endforeach
</ul>
</nav>
<!-- Footer -->
<div class="sidebar-footer">
<div class="sidebar-version">
<small class="text-muted">Versão {{ config('app.version', '1.0.0') }}</small>
</div>
<div class="sidebar-actions">
<a href="{{ route('profile.edit') }}" class="sidebar-action" title="Perfil">
<i class="bi bi-person"></i>
</a>
<a href="{{ route('settings.index') }}" class="sidebar-action" title="Configurações">
<i class="bi bi-gear"></i>
</a>
<form method="POST" action="{{ route('logout') }}" class="sidebar-action" title="Sair">
@csrf
<button type="submit" class="btn-sidebar-action">
<i class="bi bi-box-arrow-right"></i>
</button>
</form>
</div>
</div>
</aside>
@push('styles')
<style>
.sidebar {
width: 250px;
background-color: #fff;
border-right: 1px solid #e9ecef;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 1000;
transition: transform 0.3s ease;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo-img {
max-height: 40px;
width: auto;
}
.logo-text {
font-size: 1.2rem;
font-weight: bold;
color: #0d6efd;
}
.sidebar-toggle {
display: none;
background: none;
border: none;
font-size: 1.2rem;
color: #6c757d;
cursor: pointer;
}
.sidebar-user {
padding: 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
align-items: center;
gap: 15px;
}
.user-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.avatar-placeholder {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #0d6efd;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1rem;
}
.user-info {
flex: 1;
}
.user-name {
font-weight: 600;
font-size: 1rem;
}
.user-role {
font-size: 0.8rem;
color: #6c757d;
}
.sidebar-nav {
padding: 20px;
flex: 1;
overflow-y: auto;
}
.nav-link {
color: #333;
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 5px;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
}
.nav-link:hover {
background-color: #f8f9fa;
color: #0d6efd;
}
.nav-link.active {
background-color: #e7f1ff;
color: #0d6efd;
border-left: 4px solid #0d6efd;
}
.nav-link i {
font-size: 1.1rem;
width: 20px;
text-align: center;
}
.nav-dropdown {
position: relative;
}
.dropdown-icon {
margin-left: auto;
transition: transform 0.2s ease;
}
.nav-dropdown[aria-expanded="true"] .dropdown-icon {
transform: rotate(180deg);
}
.sub-menu {
padding-left: 30px;
}
.sub-menu .nav-link {
padding: 8px 15px;
font-size: 0.9rem;
border-left: 2px solid transparent;
}
.sub-menu .nav-link.active {
border-left-color: #0d6efd;
background-color: transparent;
}
.nav-divider {
padding: 10px 20px;
font-size: 0.8rem;
text-transform: uppercase;
color: #6c757d;
font-weight: 600;
letter-spacing: 0.5px;
}
.nav-header {
padding: 15px 20px;
font-size: 0.9rem;
color: #6c757d;
font-weight: 600;
border-bottom: 1px solid #e9ecef;
}
.sidebar-footer {
padding: 20px;
border-top: 1px solid #e9ecef;
display: flex;
flex-direction: column;
gap: 15px;
}
.sidebar-actions {
display: flex;
gap: 10px;
}
.sidebar-action {
background: none;
border: none;
color: #6c757d;
cursor: pointer;
padding: 8px;
border-radius: 6px;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
}
.sidebar-action:hover {
background-color: #f8f9fa;
color: #0d6efd;
}
.btn-sidebar-action {
background: none;
border: none;
color: #6c757d;
cursor: pointer;
padding: 8px;
border-radius: 6px;
transition: all 0.2s ease;
width: 100%;
text-align: left;
}
.btn-sidebar-action:hover {
background-color: #f8f9fa;
color: #0d6efd;
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
}
.sidebar.show {
transform: translateX(0);
}
.sidebar-toggle {
display: block;
}
main {
margin-left: 0 !important;
}
}
</style>
@endpush
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const sidebar = document.getElementById('sidebar');
const sidebarToggle = document.getElementById('sidebarToggle');
// Toggle sidebar
if (sidebarToggle) {
sidebarToggle.addEventListener('click', function() {
sidebar.classList.toggle('show');
});
}
// Close sidebar on mobile when clicking outside
document.addEventListener('click', function(event) {
if (window.innerWidth <= 768) {
const isClickInsideSidebar = sidebar.contains(event.target);
const isClickOnToggle = sidebarToggle.contains(event.target);
if (!isClickInsideSidebar && !isClickOnToggle) {
sidebar.classList.remove('show');
}
}
});
// Handle dropdowns
const dropdowns = document.querySelectorAll('.nav-dropdown');
dropdowns.forEach(dropdown => {
dropdown.addEventListener('click', function(e) {
e.preventDefault();
const target = this.getAttribute('data-bs-target');
const collapse = document.querySelector(target);
if (collapse) {
const isExpanded = collapse.classList.contains('show');
// Close all other dropdowns
document.querySelectorAll('.sub-menu.show').forEach(other => {
if (other !== collapse) {
other.classList.remove('show');
}
});
// Toggle current dropdown
if (isExpanded) {
collapse.classList.remove('show');
} else {
collapse.classList.add('show');
}
}
});
});
});
</script>
@endpush
6. Navbar Component
Componente para barra de navegação superior.
Uso Básico
<x-layout.navbar :user="$user" :notifications="$notifications" />
Parâmetros
| Parâmetro | Tipo | Descrição | Padrão |
|---|---|---|---|
user |
User |
Usuário logado | Obrigatório |
notifications |
array |
Notificações | [] |
search |
bool |
Exibir campo de busca | true |
Estrutura
@props([
'user',
'notifications' => [],
'search' => true
])
<nav class="navbar navbar-expand-lg navbar-light bg-white border-bottom">
<div class="container-fluid">
<!-- Brand -->
<a class="navbar-brand d-flex align-items-center" href="{{ route('dashboard') }}">
@if(tenant('logo'))
<img src="{{ asset('storage/' . tenant('logo')) }}" alt="{{ tenant('name') }}" class="brand-logo">
@else
<span class="brand-text">{{ tenant('name') ?? 'Easy Budget' }}</span>
@endif
</a>
<!-- Toggler -->
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent">
<span class="navbar-toggler-icon"></span>
</button>
<!-- Content -->
<div class="collapse navbar-collapse" id="navbarContent">
<!-- Search -->
@if($search)
<form class="d-flex me-auto" style="max-width: 400px;">
<div class="input-group">
<input type="text" class="form-control" placeholder="Buscar..." aria-label="Buscar">
<button class="btn btn-outline-secondary" type="button">
<i class="bi bi-search"></i>
</button>
</div>
</form>
@endif
<!-- Right Menu -->
<ul class="navbar-nav ms-auto align-items-center">
<!-- Notifications -->
<li class="nav-item dropdown me-3">
<a class="nav-link position-relative" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-bell fs-5"></i>
@if(count($notifications) > 0)
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{{ count($notifications) }}
</span>
@endif
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><h6 class="dropdown-header">Notificações</h6></li>
@forelse($notifications as $notification)
<li>
<a class="dropdown-item" href="{{ $notification['url'] }}">
<div class="d-flex w-100 justify-content-between">
<div>
<strong>{{ $notification['title'] }}</strong>
<div class="small text-muted">{{ $notification['message'] }}</div>
</div>
<small class="text-muted">{{ $notification['time'] }}</small>
</div>
</a>
</li>
@empty
<li><span class="dropdown-item text-muted">Nenhuma notificação</span></li>
@endforelse
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-primary" href="{{ route('notifications.index') }}">Ver todas</a></li>
</ul>
</li>
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link d-flex align-items-center" href="#" role="button" data-bs-toggle="dropdown">
@if($user->avatar)
<img src="{{ asset('storage/' . $user->avatar) }}" alt="{{ $user->name }}" class="rounded-circle me-2" style="width: 32px; height: 32px; object-fit: cover;">
@else
<div class="avatar-circle me-2">{{ substr($user->name, 0, 2) }}</div>
@endif
<span class="d-none d-md-inline">{{ $user->name }}</span>
<i class="bi bi-chevron-down ms-1"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{{ route('profile.edit') }}"><i class="bi bi-person me-2"></i>Perfil</a></li>
<li><a class="dropdown-item" href="{{ route('settings.index') }}"><i class="bi bi-gear me-2"></i>Configurações</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="dropdown-item"><i class="bi bi-box-arrow-right me-2"></i>Sair</button>
</form>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
@push('styles')
<style>
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 100;
}
.brand-logo {
max-height: 30px;
margin-right: 10px;
}
.brand-text {
font-weight: bold;
color: #0d6efd;
font-size: 1.2rem;
}
.avatar-circle {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: #0d6efd;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.9rem;
}
.navbar-nav .nav-link {
color: #333;
padding: 0.5rem 1rem;
border-radius: 6px;
transition: all 0.2s ease;
}
.navbar-nav .nav-link:hover {
background-color: #f8f9fa;
color: #0d6efd;
}
.dropdown-menu {
border: 1px solid #e9ecef;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.dropdown-item {
padding: 0.75rem 1rem;
transition: all 0.2s ease;
}
.dro
…(truncated)