Output:
Wall time: 0.1694 seconds
Output:
[{"type":"text","text":"[2026-05-03 21:32:56] local.ERROR: Uncaught ReferenceError: fluxInputViewable is not defined http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae 1330 7 ReferenceError fluxInputViewable is not defined ReferenceError: fluxInputViewable is not defined\n    at [Alpine] fluxInputViewable (eval at safeAsyncFunction (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1385:21), <anonymous>:3:32)\n    at http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1410:28\n    at tryCatch (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1309:14)\n    at evaluate (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1343:34)\n    at Function.<anonymous> (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:3890:17)\n    at flushHandlers (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1527:48)\n    at stopDeferring (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1532:7)\n    at deferHandlingDirectives (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1535:5)\n    at initTree (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1732:5)\n    at http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1678:7 {\"url\":\"http://shop.test/account/login\",\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36\",\"timestamp\":\"2026-05-03T21:32:55.973Z\"} \n\n\n[2026-05-03 21:32:56] local.ERROR: Uncaught ReferenceError: fluxInputViewable is not defined http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae 1330 7 ReferenceError fluxInputViewable is not defined ReferenceError: fluxInputViewable is not defined\n    at [Alpine] fluxInputViewable (eval at safeAsyncFunction (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1385:21), <anonymous>:3:32)\n    at http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1410:28\n    at tryCatch (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1309:14)\n    at evaluate (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1343:34)\n    at Function.<anonymous> (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:3890:17)\n    at flushHandlers (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1527:48)\n    at stopDeferring (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1532:7)\n    at deferHandlingDirectives (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1535:5)\n    at initTree (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1732:5)\n    at http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1678:7 {\"url\":\"http://shop.test/account/login\",\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36\",\"timestamp\":\"2026-05-03T21:32:55.973Z\"} \n\n\n[2026-05-03 21:32:56] local.ERROR: Uncaught ReferenceError: fluxInputViewable is not defined http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae 1330 7 ReferenceError fluxInputViewable is not defined ReferenceError: fluxInputViewable is not defined\n    at [Alpine] fluxInputViewable (eval at safeAsyncFunction (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1385:21), <anonymous>:3:32)\n    at http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1410:28\n    at tryCatch (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1309:14)\n    at evaluate (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1343:34)\n    at Function.<anonymous> (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:3890:17)\n    at flushHandlers (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1527:48)\n    at stopDeferring (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1532:7)\n    at deferHandlingDirectives (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1535:5)\n    at initTree (http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1732:5)\n    at http://shop.test/livewire-6701cc17/livewire.js?id=cfc5c1ae:1678:7 {\"url\":\"http://shop.test/account/login\",\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36\",\"timestamp\":\"2026-05-03T21:32:55.973Z\"} \n\n\n[2026-05-03 21:32:56] local.WARNING: Livewire: missing closing tags found. Ensure your template elements contain matching closing tags. Y1jQ8DDcYzgTQ9o78LDp storefront.account.auth.login null null null false Y1jQ8DDcYzgTQ9o78LDp storefront.account.auth.login null null null false null null null 1 null {\"url\":\"http://shop.test/account/login\",\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36\",\"timestamp\":\"2026-05-03T21:32:55.973Z\"} \n\n\n[2026-05-03 21:32:56] local.WARNING: Livewire: missing closing tags found. Ensure your template elements contain matching closing tags. Y1jQ8DDcYzgTQ9o78LDp storefront.account.auth.login null null null false Y1jQ8DDcYzgTQ9o78LDp storefront.account.auth.login null null null false null null null 1 null {\"url\":\"http://shop.test/account/login\",\"user_agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36\",\"timestamp\":\"2026-05-03T21:32:55.973Z\"} \n"}]


The product form renders cleanly in Playwright with no current warnings/errors; Boost logs are still only the stale May 3 entries. I’m cleaning the generated Playwright files, then I’ll update progress and commit the product matrix slice.


git clean -f .playwright-mcp && git status --short


git clean -f .playwright-mcp && git status --short

Removing .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
Removing .playwright-mcp/console-2026-05-04T04-24-15-455Z.log
Removing .playwright-mcp/console-2026-05-04T05-10-30-496Z.log
Removing .playwright-mcp/console-2026-05-04T05-21-39-606Z.log
Removing .playwright-mcp/page-2026-05-04T05-21-39-818Z.yml
 M app/Livewire/Admin/Products/Form.php
 M resources/views/livewire/admin/products/form.blade.php
 M tests/Feature/Catalog/CatalogUiTest.php


Output:
Chunk ID: 4cde1d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 112
Output:
Removing .playwright-mcp/console-2026-05-03T23-28-42-443Z.log
Removing .playwright-mcp/console-2026-05-04T04-24-15-455Z.log
Removing .playwright-mcp/console-2026-05-04T05-10-30-496Z.log
Removing .playwright-mcp/console-2026-05-04T05-21-39-606Z.log
Removing .playwright-mcp/page-2026-05-04T05-21-39-818Z.yml
 M app/Livewire/Admin/Products/Form.php
 M resources/views/livewire/admin/products/form.blade.php
 M tests/Feature/Catalog/CatalogUiTest.php


Input:
*** Begin Patch
*** Update File: specs/progress.md
@@
-| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, product media upload/alt text/reorder/delete controls, order/customer/discount management, page index/form, navigation menu editor, theme grid, theme settings editor with iframe preview, general settings, domain management, shipping zone/rate management, tax settings, checkout policy settings, notification settings, search settings/reindexing, installed app list/detail, and developer token/webhook management with auth protection and store scoping. Advanced theme file editing is still missing. |
+| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, product option matrix generation, product media upload/alt text/reorder/delete controls, order/customer/discount management, page index/form, navigation menu editor, theme grid, theme settings editor with iframe preview, general settings, domain management, shipping zone/rate management, tax settings, checkout policy settings, notification settings, search settings/reindexing, installed app list/detail, and developer token/webhook management with auth protection and store scoping. Advanced theme file editing is still missing. |
@@
 - 2026-05-04: `mcp__laravel_boost__.get_absolute_url` resolved `http://shop.test/admin/settings/checkout` and `http://shop.test/admin/settings/notifications` before browser verification.
 - 2026-05-04: Playwright MCP verified `http://shop.test/admin/settings/checkout` and `http://shop.test/admin/settings/notifications` render with the full settings tab set; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Livewire 4 nested array form docs, Laravel 12 relationship sync/validation docs, Flux UI controls, and Pest 4 expectations before the product option matrix changes.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the product option matrix changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Catalog/CatalogUiTest.php` passed after the product option matrix changes and route smoke stabilization: 5 tests, 55 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Catalog tests/Feature/Admin/ProductMediaManagementTest.php` passed after the product option matrix changes: 28 tests, 150 assertions.
+- 2026-05-04: `npm run build` passed after the product option matrix Blade change.
+- 2026-05-04: `php artisan test --compact` passed after the product option matrix changes: 199 tests, 1010 assertions.
+- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the product option matrix changes.
+- 2026-05-04: `mcp__laravel_boost__.get_absolute_url(path: "/admin/products/create")` resolved `http://shop.test/admin/products/create` before browser verification.
+- 2026-05-04: Playwright MCP verified `http://shop.test/admin/products/create` renders the product form after the matrix control changes; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
@@
 - The catalog admin form intentionally blocks active products without a priced variant and duplicate in-store SKUs during UI edits, mirroring service-layer invariants where the current CRUD surface touches product variants directly.
+- The product admin form generates variant rows from the option cartesian product, preserves matching variant edits by option combination, and syncs product option values plus variant pivots on save; stale variants are deleted or archived if order-line history exists.
 - Navigation tree support is flat for now because the Phase 3 schema defines `navigation_items.position` but no `parent_id`; `NavigationService::buildTree()` returns a flat tree-compatible array with empty children.
@@
-- Product options can be displayed and edited as text in the admin form, but full option matrix generation and option-value reassignment UI are not complete.
*** End Patch


Success. Updated the following files:
M specs/progress.md

@@ -30,3 +30,3 @@
 | Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders/{order}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped order list/detail, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}`. OAuth/app API endpoints and broader admin REST surfaces are still missing. |
-| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, product media upload/alt text/reorder/delete controls, order/customer/discount management, page index/form, navigation menu editor, theme grid, theme settings editor with iframe preview, general settings, domain management, shipping zone/rate management, tax settings, checkout policy settings, notification settings, search settings/reindexing, installed app list/detail, and developer token/webhook management with auth protection and store scoping. Advanced theme file editing is still missing. |
+| Admin UI | `specs/03-ADMIN-UI.md` | partial | Flux/Livewire admin shell now includes dashboard KPI/date-range reporting, analytics reporting with filters/export, catalog/product/collection/inventory management, product option matrix generation, product media upload/alt text/reorder/delete controls, order/customer/discount management, page index/form, navigation menu editor, theme grid, theme settings editor with iframe preview, general settings, domain management, shipping zone/rate management, tax settings, checkout policy settings, notification settings, search settings/reindexing, installed app list/detail, and developer token/webhook management with auth protection and store scoping. Advanced theme file editing is still missing. |
 | Storefront UI | `specs/04-STOREFRONT-UI.md` | partial | Storefront layout, home, collections index/detail, product detail, SQLite-backed search with filters/sort/pagination, breadcrumbs, price display, product cards, DB-backed pages, seeded navigation menus, cached theme settings, product add-to-cart persistence, cart drawer, cart page with discount and shipping estimates, checkout address/shipping/discount/payment-selection/order-submission UI, order confirmation, customer password reset forms, and customer order list/detail views render seeded and runtime data. |
@@ -275,2 +275,11 @@
 - 2026-05-04: Playwright MCP verified `http://shop.test/admin/settings/checkout` and `http://shop.test/admin/settings/notifications` render with the full settings tab set; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.
