CSS Hooks
CSS Hook adalah titik bernama di dalam shell panel yang memiliki class stabil, dan panel dapat menambahkan class miliknya sendiri pada titik tersebut. Gunakan fitur ini ketika dua panel berbagi satu build tetapi harus memiliki tampilan berbeda, atau ketika stylesheet perlu menargetkan bagian tertentu dari panel tanpa mengganti component bawaan.
Hook merepresentasikan makna — nama hook menunjuk sebuah lokasi di layout. Karena itu daftar namanya bersifat tertutup. Mekanisme ini berbeda dari colors(), yang mengatur nilai.
Contoh minimal yang berfungsi
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
final class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->auth()
->cssHooks([
'topbar' => 'border-b-2 border-amber-500',
'table-row' => 'hover:bg-amber-50',
]);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
npm run buildTopbar sekarang dirender dengan class="panel-topbar border-b-2 border-amber-500", dan setiap table row dengan class="… panel-table-row hover:bg-amber-50". Tidak ada bagian lain yang berubah dan tidak ada component yang diganti.
Class stabil selalu tersedia terlebih dahulu
Setiap bagian yang dirender framework sudah memiliki class panel-{name} yang ditulis langsung di Vue component, bukan dibuat secara dinamis. Class tersebut selalu ada, terlepas dari apakah panel menambahkan konfigurasi atau tidak. Inilah yang membuatnya menjadi target stylesheet yang dapat diandalkan:
/* resources/css/panels/admin.css */
.panel-topbar {
backdrop-filter: blur(6px);
}
.panel-table-row:nth-child(even) {
background-color: var(--muted);
}2
3
4
5
6
7
8
cssHooks() menambahkan class ke class stabil tersebut, bukan menggantinya. Panel yang tidak mendeklarasikan apa pun tetap menghasilkan panel-topbar.
Nama hook
Seluruh nama berasal dari PandaPanel\Support\CssHooks::HOOKS, berikut component yang mengeluarkannya:
| Hook | Dihasilkan oleh | Element |
|---|---|---|
shell | SidebarPanelLayout.vue, HeaderPanelLayout.vue | root shell, yang juga membawa theme custom properties |
sidebar | PanelSidebar.vue | rail/sidebar |
topbar | PanelHeader.vue | header row |
page | kedua layout | content column |
page-header | PageHeader.vue | heading block |
table | DataTable.vue | table wrapper |
table-row | DataTable.vue | setiap body row |
form | FormRenderer.vue | element form |
infolist | InfolistRenderer.vue | infolist wrapper |
widget | WidgetShell.vue | setiap widget, termasuk custom widget |
modal | ActionModal.vue | action dialog |
Nama di luar daftar tersebut dibuang. Ini disengaja: mendaftarkan class untuk bagian shell yang sebenarnya tidak pernah dirender hanya akan menghasilkan konfigurasi yang tidak berefek dan sulit dilacak.
StylingTest memastikan setiap nama dalam allowlist benar-benar dihasilkan oleh suatu component. Menambahkan nama baru tanpa menghubungkannya ke component akan membuat test suite gagal.
PHP API
Panel::cssHooks()
/**
* @param array<string, string> $classes keyed by hook name
*/
public function cssHooks(array $classes): self2
3
4
use PandaPanel\Core\Panel;
Panel::make('admin')->cssHooks([
'shell' => 'font-mono',
'widget' => 'shadow-sm',
]);2
3
4
5
6
Pemanggilan berulang bersifat akumulatif, bukan replace, karena dua pemanggilan yang sama-sama menargetkan topbar berarti kedua class memang dimaksudkan untuk dipakai:
$panel
->cssHooks(['topbar' => 'border-amber-500'])
->cssHooks(['topbar' => 'border-b-2']);
$panel->getCssHooks();
// ['topbar' => 'border-amber-500 border-b-2']2
3
4
5
6
Behavior ini juga memungkinkan plugin menambahkan class tanpa menghapus class yang dideklarasikan panel sendiri.
Panel::getCssHooks()
/**
* @return array<string, string>
*/
public function getCssHooks(): array2
3
4
Panel::make('plain')->getCssHooks(); // []Value ini selalu ada di toSharedArray() pada key cssHooks. Untuk panel yang tidak mendeklarasikan apa pun, nilainya adalah [], karena frontend membacanya secara unconditional.
PandaPanel\Support\CssHooks
Ini adalah class yang mendasari method pada Panel. Biasanya Anda tidak perlu menginstansiasinya secara langsung, tetapi class ini mendefinisikan allowlist:
use PandaPanel\Support\CssHooks;
CssHooks::HOOKS;
// ['shell', 'sidebar', 'topbar', 'page', 'page-header', 'table',
// 'table-row', 'form', 'infolist', 'widget', 'modal']
$hooks = new CssHooks;
$hooks->add(['topbar' => 'border-b-2', 'nonsense' => 'dropped']);
$hooks->add(['topbar' => 'border-amber-500']);
$hooks->toArray();
// ['topbar' => 'border-b-2 border-amber-500']2
3
4
5
6
7
8
9
10
11
12
13
| Member | Signature | Catatan |
|---|---|---|
HOOKS | public const HOOKS | list<string> — allowlist tertutup |
add | add(array $classes): self | keyed berdasarkan hook name; nama tidak dikenal dibuang, nama valid ditambahkan |
toArray | toArray(): array | array<string, string>, hanya hook yang memiliki class |
Sisi Vue
Satu composable membaca theme dan hooks sekaligus dari shared panel props:
import { usePanelStyling } from '@/panel/composables/usePanelStyling';
const { themeStyle, hook } = usePanelStyling();2
3
| Member | Type | Arti |
|---|---|---|
themeStyle | ComputedRef<Record<string, string>> | light palette panel sebagai inline custom properties |
hook | (name: string) => string | class untuk satu bagian bernama |
hook('sidebar'); // 'panel-sidebar' ketika panel tidak menambahkan class
hook('topbar'); // 'panel-topbar border-b-2 border-amber-500'2
Cara pemakaiannya sama dengan semua shipped component:
<script setup lang="ts">
import { usePanelStyling } from '@/panel/composables/usePanelStyling';
const { hook } = usePanelStyling();
</script>
<template>
<div class="rounded-lg border p-4" :class="hook('widget')">
<slot />
</div>
</template>2
3
4
5
6
7
8
9
10
11
Gunakan composable ini juga pada component buatan Anda — misalnya custom shell atau custom page — agar hook milik panel diterapkan baik pada component bawaan framework maupun component yang Anda buat. hook() menerima string apa pun dan mengembalikan panel-{name} ditambah class yang didaftarkan panel untuk nama tersebut. Jadi replacement sidebar yang memanggil hook('sidebar') berperilaku sama dengan sidebar bawaan.
Memastikan class ikut masuk ke build
Bagian ini sering menjadi sumber masalah. Tailwind v4 memindai source file untuk nama class, sementara string yang ditulis di PHP Panel Provider tidak berada pada file yang dipindai Tailwind. Akibatnya class dapat muncul di DOM tetapi tidak tersedia di bundle CSS.
Ada tiga solusi, berdasarkan urutan yang direkomendasikan:
Gunakan class yang sudah muncul di template application. Class seperti hover:bg-muted, border-b-2, dan shadow-sm biasanya sudah ikut dikompilasi.
Tambahkan provider ke Tailwind sources. @source pada stylesheet menggunakan path relatif terhadap file tersebut:
/* resources/css/app.css */
@import 'tailwindcss';
@source '../../app/Panels';2
3
4
Gunakan plain CSS terhadap stable class. Tidak melibatkan compiler dan tidak berisiko class menghilang hanya karena template lain dihapus:
.panel-topbar {
border-bottom: 2px solid #f59e0b;
}2
3
Pendekatan ketiga adalah fungsi utama per-panel stylesheet. Daftarkan dengan assets(), dan stylesheet hanya dimuat pada page milik panel tersebut:
$panel->assets('resources/css/panels/admin.css');Lihat Panel Assets. Konfigurasi membutuhkan dua perubahan karena path tersebut juga harus ditambahkan ke input di vite.config.ts.
Hook dan theme
Kedua mekanisme saling melengkapi. Perbedaannya penting untuk dipahami:
colors() | cssHooks() | |
|---|---|---|
| Yang diatur | value CSS custom property | nama class |
| Daftar nama | terbuka, tetapi divalidasi terhadap PanelTheme::PROPERTIES | tertutup melalui CssHooks::HOOKS |
| Tempat diterapkan | attribute style pada root shell | attribute class pada bagian bernama |
| Membutuhkan Tailwind rebuild | tidak | ya, kecuali class sudah tersedia |
| Input invalid | dibuang secara silent | dibuang secara silent |
Color adalah sebuah value, dan value tidak berubah menjadi class; karena itu vocabulary-nya dapat terbuka. Hook adalah sebuah tempat, dan mendeklarasikan tempat yang tidak pernah dirender hanya menciptakan class yang tidak akan terlihat; karena itu daftar hook ditutup.
Gotchas
- Class yang tidak pernah dikompilasi Tailwind tidak menghasilkan efek dan tidak menimbulkan error. DOM tetap menampilkan class tersebut, tetapi halaman tidak berubah dan tidak ada log. Periksa built CSS sebelum menyimpulkan hook tidak bekerja.
pagedihasilkan oleh kedua layout — shell sidebar maupun header — sehingga class pada hook ini berlaku apa pun variant layout panel.table-rowberada pada setiap body row, termasuk relation manager dan table widget. Semuanya menggunakanDataTable.vue.shelljuga menjadi tempat theme diterapkan. Element yang sama membawastyle="--primary: …", sehingga hook class pada shell dapat menggunakan custom properties milik panel.- Unknown hook name dibuang tanpa warning. Allowlist adalah dasar kontraknya. Jika hook terlihat tidak bekerja, periksa spelling terhadap
CssHooks::HOOKSterlebih dahulu. - Hook berlaku per panel, bukan per page. Hook berasal dari shared panel props, sehingga berlaku pada semua page panel tersebut dan tidak berlaku di luar panel. Untuk behavior spesifik satu page, gunakan render hook atau gambar langsung di component page tersebut.