panel maintained by mylaraveltools
Panel (mylaraveltools/panel)
Panel de administración declarativo y monocromático para Laravel. API de Resources al estilo Filament/Nova, con Livewire 3, Tailwind CSS, auth integrada, permisos Spatie, import/export y navegación SPA.
Parte del ecosistema My Laravel Tools (junto con mylaraveltools/alertas).
composer require mylaraveltools/panel
Migración: si usabas
mylaraveltools/minimalist, sustituye pormylaraveltools/panel. El namespace PHP sigue siendoMyLaravelTools\Panel— no cambia tu código.
Tabla de contenidos
- Requisitos
- Instalación paso a paso
- Tu primer CRUD en 5 minutos
- Configuración (
config/panel.php) - Autenticación y perfil
- Permisos (Spatie) y suplantación
- Navegación y páginas custom
- Importar y exportar datos
- Dashboard y widgets
- Relaciones entre modelos
- Tema, layout y SPA
- Comandos Artisan
- Personalizar vistas
- Solución de problemas
- Proyecto demo
- Desarrollo y tests
- Roadmap
- Licencia
Requisitos
| Requisito | Versión |
|---|---|
| PHP | 8.2+ |
| Laravel | 11, 12 o 13 |
| Livewire | 3.5+ |
| Tailwind CSS | 3+ en la app host |
Opcionales: spatie/laravel-permission, mylaraveltools/alertas.
Instalación paso a paso
Paso 1 — Composer
composer require mylaraveltools/panel
Desarrollo local (path repository):
{
"repositories": [
{ "type": "path", "url": "../minimalist-panel-library" }
],
"require": {
"mylaraveltools/panel": "@dev"
}
}
Paso 2 — Instalar el panel
php artisan panel:install
Esto publica config/panel.php, registra rutas en /admin y prepara Livewire.
Paso 3 — Tailwind
En tailwind.config.js incluye las vistas del paquete y activa modo oscuro por clase:
export default {
darkMode: 'class',
content: [
'./resources/views/**/*.blade.php',
'./vendor/mylaraveltools/panel/resources/views/**/*.blade.php',
],
};
Paso 4 — Alpine + Livewire
En resources/js/app.js, no importes Alpine en rutas del panel (/admin/*). Livewire lo gestiona:
const panelPath = '/admin';
if (!window.location.pathname.startsWith(panelPath)) {
import('alpinejs').then(({ default: Alpine }) => {
window.Alpine = Alpine;
Alpine.start();
});
}
Importar Alpine en
/admin/loginrompewire:submit(el formulario no envía nada).
Paso 5 — Compilar y probar
npm run build
php artisan serve
Abre http://127.0.0.1:8000/admin — verás login/registro si auth.enabled es true.
APP_URL debe coincidir con host y puerto (http://127.0.0.1:8000).
Tu primer CRUD en 5 minutos
1. Crear el Resource
php artisan panel:make-resource Product --model=Product
2. Definir formulario y tabla
// 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';
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(),
];
}
}
3. Listo
Auto-discovery en app/Panel/Resources/ (configurable). URL: /admin/resources/products (slug = kebab del modelo; puedes fijarlo con protected static ?string $slug = 'productos';).
Configuración (config/panel.php)
Toda la configuración vive en este archivo (compatible con config:cache). No hace falta .env, aunque puedes usarlo si prefieres.
| Clave | Descripción | Default |
|---|---|---|
path |
Prefijo URL | admin |
guard |
Guard de auth | web |
brand.name |
Nombre en sidebar | Panel |
brand.logo |
URL del logo (null = icono) |
null |
per_page |
Registros por página | 15 |
forms_in_modal |
Crear/editar en modal | true |
discovery |
Auto-discovery Resources | enabled |
pages |
Auto-discovery Pages | enabled |
permissions |
Spatie/Gate | disabled |
navigation |
Menú lateral (null = auto) |
null |
widgets |
Dashboard | [] |
import |
Import CSV/Excel | enabled + preview |
impersonation |
Suplantar usuarios | disabled |
theme.preset |
Preset visual (minimal, corporate, contrast, ocean) |
minimal |
extensions |
Vistas custom de campos/columnas y widgets | [] |
version |
Texto en sidebar (null = paquete) |
null |
Tema monocromático
'theme' => [
'default' => 'dark',
'font' => 'Plus Jakarta Sans',
'colors' => [
'primary' => '#000000',
'primary_dark' => '#ffffff',
'accent' => '#525252',
'success' => '#16a34a',
'danger' => '#dc2626',
'warning' => '#ca8a04',
],
'light' => [ /* bg, surface, card, border, heading, text, muted… */ ],
'dark' => [ /* … */ ],
],
Variables CSS: --panel-primary, --panel-bg, etc. Toggle claro/oscuro en el footer del sidebar (persiste en localStorage).
Presets de tema
'theme' => [
'preset' => 'corporate', // minimal | corporate | contrast | ocean
'colors' => [
'primary' => '#ff0000', // sobrescribe solo el preset
],
],
Extensibilidad (campos, columnas, widgets)
Por config (config/panel.php → extensions):
'extensions' => [
'field_views' => ['rating' => 'panel-custom.fields.rating'],
'column_views' => ['rating' => 'panel-custom.columns.rating'],
'widgets' => [],
],
Por código (AppServiceProvider::boot):
use MyLaravelTools\Panel\Facades\PanelExtensions;
PanelExtensions::registrarVistaCampo('rating', 'panel-custom.fields.rating');
PanelExtensions::registrarWidget(StatWidget::make('Total', fn () => Model::count()));
Campo custom en un Resource:
CustomField::make('payload')
->type('json-editor')
->view('panel-custom.fields.json-editor')
->label('Datos JSON'),
Campos integrados nuevos: ColorField, DateTimeField, KeyValueField (pares clave/valor → JSON).
Actualizar vistas publicadas:
php artisan panel:upgrade-views --dry-run
php artisan panel:upgrade-views --force
Layout y apariencia
'layout' => [
'density' => 'compact', // comfortable | compact
'content_width' => 'boxed', // full | boxed
'sidebar_collapsible' => true,
'show_breadcrumbs' => true,
'footer_links' => [
['label' => 'Ayuda', 'route' => 'panel.dashboard'],
['label' => 'Web', 'url' => 'https://ejemplo.com', 'external' => true],
],
],
'brand' => [
'name' => 'Mi Panel',
'logo' => '/img/logo.svg',
'logo_height' => '2.5rem',
'favicon' => '/favicon.ico',
'tagline' => 'Gestiona tu negocio',
],
'auth_ui' => [
'layout' => 'split', // centered | split
'image' => '/img/auth-side.jpg',
'background' => 'linear-gradient(135deg, #0f172a, #1e3a5f)',
'show_tagline' => true,
],
'customization' => [
'css' => '.panel-sidebar { border-right-width: 2px; }',
'head_view' => 'panel-custom.head',
],
RepeaterField — filas con varias columnas (JSON):
RepeaterField::make('lineas')
->columns(['concepto' => 'Concepto', 'importe' => 'Importe'])
->minRows(1)
->maxRows(10),
Máxima personalización (v0.24)
Modos de layout — sidebar (por defecto), topbar o dual (barra superior + lateral):
'layout' => [
'mode' => 'sidebar', // sidebar | topbar | dual
'sidebar_position' => 'left', // left | right
'table_striped' => true,
'table_compact' => false,
'global_search' => true,
'per_page_options' => [15, 25, 50, 100],
],
Slots Blade — inyecta vistas en puntos del layout sin publicar todo el paquete:
'slots' => [
'sidebar.before' => 'mi-app.panel.slots.aviso',
'main.after' => 'mi-app.panel.slots.analytics',
'topbar.end' => 'mi-app.panel.slots.acciones',
],
Slots disponibles: sidebar.before, sidebar.after, main.before, main.after, topbar.start, topbar.end, footer.before.
También vía código en AppServiceProvider:
use MyLaravelTools\Panel\Support\PanelExtensions;
PanelExtensions::registrarSlot('main.before', 'mi-app.panel.banner');
Import upsert — actualiza registros existentes en lugar de fallar:
'import' => [
'upsert' => true,
'upsert_key' => 'email', // global; el Resource puede sobreescribir
],
// En tu Resource
public static function importUpsertKey(): ?string
{
return 'sku';
}
Hooks en Resources:
public static function navigationBadge(): ?string
{
return (string) static::model()::where('is_active', false)->count();
}
public static function hiddenFromNavigation(): bool
{
return ! auth()->user()?->can('view settings');
}
public static function perPageOptions(): array
{
return [10, 25, 50];
}
Presets de tema propios — archivo PHP que devuelve un array de presets:
'theme' => [
'preset' => 'mi-marca',
'presets_file' => config_path('panel-theme-presets.php'),
],
Instalación con demo:
php artisan panel:install --demo
Autenticación y perfil
Auth integrada en /admin/login y /admin/register (tabla users de Laravel):
'auth' => [
'enabled' => true,
'register' => true,
'register_role' => 'viewer', // Spatie, opcional
'password_reset' => true,
'email_verification' => false, // requiere MustVerifyEmail
],
- Auth externa (Breeze/Fortify):
'enabled' => false,'login_route' => 'login'. - Tras login: recarga completa al dashboard (sin loader SPA). Botón «Entrando» con puntos animados solo durante el POST.
- Recuperar contraseña:
/admin/forgot-password. - Perfil:
/admin/profile—'profile.enabled' => true.
Permisos (Spatie) y suplantación
Spatie Laravel Permission
composer require spatie/laravel-permission
'permissions' => [
'enabled' => true,
'panel_access' => 'access panel',
'resources' => true, // RoleResource + PermissionResource
'manage_permission' => 'manage users',
],
RolesField/RolesColumnen usuarios.PermissionsField/PermissionsColumnen roles.- En Pages:
protected static ?string $permission = 'view reports'. - Policies:
php artisan panel:make-policy Product→ extiendeResourcePolicy(deny-by-default).
Suplantación de usuario
Navega el panel como otro usuario (permisos, menú y policies reales):
'impersonation' => [
'enabled' => true,
'permission' => 'impersonate users',
'exclude_ids' => [],
'banner' => true,
],
- En el resource del modelo
User, menú ⋮ → Entrar como. - Aparece una tarjeta en el sidebar (encima del perfil) con botón Salir.
- Requiere permiso Spatie/Gate. No puedes suplantarte a ti mismo.
Navegación y páginas custom
Menú con grupos
// config/panel.php
'navigation' => require __DIR__.'/panel-navigation.php',
return [
['resource' => ProductResource::class],
['page' => SettingsPage::class],
[
'type' => 'group',
'label' => 'Catálogo',
'icon' => 'package',
'children' => [
['resource' => ProductResource::class],
['resource' => CategoryResource::class],
],
],
];
- Búsqueda global: Cmd/Ctrl+K.
- No uses
route()al cargar el config; usa la clave'route' => 'panel.dashboard'.
Páginas custom (no CRUD)
php artisan panel:make-page Settings
final class SettingsPage extends Page
{
protected static ?string $label = 'General';
protected static ?string $slug = 'settings-general';
public static function view(): string
{
return 'panel.pages.settings-general';
}
}
Ruta: /admin/pages/{slug}. Vista con <x-panel::page-header>.
Importar y exportar datos
Export
Botones CSV, XLSX y PDF en listados. Con filas seleccionadas, exporta solo la selección.
Import (con vista previa)
'import' => [
'enabled' => true,
'preview' => true,
'upsert' => true,
'upsert_key' => null,
],
- Botón Importar en el listado (permiso
create). - Sube
.csv,.txt,.xlsx,.xls. - Revisa filas válidas/errores → confirma.
- Con
upsertactivo: crea nuevos y actualiza existentes segúnimportUpsertKey()del Resource.
Personaliza columnas con Field::importable(false) o Resource::import().
Dashboard y widgets
'widgets' => [
ResourceCountWidget::make(ProductResource::class),
StatWidget::make('Activos', fn () => Product::where('is_active', true)->count())
->icon('check-circle'),
ChartWidget::make('Ventas', 'bar', fn () => [
'labels' => ['Ene', 'Feb'],
'values' => [12, 19],
])->themeColors(),
ViewWidget::make('Custom', 'panel.widgets.mi-vista', fn () => ['total' => 100])
->columnSpan(2),
],
Tipos ChartWidget: bar, line, pie, doughnut, progression. Gráficos reactivos al tema y SPA.
Relaciones entre modelos
Desde la vista Ver del registro padre:
public static function relations(): array
{
return [
RelationManager::make('products', ProductResource::class),
RelationManager::hasOne('profile', ProfileResource::class),
RelationManager::belongsToMany('tags', TagResource::class),
RelationManager::morphMany('reviews', ReviewResource::class),
RelationManager::morphToMany('tags', TagResource::class),
];
}
Tema, layout y SPA
- Sin header global — cada vista usa
<x-panel::page-header>(título + breadcrumbs). - Sidebar footer: perfil, idioma, tema, versión, logout.
- SPA:
wire:navigate, loader con porcentaje0%–100%, sidebar persistente. - Livewire: mantén
navigate.show_progress_bar => trueenconfig/livewire.php(la barra NProgress se oculta vía CSS del panel).
Fields y Columns
Fields: TextField, EmailField, PasswordField, TextareaField, BooleanField, SelectField, BelongsToField, NumberField, DateField, DateTimeField, ColorField, KeyValueField, CustomField, FileField, ImageField, RichTextField, RolesField, PermissionsField.
Columns: TextColumn, BooleanColumn, DateTimeColumn, BadgeColumn, ColorColumn, BelongsToColumn, ImageColumn, RolesColumn, PermissionsColumn.
Filtros: SelectFilter, BooleanFilter, DateRangeFilter, MultiSelectFilter.
Formularios: Section::make(), Tab::make(), soft deletes, bulk actions, RowAction.
Comandos Artisan
| Comando | Descripción |
|---|---|
php artisan panel:install |
Instalar panel |
php artisan panel:install --demo |
Instalar + navigation stub y PostResource ejemplo |
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 |
php artisan panel:make-widget Name --type=chart |
Crear clase widget para el dashboard |
php artisan panel:doctor |
Diagnosticar instalación del panel |
php artisan panel:upgrade-views |
Actualizar vistas publicadas |
php artisan vendor:publish --tag=panel-config |
Publicar config |
php artisan vendor:publish --tag=panel-views |
Publicar vistas Blade |
php artisan vendor:publish --tag=panel-documentation |
Copiar documentation/panel/ al proyecto |
Documentación interactiva (playground)
Ruta pública /playground (sin login) — documentation.enabled y documentation.path:
- Panel FAKE a pantalla completa + controles laterales
- Catálogo de todas las opciones de
config/panel.php - Vista previa en vivo (layout, marca, tema…)
- Markdown:
documentation/panel/README.md
Personalizar vistas
Si publicas vistas en resources/views/vendor/panel/, sobreescriben las del paquete.
Tras actualizar el paquete:
php artisan vendor:publish --tag=panel-views --force
php artisan view:clear
Si no publicas vistas, Laravel usa las del vendor directamente (recomendado hasta que edites Blade).
Solución de problemas
| Problema | Solución |
|---|---|
| Login no envía el formulario | No importes Alpine en rutas /admin/* |
Alpine is not defined |
show_progress_bar => true en config/livewire.php |
| Estilos rotos | Incluye vistas del paquete en tailwind.config.js y npm run build |
| Feature nueva no aparece | Republica vistas con --force o borra resources/views/vendor/panel/ |
404 en /admin/resources/users |
El slug por defecto es user (singular); define $slug = 'users' |
| «Entrar como» no visible | Permiso impersonate users + php artisan db:seed con ese permiso |
| Redirecciones raras en login | APP_URL con host y puerto correctos |
Proyecto demo
Carpeta panel-demo/ con catálogo, ventas, Spatie, gráficos, import y suplantación.
cd panel-demo
composer install && npm install && npm run build
php artisan migrate:fresh --seed
php artisan serve
| Usuario | Password | |
|---|---|---|
| Admin | admin@panel.test |
password |
| Editor | editor@panel.test |
password |
Ver panel-demo/README.md para rutas y features de prueba.
Desarrollo y tests
cd minimalist-panel-library
composer test
- Contexto para agentes/IA: AGENTS.md
- Publicar en Packagist: PUBLISHING.md
- Historial: CHANGELOG.md
Roadmap
- CRUD, SPA, Excel/PDF, búsqueda global, i18n, tests, CI
- RowAction, modales, skeletons, filtros avanzados
- Forms en modal, tabs, export PDF
- Policies, páginas custom, permisos Spatie
- Auth integrada, reset password, perfil
- Import con preview, ChartWidget, ViewWidget, email verify
- Auth UX (v0.20), suplantación de usuario (v0.21)
- Packagist —
mylaraveltools/panel - Extensibilidad — presets tema, PanelExtensions, campos custom (v0.22)
- Layout — densidad, boxed, sidebar colapsable, auth split, RepeaterField (v0.23)
- Máxima personalización — topbar/dual, slots, upsert, tablas, presets propios (v0.24)
- Playground público, gráficos interactivos,
panel:doctor,panel:make-widget(v0.25) - Layout móvil pulido —
mobile-bar, drawer, modos sidebar/topbar/dual (v0.26) - Multi-panel, starter kit completo
Licencia
MIT — Alberto Gallardo Morales