Páginas personalizadas
Cuando una pantalla no encaja en un Resource ni en una Page (un calendario, un asistente de varios pasos, un editor visual...), CustomPage la registra en la navegación del panel y su contenido es un componente Vue que escribes tú. Arrel te da el kit de componentes de su interfaz para que la pantalla se vea como el resto del panel.
Una página personalizada tiene tres piezas:
- La entrada de navegación, en PHP (
CustomPage). - El componente Vue, en tu frontend.
- Los endpoints que el componente consume, en tu aplicación.
La entrada de navegación
Panel::make('admin')
->customPages([
CustomPage::make('pass-list', 'Pass list')
->icon('clipboard-document-check')
->group('Attendance')
->sort(10)
->visible(fn (): bool => auth()->user()->can('attendance.create'))
->badge(fn (): int => Absence::pending()->count()),
])CustomPage::make($slug, $label) reserva la URL /{prefix}/{panel}/custom/{slug} y la entrada de menú.
| Método | Descripción |
|---|---|
icon(string $icon) | Icono de la navegación. Véase la lista. |
group(string $group) | Grupo de la navegación. Las páginas y los Resources que comparten nombre de grupo se muestran juntos. |
sort(int $sort) | Posición dentro de la navegación, de menor a mayor. |
visible(Closure $callback) | Si devuelve falso, la entrada no aparece. Se evalúa en cada petición, por usuario. |
badge(Closure $count) | Un recuento que se muestra en el enlace. Solo se muestra si es mayor que cero. |
visible() solo oculta el enlace. Los endpoints que alimentan la página deben hacer su propia autorización (véase Endpoints propios).
El componente
Una página se registra desde JavaScript, con un script que cargas en el panel con Panel::scripts(). Hay dos maneras de hacerlo. La recomendada es registerMountablePage.
registerMountablePage: tu aplicación Vue
Arrel te da un contenedor vacío y tú montas en él tu propia aplicación Vue, con tu build, tus dependencias y tu versión de Vue:
import '@arrel/ui.css'
import { createApp, type App } from 'vue'
import PassListPage from './PassListPage.vue'
const mounted = new WeakMap<HTMLElement, App>()
window.Arrel.registerMountablePage('pass-list', {
mount(container, props) {
const app = createApp(PassListPage, props)
app.mount(container)
mounted.set(container, app)
},
unmount(container) {
mounted.get(container)?.unmount()
mounted.delete(container)
},
})props contiene panel (el identificador del panel) y slug (el de la página). Arrel nunca reconcilia los vnodes de tu componente, así que no hay ningún riesgo de tener dos instancias de Vue mezcladas.
registerPage: componentes dentro del árbol de Arrel
registerPage monta el componente dentro del árbol de Arrel, con su instancia de Vue. Esta es la versión runtime only, sin compilador de plantillas: hacen falta h() o componentes precompilados.
const { h } = window.Arrel.Vue
window.Arrel.registerPage('about', {
render() {
return h('div', [h('h1', 'About')])
},
})window.Arrel.Vue expone h, ref, reactive, computed, watch, onMounted y onBeforeUnmount.
registerMountablePage | registerPage | |
|---|---|---|
| Instancia de Vue | La tuya, separada | La de Arrel |
| Plantillas | Cualquiera, con tu build | Solo h() o componentes precompilados |
| Ciclo de vida | Lo gestionas tú (mount() y unmount()) | Lo gestiona Arrel |
La API global window.Arrel
| Miembro | Descripción |
|---|---|
panel | El identificador del panel actual. |
prefix | El prefijo configurado en config('arrel.prefix'). |
url(path = '') | Construye una URL dentro del panel: Arrel.url('posts'). |
navigate(path) | Navega dentro de la SPA sin recargar la página. Devuelve una promesa. |
refreshNavigation() | Vuelve a cargar la navegación, por ejemplo para refrescar un badge() después de una acción. |
registerPage(slug, component) | Registra un componente Vue de Arrel. |
registerMountablePage(slug, page) | Registra una página montable. |
Vue | Funciones de Vue para registerPage. |
El kit de componentes (@arrel/ui)
El paquete publica el kit de componentes de su propia interfaz, para que tus páginas tengan los mismos botones, campos y estados. No se inyecta nada en la página: lo incorporas a tu build.
| Componente | Props principales |
|---|---|
Badge | color: gray, success, warning, danger o info. |
Button | variant: primary, secondary o danger. type, disabled, href, newTab. |
DatePicker | v-model, displayFormat, min, max, isDisabled(iso), clearable, invalid. |
Icon | name, size. |
Input | v-model, type, placeholder, invalid. |
Label | for. |
RichEditor | v-model, field, error. |
Select | v-model, options, nullable, placeholder, invalid. |
Textarea | v-model, rows, placeholder, invalid. |
Toggle | v-model. |
Además de los componentes, exporta requestJson, ApiError, formatDate y t.
requestJson<T>(url, init)hace unfetchcon la sesión y la cabecera CSRF que Sanctum espera, y lanza unApiErrorsi la respuesta no es correcta. Para una respuesta 422,error.validationErrorstiene los errores de validación por campo.formatDate(value, format)formatea una fecha con la sintaxis dedate()de PHP.t(key)devuelve un texto de la interfaz, ya traducido conPanel::translations().
Conectarlo a tu build
El kit se distribuye compilado en dist/ui dentro del paquete, con Vue como dependencia externa (es el Vue de tu build). Define un alias en Vite:
// vite.config.js
import path from 'node:path'
const arrel = path.resolve(__dirname, 'vendor/arrel/arrel')
export default defineConfig({
resolve: {
alias: {
'@arrel/ui.css': `${arrel}/dist/ui/arrel-ui.css`,
'@arrel/ui': `${arrel}/dist/ui/arrel-ui.js`,
},
},
})Y, para TypeScript, la ruta a los tipos:
{
"compilerOptions": {
"paths": {
"@arrel/ui": ["./vendor/arrel/arrel/dist/ui/types/ui.d.ts"]
}
}
}Importa @arrel/ui.css una sola vez en la entrada de tus páginas, para que los componentes lleven los estilos de Arrel.
Endpoints propios
Los datos de una página personalizada viven en tu aplicación, no en Arrel. Para que hereden la autenticación, el segundo factor y la identificación del tenant, móntalos con la misma pila de middleware que Arrel:
Route::middleware([
...config('arrel.middleware'),
ForceJsonResponse::class,
EnsureFrontendRequestsAreStateful::class,
'web',
'auth:sanctum',
EnsureEmailIsVerified::class,
EnsureTwoFactorIsSetUp::class,
...config('arrel.authenticated_middleware'),
])->group(function (): void {
Route::get('/arrel-app/pass-list', PassListController::class);
});Dentro, autoriza cada petición con tus Policies o con Gate. Que visible() oculte la entrada no impide que alguien llame al endpoint a mano.
Una vez una acción de la página cambia un recuento, llama a window.Arrel.refreshNavigation() para que el badge() se refresque.