## Scaffolding

Use the Artisan command to generate a new widget:

```bash
php artisan layup:make-widget BannerWidget
```

This creates two files:

- `app/Layup/Widgets/BannerWidget.php` -- the widget class
- `resources/views/components/layup/banner.blade.php` -- the Blade view

Add `--with-test` to also generate a Pest test file:

```bash
php artisan layup:make-widget BannerWidget --with-test
```

> **Need server-side state?** Widgets can also render through Livewire components instead of Blade. See [Livewire-rendered widgets](livewire-widgets.md).

## Widget class structure

Every custom widget extends `BaseWidget` (or `BaseLivewireWidget` for Livewire-rendered output) and implements two required methods:

```php
<?php

declare(strict_types=1);

namespace App\Layup\Widgets;

use Crumbls\Layup\View\BaseWidget;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\TextInput;

class BannerWidget extends BaseWidget
{
    public static function getType(): string
    {
        return 'banner';
    }

    public static function getLabel(): string
    {
        return 'Banner';
    }

    public static function getContentFormSchema(): array
    {
        return [
            TextInput::make('title')
                ->label('Title')
                ->required(),
            RichEditor::make('body')
                ->label('Body'),
            TextInput::make('button_url')
                ->label('Button URL')
                ->url(),
        ];
    }

    public static function getDefaultData(): array
    {
        return [
            'title' => 'Welcome',
            'body' => '',
            'button_url' => '',
        ];
    }

    public static function getPreview(array $data): string
    {
        return $data['title'] ?? '(empty)';
    }
}
```

## Required methods

### `getType(): string`

A unique kebab-case identifier stored in JSON content. Must not collide with built-in types.

### `getLabel(): string`

The display name shown in the widget picker.

## Optional overrides

### `getContentFormSchema(): array`

Return an array of Filament form components for the Content tab. This is where your widget's fields live.

### `getDefaultData(): array`

Initial data for newly created instances of this widget.

### `getPreview(array $data): string`

Short text shown on the builder canvas. The default implementation shows the first 60 characters of `$data['content']`, falls back to `$data['label']`, then `$data['src']`.

### `getIcon(): string`

Heroicon name for the widget picker. Default: `heroicon-o-puzzle-piece`.

### `getCategory(): string`

Category for grouping in the picker. Use one of: `content`, `media`, `interactive`, `layout`, `advanced`.

### `getSearchTerms(): array`

Additional keywords for the widget picker search.

### `getValidationRules(): array`

Laravel validation rules for widget data:

```php
public static function getValidationRules(): array
{
    return [
        'title' => 'required|string|max:255',
        'button_url' => 'nullable|url',
    ];
}
```

### `prepareForRender(array $data): array`

Transform stored data before it reaches the Blade view. Use for computed values, URL resolution, or data formatting.

### `getAssets(): array`

Declare JS/CSS dependencies:

```php
public static function getAssets(): array
{
    return [
        'js' => ['https://cdn.example.com/lib.js'],
        'css' => ['https://cdn.example.com/lib.css'],
    ];
}
```

## Lifecycle hooks

Override these for side effects during the widget lifecycle:

```php
// Called when widget is first added to a column
public static function onCreate(array $data, ?WidgetContext $context = null): array
{
    return $data;
}

// Called when the editor slideover is saved
public static function onSave(array $data, ?WidgetContext $context = null): array
{
    return $data;
}

// Called when the widget is deleted
public static function onDelete(array $data, ?WidgetContext $context = null): void
{
    // Clean up uploaded files, etc.
}

// Called when the widget is duplicated
public static function onDuplicate(array $data, ?WidgetContext $context = null): array
{
    // Copy files so the duplicate has its own copy
    return $data;
}
```

The `WidgetContext` object provides:

```php
$context->page;      // ?Page model
$context->rowId;     // ?string
$context->columnId;  // ?string
$context->widgetId;  // ?string
```

## The Blade view

