Branding, Logo, Icon, dan Favicon
Dokumen ini membahas nama panel, icon yang digunakannya, dan warna tampilannya. Seluruhnya merupakan konfigurasi panel yang diserialisasi ke shared prop panel, sehingga dua panel dalam satu build dapat memiliki tampilan berbeda tanpa harus mengirim component masing-masing. Tidak ada yang dikompilasi di bagian ini: warna dikirim sebagai CSS custom property dan icon berupa registry key.
Panel dengan branding
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
final class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->name('Administrator')
->brandName('Acme')
->icon('shield')
->colors(
light: ['primary' => '#4f46e5', 'sidebar' => 'oklch(0.98 0 0)'],
dark: ['primary' => '#818cf8'],
)
->cssHooks([
'topbar' => 'border-b-2 border-indigo-500',
]);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Header sidebar kemudian menampilkan icon, Acme pada baris pertama dan Administrator di bawahnya. Seluruh page panel dirender dengan --primary bernilai warna indigo tersebut.
Nama
| Method | Signature | Default | Ditampilkan di |
|---|---|---|---|
name | name(string $name): self | Str::headline($id) | Di bawah brand name pada sidebar; baris pertama panel switcher. |
brandName | brandName(string $brandName): self | config('app.name') | Brand sidebar dan header, auth layout, serta baris kedua switcher. |
Panel::make('back-office')->getName(); // 'Back Office'
Panel::make('admin')->getBrandName(); // whatever config('app.name') is2
Frontend membaca keduanya dari panel.name dan panel.brandName.
Icon
public function icon(?string $icon, ?string $darkIcon = null): self
public function darkIcon(?string $icon): self
public function getIcon(): ?string
public function getDarkIcon(): ?string2
3
4
Icon adalah registry key, bukan component, path, ataupun SVG:
$panel->icon('shield');
$panel->icon('sun', darkIcon: 'moon');
// or
$panel->icon('sun')->darkIcon('moon');2
3
4
Nama icon di-resolve melalui resources/js/panel/icons/registry.ts, yaitu allowlist berbasis import yang dihasilkan oleh php artisan panel:icons. Key yang tidak ada dalam registry tidak merender icon sama sekali — secara silent, karena elemen dekoratif tidak boleh dapat merusak page yang dihiasnya.
Workflow-nya: tulis nama Lucide yang diinginkan, lalu regenerate registry.
php artisan panel:icons # rewrite the registry from the names the PHP declares
php artisan panel:icons --check # fail instead of writing, for CI2
Command memindai ->icon('x'), argument kedua ->icon('light', 'dark'), ->darkIcon('x'), $navigationIcon = 'x', icon: 'x', 'icon' => 'x', Icon::make('x'), dan body dari setiap method bernama icon(). Setiap nama diperiksa terhadap icon yang disediakan Lucide, dan command gagal dengan menyebut nama icon yang tidak ditemukan. Failure tersebut adalah warning utama yang tersedia.
Icon panel juga digunakan oleh panel switcher. Pada dark mode, switcher, brand sidebar/header, dan auth layout memakai darkIcon jika tersedia dan fallback ke icon jika tidak.
Logo dan favicon
public function brandLogo(?string $brandLogo, ?string $darkBrandLogo = null): self
public function darkBrandLogo(?string $brandLogo): self
public function getBrandLogo(): ?string
public function getDarkBrandLogo(): ?string
public function favicon(?string $favicon, ?string $darkFavicon = null): self
public function darkFavicon(?string $favicon): self
public function getFavicon(): ?string
public function getDarkFavicon(): ?string2
3
4
5
6
7
8
9
Semuanya berupa string biasa, default ke null, dan diserialisasi ke frontend sebagai panel.brandLogo, panel.darkBrandLogo, panel.favicon, dan panel.darkFavicon.
Sidebar, header shell, dan panel auth layout bawaan merender brandLogo ketika tersedia. Dalam dark mode digunakan darkBrandLogo, dengan fallback ke light logo jika varian gelap tidak tersedia. Jika tidak ada logo, shell menggambar Lucide panel icon. Panel switcher tetap menggunakan icon agar entry tetap ringkas.
<script setup lang="ts">
import { usePanel } from '@/panel/composables/usePanel';
const { panel } = usePanel();
</script>
<template>
<img v-if="panel?.brandLogo" :src="panel.brandLogo" :alt="panel.brandName" class="h-8" />
</template>2
3
4
5
6
7
8
9
favicon tetap dirender oleh root view aplikasi karena file tersebut memiliki document head:
{{-- resources/views/app.blade.php --}}
@if ($favicon = panel()?->getFavicon())
<link rel="icon" href="{{ $favicon }}">
@endif2
3
4
Dark mode
public function darkMode(bool $darkMode = true): self // true by default
public function hasDarkMode(): bool2
Nilainya dikirim sebagai panel.darkMode. Toggle light/dark pada header dirender oleh PanelHeader.vue dan dikendalikan composable useAppearance milik host application. Varian branding dipilih berdasarkan appearance yang sudah di-resolve, sehingga nilai system mengikuti prefers-color-scheme.
Warna
/**
* @param array<string, string> $light
* @param array<string, string> $dark
*/
public function colors(array $light, array $dark = []): self
/** @return array{light: array<string, string>, dark: array<string, string>} */
public function getTheme(): array2
3
4
5
6
7
8
Yang dikirim adalah value, bukan semantic meaning. Warna tidak pernah diubah menjadi Tailwind class sehingga set value-nya terbuka, tetapi nama property dan value tetap divalidasi oleh PandaPanel\Support\PanelTheme. Value yang tidak valid dibuang daripada membuat seluruh konfigurasi gagal. Satu warna yang salah tidak boleh membuat sisa theme ikut hilang.
Property yang dapat diatur panel adalah seluruh PanelTheme::PROPERTIES:
primary | primary-foreground | secondary |
secondary-foreground | accent | accent-foreground |
background | foreground | muted |
muted-foreground | destructive | border |
ring | sidebar | sidebar-foreground |
sidebar-primary | sidebar-accent | sidebar-border |
Tulis tanpa prefix --; leading dash akan dihapus. Nama di luar daftar dibuang karena typo sebaliknya akan menghasilkan custom property yang tidak pernah dibaca — theme terlihat tidak bekerja tanpa alasan yang jelas.
Syntax value yang diterima, yaitu seluruh format yang diizinkan PanelTheme:
$panel->colors([
'primary' => '#fff', // 3 to 8 hex digits
'secondary' => 'rgb(10, 20, 30)', // rgb() / rgba()
'accent' => 'hsl(210 40% 96%)', // hsl() / hsla()
'muted' => 'oklch(0.97 0 0)', // oklch(), which is what the stylesheet is written in
]);2
3
4
5
6
Value lain dibuang karena akhirnya akan ditempatkan di dalam attribute style, sedangkan red; content: url(...) bukanlah warna tetapi stylesheet.
Panel::make('x')->colors([
'primary' => '#4f46e5',
'background' => 'red; content: url(https://evil.test)', // dropped
'primry' => '#ffffff', // dropped: unknown property
])->getTheme();
// ['light' => ['primary' => '#4f46e5'], 'dark' => []]2
3
4
5
6
Dua pemanggilan colors() melakukan merge, bukan replace, sehingga plugin dapat menambahkan warna tanpa menghapus theme panel.
Cara warna diterapkan
usePanelStyling() mengubah light palette menjadi inline custom properties pada root shell, tempat Tailwind v4 theme membacanya:
import { usePanelStyling } from '@/panel/composables/usePanelStyling';
const { themeStyle, hook } = usePanelStyling();
// themeStyle === { '--primary': '#4f46e5', '--sidebar': 'oklch(0.98 0 0)' }2
3
4
Dark palette diserialisasi tetapi tidak diterapkan sebagai inline style. Inline style tidak dapat mengekspresikan "hanya ketika berada di bawah .dark", sehingga dark value tetap dikirim melalui panel.theme.dark untuk digunakan component atau stylesheet. Theme yang harus berbeda berdasarkan color scheme sebaiknya ditulis pada stylesheet yang dimuat melalui assets():
/* resources/css/panels/admin.css */
.dark .panel-shell {
--primary: #818cf8;
}2
3
4
CSS hooks
/** @param array<string, string> $classes keyed by hook name */
public function cssHooks(array $classes): self
/** @return array<string, string> */
public function getCssHooks(): array2
3
4
5
Berbeda dengan warna, nama hook merupakan closed allowlist. Nama hook merepresentasikan posisi nyata pada layout; nama yang tidak dirender hanya akan membuat class yang tidak pernah terlihat.
$panel->cssHooks([
'topbar' => 'border-b-2 border-amber-500',
'table-row' => 'hover:bg-amber-50',
]);2
3
4
| Hook | Dirender oleh |
|---|---|
shell | SidebarPanelLayout.vue, HeaderPanelLayout.vue |
sidebar | PanelSidebar.vue |
topbar | PanelHeader.vue |
page | kedua layout |
page-header | PageHeader.vue |
table, table-row | DataTable.vue |
form | FormRenderer.vue |
infolist | InfolistRenderer.vue |
widget | WidgetShell.vue |
modal | ActionModal.vue |
Setiap bagian sudah memiliki class stabil panel-{name} walaupun panel tidak menambahkan konfigurasi apa pun. cssHooks() menambahkan class tambahan di samping class bawaan tersebut:
hook('topbar'); // 'panel-topbar border-b-2 border-amber-500'
hook('sidebar'); // 'panel-sidebar' when the panel added nothing2
Dua pemanggilan pada hook yang sama melakukan append, bukan replace, karena keduanya memang dimaksudkan berlaku. Nama hook yang tidak dikenal dibuang.
Catatan
- Class yang ditulis di panel provider tidak berada di file yang discan Tailwind. Gunakan class yang sudah muncul di template aplikasi atau tambahkan provider ke content glob Tailwind; jika tidak, class terlihat di DOM tetapi tidak ada di bundle.
colors()dancssHooks()adalah jalur customization yang paling ringan. Jika panel membutuhkan lebih banyak, muat stylesheet melaluiassets(), atau ganti sidebar/topbar melaluisidebarComponent()/topbarComponent().getTheme()dangetCssHooks()selalu hadir ditoSharedArray(), masing-masing sebagai['light' => [], 'dark' => []]dan[]untuk panel yang tidak mendeklarasikan apa pun. Frontend membacanya tanpa conditional existence check.- Icon adalah satu-satunya tempat invalid value sengaja gagal secara silent. Jalankan
panel:icons --checkdi CI agar kesalahan tersebut tetap terdeteksi sebelum release.