Layout Sidebar dan Header
Shell tempat panel dirender: side rail atau top bar, lebarnya, bagian mana yang tersedia, serta batas lebar content column. Seluruhnya merupakan konfigurasi panel — page tidak memilih layout sendiri — dan dikirim ke frontend melalui panel.sidebar serta panel.shell.
Beralih ke top navigation
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
final class KioskPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('kiosk')
->auth()
->topNavigation() // a top bar instead of a side rail
->breadcrumbs(false)
->maxContentWidth('5xl');
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
PanelLayout.vue membaca panel.sidebar.variant lalu merender SidebarPanelLayout atau HeaderPanelLayout. Tidak ada page atau konfigurasi app.ts yang memilihnya secara terpisah.
Pemanggilan sidebar()
/**
* @param 'sidebar'|'header' $variant
* @param 'sidebar'|'floating'|'inset' $appearance
*/
public function sidebar(
bool $collapsible = true,
bool $defaultOpen = true,
string $variant = 'sidebar',
string $appearance = 'inset',
): self2
3
4
5
6
7
8
9
10
Semua argument memiliki default sehingga biasanya dipanggil dengan named argument:
$panel->sidebar(appearance: 'floating');
$panel->sidebar(collapsible: false, defaultOpen: false);
$panel->sidebar(variant: 'header');2
3
| Argument | Value | Default | Efek |
|---|---|---|---|
collapsible | bool | true | false merender rail tetap (shadcn collapsible="none") dan membuat seluruh navigation group non-collapsible. |
defaultOpen | bool | true | Diserialisasi sebagai panel.sidebar.defaultOpen. Lihat catatan di bawah. |
variant | 'sidebar', 'header' | 'sidebar' | Menentukan shell yang dirender. Sama dengan AppVariant milik starter kit. |
appearance | 'sidebar', 'floating', 'inset' | 'inset' | Cara rail dirender. Diabaikan oleh header shell. |
Tiga appearance: inset membuat content pane tampak mengambang di dalam background sidebar; floating memisahkan rail sebagai rounded card; sidebar menempelkan rail ke sisi layar dengan border. Value yang tidak dikenal fallback ke inset di frontend agar tidak sampai ke shadcn component sebagai unhandled variant.
topNavigation() merupakan bentuk API yang lebih sesuai cara developer memikirkan pilihan tersebut:
public function topNavigation(bool $topNavigation = true): self$panel->topNavigation(); // identical to sidebar(variant: 'header')
$panel->topNavigation(false); // back to the rail2
Perhatikan bahwa sidebar() mengatur keempat value sekaligus. Memanggilnya setelah topNavigation() akan mengembalikan variant ke default kecuali variant ikut diberikan.
Lebar
public function sidebarWidth(string $width, ?string $collapsedWidth = null): self
public function collapsedSidebarWidth(string $width): self2
$panel->sidebarWidth('18rem', '4rem');
$panel->collapsedSidebarWidth('3.5rem');2
Default-nya 16rem dan 3rem. Nilai ini adalah CSS length, bukan size token: value menjadi custom property --sidebar-width dan --sidebar-width-icon pada rail. Angka yang harus diubah menjadi class akan memerlukan interpolated class, yang tidak akan tersedia di bundle Tailwind.
Menonaktifkan bagian shell
public function navigation(bool $navigation = true): self // true
public function topbar(bool $topbar = true): self // true
public function breadcrumbs(bool $breadcrumbs = true): self // true
public function hasNavigation(): bool
public function hasTopbar(): bool
public function hasBreadcrumbs(): bool2
3
4
5
6
7
$panel->navigation(false) // no rail, and no top nav bar either
->topbar(false) // no header row: no breadcrumbs, search, switcher, bell, theme toggle
->breadcrumbs(false); // header stays, breadcrumb trail goes2
3
Setiap option benar-benar menghapus bagian tersebut, bukan hanya menyembunyikannya. Tidak ada component yang dirender dan, untuk navigation, tidak ada navigation tree yang digunakan dari shared prop. Panel satu page tidak memiliki sesuatu untuk dinavigasi; kiosk mungkin tidak membutuhkan breadcrumbs.
Mematikan topbar juga menghapus global search, panel switcher, tenant switcher, dan notification bell karena keempat fitur tersebut berada di dalam topbar.
Lebar content
public function maxContentWidth(?string $maxContentWidth): self // null by default
public function getMaxContentWidth(): ?string2
Nilainya berupa token yang dipetakan di frontend ke literal Tailwind class, karena class hasil interpolation tidak akan bertahan saat compile:
| Token | Class |
|---|---|
full | max-w-full |
7xl | max-w-7xl |
6xl | max-w-6xl |
5xl | max-w-5xl |
4xl | max-w-4xl |
3xl | max-w-3xl |
$panel->maxContentWidth('5xl');null atau token yang tidak ada pada tabel fallback ke max-w-full.
Mengganti sidebar atau topbar
public function sidebarComponent(?string $component): self
public function topbarComponent(?string $component): self2
$panel
->sidebarComponent('Panels/Admin/Shell/Sidebar')
->topbarComponent('Panels/Admin/Shell/Topbar');2
3
Argument adalah build-time registry key, bukan markup dan bukan filesystem path. Component harus berada di resources/js/pages/Panels/{Panel}/Shell/*.vue, lokasi yang dipindai oleh glob resolveShellComponent(). Nama yang tidak pernah ikut build tidak dapat dicapai dari runtime apa pun.
<!-- resources/js/pages/Panels/Admin/Shell/Sidebar.vue -->
<script setup lang="ts">
import { useNavigation } from '@/panel/composables/useNavigation';
import { usePanel } from '@/panel/composables/usePanel';
const { groups } = useNavigation();
const { panel } = usePanel();
</script>2
3
4
5
6
7
8
Replacement menerima navigation yang sama seperti component bawaan, sehingga ia hanya merupakan cara menggambar panel yang berbeda, bukan sumber kebenaran kedua tentang navigation. Nama yang tidak terdaftar fallback ke built-in rail — typo component tidak boleh membuat user terjebak pada page tanpa navigation. Jalankan npm run build setelah menambahkan component karena glob dievaluasi saat build.
Replacement topbar hanya digunakan oleh header shell (HeaderPanelLayout) dan menerima navigation groups melalui prop groups.
Account menu
/** @param array<array-key, array{label: string, url: string, icon?: string|null}> $items */
public function userMenuItems(array $items): self
/** @return list<array<string, mixed>> */
public function getUserMenuItems(): array2
3
4
5
$panel->userMenuItems([
['label' => 'Support', 'url' => '/support', 'icon' => 'info'],
['label' => 'Status', 'url' => 'https://status.example.com'],
]);2
3
4
Entry bersifat akumulatif, icon default ke null, dan setiap entry adalah link yang dibuat server, bukan action name. Menu dirender pada setiap page panel, dan destination link melakukan authorization untuk dirinya sendiri ketika dibuka.
Data tiba sebagai panel.shell.userMenuItems. Isi account menu berasal dari component milik host application (UserMenuContent.vue, bagian starter-kit seam), sehingga tidak ada component yang dikirim package yang otomatis merender entry tersebut. Baca datanya di tempat yang Anda inginkan:
import { usePanel } from '@/panel/composables/usePanel';
const { shell } = usePanel(); // shell.userMenuItems2
3
Data yang diterima frontend
panel('admin')->getSidebar();
// [
// 'collapsible' => true,
// 'defaultOpen' => true,
// 'variant' => 'sidebar',
// 'appearance' => 'inset',
// 'width' => '16rem',
// 'collapsedWidth' => '3rem',
// 'component' => null,
// ]
panel('admin')->getShell();
// [
// 'navigation' => true,
// 'topbar' => true,
// 'breadcrumbs' => true,
// 'topbarComponent' => null,
// 'userMenuItems' => [],
// ]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Keduanya berada di dalam toSharedArray() sehingga tersedia pada setiap panel page:
import { usePanel } from '@/panel/composables/usePanel';
const { panel, shell, maxContentWidthClass } = usePanel();
// panel.value.sidebar.variant, shell.value.breadcrumbs, ...2
3
4
Refresh shell
Tidak ada endpoint khusus yang menjawab "bagaimana bentuk sidebar saat ini". Endpoint semacam itu tetap harus me-resolve panel, user, dan URL agar jawabannya benar — pekerjaan yang memang sudah dilakukan request page. Karena itu refetch shared props:
import { usePanelShell } from '@/panel/composables/usePanelShell';
const { reloadNavigation, reloadTopbar, reloadShell } = usePanelShell();
reloadNavigation(); // router.reload({ only: ['navigation'] })
reloadTopbar(); // only: ['panel', 'notifications', 'panels']
reloadShell(); // both of the above2
3
4
5
6
7
Gunakan setelah sesuatu mengubah isi navigation: badge jumlah pending record, resource yang baru menjadi visible, atau tenant switch.
Catatan
defaultOpenbukan penentu rail benar-benar terbuka.AppShell.vuebawaan meneruskan propsidebarOpenmilik host application — yang dibaca dari cookiesidebar_statediHandleInertiaRequests— keSidebarProvidermilik shadcn.panel.sidebar.defaultOpendiserialisasi agar custom component dapat membacanya, tetapi saat ini tidak meng-override cookie tersebut.collapsible: falsememiliki dua efek: rail menjadi fixed dan navigation group menjadi non-collapsible. Efek kedua berasal dariNavigationRegistry, yang dibangun menggunakan collapsible flag panel.- Layout berada pada level panel. Page yang membutuhkan shell berbeda sebaiknya menjadi panel berbeda, atau merender custom component di dalam shell yang sudah diberikan.
- Setiap panel page mendeklarasikan
defineOptions({ layout: PanelLayout })sendiri. Assignment unconditionalpage.default.layout = AppLayoutdiresources/js/app.tsmenimpa deklarasi tersebut dan membuat seluruh screen panel masuk ke shell aplikasi pada HTTP 200 tanpa log error. Gunakan??=;panel:installsengaja tidak menyelesaikan instalasi secara silent jika menemukan bentuk assignment unconditional. - Layout memakai
overflow-x-clip, bukanoverflow-x-hidden, pada content column.hiddenpada satu axis membuat axis lain terhitung sebagaiauto, sehingga element berubah menjadi scroll container dan diam-diam merusak sticky bar di dalamnya.