formfeed-uk/nova-resource-cards
Nova Resource Cards replaces Laravel Nova's default resource index table with responsive card grids, letting you display records as configurable cards with images, badges, and custom fields for easier scanning of listings and search results.
Install with Composer alongside Nova:
composer require formfeed-uk/nova-resource-cards
The service provider is auto-discovered; there is generally no publish step. Check composer.json first for the Nova major-version constraint — this package tracks Nova releases closely, so match it to the Nova version in your app before debugging anything else.
First use case: putting KPI/metric cards on a Nova resource index page. Add the package's card trait to the resource you want cards on, then implement a cards() method returning an array of standard Nova card instances (Value, Trend, Partition, etc.):
use FormfeedUK\NovaResourceCards\HasCards; // confirm exact trait name in the README
class Order extends Resource
{
use HasCards;
public function cards(Request $request): array
{
return [
(new Metrics\OrdersPerDay)->width('1/3'),
(new Metrics\RevenuePerDay)->width('1/3'),
];
}
}
As of 1.1.0 the same card definitions can also be surfaced on Dashboards and Lenses, so before writing new cards for those pages, check whether an existing resource card can be reused.
Single source of truth for card definitions. Put card arrays in a dedicated class or config key (e.g. OrderCards::make()) and return it from the resource, the dashboard, and any lenses. With 1.1.0 adding dashboard/lens support, the same metric will now be requested from three places; duplicating the definitions is how widths, cache durations, and URI keys drift apart.
Conditional cards per user. Return [] early or filter the array before returning it when the current user lacks permission. This is cheaper than relying on canSee() alone because unauthorised cards are never instantiated and their queries never run.
Filter-aware cards. Where a card supports it, call ->refreshWhenFiltersChange() so the dashboard/lens filter bar actually re-queries the metric. This is especially relevant for the new lens support — a lens page with filters and a stale card is a common source of "the numbers don't match the table" bug reports.
Dashboards. Register the reused cards in your dashboard's cards() method as normal. If a card appears on both a resource and a dashboard, give it an explicit ->uriKey('orders-per-day-dashboard') to avoid Nova treating them as the same card and reusing cached/authorised state unexpectedly.
Lenses. Add cards to the lens's cards() method. Verify the lens's request filters are visible to the card's query — lens cards can be built before all filter state is applied, so assert the numbers against the table in a test or manually.
Testing. Use Nova's testing helpers to hit the resource index and assert the card is present in the response, and add a feature test that the underlying metric query respects the current user's scope. Cards are easy to leave unauthorised because they bypass the resource's normal policy checks in some Nova versions.
1/3, 1/2, 1/4, 2/3, 3/4, and full. Anything else fails silently and produces a broken grid, not an exception.$cacheFor on every metric. A resource index with three uncached Trend cards can add seconds to the page and will be blamed on the package.uriKey() collisions. Nova identifies cards by URI key. The same card class used on a resource, a dashboard, and a lens can collide — particularly after the 1.1.0 dashboard/lens feature. Set explicit keys when reusing.php artisan optimize:clear (and Nova's own asset/cache commands if you've published assets) resolves most "the card isn't showing up" reports before you start reading source.canSee are separate. A card can leak aggregate data to a user who can't view the resource if you only gate the resource.refreshWhenFiltersChange() or document that the lens card is a static snapshot — otherwise users will report inconsistent numbers.cards() return in your own method rather than editing vendor code; the package is small and the array return is the intended seam.How can I help you explore Laravel packages today?