Tailwind Theme
Stylesheet panel menggunakan Tailwind v4, dan seluruh tampilan visualnya dibangun dari CSS custom properties. Gunakan halaman ini ketika sebuah panel membutuhkan palette sendiri, ketika color yang Anda set tidak terlihat, atau ketika Anda ingin mengetahui property mana yang benar-benar dibaca stylesheet.
Ada dua mekanisme yang sengaja dipisahkan: color adalah value, diset sebagai custom property, divalidasi, tetapi vocabulary-nya relatif terbuka; sedangkan hook adalah makna/lokasi, diset sebagai class name terhadap daftar tempat yang tertutup.
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()
->colors(
light: ['primary' => '#4f46e5', 'sidebar' => 'oklch(0.98 0 0)'],
dark: ['primary' => '#818cf8'],
);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Tidak perlu rebuild. Value light masuk ke attribute style pada root shell sebagai --primary dan --sidebar, tepat pada custom property yang sudah dibaca stylesheet:
<div class="panel-shell" style="--primary: #4f46e5; --sidebar: oklch(0.98 0 0)">Stylesheet
resources/css/panda-panel.css dipublish ke application Anda, sehingga setelah publish file tersebut menjadi milik project dan dapat diedit. File ini memiliki lima bagian utama.
Imports dan sources
@import 'tailwindcss';
@import 'tw-animate-css';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';2
3
4
5
Pada Tailwind v4, theme, variants, dan content scan dideklarasikan di CSS, bukan config file terpisah. Dua baris @source menambahkan lokasi yang tidak terdeteksi otomatis oleh Tailwind.
Dark variant
@custom-variant dark (&:is(.dark *));Dark mode direpresentasikan sebagai class pada <html>, dikendalikan oleh composable useAppearance milik host application. Karena itu light/dark toggle pada Panel Header dapat bekerja tanpa panel harus memiliki state appearance sendiri.
Theme mapping
@theme inline {
--color-background: var(--background);
--color-primary: var(--primary);
--color-sidebar: var(--sidebar-background);
/* … */
}2
3
4
5
6
Mapping inilah yang mengubah custom property menjadi Tailwind utility. --color-primary: var(--primary) membuat bg-primary mengambil value dari --primary saat ini, termasuk value yang diset inline oleh panel saat runtime.
Family yang dipetakan: background, foreground, card, popover, primary, secondary, muted, accent, destructive, border, input, ring, chart-1 sampai chart-5, dan delapan color sidebar-*. Selain itu ada --font-sans dan tiga radius step yang diturunkan dari --radius.
Palettes
:root {
--background: hsl(0 0% 100%);
--primary: hsl(0 0% 9%);
--radius: 0.5rem;
--sidebar-background: hsl(0 0% 98%);
/* … */
}
.dark {
--background: hsl(0 0% 3.9%);
--primary: hsl(0 0% 98%);
--sidebar-background: hsl(0 0% 7%);
/* … */
}2
3
4
5
6
7
8
9
10
11
12
13
14
Mengedit palette ini mengubah seluruh application. Jika hanya satu panel yang membutuhkan palette khusus, gunakan colors() atau per-panel stylesheet agar beberapa panel yang berbagi satu build tetap dapat memiliki tampilan berbeda.
Base dan component layers
@layer base {
* { @apply border-border outline-ring/50; }
body { @apply bg-background text-foreground; }
}2
3
4
Ada juga satu component class yang dibutuhkan panel dan tidak praktis diekspresikan hanya dengan utility Tailwind: .panel-table-frozen-edge, yaitu seam pada batas group frozen column. Marker tersebut digambar menggunakan pseudo-element, bukan border, karena border akan menggeser content cell satu pixel ketika table mulai scroll. Marker seharusnya menandai edge tanpa membuat layout lain terlihat bergerak.
Panel colors
/**
* @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
API ini menerima value, bukan makna. Color tidak berubah menjadi Tailwind class, sehingga vocabulary value dapat lebih terbuka. Namun property name dan value tetap divalidasi oleh PandaPanel\Support\PanelTheme. Input yang gagal validation dibuang, bukan membuat seluruh panel gagal. Satu color invalid seharusnya tidak menghentikan theme valid lainnya.
Property yang boleh diset panel
Daftar property menggunakan allowlist karena typo akan menghasilkan custom property yang tidak pernah dibaca stylesheet — theme terlihat seperti tidak bekerja tanpa error. Semua nama berikut benar-benar digunakan stylesheet:
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 nama tanpa prefix --; leading dash akan dibuang.
Panel::make('admin')->colors([
'primary' => '#4f46e5',
'primry' => '#ffffff', // dibuang: bukan property yang dibaca stylesheet
])->getTheme();
// ['light' => ['primary' => '#4f46e5'], 'dark' => []]2
3
4
5
Syntax value yang diterima
$panel->colors([
'primary' => '#fff', // 3 sampai 8 hex digits
'secondary' => 'rgb(10, 20, 30)', // rgb() dan rgba()
'accent' => 'hsl(210 40% 96%)', // hsl() dan hsla()
'muted' => 'oklch(0.97 0 0)', // oklch(), seperti yang digunakan stylesheet
]);2
3
4
5
6
Syntax lain dibuang. Value akhirnya ditempatkan di attribute style, sehingga string seperti red; content: url(https://…) bukan color lagi, melainkan potongan stylesheet:
Panel::make('admin')->colors([
'primary' => '#4f46e5',
'background' => 'red; content: url(https://evil.test)', // dibuang
'accent' => 'expression(alert(1))', // dibuang
])->getTheme()['light'];
// ['primary' => '#4f46e5']2
3
4
5
6
Dua pemanggilan colors() melakukan merge, bukan replace, sehingga plugin dapat menambahkan color tanpa menghapus konfigurasi panel.
PandaPanel\Support\PanelTheme
Class yang digunakan di balik API Panel:
use PandaPanel\Support\PanelTheme;
$theme = new PanelTheme;
$theme->light(['primary' => '#4f46e5']);
$theme->dark(['primary' => '#818cf8']);
$theme->isEmpty(); // false
$theme->toArray();
// ['light' => ['primary' => '#4f46e5'], 'dark' => ['primary' => '#818cf8']]2
3
4
5
6
7
8
9
10
| Member | Signature | Catatan |
|---|---|---|
light | light(array $colors): self | merge; disanitasi |
dark | dark(array $colors): self | merge; disanitasi |
isEmpty | isEmpty(): bool | true jika kedua palette kosong |
toArray | toArray(): array | array{light: …, dark: …} |
Panel::colors() hanya memanggil dark() jika argument kedua tidak kosong. Jadi colors(['primary' => '#fff']) tidak menghapus dark palette yang mungkin sudah ada.
Cara color diterapkan
import { usePanelStyling } from '@/panel/composables/usePanelStyling';
const { themeStyle } = usePanelStyling();
// { '--primary': '#4f46e5', '--sidebar': 'oklch(0.98 0 0)' }2
3
4
Kedua layout mengikat value tersebut ke root shell, element yang sama tempat hook('shell') diterapkan:
<AppShell variant="sidebar" :class="hook('shell')" :style="themeStyle">Custom properties ditempatkan pada root shell agar seluruh content panel berada dalam scope theme, sementara page di luar panel tidak terkena pengaruhnya.
Dark palette diserialisasi tetapi tidak diterapkan inline
themeStyle hanya berisi light values. Inline style tidak dapat mengekspresikan kondisi “hanya ketika berada di bawah .dark”, sehingga dark values tetap dikirim pada panel.theme.dark agar dapat digunakan component atau stylesheet custom.
Jika theme harus berbeda berdasarkan color scheme, gunakan stylesheet dan Panel::assets():
/* resources/css/panels/admin.css */
.panel-shell {
--primary: #4f46e5;
}
.dark .panel-shell {
--primary: #818cf8;
}2
3
4
5
6
7
8
$panel->assets('resources/css/panels/admin.css');Diperlukan dua perubahan: path juga harus ditambahkan ke input pada vite.config.ts. Jika tidak, Vite tidak memiliki asset untuk disajikan dan page akan gagal dengan manifest error. Lihat Panel Assets.
Color yang sebenarnya merupakan makna
Tidak semua color dalam panel adalah arbitrary value. Status yang ditampilkan sebagai badge adalah makna, dan setiap makna harus dipetakan ke literal Tailwind class yang terlihat oleh build. Karena itu vocabulary-nya tertutup, di server sebagai PandaPanel\Tables\Enums\BadgeColor dan di frontend sebagai satu palette module:
import {
BADGE_CLASSES,
ICON_CLASSES,
SELECTED_CLASSES,
} from '@/panel/palette';
import type { BadgeColorName } from '@/panel/palette';
// 'neutral' | 'success' | 'warning' | 'danger' | 'info'
BADGE_CLASSES.success;
// 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300'2
3
4
5
6
7
8
9
10
| Export | Digunakan untuk |
|---|---|
BadgeColorName | lima nama yang mencerminkan BadgeColor |
BADGE_CLASSES | badge pada tables, forms, dan infolists |
ICON_CLASSES | icon, memakai vocabulary color yang sama |
SELECTED_CLASSES | border dan background untuk pressed/selected control |
Mapping didefinisikan satu kali dan digunakan bersama. Dengan demikian status yang sama pada badge table dan toggle button form memiliki color yang sama karena benar-benar memakai map yang sama, bukan dua map berbeda yang kebetulan sinkron hari ini.
Aturan literal class
Setiap Tailwind class dalam panel ditulis lengkap. Interpolated class tidak terlihat oleh compiler, sehingga class tersebut tidak ada di bundle dan failure-nya silent.
Empat area yang menggunakan mapping literal:
| Value | Dipetakan oleh | Ditulis sebagai |
|---|---|---|
| Badge colors | @/panel/palette | BADGE_CLASSES |
| Grid columns dan spans | @/panel/lib/grid | GRID_CLASSES, MD_SPAN_CLASSES, LG_SPAN_CLASSES |
| Widget column spans | WidgetGrid.vue | SPAN_CLASSES |
| Content width | usePanel.ts | MAX_WIDTH_CLASSES |
// usePanel.ts
const MAX_WIDTH_CLASSES = {
full: 'max-w-full',
'7xl': 'max-w-7xl',
'6xl': 'max-w-6xl',
'5xl': 'max-w-5xl',
'4xl': 'max-w-4xl',
'3xl': 'max-w-3xl',
} as const;2
3
4
5
6
7
8
9
maxContentWidth('5xl') adalah token, bukan class string, karena alasan ini. Value yang tidak dikenal fallback ke max-w-full.
Dua value lain yang mungkin terlihat cocok menjadi class tetapi sengaja tidak: width dan collapsedWidth milik sidebar. Keduanya adalah CSS length dalam rem, diterapkan sebagai custom property --sidebar-width dan --sidebar-width-icon. Column::width() juga menggunakan inline style untuk alasan yang sama.
Memastikan class custom ikut build
Class yang hanya ditulis pada PHP panel provider — misalnya melalui cssHooks() — tidak berada dalam file yang otomatis dipindai Tailwind:
/* resources/css/app.css */
@import 'tailwindcss';
@source '../../app/Panels';2
3
4
Alternatifnya gunakan class yang sudah muncul pada template lain di application, atau tulis plain CSS terhadap stable class panel-*. Lihat CSS Hooks.
Gotchas
- Color yang dibuang tidak menghasilkan warning. Unknown property atau value yang tidak dapat diparse hanya hilang dari theme. Periksa
getTheme()jika color tidak diterapkan. - Dark palette tidak melakukan apa pun secara otomatis. Value dikirim ke frontend tetapi shipped component tidak mengaplikasikannya. Gunakan per-panel stylesheet.
colors()mengatur value, bukan utility. Tidak ada arbitrary property seperti--color-brand; property harus termasuk vocabulary yang benar-benar dibaca stylesheet.--sidebardan--sidebar-backgroundadalah property berbeda. Theme mapping memetakan--color-sidebarke--sidebar-background, sementara:rootmendefinisikan keduanya.colors(['sidebar' => …])mengatur--sidebar.- Mengedit
:rootdipanda-panel.cssmengubah seluruh application, termasuk screen starter kit. Color khusus panel sebaiknya diletakkan dicolors()atau panel stylesheet. - Stylesheet merupakan published file.
panel:assetsakan melaporkan edit Anda sebagaimodifieddan tidak overwrite saat upgrade — konsekuensinya upstream improvement pada file tersebut tidak otomatis masuk. tw-animate-cssadalah dependency stylesheet. Package mendeklarasikannya didependencies, sehinggapanel:installmelaporkan jika application belum mendeklarasikan dependency tersebut.