Contributing Tiles & Cards
Ship a module. Months later, ship a second module that has something to say about the same business object. The second module's tile appears on the first module's dashboard — and you never touch the first module. The dashboard grew because the ecosystem grew.
This is LUX Weave applied to dashboards: name a stable anchor, contribute to it, and the receiving side stays untouched. Where Weave's links anchor on a concept, a dashboard contribution anchors on a dashboard group's namespace.
In one sentence. A tile or card marks itself
#[Contribute('<target group namespace>')]; the SDK drops it into that group, and the consuming dashboard renders it with no edit to the consuming app — Core's composition, the visibility veto chain, and per-tile error isolation apply to it like to any other tile.
The two roles
Cross-module dashboard composition has two sides, and only one of them writes code:
| Role | Who | What they do |
|---|---|---|
| Contributor | the module that has a tile to add | marks the tile #[Contribute(<groupNs>)] and registers it on its own module |
| Consumer | the module that owns the dashboard | nothing — every dashboard group is composable already |
There is no "make my dashboard extensible" step. The consuming group is a plain, static declaration; it does not know its contributors and is never edited to receive them.
Contributing a tile
Put the #[Contribute] attribute on any Tile or Card, naming the published namespace of the target dashboard group, and register the tile on your own module as usual:
namespace App\Billing\Tiles;
use LaravelUi5\Core\Ui5\AbstractUi5Tile;
use LaravelUi5\Sdk\Weave\Dashboard\Attributes\Contribute;
#[Contribute('com.laravelui5.partners.groups.registry-kpi', weight: 50)]
class OutstandingInvoicesTile extends AbstractUi5Tile
{
public const NAMESPACE = 'app.billing.tiles.outstanding_invoices';
// … VERSION, TITLE, getTileProvider() …
}// App\Billing\BillingModule
public function getTiles(): array
{
return [
new Tiles\OutstandingInvoicesTile($this),
];
}That is the whole contract. The tile is a normal artifact on the Billing module — its provider renders it — and the #[Contribute] marker routes it into the Partners registry-KPI group. Note that nothing on the dashboard reads the tile's #[Access]: see Visibility for who sees it.
The anchor is a namespace string
The first argument is the target group's published namespace — the value of the group's NAMESPACE constant. Pass it as a string literal, not a class import: the contributor depends only on the other module's published surface, never on its PHP classes. A group's namespace is a stable, published identifier, exactly like a Concept name or an app namespace.
Find the namespace in the owning module's documentation (for the Partners Overview it is
com.laravelui5.partners.groups.registry-kpi, exposed asRegistryKpiGroup::NAMESPACE).
Nothing to wire on the consuming side
Core ships a DashboardGroupCollector that the dashboard transformer already merges into every group's children. The SDK reads your #[Contribute] markers and feeds that collector for you the first time a dashboard renders. So the consuming group stays a plain list of its own tiles:
class RegistryKpiGroup extends AbstractUi5DashboardGroup
{
public const NAMESPACE = 'com.laravelui5.partners.groups.registry-kpi';
public function getChildNamespaces(): array
{
return [
TotalPartnersTile::NAMESPACE,
OrganizationsTile::NAMESPACE,
PersonsTile::NAMESPACE,
];
// Contributions are merged in by the transformer — not listed here.
}
}No harvest command, no registry entry, no migration. Deploy the contributing module, and the tile is there on the next request.
Order, visibility, and errors
Contributed children travel the same composition path as a group's own children:
- Order. Contributions render after the group's own tiles. Among themselves they sort by
weightascending (small = high priority), then by namespace.weightdefaults to0. - Visibility. A contributed tile passes through the same veto chain as every other tile, before any data is fetched. Today nothing in that chain reads a tile's
#[Access]: the only vetoer the SDK registers is the Launchpad's, and it prunes Launchpad launcher tiles only. A contributed tile therefore renders for everyone who can open the dashboard, unless you register a vetoer of your own (below). - Errors. If a contributed tile's provider throws, that tile is dropped and its siblings still render (per-tile error isolation). One module's bug cannot break another module's dashboard.
- Lifecycle. Remove the contributing module and its tile simply disappears — the contribution is read live from what is installed, so there are never orphaned doorways.
Hiding a contributed tile
To hide a tile from some viewers, implement Core's VetoerInterface and tag it into the veto chain from a service provider. Vetoers compose by union: any Hide wins.
use LaravelUi5\Core\Ui5\Contracts\Ui5ArtifactInterface;
use LaravelUi5\Core\Ui5\Contracts\Ui5ContextInterface;
use LaravelUi5\Core\Ui5\Emit\Veto\Disposition;
use LaravelUi5\Core\Ui5\Emit\Veto\VetoerInterface;
final class BillingTileVetoer implements VetoerInterface
{
public function dispose(Ui5ArtifactInterface $artifact, Ui5ContextInterface $context): Disposition
{
if ($artifact->getNamespace() !== OutstandingInvoicesTile::NAMESPACE) {
return Disposition::Show;
}
// Your rule, decided from the context (actor, abilities, …).
return $this->allows($context) ? Disposition::Show : Disposition::Hide;
}
}
// in a service provider
$this->app->tag([BillingTileVetoer::class], VetoerInterface::class);A group whose children are all pruned still renders, as an empty panel. The veto chain votes on groups too, so the same vetoer can hide the group itself.
Worked example — settings overrides on the Partners Overview
The SettingsApp ships a single tile:
#[Contribute('com.laravelui5.partners.groups.registry-kpi', weight: 50)]
class PartnerSettingsOverridesTile extends AbstractUi5Tile { /* … */ }Its provider counts the settings that have been overridden above the platform default and renders the number. The tile lives entirely on the SettingsApp; the Partners app is not edited. Open the Partners Overview and the KPI region now carries a "Settings Overrides" tile next to the partner counts — a panel that grew because a second app shipped, not because the Partners app changed. No vetoer covers it, so every viewer of the Partners Overview sees it.
Scope today
- Applies to Tiles and Cards (
Ui5TileInterface/Ui5CardInterface). - One target group per tile (the attribute is not repeatable).
weightgives a stable order; a richer ordering / de-duplication / per-user personalization contract is intentionally deferred.- No
#[Access]-based pruning ships yet; who sees a contributed tile is decided by a vetoer you register.
See also
- LUX Weave: the concept graph and the other contribution facets
- Value Helps: contributing a scope to another vendor's picker
- The Launchpad: the shipped dashboard every module contributes its launcher to
- The Settings App: the app behind the worked example above