Icons
Setiap icon di panel direpresentasikan sebagai string di sisi PHP dan sebagai Lucide component di sisi Vue. Keduanya dihubungkan oleh satu generated file: resources/js/panel/icons/registry.ts. Gunakan halaman ini ketika icon tidak tampil, ketika menambahkan nama icon baru ke panel, atau ketika memasukkan panel:icons ke dalam CI.
Registry ini adalah build-time allowlist, bukan dynamic lookup. Lucide menyediakan lebih dari seribu icon, sementara satu panel biasanya hanya memakai beberapa puluh. Hanya icon yang benar-benar digunakan yang seharusnya masuk bundle.
Contoh minimal yang berfungsi
Deklarasikan icon di PHP menggunakan nama Lucide dalam kebab-case:
use PandaPanel\Pages\Page;
final class Reports extends Page
{
protected static ?string $navigationIcon = 'chart-line';
}2
3
4
5
6
Bangun ulang registry dari source, lalu build frontend:
php artisan panel:icons
npm run build2
Registered 24 icons.Jika panel:icons tidak dijalankan, icon hanya tidak muncul — tanpa error, tanpa fallback glyph, dan tanpa element visual apa pun.
Tempat nama icon dapat dideklarasikan
panel:icons memindai PHP source untuk berbagai bentuk deklarasi nama icon. Ada lima pattern utama ditambah satu special case:
| Bentuk | Contoh |
|---|---|
| Fluent setter | ->icon('trash-2') |
| Navigation property | protected static ?string $navigationIcon = 'users'; |
| Named argument | icon: 'shield' |
| Array key | 'icon' => 'mail' |
| Prime component | Icon::make('info') |
Method bernama icon() | setiap string literal di dalam body method tersebut |
Pattern terakhir digunakan untuk enum. Enum yang menjawab “icon apa yang dipakai case ini?” biasanya menyimpan nama icon di dalam match arms, yang tidak terlihat oleh pattern lain:
enum OrderState: string
{
case Pending = 'pending';
case Shipped = 'shipped';
public function icon(): string
{
return match ($this) {
self::Pending => 'clock',
self::Shipped => 'truck',
};
}
}2
3
4
5
6
7
8
9
10
11
12
13
Scanner dibatasi berdasarkan nama method icon(), bukan sekadar mencari setiap match, sehingga match yang mengembalikan string biasa di tempat lain tidak salah dianggap sebagai daftar icon.
Nama icon dibaca langsung dari source, bukan dari panel yang di-boot saat runtime. Ini disengaja karena icon dapat dideklarasikan pada tempat yang tidak pernah dijangkau runtime walk — wizard step, filter tab, atau header action yang dibangun di dalam method. String literal di file adalah denominator bersama dari seluruh deklarasi tersebut.
API yang menerima nama icon
Pada seluruh API berikut, nama icon selalu merupakan registry key, bukan component path dan bukan class:
| Class | Method atau property |
|---|---|
PandaPanel\Core\Panel | icon(?string $icon): self — brand mark |
PandaPanel\Pages\Page | $navigationIcon, $activeNavigationIcon |
PandaPanel\Resources\Resource | $navigationIcon, $activeNavigationIcon |
PandaPanel\Actions\Action | icon(string $icon): static |
PandaPanel\Tables\Tab | icon(?string $icon): self |
PandaPanel\Forms\Layouts\Tab | icon(string $icon): self |
PandaPanel\Forms\Layouts\Step | icon(?string $icon): self |
PandaPanel\Forms\Layouts\Callout | icon(string $icon): self |
PandaPanel\Forms\Layouts\EmptyState | icon(string $icon): self |
PandaPanel\Forms\Prime\Icon | Icon::make(string $icon): self |
PandaPanel\Forms\Prime\Text | icon(string $icon): self |
PandaPanel\Forms\Support\Block | icon(string $icon): self |
PandaPanel\Infolists\Layouts\Tab | icon(string $icon): self |
PandaPanel\Notifications\Notification | icon(string $icon): self |
PandaPanel\Widgets\Support\Stat | icon(string $icon): self |
PandaPanel\Tables\TableSchema | emptyState(string $heading, ?string $description = null, ?string $icon = null): self |
Panel::userMenuItems() | optional key icon pada setiap entry |
Command
php artisan panel:icons
php artisan panel:icons --check2
| Option | Efek |
|---|---|
| (tidak ada) | menulis ulang resources/js/panel/icons/registry.ts |
--check | tidak menulis apa pun; gagal jika registry sudah tidak sinkron |
Command melakukan langkah berikut:
- membaca seluruh icon Lucide yang tersedia di disk dari
node_modules/@lucide/vue/dist/esm/icons; - memindai dua source tree untuk nama icon — directory
app/milik application dan source milik framework; - membuang nama yang tidak tersedia di Lucide dan melaporkannya secara eksplisit;
- menulis registry dalam urutan ter-sort.
Kedua tree selalu dipindai. Source framework tidak optional. Sebagian besar icon yang tampil di panel berasal dari built-in actions seperti delete, edit, dan export. Jika scanner hanya membaca app/, registry dapat ditulis ulang tanpa icon built-in tersebut sehingga action bawaan kehilangan icon tanpa error.
Nama yang tidak tersedia di Lucide dianggap typo, karena typo pada jalur ini menghasilkan button tanpa icon dan tanpa warning runtime:
Not a Lucide icon: users-round-altCommand keluar dengan non-zero status ketika hal ini terjadi, walaupun icon valid lainnya tetap ditulis.
Jika Lucide belum tersedia di disk — misalnya panel:icons dijalankan sebelum npm install — command tidak memiliki sumber untuk memvalidasi nama. Daripada menganggap semua nama invalid dan mengosongkan registry, command memberi warning dan menerima nama apa adanya:
@lucide/vue is not installed; nothing to check names against.Output ditulis ke FrontendPaths::panel('icons/registry.ts'), sehingga project yang memindahkan panda-panel.frontend.panel_path tetap mendapat registry pada path yang benar.
Generated file
import { Check, CircleAlert, Copy, Eye, /* … */ } from '@lucide/vue';
import type { Component } from 'vue';
const ICONS = {
check: Check,
'circle-alert': CircleAlert,
copy: Copy,
eye: Eye,
// …
} satisfies Record<string, Component>;
export type PanelIconName = keyof typeof ICONS;
export function isPanelIconName(name: string): name is PanelIconName;
export function resolveIcon(name: string | null | undefined): Component | null;2
3
4
5
6
7
8
9
10
11
12
13
14
15
Nama kebab-case ditulis sebagai quoted keys, sedangkan nama satu kata tidak. Keduanya generated; jangan edit file ini secara manual karena panel:icons berikutnya akan overwrite perubahan Anda.
Me-resolve icon di Vue
import { resolveIcon, isPanelIconName } from '@/panel/icons/registry';
import type { PanelIconName } from '@/panel/icons/registry';
resolveIcon('shield'); // Component
resolveIcon('not-an-icon'); // null
resolveIcon(null); // null
resolveIcon(undefined); // null
isPanelIconName('shield'); // true2
3
4
5
6
7
8
9
| Export | Signature |
|---|---|
PanelIconName | type PanelIconName = keyof typeof ICONS |
isPanelIconName | (name: string) => name is PanelIconName |
resolveIcon | (name: string | null | undefined) => Component | null |
Gunakan seperti shipped components: resolve terlebih dahulu, lalu render hanya jika hasilnya tersedia.
<script setup lang="ts">
import { computed } from 'vue';
import { resolveIcon } from '@/panel/icons/registry';
const props = defineProps<{ icon: string | null }>();
const resolved = computed(() => resolveIcon(props.icon));
</script>
<template>
<component :is="resolved" v-if="resolved" class="size-4" />
</template>2
3
4
5
6
7
8
9
10
11
12
resolveIcon menerima null dan undefined, sehingga caller tidak perlu melakukan guard sebelum memanggilnya. Guard ditempatkan di template terhadap hasil resolver.
Mengapa menggunakan generated map, bukan dynamic import
Ada dua alasan, dan alasan kedua menentukan design.
Me-resolve arbitrary server-supplied string langsung menjadi component akan membuat panel metadata memiliki jalur ke executable code di bundle. Nama icon memang berasal dari registered PHP declaration dan bukan dari request input, sehingga allowlist adalah lapisan keamanan kedua, bukan pertama — tetapi lapisan kedua tetap bernilai pada batas antara data dan code execution.
Alasan lain adalah kebutuhan bundler. Dynamic import berdasarkan runtime string tidak dapat dianalisis statis, sehingga bundler tidak tahu file apa yang harus dimasukkan ke output. Generated import list dapat dianalisis: setiap icon di map dipastikan masuk bundle dan icon lain tidak ikut masuk.
Ketika icon tidak tampil
Pada development, resolveIcon menulis warning satu kali per nama:
[panel] The icon [chart-line] is not in the icon registry, so nothing is drawn
for it. Run `php artisan panel:icons` to rebuild the registry from the icons
your panels declare.2
3
Production diam secara sengaja. Ini adalah build problem dan console warning pada live panel tidak membantu end user.
Urutan pemeriksaan:
- Apakah
panel:iconssudah dijalankan? Ini adalah penyebab paling umum. Menambahkan nama icon di PHP tanpa regenerate registry membuat icon tidak tersedia. - Apakah nama tersebut benar-benar ada di Lucide?
panel:iconsmelaporkan nama invalid. - Apakah menggunakan kebab-case? Gunakan
trash-2, bukanTrash2atautrash2. - Apakah bundle sudah dibuild ulang? Registry adalah source file; build lama tidak mengandung import baru.
Di CI
php artisan panel:icons --checkCommand gagal jika file registry di disk berbeda dari hasil yang seharusnya ditulis, dan juga gagal jika ada nama icon yang tidak valid. Ini mengubah silent missing icon menjadi red build:
The icon registry is out of date. Run php artisan panel:icons.Test suite package melakukan assertion yang sama, sekaligus memeriksa navigation, resource icons, row actions, dan bulk actions dari setiap registered panel.
Gotchas
- Nama yang tidak terdaftar tidak menggambar apa pun. Tidak ada placeholder atau broken image — hanya ruang kosong. Ini adalah salah satu failure mode yang sengaja silent, sehingga
--checkdi CI sangat penting. panel:iconsmembutuhkannode_modules. Tanpa@lucide/vuedi disk, command tidak dapat memvalidasi nama dan memberi warning alih-alih menghapus registry.- Jangan edit registry secara manual. File memiliki generated banner dan run berikutnya akan overwrite. Tambahkan nama icon melalui PHP declaration.
activeNavigationIconfallback kenavigationIcon. Keduanya dikirim pada navigation item agar icon dapat berganti saat client-side navigation tanpa menunggu server menentukan active item lagi — sehingga kedua nama harus terdaftar. Scanner secara eksplisit mengenali$navigationIcon, tetapi nama yang hanya dipakai pada$activeNavigationIcondapat terlewat. Deklarasikan juga pada pattern yang dapat dipindai agar active icon tidak hilang.- Icon yang hanya muncul di
data()payload tidak dipindai. Scanner mencari literal pada PHP source. Nama yang disusun saat runtime seperti'chart-'.$typetidak terlihat dan tidak otomatis masuk registry. - Scanner membaca
app/dan source framework. Icon yang dideklarasikan di package custom lain di luarapp/tidak terlihat; deklarasikan nama tersebut pada file yang dipindai atau register dari panel provider.