K

Panels

Panels

The owner layer — one entity declared once, the pages that render it, the menu it appears in and the routes that reach it.

On this page

wire-panels is the layer above the components. A table, a form and an infolist are independent primitives that know nothing about each other; this package adds the class that says they are all the same entity, and the pages, menu and routes that follow from saying it once.

composer require nyoncode/wire-panels

Nothing requires this package. An application that renders its tables and forms from its own Livewire components never installs it, and everything below keeps working — which is the test of whether an owner layer is really optional.

How It Works

Four things sit on top of each other, and each one is usable without the one above it:

Layer What it answers Page
A resource "this is the Order entity, this is how it lists, is edited, is shown" Resources
A page "this Livewire component renders that surface for one record or all of them" Pages
The menu "which of them appear where, under what heading, in what order" Navigation
The router "which URL reaches which page, in which zone" Routing

A resource with no pages is still registered and still introspectable. Pages work with no resource — write table() on the page and it is an ordinary WithTable component. A menu entry whose key routes nowhere renders without a link rather than failing. Each layer degrades to the one below instead of requiring it.

What holds them together is a registry of class names, not a panel object. Nothing here builds a table to answer "what is in the menu": identity is static, the surfaces are instance methods, and a menu of forty resources composes zero of them.

Which Package Ships What

A resource is declared across the packages that own the types it names, so an application installs only what its resources actually use:

You need It lives in Because it names
DescribesResource, DescribesRecords, ResourceRegistry wire-core nothing but scalars
ProvidesResourceForm wire-forms Form
ProvidesResourceInfolist wire-core (beside Infolists) Infolist
ProvidesResourceTable, ProvidesRelationManagers wire-panels Table, RelationManager
ListPage and the other pages wire-panels Table, Form, the host traits

The practical consequence: a resource with a form and no list needs wire-forms and nothing else. Identity comes from wire-core, which wire-forms already requires, so declaring it never pulls in a table package — its assets, migrations, config and Livewire synthesizer included.

wire-panels sits above every component package and nothing depends on it. That direction is the point: a resource composes the primitives, so the package that owns resources is the one allowed to name all of them, and none of them may name it back.

Quick Start

One entity, declared once, rendered by a page of four lines:

use App\Models\Order;
use NyonCode\WireCore\Core\Resources\Concerns\DescribesRecords;
use NyonCode\WireCore\Core\Resources\Contracts\DescribesResource;
use NyonCode\WirePanels\Resources\Contracts\ProvidesResourceTable;
use NyonCode\WirePanels\Resources\Pages\ListPage;
use NyonCode\WireTable\Columns\TextColumn;
use NyonCode\WireTable\Table;
 
final class OrderResource implements DescribesResource, ProvidesResourceTable
{
use DescribesRecords;
 
public static function modelClass(): ?string
{
return Order::class;
}
 
public function table(Table $table): Table
{
return $table->columns([
TextColumn::make('number')->searchable(),
TextColumn::make('customer.name')->label('Customer'),
]);
}
}
 
final class ListOrders extends ListPage
{
protected static ?string $resource = OrderResource::class;
}
// config/wire-core.php
'resources' => [
App\Resources\OrderResource::class,
],

That is a working list. Adding a form makes create and edit work; adding navigation() puts it in a menu; declaring pages() gives it URLs — each an opt-in, in that order.

What This Package Does Not Do

It ships no layout, no sidebar markup and no dashboard chrome. A full-page Livewire component needs a layout and this package does not supply one — either set livewire.component_layout to your own, or install the admin shell, which is the ready-made answer and a separate package for exactly this reason.

In This Section

Page What it covers
Resources Identity, the surface contracts, naming, registration, reading the registry
Pages ListPage, CreatePage, EditPage, ViewPage, record resolution, embedded relation managers
Navigation NavigationItem, NavigationGroup, Workspace, the catalogue all three surfaces read
Routing pages(), Route::wireResources(), the URL shape, zones, config-declared routes
Modules One business area's manifest — its resources, dashboards and menu heading in one class

Reaching A Page A Module Ships

Every resource page implements IdentifiesHookTarget, so a plugin hook can be scoped to the resource it shows — for: 'invoices' reaches that module's list, form and detail and nothing else. The page itself has a hook too: page.mounting fires once a page has mounted.

It runs last, which is a measurement rather than a preference: Livewire calls a component's own mount() before the mount{Trait} hooks, so by then an edit page has resolved its record and seeded its form — which is why a callback can add a key to the state bag rather than have the seed overwrite it.

Change the page through its public surface. A page mounts once and answers every update after from its snapshot, which carries public properties and nothing else, so state written anywhere else is right on the first paint and gone on the second.

$manager->hook(Hook::PageMounting, function (PageMountingPayload $payload) {
$payload->page->data['team_id'] = auth()->user()->team_id;
 
return $payload;
}, for: 'invoices');

$payload->title is read-only for the same reason: a page's $title is protected, so a hook that set it would be offering exactly that footgun.