Widget class, without touching any panel source.
The widget contract
A widget extendsLunar\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 (seeIcon.vue); add-ons cannot register their own SVGs.span()—Lunar\Panel\Dashboard\WidgetSpan::Half(default, one grid column) orWidgetSpan::Full(spans both columns).flat()—truerenders the widget with no card chrome at all, for a KPI-row style widget. Most widgets leave thisfalse.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 sharedLunar\Panel\Support\Positionprimitive, used only to seed the default order; staff reordering overrides it per user (see Ordering with Position below).visibleByDefault()—falseships the widget hidden until a staff member adds it from the customise dialog. Useful for a widget that is not universally relevant.
Registering a widget
ReturnWidget classes from Section::widgets():
Worked example
src/Dashboard/CustomerCountWidget.php in panel-addon-example:
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 againstcomponent() 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:
window.LunarPanel.booting() (its callbacks run after the panel’s first render, too late for a component the dashboard needs immediately):
@lunarphp/panel — TimeSeriesChart, Sparkline, DonutChart, KpiCard.
Ordering with Position
position() returns a Lunar\Panel\Support\Position — Position::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).
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.