
layout-navigation-components
by JoseApRJunior
Easy-Budget migrado para Laravel
SKILL.md
name: layout-navigation-components description: Componentes de UI para layout e navegação seguindo o padrão de components do Easy Budget.
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;
}
.dropdown-item:hover {
background-color: #f8f9fa;
}
/* Responsive */
@media (max-width: 768px) {
.brand-text {
display: none;
}
.navbar-nav {
margin-top: 1rem;
}
.navbar-nav .nav-link {
padding: 0.75rem 1rem;
}
}
</style>
@endpush
7. Breadcrumbs Component
Componente para breadcrumb de navegação.
Uso Básico
<x-layout.breadcrumbs :breadcrumbs="$breadcrumbs" />
Parâmetros
| Parâmetro | Tipo | Descrição | Padrão |
|---|---|---|---|
breadcrumbs | array | Estrutura do breadcrumb | Obrigatório |
Estrutura
@props(['breadcrumbs'])
@if(!empty($breadcrumbs))
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
@foreach($breadcrumbs as $breadcrumb)
@if($loop->last)
<li class="breadcrumb-item active" aria-current="page">
@if(isset($breadcrumb['icon']))
<i class="bi {{ $breadcrumb['icon'] }} me-2"></i>
@endif
{{ $breadcrumb['label'] }}
</li>
@else
<li class="breadcrumb-item">
<a href="{{ $breadcrumb['url'] }}">
@if(isset($breadcrumb['icon']))
<i class="bi {{ $breadcrumb['icon'] }} me-2"></i>
@endif
{{ $breadcrumb['label'] }}
</a>
</li>
@endif
@endforeach
</ol>
</nav>
@endif
@push('styles')
<style>
.breadcrumb {
background-color: transparent;
padding: 0;
margin-bottom: 1.5rem;
border-radius: 0;
}
.breadcrumb-item {
font-size: 0.9rem;
color: #6c757d;
}
.breadcrumb-item a {
color: #0d6efd;
text-decoration: none;
transition: color 0.2s ease;
}
.breadcrumb-item a:hover {
color: #0b5ed7;
text-decoration: underline;
}
.breadcrumb-item.active {
color: #333;
font-weight: 500;
}
.breadcrumb-item + .breadcrumb-item::before {
color: #6c757d;
}
</style>
@endpush
8. Footer Component
Componente para rodapé do sistema.
Uso Básico
<x-layout.footer :links="$links" :social="$social" />
Parâmetros
| Parâmetro | Tipo | Descrição | Padrão |
|---|---|---|---|
links | array | Links do rodapé | [] |
social | array | Links sociais | [] |
showCopyright | bool | Exibir copyright | true |
Estrutura
@props([
'links' => [],
'social' => [],
'showCopyright' => true
])
<footer class="footer bg-light border-top">
<div class="container">
<div class="row">
<!-- Company Info -->
<div class="col-md-4 mb-3 mb-md-0">
<div class="footer-logo">
@if(tenant('logo'))
<img src="{{ asset('storage/' . tenant('logo')) }}" alt="{{ tenant('name') }}" class="footer-logo-img">
@else
<h5 class="footer-title">{{ tenant('name') ?? 'Easy Budget' }}</h5>
@endif
</div>
<p class="footer-description text-muted">
{{ tenant('description') ?? 'Sua solução de gestão empresarial completa.' }}
</p>
</div>
<!-- Quick Links -->
@if(!empty($links))
<div class="col-md-2 mb-3 mb-md-0">
<h6 class="footer-heading">Links Rápidos</h6>
<ul class="list-unstyled">
@foreach($links as $link)
<li><a href="{{ $link['url'] }}" class="footer-link">{{ $link['label'] }}</a></li>
@endforeach
</ul>
</div>
@endif
<!-- Support -->
<div class="col-md-3 mb-3 mb-md-0">
<h6 class="footer-heading">Suporte</h6>
<ul class="list-unstyled">
<li><a href="{{ route('support.create') }}" class="footer-link">Centro de Ajuda</a></li>
<li><a href="{{ route('docs.index') }}" class="footer-link">Documentação</a></li>
<li><a href="{{ route('contact.index') }}" class="footer-link">Contato</a></li>
</ul>
</div>
<!-- Social -->
@if(!empty($social))
<div class="col-md-3">
<h6 class="footer-heading">Siga-nos</h6>
<div class="social-links">
@foreach($social as $socialLink)
<a href="{{ $socialLink['url'] }}" class="social-link" target="_blank" rel="noopener noreferrer">
<i class="bi bi-{{ $socialLink['icon'] }}"></i>
</a>
@endforeach
</div>
</div>
@endif
</div>
<!-- Bottom Bar -->
<div class="row mt-4">
<div class="col-12">
<div class="footer-bottom">
@if($showCopyright)
<div class="copyright">
© {{ date('Y') }} {{ tenant('name') ?? 'Easy Budget' }}. Todos os direitos reservados.
</div>
@endif
<div class="footer-links">
<a href="{{ route('terms.index') }}" class="footer-link small">Termos de Uso</a>
<a href="{{ route('privacy.index') }}" class="footer-link small">Política de Privacidade</a>
<a href="{{ route('cookies.index') }}" class="footer-link small">Cookies</a>
</div>
</div>
</div>
</div>
</div>
</footer>
@push('styles')
<style>
.footer {
margin-top: auto;
}
.footer-logo-img {
max-height: 40px;
margin-bottom: 10px;
}
.footer-title {
color: #0d6efd;
margin-bottom: 10px;
}
.footer-description {
font-size: 0.9rem;
line-height: 1.5;
}
.footer-heading {
font-size: 1rem;
font-weight: 600;
color: #333;
margin-bottom: 15px;
position: relative;
padding-bottom: 10px;
}
.footer-heading::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 40px;
height: 2px;
background-color: #0d6efd;
}
.footer-link {
color: #6c757d;
text-decoration: none;
font-size: 0.9rem;
transition: color 0.2s ease;
}
.footer-link:hover {
color: #0d6efd;
text-decoration: none;
}
.social-links {
display: flex;
gap: 10px;
}
.social-link {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background-color: #0d6efd;
color: white;
border-radius: 50%;
text-decoration: none;
transition: all 0.2s ease;
}
.social-link:hover {
background-color: #0b5ed7;
transform: translateY(-1px);
}
.footer-bottom {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 20px;
border-top: 1px solid #e9ecef;
margin-top: 20px;
}
.copyright {
font-size: 0.8rem;
color: #6c757d;
}
.footer-links {
display: flex;
gap: 15px;
}
/* Responsive */
@media (max-width: 768px) {
.footer-bottom {
flex-direction: column;
gap: 10px;
text-align: center;
}
.footer-links {
justify-content: center;
}
}
</style>
@endpush
9. Theme Switcher Component
Componente para troca de tema.
Uso Básico
<x-layout.theme-switcher :currentTheme="$currentTheme" />
Parâmetros
| Parâmetro | Tipo | Descrição | Padrão |
|---|---|---|---|
currentTheme | string | Tema atual | auto |
themes | array | Temas disponíveis | [] |
Estrutura
@props([
'currentTheme' => 'auto',
'themes' => [
'auto' => ['name' => 'Automático', 'icon' => 'bi-circle-half'],
'light' => ['name' => 'Claro', 'icon' => 'bi-sun'],
'dark' => ['name' => 'Escuro', 'icon' => 'bi-moon']
]
])
<div class="theme-switcher" id="themeSwitcher">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="{{ $themes[$currentTheme]['icon'] }} me-2"></i>
{{ $themes[$currentTheme]['name'] }}
</button>
<ul class="dropdown-menu">
@foreach($themes as $key => $theme)
<li>
<button class="dropdown-item theme-option" type="button" data-theme="{{ $key }}">
<i class="{{ $theme['icon'] }} me-2"></i>
{{ $theme['name'] }}
</button>
</li>
@endforeach
</ul>
</div>
@push('styles')
<style>
.theme-switcher {
display: inline-block;
}
.theme-option {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 8px 16px;
border: none;
background: none;
cursor: pointer;
text-align: left;
font-size: 0.9rem;
color: #333;
transition: all 0.2s ease;
}
.theme-option:hover {
background-color: #f8f9fa;
color: #0d6efd;
}
.theme-option.active {
background-color: #e7f1ff;
color: #0d6efd;
font-weight: 500;
}
</style>
@endpush
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const themeSwitcher = document.getElementById('themeSwitcher');
const themeOptions = document.querySelectorAll('.theme-option');
const currentTheme = localStorage.getItem('theme') || 'auto';
// Apply initial theme
applyTheme(currentTheme);
// Handle theme changes
themeOptions.forEach(option => {
option.addEventListener('click', function() {
const theme = this.getAttribute('data-theme');
applyTheme(theme);
localStorage.setItem('theme', theme);
});
});
function applyTheme(theme) {
// Update data-theme attribute
document.documentElement.setAttribute('data-theme', theme);
// Update button text and icon
const button = themeSwitcher.querySelector('button');
const icon = button.querySelector('i');
const text = button.querySelector('span');
// Update button content based on theme
const themeData = getThemeData(theme);
icon.className = themeData.icon + ' me-2';
// Update dropdown items
themeOptions.forEach(option => {
option.classList.remove('active');
if (option.getAttribute('data-theme') === theme) {
option.classList.add('active');
}
});
// Dispatch theme change event
window.dispatchEvent(new CustomEvent('themechange', { detail: { theme } }));
}
function getThemeData(theme) {
const themes = @json($themes);
return themes[theme] || themes.auto;
}
// Listen for system theme changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', (e) => {
if (currentTheme === 'auto') {
applyTheme(e.matches ? 'dark' : 'light');
}
});
});
</script>
@endpush
10. Notifications Component
Componente para sistema de notificações.
Uso Básico
<x-layout.notifications :notifications="$notifications" />
Parâmetros
| Parâmetro | Tipo | Descrição | Padrão |
|---|---|---|---|
notifications | array | Notificações | [] |
maxVisible | int | Máximo de notificações visíveis | 5 |
Estrutura
@props([
'notifications' => [],
'maxVisible' => 5
])
<div class="notifications-container" id="notificationsContainer">
@foreach($notifications as $notification)
<div class="notification-item {{ $notification['type'] }}" data-id="{{ $notification['id'] }}">
<div class="notification-content">
<div class="notification-icon">
<i class="bi {{ $notification['icon'] }}"></i>
</div>
<div class="notification-body">
<div class="notification-title">{{ $notification['title'] }}</div>
<div class="notification-message">{{ $notification['message'] }}</div>
<div class="notification-time">{{ $notification['time'] }}</div>
</div>
<button class="notification-close" type="button" data-id="{{ $notification['id'] }}">
<i class="bi bi-x"></i>
</button>
</div>
</div>
@endforeach
</div>
@push('styles')
<style>
.notifications-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
display: flex;
flex-direction: column;
gap: 10px;
max-width: 350px;
}
.notification-item {
background: white;
border: 1px solid #e9ecef;
border-left: 4px solid;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
animation: slideIn 0.3s ease;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.notification-item.show {
transform: translateX(0);
}
.notification-item.success {
border-left-color: #27ae60;
}
.notification-item.error {
border-left-color: #e74c3c;
}
.notification-item.warning {
border-left-color: #f39c12;
}
.notification-item.info {
border-left-color: #3498db;
}
.notification-content {
display: flex;
gap: 12px;
padding: 15px;
position: relative;
}
.notification-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
color: white;
flex-shrink: 0;
}
.notification-item.success .notification-icon {
background-color: #27ae60;
}
.notification-item.error .notification-icon {
background-color: #e74c3c;
}
.notification-item.warning .notification-icon {
background-color: #f39c12;
}
.notification-item.info .notification-icon {
background-color: #3498db;
}
.notification-body {
flex: 1;
min-width: 0;
}
.notification-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 4px;
color: #333;
}
.notification-message {
font-size: 0.85rem;
color: #666;
margin-bottom: 8px;
line-height: 1.4;
}
.notification-time {
font-size: 0.75rem;
color: #999;
}
.notification-close {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
color: #999;
cursor: pointer;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.notification-close:hover {
background-color: #f8f9fa;
color: #333;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.notification-item.removing {
animation: slideOut 0.3s ease forwards;
}
/* Responsive */
@media (max-width: 768px) {
.notifications-container {
right: 15px;
left: 15px;
max-width: none;
}
.notification-item {
max-width: none;
}
}
</style>
@endpush
@push('scripts')
<script>
class NotificationManager {
constructor() {
this.container = document.getElementById('notificationsContainer');
this.notifications = new Map();
}
show(notification) {
const id = notification.id || Date.now();
const element = this.createNotificationElement(notification, id);
this.container.appendChild(element);
this.notifications.set(id, element);
// Auto-hide after 5 seconds
setTimeout(() => {
this.hide(id);
}, 5000);
return id;
}
hide(id) {
const element = this.notifications.get(id);
if (element) {
element.classList.add('removing');
setTimeout(() => {
element.remove();
this.notifications.delete(id);
}, 300);
}
}
createNotificationElement(notification, id) {
const item = document.createElement('div');
item.className = `notification-item ${notification.type || 'info'} show`;
item.setAttribute('data-id', id);
item.innerHTML = `
<div class="notification-content">
<div class="notification-icon">
<i class="bi ${notification.icon || 'bi-info-circle'}"></i>
</div>
<div class="notification-body">
<div class="notification-title">${notification.title}</div>
<div class="notification-message">${notification.message}</div>
<div class="notification-time">${notification.time || new Date().toLocaleTimeString()}</div>
</div>
<button class="notification-close" type="button" data-id="${id}">
<i class="bi bi-x"></i>
</button>
</div>
`;
// Add close button event
const closeButton = item.querySelector('.notification-close');
closeButton.addEventListener('click', () => {
this.hide(id);
});
return item;
}
}
// Initialize notification manager
window.notificationManager = new NotificationManager();
// Handle existing notifications
document.addEventListener('DOMContentLoaded', function() {
const notifications = @json($notifications);
notifications.forEach(notification => {
window.notificationManager.show(notification);
});
});
// Global function to show notifications
window.showNotification = function(notification) {
if (window.notificationManager) {
return window.notificationManager.show(notification);
}
};
</script>
@endpush
11. Integração com Padrões Existentes
Uso em Views
{{-- Layout Principal --}}
<x-layout.app :title="'Dashboard - ' . tenant('name')">
<x-layout.breadcrumbs :breadcrumbs="[
['label' => 'Dashboard', 'url' => route('dashboard')],
['label' => 'Visão Geral']
]" />
<div class="dashboard-content">
<!-- Conteúdo da página -->
</div>
</x-layout.app>
{{-- Dashboard --}}
<x-layout.dashboard
:title="'Visão Geral do Negócio'"
:stats="$stats"
:charts="$charts"
/>
{{-- Autenticação --}}
<x-layout.auth :title="'Login'">
<form method="POST" action="{{ route('login') }}">
@csrf
<!-- Campos do formulário -->
</form>
</x-layout.auth>
Estilos CSS
/* Layout Components Styles */
.sidebar {
width: 250px;
background-color: #fff;
border-right: 1px solid #e9ecef;
}
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 100;
}
.footer {
margin-top: auto;
background-color: #f8f9fa;
border-top: 1px solid #e9ecef;
}
.notification-item {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
12. JavaScript Interatividade
Sistema de Layout
// layout-system.js
document.addEventListener('DOMContentLoaded', function() {
// Sidebar toggle
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
if (sidebarToggle && sidebar) {
sidebarToggle.addEventListener('click', function() {
sidebar.classList.toggle('show');
});
}
// Theme switcher
const themeSwitcher = document.getElementById('themeSwitcher');
if (themeSwitcher) {
// Theme switching logic
}
// Responsive navigation
window.addEventListener('resize', function() {
if (window.innerWidth > 768) {
sidebar.classList.remove('show');
}
});
});
13. Validação e Segurança
Autorização
// LayoutPolicy.php
public function viewSidebar(User $user)
{
return $user->tenant_id === tenant('id');
}
public function viewNotifications(User $user)
{
return $user->tenant_id === tenant('id');
}
public function changeTheme(User $user)
{
return $user->can('manage-settings');
}
Validations
{{-- Sidebar com validação de permissões --}}
@can('viewSidebar')
<x-layout.sidebar :menu="$menu" :active="$active" />
@endcan
{{-- Theme Switcher com validação de permissões --}}
@can('changeTheme')
<x-layout.theme-switcher :currentTheme="$currentTheme" />
@endcan
Este padrão de components para layout e navegação garante consistência visual, reutilização de código e manutenibilidade, seguindo os mesmos princípios estabelecidos nos outros components do sistema.
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon