smithery.ai

shopper-development

Coding standards and patterns for Shopper monorepo development. Use when creating or modifying Models, Actions, Enums, Livewire components, migrations, tests, or building admin UI with Filament Schemas, Tables, and Actions.

First seen Mar 19, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

LicenseMIT
More metadata
author
shopperlabs

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 10,156 B
  • docs SUMMARY.md 218 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Developing Shopper

Monorepo Structure

Package Namespace Purpose
packages/admin Shopper\ Livewire components, views, routes, assets
packages/core Shopper\Core\ Models, Actions, Enums, Contracts
packages/cart Shopper\Cart\ Cart management, pipeline-based calculation
packages/payment Shopper\Payment\ Payment processing, driver architecture
packages/shipping Shopper\Shipping\ Shipping providers, driver architecture
packages/sidebar Shopper\Sidebar\ Sidebar navigation builder
packages/stripe Shopper\Stripe\ Stripe payment driver
packages/types — TypeScript type definitions (NPM package)
packages/upgrade — Upgrade utilities

Required in Every PHP File

<?php

declare(strict_types=1);

Class Element Order

  1. Traits → 2. Cases → 3. Constants → 4. Properties → 5. Constructor → 6. Magic methods → 7. Methods (public → protected → private)

Models

See [MODELS.md](MODELS.md) for the swappable model pattern.

class Product extends Model implements ProductContract
{
    use HasFactory;
    use HasModelContract;
    use HasSlug;

    protected $guarded = [];

    public static function configuredClass(): string
    {
        return config('shopper.models.product', static::class);
    }

    public function getTable(): string
    {
        return shopper_table('products');
    }

    protected function casts(): array
    {
        return ['featured' => 'boolean'];
    }
}

Actions

final class CreateOrderAction
{
    public function execute(array $data): Order
    {
        // Single responsibility
    }
}

Enums

enum OrderStatus: string implements HasColor, HasIcon, HasLabel
{
    use ArrayableEnum;
    use HasEnumStaticMethods;

    case Pending = 'pending';

    public function getLabel(): string
    {
        return __('shopper-core::enum/order.pending');
    }
}

Livewire Components

class ProductForm extends Component implements HasActions, HasSchemas
{
    use InteractsWithActions;
    use InteractsWithSchemas;

    public ?array $data = [];

    #[Computed]
    public function categories(): Collection
    {
        return resolve(CategoryContract::class)::query()->get();
    }

    public function render(): View
    {
        return view('shopper::livewire.product-form');
    }
}

Migrations

return new class extends \Shopper\Core\Helpers\Migration
{
    public function up(): void
    {
        Schema::create($this->getTableName('products'), function (Blueprint $table): void {
            $this->addCommonFields($table, hasSoftDelete: true);
            $this->addSeoFields($table);
            $this->addShippingFields($table);
            $this->addForeignKey($table, 'brand_id', $this->getTableName('brands'));
        });
    }
};

Testing

beforeEach(function (): void {
    $this->user = User::factory()->create();
});

it('creates a product', function (): void {
    $this->actingAs($this->user);

    Livewire::test(ProductForm::class)
        ->set('data.name', 'Test')
        ->call('save')
        ->assertHasNoErrors();
})->group('products');

Commands

composer test:sqlite   # Run tests
composer test:types    # PHPStan
composer cs            # Rector + Pint + Prettier

Livewire Patterns

Component Types

Type Base Class Location
Page AbstractPageComponent Livewire/Pages/
SlideOver SlideOverComponent (Laravelcm\LivewireSlideOvers) Livewire/SlideOvers/
Component Component Livewire/Components/

Page with Table

class Index extends AbstractPageComponent implements HasActions, HasSchemas, HasTable
{
    use InteractsWithActions;
    use InteractsWithSchemas;
    use InteractsWithTable;

    public function mount(): void
    {
        $this->authorize('browse_brands');
    }

    public function table(Table $table): Table
    {
        return $table
            ->query(resolve(BrandContract::class)::query()->latest())
            ->columns([
                TextColumn::make('name')
                    ->label(__('shopper::forms.label.name'))
                    ->searchable()
                    ->sortable(),
            ])
            ->recordActions([
                Action::make('edit')
                    ->icon(Untitledui::Edit03)
                    ->iconButton()
                    ->action(fn ($record) => $this->dispatch(
                        'openPanel',
                        component: 'shopper-slide-overs.brand-form',
                        arguments: ['brand' => $record]
                    )),
            ]);
    }

