sistemas-eel / portal-ui
Biblioteca de UI Blade-first reutilizável para sistemas Laravel.
Requires
- php: ^7.4 || ^8.0
- illuminate/support: ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
- illuminate/view: ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0
Requires (Dev)
- orchestra/testbench: ^6.47 || ^7.56 || ^8.37 || ^9.17 || ^10.11 || ^11.1
- phpunit/phpunit: ^9.6 || ^10.5 || ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Biblioteca de UI Laravel reutilizável para sistemas administrativos. O pacote é Blade-first: entrega layouts, componentes, configuração e assets compilados sem exigir Livewire, Alpine, Tailwind ou Vite no app consumidor.
Versão atual: v0.3.0.
Comece aqui
Se esta é sua primeira instalação, siga o tutorial Primeira tela com Portal UI. Ele começa em um projeto Laravel novo e termina com uma página acessível no navegador, contendo layout, topbar, menu, card, ícones e modo escuro.
O caminho automático recomendado é:
composer require sistemas-eel/portal-ui:^0.2 php artisan portal-ui:install --starter php artisan serve
Depois, abra /portal-ui-starter. O instalador preserva arquivos existentes por padrão.
Guias complementares:
- Autenticação: como transformar a primeira tela em uma área protegida.
- Integração SenhaÚnica: layout, rotas administrativas, LoginAs e gate
admin. - Solução de problemas: diagnóstico por sintoma.
- Exemplos de componentes: navegação e telas mais completas.
Requisitos
{
"php": "^7.4 || ^8.0",
"illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0",
"illuminate/view": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0 || ^13.0"
}
Essas combinações são verificadas continuamente no GitHub Actions com uma matriz que cobre PHP 7.4 a 8.4 e Laravel 8 a 13, usando a geração correspondente do Orchestra Testbench.
Instalação
Instale o pacote no app consumidor com Composer:
composer require sistemas-eel/portal-ui
O Laravel executa o package discovery automaticamente durante a instalação. Se a descoberta automática estiver desabilitada no projeto, execute manualmente:
php artisan package:discover
Se quiser fixar uma faixa de versão, use sistemas-eel/portal-ui:^0.2.
Publicação
O instalador publica configuração e assets automaticamente:
php artisan portal-ui:install
Para também gerar uma primeira tela funcional:
php artisan portal-ui:install --starter
Os comandos de publicação equivalentes são:
php artisan vendor:publish --tag=portal-ui-config php artisan vendor:publish --tag=portal-ui-assets php artisan vendor:publish --tag=portal-ui-stubs
Views tematizadas para uspdev/senhaunica-socialite podem ser publicadas separadamente quando o app consumidor precisar customizar algum ponto:
php artisan vendor:publish --tag=portal-ui-senhaunica-views
Publique views apenas quando precisar sobrescrever Blade no app consumidor:
php artisan vendor:publish --tag=portal-ui-views
Regra prática:
assets: necessários para carregar o CSS/JS compilado do tema.config: útil para ajustar marca, cores, rotas, navegação e mensagens.stubs: modelos opcionais de navegação e rotas de demo.portal-ui-senhaunica-views: opcional; copia apenas os overrides tematizados da SenhaUnica.views: use somente para customizações locais de layout, partials ou componentes.
Evite publicar views sem necessidade. Quando elas existem em resources/views/vendor/portal-ui, o Laravel usa a cópia local e deixa de acompanhar automaticamente as views do pacote.
O instalador não sobrescreve configuração, layout, view ou rota do starter já existentes. A opção --force permite a sobrescrita explícita dos arquivos gerenciados e da configuração; use-a somente depois de revisar customizações locais. A inclusão criada em routes/web.php é marcada e nunca é duplicada.
Atualização para 0.2
Depois de atualizar o pacote, republique os assets e limpe os caches:
composer require sistemas-eel/portal-ui:^0.2 -W php artisan vendor:publish --tag=portal-ui-assets --force php artisan optimize:clear php artisan portal-ui:doctor
O --force é necessário porque o Composer atualiza vendor, mas não substitui automaticamente arquivos que já estão em public/vendor/portal-ui. Se o sistema publicou views da SenhaÚnica, compare suas customizações antes de republicá-las; essas cópias locais têm prioridade sobre as correções do pacote.
Uso Rápido
Para uma instalação nova, prefira o tutorial completo da primeira tela. Os exemplos abaixo servem como referência rápida para projetos que já possuem rotas e layouts organizados.
Layout de área interna, normalmente usado depois da autenticação:
@extends('portal-ui::layouts.app') @section('title', 'Painel') @section('content') <x-portal::page-header title="Painel" subtitle="Resumo da operação" /> <x-portal::card> <x-slot name="header"> Indicadores </x-slot> Conteúdo principal da página. </x-portal::card> @endsection
Se o sistema já padroniza suas páginas com @extends('layouts.app'), crie um layout intermediário mínimo:
{{-- resources/views/layouts/app.blade.php --}} @extends('portal-ui::layouts.app')
As páginas da aplicação continuam simples:
@extends('layouts.app') @section('title', 'Teste autenticado') @section('content') <x-portal::card>Usuário autenticado: {{ auth()->user()->name }}</x-portal::card> @endsection
Não copie o conteúdo interno de portal-ui::layouts.app para esse arquivo. O layout do pacote já monta o <head>, topbar, sidebar, stacks e referências ao CSS/JS; manter apenas a extensão evita que o consumidor fique preso a uma versão antiga.
Se a aplicação utiliza classes Tailwind próprias, carregue também o CSS compilado pelo Vite:
@extends('portal-ui::layouts.app') @push('styles') @vite('resources/css/app.css') @endpush
Layout visitante:
@extends('portal-ui::layouts.guest') @section('title', 'Entrar') @section('content') <x-portal::input label="E-mail" name="email" type="email" /> <x-portal::input label="Senha" name="password" type="password" /> <x-portal::button type="submit" full="true">Entrar</x-portal::button> @endsection
Também existem componentes de layout baseados em classe:
<x-portal-app-layout title="Painel"> <x-portal::card> Conteúdo principal. </x-portal::card> </x-portal-app-layout>
Configuração
O arquivo publicado é:
config/portal-ui.php
Principais seções:
brand: nome, subtítulo, logo e favicon.colors: tokens visuais principais.layout: comportamento geral do layout.assets: carregamento de CSS/JS.navigation: grupos e itens da sidebar.routes: nomes de rotas comuns, comologin,logoutehome.integrations: integrações opcionais com outros pacotes, comouspdev/senhaunica-socialite.flash: chaves de sessão usadas por mensagens.
Integração SenhaUnica
O Portal UI detecta uspdev/senhaunica-socialite automaticamente. Quando o service provider da dependência está disponível e portal-ui.integrations.senhaunica.enabled está ativo, o pacote registra views tematizadas no namespace senhaunica.
Sem a biblioteca instalada, a integração permanece inativa e não registra um namespace incompleto. Ainda é seguro executar vendor:publish --tag=portal-ui-senhaunica-views: a publicação apenas copia arquivos e não os renderiza.
A ordem de resolução fica:
resources/views/vendor/senhaunicado sistema consumidor.resources/views/integrations/senhaunicadoportal-ui.- Views originais registradas por
uspdev/senhaunica-socialite.
Isso permite que os sistemas usem a lista de usuários, permissões, LoginAs, modais e login local da SenhaUnica já adaptados ao tema, sem copiar todas as views para cada aplicação.
Para desabilitar:
PORTAL_UI_SENHAUNICA_VIEWS=false
Para forçar a ativação, use PORTAL_UI_SENHAUNICA_VIEWS=true, mas a dependência ainda precisa estar instalada. Se ela estiver ausente, portal-ui:doctor apresentará um aviso e as views não serão registradas.
Por padrão, as páginas da integração estendem diretamente portal-ui::layouts.app. Para fazê-las passar pelo layout intermediário do sistema:
PORTAL_UI_SENHAUNICA_LAYOUT=layouts.app
O layout escolhido precisa expor a seção content. A configuração antiga senhaunica.template não controla as views fornecidas pelo Portal UI 0.2.
Autorização e erro 403
O Portal UI apenas substitui a apresentação das telas; ele não concede acesso administrativo. O uspdev/senhaunica-socialite chama $this->authorize('admin') em /senhaunica-users e /loginas, portanto o usuário autenticado precisa ser reconhecido pelo gate admin no app consumidor.
Se a autenticação vem de outro cliente SSO, valide especialmente:
- se o model autenticado possui as permissões/roles esperadas;
- se o
guard_nameusado pelo Spatie Permission coincide com o guard da sessão; - se a lista
SENHAUNICA_ADMINSfoi convertida em permissão no fluxo de login; - se
Gate::allows('admin')retornatruepara o usuário que deveria administrar.
Quando for necessário adaptar uma fonte externa de administradores ao gate exigido pela SenhaÚnica, essa ponte deve ficar no AppServiceProvider (ou em um provider de autorização) da aplicação. Ela não deve ficar no Portal UI: uma biblioteca visual não conhece a política de segurança nem pode tornar usuários administradores por conta própria.
Navegação
A sidebar é montada por config('portal-ui.navigation.groups'). Se não houver item visível, a sidebar e o botão de menu não são renderizados.
Exemplo mínimo:
'navigation' => [ 'hide_missing_routes' => true, 'groups' => [ 'main' => [ 'label' => 'Menu Principal', 'items' => [ [ 'label' => 'Início', 'route' => 'home', 'icon' => 'fa-home', 'active' => 'home', ], ], ], ], ],
Itens podem usar route, url, icon, active, can, guest, children, external, target e rel. Quando url for absoluta (http/https) ou external for true, o item abre em nova aba com rel="noopener noreferrer" por padrão.
Submenus usam children e o item pai fica visível quando tiver pelo menos um filho visível:
[
'label' => 'Cadastros',
'icon' => 'fa-folder',
'active' => 'cadastros.*',
'children' => [
[
'label' => 'Unidades',
'route' => 'unidades.index',
'active' => 'unidades.*',
],
[
'label' => 'Manual',
'url' => 'https://example.org/manual',
'external' => true,
],
],
]
Menus mais completos estão em docs/examples.md.
Componentes
Componentes disponíveis com prefixo x-portal:::
alertbadgebuttoncardconfirm-modalempty-stateflash-messagesiconinputmodalpage-headerresource-actionssection-footersection-headerselectflash-alertsidebar-itemswitchtable-actionstabletextarea
Exemplo:
<x-portal::alert variant="success" title="Sucesso"> Operação realizada. </x-portal::alert> <x-portal::button type="submit" icon="fa-save"> Salvar </x-portal::button> <x-portal::icon name="fa-user" /> <x-portal::icon name="fa-brands fa-github" label="GitHub" /> <x-portal::confirm-modal wire:model="showConfirmModal" title="Confirmar exclusao" message="Este registro sera excluido permanentemente." confirm-label="Excluir" confirm-variant="danger" confirm-icon="fa-trash" confirm-action="confirmarExclusao" cancel-action="fecharConfirmacaoExclusao" /> <x-portal::empty-state title="Nenhum registro encontrado" message="Assim que houver dados, eles aparecerao aqui." icon="fa-inbox" /> <x-portal::table> <x-slot name="head"> <tr> <th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500">Nome</th> </tr> </x-slot> <x-slot name="body"> <tr> <td class="px-4 py-3 text-sm text-gray-600">Exemplo</td> </tr> </x-slot> </x-portal::table> <x-portal::section-footer align="center"> <x-portal::button variant="secondary" icon="fa-plus">Adicionar item</x-portal::button> </x-portal::section-footer> <x-portal::resource-actions :view-click="'visualizar('.$item->id.')'" :edit-click="'editar('.$item->id.')'" :delete-click="'confirmarExclusao('.$item->id.')'" mode="icon" /> <x-portal::resource-actions :view-href="route('items.show', $item)" :edit-href="route('items.edit', $item)" only="view,edit" mode="label" />
Assets
O pacote distribui:
public/portal-ui.csspublic/portal-ui.js- fontes locais do Font Awesome (
public/fa-*.woff2)
O app consumidor deve publicar esses assets para public/vendor/portal-ui:
php artisan vendor:publish --tag=portal-ui-assets
O CSS da versão 0.2 já inclui o Font Awesome e referencia as fontes publicadas no mesmo diretório. Assim, os ícones funcionam sem CDN. O carregamento externo permanece disponível apenas como fallback explícito:
PORTAL_UI_FONTAWESOME_CDN=true
Se os ícones não aparecerem depois de uma atualização, execute a publicação com --force e rode php artisan portal-ui:doctor. O diagnóstico também detecta arquivos de fonte ausentes, layout SenhaÚnica inexistente e CSS/JS defasados.
Ao alterar CSS ou JavaScript do pacote, gere novamente os arquivos distribuíveis:
npm install npm run build
Extensão de CSS e JS
Os layouts expõem stacks para CSS, metas e scripts customizados:
portal-ui-head: conteúdo extra no<head>.portal-ui-before-scripts: scripts antes doportal-ui.js.portal-ui-after-scripts: scripts depois doportal-ui.js.
Exemplo:
@push('portal-ui-head') <style> .painel-kpi { border-color: var(--portal-ui-primary); } </style> @endpush @push('portal-ui-after-scripts') <script> console.log('JS extra da tela'); </script> @endpush
Livewire
Livewire não é dependência obrigatória. O pacote não registra componentes Livewire nem inclui @livewireStyles ou @livewireScripts no layout.
Apps consumidores que usam Livewire continuam responsáveis por carregar seus próprios assets.
Os componentes de formulário aceitam atributos wire:* normalmente, então o mesmo componente pode ser usado tanto com Blade tradicional quanto com Livewire:
<x-portal::input label="Nome" wire:model.live="form.nome" /> <x-portal::select label="Setor" wire:model.defer="form.setor" :options="$setores" /> <x-portal::textarea label="Observação" wire:model.blur="form.observacao" /> <x-portal::switch label="Ativo" wire:model="form.ativo" /> <x-portal::button click="save" wire:target="save" icon="fa-save" > Salvar </x-portal::button>
O modal também aceita wire:model de forma opcional. Quando esse atributo está presente, o pacote renderiza apenas atributos Alpine compatíveis com o $wire.entangle(...) do próprio Livewire, sem usar a diretiva Blade @entangle. Em aplicações sem Livewire, basta continuar usando o modal com a prop show.
Exemplo mínimo com Livewire:
<?php namespace App\Livewire\Admin\Exemplo; use Livewire\Component; class ExemploModal extends Component { public bool $showFormModal = false; public ?int $editingId = null; public function create(): void { $this->editingId = null; $this->showFormModal = true; } public function edit(int $id): void { $this->editingId = $id; $this->showFormModal = true; } public function closeModal(): void { $this->showFormModal = false; } }
<x-portal::button click="create" icon="fa-plus"> Novo registro </x-portal::button> <x-portal::modal wire:model="showFormModal" :title="$editingId ? 'Editar registro' : 'Novo registro'" :icon="$editingId ? 'fa-edit' : 'fa-plus'" > Conteúdo do formulário aqui. <x-slot name="footer"> <x-portal::button variant="secondary" click="$set('showFormModal', false)" > Cancelar </x-portal::button> <x-portal::button click="save" icon="fa-save"> Salvar </x-portal::button> </x-slot> </x-portal::modal>
Fluxo esperado:
- crie uma property booleana, como
showFormModal, iniciando comfalse; - abra o modal alterando essa property para
true; - ligue o componente com
wire:model="showFormModal"; - para título dinâmico, use
:titlecom uma expressão Livewire/Blade; - para fechar, use
$set('showFormModal', false)ou um método do componente.
Exemplos
Views de exemplo incluídas:
portal-ui::examples.minimal-showcaseportal-ui::examples.simple-showcaseportal-ui::examples.admin-crud-showcaseportal-ui::examples.guest-showcase
Como usar os stubs de exemplos e rotas no app consumidor
Após publicar os stubs com php artisan vendor:publish --tag=portal-ui-stubs, você pode carregar as rotas de demonstração diretamente no arquivo routes/web.php do seu projeto:
if (file_exists(base_path('stubs/portal-ui/routes/demo.php'))) { require base_path('stubs/portal-ui/routes/demo.php'); }
Isso registrará as rotas automaticamente (como /portal-ui-demo).
Para configurar a barra lateral (sidebar) com um dos modelos de menu incluídos em stubs/portal-ui/navigation/:
- Escolha um dos arquivos (ex:
simple.phpouadmin.php). - Copie a estrutura de grupos contida no
returndele. - Cole no array
'groups'dentro do arquivoconfig/portal-ui.phppublicado no seu projeto.
Como alternativa manual, você pode registrar as rotas sugeridas diretamente:
use Illuminate\Support\Facades\Route; Route::middleware(['web'])->group(function () { Route::view('/portal-ui-demo-minimal', 'portal-ui::examples.minimal-showcase')->name('portal-ui.demo.minimal'); Route::view('/portal-ui-demo', 'portal-ui::examples.simple-showcase')->name('portal-ui.demo'); Route::view('/portal-ui-demo-crud', 'portal-ui::examples.admin-crud-showcase')->name('portal-ui.demo.crud'); Route::view('/portal-ui-demo-guest', 'portal-ui::examples.guest-showcase')->name('portal-ui.demo.guest'); });
Mais exemplos de layout, menus e componentes estão em docs/examples.md.
Desenvolvimento
Instale dependências e rode os testes dentro da pasta do pacote:
composer install
composer test
Para validar assets:
npm install npm run build
Versões
O pacote segue Versionamento Semântico (MAJOR.MINOR.PATCH):
PATCH: correções compatíveis.MINOR: novos recursos compatíveis.MAJOR: mudanças incompatíveis em layouts, componentes, configuração ou assets.
Consulte CHANGELOG.md antes de atualizar consumidores. Republique assets sempre que houver mudanças visuais ou de JavaScript no tema.