+- 2026-05-04: `mcp__laravel_boost__.search_docs` consulted Livewire 4 nested array form docs, Laravel 12 relationship sync/validation docs, Flux UI controls, and Pest 4 expectations before the product option matrix changes.
+- 2026-05-04: `vendor/bin/pint --dirty --format agent` passed after the product option matrix changes.
+- 2026-05-04: `php artisan test --compact tests/Feature/Catalog/CatalogUiTest.php` passed after the product option matrix changes and route smoke stabilization: 5 tests, 55 assertions.
+- 2026-05-04: `php artisan test --compact tests/Feature/Catalog tests/Feature/Admin/ProductMediaManagementTest.php` passed after the product option matrix changes: 28 tests, 150 assertions.
+- 2026-05-04: `npm run build` passed after the product option matrix Blade change.
+- 2026-05-04: `php artisan test --compact` passed after the product option matrix changes: 199 tests, 1010 assertions.
+- 2026-05-04: `php artisan migrate:fresh --seed --no-interaction` passed after the product option matrix changes.
+- 2026-05-04: `mcp__laravel_boost__.get_absolute_url(path: "/admin/products/create")` resolved `http://shop.test/admin/products/create` before browser verification.
+- 2026-05-04: Playwright MCP verified `http://shop.test/admin/products/create` renders the product form after the matrix control changes; current Playwright console checks reported no warnings/errors. Boost `browser_logs` only returned older May 3 account-login warnings from a previously fixed issue.

@@ -288,2 +297,3 @@
 - The catalog admin form intentionally blocks active products without a priced variant and duplicate in-store SKUs during UI edits, mirroring service-layer invariants where the current CRUD surface touches product variants directly.
+- The product admin form generates variant rows from the option cartesian product, preserves matching variant edits by option combination, and syncs product option values plus variant pivots on save; stale variants are deleted or archived if order-line history exists.
 - Navigation tree support is flat for now because the Phase 3 schema defines `navigation_items.position` but no `parent_id`; `NavigationService::buildTree()` returns a flat tree-compatible array with empty children.
@@ -336,3 +346,2 @@
 - `php artisan route:list --except-vendor` hides Livewire full-page routes because their controller is vendor-provided; path-filtered route-list commands are used as evidence for those routes.
-- Product options can be displayed and edited as text in the admin form, but full option matrix generation and option-value reassignment UI are not complete.
 - Theme file editing and schema-driven visual sections are still missing; the current theme editor updates seeded settings only.


