home / skills / hackur / web-standards-playground-showcase / nova-resource

This skill helps you create and modify Laravel Nova 5.x resources with PCR Card patterns, including tabs, badges, and constant selects.

npx playbooks add skill hackur/web-standards-playground-showcase --skill nova-resource

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.8 KB
---
name: Nova Resource Builder
description: Create and modify Laravel Nova 5.x resources with PCR Card patterns (tab panels, Badge fields with closures, Select fields with constants). Triggers include "nova resource", "nova badge", "nova tabs", "nova field".
allowed-tools:
  - Read
  - Write
  - Edit
  - Grep
  - Glob
  - Bash
---

# Nova Resource Builder

Build Nova 5.x resources following PCR Card's established patterns.

## When to Use

- Creating new Nova resources
- Adding fields to existing resources
- Implementing tab-based layouts
- Configuring Badge/Select fields with constants
- Setting up Nova search

## Quick Commands

```bash
# Create resource
php artisan nova:resource ResourceName

# Validate Nova search configs
./scripts/dev.sh validate:nova-search

# Clear Nova cache
./scripts/dev.sh nova:publish
```

## PCR Card Nova Patterns

### 1. Badge Fields (Closure Pattern)

**CRITICAL**: Badge fields use closures + hardcoded string maps, NOT constants.

```php
use Laravel\Nova\Fields\Badge;

Badge::make('Status', function () {
    // Closure returns calculated value
    return $this->is_active ? 'active' : 'inactive';
})
->map([
    'active' => 'success',      // Hardcoded strings
    'inactive' => 'danger',
    'pending' => 'warning',
])
->label(function ($value) {
    return match ($value) {
        'active' => 'Active',
        'inactive' => 'Inactive',
        'pending' => 'Pending',
    };
});
```

### 2. Select Fields (Constants Pattern)

Use constant class `options()` method:

```php
use App\Constants\PromoCodeType;
use Laravel\Nova\Fields\Select;

Select::make('Type')
    ->options(PromoCodeType::options())  // Returns ['fixed' => 'Fixed Amount', ...]
    ->displayUsingLabels()
    ->sortable()
    ->rules('required');
```

### 3. Tab-Based Layouts

Use `Tab::group()` with `Heading::make()` for sections:

```php
use Laravel\Nova\Tabs\Tab;
use Laravel\Nova\Fields\Heading;

public function fields(NovaRequest $request): array
{
    return [
        ID::make()->sortable(),

        Tab::group('Resource Information', [
            Tab::make('Overview', [
                Heading::make('Basic Details'),
                Text::make('Name')->required(),

                Heading::make('Settings'),
                Boolean::make('Is Active'),
            ]),

            Tab::make('Details', [
                Heading::make('Additional Information'),
                Textarea::make('Description'),
            ]),
        ]),
    ];
}
```

**Rules**:
- Use `Tab::group('Title', [...])` for panel with heading
- Use `Heading::make('Name')` for section dividers
- NO `Panel::make()` inside tabs
- HasMany relationships work in tabs

### 4. Search Configuration

```php
public static $search = [
    'id',
    'name',
    'user.email',           // Relationship search
    'submission.submission_number',
];

// REQUIRED: Eager load relationships
public static $with = ['user', 'submission'];
```

**Validate before commit**:
```bash
./scripts/dev.sh validate:nova-search
```

## Constants Reference

All constants follow this pattern:

```php
class ConstantName
{
    public const PREFIX_VALUE = 'value';

    public static function all(): array;        // All values
    public static function label(string $value): string;  // Human label
    public static function options(): array;    // For Select fields
    public static function isValid(string $value): bool;  // Validation
}
```

**Available Constants** (18 total):
- `App\Constants\PromoCodeType` - TYPE_FIXED, TYPE_PERCENTAGE
- `App\Constants\ManualPaymentMethod` - METHOD_CASH, METHOD_CHECK, etc.
- `App\Constants\ManualPaymentStatus` - STATUS_PENDING, STATUS_VERIFIED, etc.
- `App\Constants\SubmissionState` - DRAFT, SUBMITTED, RECEIVED, etc. (stores `::class` refs)
- `App\Constants\CardState` - RECEIVED, ASSESSMENT, IN_PROGRESS, etc.
- See `app/Constants/` for all 18 classes

## Common Pitfalls

**❌ WRONG**: Using NovaBadgeType constants in Badge fields
```php
Badge::make('Status')
    ->map(fn($value) => NovaBadgeType::SUCCESS);  // ❌ Don't do this
```

**✅ CORRECT**: Use hardcoded strings
```php
Badge::make('Status', function () {
    return $this->state;
})
->map([
    'active' => 'success',    // ✅ Hardcoded strings
    'inactive' => 'danger',
]);
```

**❌ WRONG**: Manual options array for Select
```php
Select::make('Type')
    ->options([
        'fixed' => 'Fixed Amount',
        'percentage' => 'Percentage',
    ]);
```

**✅ CORRECT**: Use constant `options()` method
```php
Select::make('Type')
    ->options(PromoCodeType::options());  // ✅ Centralized
```

## Documentation Links

- Nova Admin Guide: `docs/development/NOVA-ADMIN-GUIDE.md`
- Nova Search Guide: `docs/development/NOVA-SEARCH-GUIDE.md`
- Constants Pattern: CLAUDE.md "Constants Pattern & Nova Best Practices"
- Laravel Nova Docs: https://nova.laravel.com/docs/5.0

Overview

This skill helps create and modify Laravel Nova 5.x resources using the PCR Card patterns. It generates resources, organizes fields into tabbed panels, and configures Badge and Select fields following established closure and constants conventions. Use it to enforce consistent Nova layouts, search configuration, and field behaviors across projects.

How this skill works

The skill inspects Nova resource definitions and scaffolds or updates fields using three core patterns: Badge fields using closures with hardcoded string maps, Select fields wired to constant classes via options(), and Tab::group layouts with Heading section dividers. It also validates Nova search arrays and required eager-loaded relationships, and offers quick commands to create resources or run search validation.

When to use it

  • Creating new Nova resources consistent with PCR Card patterns
  • Adding or refactoring fields to follow Badge and Select conventions
  • Building tab-based UIs with Tab::group and Heading section dividers
  • Configuring Nova search and ensuring required eager loads
  • Validating resource conventions before committing changes

Best practices

  • Badge fields: return values from closures and map to hardcoded string statuses (avoid using Nova badge constants)
  • Select fields: always use ConstantClass::options() to centralize labels and values
  • Tabs: wrap panels with Tab::group('Title', [...]) and use Heading::make() for section separators
  • Search: include relationship keys in the static $search array and list those relationships in public static $with to ensure eager loading
  • Run the provided validation script (validate:nova-search) before committing search changes

Example use cases

  • Scaffold a new Product Nova resource with tabbed sections for Overview and Details
  • Convert badge fields to closure+map pattern to fix inconsistent label styles
  • Replace inline select option arrays with constant::options() to centralize translations and validation
  • Add relationship fields to search and update public static $with to prevent N+1 issues
  • Validate and publish Nova cache after structural changes using provided dev scripts

FAQ

Why use closures for Badge fields instead of constants?

Closures let the badge value be computed per-record; PCR Card pattern maps those returned string keys to hardcoded Nova visual styles. Avoid using Nova badge constants in the map to keep mappings explicit.

When should I use a constants class for Select options?

Use constants whenever options are reused, validated, or referenced elsewhere. The class provides all(), label(), options(), and isValid() helpers to keep selects centralized and consistent.