    public function render(): View
    {
        return view('shopper::livewire.pages.brand.index')
            ->title(__('shopper::pages/brands.menu'));
    }
}

Blade View Structure

<x-shopper::container>
    {{-- Breadcrumb --}}
    <x-shopper::breadcrumb :back="route('shopper.settings.index')" :current="__('Shipping')">
        <x-untitledui-chevron-left class="size-4 shrink-0 text-gray-300 dark:text-gray-600" />
        <x-shopper::breadcrumb.link
            :link="route('shopper.settings.index')"
            :title="__('Settings')"
        />
    </x-shopper::breadcrumb>

    {{-- Page Heading --}}
    <x-shopper::heading class="my-6" :title="__('Shipping Methods')">
        <x-slot name="action">
            <x-filament::button wire:click="create">
                {{ __('Add Method') }}
            </x-filament::button>
        </x-slot>
    </x-shopper::heading>

    {{-- Content --}}
    <x-shopper::card class="mt-5">
        {{ $this->table }}
    </x-shopper::card>
</x-shopper::container>

Blade Components

Component Purpose
<x-shopper::container> Main content wrapper
<x-shopper::card> Card wrapper
<x-shopper::heading :title=""> Page title with optional action slot
<x-shopper::breadcrumb> Navigation breadcrumb
<x-shopper::separator> Section separator
<x-shopper::empty-card> Empty state
<x-filament::button> Primary button

SlideOver with Form

/**
 * @property-read Schema $form
 */
class BrandForm extends SlideOverComponent implements HasActions, HasSchemas, SlideOverForm
{
    use InteractsWithActions;
    use InteractsWithSchemas;

    public Brand $brand;
    public ?array $data = [];

    public function mount(?Brand $brand = null): void
    {
        $this->brand = $brand ?? resolve(Brand::class)::query()->newModelInstance();
        $this->form->fill($this->brand->toArray());
    }

    public function form(Schema $schema): Schema
    {
        return $schema
            ->components([
                Section::make(__('shopper::words.general'))
                    ->collapsible()
                    ->compact()
                    ->schema([
                        TextInput::make('name')
                            ->label(__('shopper::forms.label.name'))
                            ->required()
                            ->live(onBlur: true)
                            ->afterStateUpdated(fn ($state, Set $set) => $set('slug', Str::slug($state))),
                        Hidden::make('slug'),
                    ]),
            ])
            ->statePath('data')
            ->model($this->brand);
    }

    public function save(): void
    {
        if ($this->brand->id) {
            $this->authorize('edit_brands', $this->brand);
            $this->brand->update($this->form->getState());
        } else {
            $this->authorize('add_brands');
            $brand = resolve(Brand::class)::query()->create($this->form->getState());
            $this->form->model($brand)->saveRelationships();
        }

        Notification::make()
            ->title(__('shopper::notifications.save', ['item' => __('shopper::pages/brands.single')]))
            ->success()
            ->send();

        $this->redirectRoute('shopper.brands.index', navigate: true);
    }

    public function render(): View
    {
        return view('shopper::livewire.slide-overs.brand-form');
    }
}

SlideOver Operations

// Open
$this->dispatch(
    'openPanel',
    component: 'shopper-slide-overs.brand-form',
    arguments: ['brand' => $record]
);

// Close
$this->closePanel();

// Close with events
$this->closePanelWithEvents(['refresh']);

SlideOver Configuration

public static function panelMaxWidth(): string
{
    return '2xl'; // sm, md, lg, xl, 2xl, 3xl, 4xl, 5xl, 6xl, 7xl
}

public static function closePanelOnClickAway(): bool
{
    return false;
}

Computed Properties

#[Computed]
public function categories(): Collection
{
    return resolve(CategoryContract::class)::query()->get();
}

Authorization

// In mount
$this->authorize('browse_brands');

// In table actions
->visible(Shopper::auth()->user()->can('edit_brands'))