Looking to hire Laravel developers? Try LaraJobs

minimalist maintained by mylaraveltools

Description
Minimalist — declarative monochrome admin panel for Laravel with Livewire and Tailwind
Last update
2026/06/20 12:08 (dev-main)
License
Links
Downloads
0

Comments
comments powered by Disqus

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 clave route para 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
],
  • EnsurePanelAccess exige el permiso panel_access para entrar al panel
  • Con resources => true y Spatie instalado, se registran RoleResource y PermissionResource en /admin/resources/roles y /admin/resources/permissions
  • Asigna permisos a roles con PermissionsField; asigna roles a usuarios con RolesField
  • Si defines tus propios resources con slug roles o permissions en app/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):

  1. Hooks en el ResourcecanViewAny(), canCreate(), canEdit(), etc.
  2. 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:navigate en enlaces internos
  • Sidebar persistente (@persist en 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 a 100%)

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 / PermissionResource integrados, PermissionsField / PermissionsColumn
  • Packagist — mylaraveltools/minimalist

Licencia

MIT