Pengaturan Tampilan
Halaman Account tempat user memilih mode tampilan Light, Dark, atau mengikuti System, dan dirender di dalam Panel tempat user sedang berada.
Ini adalah satu-satunya Settings Page yang sama sekali tidak memiliki state di server. Pilihan tampilan disimpan di browser.
Gunakan dokumentasi ini ketika Anda perlu mengetahui:
- di mana preference tampilan disimpan;
- bagian mana yang membacanya;
- bagaimana theme diterapkan;
- dan bagaimana warna yang dikonfigurasi pada Panel berinteraksi dengan Light/Dark mode.
Contoh Minimal
Tidak ada yang perlu diregistrasikan:
<?php
declare(strict_types=1);
namespace App\Panels\Admin;
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
final class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->auth();
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Periksa route:
php artisan route:list --name=panel.admin.pages.settings-appearanceHasil:
GET admin/settings/appearance panel.admin.pages.settings-appearanceClass Page
PandaPanel\Pages\Settings\AppearanceSettings extends PandaPanel\Pages\Page dan hanya mengoverride satu method.
| Member | Value |
|---|---|
$title | 'Appearance' |
$subheading | 'Choose how the interface looks on this device.' |
$slug | 'settings-appearance' |
$component | 'panel/settings/Appearance' |
$navigationIcon | 'palette' |
$navigationGroup | 'Account' |
$navigationSort | 30 |
$middleware | tidak ada |
routePath() | 'settings/appearance' |
Contoh:
use PandaPanel\Pages\Settings\AppearanceSettings;
AppearanceSettings::routeName('admin');
// 'panel.admin.pages.settings-appearance'
AppearanceSettings::url('admin');
// '/admin/settings/appearance'
AppearanceSettings::url('app');
// '/app/settings/appearance'2
3
4
5
6
7
8
9
10
Signature:
public static function routeName(
PandaPanel\Core\Panel|string|null $panel = null
): string;
public static function url(
PandaPanel\Core\Panel|string|null $panel = null
): string;2
3
4
5
6
7
Class ini tidak mengoverride props().
Base implementation mengembalikan:
[]Jadi component hanya menerima:
- metadata
page; - shared prop
panel.
Tidak ada:
appearance
savedValue
server-side preference
form submission2
3
4
Apa yang Dirender
File:
resources/js/pages/panel/settings/Appearance.vuemenggunakan PanelLayout dan merender satu component utama:
<script setup lang="ts">
import AppearanceTabs from '@/components/AppearanceTabs.vue';
import { Card, CardContent } from '@/components/ui/card';
import PanelLayout from '@/panel/layouts/PanelLayout.vue';
defineOptions({ layout: PanelLayout });
defineProps<{ page: PageMetadata }>();
</script>
<template>
<Card>
<CardContent>
<AppearanceTabs />
</CardContent>
</Card>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AppearanceTabs.vue menampilkan tiga pilihan:
Light
Dark
System2
3
Saat user memilih salah satunya, component memanggil:
updateAppearance(value)Tidak terjadi:
- HTTP request;
- flash message;
- reload;
- submit ke server.
Composable
File:
resources/js/composables/useAppearance.tsdisediakan package dan dipublish ke path yang sama pada application.
API:
export type Appearance =
| 'light'
| 'dark'
| 'system';
export type ResolvedAppearance =
| 'light'
| 'dark';
export type UseAppearanceReturn = {
appearance: Ref<Appearance>;
resolvedAppearance: ComputedRef<ResolvedAppearance>;
updateAppearance: (value: Appearance) => void;
};
export function useAppearance(): UseAppearanceReturn;
export function updateTheme(value: Appearance): void;
export function initializeTheme(): void;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Appearance dan ResolvedAppearance juga dire-export melalui:
@/types| Export | Fungsi |
|---|---|
useAppearance() | Membaca preference yang tersimpan saat mount dan mengembalikan state serta setter |
updateAppearance(value) | Menulis ke localStorage, cookie, lalu menerapkan theme |
resolvedAppearance | Me-resolve system melalui matchMedia('(prefers-color-scheme: dark)') |
updateTheme(value) | Menambah/menghapus class dark pada document.documentElement |
initializeTheme() | Menerapkan stored preference saat boot dan mendaftarkan listener perubahan theme OS |
Contoh:
<script setup lang="ts">
import { useAppearance } from '@/composables/useAppearance';
const {
appearance,
resolvedAppearance,
updateAppearance,
} = useAppearance();
</script>
<template>
<button @click="updateAppearance('dark')">
Dark
</button>
<p>
Currently {{ appearance }},
rendering as {{ resolvedAppearance }}.
</p>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
initializeTheme()
initializeTheme() seharusnya dijalankan dari entry Inertia application sebelum UI selesai dimount.
// resources/js/app.ts
import {
initializeTheme,
} from '@/composables/useAppearance';
createInertiaApp({
// ...
});
initializeTheme();2
3
4
5
6
7
8
9
10
11
Tanpa ini, preference baru diterapkan setelah component yang menggunakan useAppearance() mencapai onMounted().
Akibatnya dapat terjadi:
Stored Theme = Dark
↓
First Paint = Light
↓
Vue Mounted
↓
Theme berubah menjadi Dark2
3
4
5
6
7
User akan melihat flash singkat dengan theme yang salah.
Selain itu listener matchMedia() untuk mode system tidak akan didaftarkan.
Tempat Preference Disimpan
Dua tempat digunakan untuk dua pembaca yang berbeda:
| Storage | Key | Ditulis oleh | Dibaca oleh |
|---|---|---|---|
localStorage | appearance | updateAppearance() | useAppearance() dan initializeTheme() |
| Cookie | appearance, path=/, max-age 365 hari, SameSite=Lax | updateAppearance() | Server-side rendering agar first paint menggunakan theme yang benar |
Tidak ada data yang:
- ditulis ke database;
- dikirim ke route Panel;
- disimpan pada User Model.
Artinya preference bersifat per browser/device, bukan per user account.
User yang sama pada device lain akan kembali menggunakan:
systemClass yang Diterapkan
Hasil akhirnya adalah:
document.documentElement.classList.toggle(
'dark',
systemTheme === 'dark',
);2
3
4
Tailwind menggunakan class tersebut untuk dark variant.
Toggle pada Panel Header
PanelHeader.vue menggunakan composable yang sama:
const {
appearance,
updateAppearance,
} = useAppearance();
const isDark = computed(
() => appearance.value === 'dark',
);
function toggleAppearance(): void {
updateAppearance(
isDark.value
? 'light'
: 'dark',
);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Header toggle hanya berpindah:
light ↔ darkIa tidak dapat memilih system.
User yang ingin kembali ke mode system harus membuka halaman Appearance Settings.
Warna Milik Panel
Appearance dan Panel Theme adalah dua concern berbeda.
Appearance menentukan:
Light / DarkSedangkan Panel menentukan palette.
API:
public function colors(
array $light,
array $dark = []
): self;
public function getTheme(): array;
// [
// 'light' => [...],
// 'dark' => [...],
// ]2
3
4
5
6
7
8
9
10
Contoh:
$panel->colors(
light: [
'primary' => 'oklch(0.55 0.18 265)',
'sidebar' => '#f8fafc',
],
dark: [
'primary' => 'oklch(0.72 0.15 265)',
],
);2
3
4
5
6
7
8
9
10
Value menjadi CSS custom properties.
PandaPanel\Support\PanelTheme memvalidasinya terhadap:
- 18 nama property yang diizinkan;
- empat bentuk value.
Format warna yang didukung:
hex
rgb()
hsl()
oklch()2
3
4
Value invalid dibuang, bukan membuat Panel gagal dirender.
panel.theme
Kedua map dikirim ke frontend melalui:
panel.themeusePanelStyling() menerapkan Light map sebagai inline custom properties:
for (
const [property, value]
of Object.entries(theme.light ?? {})
) {
style[`--${property}`] = value;
}2
3
4
5
6
Dark map dikirim tetapi tidak diterapkan inline.
Alasannya inline style tidak dapat memiliki conditional selector atau media query.
Jika palette harus berbeda berdasarkan Light/Dark scheme, gunakan stylesheet yang dimuat melalui:
Panel::assets()Dark Mode Flag
API:
public function darkMode(
bool $darkMode = true
): self;
public function hasDarkMode(): bool;
// true secara default2
3
4
5
6
Value dikirim sebagai:
panel.darkModeShell bawaan saat ini tidak menjadikan flag tersebut sebagai syarat untuk merender Header Toggle.
Jadi:
$panel->darkMode(false);lebih merupakan declaration yang dapat dibaca oleh custom component Anda.
Misalnya custom topbar dapat memilih untuk tidak menampilkan theme toggle.
Mematikan Halaman
$panel->settings(false);Settings bersifat all-or-nothing.
Yang dimatikan sekaligus:
Profile
Security
Appearance2
3
Jika Panel seperti kiosk tidak ingin menyediakan theme preference, matikan built-in settings dan daftarkan custom account page sendiri bila dibutuhkan.
Testing
Tidak ada server state yang perlu diuji.
Yang diuji adalah apakah Page dirender dengan shell dan component yang benar.
use Inertia\Testing\AssertableInertia;
it(
'renders the appearance settings page with no server state',
function (): void {
$this
->actingAs($user)
->get('/app/settings/appearance')
->assertOk()
->assertInertia(
fn (AssertableInertia $page) =>
$page
->component(
'panel/settings/Appearance'
)
->where(
'panel.id',
'app'
)
);
}
);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Test terkait berada di:
tests/Feature/Panel/PanelSettingsTest.phpHal yang Perlu Diperhatikan
- Preference bersifat per device, bukan per user. Local storage dan cookie hanya berlaku pada browser tersebut.
vendor:publishtidak overwrite secara default. Jika starter kit sudah memilikiuseAppearance.ts, file PandaBear dilewati kecuali menggunakan--force.- Tanpa
initializeTheme(), first paint dapat salah. - Header toggle tidak pernah memilih
system. panel.theme.darkdikirim tetapi tidak diterapkan inline.- Route hanya GET. POST menghasilkan
405. - Panel access tetap berlaku. User yang tidak boleh memasuki Panel mendapat
403; guest diarahkan ke login.