Column Span dan Layout
Widget ditempatkan di dalam responsive CSS grid. Setiap widget mendeklarasikan berapa banyak kolom yang ditempatinya dan pada breakpoint mana aturan tersebut berlaku. Gunakan $columnSpan ketika default satu kolom tidak sesuai: deretan empat angka mungkin membutuhkan seluruh lebar grid, chart biasanya membutuhkan setengahnya, sedangkan badge status mungkin cukup menggunakan satu kolom.
Contoh minimal yang dapat langsung digunakan
use PandaPanel\Widgets\StatsWidget;
final class UserStats extends StatsWidget
{
protected static int|string|array $columnSpan = ['default' => 1, 'md' => 2, 'lg' => 3, 'xl' => 4];
protected static int $sort = 10;
public function stats(): array { /* ... */ }
}2
3
4
5
6
7
8
9
10
Satu kolom pada layar kecil, dua mulai md, tiga mulai lg, lalu memenuhi seluruh lebar mulai xl.
Grid
Setiap page yang merender widget menggunakan grid yang sama, didefinisikan pada WidgetGrid.vue:
grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4| Breakpoint | Jumlah kolom |
|---|---|
default | 1 |
md | 2 |
lg | 3 |
xl | 4 |
Hanya ada empat breakpoint dan empat jumlah kolom tersebut. Sebelum diserialisasi, span milik widget di-resolve menjadi satu value untuk setiap breakpoint.
$columnSpan
/** @var int|string|array<string, int|string> */
protected static int|string|array $columnSpan = 1;2
Tiga bentuk value dapat digunakan.
Integer
protected static int|string|array $columnSpan = 2;Span yang sama digunakan pada semua breakpoint: ['default' => 2, 'md' => 2, 'lg' => 2, 'xl' => 2].
'full'
protected static int|string|array $columnSpan = 'full';Widget memenuhi satu row penuh pada setiap breakpoint, berapa pun jumlah kolom grid. Di frontend value ini dirender sebagai col-span-full.
Array per breakpoint
protected static int|string|array $columnSpan = ['default' => 1, 'md' => 2, 'lg' => 2, 'xl' => 2];Key harus salah satu dari default, md, lg, atau xl. Breakpoint yang tidak Anda tulis akan mewarisi value breakpoint sebelumnya, mengikuti pola perilaku breakpoint CSS:
['default' => 1, 'lg' => 2]
// resolves to ['default' => 1, 'md' => 1, 'lg' => 2, 'xl' => 2]2
Inheritance dimulai dari 1, sehingga array yang tidak memiliki default akan mulai dari satu kolom:
['lg' => 3]
// resolves to ['default' => 1, 'md' => 1, 'lg' => 3, 'xl' => 3]2
Value dapat berupa integer maupun 'full', dan keduanya dapat dikombinasikan:
['default' => 'full', 'lg' => 2];columnSpan()
/** @return array{default: int|string, md: int|string, lg: int|string, xl: int|string} */
public static function columnSpan(): array2
Method ini mengembalikan span yang sudah dinormalisasi dan value tersebut yang kemudian diserialisasi ke widget definition. Normalisasi didelegasikan ke PandaPanel\Widgets\Support\ColumnSpan:
use PandaPanel\Widgets\Support\ColumnSpan;
/**
* @param int|string|array<string, int|string> $span
* @return array{default: int|string, md: int|string, lg: int|string, xl: int|string}
*/
public static function normalize(int|string|array $span, string $context = 'A widget'): array2
3
4
5
6
7
$context hanya digunakan pada exception message — nama class widget akan diberikan secara otomatis.
ColumnSpan::normalize(2);
// ['default' => 2, 'md' => 2, 'lg' => 2, 'xl' => 2]
ColumnSpan::normalize(['default' => 1, 'lg' => 2]);
// ['default' => 1, 'md' => 1, 'lg' => 2, 'xl' => 2]
ColumnSpan::normalize('full')['default'];
// 'full'2
3
4
5
6
7
8
Clamping
Angka di luar batas grid akan di-clamp ke range 1..4:
ColumnSpan::normalize(99); // ['default' => 4, 'md' => 4, 'lg' => 4, 'xl' => 4]
ColumnSpan::normalize(0); // ['default' => 1, ...]2
Span 99 berarti developer meminta lebih banyak kolom daripada yang dimiliki grid, sehingga empat merupakan jawaban paling masuk akal yang dapat diberikan sistem.
Kondisi yang melempar exception
Value selain angka atau 'full' akan melempar PandaPanel\Exceptions\PanelSchemaException:
ColumnSpan::normalize('ful');A widget declares a column span of [ful], which is neither a number nor "full". It
would otherwise be read as 1 — a quarter of the width that was asked for, with
nothing to say why.2
3
Key yang bukan salah satu dari empat breakpoint juga akan melempar exception:
ColumnSpan::normalize(['default' => 1, 'sm' => 2]);A widget declares a column span at [sm], which is not a breakpoint this grid has. It
has: default, md, lg, xl. A key that is not one of those is a line of configuration
that does nothing.2
3
Perbedaan perilaku ini disengaja. Angka di luar range adalah permintaan yang masih dapat dijawab grid secara masuk akal; typo adalah kesalahan konfigurasi, dan melakukan clamp terhadap typo justru akan menyembunyikan masalah.
Default berdasarkan type widget
| Base class | $columnSpan |
|---|---|
PandaPanel\Widgets\Widget | 1 |
PandaPanel\Widgets\StatsWidget | 1 (diwarisi) |
PandaPanel\Widgets\CustomWidget | 1 (diwarisi) |
PandaPanel\Widgets\ChartWidget | ['default' => 1, 'md' => 2, 'lg' => 2, 'xl' => 2] |
PandaPanel\Widgets\TableWidget | ['default' => 1, 'md' => 2, 'lg' => 2, 'xl' => 2] |
Chart dan table melakukan override karena keduanya sulit dibaca jika dipaksa masuk ke seperempat lebar page. Stats widget tidak mengubah default, sehingga widget berisi beberapa angka biasanya mendeklarasikan span sendiri — lihat contoh di bagian awal halaman.
Urutan widget
protected static int $sort = 0;
public static function sort(): int2
3
Widget diurutkan ascending berdasarkan [sort(), id()]. Id — basename class dalam kebab-case — menjadi tiebreaker. Dengan begitu dua widget yang memiliki $sort sama tetap memiliki urutan stabil dan repeatable, bukan bergantung pada urutan hasil discovery.
protected static int $sort = 10; // early
protected static int $sort = 40; // late2
Sisakan jarak antar nilai sort. Menyisipkan widget antara nilai 10 dan 20 lebih mudah daripada harus melakukan renumber terhadap seluruh widget.
Elemen yang membungkus widget
WidgetShell.vue membungkus semua type widget dan menggambar:
$heading, jika tersedia;$descriptiondi bawah heading;- form filter atau tombol Filters di sisi kanan pada row yang sama;
- CSS hook class
panel-widgetpada wrapper; - tidak ada elemen lain — body sepenuhnya ditentukan renderer masing-masing type.
Header row tidak dirender sama sekali ketika widget tidak memiliki heading, description, maupun filter, sehingga widget sederhana tidak membuang vertical space untuk elemen yang tidak digunakan.
protected static ?string $heading = 'Recent sign-ups';
protected static ?string $description = 'The newest accounts, searchable and sortable.';2
3
Keduanya bersifat static, sehingga value-nya sama untuk setiap request.
Layout di dalam widget
Grid menentukan ukuran cell; renderer masing-masing type menentukan layout di dalam cell tersebut.
| Type | Isi cell |
|---|---|
| stats | auto-fitting grid dengan minmax(240px, 1fr) untuk setiap angka |
| chart | card dengan tinggi plot ditentukan $maxHeight (default 220) |
| table | row search, card berisi rows, dan pagination ketika lastPage > 1 |
| custom | sesuai component yang Anda buat |
Jadi stats widget yang mengambil empat kolom dapat menampilkan beberapa angka berdampingan; widget yang sama pada satu kolom akan melakukan wrapping. $columnSpan menentukan lebar area widget, bukan jumlah stat yang harus muat di dalamnya.
Styling hook
Wrapper membawa panel-widget, salah satu CSS hook panel:
$panel->cssHooks([
'widget' => 'rounded-2xl',
]);2
3
Hanya nama hook yang benar-benar dikeluarkan oleh shell yang diterima; widget termasuk salah satunya. Lihat CSS hooks.
Widget pada resource page
headerWidgets() dan footerWidgets() masing-masing merender WidgetGrid sendiri. Karena itu kedua group tidak berbagi row: header widget yang mengambil dua kolom dan footer widget yang mengambil dua kolom tidak akan ditempatkan berdampingan. Jika sebuah group tidak memiliki widget, tidak ada markup yang dirender untuk group tersebut.
Standard resource page membaca default pada level resource terlebih dahulu. Resource::getWidgets() ditampilkan di atas index table, Resource::getHeaderWidgets($page) mengontrol header group untuk page mana pun, dan Resource::getFooterWidgets($page) mengontrol footer group.
use PandaPanel\Resources\Pages\ListRecords;
use PandaPanel\Resources\Resource;
use PandaPanel\Widgets\Widget;
final class OrderResource extends Resource
{
/** @return list<class-string<Widget>> */
public static function getWidgets(): array
{
return [OrderStats::class];
}
}
final class ListOrders extends ListRecords
{
public function headerWidgets(): array
{
return [
...parent::headerWidgets(), // resource defaults
RevenueChart::class,
];
}
public function footerWidgets(): array
{
return [RevenueChart::class]; // separate grid below it
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Catatan
- Semua span class ditulis secara literal di
WidgetGrid.vue. Interpolasi sepertimd:col-span-${n}tidak terlihat oleh Tailwind compiler, sehingga class tersebut tidak akan tersedia di bundle dan semua widget dapat diam-diam menjadi satu kolom. Karena itu value yang diterima dibuat sebagai closed set dan di-clamp, bukan string bebas. - Span yang lebih besar daripada jumlah kolom pada breakpoint tertentu — misalnya
md: 4pada grid dua kolom — bukan error dan tidak di-clamp lagi; browser akan me-resolve-nya sebagai full row. - Gap menggunakan
gap-4dan tidak dapat dikonfigurasi per widget. columnSpan()dipanggil saat serialisasi, sehingga$columnSpanyang tidak valid melempar exception ketika page dirender, bukan ketika class pertama kali di-load.- Chart memiliki kontrol ukuran kedua,
$maxHeight, yang tidak berhubungan dengan grid. Lihat Chart.