diff --git a/demo/.ai/guidelines b/demo/.ai/guidelines
new file mode 120000
index 000000000..45f9e054c
--- /dev/null
+++ b/demo/.ai/guidelines
@@ -0,0 +1 @@
+../../resources/boost/guidelines
\ No newline at end of file
diff --git a/demo/.gitignore b/demo/.gitignore
index acda100fc..6f85a790f 100644
--- a/demo/.gitignore
+++ b/demo/.gitignore
@@ -10,3 +10,17 @@ node_modules
.DS_Store
.phpunit.result.cache
.env
+
+# AI
+AGENTS.md
+boost.json
+.claude
+CLAUDE.md
+.cursor
+.gemini
+GEMINI.md
+.github
+.junie
+opencode.json
+.mcp.json
+.vscode
diff --git a/demo/composer.json b/demo/composer.json
index ef55e31f4..063132031 100644
--- a/demo/composer.json
+++ b/demo/composer.json
@@ -24,6 +24,7 @@
"require-dev": {
"fakerphp/faker": "^1.9.1",
"itsgoingd/clockwork": "^5.1",
+ "laravel/boost": "^1.8",
"laravel/sail": "^1.0.1",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^8.6",
@@ -52,7 +53,8 @@
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi",
- "@php artisan vendor:publish --tag=sharp-assets --force"
+ "@php artisan vendor:publish --tag=sharp-assets --force",
+ "@php artisan boost:update --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
diff --git a/demo/composer.lock b/demo/composer.lock
index 148d524d8..4578bbcfe 100644
--- a/demo/composer.lock
+++ b/demo/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "b1313d765c8b8fbe765f35c0e151e138",
+ "content-hash": "479e7666753599f88fecc36d866c8ae9",
"packages": [
{
"name": "bacon/bacon-qr-code",
@@ -7494,6 +7494,206 @@
],
"time": "2025-09-14T15:34:49+00:00"
},
+ {
+ "name": "laravel/boost",
+ "version": "v1.8.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/boost.git",
+ "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/boost/zipball/aad8b2a423b0a886c2ce7ee92abbfde69992ff32",
+ "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^7.9",
+ "illuminate/console": "^10.49.0|^11.45.3|^12.41.1",
+ "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1",
+ "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1",
+ "illuminate/support": "^10.49.0|^11.45.3|^12.41.1",
+ "laravel/mcp": "^0.5.1",
+ "laravel/prompts": "0.1.25|^0.3.6",
+ "laravel/roster": "^0.2.9",
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.20.0",
+ "mockery/mockery": "^1.6.12",
+ "orchestra/testbench": "^8.36.0|^9.15.0|^10.6",
+ "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5",
+ "phpstan/phpstan": "^2.1.27",
+ "rector/rector": "^2.1"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Boost\\BoostServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Boost\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.",
+ "homepage": "https://github.com/laravel/boost",
+ "keywords": [
+ "ai",
+ "dev",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/boost/issues",
+ "source": "https://github.com/laravel/boost"
+ },
+ "time": "2026-01-14T14:51:16+00:00"
+ },
+ {
+ "name": "laravel/mcp",
+ "version": "v0.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/mcp.git",
+ "reference": "39b9791b989927642137dd5b55dde0529f1614f9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/mcp/zipball/39b9791b989927642137dd5b55dde0529f1614f9",
+ "reference": "39b9791b989927642137dd5b55dde0529f1614f9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "illuminate/console": "^10.49.0|^11.45.3|^12.41.1",
+ "illuminate/container": "^10.49.0|^11.45.3|^12.41.1",
+ "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1",
+ "illuminate/http": "^10.49.0|^11.45.3|^12.41.1",
+ "illuminate/json-schema": "^12.41.1",
+ "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1",
+ "illuminate/support": "^10.49.0|^11.45.3|^12.41.1",
+ "illuminate/validation": "^10.49.0|^11.45.3|^12.41.1",
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.20",
+ "orchestra/testbench": "^8.36|^9.15|^10.8",
+ "pestphp/pest": "^2.36.0|^3.8.4|^4.1.0",
+ "phpstan/phpstan": "^2.1.27",
+ "rector/rector": "^2.2.4"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp"
+ },
+ "providers": [
+ "Laravel\\Mcp\\Server\\McpServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Mcp\\": "src/",
+ "Laravel\\Mcp\\Server\\": "src/Server/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "Rapidly build MCP servers for your Laravel applications.",
+ "homepage": "https://github.com/laravel/mcp",
+ "keywords": [
+ "laravel",
+ "mcp"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/mcp/issues",
+ "source": "https://github.com/laravel/mcp"
+ },
+ "time": "2026-01-26T10:25:21+00:00"
+ },
+ {
+ "name": "laravel/roster",
+ "version": "v0.2.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/roster.git",
+ "reference": "82bbd0e2de614906811aebdf16b4305956816fa6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/roster/zipball/82bbd0e2de614906811aebdf16b4305956816fa6",
+ "reference": "82bbd0e2de614906811aebdf16b4305956816fa6",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/console": "^10.0|^11.0|^12.0",
+ "illuminate/contracts": "^10.0|^11.0|^12.0",
+ "illuminate/routing": "^10.0|^11.0|^12.0",
+ "illuminate/support": "^10.0|^11.0|^12.0",
+ "php": "^8.1|^8.2",
+ "symfony/yaml": "^6.4|^7.2"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.14",
+ "mockery/mockery": "^1.6",
+ "orchestra/testbench": "^8.22.0|^9.0|^10.0",
+ "pestphp/pest": "^2.0|^3.0",
+ "phpstan/phpstan": "^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Roster\\RosterServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Roster\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Detect packages & approaches in use within a Laravel project",
+ "homepage": "https://github.com/laravel/roster",
+ "keywords": [
+ "dev",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/roster/issues",
+ "source": "https://github.com/laravel/roster"
+ },
+ "time": "2025-10-20T09:56:46+00:00"
+ },
{
"name": "laravel/sail",
"version": "v1.51.0",
diff --git a/resources/boost/guidelines/blueprint.blade.php b/resources/boost/guidelines/blueprint.blade.php
new file mode 100644
index 000000000..e79cc0a43
--- /dev/null
+++ b/resources/boost/guidelines/blueprint.blade.php
@@ -0,0 +1,864 @@
+# Sharp Blueprint
+
+A Sharp Blueprint is a detailed implementation plan that helps AI agents write correct Sharp code. It bridges the gap between requirements and implementation by providing a structured specification for Sharp's unique patterns.
+
+## Core Philosophy
+
+Sharp separates the data structure from its visual representation. A blueprint should clearly map domain models to Sharp components:
+- **Entity Lists**: The primary data table view
+- **Forms**: Data entry and validation
+- **Show Pages**: Detailed read-only views
+- **Commands**: Business logic triggers (instance, entity, wizard)
+- **State Handlers**: Lifecycle management
+- **Filters**: Data filtering for entity lists
+
+## Blueprint Structure
+
+When asking an AI to create a blueprint, it should produce:
+1. **Overview**: Brief description of the system and key decisions
+2. **User Flows**: Step-by-step navigation through features
+3. **Commands**: Required Artisan commands for scaffolding
+4. **Models**: Detailed attributes, relationships, casts, and methods
+5. **Sharp Resources**: Specific configurations for Lists, Forms, Shows, Commands, and State Handlers
+
+---
+
+# SaaS Invoicing System - Sharp Implementation Plan
+
+## Overview
+
+A single-tenant admin panel for managing customers, products, invoices with line items, and tracking payments. All authenticated users can access all data.
+
+**Key decisions:**
+- Single-tenant (all authenticated users see all data)
+- Manual invoice sending (mark as sent, no email automation)
+- Partial payments supported (multiple payments until balance = 0)
+- In-app Sharp notifications only
+
+---
+
+## User Flows
+
+### Flow 1: Creating an Invoice
+1. User navigates to Invoices → Create
+2. User selects a Customer (autocomplete field with search)
+3. User sets invoice date and due date
+4. User adds line items via List field:
+ - Select Product (autocomplete, shows name + price)
+ - Enter quantity
+ - Line total auto-calculates (quantity × unit_price)
+5. Tax rate is entered (percentage)
+6. Subtotal, tax amount, and total auto-calculate
+7. Invoice saves as Draft status with auto-generated invoice number
+
+### Flow 2: Sending an Invoice
+1. User views a Draft invoice in Show Page
+2. User clicks "Mark as Sent" (Instance Command)
+3. Confirmation modal appears
+4. On confirm: status → Sent, sent_at → now()
+5. Success notification shown
+
+### Flow 3: Recording a Payment
+1. User views a Sent invoice (or Overdue)
+2. User clicks "Record Payment" (Instance Command)
+3. Modal form appears with:
+ - Amount (defaults to balance due, validates ≤ balance)
+ - Payment method (select)
+ - Reference (optional)
+ - Payment date
+4. On submit: Payment record created
+5. If total payments ≥ invoice total: status → Paid, paid_at → now()
+6. Success notification shown
+
+### Flow 4: Viewing Invoice Status
+1. User navigates to Invoices list
+2. Table shows status badges (Draft/Sent/Paid/Overdue/Cancelled)
+3. User can filter by status, customer, date range
+4. User can search by invoice number or customer name
+
+---
+
+## Commands
+
+Run these in order:
+
+```bash
+# 1. Create models with migrations and factories
+php artisan make:model Customer -mf --no-interaction
+php artisan make:model Product -mf --no-interaction
+php artisan make:model Invoice -mf --no-interaction
+php artisan make:model InvoiceItem -mf --no-interaction
+php artisan make:model Payment -mf --no-interaction
+
+# 2. Create enums
+php artisan make:enum InvoiceStatus
+php artisan make:enum PaymentMethod
+
+# 3. Create Sharp resources
+php artisan sharp:make:entity-list InvoiceList --model=Invoice
+php artisan sharp:make:form InvoiceForm --model=Invoice
+php artisan sharp:make:show InvoiceShow --model=Invoice
+php artisan sharp:make:entity-list CustomerList --model=Customer
+php artisan sharp:make:form CustomerForm --model=Customer
+php artisan sharp:make:show CustomerShow --model=Customer
+php artisan sharp:make:entity-list ProductList --model=Product
+php artisan sharp:make:form ProductForm --model=Product
+
+# 4. Create state handler
+php artisan sharp:make:state-handler InvoiceStateHandler
+
+# 5. Create commands
+php artisan sharp:make:instance-command SendInvoiceCommand
+php artisan sharp:make:instance-command RecordPaymentCommand
+
+# 6. Create filters
+php artisan sharp:make:entity-list-filter InvoiceStatusFilter
+php artisan sharp:make:entity-list-filter CustomerFilter
+php artisan sharp:make:entity-list-filter InvoiceDateRangeFilter
+```
+
+---
+
+## Models
+
+### Enum: InvoiceStatus
+
+```
+Enum: InvoiceStatus
+ Location: App\Enums\InvoiceStatus
+ Type: string backed enum
+ Cases:
+ - Draft = 'draft'
+ - Sent = 'sent'
+ - Paid = 'paid'
+ - Overdue = 'overdue'
+ - Cancelled = 'cancelled'
+ Methods:
+ - label(): string (Draft, Sent, Paid, Overdue, Cancelled)
+ - color(): string (gray, blue, green, red, orange)
+```
+
+### Enum: PaymentMethod
+
+```
+Enum: PaymentMethod
+ Location: App\Enums\PaymentMethod
+ Type: string backed enum
+ Cases:
+ - Cash = 'cash'
+ - Check = 'check'
+ - BankTransfer = 'bank_transfer'
+ - CreditCard = 'credit_card'
+ - Other = 'other'
+ Methods:
+ - label(): string
+```
+
+### Model: Customer
+
+```
+Model: Customer
+ Table: customers
+ Attributes:
+ - id: bigint, primary
+ - name: string, required
+ - email: string, required
+ - phone: string, nullable
+ - address_line_1: string, nullable
+ - address_line_2: string, nullable
+ - city: string, nullable
+ - state: string, nullable
+ - postal_code: string, nullable
+ - country: string, nullable
+ - notes: text, nullable
+ - created_at: timestamp
+ - updated_at: timestamp
+ - deleted_at: timestamp, nullable
+ Relationships:
+ - hasMany: Invoice via customer_id
+ Traits:
+ - SoftDeletes
+```
+
+### Model: Product
+
+```
+Model: Product
+ Table: products
+ Attributes:
+ - id: bigint, primary
+ - name: string, required
+ - sku: string, nullable, unique
+ - description: text, nullable
+ - unit_price: integer, required (stored in cents)
+ - is_active: boolean, default:true
+ - created_at: timestamp
+ - updated_at: timestamp
+ - deleted_at: timestamp, nullable
+ Relationships:
+ - hasMany: InvoiceItem via product_id
+ Traits:
+ - SoftDeletes
+ Casts:
+ - unit_price: integer
+ - is_active: boolean
+```
+
+### Model: Invoice
+
+```
+Model: Invoice
+ Table: invoices
+ Attributes:
+ - id: bigint, primary
+ - customer_id: bigint, foreign(customers.id), required
+ - invoice_number: string, required, unique
+ - status: string, default:'draft' (uses InvoiceStatus enum)
+ - invoice_date: date, required
+ - due_date: date, required
+ - subtotal: integer, default:0 (cents)
+ - tax_rate: decimal(5,2), default:0
+ - tax_amount: integer, default:0 (cents)
+ - total: integer, default:0 (cents)
+ - amount_paid: integer, default:0 (cents)
+ - notes: text, nullable
+ - sent_at: timestamp, nullable
+ - paid_at: timestamp, nullable
+ - created_at: timestamp
+ - updated_at: timestamp
+ - deleted_at: timestamp, nullable
+ Relationships:
+ - belongsTo: Customer via customer_id
+ - hasMany: InvoiceItem via invoice_id
+ - hasMany: Payment via invoice_id
+ Traits:
+ - SoftDeletes
+ Casts:
+ - status: InvoiceStatus::class
+ - invoice_date: date
+ - due_date: date
+ - tax_rate: decimal:2
+ - sent_at: datetime
+ - paid_at: datetime
+ Accessors:
+ - balance_due: int (total - amount_paid)
+ Methods:
+ - generateInvoiceNumber(): string (format: INV-YYYYMM-XXXX)
+ - recalculateTotals(): void (sum line items, apply tax)
+ - markAsSent(): void
+ - markAsPaid(): void
+ - recordPayment(int $amount, PaymentMethod $method, ?string $reference, Carbon $date): Payment
+```
+
+### Model: InvoiceItem
+
+```
+Model: InvoiceItem
+ Table: invoice_items
+ Attributes:
+ - id: bigint, primary
+ - invoice_id: bigint, foreign(invoices.id), required, onDelete:cascade
+ - product_id: bigint, foreign(products.id), nullable
+ - description: string, required
+ - quantity: integer, required, default:1
+ - unit_price: integer, required (cents)
+ - total: integer, required (cents, quantity × unit_price)
+ - sort_order: integer, default:0
+ - created_at: timestamp
+ - updated_at: timestamp
+ Relationships:
+ - belongsTo: Invoice via invoice_id
+ - belongsTo: Product via product_id (nullable)
+ Casts:
+ - quantity: integer
+ - unit_price: integer
+ - total: integer
+ - sort_order: integer
+```
+
+### Model: Payment
+
+```
+Model: Payment
+ Table: payments
+ Attributes:
+ - id: bigint, primary
+ - invoice_id: bigint, foreign(invoices.id), required, onDelete:cascade
+ - amount: integer, required (cents)
+ - method: string, required (uses PaymentMethod enum)
+ - reference: string, nullable
+ - payment_date: date, required
+ - notes: text, nullable
+ - created_at: timestamp
+ - updated_at: timestamp
+ Relationships:
+ - belongsTo: Invoice via invoice_id
+ Casts:
+ - method: PaymentMethod::class
+ - amount: integer
+ - payment_date: date
+```
+
+---
+
+## Sharp Resources
+
+### CustomerList
+
+@verbatim
+```
+Resource: CustomerList
+ Location: App\Sharp\Customers\CustomerList
+ Extends: Code16\Sharp\EntityList\SharpEntityList
+ Docs: https://sharp.code16.fr/docs/guide/building-entity-list
+
+ Method: buildList(EntityListFieldsContainer $fields): void
+ Fields:
+ - EntityListField::make('name')
+ ->setLabel('Name')
+ ->setSortable()
+
+ - EntityListField::make('email')
+ ->setLabel('Email')
+ ->setSortable()
+
+ - EntityListField::make('phone')
+ ->setLabel('Phone')
+ ->hideOnSmallScreens()
+
+ - EntityListField::make('city')
+ ->setLabel('City')
+ ->hideOnSmallScreens()
+
+ - EntityListField::make('invoices_count')
+ ->setLabel('Invoices')
+ ->setSortable()
+
+ Method: buildListConfig(): void
+ Config:
+ - configureSearchable()
+ - configureDefaultSort('name', 'asc')
+ - configureReorderable(false)
+ - configurePaginated()
+
+ Method: getListData(EntityListQueryParams $params): array
+ - Query: Customer::query()->withCount('invoices')
+ - Search: name, email, phone
+ - Transform: id, name, email, phone, city, invoices_count
+```
+@endverbatim
+
+### CustomerForm
+
+@verbatim
+```
+Resource: CustomerForm
+ Location: App\Sharp\Customers\CustomerForm
+ Extends: Code16\Sharp\Form\SharpForm
+ Docs: https://sharp.code16.fr/docs/guide/building-form
+
+ Method: buildFormFields(FieldsContainer $formFields): void
+ Fields:
+ - SharpFormTextField::make('name')
+ ->setLabel('Name')
+ ->setMaxLength(255)
+
+ - SharpFormTextField::make('email')
+ ->setLabel('Email')
+ ->setMaxLength(255)
+
+ - SharpFormTextField::make('phone')
+ ->setLabel('Phone')
+ ->setMaxLength(50)
+
+ - SharpFormTextField::make('address_line_1')
+ ->setLabel('Address Line 1')
+ ->setMaxLength(255)
+
+ - SharpFormTextField::make('address_line_2')
+ ->setLabel('Address Line 2')
+ ->setMaxLength(255)
+
+ - SharpFormTextField::make('city')
+ ->setLabel('City')
+ ->setMaxLength(100)
+
+ - SharpFormTextField::make('state')
+ ->setLabel('State')
+ ->setMaxLength(100)
+
+ - SharpFormTextField::make('postal_code')
+ ->setLabel('Postal Code')
+ ->setMaxLength(20)
+
+ - SharpFormTextField::make('country')
+ ->setLabel('Country')
+ ->setMaxLength(100)
+
+ - SharpFormTextareaField::make('notes')
+ ->setLabel('Notes')
+ ->setRowCount(4)
+
+ Method: buildFormLayout(FormLayout $formLayout): void
+ Layout:
+ - Column 8:
+ - Fieldset "Contact Information":
+ - name (full width)
+ - Row: email, phone
+ - Fieldset "Address":
+ - address_line_1 (full width)
+ - address_line_2 (full width)
+ - Row: city, state, postal_code
+ - country (full width)
+ - Column 4:
+ - Fieldset "Additional Information":
+ - notes (full width)
+
+ Method: create(): array
+ - Return: empty customer data array
+
+ Method: update(mixed $id, array $data): bool
+ - Find/create Customer model
+ - Save attributes
+ - Return true
+
+ Method: find(mixed $id): array
+ - Find Customer by id
+ - Transform to array
+```
+@endverbatim
+
+### InvoiceList
+
+@verbatim
+```
+Resource: InvoiceList
+ Location: App\Sharp\Invoices\InvoiceList
+ Extends: Code16\Sharp\EntityList\SharpEntityList
+ Docs: https://sharp.code16.fr/docs/guide/building-entity-list
+
+ Method: buildList(EntityListFieldsContainer $fields): void
+ Fields:
+ - EntityListField::make('invoice_number')
+ ->setLabel('Number')
+ ->setSortable()
+
+ - EntityListField::make('customer:name')
+ ->setLabel('Customer')
+ ->setSortable()
+
+ - EntityListField::make('invoice_date')
+ ->setLabel('Date')
+ ->setSortable()
+
+ - EntityListField::make('due_date')
+ ->setLabel('Due Date')
+ ->setSortable()
+ ->hideOnSmallScreens()
+
+ - EntityListField::make('total')
+ ->setLabel('Total')
+ ->setSortable()
+
+ - EntityListStateField::make()
+ ->setLabel('Status')
+
+ Method: buildListConfig(): void
+ Config:
+ - configureSearchable()
+ - configureDefaultSort('invoice_date', 'desc')
+ - configureEntityState('status', InvoiceStateHandler::class)
+ - configurePaginated()
+
+ Filters:
+ - InvoiceStatusFilter
+ - CustomerFilter
+ - InvoiceDateRangeFilter
+
+ Method: getListData(EntityListQueryParams $params): array
+ - Query: Invoice::with('customer')
+ - Search: invoice_number, customer.name
+ - Filters: status, customer_id, date range
+ - Transform: id, invoice_number, customer:name, invoice_date, due_date, total (formatted), status
+```
+@endverbatim
+
+### InvoiceForm
+
+@verbatim
+```
+Resource: InvoiceForm
+ Location: App\Sharp\Invoices\InvoiceForm
+ Extends: Code16\Sharp\Form\SharpForm
+ Docs: https://sharp.code16.fr/docs/guide/building-form
+
+ Method: buildFormFields(FieldsContainer $formFields): void
+ Fields:
+ - SharpFormAutocompleteField::make('customer_id', 'remote')
+ ->setLabel('Customer')
+ ->setRemoteEndpoint('/sharp/api/autocomplete/customers')
+ ->setResultItemInlineTemplate('{{ $name }} - {{ $email }}')
+ ->setListItemInlineTemplate('{{ $name }}')
+
+ - SharpFormTextField::make('invoice_number')
+ ->setLabel('Invoice Number')
+ ->setReadOnly()
+ ->setMaxLength(50)
+
+ - SharpFormDateField::make('invoice_date')
+ ->setLabel('Invoice Date')
+
+ - SharpFormDateField::make('due_date')
+ ->setLabel('Due Date')
+
+ - SharpFormListField::make('items')
+ ->setLabel('Line Items')
+ ->setAddable()
+ ->setRemovable()
+ ->setSortable()
+ ->setOrderAttribute('sort_order')
+ ->addItemField(
+ SharpFormAutocompleteField::make('product_id', 'remote')
+ ->setLabel('Product')
+ ->setRemoteEndpoint('/sharp/api/autocomplete/products')
+ ->setResultItemInlineTemplate('{{ $name }} - {{ $unit_price }}')
+ )
+ ->addItemField(
+ SharpFormTextField::make('description')
+ ->setLabel('Description')
+ ->setMaxLength(255)
+ )
+ ->addItemField(
+ SharpFormTextField::make('quantity')
+ ->setLabel('Quantity')
+ )
+ ->addItemField(
+ SharpFormTextField::make('unit_price')
+ ->setLabel('Unit Price')
+ )
+ ->addItemField(
+ SharpFormTextField::make('total')
+ ->setLabel('Total')
+ ->setReadOnly()
+ )
+
+ - SharpFormTextField::make('tax_rate')
+ ->setLabel('Tax Rate (%)')
+ ->setInputTypeNumber()
+ ->setStep(0.01)
+
+ - SharpFormTextField::make('subtotal')
+ ->setLabel('Subtotal')
+ ->setReadOnly()
+
+ - SharpFormTextField::make('tax_amount')
+ ->setLabel('Tax Amount')
+ ->setReadOnly()
+
+ - SharpFormTextField::make('total')
+ ->setLabel('Total')
+ ->setReadOnly()
+
+ - SharpFormTextareaField::make('notes')
+ ->setLabel('Notes')
+ ->setRowCount(4)
+
+ Method: buildFormLayout(FormLayout $formLayout): void
+ Layout:
+ - Column 8:
+ - Fieldset "General":
+ - customer_id (full width)
+ - invoice_number (full width)
+ - Row: invoice_date, due_date
+ - Fieldset "Line Items":
+ - items (full width)
+ - Fieldset "Notes":
+ - notes (full width)
+ - Column 4:
+ - Fieldset "Totals":
+ - tax_rate (full width)
+ - subtotal (full width)
+ - tax_amount (full width)
+ - total (full width)
+
+ Method: create(): array
+ - Generate invoice number
+ - Set default dates
+ - Return empty invoice data
+
+ Method: update(mixed $id, array $data): bool
+ - Find/create Invoice model
+ - Save attributes and relationships
+ - Recalculate totals
+ - Return true
+
+ Method: find(mixed $id): array
+ - Find Invoice with items
+ - Transform to array with calculated totals
+```
+@endverbatim
+
+### InvoiceShow
+
+@verbatim
+```
+Resource: InvoiceShow
+ Location: App\Sharp\Invoices\InvoiceShow
+ Extends: Code16\Sharp\Show\SharpShow
+ Docs: https://sharp.code16.fr/docs/guide/building-show-page
+
+ Method: buildShowFields(FieldsContainer $showFields): void
+ Fields:
+ - SharpShowTextField::make('invoice_number')
+ ->setLabel('Invoice Number')
+
+ - SharpShowTextField::make('status')
+ ->setLabel('Status')
+
+ - SharpShowTextField::make('customer')
+ ->setLabel('Customer')
+
+ - SharpShowTextField::make('invoice_date')
+ ->setLabel('Invoice Date')
+
+ - SharpShowTextField::make('due_date')
+ ->setLabel('Due Date')
+
+ - SharpShowListField::make('items')
+ ->setLabel('Line Items')
+ ->addItemField(SharpShowTextField::make('description')->setLabel('Description'))
+ ->addItemField(SharpShowTextField::make('quantity')->setLabel('Quantity'))
+ ->addItemField(SharpShowTextField::make('unit_price')->setLabel('Unit Price'))
+ ->addItemField(SharpShowTextField::make('total')->setLabel('Total'))
+
+ - SharpShowTextField::make('subtotal')
+ ->setLabel('Subtotal')
+
+ - SharpShowTextField::make('tax_rate')
+ ->setLabel('Tax Rate')
+
+ - SharpShowTextField::make('tax_amount')
+ ->setLabel('Tax Amount')
+
+ - SharpShowTextField::make('total')
+ ->setLabel('Total')
+
+ - SharpShowTextField::make('amount_paid')
+ ->setLabel('Amount Paid')
+
+ - SharpShowTextField::make('balance_due')
+ ->setLabel('Balance Due')
+
+ - SharpShowEntityListField::make('payments', PaymentEntity::class)
+ ->setLabel('Payments')
+ ->hideFilterWithValue('invoice', fn($instanceId) => $instanceId)
+
+ - SharpShowTextField::make('notes')
+ ->setLabel('Notes')
+
+ Method: buildShowLayout(ShowLayout $showLayout): void
+ Layout:
+ - Section "Invoice Details":
+ - Column 8:
+ - invoice_number, status, customer
+ - invoice_date, due_date
+ - Column 4: (empty for spacing)
+ - Section "Line Items":
+ - Column 12:
+ - items (full width)
+ - Section "Totals":
+ - Column 8: (empty)
+ - Column 4:
+ - subtotal, tax_rate, tax_amount, total
+ - amount_paid, balance_due
+ - Section "Payments":
+ - Column 12:
+ - payments (full width)
+ - Section "Additional Information":
+ - Column 12:
+ - notes (full width)
+
+ Method: find(mixed $id): array
+ - Find Invoice with customer, items, payments
+ - Transform to array with formatted values
+
+ Commands:
+ - SendInvoiceCommand (instance)
+ - RecordPaymentCommand (instance)
+```
+@endverbatim
+
+### InvoiceStateHandler
+
+@verbatim
+
+namespace App\Sharp\Invoices;
+
+use App\Enums\InvoiceStatus;
+use Code16\Sharp\EntityList\Commands\EntityState;
+
+class InvoiceStateHandler extends EntityState
+{
+ protected function buildStates(): void
+ {
+ $this
+ ->addState(InvoiceStatus::Draft->value, InvoiceStatus::Draft->label(), InvoiceStatus::Draft->color())
+ ->addState(InvoiceStatus::Sent->value, InvoiceStatus::Sent->label(), InvoiceStatus::Sent->color())
+ ->addState(InvoiceStatus::Paid->value, InvoiceStatus::Paid->label(), InvoiceStatus::Paid->color())
+ ->addState(InvoiceStatus::Overdue->value, InvoiceStatus::Overdue->label(), InvoiceStatus::Overdue->color())
+ ->addState(InvoiceStatus::Cancelled->value, InvoiceStatus::Cancelled->label(), InvoiceStatus::Cancelled->color());
+ }
+
+ protected function updateState(mixed $instanceId, string $stateId): array
+ {
+ $invoice = \App\Models\Invoice::findOrFail($instanceId);
+
+ $invoice->update([
+ 'status' => InvoiceStatus::from($stateId),
+ ]);
+
+ if ($stateId === InvoiceStatus::Sent->value && !$invoice->sent_at) {
+ $invoice->update(['sent_at' => now()]);
+ }
+
+ if ($stateId === InvoiceStatus::Paid->value && !$invoice->paid_at) {
+ $invoice->update(['paid_at' => now()]);
+ }
+
+ return $this->reload();
+ }
+}
+
+@endverbatim
+
+### SendInvoiceCommand
+
+@verbatim
+
+namespace App\Sharp\Invoices\Commands;
+
+use App\Enums\InvoiceStatus;
+use App\Models\Invoice;
+use Code16\Sharp\EntityList\Commands\InstanceCommand;
+
+class SendInvoiceCommand extends InstanceCommand
+{
+ public function label(): string
+ {
+ return 'Mark as Sent';
+ }
+
+ public function execute(mixed $instanceId, array $data = []): array
+ {
+ $invoice = Invoice::findOrFail($instanceId);
+
+ if ($invoice->status !== InvoiceStatus::Draft) {
+ return $this->error('Only draft invoices can be sent.');
+ }
+
+ $invoice->markAsSent();
+
+ return $this->reload();
+ }
+
+ public function authorizeFor(mixed $instanceId): bool
+ {
+ $invoice = Invoice::find($instanceId);
+
+ return $invoice && $invoice->status === InvoiceStatus::Draft;
+ }
+}
+
+@endverbatim
+
+### RecordPaymentCommand
+
+@verbatim
+
+namespace App\Sharp\Invoices\Commands;
+
+use App\Enums\PaymentMethod;
+use App\Models\Invoice;
+use Code16\Sharp\EntityList\Commands\InstanceCommand;
+use Code16\Sharp\Form\Fields\SharpFormDateField;
+use Code16\Sharp\Form\Fields\SharpFormSelectField;
+use Code16\Sharp\Form\Fields\SharpFormTextareaField;
+use Code16\Sharp\Form\Fields\SharpFormTextField;
+use Code16\Sharp\Utils\Fields\FieldsContainer;
+
+class RecordPaymentCommand extends InstanceCommand
+{
+ public function label(): string
+ {
+ return 'Record Payment';
+ }
+
+ public function buildFormFields(FieldsContainer $formFields): void
+ {
+ $formFields
+ ->addField(
+ SharpFormTextField::make('amount')
+ ->setLabel('Amount')
+ )
+ ->addField(
+ SharpFormSelectField::make('method', collect(PaymentMethod::cases())->map(fn($case) => [
+ 'id' => $case->value,
+ 'label' => $case->label(),
+ ])->all())
+ ->setLabel('Payment Method')
+ )
+ ->addField(
+ SharpFormTextField::make('reference')
+ ->setLabel('Reference')
+ ->setMaxLength(255)
+ )
+ ->addField(
+ SharpFormDateField::make('payment_date')
+ ->setLabel('Payment Date')
+ )
+ ->addField(
+ SharpFormTextareaField::make('notes')
+ ->setLabel('Notes')
+ ->setRowCount(3)
+ );
+ }
+
+ public function initialData(mixed $instanceId): array
+ {
+ $invoice = Invoice::findOrFail($instanceId);
+
+ return [
+ 'amount' => $invoice->balance_due / 100,
+ 'payment_date' => now()->format('Y-m-d'),
+ ];
+ }
+
+ public function execute(mixed $instanceId, array $data = []): array
+ {
+ $invoice = Invoice::findOrFail($instanceId);
+
+ $amountInCents = (int) ($data['amount'] * 100);
+
+ if ($amountInCents > $invoice->balance_due) {
+ return $this->error('Payment amount cannot exceed balance due.');
+ }
+
+ $invoice->recordPayment(
+ $amountInCents,
+ PaymentMethod::from($data['method']),
+ $data['reference'] ?? null,
+ \Carbon\Carbon::parse($data['payment_date'])
+ );
+
+ return $this->reload();
+ }
+
+ public function authorizeFor(mixed $instanceId): bool
+ {
+ $invoice = Invoice::find($instanceId);
+
+ return $invoice && $invoice->balance_due > 0;
+ }
+}
+
+@endverbatim
diff --git a/resources/boost/guidelines/core.blade.php b/resources/boost/guidelines/core.blade.php
new file mode 100644
index 000000000..9dec5e723
--- /dev/null
+++ b/resources/boost/guidelines/core.blade.php
@@ -0,0 +1,235 @@
+## Sharp
+- Sharp is used by this application. Follow existing conventions for how and where it's implemented.
+- Sharp is a content management framework for Laravel that allows you to define user interfaces in PHP using structured configuration objects.
+- Sharp allows you to build Entity Lists, Forms, Show Pages, and Dashboards.
+
+### Patterns
+Use `make()` static methods to initialize fields, columns, and other components.
+
+#### Entity Lists
+Entity Lists are used to display a list of records.
+@verbatim
+
+
+use Code16\Sharp\EntityList\Fields\EntityListField;
+use Code16\Sharp\EntityList\Fields\EntityListFieldsContainer;
+use Code16\Sharp\EntityList\SharpEntityList;
+use Illuminate\Contracts\Support\Arrayable;
+
+class UserList extends SharpEntityList
+{
+ protected function buildList(EntityListFieldsContainer $fields): void
+ {
+ $fields
+ ->addField(
+ EntityListField::make('name')
+ ->setLabel('Name')
+ ->setSortable()
+ )
+ ->addField(
+ EntityListField::make('email')
+ ->setLabel('Email')
+ );
+ }
+
+ public function buildListConfig(): void
+ {
+ $this
+ ->configureSearchable()
+ ->configureDefaultSort('name', 'asc');
+ }
+
+ public function getListData(): array|Arrayable
+ {
+ $users = User::query()
+ ->when($this->queryParams->hasSearch(), function ($query) {
+ foreach ($this->queryParams->searchWords() as $word) {
+ $query->where('name', 'like', $word);
+ }
+ })
+ ->orderBy(
+ $this->queryParams->sortedBy() ?: 'name',
+ $this->queryParams->sortedDir() ?: 'asc'
+ );
+
+ return $this->transform($users->paginate(30));
+ }
+}
+
+@endverbatim
+
+#### Forms
+Forms are used to create or edit records.
+@verbatim
+
+use Code16\Sharp\Form\Fields\SharpFormTextField;
+use Code16\Sharp\Form\Fields\SharpFormTextareaField;
+use Code16\Sharp\Form\Layout\FormLayout;
+use Code16\Sharp\Form\Layout\FormLayoutColumn;
+use Code16\Sharp\Form\SharpForm;
+use Code16\Sharp\Utils\Fields\FieldsContainer;
+
+class UserForm extends SharpForm
+{
+ public function buildFormFields(FieldsContainer $formFields): void
+ {
+ $formFields
+ ->addField(
+ SharpFormTextField::make('name')
+ ->setLabel('Name')
+ ->setMaxLength(150)
+ )
+ ->addField(
+ SharpFormTextareaField::make('bio')
+ ->setLabel('Biography')
+ ->setMaxLength(500)
+ );
+ }
+
+ public function buildFormLayout(FormLayout $formLayout): void
+ {
+ $formLayout
+ ->addColumn(6, fn (FormLayoutColumn $column) => $column
+ ->withField('name')
+ ->withField('bio')
+ );
+ }
+
+ public function find($id): array
+ {
+ return $this->transform(User::findOrFail($id));
+ }
+
+ public function update($id, array $data)
+ {
+ $this->validate($data, [
+ 'name' => ['required', 'string', 'max:150'],
+ ]);
+
+ $user = $id ? User::findOrFail($id) : new User();
+ $user->fill($data);
+ $user->save();
+
+ return $user->id;
+ }
+}
+
+@endverbatim
+
+#### Show Pages
+Show Pages are used to display details of a single record.
+@verbatim
+
+use Code16\Sharp\Show\Fields\SharpShowTextField;
+use Code16\Sharp\Show\Layout\ShowLayout;
+use Code16\Sharp\Show\Layout\ShowLayoutColumn;
+use Code16\Sharp\Show\Layout\ShowLayoutSection;
+use Code16\Sharp\Show\SharpShow;
+use Code16\Sharp\Utils\Fields\FieldsContainer;
+
+class UserShow extends SharpShow
+{
+ protected function buildShowFields(FieldsContainer $showFields): void
+ {
+ $showFields
+ ->addField(
+ SharpShowTextField::make('name')
+ ->setLabel('Name')
+ )
+ ->addField(
+ SharpShowTextField::make('email')
+ ->setLabel('Email')
+ )
+ ->addField(
+ SharpShowTextField::make('bio')
+ ->setLabel('Biography')
+ );
+ }
+
+ protected function buildShowLayout(ShowLayout $showLayout): void
+ {
+ $showLayout
+ ->addSection(fn (ShowLayoutSection $section) => $section
+ ->addColumn(6, fn (ShowLayoutColumn $column) => $column
+ ->withField('name')
+ ->withField('email')
+ ->withField('bio')
+ )
+ );
+ }
+
+ public function find(mixed $id): array
+ {
+ return $this->transform(User::findOrFail($id));
+ }
+
+ public function delete($id): void
+ {
+ User::findOrFail($id)->delete();
+ }
+}
+
+@endverbatim
+
+### Transformers
+Sharp uses Transformers to map your model data to the format expected by the UI. Use `setCustomTransformer()` to transform field values before displaying them.
+@verbatim
+
+// In Entity Lists, Forms, or Show Pages
+public function getListData(): array|Arrayable
+{
+ $users = User::with('company')->get();
+
+ return $this
+ ->setCustomTransformer('name', function($value, $user) {
+ return strtoupper($value);
+ })
+ ->setCustomTransformer('company:name', function($value, $user) {
+ return $value ?? 'N/A';
+ })
+ ->transform($users);
+}
+
+@endverbatim
+
+### Common Configuration Methods
+@verbatim
+
+// Entity List Configuration
+public function buildListConfig(): void
+{
+ $this
+ ->configureSearchable()
+ ->configureDefaultSort('created_at', 'desc')
+ ->configureDelete(confirmationText: 'Delete this item?')
+ ->configureCreateButtonLabel('Add new...');
+}
+
+// Form Configuration
+public function buildFormConfig(): void
+{
+ $this
+ ->configureDisplayShowPageAfterCreation()
+ ->configureCreateFormTitle('Create new user')
+ ->configureEditFormTitle('Edit user');
+}
+
+// Show Configuration
+public function buildShowConfig(): void
+{
+ $this
+ ->configureBreadcrumbCustomLabelAttribute('name')
+ ->configurePageTitle('name');
+}
+
+@endverbatim
+
+### Common Classes & Namespaces
+- **Entity Lists:** `Code16\Sharp\EntityList\SharpEntityList`
+- **Forms:** `Code16\Sharp\Form\SharpForm`
+- **Show Pages:** `Code16\Sharp\Show\SharpShow`
+- **Dashboards:** `Code16\Sharp\Dashboard\SharpDashboard`
+- **Form Fields:** `Code16\Sharp\Form\Fields\...`
+- **Show Fields:** `Code16\Sharp\Show\Fields\...`
+- **Entity List Fields:** `Code16\Sharp\EntityList\Fields\...`
+- **Eloquent Updater:** `Code16\Sharp\Form\Eloquent\WithSharpFormEloquentUpdater`