Skip to main content
The panel dashboard is a per-staff grid of widgets. An add-on contributes a card to it by registering a Widget class, without touching any panel source.

The widget contract

A widget extends Lunar\Panel\Dashboard\Widget:
  • key() — the widget’s stable identity. Staff order and visibility preferences are stored against it, so it must not change once shipped.
  • component() — the Vue component name, resolved the same way as a slot component: a bare name for a first-party widget, a namespaced name (example-addon::CustomerCountWidget) for an add-on.
  • label() / description() — shown in the card header and the dashboard’s “Add a widget” dialog. Resolve them through __() so they translate.
  • icon() — a name from the panel’s built-in icon set (see Icon.vue); add-ons cannot register their own SVGs.
  • span()Lunar\Panel\Dashboard\WidgetSpan::Half (default, one grid column) or WidgetSpan::Full (spans both columns).
  • flat()true renders the widget with no card chrome at all, for a KPI-row style widget. Most widgets leave this false.
  • permission() — a manifest permission handle. A staff member lacking it never receives the widget: it is excluded from the dashboard props entirely, not merely hidden client-side.
  • position() — the shared Lunar\Panel\Support\Position primitive, used only to seed the default order; staff reordering overrides it per user (see Ordering with Position below).
  • visibleByDefault()false ships the widget hidden until a staff member adds it from the customise dialog. Useful for a widget that is not universally relevant.
The dashboard grid owns the card chrome — header (icon, label, description), drag handle, and hide button. A widget’s Vue component renders only the body.

Registering a widget

Return Widget classes from Section::widgets():

Worked example

src/Dashboard/CustomerCountWidget.php in panel-addon-example:
This widget ships hidden by default (visibleByDefault(): false), so it appears in the “Add a widget” dialog rather than on the dashboard immediately, and it is gated on the same sales:manage-customers permission the add-on’s other Customers-area extensions use.

Deferred data

data(DashboardRange $range) is computed server-side against the currently selected range and ships as a deferred Inertia prop, keyed by the widget: widgetData.{key} (widgetData.example-addon-customers here). This means:
  • A slow widget’s query never blocks the rest of the dashboard from rendering.
  • Nothing is computed for a widget the staff member has hidden — an unregistered or invisible widget’s data() is never called.
DashboardRange (Lunar\Panel\Dashboard\DashboardRange) is a string-backed enum: Today (today), SevenDays (7d), ThirtyDays (30d), NinetyDays (90d). It exposes start() / end() (the current window) and previousStart() / previousEnd() (the equivalent prior window, for delta comparisons), plus buckets(), which returns hourly buckets for Today and daily buckets otherwise — use it so a chart widget aggregates on the same boundaries as every first-party chart.

The Vue component

The component registered against component() receives two props: data (whatever data() returned) and range (the current range’s string value). It renders body content only — no card wrapper, no header. resources/js/components/CustomerCountWidget.vue:
Register the component at the top level of the add-on’s bundle, alongside pages and slot components, never inside window.LunarPanel.booting() (its callbacks run after the panel’s first render, too late for a component the dashboard needs immediately):
Optionally, build the widget’s body with the panel’s chart primitives exported from @lunarphp/panelTimeSeriesChart, Sparkline, DonutChart, KpiCard.

Ordering with Position

position() returns a Lunar\Panel\Support\PositionPosition::priority(int) for coarse ordering, or Position::before('key') / Position::after('key') to anchor next to another widget, first-party or add-on. This only sets the default order a newly-registered widget appears in: staff drag-reorder their own dashboard afterward, and that per-staff order takes over from registration order once set.

Per-staff visibility and layout

The dashboard is per-staff, not global. Each staff member can:
  • Reorder widgets by dragging (customise mode).
  • Hide a visible widget.
  • Re-add a hidden widget from the “Add a widget” dialog (label, icon, and description come from the widget’s registration).
This layout persists server-side per staff member, so it follows them across devices. A widget whose permission() the current staff member lacks is never included in the dashboard’s widget list at all — it cannot be added, hidden, or discovered by a user without the permission.
Widget component names are resolved through window.LunarPanel.resolveExtensionComponent(), the same mechanism PanelSlot and component-rendered table columns use. An unresolvable component name is skipped with a console warning rather than breaking the dashboard.