minimalist maintained by mylaraveltools
Minimalist
Panel de administración declarativo y monocromático para Laravel. Alternativa moderna a AdminLTE, basado en Livewire 3, Tailwind CSS y una API de Resources al estilo Filament/Nova.
composer require mylaraveltools/minimalist
Requisitos
- PHP 8.2+
- Laravel 11, 12 o 13
- Livewire 3.5+
- Tailwind CSS 3+ en la app host
Instalación
1. Composer
composer require mylaraveltools/minimalist
Repositorio local (desarrollo):
{
"repositories": [
{ "type": "path", "url": "../minimalist-panel-library" }
],
"require": {
"mylaraveltools/minimalist": "@dev"
}
}
2. Publicar e instalar
php artisan panel:install
Esto publica config/panel.php, registra rutas en /admin y prepara la estructura. También publica config/livewire.php si no existe.
El panel incluye login y registro en /admin/login y /admin/register usando la tabla users de Laravel. No necesitas Breeze salvo que quieras auth separada.
// config/panel.php
'auth' => [
'enabled' => true,
'register' => true,
'register_role' => 'viewer', // opcional, con Spatie HasRoles
],
Con auth externa: 'enabled' => false y 'login_route' => 'login'.
Recuperar contraseña (activo por defecto):
'auth' => [
'password_reset' => true, // /admin/forgot-password
],
Perfil de usuario
Ruta /admin/profile — el usuario logueado edita su cuenta:
'profile' => [
'enabled' => true,
],
Desactivar con 'enabled' => false si no lo necesitas.
3. Tailwind (app host)
Incluye las vistas del paquete en tailwind.config.js:
content: [
'./resources/views/**/*.blade.php',
'./vendor/mylaraveltools/minimalist/resources/views/**/*.blade.php',
],
Activa modo oscuro por clase:
darkMode: 'class',
4. Alpine + Livewire
En resources/js/app.js, no importes Alpine en rutas del panel (/admin/*). Livewire lo incluye y arranca solo:
const panelPath = '/admin';
if (! window.location.pathname.startsWith(panelPath)) {
import('alpinejs').then(({ default: Alpine }) => {
window.Alpine = Alpine;
Alpine.start();
});
}
Importar alpinejs en /admin/login rompe wire:submit (el formulario no envía nada).
APP_URL debe coincidir con tu servidor de desarrollo (host y puerto), p. ej. http://127.0.0.1:8000.
Primer Resource
php artisan panel:make-resource Product --model=Product
// app/Panel/Resources/ProductResource.php
final class ProductResource extends Resource
{
protected static string $model = Product::class;
protected static ?string $label = 'Productos';
protected static ?string $icon = 'package'; // icono Lucide
public static function form(): array
{
return [
TextField::make('name')->label('Nombre')->required(),
NumberField::make('price')->label('Precio')->min(0),
];
}
public static function table(): array
{
return [
TextColumn::make('name')->label('Nombre')->searchable()->sortable(),
TextColumn::make('price')->label('Precio')->sortable(),
];
}
}
Auto-discovery en app/Panel/Resources/ (configurable en config/panel.php).
Configuración (config/panel.php)
| Clave | Descripción | Default |
|---|---|---|
path |
Prefijo URL del panel | admin |
middleware |
Middleware de rutas | web + EnsurePanelAccess |
guard |
Guard de autenticación | web |
brand.name |
Nombre en sidebar | Panel |
brand.logo |
URL o ruta del logo (null = icono por defecto) |
null |
per_page |
Registros por página | 15 |
discovery |
Auto-discovery de Resources | enabled |
pages |
Auto-discovery de Pages custom | enabled |
permissions |
Spatie/Gate (enabled, panel_access) |
disabled |
navigation |
Menú lateral personalizado (null = auto desde resources) |
null |
widgets |
Widgets del dashboard | [] |
Navegación con grupos desplegables
Define navigation en config/panel.php o en un archivo dedicado:
// config/panel.php
'navigation' => require __DIR__.'/panel-navigation.php',
Formato de ítems:
return [
// Enlace a un Resource (resuelve label, icono y URL automáticamente)
['resource' => ProductResource::class],
// Enlace a una Page custom (informes, ajustes)
['page' => SettingsPage::class],
// Enlace manual
[
'label' => 'Informe de ventas',
'icon' => 'bar-chart',
'route' => 'panel.dashboard', // preferido (se resuelve en runtime)
'badge' => 'Demo', // opcional
],
// Grupo desplegable (Alpine.js)
[
'type' => 'group',
'label' => 'Catálogo',
'icon' => 'package',
'children' => [
['resource' => ProductResource::class],
['resource' => CategoryResource::class],
],
],
];
- Los grupos se abren automáticamente si contienen la ruta activa.
- La búsqueda global (
Cmd/Ctrl+K) indexa todos los enlaces (aplanando grupos). - Iconos: nombres Lucide soportados en
resources/views/components/icon.blade.php. - No uses
route()al cargar el config; usa la claveroutepara enlaces nombrados.
Configuración (config/panel.php)
Tras panel:install, edita config/panel.php. No hace falta usar variables .env — todo vive en el archivo de config (buena práctica Laravel; compatible con config:cache).
'path' => 'admin',
'brand' => [
'name' => 'Mi Panel',
'logo' => '/images/logo.svg',
],
'theme' => [
'default' => 'dark',
'colors' => [
'primary' => '#000000',
'primary_dark' => '#ffffff',
'accent' => '#525252',
],
],
Si prefieres .env, puedes envolver valores en el config publicado: 'path' => env('PANEL_PATH', 'admin').
Páginas custom
Para informes, ajustes o pantallas que no son CRUD:
php artisan panel:make-page Settings
use MyLaravelTools\Panel\Pages\Page;
final class SettingsPage extends Page
{
protected static ?string $label = 'General';
protected static ?string $slug = 'settings-general';
protected static ?string $permission = 'manage settings';
public static function view(): string
{
return 'panel.pages.settings-general';
}
public static function data(): array
{
return ['storeName' => config('app.name')];
}
}
- Ruta:
/admin/pages/{slug} - Auto-discovery en
app/Panel/Pages - Añade al menú:
['page' => SettingsPage::class]
Vista Blade con cabecera unificada (título + miga de pan en la misma fila):
<x-panel::page-header class="mb-8">
<h1>{{ $pageClass::label() }}</h1>
<p class="panel-muted mt-1 text-sm">Descripción opcional.</p>
</x-panel::page-header>
Layout y cabecera de página
Desde v0.13.0 no hay barra superior global. Cada pantalla usa <x-panel::page-header>:
- Izquierda: título (y subtítulo, enlace «volver», etc.)
- Derecha: miga de pan automática
- Móvil: botón menú a la izquierda del título
El sidebar incluye en el footer: enlace al perfil, toggle claro/oscuro, versión del panel (panel.version) e icono de cerrar sesión.
// config/panel.php — versión mostrada en el sidebar (null = v{Package::VERSION})
'version' => null,
Tema y colores
Paleta monocromática por defecto (blanco/negro). Totalmente personalizable vía config.
Estructura
'theme' => [
'default' => 'dark', // dark | light
'font' => 'Plus Jakarta Sans',
'radius' => '0.75rem',
'sidebar_width' => '16rem',
'colors' => [
'primary' => '#000000', // modo claro: botones, acentos
'primary_hover' => '#262626',
'primary_dark' => '#ffffff', // modo oscuro
'primary_hover_dark' => '#e5e5e5',
'accent' => '#525252',
'accent_dark' => '#a3a3a3',
'success' => '#16a34a',
'danger' => '#dc2626',
'warning' => '#ca8a04',
],
'light' => [
'bg' => '#ffffff',
'surface' => '#fafafa',
'card' => '#ffffff',
'elevated' => '#f5f5f5',
'border' => '#e5e5e5',
'heading' => '#0a0a0a',
'text' => '#404040',
'muted' => '#737373',
'input_bg' => '#ffffff',
'input_border' => '#d4d4d4',
],
'dark' => [
'bg' => '#0a0a0a',
'surface' => '#111111',
'card' => '#141414',
'elevated' => '#1a1a1a',
'border' => '#262626',
'heading' => '#fafafa',
'text' => '#d4d4d4',
'muted' => '#737373',
'input_bg' => '#0a0a0a',
'input_border' => '#404040',
],
],
Los colores se inyectan como variables CSS (--panel-primary, --panel-bg, etc.) mediante ThemeResolver. El contraste del texto en botones primarios se calcula automáticamente.
Toggle claro/oscuro
Botón en el footer del sidebar. Persistencia en localStorage (panel-theme).
Clases semánticas
| Clase | Uso |
|---|---|
.panel-body |
Fondo y texto base |
.panel-card |
Tarjetas |
.panel-heading / .panel-text / .panel-muted |
Tipografía |
.panel-input |
Formularios |
.panel-btn-primary |
Botón principal |
.panel-nav-link-active |
Nav activo |
.panel-table |
Tablas |
Iconos (Lucide)
protected static ?string $icon = 'users';
<x-panel::icon name="package" class="h-4 w-4" />
Disponibles: layout-dashboard, package, folder, users, plus, pencil, trash-2, eye, search, download, layers, check-circle, loader-2, etc.
Permisos
Spatie Laravel Permission (opcional)
composer require spatie/laravel-permission
// config/panel.php
'permissions' => [
'enabled' => true,
'panel_access' => 'access panel',
'resources' => true, // RoleResource + PermissionResource integrados
'manage_permission' => 'manage users', // permiso para CRUD roles/permisos
],
EnsurePanelAccessexige el permisopanel_accesspara entrar al panel- Con
resources => truey Spatie instalado, se registranRoleResourceyPermissionResourceen/admin/resources/rolesy/admin/resources/permissions - Asigna permisos a roles con
PermissionsField; asigna roles a usuarios conRolesField - Si defines tus propios resources con slug
rolesopermissionsenapp/Panel/Resources, prevalecen sobre los integrados - En Pages:
protected static ?string $permission = 'view reports' - En navegación:
'permission' => 'manage settings'en enlaces manuales - Resources y Pages en el menú se ocultan si el usuario no tiene acceso
Usa Spatie en tus Policies: $user->can('manage products').
Roles en usuarios
Con Spatie instalado y HasRoles en tu modelo User:
use MyLaravelTools\Panel\Fields\RolesField;
use MyLaravelTools\Panel\Columns\RolesColumn;
RolesField::make('roles')->label('Roles'),
RolesColumn::make('roles')->label('Roles'),
Los roles se sincronizan con syncRoles() al crear o editar (no van en $fillable).
Permisos en roles
use MyLaravelTools\Panel\Fields\PermissionsField;
use MyLaravelTools\Panel\Columns\PermissionsColumn;
PermissionsField::make('permissions')->label('Permisos'),
PermissionsColumn::make('permissions')->label('Permisos'),
Los permisos se sincronizan con syncPermissions() al guardar el rol.
Dos capas en Resources combinadas con AND (ambas deben permitir):
- Hooks en el Resource —
canViewAny(),canCreate(),canEdit(), etc. - Policy de Laravel — si existe para el modelo
Hooks rápidos
public static function canViewAny(): bool { return true; }
public static function canCreate(): bool { return true; }
public static function canEdit(Model $record): bool { return true; }
public static function canDelete(Model $record): bool { return true; }
Policies (recomendado)
php artisan panel:make-policy Product
Genera App\Policies\ProductPolicy extendiendo MyLaravelTools\Panel\Policies\ResourcePolicy.
Las policies hijas deben mantener $user y $record sin type-hint (restricción de PHP al heredar). Usa instanceof en el cuerpo si necesitas tu modelo:
public function delete($user, $record): bool
{
return $user instanceof User && $user->isPanelAdmin();
}
use App\Policies\ProductPolicy;
protected static ?string $policy = ProductPolicy::class;
Sin $policy explícita, auto-detecta App\Policies\{Model}Policy si panel.policies.auto_register es true. La base niega todo por defecto.
Filtros
public static function filters(): array
{
return [
SelectFilter::make('category_id')
->label('Categoría')
->relationship(Category::class, 'name'),
BooleanFilter::make('is_active')->label('Activo'),
];
}
Acciones masivas
public static function bulkActions(): array
{
return [
BulkAction::make('delete', 'Eliminar')
->action(fn ($records) => $records->each->delete())
->color('rose')
->requiresConfirmation(),
];
}
Incluye exportSelection para CSV.
Soft deletes
Si el modelo usa SoftDeletes, el listado muestra papelera, restaurar y eliminar permanente.
Vista detalle
public static function detail(): array
{
return static::table(); // o columnas propias
}
Ruta: GET /admin/resources/{slug}/{id}
Form Sections
use MyLaravelTools\Panel\Forms\Section;
public static function form(): array
{
return [
Section::make('Información', [
TextField::make('name')->required(),
])->description('Datos básicos'),
TextField::make('email')->required(),
];
}
BelongsToMany
public static function relations(): array
{
return [
RelationManager::belongsToMany('tags', TagResource::class)
->title('Etiquetas'),
];
}
En la vista Ver del registro padre: crear etiqueta y vincular, o desvincular.
RelationManager (HasMany)
Gestiona relaciones HasMany desde la vista detalle:
public static function relations(): array
{
return [
RelationManager::make('products', ProductResource::class)
->title('Productos de esta categoría'),
];
}
Widgets
// config/panel.php
'widgets' => [
ResourceCountWidget::make(ProductResource::class),
StatWidget::make('Activos', fn () => Product::where('is_active', true)->count())
->icon('check-circle'),
],
Export CSV
- Botón Exportar CSV en listados (respeta búsqueda y filtros)
- Acción masiva Exportar selección
Navegación SPA
wire:navigateen enlaces internos- Sidebar persistente (
@persisten toasts) - Loader a pantalla completa del área de contenido con porcentaje entero (
0%–100%) en el anillo - Prefetch con
wire:navigate.hover(páginas cacheadas saltan a100%)
Mantén show_progress_bar => true en Livewire (obligatorio). La barra NProgress de Livewire se oculta vía CSS del panel; usar false provoca Alpine is not defined al cargar /admin:
// config/livewire.php
'navigate' => [
'show_progress_bar' => true,
],
Fields disponibles
TextField, EmailField, PasswordField, TextareaField, BooleanField, SelectField, BelongsToField, NumberField, ImageField
Columns disponibles
TextColumn, BooleanColumn, DateTimeColumn, BadgeColumn, BelongsToColumn, ImageColumn
Comandos
| Comando | Descripción |
|---|---|
php artisan panel:install |
Instalar panel |
php artisan panel:make-resource Name |
Crear Resource |
php artisan panel:make-page Name |
Crear página custom |
php artisan panel:make-policy Name |
Crear Policy para un modelo |
php artisan vendor:publish --tag=panel-config |
Publicar config |
php artisan vendor:publish --tag=panel-views |
Publicar vistas |
Demo
Proyecto de prueba en panel-demo/:
admin@panel.test / password → /admin
RowAction (acciones por fila)
use MyLaravelTools\Panel\Actions\RowAction;
public static function rowActions(): array
{
return [
RowAction::view(),
RowAction::edit(),
RowAction::make('duplicate')
->label('Duplicar')
->icon('copy')
->handle(fn (Model $record) => /* ... */),
];
}
Por defecto: view, edit, delete (+ restore / forceDelete con soft deletes).
Breadcrumbs
Automáticos según la ruta (Panel / Productos / Editar). Se renderizan dentro de <x-panel::page-header> a la derecha del título.
Título del registro en show/edit:
protected static ?string $recordTitleAttribute = 'name';
// o automático desde la primera columna searchable
Tests
cd minimalist-panel-library
composer test
Incluye tests de layout SPA (SpaLoaderTest): markup del loader con %, script de progreso y page-header.
Filtros avanzados
DateRangeFilter::make('published_at')->label('Publicado entre'),
MultiSelectFilter::make('category_id')->relationship(Category::class, 'name'),
Export Excel
// En el listado: botones "Exportar CSV" y "Exportar Excel"
// Bulk actions por defecto incluyen exportSelection y exportSelectionExcel
Requiere phpoffice/phpspreadsheet (incluido en el paquete).
Búsqueda global
- Cmd+K / Ctrl+K abre la paleta de búsqueda
- Busca en navegación y registros (columnas
searchable()) - Componente:
panel.global-search
Nuevos Fields
use MyLaravelTools\Panel\Fields\DateField;
use MyLaravelTools\Panel\Fields\FileField;
use MyLaravelTools\Panel\Fields\RichTextField;
DateField::make('published_at')->label('Publicación')->time(),
FileField::make('brochure')->directory('docs')->acceptedMimes(['pdf']),
RichTextField::make('description')->label('Descripción'),
Internacionalización
__('panel::panel.save')
Traducciones en lang/es/panel.php y lang/en/panel.php (namespace panel::panel.*).
Formularios en modal
Por defecto, crear y editar se abren en un modal sobre el listado sin salir de la página:
// config/panel.php
'forms_in_modal' => true,
Con false, se usan las rutas de página completa (panel.resources.create / edit).
Tabs en formularios
Organiza secciones en pestañas con Tab::make():
use MyLaravelTools\Panel\Forms\Section;
use MyLaravelTools\Panel\Forms\Tab;
public static function form(): array
{
return [
Tab::make('General', [
Section::make('Datos', [
TextField::make('name')->required(),
]),
]),
Tab::make('Precio', [
NumberField::make('price')->required(),
]),
];
}
Export PDF
Botones CSV, XLSX y PDF en la toolbar del listado. Si hay filas seleccionadas, exportan solo la selección; si no, el listado filtrado completo.
Publicar en Packagist
Ver PUBLISHING.md para subir el paquete a Packagist y etiquetar releases.
Roadmap
- Fases 1–5 (CRUD, SPA, Excel, búsqueda global, i18n, tests, CI)
- Fase 6: RowAction, confirm modal, skeletons, DateRange/MultiSelect, breadcrumbs con título
- Fase 7: crear/editar en modal, tabs en formularios, export PDF
- Fase 8: Policies Laravel,
panel:make-policy,ResourcePolicy - Fase 9: páginas custom (
Page) y permisos Spatie/Gate - Fase 10: autenticación integrada (login, registro, logout)
- Fase 11: recuperar contraseña,
RolesField/RolesColumn - Fase 12: perfil de usuario (
/admin/profile) - Fase 13: layout sin header,
<x-panel::page-header>, loader SPA con% - Fase 14:
RoleResource/PermissionResourceintegrados,PermissionsField/PermissionsColumn - Packagist —
mylaraveltools/minimalist
Licencia
MIT