Layout Infolist
Layout mengelompokkan entry tanpa mengubah nilai yang dibacanya. Section, Grid, Tabs, dan Tab semuanya mewarisi PandaPanel\Infolists\Components\InfolistComponent, sama seperti entry, sehingga sebuah schema tetap berupa satu tree component dalam susunan apa pun. Gunakan layout ketika halaman view memiliki informasi yang terlalu banyak untuk dibaca nyaman sebagai daftar datar.
Layout hanya mengatur susunan, sama seperti pada form: memindahkan entry dari satu section ke section lain tidak boleh mengubah nilai yang ditampilkan.
Layout minimal
use PandaPanel\Infolists\Components\DateTimeEntry;
use PandaPanel\Infolists\Components\TextEntry;
use PandaPanel\Infolists\InfolistSchema;
use PandaPanel\Infolists\Layouts\Section;
return $schema->columns(2)->schema([
Section::make('Account')
->description('Who they are.')
->columns(2)
->schema([
TextEntry::make('name'),
TextEntry::make('email'),
DateTimeEntry::make('created_at')->label('Joined')->columnSpanFull(),
]),
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
Empat layout
| Class | component | Yang dirender |
|---|---|---|
PandaPanel\Infolists\Layouts\Section | section | Card dengan heading, description opsional, dan header action |
PandaPanel\Infolists\Layouts\Grid | grid | Kolom tanpa heading dan tanpa box |
PandaPanel\Infolists\Layouts\Tabs | tabs | Tab bar dan panel-panelnya |
PandaPanel\Infolists\Layouts\Tab | tab | Satu panel di dalam tab set |
Tidak ada Callout, EmptyState, Wizard, atau Relationship di sini. Semua itu adalah layout form dan ada untuk input, step, atau proses write — hal yang tidak dimiliki infolist. Tidak ada CustomComponent juga; extension point untuk infolist adalah custom entry. Lihat Custom entries.
Section
Sekelompok entry dengan judul yang dirender sebagai card.
| Method | Signature | Default |
|---|---|---|
make() | static make(string $heading): self | — |
schema() | schema(array $components): self | [] |
description() | description(string $description): self | null |
columns() | columns(int $columns): self | 1, di-clamp ke 1–4 |
headerActions() | headerActions(array $actions): self | [] |
getHeaderActions() | getHeaderActions(): list<Action> | — |
entries() | entries(): list<Entry> | Semua entry di dalamnya, seberapa dalam pun nested |
toArray() | toArray(Model $record): ?array | Null jika semua child hidden |
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Actions\Action;
use PandaPanel\Actions\Enums\ActionVariant;
use PandaPanel\Infolists\Components\TextEntry;
use PandaPanel\Infolists\Layouts\Section;
Section::make('Invitation')
->description('Sent when the account was created.')
->columns(2)
->headerActions([
Action::make('resend')
->label('Resend')
->icon('mail')
->variant(ActionVariant::Ghost)
->action(static fn (Model $record) => $record->sendEmailVerificationNotification()),
])
->schema([
TextEntry::make('email'),
TextEntry::make('invited_by.name')->label('Invited by'),
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Header action berkaitan dengan kelompok entry, bukan satu nilai tertentu — misalnya "resend invitation" berada di samping detail undangan. Action dideklarasikan di sini dan dijalankan melalui endpoint yang sama dengan infolist action lain; lihat Actions in infolists.
Heading adalah argument constructor, sehingga new Section('Account') setara dengan Section::make('Account').
Grid
Grid mengatur kolom tanpa heading. Section menyampaikan "data ini satu kelompok dan ini judulnya"; grid hanya menyampaikan "data ini diletakkan berdampingan". Karena itu grid cocok digunakan di dalam section yang sudah memiliki judul.
| Method | Signature | Default |
|---|---|---|
make() | static make(int $columns = 2): self | 2, di-clamp ke 1–4 |
schema() | schema(array $components): self | [] |
entries() | entries(): list<Entry> | Semua entry di dalamnya |
toArray() | toArray(Model $record): ?array | Null jika semua child hidden |
use PandaPanel\Infolists\Components\DateTimeEntry;
use PandaPanel\Infolists\Components\TextEntry;
use PandaPanel\Infolists\Layouts\Grid;
use PandaPanel\Infolists\Layouts\Section;
Section::make('Passkey')->schema([
Grid::make(3)->schema([
TextEntry::make('name'),
DateTimeEntry::make('created_at')->label('Added'),
DateTimeEntry::make('last_used_at')->label('Last used')->since(),
]),
]);2
3
4
5
6
7
8
9
10
11
12
Grid juga menjadi body yang umum untuk item repeatable, karena heading tambahan pada setiap item biasanya hanya menambah noise. Lihat Repeatable entries.
Tabs dan Tab
Gunakan ketika sebuah record memiliki lebih banyak informasi daripada yang nyaman ditampilkan sekaligus dalam satu layar.
Tabs
| Method | Signature | Default |
|---|---|---|
make() | static make(array $tabs = []): self | [] |
tabs() | tabs(array $tabs): self | Mengganti daftar tab |
persistTab() | persistTab(bool $persist = true): self | false |
entries() | entries(): list<Entry> | Semua entry pada semua tab |
toArray() | toArray(Model $record): ?array | Null jika semua tab kosong |
Tab
| Method | Signature | Default |
|---|---|---|
make() | static make(string $label): self | — |
schema() | schema(array $components): self | [] |
icon() | icon(string $icon): self | null |
badge() | badge(string $badge): self | null |
columns() | columns(int $columns): self | 1, di-clamp ke 1–4 |
entries() | entries(): list<Entry> | Semua entry di dalamnya |
toArray() | toArray(Model $record): ?array | Null jika semua child hidden |
use PandaPanel\Infolists\Components\TextEntry;
use PandaPanel\Infolists\Layouts\Section;
use PandaPanel\Infolists\Layouts\Tab;
use PandaPanel\Infolists\Layouts\Tabs;
return $schema->schema([
Tabs::make([
Tab::make('Account')
->icon('user')
->columns(2)
->schema([
Section::make('Identity')->schema([
TextEntry::make('name'),
TextEntry::make('email'),
]),
]),
Tab::make('Security')
->icon('shield')
->badge('2')
->schema([TextEntry::make('two_factor_confirmed_at')]),
])->persistTab(),
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Icon adalah registry key yang di-resolve melalui resources/js/panel/icons/registry.ts, bukan markup. Nama yang tidak ada di registry tidak menampilkan ikon alih-alih merender sesuatu yang arbitrer. Jalankan php artisan panel:icons setelah menambahkan ikon baru. Lihat Icons.
Badge adalah string biasa — misalnya jumlah atau status. Nilainya tidak dihitung otomatis; format nilainya sendiri sebelum diberikan ke badge().
Semua tab selalu diserialisasi
Semua tab selalu diserialisasi. Seluruh tab hanyalah cara berbeda membaca record yang sama, sehingga meminta data tab saat tab dibuka berarti melakukan request untuk data yang sebenarnya sudah dimiliki halaman. Berpindah tab tidak membutuhkan request.
persistTab()
Menyimpan tab yang sedang aktif ke parameter URL tab, sehingga reload atau link yang dibagikan akan membuka tab yang sama:
Tabs::make([...])->persistTab(); // /admin/users/1?tab=securityKey tab adalah Str::slug() dari label dan dibuat di server agar pesan error serta URL dapat menggunakan nama yang konsisten:
Tab::make('Account Details'); // key: 'account-details'Jika URL menyebut tab yang tidak ada, tab pertama akan dibuka alih-alih menampilkan keadaan kosong.
Kolom dan span
Setiap container memiliki jumlah kolomnya sendiri, dan span sebuah entry dihitung terhadap kolom milik container tersebut:
$schema->columns(2)->schema([ // the root grid: 2
Section::make('Wide')->columns(3)->schema([
TextEntry::make('a'), // 1 of 3
TextEntry::make('b')->columnSpan(2), // 2 of 3
TextEntry::make('c')->columnSpanFull(), // the whole row
]),
]);2
3
4
5
6
7
Layout itu sendiri selalu mengambil seluruh row dari parent. InfolistNode.vue meneruskan 'full' untuk semua node yang bukan entry. Karena itu renderer tidak menyediakan dua section berdampingan. Jika membutuhkan susunan horizontal, letakkan Grid di dalam section.
Jumlah kolom di-clamp ke 1–4 oleh PandaPanel\Support\ColumnCount::clamp(), karena resources/js/panel/lib/grid.ts hanya memiliki class Tailwind literal untuk satu sampai empat kolom. Class interpolasi seperti grid-cols-${n} tidak akan tersedia di bundle. Responsiveness-nya: satu kolom di mobile, maksimal dua di md, lalu tiga atau empat di lg.
Layout kosong tidak dirender
toArray() pada setiap layout mengembalikan null jika semua child-nya juga mengembalikan null:
use PandaPanel\Infolists\Layouts\Section;
$section = Section::make('Nothing')->schema([
TextEntry::make('email')->visible(static fn (): bool => false),
]);
$section->toArray($record); // null2
3
4
5
6
7
Hal yang sama berlaku pada Grid, Tab, dan — jika semua tab kosong — Tabs. Heading kosong terlihat seperti masalah rendering, sehingga heading ikut menghilang bersama entry. Perilaku ini berantai: section yang hanya berisi grid kosong juga dianggap kosong.
Data yang dikirim ke frontend
// Section
['component' => 'section', 'heading' => 'Account', 'description' => null, 'columns' => 2, 'schema' => [...], 'headerActions' => []]
// Grid
['component' => 'grid', 'columns' => 3, 'schema' => [...]]
// Tabs
['component' => 'tabs', 'persistTab' => true, 'tabs' => [...]]
// Tab
['component' => 'tab', 'label' => 'Account', 'key' => 'account', 'icon' => 'user', 'badge' => null, 'columns' => 2, 'schema' => [...]]2
3
4
5
6
7
8
9
10
11
Mirror TypeScript-nya adalah InfolistSectionDefinition, InfolistGridDefinition, InfolistTabsDefinition, dan InfolistTabDefinition di resources/js/panel/types/infolist.ts. InfolistNode.vue melakukan recursion berdasarkan component, sehingga kedalaman nesting merupakan persoalan data, bukan persoalan jumlah komponen Vue yang berbeda.
Catatan
- Loose entry dikelompokkan. Entry yang dideklarasikan langsung pada top-level schema, di luar layout, dikumpulkan menjadi satu card oleh
InfolistRenderer.vue, bukan dibuat satu box per entry. Menggabungkan loose entry dan section diperbolehkan: card loose entry muncul lebih dulu, lalu layout mengikuti sesuai urutan deklarasi. Grid::make()melakukan clamp; constructor langsung tidak.Grid::make(9)menjadi empat kolom.new Grid(9)tetap mengirim sembilan pada payload, lalu renderer fallback ke satu kolom karena class literal untuk sembilan tidak ada. Gunakanmake().- Tabs dengan satu tab tetap menampilkan tab bar. Jika tidak menginginkan itu, gunakan section.
- Header action hanya tersedia pada
Section.Grid,Tab, danTabstidak memilikinya.InfolistSchema::allActions()juga hanya menelusuri top-level section untuk header action — section di dalam tab dapat menampilkan action yang tidak bisa ditemukan endpoint. Letakkan action seperti itu pada schema melaluiactions()atau pada entry. Tab::badge()menerima string. Cast jumlah sendiri, misalnya->badge((string) $record->passkeys->count())pada schema yang dibangun melalui closure, atau format nilainya sebelum schema dibangun.