The view receives `$data` (the widget's data array) and `$children` (child components, if any):

```blade
@props(['data', 'children'])

<div>
    <h2>{{ $data['title'] ?? '' }}</h2>
    {!! $data['body'] ?? '' !!}

    @if(!empty($data['button_url']))
        <a href="{{ $data['button_url'] }}">Learn More</a>
    @endif
</div>
```

View naming convention: `resources/views/components/layup/{type}.blade.php` where `{type}` matches `getType()`.

## Registration

Widgets in the `App\Layup\Widgets` namespace are auto-discovered. No config changes needed.

To use a different namespace, update `config/layup.php`:

```php
'widget_discovery' => [
    'namespace' => 'App\\Custom\\Widgets',
    'directory' => app_path('Custom/Widgets'),
],
```

Or register explicitly in the `widgets` config array:

```php
'widgets' => [
    // ... built-in widgets
    \App\Layup\Widgets\BannerWidget::class,
],
```

Or via the plugin:

```php
LayupPlugin::make()
    ->widgets([
        \App\Layup\Widgets\BannerWidget::class,
    ]),
```

---

## Documentation

- [Introduction](https://documentation.crumbls.com/layup/-/index.md)
- [Introduction](https://documentation.crumbls.com/layup/-/introduction.md)
- [v1](https://documentation.crumbls.com/layup/-/_index.md)
- [Installation](https://documentation.crumbls.com/layup/-/installation.md)
- [Contributing to Layup](https://documentation.crumbls.com/layup/-/CONTRIBUTING.md)
- [Usage](https://documentation.crumbls.com/layup/-/usage.md)
- [Configuration](https://documentation.crumbls.com/layup/-/configuration.md)
- [Examples](https://documentation.crumbls.com/layup/-/examples.md)
- [Grid System](https://documentation.crumbls.com/layup/-/grid-system.md)
- [Field-only Installation](https://documentation.crumbls.com/layup/-/field-only-installation.md)
- [Embedding the Field](https://documentation.crumbls.com/layup/-/embedding-the-field.md)
- [Widgets](https://documentation.crumbls.com/layup/-/widgets/_index.md)
- [Getting Started](https://documentation.crumbls.com/layup/-/getting-started/_index.md)
- [Customization](https://documentation.crumbls.com/layup/-/customization/_index.md)
- [API Reference](https://documentation.crumbls.com/layup/-/api-reference/_index.md)
- [Upgrade guide](https://documentation.crumbls.com/layup/-/upgrade-guide.md)
- [Troubleshooting](https://documentation.crumbls.com/layup/-/troubleshooting.md)
- [Changelog](https://documentation.crumbls.com/layup/-/CHANGELOG.md)
- [🔐 Security Policy](https://documentation.crumbls.com/layup/-/SECURITY.md)
- [The MIT License (MIT)](https://documentation.crumbls.com/layup/-/LICENSE.md)
- [Layup Widget Development Guide for AI Agents](https://documentation.crumbls.com/layup/-/AGENTS.md)
- [Layup — Sprint TODO (overnight Feb 23-24)](https://documentation.crumbls.com/layup/-/TODO.md)
- [Vision](https://documentation.crumbls.com/layup/-/VISION.md)
- [Content Widgets](https://documentation.crumbls.com/layup/-/widgets/content.md)
- [Media Widgets](https://documentation.crumbls.com/layup/-/widgets/media.md)
- [Interactive Widgets](https://documentation.crumbls.com/layup/-/widgets/interactive.md)
- [Layout Widgets](https://documentation.crumbls.com/layup/-/widgets/layout.md)
- [Advanced Widgets](https://documentation.crumbls.com/layup/-/widgets/advanced.md)
- [Creating Pages](https://documentation.crumbls.com/layup/-/getting-started/creating-pages.md)
- [Adding Widgets](https://documentation.crumbls.com/layup/-/getting-started/adding-widgets.md)
- [Saving and Publishing](https://documentation.crumbls.com/layup/-/getting-started/publishing.md)
- [Disable the Pages Resource](https://documentation.crumbls.com/layup/-/customization/disable-pages-resource.md)
- [Rendering Content](https://documentation.crumbls.com/layup/-/customization/rendering-content.md)
- [Frontend Rendering](https://documentation.crumbls.com/layup/-/customization/frontend-rendering.md)
- [Extending Existing Widgets](https://documentation.crumbls.com/layup/-/customization/extending-widgets.md)
- [Livewire-rendered widgets](https://documentation.crumbls.com/layup/-/customization/livewire-widgets.md)
- [Filament Plugin API](https://documentation.crumbls.com/layup/-/customization/filament-plugin-api.md)
- [Theme System](https://documentation.crumbls.com/layup/-/customization/theme-system.md)
- [Tailwind Safelist](https://documentation.crumbls.com/layup/-/customization/tailwind-safelist.md)
- [SEO Meta](https://documentation.crumbls.com/layup/-/customization/seo-meta.md)
- [Page Templates](https://documentation.crumbls.com/layup/-/customization/page-templates.md)
- [Revision History](https://documentation.crumbls.com/layup/-/customization/revision-history.md)
- [Swapping the Page Model](https://documentation.crumbls.com/layup/-/customization/swapping-the-page-model.md)
- [Testing](https://documentation.crumbls.com/layup/-/customization/testing.md)
- [Widget Contract](https://documentation.crumbls.com/layup/-/api-reference/widget-contract.md)
- [Models](https://documentation.crumbls.com/layup/-/api-reference/models.md)
- [Service Provider](https://documentation.crumbls.com/layup/-/api-reference/service-provider.md)
- [Support Classes](https://documentation.crumbls.com/layup/-/api-reference/support-classes.md)
- [Commands](https://documentation.crumbls.com/layup/-/api-reference/commands.md)