Output: {"output":"Success. Updated the following files:\nM specs/progress.md\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


The product matrix gap is now reflected in progress, with test/build/browser evidence and the old open issue removed. I’m checking the diff and committing.


Output:
Chunk ID: 4327c4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


 app/Livewire/Admin/Products/Form.php               | 225 ++++++++++++++++++++-
 .../views/livewire/admin/products/form.blade.php   |   5 +-
 specs/progress.md                                  |  13 +-
 tests/Feature/Catalog/CatalogUiTest.php            |  77 ++++++-
 4 files changed, 314 insertions(+), 6 deletions(-)


 M app/Livewire/Admin/Products/Form.php
 M resources/views/livewire/admin/products/form.blade.php
 M specs/progress.md
 M tests/Feature/Catalog/CatalogUiTest.php


Output:
Chunk ID: d51689
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 41
Output:
 M app/Livewire/Admin/Products/Form.php
 M resources/views/livewire/admin/products/form.blade.php
 M specs/progress.md
 M tests/Feature/Catalog/CatalogUiTest.php


Output:
Chunk ID: b81568
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 80
Output:
 app/Livewire/Admin/Products/Form.php               | 225 ++++++++++++++++++++-
 .../views/livewire/admin/products/form.blade.php   |   5 +-
 specs/progress.md                                  |  13 +-
 tests/Feature/Catalog/CatalogUiTest.php            |  77 ++++++-
 4 files changed, 314 insertions(+), 6 deletions(-)


git add app/Livewire/Admin/Products/Form.php resources/views/livewire/admin/products/form.blade.php tests/Feature/Catalog/CatalogUiTest.php specs/progress.md && git commit -m "Add admin product option matrix"


git add app/Livewire/Admin/Products/Form.php resources/views/livewire/admin/products/form.blade.php tests/Feature/Catalog/CatalogUiTest.php specs/progress.md && git commit -m "Add admin product option matrix"

[2026-05-04-codex-5-5-goal 267be7e0] Add admin product option matrix
 4 files changed, 314 insertions(+), 6 deletions(-)


Output:
Chunk ID: 57fbfc
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 31
Output:
[2026-05-04-codex-5-5-goal 267be7e0] Add admin product option matrix
 4 files changed, 314 insertions(+), 6 deletions(-)


Product option matrix support is committed as `267be7e0`. Remaining gaps are getting larger or dependency-bound; I’m taking the theme editor next because file editing can be added inside the existing theme schema without new packages.


laravel/framework livewire/livewire livewire/flux pestphp/pest

livewire textarea form validation testing


# Search Results

## Validation Form Request Validation
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

## Form Request Validation

---

## Wire Model Input fields
Wire Model | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-model

## Input fields

Livewire supports most native input elements out of the box. Meaning you should just be able to attach `wire:model` to any input element in the browser and easily bind properties to them.

Here's a comprehensive list of the different available input types and how you use them in a Livewire context.

### Text inputs

First and foremost, text inputs are the bedrock of most forms. Here's how to bind a property named "title" to one:

```blade
<input type="text" wire:model="title">
```

### Textarea inputs

Textarea elements are similarly straightforward. Simply add `wire:model` to a textarea and the value will be bound:

```blade
<textarea type="text" wire:model="content"></textarea>
```

If the "content" value is initialized with a string, Livewire will fill the textarea with that value - there's no need to do something like the following:

```blade
<!-- Warning: This snippet demonstrates what NOT to do... -->

<textarea type="text" wire:model="content">{{ $content }}</textarea>
```

### Checkboxes

Checkboxes can be used for single values, such as when toggling a boolean property. Or, checkboxes may be used to toggle a single value in a group of related values. We'll discuss both scenarios:

#### Single checkbox

At the end of a signup form, you might have a checkbox allowing the user to opt-in to email updates. You might call this property `$receiveUpdates`. You can easily bind this value to the checkbox using `wire:model`:

```blade
<input type="checkbox" wire:model="receiveUpdates">
```

Now when the `$receiveUpdates` value is `false`, the checkbox will be unchecked. Of course, when the value is `true`, the checkbox will be checked.

#### Multiple checkboxes

Now, let's say in addition to allowing the user to decide to receive updates, you have an array property in your class called `$updateTypes`, allowing the user to choose from a variety of update types:

```php
public $updateTypes = [];
```

By binding multiple checkboxes to the `$updateTypes` property, the user can select multiple update types and they will be added to the `$updateTypes` array property:

```blade
<input type="checkbox" value="email" wire:model="updateTypes">
<input type="checkbox" value="sms" wire:model="updateTypes">
<input type="checkbox" value="notification" wire:model="updateTypes">
```

For example, if the user checks the first two boxes but not the third, the value of `$updateTypes` will be: `["email", "sms"]`

### Radio buttons

To toggle between two different values for a single property, you may use radio buttons:

```blade
<input type="radio" value="yes" wire:model="receiveUpdates">
<input type="radio" value="no" wire:model="receiveUpdates">
```

### Select dropdowns

Livewire makes it simple to work with `<select>` dropdowns. When adding `wire:model` to a dropdown, the currently selected value will be bound to the provided property name and vice versa.

In addition, there's no need to manually add `selected` to the option that will be selected - Livewire handles that for you automatically.

Below is an example of a select dropdown filled with a static list of states:

```blade
<select wire:model="state">
    <option value="AL">Alabama</option>
    <option value="AK">Alaska</option>
    <option value="AZ">Arizona</option>
    ...
</select>
```

When a specific state is selected, for example, "Alaska", the `$state` property on the component will be set to `AK`. If you would prefer the value to be set to "Alaska" instead of "AK", you can leave the `value=""` attribute off the `<option>` element entirely.

Often, you may build your dropdown options dynamically using Blade:

```blade
<select wire:model="state">
    @foreach (\App\Models\State::all() as $state)
        <option value="{{ $state->id }}">{{ $state->label }}</option>
    @endforeach
</select>
```

If you don't have a specific option selected by default, you may want to show a muted placeholder option by default, such as "Select a state":

```blade
<select wire:model="state">
    <option disabled value="">Select a state...</option>

    @foreach (\App\Models\State::all() as $state)
        <option value="{{ $state->id }}">{{ $state->label }}</option>
    @endforeach
</select>
```

As you can see, there is no "placeholder" attribute for a select menu like there is for text inputs. Instead, you have to add a `disabled` option element as the first option in the list.

### Dependent select dropdowns

Sometimes you may want one select menu to be dependent on another. For example, a list of cities that changes based on which state is selected.

For the most part, this works as you'd expect, however there is one important gotcha: You must add a `wire:key` to the changing select so that Livewire properly refreshes its value when the options change.

Here's an example of two selects, one for states, one for cities. When the state select changes, the options in the city select will change properly:

```blade
<!-- States select menu... -->
<select wire:model.live="selectedState">
    @foreach (State::all() as $state)
        <option value="{{ $state->id }}">{{ $state->label }}</option>
    @endforeach
</select>

<!-- Cities dependent select menu... -->
<select wire:model.live="selectedCity" wire:key="{{ $selectedState }}"> <!-- [tl! highlight] -->
    @foreach (City::whereStateId($selectedState->id)->get() as $city)
        <option value="{{ $city->id }}">{{ $city->label }}</option>
    @endforeach
</select>
```

Again, the only thing non-standard here is the `wire:key` that has been added to the second select. This ensures that when the state changes, the "selectedCity" value will be reset properly.

### Multi-select dropdowns

If you are using a "multiple" select menu, Livewire works as expected. In this example, states will be added to the `$states` array property when they are selected and removed if they are deselected:

```blade
<select wire:model="states" multiple>
    <option value="AL">Alabama</option>
    <option value="AK">Alaska</option>
    <option value="AZ">Arizona</option>
    ...
</select>
```

---

## Components Page components
Components | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/components

## Page components

Components can be routed to directly as full pages using `Route::livewire()`. This is one of Livewire's most powerful features, allowing you to build entire pages without traditional controllers.

```php
Route::livewire('/posts/create', 'pages::post.create');
```

When a user visits `/posts/create`, Livewire will render the `pages::post.create` component inside your application's layout file.

Page components work just like regular components, but they're rendered as full pages with access to:
- Custom layouts
- Page titles
- Route parameters and model binding
- Named slots for layouts

For complete information about page components, including layouts, titles, and advanced routing, see the [Pages documentation](/docs/4.x/pages).

---

## Urls URLs for Named Routes Eloquent Models
Urls | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/urls.md

#### Eloquent Models

You will often be generating URLs using the route key (typically the primary key) of [Eloquent models](/docs/{{version}}/eloquent). For this reason, you may pass Eloquent models as parameter values. The `route` helper will automatically extract the model's route key:

```php
echo route('post.show', ['post' => $post]);
```

---

## Components Textarea > flux:textarea
Components Textarea | livewire/flux@2.x | Source: https://fluxui.dev/components/textarea#livewire/flux

flux:textarea

<ui-table-scroll-area>

Prop

Description

    wire:model

Binds the textarea to a Livewire property. See the wire:model documentation for more information.

    placeholder

Placeholder text displayed when the textarea is empty.

    label

Label text displayed above the textarea. When provided, wraps the textarea in a flux:field component with an adjacent flux:label component. See the field component.

    description

Help text displayed below the textarea. When provided alongside label, appears between the label and textarea within the flux:field wrapper. See the field component.

    description:trailing

The description provided will be displayed below the textarea instead of above it.

    badge

Badge text displayed at the end of the flux:label component when the label prop is provided.

    rows

Number of visible text lines. Use "auto" for automatic height adjustment. Default: 4.

    resize

Control how the textarea can be resized. Options: vertical (default), horizontal, both, none.

    invalid

If true, applies error styling to the textarea.

</ui-table-scroll-area>

<ui-table-scroll-area>

Attribute

Description

    data-flux-textarea

Applied to the textarea element for styling and identification.

</ui-table-scroll-area>

---

## Pages Using route model binding
Pages | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/pages

## Using route model binding

Laravel's route model binding allows you to automatically resolve Eloquent models from route parameters.

After defining a route with a model parameter in your `routes/web.php` file:

```php
Route::livewire('/posts/{post}', 'pages::show-post');
```

You can now accept the route model parameter through the `mount()` method of your component:

```php
<?php

use App\Models\Post;
use Livewire\Component;

new class extends Component {
    public Post $post;

    public function mount(Post $post) // [tl! highlight]
    {
        $this->post = $post;
    }
};
```

Livewire knows to use "route model binding" because the `Post` type-hint is prepended to the `$post` parameter in `mount()`.

Like before, you can reduce boilerplate by omitting the `mount()` method:

```php
<?php

use Livewire\Component;
use App\Models\Post;

new class extends Component {
    public Post $post; // [tl! highlight]
};
```

The `$post` property will automatically be assigned to the model bound via the route's `{post}` parameter.

---

## Attribute Validate Example: Contact form
Attribute Validate | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-validate

## Example: Contact form

Here's a complete contact form with validation:

```php
<?php // resources/views/pages/⚡contact.blade.php

use Livewire\Attributes\Validate;
use Livewire\Component;
use App\Mail\ContactMessage;
use Illuminate\Support\Facades\Mail;

new class extends Component {
    #[Validate('required|min:2', as: 'name')]
    public $name = '';

    #[Validate('required|email')]
    public $email = '';

    #[Validate('required')]
    public $subject = '';

    #[Validate('required|min:10', as: 'message')]
    public $message = '';

    public function submit()
    {
        $validated = $this->validate();

        Mail::to('support@example.com')->send(new ContactMessage($validated));

        session()->flash('success', 'Message sent successfully!');

        $this->reset();
    }
};
?>

<div>
    @if (session('success'))
        <div class="alert">{{ session('success') }}</div>
    @endif

    <form wire:submit="submit">
        <div>
            <input type="text" wire:model.live.blur="name" placeholder="Your name">
            @error('name') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <input type="email" wire:model.live.blur="email" placeholder="Your email">
            @error('email') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <input type="text" wire:model.live.blur="subject" placeholder="Subject">
            @error('subject') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <textarea wire:model.live.blur="message" placeholder="Your message"></textarea>
            @error('message') <span class="error">{{ $message }}</span> @enderror
        </div>

        <button type="submit">Send Message</button>
    </form>
</div>
```

Users get immediate feedback as they fill out the form, with friendly field names and helpful error messages.

---

## Routing Route Model Binding
Routing | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md

## Route Model Binding

When injecting a model ID to a route or controller action, you will often query the database to retrieve the model that corresponds to that ID. Laravel route model binding provides a convenient way to automatically inject the model instances directly into your routes. For example, instead of injecting a user's ID, you can inject the entire `User` model instance that matches the given ID.

---

## Quickstart Write the component
Quickstart | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/quickstart

## Write the component

Open `resources/views/pages/post/⚡create.blade.php` and replace its contents with the following:

```blade
<?php

use Livewire\Component;

new class extends Component {
    public string $title = '';

    public string $content = '';

    public function save()
    {
        $this->validate([
            'title' => 'required|max:255',
            'content' => 'required',
        ]);

        dd($this->title, $this->content);
    }
};
?>

<form wire:submit="save">
    <label>
        Title
        <input type="text" wire:model="title">
        @error('title') <span style="color: red;">{{ $message }}</span> @enderror
    </label>

    <label>
        Content
        <textarea wire:model="content" rows="5"></textarea>
        @error('content') <span style="color: red;">{{ $message }}</span> @enderror
    </label>

    <button type="submit">Save Post</button>
</form>
```

> [!info] Don't worry about styling
> This form is intentionally unstyled so we can focus on Livewire's functionality. In a real application, you'd add CSS or use a framework like Tailwind.

Here's what's happening in the code above:

**Component properties:**
- `public string $title = '';` — Declares a public property for the post title
- `public string $content = '';` — Declares a public property for the post content

**Component methods:**
- `public function save()` — Called when the form is submitted. Validates the data and dumps the output for testing.

**Livewire directives:**
- `wire:submit="save"` — Calls the `save()` method when the form is submitted, preventing the default page reload
- `wire:model="title"` — Creates two-way data binding between the input and the `$title` property. As you type, the property updates automatically
- `wire:model="content"` — Same two-way binding for the textarea and `$content` property
- `@error('title')` and `@error('content')` — Display validation error messages when validation fails

> [!warning] Livewire components MUST have a single root element
> Components must have exactly one root HTML element. In this example, the `<form>` element is the single root. Multiple root elements or HTML comments outside the root element will cause an error. When rendering [full-page components](/docs/4.x/pages), named slots for the layout can be placed outside the root element.

> [!tip] In a real application
> The `save()` method uses `dd()` to dump the values for testing purposes. In a production application, you would typically save the data to a database and redirect:
> ```php
> public function save()
> {
>     $validated = $this->validate([
>         'title' => 'required|max:255',
>         'content' => 'required',
>     ]);
>
>     Post::create($validated); // Assumes you have a Post model and database table
>
>     return $this->redirect('/posts');
> }
> ```

---

## Upgrade Upgrading To 12.0 From 11.x Eloquent Models and UUIDv7
Upgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md

#### Models and UUIDv7

**Likelihood Of Impact: Medium**

The `HasUuids` trait now returns UUIDs that are compatible with version 7 of the UUID spec (ordered UUIDs). If you would like to continue using ordered UUIDv4 strings for your model's IDs, you should now use the `HasVersion4Uuids` trait:

```php
use Illuminate\Database\Eloquent\Concerns\HasUuids; // [tl! remove]
use Illuminate\Database\Eloquent\Concerns\HasVersion4Uuids as HasUuids; // [tl! add]
```

The `HasVersion7Uuids` trait has been removed. If you were previously using this trait, you should use the `HasUuids` trait instead, which now provides the same behavior.

---

## Eloquent Inserting and Updating Models Mass Assignment Mass Assignment and JSON Columns
Eloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md

#### Mass Assignment and JSON Columns

When assigning JSON columns, each column's mass assignable key must be specified in your model's `$fillable` array. For security, Laravel does not support updating nested JSON attributes when using the `guarded` property:

```php
/**
 * The attributes that are mass assignable.
 *
 * @var array<int, string>
 */
protected $fillable = [
    'options->enabled',
];
```

---

## Forms Extracting input fields to Blade components
Forms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms

## Extracting input fields to Blade components

Even in a small component such as the `post.create` example we've been discussing, we end up duplicating lots of form field boilerplate like validation messages and labels.

It can be helpful to extract repetitive UI elements such as these into dedicated [Blade components](https://laravel.com/docs/blade#components) to be shared across your application.

For example, below is the original Blade template from the `post.create` component. We will be extracting the following two text inputs into dedicated Blade components:

```blade
<form wire:submit="save">
    <input type="text" wire:model="title"> <!-- [tl! highlight:3] -->
    <div>
        @error('title') <span class="error">{{ $message }}</span> @enderror
    </div>

    <input type="text" wire:model="content"> <!-- [tl! highlight:3] -->
    <div>
        @error('content') <span class="error">{{ $message }}</span> @enderror
    </div>

    <button type="submit">Save</button>
</form>
```

Here's what the template will look like after extracting a re-usable Blade component called `<x-input-text>`:

```blade
<form wire:submit="save">
    <x-input-text name="title" wire:model="title" /> <!-- [tl! highlight] -->

    <x-input-text name="content" wire:model="content" /> <!-- [tl! highlight] -->

    <button type="submit">Save</button>
</form>
```

Next, here's the source for the `x-input-text` component:

```blade
<!-- resources/views/components/input-text.blade.php -->

@props(['name'])

<input type="text" name="{{ $name }}" {{ $attributes }}>

<div>
    @error($name) <span class="error">{{ $message }}</span> @enderror
</div>
```

As you can see, we took the repetitive HTML and placed it inside a dedicated Blade component.

For the most part, the Blade component contains only the extracted HTML from the original component. However, we have added two things:

* The `@props` directive
* The `{{ $attributes }}` statement on the input

Let's discuss each of these additions:

By specifying `name` as a "prop" using `@props(['name'])` we are telling Blade: if an attribute called "name" is set on this component, take its value and make it available inside this component as `$name`.

For other attributes that don't have an explicit purpose, we used the `{{ $attributes }}` statement. This is used for "attribute forwarding", or in other words, taking any HTML attributes written on the Blade component and forwarding them onto an element within the component.

This ensures `wire:model="title"` and any other extra attributes such as `disabled`, `class="..."`, or `required` still get forwarded to the actual `<input>` element.

### Custom form controls

In the previous example, we "wrapped" an input element into a re-usable Blade component we can use as if it was a native HTML input element.

This pattern is very useful; however, there might be some cases where you want to create an entire input component from scratch (without an underlying native input element), but still be able to bind its value to Livewire properties using `wire:model`.

For example, let's imagine you wanted to create an `<x-input-counter />` component that was a simple "counter" input written in Alpine.

Before we create a Blade component, let's first look at a simple, pure-Alpine, "counter" component for reference:

```blade
<div x-data="{ count: 0 }">
    <button x-on:click="count--">-</button>

    <span x-text="count"></span>

    <button x-on:click="count++">+</button>
</div>
```

As you can see, the component above shows a number alongside two buttons to increment and decrement that number.

Now, let's imagine we want to extract this component into a Blade component called `<x-input-counter />` that we would use within a component like so:

```blade
<x-input-counter wire:model="quantity" />
```

Creating this component is mostly simple. We take the HTML of the counter and place it inside a Blade component template like `resources/views/components/input-counter.blade.php`.

However, making it work with `wire:model="quantity"` so that you can easily bind data from your Livewire component to the "count" inside this Alpine component needs one extra step.

Here's the source for the component:

```blade
<!-- resources/view/components/input-counter.blade.php -->

<div x-data="{ count: 0 }" x-modelable="count" {{ $attributes}}>
    <button x-on:click="count--">-</button>

    <span x-text="count"></span>

    <button x-on:click="count++">+</button>
</div>
```

As you can see, the only different bit about this HTML is the `x-modelable="count"` and `{{ $attributes }}`.

`x-modelable` is a utility in Alpine that tells Alpine to make a certain piece of data available for binding from outside. [The Alpine documentation has more information on this directive.](https://alpinejs.dev/directives/modelable)

`{{ $attributes }}`, as we explored earlier, forwards any attributes passed into the Blade component from outside. In this case, the `wire:model` directive will be forwarded.

Because of `{{ $attributes }}`, when the HTML is rendered in the browser, `wire:model="quantity"` will be rendered alongside `x-modelable="count"` on the root `<div>` of the Alpine component like so:

```blade
<div x-data="{ count: 0 }" x-modelable="count" wire:model="quantity">
```

`x-modelable="count"` tells Alpine to look for any `x-model` or `wire:model` statements and use "count" as the data to bind them to.

Because `x-modelable` works for both `wire:model` and `x-model`, you can also use this Blade component interchangeably with Livewire and Alpine. Here’s an example of using this Blade component in a purely Alpine context:

```blade
<x-input-counter x-model="quantity" />
```

Creating custom input elements in your application is extremely powerful but requires a deeper understanding of the utilities Livewire and Alpine provide and how they interact with each other.

_[Truncated]_


Output:
Wall time: 4.0061 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Validation Form Request Validation\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n## Form Request Validation\n\n---\n\n## Wire Model Input fields\nWire Model | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-model\n\n## Input fields\n\nLivewire supports most native input elements out of the box. Meaning you should just be able to attach `wire:model` to any input element in the browser and easily bind properties to them.\n\nHere's a comprehensive list of the different available input types and how you use them in a Livewire context.\n\n### Text inputs\n\nFirst and foremost, text inputs are the bedrock of most forms. Here's how to bind a property named \"title\" to one:\n\n```blade\n<input type=\"text\" wire:model=\"title\">\n```\n\n### Textarea inputs\n\nTextarea elements are similarly straightforward. Simply add `wire:model` to a textarea and the value will be bound:\n\n```blade\n<textarea type=\"text\" wire:model=\"content\"></textarea>\n```\n\nIf the \"content\" value is initialized with a string, Livewire will fill the textarea with that value - there's no need to do something like the following:\n\n```blade\n<!-- Warning: This snippet demonstrates what NOT to do... -->\n\n<textarea type=\"text\" wire:model=\"content\">{{ $content }}</textarea>\n```\n\n### Checkboxes\n\nCheckboxes can be used for single values, such as when toggling a boolean property. Or, checkboxes may be used to toggle a single value in a group of related values. We'll discuss both scenarios:\n\n#### Single checkbox\n\nAt the end of a signup form, you might have a checkbox allowing the user to opt-in to email updates. You might call this property `$receiveUpdates`. You can easily bind this value to the checkbox using `wire:model`:\n\n```blade\n<input type=\"checkbox\" wire:model=\"receiveUpdates\">\n```\n\nNow when the `$receiveUpdates` value is `false`, the checkbox will be unchecked. Of course, when the value is `true`, the checkbox will be checked.\n\n#### Multiple checkboxes\n\nNow, let's say in addition to allowing the user to decide to receive updates, you have an array property in your class called `$updateTypes`, allowing the user to choose from a variety of update types:\n\n```php\npublic $updateTypes = [];\n```\n\nBy binding multiple checkboxes to the `$updateTypes` property, the user can select multiple update types and they will be added to the `$updateTypes` array property:\n\n```blade\n<input type=\"checkbox\" value=\"email\" wire:model=\"updateTypes\">\n<input type=\"checkbox\" value=\"sms\" wire:model=\"updateTypes\">\n<input type=\"checkbox\" value=\"notification\" wire:model=\"updateTypes\">\n```\n\nFor example, if the user checks the first two boxes but not the third, the value of `$updateTypes` will be: `[\"email\", \"sms\"]`\n\n### Radio buttons\n\nTo toggle between two different values for a single property, you may use radio buttons:\n\n```blade\n<input type=\"radio\" value=\"yes\" wire:model=\"receiveUpdates\">\n<input type=\"radio\" value=\"no\" wire:model=\"receiveUpdates\">\n```\n\n### Select dropdowns\n\nLivewire makes it simple to work with `<select>` dropdowns. When adding `wire:model` to a dropdown, the currently selected value will be bound to the provided property name and vice versa.\n\nIn addition, there's no need to manually add `selected` to the option that will be selected - Livewire handles that for you automatically.\n\nBelow is an example of a select dropdown filled with a static list of states:\n\n```blade\n<select wire:model=\"state\">\n    <option value=\"AL\">Alabama</option>\n    <option value=\"AK\">Alaska</option>\n    <option value=\"AZ\">Arizona</option>\n    ...\n</select>\n```\n\nWhen a specific state is selected, for example, \"Alaska\", the `$state` property on the component will be set to `AK`. If you would prefer the value to be set to \"Alaska\" instead of \"AK\", you can leave the `value=\"\"` attribute off the `<option>` element entirely.\n\nOften, you may build your dropdown options dynamically using Blade:\n\n```blade\n<select wire:model=\"state\">\n    @foreach (\\App\\Models\\State::all() as $state)\n        <option value=\"{{ $state->id }}\">{{ $state->label }}</option>\n    @endforeach\n</select>\n```\n\nIf you don't have a specific option selected by default, you may want to show a muted placeholder option by default, such as \"Select a state\":\n\n```blade\n<select wire:model=\"state\">\n    <option disabled value=\"\">Select a state...</option>\n\n    @foreach (\\App\\Models\\State::all() as $state)\n        <option value=\"{{ $state->id }}\">{{ $state->label }}</option>\n    @endforeach\n</select>\n```\n\nAs you can see, there is no \"placeholder\" attribute for a select menu like there is for text inputs. Instead, you have to add a `disabled` option element as the first option in the list.\n\n### Dependent select dropdowns\n\nSometimes you may want one select menu to be dependent on another. For example, a list of cities that changes based on which state is selected.\n\nFor the most part, this works as you'd expect, however there is one important gotcha: You must add a `wire:key` to the changing select so that Livewire properly refreshes its value when the options change.\n\nHere's an example of two selects, one for states, one for cities. When the state select changes, the options in the city select will change properly:\n\n```blade\n<!-- States select menu... -->\n<select wire:model.live=\"selectedState\">\n    @foreach (State::all() as $state)\n        <option value=\"{{ $state->id }}\">{{ $state->label }}</option>\n    @endforeach\n</select>\n\n<!-- Cities dependent select menu... -->\n<select wire:model.live=\"selectedCity\" wire:key=\"{{ $selectedState }}\"> <!-- [tl! highlight] -->\n    @foreach (City::whereStateId($selectedState->id)->get() as $city)\n        <option value=\"{{ $city->id }}\">{{ $city->label }}</option>\n    @endforeach\n</select>\n```\n\nAgain, the only thing non-standard here is the `wire:key` that has been added to the second select. This ensures that when the state changes, the \"selectedCity\" value will be reset properly.\n\n### Multi-select dropdowns\n\nIf you are using a \"multiple\" select menu, Livewire works as expected. In this example, states will be added to the `$states` array property when they are selected and removed if they are deselected:\n\n```blade\n<select wire:model=\"states\" multiple>\n    <option value=\"AL\">Alabama</option>\n    <option value=\"AK\">Alaska</option>\n    <option value=\"AZ\">Arizona</option>\n    ...\n</select>\n```\n\n---\n\n## Components Page components\nComponents | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/components\n\n## Page components\n\nComponents can be routed to directly as full pages using `Route::livewire()`. This is one of Livewire's most powerful features, allowing you to build entire pages without traditional controllers.\n\n```php\nRoute::livewire('/posts/create', 'pages::post.create');\n```\n\nWhen a user visits `/posts/create`, Livewire will render the `pages::post.create` component inside your application's layout file.\n\nPage components work just like regular components, but they're rendered as full pages with access to:\n- Custom layouts\n- Page titles\n- Route parameters and model binding\n- Named slots for layouts\n\nFor complete information about page components, including layouts, titles, and advanced routing, see the [Pages documentation](/docs/4.x/pages).\n\n---\n\n## Urls URLs for Named Routes Eloquent Models\nUrls | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/urls.md\n\n\n#### Eloquent Models\n\nYou will often be generating URLs using the route key (typically the primary key) of [Eloquent models](/docs/{{version}}/eloquent). For this reason, you may pass Eloquent models as parameter values. The `route` helper will automatically extract the model's route key:\n\n```php\necho route('post.show', ['post' => $post]);\n```\n\n---\n\n## Components Textarea > flux:textarea\nComponents Textarea | livewire/flux@2.x | Source: https://fluxui.dev/components/textarea#livewire/flux\n\n
flux:textarea

<ui-table-scroll-area>

Prop

Description

\n    wire:model\n

Binds the textarea to a Livewire property. See the wire:model documentation for more information.

\n    placeholder\n

Placeholder text displayed when the textarea is empty.

\n    label\n

Label text displayed above the textarea. When provided, wraps the textarea in a flux:field component with an adjacent flux:label component. See the field component.

\n    description\n

Help text displayed below the textarea. When provided alongside label, appears between the label and textarea within the flux:field wrapper. See the field component.

\n    description:trailing\n

The description provided will be displayed below the textarea instead of above it.

\n    badge\n

Badge text displayed at the end of the flux:label component when the label prop is provided.

\n    rows\n

Number of visible text lines. Use \"auto\" for automatic height adjustment. Default: 4.

\n    resize\n

Control how the textarea can be resized. Options: vertical (default), horizontal, both, none.

\n    invalid\n

If true, applies error styling to the textarea.

</ui-table-scroll-area>

<ui-table-scroll-area>

Attribute

Description

\n    data-flux-textarea\n

Applied to the textarea element for styling and identification.

</ui-table-scroll-area>

\n\n---\n\n## Pages Using route model binding\nPages | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/pages\n\n## Using route model binding\n\nLaravel's route model binding allows you to automatically resolve Eloquent models from route parameters.\n\nAfter defining a route with a model parameter in your `routes/web.php` file:\n\n```php\nRoute::livewire('/posts/{post}', 'pages::show-post');\n```\n\nYou can now accept the route model parameter through the `mount()` method of your component:\n\n```php\n<?php\n\nuse App\\Models\\Post;\nuse Livewire\\Component;\n\nnew class extends Component {\n    public Post $post;\n\n    public function mount(Post $post) // [tl! highlight]\n    {\n        $this->post = $post;\n    }\n};\n```\n\nLivewire knows to use \"route model binding\" because the `Post` type-hint is prepended to the `$post` parameter in `mount()`.\n\nLike before, you can reduce boilerplate by omitting the `mount()` method:\n\n```php\n<?php\n\nuse Livewire\\Component;\nuse App\\Models\\Post;\n\nnew class extends Component {\n    public Post $post; // [tl! highlight]\n};\n```\n\nThe `$post` property will automatically be assigned to the model bound via the route's `{post}` parameter.\n\n---\n\n## Attribute Validate Example: Contact form\nAttribute Validate | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-validate\n\n## Example: Contact form\n\nHere's a complete contact form with validation:\n\n```php\n<?php // resources/views/pages/⚡contact.blade.php\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Component;\nuse App\\Mail\\ContactMessage;\nuse Illuminate\\Support\\Facades\\Mail;\n\nnew class extends Component {\n    #[Validate('required|min:2', as: 'name')]\n    public $name = '';\n\n    #[Validate('required|email')]\n    public $email = '';\n\n    #[Validate('required')]\n    public $subject = '';\n\n    #[Validate('required|min:10', as: 'message')]\n    public $message = '';\n\n    public function submit()\n    {\n        $validated = $this->validate();\n\n        Mail::to('support@example.com')->send(new ContactMessage($validated));\n\n        session()->flash('success', 'Message sent successfully!');\n\n        $this->reset();\n    }\n};\n?>\n\n<div>\n    @if (session('success'))\n        <div class=\"alert\">{{ session('success') }}</div>\n    @endif\n\n    <form wire:submit=\"submit\">\n        <div>\n            <input type=\"text\" wire:model.live.blur=\"name\" placeholder=\"Your name\">\n            @error('name') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <div>\n            <input type=\"email\" wire:model.live.blur=\"email\" placeholder=\"Your email\">\n            @error('email') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <div>\n            <input type=\"text\" wire:model.live.blur=\"subject\" placeholder=\"Subject\">\n            @error('subject') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <div>\n            <textarea wire:model.live.blur=\"message\" placeholder=\"Your message\"></textarea>\n            @error('message') <span class=\"error\">{{ $message }}</span> @enderror\n        </div>\n\n        <button type=\"submit\">Send Message</button>\n    </form>\n</div>\n```\n\nUsers get immediate feedback as they fill out the form, with friendly field names and helpful error messages.\n\n---\n\n## Routing Route Model Binding\nRouting | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/routing.md\n\n\n## Route Model Binding\n\nWhen injecting a model ID to a route or controller action, you will often query the database to retrieve the model that corresponds to that ID. Laravel route model binding provides a convenient way to automatically inject the model instances directly into your routes. For example, instead of injecting a user's ID, you can inject the entire `User` model instance that matches the given ID.\n\n---\n\n## Quickstart Write the component\nQuickstart | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/quickstart\n\n## Write the component\n\nOpen `resources/views/pages/post/⚡create.blade.php` and replace its contents with the following:\n\n```blade\n<?php\n\nuse Livewire\\Component;\n\nnew class extends Component {\n    public string $title = '';\n\n    public string $content = '';\n\n    public function save()\n    {\n        $this->validate([\n            'title' => 'required|max:255',\n            'content' => 'required',\n        ]);\n\n        dd($this->title, $this->content);\n    }\n};\n?>\n\n<form wire:submit=\"save\">\n    <label>\n        Title\n        <input type=\"text\" wire:model=\"title\">\n        @error('title') <span style=\"color: red;\">{{ $message }}</span> @enderror\n    </label>\n\n    <label>\n        Content\n        <textarea wire:model=\"content\" rows=\"5\"></textarea>\n        @error('content') <span style=\"color: red;\">{{ $message }}</span> @enderror\n    </label>\n\n    <button type=\"submit\">Save Post</button>\n</form>\n```\n\n> [!info] Don't worry about styling\n> This form is intentionally unstyled so we can focus on Livewire's functionality. In a real application, you'd add CSS or use a framework like Tailwind.\n\nHere's what's happening in the code above:\n\n**Component properties:**\n- `public string $title = '';` — Declares a public property for the post title\n- `public string $content = '';` — Declares a public property for the post content\n\n**Component methods:**\n- `public function save()` — Called when the form is submitted. Validates the data and dumps the output for testing.\n\n**Livewire directives:**\n- `wire:submit=\"save\"` — Calls the `save()` method when the form is submitted, preventing the default page reload\n- `wire:model=\"title\"` — Creates two-way data binding between the input and the `$title` property. As you type, the property updates automatically\n- `wire:model=\"content\"` — Same two-way binding for the textarea and `$content` property\n- `@error('title')` and `@error('content')` — Display validation error messages when validation fails\n\n> [!warning] Livewire components MUST have a single root element\n> Components must have exactly one root HTML element. In this example, the `<form>` element is the single root. Multiple root elements or HTML comments outside the root element will cause an error. When rendering [full-page components](/docs/4.x/pages), named slots for the layout can be placed outside the root element.\n\n> [!tip] In a real application\n> The `save()` method uses `dd()` to dump the values for testing purposes. In a production application, you would typically save the data to a database and redirect:\n> ```php\n> public function save()\n> {\n>     $validated = $this->validate([\n>         'title' => 'required|max:255',\n>         'content' => 'required',\n>     ]);\n>\n>     Post::create($validated); // Assumes you have a Post model and database table\n>\n>     return $this->redirect('/posts');\n> }\n> ```\n\n---\n\n## Upgrade Upgrading To 12.0 From 11.x Eloquent Models and UUIDv7\nUpgrade | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/upgrade.md\n\n\n#### Models and UUIDv7\n\n**Likelihood Of Impact: Medium**\n\nThe `HasUuids` trait now returns UUIDs that are compatible with version 7 of the UUID spec (ordered UUIDs). If you would like to continue using ordered UUIDv4 strings for your model's IDs, you should now use the `HasVersion4Uuids` trait:\n\n```php\nuse Illuminate\\Database\\Eloquent\\Concerns\\HasUuids; // [tl! remove]\nuse Illuminate\\Database\\Eloquent\\Concerns\\HasVersion4Uuids as HasUuids; // [tl! add]\n```\n\nThe `HasVersion7Uuids` trait has been removed. If you were previously using this trait, you should use the `HasUuids` trait instead, which now provides the same behavior.\n\n---\n\n## Eloquent Inserting and Updating Models Mass Assignment Mass Assignment and JSON Columns\nEloquent | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/eloquent.md\n\n\n#### Mass Assignment and JSON Columns\n\nWhen assigning JSON columns, each column's mass assignable key must be specified in your model's `$fillable` array. For security, Laravel does not support updating nested JSON attributes when using the `guarded` property:\n\n```php\n/**\n * The attributes that are mass assignable.\n *\n * @var array<int, string>\n */\nprotected $fillable = [\n    'options->enabled',\n];\n```\n\n---\n\n## Forms Extracting input fields to Blade components\nForms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms\n\n## Extracting input fields to Blade components\n\nEven in a small component such as the `post.create` example we've been discussing, we end up duplicating lots of form field boilerplate like validation messages and labels.\n\nIt can be helpful to extract repetitive UI elements such as these into dedicated [Blade components](https://laravel.com/docs/blade#components) to be shared across your application.\n\nFor example, below is the original Blade template from the `post.create` component. We will be extracting the following two text inputs into dedicated Blade components:\n\n```blade\n<form wire:submit=\"save\">\n    <input type=\"text\" wire:model=\"title\"> <!-- [tl! highlight:3] -->\n    <div>\n        @error('title') <span class=\"error\">{{ $message }}</span> @enderror\n    </div>\n\n    <input type=\"text\" wire:model=\"content\"> <!-- [tl! highlight:3] -->\n    <div>\n        @error('content') <span class=\"error\">{{ $message }}</span> @enderror\n    </div>\n\n    <button type=\"submit\">Save</button>\n</form>\n```\n\nHere's what the template will look like after extracting a re-usable Blade component called `<x-input-text>`:\n\n```blade\n<form wire:submit=\"save\">\n    <x-input-text name=\"title\" wire:model=\"title\" /> <!-- [tl! highlight] -->\n\n    <x-input-text name=\"content\" wire:model=\"content\" /> <!-- [tl! highlight] -->\n\n    <button type=\"submit\">Save</button>\n</form>\n```\n\nNext, here's the source for the `x-input-text` component:\n\n```blade\n<!-- resources/views/components/input-text.blade.php -->\n\n@props(['name'])\n\n<input type=\"text\" name=\"{{ $name }}\" {{ $attributes }}>\n\n<div>\n    @error($name) <span class=\"error\">{{ $message }}</span> @enderror\n</div>\n```\n\nAs you can see, we took the repetitive HTML and placed it inside a dedicated Blade component.\n\nFor the most part, the Blade component contains only the extracted HTML from the original component. However, we have added two things:\n\n* The `@props` directive\n* The `{{ $attributes }}` statement on the input\n\nLet's discuss each of these additions:\n\nBy specifying `name` as a \"prop\" using `@props(['name'])` we are telling Blade: if an attribute called \"name\" is set on this component, take its value and make it available inside this component as `$name`.\n\nFor other attributes that don't have an explicit purpose, we used the `{{ $attributes }}` statement. This is used for \"attribute forwarding\", or in other words, taking any HTML attributes written on the Blade component and forwarding them onto an element within the component.\n\nThis ensures `wire:model=\"title\"` and any other extra attributes such as `disabled`, `class=\"...\"`, or `required` still get forwarded to the actual `<input>` element.\n\n### Custom form controls\n\nIn the previous example, we \"wrapped\" an input element into a re-usable Blade component we can use as if it was a native HTML input element.\n\nThis pattern is very useful; however, there might be some cases where you want to create an entire input component from scratch (without an underlying native input element), but still be able to bind its value to Livewire properties using `wire:model`.\n\nFor example, let's imagine you wanted to create an `<x-input-counter />` component that was a simple \"counter\" input written in Alpine.\n\nBefore we create a Blade component, let's first look at a simple, pure-Alpine, \"counter\" component for reference:\n\n```blade\n<div x-data=\"{ count: 0 }\">\n    <button x-on:click=\"count--\">-</button>\n\n    <span x-text=\"count\"></span>\n\n    <button x-on:click=\"count++\">+</button>\n</div>\n```\n\nAs you can see, the component above shows a number alongside two buttons to increment and decrement that number.\n\nNow, let's imagine we want to extract this component into a Blade component called `<x-input-counter />` that we would use within a component like so:\n\n```blade\n<x-input-counter wire:model=\"quantity\" />\n```\n\nCreating this component is mostly simple. We take the HTML of the counter and place it inside a Blade component template like `resources/views/components/input-counter.blade.php`.\n\nHowever, making it work with `wire:model=\"quantity\"` so that you can easily bind data from your Livewire component to the \"count\" inside this Alpine component needs one extra step.\n\nHere's the source for the component:\n\n```blade\n<!-- resources/view/components/input-counter.blade.php -->\n\n<div x-data=\"{ count: 0 }\" x-modelable=\"count\" {{ $attributes}}>\n    <button x-on:click=\"count--\">-</button>\n\n    <span x-text=\"count\"></span>\n\n    <button x-on:click=\"count++\">+</button>\n</div>\n```\n\nAs you can see, the only different bit about this HTML is the `x-modelable=\"count\"` and `{{ $attributes }}`.\n\n`x-modelable` is a utility in Alpine that tells Alpine to make a certain piece of data available for binding from outside. [The Alpine documentation has more information on this directive.](https://alpinejs.dev/directives/modelable)\n\n`{{ $attributes }}`, as we explored earlier, forwards any attributes passed into the Blade component from outside. In this case, the `wire:model` directive will be forwarded.\n\nBecause of `{{ $attributes }}`, when the HTML is rendered in the browser, `wire:model=\"quantity\"` will be rendered alongside `x-modelable=\"count\"` on the root `<div>` of the Alpine component like so:\n\n```blade\n<div x-data=\"{ count: 0 }\" x-modelable=\"count\" wire:model=\"quantity\">\n```\n\n`x-modelable=\"count\"` tells Alpine to look for any `x-model` or `wire:model` statements and use \"count\" as the data to bind them to.\n\nBecause `x-modelable` works for both `wire:model` and `x-model`, you can also use this Blade component interchangeably with Livewire and Alpine. Here’s an example of using this Blade component in a purely Alpine context:\n\n```blade\n<x-input-counter x-model=\"quantity\" />\n```\n\nCreating custom input elements in your application is extremely powerful but requires a deeper understanding of the utilities Livewire and Alpine provide and how they interact with each other.\n\n_[Truncated]_"}]


sed -n '1680,1845p' specs/03-ADMIN-UI.md


sed -n '1,260p' app/Livewire/Admin/Themes/Editor.php && sed -n '1,280p' resources/views/livewire/admin/themes/editor.blade.php


sed -n '1,220p' app/Models/Theme.php && sed -n '1,220p' app/Models/ThemeFile.php && sed -n '1,220p' app/Models/ThemeSettings.php


sed -n '1,260p' database/seeders/ThemeSeeder.php && sed -n '1,260p' database/seeders/ThemeFileSeeder.php && sed -n '1,260p' tests/Feature/Admin/ContentManagementTest.php


sed -n '1680,1845p' specs/03-ADMIN-UI.md

+==================================================================+
| Themes                                                            |
+==================================================================+
|                                                                   |
| +------------------+ +------------------+ +------------------+   |
| | [preview image]  | | [preview image]  | | [preview image]  |   |
| |                  | |                  | |                  |   |
| | My Theme   v1.0  | | Dark Theme v1.2  | | Minimal    v2.0  |   |
| | [Published]      | | [Draft]          | | [Draft]          |   |
| |                  | |                  | |                  |   |
| | [Customize] [...] | | [Customize] [...] | | [Customize] [...] |   |
| +------------------+ +------------------+ +------------------+   |
|                                                                   |
+==================================================================+
```

**Theme grid:** Responsive 3-column grid (1 on mobile, 2 on md, 3 on lg).

Each theme card contains:
- **Thumbnail area:** Aspect-video container with theme preview image or placeholder
- **Info section:**
  - Theme name (heading, size md) and version text
  - Status badge (published=green, draft=zinc)
- **Actions:**
  - "Customize" primary button (links to theme editor)
  - More actions dropdown (triggered by ellipsis icon button):
    - "Preview" menu item
    - "Publish" menu item (hidden if already published)
    - "Duplicate" menu item
    - Separator
    - "Delete" menu item (styled red/danger)

The published theme card has a blue ring border highlight.

### 12.2 Theme Editor

**Route:** `GET /admin/themes/{theme}/editor`

**Livewire component:** `App\Livewire\Admin\Themes\Editor`

**Blade view:** `resources/views/livewire/admin/themes/editor.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `theme` | `Theme` | The theme being edited |
| `sections` | `array` | List of theme sections from settings schema |
| `selectedSection` | `?string` | Currently selected section key |
| `sectionSettings` | `array` | Settings values for the selected section |
| `previewUrl` | `string` | URL for the live preview iframe |

| Action | Behavior |
|--------|----------|
| `mount(Theme $theme)` | Loads theme and its sections |
| `selectSection(string $sectionKey)` | Changes the selected section and loads its settings |
| `updateSetting(string $key, mixed $value)` | Updates a setting value |
| `save()` | Saves all section settings to theme_settings |
| `publish()` | Publishes the theme |
| `refreshPreview()` | Reloads the preview iframe |

#### Layout Wireframe (Full Width, Three-Panel)

```
+==================================================================+
| [<- Back to themes]                     [Save]  [Save & publish] |
+==================================================================+
| SECTIONS   |  LIVE PREVIEW (iframe)       |  SETTINGS PANEL      |
| (264px)    |  (flexible width)            |  (320px)             |
|            |                              |                      |
| Header     |  +------------------------+ |  Header Settings     |
| Hero       |  |                        | |  [separator]         |
| Features*  |  |  Live storefront       | |                      |
| Products   |  |  preview rendered      | |  Logo text:          |
| Footer     |  |  in an iframe          | |  [My Store        ]  |
|            |  |                        | |                      |
|            |  |                        | |  Background color:   |
|            |  |                        | |  [color picker]      |
|            |  |                        | |                      |
|            |  |                        | |  Show search:        |
|            |  |                        | |  [x] checkbox        |
|            |  |                        | |                      |
|            |  +------------------------+ |  Layout:             |
|            |                              |  [Centered       v]  |
+==================================================================+
  * = currently selected section (highlighted)
```

**Full-width layout** (no standard admin shell padding). Three-panel layout:

**Top toolbar:** A horizontal bar with:
- "Back to themes" button (ghost, with left arrow icon) linking to themes index
- "Save" ghost button and "Save and publish" primary button

**Left panel (264px):** Section list
- Heading "Sections"
- Vertical list of section buttons
- Active section gets highlighted background and bold text
- Clicking a section calls `selectSection()`

**Center panel (flexible):** Live preview
- Background color behind the iframe
- Full-height iframe loading the storefront preview URL
- Iframe has rounded border and shadow

**Right panel (320px):** Settings form
- Shows settings for the selected section
- Heading with section label
- Separator
- Dynamic form fields based on section field definitions:

| Field Type | UI Element |
|-----------|------------|
| `text` | Flux input (live-debounced at 500ms) |
| `textarea` | Flux textarea (live-debounced at 500ms, 3 rows) |
| `color` | Native color picker input |
| `select` | Flux select with options from field definition |
| `checkbox` | Flux checkbox |

- When no section is selected: centered text "Select a section to edit its settings."

---

## 13. Pages Management

### 13.1 Pages List

**Route:** `GET /admin/pages`

**Livewire component:** `App\Livewire\Admin\Pages\Index`

**Blade view:** `resources/views/livewire/admin/pages/index.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `search` | `string` | Search by page title |

| Computed Property | Purpose |
|-------------------|---------|
| `pages` | Paginated pages for the current store |

#### Layout

- Heading "Pages" with "Add page" primary button
- Search input
- Table columns: Title (link to edit), Handle, Status (Flux badge), Updated date
- Pagination

### 13.2 Page Form

**Route:** `GET /admin/pages/create` and `GET /admin/pages/{page}/edit`

**Livewire component:** `App\Livewire\Admin\Pages\Form`

**Blade view:** `resources/views/livewire/admin/pages/form.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `page` | `?Page` | Page being edited |
| `title` | `string` | Page title |
| `handle` | `string` | URL handle |
| `bodyHtml` | `string` | Page body content (HTML) |


sed -n '1,260p' app/Livewire/Admin/Themes/Editor.php && sed -n '1,280p' resources/views/livewire/admin/themes/editor.blade.php

editor.blade.php

<?php

namespace App\Livewire\Admin\Themes;

use App\Enums\ThemeStatus;
use App\Models\Store;
use App\Models\Theme;
use App\Models\ThemeSettings;
use App\Services\ThemeSettingsService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\DB;
use Livewire\Component;

class Editor extends Component
{
    use AuthorizesRequests;

    public Theme $theme;

    public string $selectedSection = 'announcement';

    /**
     * @var array<string, mixed>
     */
    public array $settings = [];

    public function mount(Theme $theme, ThemeSettingsService $themeSettings): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);
        abort_unless((int) $theme->store_id === $store->getKey(), 404);

        $this->authorize('update', $theme);

        $this->theme = $theme;
        $this->settings = array_replace_recursive(
            $themeSettings->defaultsForStore($store),
            $theme->settings?->settings_json ?? [],
        );
    }

    public function selectSection(string $sectionKey): void
    {
        abort_unless(array_key_exists($sectionKey, $this->sections()), 404);

        $this->selectedSection = $sectionKey;
    }

    public function save(ThemeSettingsService $settings): void
    {
        $this->authorize('update', $this->theme);

        ThemeSettings::withoutGlobalScopes()->updateOrCreate(
            ['theme_id' => $this->theme->getKey()],
            [
                'settings_json' => $this->settings,
                'updated_at' => now(),
            ],
        );

        $settings->forget($this->store());

        session()->flash('status', 'Theme saved');
        $this->dispatch('toast', type: 'success', message: __('Theme saved'));
    }

    public function publish(ThemeSettingsService $settings): void
    {
        $this->authorize('publish', $this->theme);

        DB::transaction(function (): void {
            Theme::withoutGlobalScopes()
                ->where('store_id', $this->theme->store_id)
                ->update([
                    'status' => ThemeStatus::Draft,
                    'published_at' => null,
                ]);

            $this->theme->forceFill([
                'status' => ThemeStatus::Published,
                'published_at' => now(),
            ])->save();
        });

        $this->save($settings);

        session()->flash('status', 'Theme saved and published');
    }

    public function refreshPreview(): void
    {
        $this->dispatch('theme-preview-refresh');
    }

    /**
     * @return array<string, array{label: string, fields: array<int, array{key: string, label: string, type: string, options?: array<string, string>}>}>
     */
    public function sections(): array
    {
        return [
            'announcement' => [
                'label' => 'Announcement',
                'fields' => [
                    ['key' => 'announcement.enabled', 'label' => 'Enabled', 'type' => 'checkbox'],
                    ['key' => 'announcement.text', 'label' => 'Text', 'type' => 'text'],
                    ['key' => 'announcement.url', 'label' => 'URL', 'type' => 'text'],
                ],
            ],
            'header' => [
                'label' => 'Header',
                'fields' => [
                    ['key' => 'header.sticky', 'label' => 'Sticky header', 'type' => 'checkbox'],
                    ['key' => 'header.main_menu', 'label' => 'Main menu handle', 'type' => 'text'],
                ],
            ],
            'home' => [
                'label' => 'Home hero',
                'fields' => [
                    ['key' => 'home.hero.eyebrow', 'label' => 'Eyebrow', 'type' => 'text'],
                    ['key' => 'home.hero.heading', 'label' => 'Heading', 'type' => 'text'],
                    ['key' => 'home.hero.subheading', 'label' => 'Subheading', 'type' => 'textarea'],
                    ['key' => 'home.hero.primary_label', 'label' => 'Primary button label', 'type' => 'text'],
                    ['key' => 'home.hero.primary_url', 'label' => 'Primary button URL', 'type' => 'text'],
                    ['key' => 'home.featured_product_limit', 'label' => 'Featured products', 'type' => 'number'],
                    ['key' => 'home.featured_collection_limit', 'label' => 'Featured collections', 'type' => 'number'],
                ],
            ],
            'footer' => [
                'label' => 'Footer',
                'fields' => [
                    ['key' => 'footer.menu', 'label' => 'Menu handle', 'type' => 'text'],
                    ['key' => 'footer.tagline', 'label' => 'Tagline', 'type' => 'textarea'],
                ],
            ],
        ];
    }

    public function previewUrl(): string
    {
        return route('home');
    }

    public function render(): mixed
    {
        return view('livewire.admin.themes.editor', [
            'sections' => $this->sections(),
            'activeSection' => $this->sections()[$this->selectedSection],
            'previewUrl' => $this->previewUrl(),
        ])->layout('layouts.app', [
            'title' => __('Theme editor'),
        ]);
    }

    private function store(): Store
    {
        return Store::query()->whereKey($this->theme->store_id)->firstOrFail();
    }
}

        <flux:button :href="route('admin.themes.index')" wire:navigate variant="filled" icon="arrow-left">Themes</flux:button>

            <flux:button type="button" wire:click="save" variant="filled">Save</flux:button>
            <flux:button type="button" wire:click="publish" variant="primary">Save and publish</flux:button>

    @if (session('status'))
        <flux:callout color="green" icon="check-circle">{{ session('status') }}</flux:callout>
    @endif

            <flux:heading size="lg">Sections</flux:heading>

                @foreach ($sections as $key => $section)
                    <button
                        type="button"
                        wire:click="selectSection('{{ $key }}')"
                        class="block w-full rounded-lg px-3 py-2 text-left text-sm {{ $selectedSection === $key ? 'bg-blue-50 font-medium text-blue-950 dark:bg-blue-950/40 dark:text-blue-100' : 'text-zinc-700 hover:bg-zinc-50 dark:text-zinc-200 dark:hover:bg-zinc-800' }}"
                    >
                        {{ $section['label'] }}
                    </button>
                @endforeach

                    <flux:heading size="lg">Preview</flux:heading>
                    <flux:text>{{ $theme->name }}</flux:text>

                <flux:button type="button" wire:click="refreshPreview" variant="filled" icon="arrow-path">Refresh</flux:button>

        <form wire:submit="save" class="rounded-lg border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900">
            <flux:heading size="lg">{{ $activeSection['label'] }}</flux:heading>

                @foreach ($activeSection['fields'] as $field)
                    @php($model = 'settings.'.str_replace('.', '.', $field['key']))

                    @if ($field['type'] === 'checkbox')
                        <flux:checkbox wire:model="{{ $model }}" label="{{ $field['label'] }}" />
                    @elseif ($field['type'] === 'textarea')
                        <flux:textarea wire:model.live.debounce.500ms="{{ $model }}" label="{{ $field['label'] }}" rows="3" />
                    @elseif ($field['type'] === 'number')
                        <flux:input wire:model.live.debounce.500ms="{{ $model }}" type="number" min="0" label="{{ $field['label'] }}" />
                    @else
                        <flux:input wire:model.live.debounce.500ms="{{ $model }}" label="{{ $field['label'] }}" />
                    @endif
                @endforeach

        </form>


Output:
Chunk ID: f882d8
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1636
Output:
+==================================================================+
| Themes                                                            |
+==================================================================+
|                                                                   |
| +------------------+ +------------------+ +------------------+   |
| | [preview image]  | | [preview image]  | | [preview image]  |   |
| |                  | |                  | |                  |   |
| | My Theme   v1.0  | | Dark Theme v1.2  | | Minimal    v2.0  |   |
| | [Published]      | | [Draft]          | | [Draft]          |   |
| |                  | |                  | |                  |   |
| | [Customize] [...] | | [Customize] [...] | | [Customize] [...] |   |
| +------------------+ +------------------+ +------------------+   |
|                                                                   |
+==================================================================+
```

**Theme grid:** Responsive 3-column grid (1 on mobile, 2 on md, 3 on lg).

Each theme card contains:
- **Thumbnail area:** Aspect-video container with theme preview image or placeholder
- **Info section:**
  - Theme name (heading, size md) and version text
  - Status badge (published=green, draft=zinc)
- **Actions:**
  - "Customize" primary button (links to theme editor)
  - More actions dropdown (triggered by ellipsis icon button):
    - "Preview" menu item
    - "Publish" menu item (hidden if already published)
    - "Duplicate" menu item
    - Separator
    - "Delete" menu item (styled red/danger)

The published theme card has a blue ring border highlight.

### 12.2 Theme Editor

**Route:** `GET /admin/themes/{theme}/editor`

**Livewire component:** `App\Livewire\Admin\Themes\Editor`

**Blade view:** `resources/views/livewire/admin/themes/editor.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `theme` | `Theme` | The theme being edited |
| `sections` | `array` | List of theme sections from settings schema |
| `selectedSection` | `?string` | Currently selected section key |
| `sectionSettings` | `array` | Settings values for the selected section |
| `previewUrl` | `string` | URL for the live preview iframe |

| Action | Behavior |
|--------|----------|
| `mount(Theme $theme)` | Loads theme and its sections |
| `selectSection(string $sectionKey)` | Changes the selected section and loads its settings |
| `updateSetting(string $key, mixed $value)` | Updates a setting value |
| `save()` | Saves all section settings to theme_settings |
| `publish()` | Publishes the theme |
| `refreshPreview()` | Reloads the preview iframe |

#### Layout Wireframe (Full Width, Three-Panel)

```
+==================================================================+
| [<- Back to themes]                     [Save]  [Save & publish] |
+==================================================================+
| SECTIONS   |  LIVE PREVIEW (iframe)       |  SETTINGS PANEL      |
| (264px)    |  (flexible width)            |  (320px)             |
|            |                              |                      |
| Header     |  +------------------------+ |  Header Settings     |
| Hero       |  |                        | |  [separator]         |
| Features*  |  |  Live storefront       | |                      |
| Products   |  |  preview rendered      | |  Logo text:          |
| Footer     |  |  in an iframe          | |  [My Store        ]  |
|            |  |                        | |                      |
|            |  |                        | |  Background color:   |
|            |  |                        | |  [color picker]      |
|            |  |                        | |                      |
|            |  |                        | |  Show search:        |
|            |  |                        | |  [x] checkbox        |
|            |  |                        | |                      |
|            |  +------------------------+ |  Layout:             |
|            |                              |  [Centered       v]  |
+==================================================================+
  * = currently selected section (highlighted)
```

**Full-width layout** (no standard admin shell padding). Three-panel layout:

**Top toolbar:** A horizontal bar with:
- "Back to themes" button (ghost, with left arrow icon) linking to themes index
- "Save" ghost button and "Save and publish" primary button

**Left panel (264px):** Section list
- Heading "Sections"
- Vertical list of section buttons
- Active section gets highlighted background and bold text
- Clicking a section calls `selectSection()`

**Center panel (flexible):** Live preview
- Background color behind the iframe
- Full-height iframe loading the storefront preview URL
- Iframe has rounded border and shadow

**Right panel (320px):** Settings form
- Shows settings for the selected section
- Heading with section label
- Separator
- Dynamic form fields based on section field definitions:

| Field Type | UI Element |
|-----------|------------|
| `text` | Flux input (live-debounced at 500ms) |
| `textarea` | Flux textarea (live-debounced at 500ms, 3 rows) |
| `color` | Native color picker input |
| `select` | Flux select with options from field definition |
| `checkbox` | Flux checkbox |

- When no section is selected: centered text "Select a section to edit its settings."

---

## 13. Pages Management

### 13.1 Pages List

**Route:** `GET /admin/pages`

**Livewire component:** `App\Livewire\Admin\Pages\Index`

**Blade view:** `resources/views/livewire/admin/pages/index.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `search` | `string` | Search by page title |

| Computed Property | Purpose |
|-------------------|---------|
| `pages` | Paginated pages for the current store |

#### Layout

- Heading "Pages" with "Add page" primary button
- Search input
- Table columns: Title (link to edit), Handle, Status (Flux badge), Updated date
- Pagination

### 13.2 Page Form

**Route:** `GET /admin/pages/create` and `GET /admin/pages/{page}/edit`

**Livewire component:** `App\Livewire\Admin\Pages\Form`

**Blade view:** `resources/views/livewire/admin/pages/form.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `page` | `?Page` | Page being edited |
| `title` | `string` | Page title |
| `handle` | `string` | URL handle |
| `bodyHtml` | `string` | Page body content (HTML) |


Output:
Chunk ID: f4e90d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2230
Output:
<?php

namespace App\Livewire\Admin\Themes;

use App\Enums\ThemeStatus;
use App\Models\Store;
use App\Models\Theme;
use App\Models\ThemeSettings;
use App\Services\ThemeSettingsService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\DB;
use Livewire\Component;

class Editor extends Component
{
    use AuthorizesRequests;

    public Theme $theme;

    public string $selectedSection = 'announcement';

    /**
     * @var array<string, mixed>
     */
    public array $settings = [];

    public function mount(Theme $theme, ThemeSettingsService $themeSettings): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);
        abort_unless((int) $theme->store_id === $store->getKey(), 404);

        $this->authorize('update', $theme);

        $this->theme = $theme;
        $this->settings = array_replace_recursive(
            $themeSettings->defaultsForStore($store),
            $theme->settings?->settings_json ?? [],
        );
    }

    public function selectSection(string $sectionKey): void
    {
        abort_unless(array_key_exists($sectionKey, $this->sections()), 404);

        $this->selectedSection = $sectionKey;
    }

    public function save(ThemeSettingsService $settings): void
    {
        $this->authorize('update', $this->theme);

        ThemeSettings::withoutGlobalScopes()->updateOrCreate(
            ['theme_id' => $this->theme->getKey()],
            [
                'settings_json' => $this->settings,
                'updated_at' => now(),
            ],
        );

        $settings->forget($this->store());

        session()->flash('status', 'Theme saved');
        $this->dispatch('toast', type: 'success', message: __('Theme saved'));
    }

    public function publish(ThemeSettingsService $settings): void
    {
        $this->authorize('publish', $this->theme);

        DB::transaction(function (): void {
            Theme::withoutGlobalScopes()
                ->where('store_id', $this->theme->store_id)
                ->update([
                    'status' => ThemeStatus::Draft,
                    'published_at' => null,
                ]);

            $this->theme->forceFill([
                'status' => ThemeStatus::Published,
                'published_at' => now(),
            ])->save();
        });

        $this->save($settings);

        session()->flash('status', 'Theme saved and published');
    }

    public function refreshPreview(): void
    {
        $this->dispatch('theme-preview-refresh');
    }

    /**
     * @return array<string, array{label: string, fields: array<int, array{key: string, label: string, type: string, options?: array<string, string>}>}>
     */
    public function sections(): array
    {
        return [
            'announcement' => [
                'label' => 'Announcement',
                'fields' => [
                    ['key' => 'announcement.enabled', 'label' => 'Enabled', 'type' => 'checkbox'],
                    ['key' => 'announcement.text', 'label' => 'Text', 'type' => 'text'],
                    ['key' => 'announcement.url', 'label' => 'URL', 'type' => 'text'],
                ],
            ],
            'header' => [
                'label' => 'Header',
                'fields' => [
                    ['key' => 'header.sticky', 'label' => 'Sticky header', 'type' => 'checkbox'],
                    ['key' => 'header.main_menu', 'label' => 'Main menu handle', 'type' => 'text'],
                ],
            ],
            'home' => [
                'label' => 'Home hero',
                'fields' => [
                    ['key' => 'home.hero.eyebrow', 'label' => 'Eyebrow', 'type' => 'text'],
                    ['key' => 'home.hero.heading', 'label' => 'Heading', 'type' => 'text'],
                    ['key' => 'home.hero.subheading', 'label' => 'Subheading', 'type' => 'textarea'],
                    ['key' => 'home.hero.primary_label', 'label' => 'Primary button label', 'type' => 'text'],
                    ['key' => 'home.hero.primary_url', 'label' => 'Primary button URL', 'type' => 'text'],
                    ['key' => 'home.featured_product_limit', 'label' => 'Featured products', 'type' => 'number'],
                    ['key' => 'home.featured_collection_limit', 'label' => 'Featured collections', 'type' => 'number'],
                ],
            ],
            'footer' => [
                'label' => 'Footer',
                'fields' => [
                    ['key' => 'footer.menu', 'label' => 'Menu handle', 'type' => 'text'],
                    ['key' => 'footer.tagline', 'label' => 'Tagline', 'type' => 'textarea'],
                ],
            ],
        ];
    }

    public function previewUrl(): string
    {
        return route('home');
    }

    public function render(): mixed
    {
        return view('livewire.admin.themes.editor', [
            'sections' => $this->sections(),
            'activeSection' => $this->sections()[$this->selectedSection],
            'previewUrl' => $this->previewUrl(),
        ])->layout('layouts.app', [
            'title' => __('Theme editor'),
        ]);
    }

    private function store(): Store
    {
        return Store::query()->whereKey($this->theme->store_id)->firstOrFail();
    }
}

        <flux:button :href="route('admin.themes.index')" wire:navigate variant="filled" icon="arrow-left">Themes</flux:button>

            <flux:button type="button" wire:click="save" variant="filled">Save</flux:button>
            <flux:button type="button" wire:click="publish" variant="primary">Save and publish</flux:button>

    @if (session('status'))
        <flux:callout color="green" icon="check-circle">{{ session('status') }}</flux:callout>
    @endif

            <flux:heading size="lg">Sections</flux:heading>

                @foreach ($sections as $key => $section)
                    <button
                        type="button"
                        wire:click="selectSection('{{ $key }}')"
                        class="block w-full rounded-lg px-3 py-2 text-left text-sm {{ $selectedSection === $key ? 'bg-blue-50 font-medium text-blue-950 dark:bg-blue-950/40 dark:text-blue-100' : 'text-zinc-700 hover:bg-zinc-50 dark:text-zinc-200 dark:hover:bg-zinc-800' }}"
                    >
                        {{ $section['label'] }}
                    </button>
                @endforeach

                    <flux:heading size="lg">Preview</flux:heading>
                    <flux:text>{{ $theme->name }}</flux:text>

                <flux:button type="button" wire:click="refreshPreview" variant="filled" icon="arrow-path">Refresh</flux:button>

        <form wire:submit="save" class="rounded-lg border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900">
            <flux:heading size="lg">{{ $activeSection['label'] }}</flux:heading>

                @foreach ($activeSection['fields'] as $field)
                    @php($model = 'settings.'.str_replace('.', '.', $field['key']))

                    @if ($field['type'] === 'checkbox')
                        <flux:checkbox wire:model="{{ $model }}" label="{{ $field['label'] }}" />
                    @elseif ($field['type'] === 'textarea')
                        <flux:textarea wire:model.live.debounce.500ms="{{ $model }}" label="{{ $field['label'] }}" rows="3" />
                    @elseif ($field['type'] === 'number')
                        <flux:input wire:model.live.debounce.500ms="{{ $model }}" type="number" min="0" label="{{ $field['label'] }}" />
                    @else
                        <flux:input wire:model.live.debounce.500ms="{{ $model }}" label="{{ $field['label'] }}" />
                    @endif
                @endforeach

        </form>