server: laravel-boost

tool: search-docs

type: text

text:
# Search Results

## 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');
> }
> ```

---

## Validation Validating Arrays Validating Nested Array Input
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

### Validating Nested Array Input

Validating nested array-based form input fields doesn't have to be a pain. You may use "dot notation" to validate attributes within an array. For example, if the incoming HTTP request contains a `photos[profile]` field, you may validate it like so:

```php
use Illuminate\Support\Facades\Validator;

$validator = Validator::make($request->all(), [
    'photos.profile' => 'required|image',
]);
```

You may also validate each element of an array. For example, to validate that each email in a given array input field is unique, you may do the following:

```php
$validator = Validator::make($request->all(), [
    'users.*.email' => 'email|unique:users',
    'users.*.first_name' => 'required_with:users.*.last_name',
]);
```

Likewise, you may use the `*` character when specifying [custom validation messages in your language files](#custom-messages-for-specific-attributes), making it a breeze to use a single validation message for array-based fields:

```php
'custom' => [
    'users.*.email' => [
        'unique' => 'Each user must have a unique email address',
    ]
],
```

---

## Browser Testing check
Browser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md

### check

The `check` method checks the given checkbox:

```php
$page->check('terms');
$page->check('color', 'blue'); // For checkbox with specific value
```

---

## Lazy Full-page lazy loading
Lazy | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/lazy

## Full-page lazy loading

You can lazy load or defer full-page Livewire components using route methods.

### Lazy loading full pages

Use `->lazy()` to load the component when it enters the viewport:

```php
Route::livewire('/dashboard', 'pages::dashboard')->lazy();
```

### Deferring full pages

Use `->defer()` to load the component immediately after the page loads:

```php
Route::livewire('/dashboard', 'pages::dashboard')->defer();
```

### Disabling lazy/defer loading

If a component is lazy or deferred by default (via the `#[Lazy]` or `#[Defer]` attribute), you can opt-out using `enabled: false`:

```php
Route::livewire('/dashboard', 'pages::dashboard')->lazy(enabled: false);
Route::livewire('/dashboard', 'pages::dashboard')->defer(enabled: false);
```

---

## Redirecting Redirecting to full-page components
Redirecting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/redirecting

## Redirecting to full-page components

Because Livewire uses Laravel's built-in redirection feature, you can use all of the redirection methods available to you in a typical Laravel application.

For example, if you are using a Livewire component as a full-page component for a route like so:

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

You can redirect to it simply by using the route path:

```php
public function save()
{
    // ...

    $this->redirect('/posts');
}
```

---

## Attribute Title Only for full-page components
Attribute Title | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-title

## Only for full-page components

> [!info] Full-page components only
> The `#[Title]` attribute only works for full-page components accessed via routes. Regular components rendered within other views don't use titles—they inherit the parent page's title.

---

## Attribute Layout Learn more
Attribute Layout | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-layout

## Learn more

For more information about full-page components and layouts, see the [Pages documentation](/docs/4.x/pages#layouts).

---

## 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>
```

---

## Javascript Reference
Javascript | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/javascript

## Reference

When extending Livewire's JavaScript system, it's important to understand the different objects you might encounter.

Here is an exhaustive reference of each of Livewire's relevant internal properties.

As a reminder, the average Livewire user may never interact with these. Most of these objects are available for Livewire's internal system or advanced users.

### The `$wire` object

Given the following generic `Counter` component:

```php
<?php

namespace App\Livewire;

use Livewire\Component;

class Counter extends Component
{
    public $count = 1;

    public function increment()
    {
        $this->count++;
    }

    public function render()
    {
        return view('livewire.counter');
    }
}
```

Livewire exposes a JavaScript representation of the server-side component in the form of an object that is commonly referred to as `$wire`:

```js
let $wire = {
    // All component public properties are directly accessible on $wire...
    count: 0,

    // All public methods are exposed and callable on $wire...
    increment() { ... },

    // Access the `$wire` object of the parent component if one exists...
    $parent,

    // Access the root DOM element of the Livewire component...
    $el,

    // Access the ID of the current Livewire component...
    $id,

    // Get the value of a property by name...
    // Usage: $wire.$get('count')
    $get(name) { ... },

    // Set a property on the component by name...
    // Usage: $wire.$set('count', 5)
    $set(name, value, live = true) { ... },

    // Toggle the value of a boolean property...
    $toggle(name, live = true) { ... },

    // Call the method...
    // Usage: $wire.$call('increment')
    $call(method, ...params) { ... },

    // Define a JavaScript action...
    // Usage: $wire.$js('increment', () => { ... })
    // Usage: $wire.$js.increment = () => { ... }
    $js(name, callback) { ... },

    // [DEPRECATED] Entangle - You probably don't need this.
    // Use $wire directly to access properties instead.
    // Usage: <div x-data="{ count: $wire.$entangle('count') }">
    $entangle(name, live = false) { ... },

    // Watch the value of a property for changes...
    // Usage: Alpine.$watch('count', (value, old) => { ... })
    $watch(name, callback) { ... },

    // Scope the next action to a named island...
    // Returns $wire so you can chain any method call.
    // The chained action will only re-render the named island.
    // Usage: $wire.$island('revenue').$refresh()
    // Usage: $wire.$island('feed', { mode: 'append' }).loadMore()
    $island(name, options = {}) { ... },

    // Refresh a component by sending a message to the server
    // to re-render the HTML and swap it into the page...
    $refresh() { ... },

    // Identical to the above `$refresh`. Just a more technical name...
    $commit() { ... }, // Alias for $refresh()

    // Listen for a an event dispatched from this component or its children...
    // Usage: $wire.$on('post-created', () => { ... })
    $on(event, callback) { ... },

    // Listen for a lifecycle hook triggered from this component or the request...
    // Usage: $wire.$hook('message.sent', () => { ... })
    $hook(name, callback) { ... },

    // Dispatch an event from this component...
    // Usage: $wire.$dispatch('post-created', { postId: 2 })
    $dispatch(event, params = {}) { ... },

    // Dispatch an event onto another component...
    // Usage: $wire.$dispatchTo('dashboard', 'post-created', { postId: 2 })
    $dispatchTo(otherComponentName, event, params = {}) { ... },

    // Dispatch an event onto this component and no others...
    $dispatchSelf(event, params = {}) { ... },

    // A JS API to upload a file directly to component
    // rather than through `wire:model`...
    $upload(
        name, // The property name
        file, // The File JavaScript object
        finish = () => { ... }, // Runs when the upload is finished...
        error = () => { ... }, // Runs if an error is triggered mid-upload...
        progress = (event) => { // Runs as the upload progresses...
            event.detail.progress // An integer from 1-100...
        },
    ) { ... },

    // API to upload multiple files at the same time...
    $uploadMultiple(name, files, finish, error, progress) { },

    // Remove an upload after it's been temporarily uploaded but not saved...
    $removeUpload(name, tmpFilename, finish, error) { ... },

    // Register an action interceptor for this component instance
    // Usage: $wire.intercept(({ action, onSend, onCancel, onSuccess, onError, onFailure, onFinish }) => { ... })
    // Or scope to specific action: $wire.intercept('save', ({ action, onSuccess }) => { ... })
    intercept(actionOrCallback, callback) { ... },

    // Alias for intercept
    interceptAction(actionOrCallback, callback) { ... },

    // Register a message interceptor for this component instance
    // Usage: $wire.interceptMessage(({ message, cancel, onSend, onCancel, onSuccess, onSkipped, onError, onFailure, onFinish }) => { ... })
    // Or scope to specific action: $wire.interceptMessage('save', callback)
    interceptMessage(actionOrCallback, callback) { ... },

    // Register a request interceptor for this component instance
    // Usage: $wire.interceptRequest(({ request, onSend, onCancel, onSuccess, onError, onFailure, onFinish }) => { ... })
    // Or scope to specific action: $wire.interceptRequest('save', callback)
    interceptRequest(actionOrCallback, callback) { ... },

    // Retrieve the underlying "component" object...
    __instance() { ... },
}
```

You can learn more about `$wire` in [Livewire's documentation on accessing properties in JavaScript](/docs/4.x/properties#accessing-properties-from-javascript).

### The `snapshot` object

Between each network request, Livewire serializes the PHP component into an object that can be consumed in JavaScript. This snapshot is used to unserialize the component back into a PHP object and therefore has mechanisms built in to prevent tampering:

```js
let snapshot = {
    // The serialized state of the component (public properties)...
    data: { count: 0 },

    // Long-standing information about the component...
    memo: {
        // The component's unique ID...
        id: '0qCY3ri9pzSSMIXPGg8F',

        // The component's name. Ex. <livewire:[name] />
        name: 'counter',

        // The URI, method, and locale of the web page that the
        // component was originally loaded on. This is used
        // to re-apply any middleware from the original request
        // to subsequent component update requests (messages)...
        path: '/',
        method: 'GET',
        locale: 'en',

        // A list of any nested "child" components. Keyed by
        // internal template ID with the component ID as the values...
        children: [],

        // Whether or not this component was "lazy loaded"...
        lazyLoaded: false,

        // A list of any validation errors thrown during the
        // last request...
        errors: [],
    },

    // A securely encrypted hash of this snapshot. This way,
    // if a malicious user tampers with the snapshot with
    // the goal of accessing un-owned resources on the server,
    // the checksum validation will fail and an error will
    // be thrown...
    checksum: '1bc274eea17a434e33d26bcaba4a247a4a7768bd286456a83ea6e9be2d18c1e7',
}
```

### The `component` object

Every component on a page has a corresponding component object behind the scenes keeping track of its state and exposing its underlying functionality. This is one layer deeper than `$wire`. It is only meant for advanced usage.

Here's an actual component object for the above `Counter` component with descriptions of relevant properties in JS comments:

```js
let component = {
    // The root HTML element of the component...
    el: HTMLElement,

    // The unique ID of the component...
    id: '0qCY3ri9pzSSMIXPGg8F',

    // The component's "name" (<livewire:[name] />)...
    name: 'counter',

    // The latest "effects" object. Effects are "side-effects" from server
    // round-trips. These include redirects, file downloads, etc...
    effects: {},

    // The component's last-known server-side state...
    canonical: { count: 0 },

    // The component's mutable data object representing its
    // live client-side state...
    ephemeral: { count: 0 },

    // A reactive version of `this.ephemeral`. Changes to
    // this object will be picked up by AlpineJS expressions...
    reactive: Proxy,

    // A Proxy object that is typically used inside Alpine
    // expressions as `$wire`. This is meant to provide a
    // friendly JS object interface for Livewire components...
    $wire: Proxy,

    // A list of any nested "child" components. Keyed by
    // internal template ID with the component ID as the values...
    children: [],

    // The last-known "snapshot" representation of this component.
    // Snapshots are taken from the server-side component and used
    // to re-create the PHP object on the backend...
    snapshot: {...},

    // The un-parsed version of the above snapshot. This is used to send back to the
    // server on the next roundtrip because JS parsing messes with PHP encoding
    // which often results in checksum mis-matches.
    snapshotEncoded: '{"data":{"count":0},"memo":{"id":"0qCY3ri9pzSSMIXPGg8F","name":"counter","path":"\/","method":"GET","children":[],"lazyLoaded":true,"errors":[],"locale":"en"},"checksum":"1bc274eea17a434e33d26bcaba4a247a4a7768bd286456a83ea6e9be2d18c1e7"}',
}
```

### The `message` payload

When an action is performed on a Livewire component in the browser, a network request is triggered. That network request contains one or many components and various instructions for the server. Internally, these component network payloads are called "messages".

A "message" represents the data sent from the frontend to the backend when a component needs to update. A component is rendered and manipulated on the frontend until an action is performed that requires it to send a message with its state and updates to the backend.

You will recognize this schema from the payload in the network tab of your browser's DevTools, or [Livewire's JavaScript hooks](#javascript-hooks):

```js
let message = {
    // Snapshot object...
    snapshot: { ... },

    // A key-value pair list of properties
    // to update on the server...
    updates: {},

    // An array of methods (with parameters) to call server-side...
    calls: [
        { method: 'increment', params: [] },
    ],
}
```

---

## 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).

---

## Validation Form objects
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## Form objects

As more properties and validation rules are added to a Livewire component, it can begin to feel too crowded. To alleviate this pain and also provide a helpful abstraction for code reuse, you can use Livewire's *Form Objects* to store your properties and validation rules.

Below is the same `CreatePost` example, but now the properties and rules have been extracted to a dedicated form object named `PostForm`:

```php
<?php

namespace App\Livewire\Forms;

use Livewire\Attributes\Validate;
use Livewire\Form;

class PostForm extends Form
{
    #[Validate('required|min:3')]
	public $title = '';

    #[Validate('required|min:3')]
    public $content = '';
}
```

The `PostForm` above can now be defined as a property on the `CreatePost` component:

```php
<?php

namespace App\Livewire;

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

class CreatePost extends Component
{
    public PostForm $form;

    public function save()
    {
		Post::create(
    		$this->form->all()
    	);

		return redirect()->to('/posts');
    }

    // ...
}
```

As you can see, instead of listing out each property individually, we can retrieve all the property values using the `->all()` method on the form object.

Also, when referencing the property names in the template, you must prepend `form.` to each instance:

```blade
<form wire:submit="save">
	<input type="text" wire:model="form.title">
    <div>@error('form.title') {{ $message }} @enderror</div>

	<textarea wire:model="form.content"></textarea>
    <div>@error('form.content') {{ $message }} @enderror</div>

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

When using form objects, `#[Validate]` attribute validation will be run every time a property is updated. However, if you disable this behavior by specifying `onUpdate: false` on the attribute, you can manually run a form object's validation using `$this->form->validate()`:

```php
public function save()
{
    Post::create(
        $this->form->validate()
    );

    return redirect()->to('/posts');
}
```

Form objects are a useful abstraction for most larger datasets and a variety of additional features that make them even more powerful. For more information, check out the comprehensive [form object documentation](/docs/4.x/forms#extracting-a-form-object).

---

## Wire Confirm Prompting users for input
Wire Confirm | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-confirm

## Prompting users for input

For even more dangerous actions such as deleting a user's account entirely, you may want to present them with a confirmation prompt which they would need to type in a specific string of characters to confirm the action.

Livewire provides a helpful `.prompt` modifier, that when applied to `wire:confirm`, it will prompt the user for input and only confirm the action if the input matches (case-sensitive) the provided string (designated by a "|" (pipe) character at the end if the `wire:confirm` value):

```blade
<button
    type="button"
    wire:click="delete"
    wire:confirm.prompt="Are you sure?\n\nType DELETE to confirm|DELETE"
>
    Delete account <!-- [tl! highlight:-2,1] -->
</button>
```

When a user presses "Delete account", the action will only be performed if "DELETE" is entered into the prompt, otherwise, the action will be cancelled.

---

## Components Button > Full width
Components Button | livewire/flux@2.x | Source: https://fluxui.dev/components/button#livewire/flux

Full width

A button that spans the full width of the container.

<flux:button variant="primary" class="w-full">Send invite</flux:button>

---

## Attribute Title Learn more
Attribute Title | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-title

## Learn more

For more information about full-page components, layouts, and routing, see the [Pages documentation](/docs/4.x/pages#setting-a-page-title).

---

## Understanding Nesting Performance implications
Understanding Nesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/understanding-nesting

## Performance implications

Livewire's independent component architecture can have both positive and negative implications for your application.

An advantage of this architecture is it allows you to isolate expensive portions of your application. For example, you can quarantine a slow database query to its own independent component, and its performance overhead won't impact the rest of the page.

However, the biggest drawback of this approach is that because components are entirely separate, inter-component communication/dependencies becomes more difficult.

For example, if you had a property passed down from the above parent `Posts` component to the nested `ShowPost` component, it wouldn't be "reactive". Because each component is independent, if a request to the parent component changed the value of the property being passed into `ShowPost`, it wouldn't update inside `ShowPost`.

Livewire has overcome a number of these hurdles and provides dedicated APIs for these scenarios like: [Reactive properties](/docs/4.x/nesting#reactive-props), [Modelable components](/docs/4.x/nesting#binding-to-child-data-using-wiremodel), and [the `$parent` object](/docs/4.x/nesting#directly-accessing-the-parent-from-the-child).

Armed with this knowledge of how nested Livewire components operate, you will be able to make more informed decisions about when and how to nest components within your application.

---

## Forms Throttling input
Forms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms

## Throttling input

As stated previously, when an input debounce is applied to a field, a network request will not be sent until the user has stopped typing for a certain amount of time. This means if the user continues typing a long message, a network request won't be sent until the user is finished.

Sometimes this isn't the desired behavior, and you would rather send a request as the user types, not when they've finished or taken a break.

In these cases, you can instead use `.throttle` to signify a time interval to send network requests:

```blade
<input type="text" wire:model.live.throttle.150ms="title" >
```

In the above example, as a user is typing continuously in the "title" field, a network request will be sent every 150 milliseconds until the user is finished.

---

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

### Preparing Input for Validation

If you need to prepare or sanitize any data from the request before you apply your validation rules, you may use the `prepareForValidation` method:

```php
use Illuminate\Support\Str;

/**
 * Prepare the data for validation.
 */
protected function prepareForValidation(): void
{
    $this->merge([
        'slug' => Str::slug($this->slug),
    ]);
}
```

Likewise, if you need to normalize any request data after validation is complete, you may use the `passedValidation` method:

```php
/**
 * Handle a passed validation attempt.
 */
protected function passedValidation(): void
{
    $this->replace(['name' => 'Taylor']);
}
```

_[Truncated]_


output:
Wall time: 4.0899 seconds
Output:
[{"type":"text","text":"# Search Results\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## Validation Validating Arrays Validating Nested Array Input\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n### Validating Nested Array Input\n\nValidating nested array-based form input fields doesn't have to be a pain. You may use \"dot notation\" to validate attributes within an array. For example, if the incoming HTTP request contains a `photos[profile]` field, you may validate it like so:\n\n```php\nuse Illuminate\\Support\\Facades\\Validator;\n\n$validator = Validator::make($request->all(), [\n    'photos.profile' => 'required|image',\n]);\n```\n\nYou may also validate each element of an array. For example, to validate that each email in a given array input field is unique, you may do the following:\n\n```php\n$validator = Validator::make($request->all(), [\n    'users.*.email' => 'email|unique:users',\n    'users.*.first_name' => 'required_with:users.*.last_name',\n]);\n```\n\nLikewise, you may use the `*` character when specifying [custom validation messages in your language files](#custom-messages-for-specific-attributes), making it a breeze to use a single validation message for array-based fields:\n\n```php\n'custom' => [\n    'users.*.email' => [\n        'unique' => 'Each user must have a unique email address',\n    ]\n],\n```\n\n---\n\n## Browser Testing check\nBrowser Testing | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/browser-testing.md\n\n\n### check\n\nThe `check` method checks the given checkbox:\n\n```php\n$page->check('terms');\n$page->check('color', 'blue'); // For checkbox with specific value\n```\n\n---\n\n## Lazy Full-page lazy loading\nLazy | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/lazy\n\n## Full-page lazy loading\n\nYou can lazy load or defer full-page Livewire components using route methods.\n\n### Lazy loading full pages\n\nUse `->lazy()` to load the component when it enters the viewport:\n\n```php\nRoute::livewire('/dashboard', 'pages::dashboard')->lazy();\n```\n\n### Deferring full pages\n\nUse `->defer()` to load the component immediately after the page loads:\n\n```php\nRoute::livewire('/dashboard', 'pages::dashboard')->defer();\n```\n\n### Disabling lazy/defer loading\n\nIf a component is lazy or deferred by default (via the `#[Lazy]` or `#[Defer]` attribute), you can opt-out using `enabled: false`:\n\n```php\nRoute::livewire('/dashboard', 'pages::dashboard')->lazy(enabled: false);\nRoute::livewire('/dashboard', 'pages::dashboard')->defer(enabled: false);\n```\n\n---\n\n## Redirecting Redirecting to full-page components\nRedirecting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/redirecting\n\n## Redirecting to full-page components\n\nBecause Livewire uses Laravel's built-in redirection feature, you can use all of the redirection methods available to you in a typical Laravel application.\n\nFor example, if you are using a Livewire component as a full-page component for a route like so:\n\n```php\nRoute::livewire('/posts', 'pages::show-posts');\n```\n\nYou can redirect to it simply by using the route path:\n\n```php\npublic function save()\n{\n    // ...\n\n    $this->redirect('/posts');\n}\n```\n\n---\n\n## Attribute Title Only for full-page components\nAttribute Title | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-title\n\n## Only for full-page components\n\n> [!info] Full-page components only\n> The `#[Title]` attribute only works for full-page components accessed via routes. Regular components rendered within other views don't use titles—they inherit the parent page's title.\n\n---\n\n## Attribute Layout Learn more\nAttribute Layout | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-layout\n\n## Learn more\n\nFor more information about full-page components and layouts, see the [Pages documentation](/docs/4.x/pages#layouts).\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## Javascript Reference\nJavascript | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/javascript\n\n## Reference\n\nWhen extending Livewire's JavaScript system, it's important to understand the different objects you might encounter.\n\nHere is an exhaustive reference of each of Livewire's relevant internal properties.\n\nAs a reminder, the average Livewire user may never interact with these. Most of these objects are available for Livewire's internal system or advanced users.\n\n### The `$wire` object\n\nGiven the following generic `Counter` component:\n\n```php\n<?php\n\nnamespace App\\Livewire;\n\nuse Livewire\\Component;\n\nclass Counter extends Component\n{\n    public $count = 1;\n\n    public function increment()\n    {\n        $this->count++;\n    }\n\n    public function render()\n    {\n        return view('livewire.counter');\n    }\n}\n```\n\nLivewire exposes a JavaScript representation of the server-side component in the form of an object that is commonly referred to as `$wire`:\n\n```js\nlet $wire = {\n    // All component public properties are directly accessible on $wire...\n    count: 0,\n\n    // All public methods are exposed and callable on $wire...\n    increment() { ... },\n\n    // Access the `$wire` object of the parent component if one exists...\n    $parent,\n\n    // Access the root DOM element of the Livewire component...\n    $el,\n\n    // Access the ID of the current Livewire component...\n    $id,\n\n    // Get the value of a property by name...\n    // Usage: $wire.$get('count')\n    $get(name) { ... },\n\n    // Set a property on the component by name...\n    // Usage: $wire.$set('count', 5)\n    $set(name, value, live = true) { ... },\n\n    // Toggle the value of a boolean property...\n    $toggle(name, live = true) { ... },\n\n    // Call the method...\n    // Usage: $wire.$call('increment')\n    $call(method, ...params) { ... },\n\n    // Define a JavaScript action...\n    // Usage: $wire.$js('increment', () => { ... })\n    // Usage: $wire.$js.increment = () => { ... }\n    $js(name, callback) { ... },\n\n    // [DEPRECATED] Entangle - You probably don't need this.\n    // Use $wire directly to access properties instead.\n    // Usage: <div x-data=\"{ count: $wire.$entangle('count') }\">\n    $entangle(name, live = false) { ... },\n\n    // Watch the value of a property for changes...\n    // Usage: Alpine.$watch('count', (value, old) => { ... })\n    $watch(name, callback) { ... },\n\n    // Scope the next action to a named island...\n    // Returns $wire so you can chain any method call.\n    // The chained action will only re-render the named island.\n    // Usage: $wire.$island('revenue').$refresh()\n    // Usage: $wire.$island('feed', { mode: 'append' }).loadMore()\n    $island(name, options = {}) { ... },\n\n    // Refresh a component by sending a message to the server\n    // to re-render the HTML and swap it into the page...\n    $refresh() { ... },\n\n    // Identical to the above `$refresh`. Just a more technical name...\n    $commit() { ... }, // Alias for $refresh()\n\n    // Listen for a an event dispatched from this component or its children...\n    // Usage: $wire.$on('post-created', () => { ... })\n    $on(event, callback) { ... },\n\n    // Listen for a lifecycle hook triggered from this component or the request...\n    // Usage: $wire.$hook('message.sent', () => { ... })\n    $hook(name, callback) { ... },\n\n    // Dispatch an event from this component...\n    // Usage: $wire.$dispatch('post-created', { postId: 2 })\n    $dispatch(event, params = {}) { ... },\n\n    // Dispatch an event onto another component...\n    // Usage: $wire.$dispatchTo('dashboard', 'post-created', { postId: 2 })\n    $dispatchTo(otherComponentName, event, params = {}) { ... },\n\n    // Dispatch an event onto this component and no others...\n    $dispatchSelf(event, params = {}) { ... },\n\n    // A JS API to upload a file directly to component\n    // rather than through `wire:model`...\n    $upload(\n        name, // The property name\n        file, // The File JavaScript object\n        finish = () => { ... }, // Runs when the upload is finished...\n        error = () => { ... }, // Runs if an error is triggered mid-upload...\n        progress = (event) => { // Runs as the upload progresses...\n            event.detail.progress // An integer from 1-100...\n        },\n    ) { ... },\n\n    // API to upload multiple files at the same time...\n    $uploadMultiple(name, files, finish, error, progress) { },\n\n    // Remove an upload after it's been temporarily uploaded but not saved...\n    $removeUpload(name, tmpFilename, finish, error) { ... },\n\n    // Register an action interceptor for this component instance\n    // Usage: $wire.intercept(({ action, onSend, onCancel, onSuccess, onError, onFailure, onFinish }) => { ... })\n    // Or scope to specific action: $wire.intercept('save', ({ action, onSuccess }) => { ... })\n    intercept(actionOrCallback, callback) { ... },\n\n    // Alias for intercept\n    interceptAction(actionOrCallback, callback) { ... },\n\n    // Register a message interceptor for this component instance\n    // Usage: $wire.interceptMessage(({ message, cancel, onSend, onCancel, onSuccess, onSkipped, onError, onFailure, onFinish }) => { ... })\n    // Or scope to specific action: $wire.interceptMessage('save', callback)\n    interceptMessage(actionOrCallback, callback) { ... },\n\n    // Register a request interceptor for this component instance\n    // Usage: $wire.interceptRequest(({ request, onSend, onCancel, onSuccess, onError, onFailure, onFinish }) => { ... })\n    // Or scope to specific action: $wire.interceptRequest('save', callback)\n    interceptRequest(actionOrCallback, callback) { ... },\n\n    // Retrieve the underlying \"component\" object...\n    __instance() { ... },\n}\n```\n\nYou can learn more about `$wire` in [Livewire's documentation on accessing properties in JavaScript](/docs/4.x/properties#accessing-properties-from-javascript).\n\n### The `snapshot` object\n\nBetween each network request, Livewire serializes the PHP component into an object that can be consumed in JavaScript. This snapshot is used to unserialize the component back into a PHP object and therefore has mechanisms built in to prevent tampering:\n\n```js\nlet snapshot = {\n    // The serialized state of the component (public properties)...\n    data: { count: 0 },\n\n    // Long-standing information about the component...\n    memo: {\n        // The component's unique ID...\n        id: '0qCY3ri9pzSSMIXPGg8F',\n\n        // The component's name. Ex. <livewire:[name] />\n        name: 'counter',\n\n        // The URI, method, and locale of the web page that the\n        // component was originally loaded on. This is used\n        // to re-apply any middleware from the original request\n        // to subsequent component update requests (messages)...\n        path: '/',\n        method: 'GET',\n        locale: 'en',\n\n        // A list of any nested \"child\" components. Keyed by\n        // internal template ID with the component ID as the values...\n        children: [],\n\n        // Whether or not this component was \"lazy loaded\"...\n        lazyLoaded: false,\n\n        // A list of any validation errors thrown during the\n        // last request...\n        errors: [],\n    },\n\n    // A securely encrypted hash of this snapshot. This way,\n    // if a malicious user tampers with the snapshot with\n    // the goal of accessing un-owned resources on the server,\n    // the checksum validation will fail and an error will\n    // be thrown...\n    checksum: '1bc274eea17a434e33d26bcaba4a247a4a7768bd286456a83ea6e9be2d18c1e7',\n}\n```\n\n### The `component` object\n\nEvery component on a page has a corresponding component object behind the scenes keeping track of its state and exposing its underlying functionality. This is one layer deeper than `$wire`. It is only meant for advanced usage.\n\nHere's an actual component object for the above `Counter` component with descriptions of relevant properties in JS comments:\n\n```js\nlet component = {\n    // The root HTML element of the component...\n    el: HTMLElement,\n\n    // The unique ID of the component...\n    id: '0qCY3ri9pzSSMIXPGg8F',\n\n    // The component's \"name\" (<livewire:[name] />)...\n    name: 'counter',\n\n    // The latest \"effects\" object. Effects are \"side-effects\" from server\n    // round-trips. These include redirects, file downloads, etc...\n    effects: {},\n\n    // The component's last-known server-side state...\n    canonical: { count: 0 },\n\n    // The component's mutable data object representing its\n    // live client-side state...\n    ephemeral: { count: 0 },\n\n    // A reactive version of `this.ephemeral`. Changes to\n    // this object will be picked up by AlpineJS expressions...\n    reactive: Proxy,\n\n    // A Proxy object that is typically used inside Alpine\n    // expressions as `$wire`. This is meant to provide a\n    // friendly JS object interface for Livewire components...\n    $wire: Proxy,\n\n    // A list of any nested \"child\" components. Keyed by\n    // internal template ID with the component ID as the values...\n    children: [],\n\n    // The last-known \"snapshot\" representation of this component.\n    // Snapshots are taken from the server-side component and used\n    // to re-create the PHP object on the backend...\n    snapshot: {...},\n\n    // The un-parsed version of the above snapshot. This is used to send back to the\n    // server on the next roundtrip because JS parsing messes with PHP encoding\n    // which often results in checksum mis-matches.\n    snapshotEncoded: '{\"data\":{\"count\":0},\"memo\":{\"id\":\"0qCY3ri9pzSSMIXPGg8F\",\"name\":\"counter\",\"path\":\"\\/\",\"method\":\"GET\",\"children\":[],\"lazyLoaded\":true,\"errors\":[],\"locale\":\"en\"},\"checksum\":\"1bc274eea17a434e33d26bcaba4a247a4a7768bd286456a83ea6e9be2d18c1e7\"}',\n}\n```\n\n### The `message` payload\n\nWhen an action is performed on a Livewire component in the browser, a network request is triggered. That network request contains one or many components and various instructions for the server. Internally, these component network payloads are called \"messages\".\n\nA \"message\" represents the data sent from the frontend to the backend when a component needs to update. A component is rendered and manipulated on the frontend until an action is performed that requires it to send a message with its state and updates to the backend.\n\nYou will recognize this schema from the payload in the network tab of your browser's DevTools, or [Livewire's JavaScript hooks](#javascript-hooks):\n\n```js\nlet message = {\n    // Snapshot object...\n    snapshot: { ... },\n\n    // A key-value pair list of properties\n    // to update on the server...\n    updates: {},\n\n    // An array of methods (with parameters) to call server-side...\n    calls: [\n        { method: 'increment', params: [] },\n    ],\n}\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## Validation Form objects\nValidation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation\n\n## Form objects\n\nAs more properties and validation rules are added to a Livewire component, it can begin to feel too crowded. To alleviate this pain and also provide a helpful abstraction for code reuse, you can use Livewire's *Form Objects* to store your properties and validation rules.\n\nBelow is the same `CreatePost` example, but now the properties and rules have been extracted to a dedicated form object named `PostForm`:\n\n```php\n<?php\n\nnamespace App\\Livewire\\Forms;\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\Form;\n\nclass PostForm extends Form\n{\n    #[Validate('required|min:3')]\n\tpublic $title = '';\n\n    #[Validate('required|min:3')]\n    public $content = '';\n}\n```\n\nThe `PostForm` above can now be defined as a property on the `CreatePost` component:\n\n```php\n<?php\n\nnamespace App\\Livewire;\n\nuse App\\Livewire\\Forms\\PostForm;\nuse Livewire\\Component;\nuse App\\Models\\Post;\n\nclass CreatePost extends Component\n{\n    public PostForm $form;\n\n    public function save()\n    {\n\t\tPost::create(\n    \t\t$this->form->all()\n    \t);\n\n\t\treturn redirect()->to('/posts');\n    }\n\n    // ...\n}\n```\n\nAs you can see, instead of listing out each property individually, we can retrieve all the property values using the `->all()` method on the form object.\n\nAlso, when referencing the property names in the template, you must prepend `form.` to each instance:\n\n```blade\n<form wire:submit=\"save\">\n\t<input type=\"text\" wire:model=\"form.title\">\n    <div>@error('form.title') {{ $message }} @enderror</div>\n\n\t<textarea wire:model=\"form.content\"></textarea>\n    <div>@error('form.content') {{ $message }} @enderror</div>\n\n\t<button type=\"submit\">Save</button>\n</form>\n```\n\nWhen using form objects, `#[Validate]` attribute validation will be run every time a property is updated. However, if you disable this behavior by specifying `onUpdate: false` on the attribute, you can manually run a form object's validation using `$this->form->validate()`:\n\n```php\npublic function save()\n{\n    Post::create(\n        $this->form->validate()\n    );\n\n    return redirect()->to('/posts');\n}\n```\n\nForm objects are a useful abstraction for most larger datasets and a variety of additional features that make them even more powerful. For more information, check out the comprehensive [form object documentation](/docs/4.x/forms#extracting-a-form-object).\n\n---\n\n## Wire Confirm Prompting users for input\nWire Confirm | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-confirm\n\n## Prompting users for input\n\nFor even more dangerous actions such as deleting a user's account entirely, you may want to present them with a confirmation prompt which they would need to type in a specific string of characters to confirm the action.\n\nLivewire provides a helpful `.prompt` modifier, that when applied to `wire:confirm`, it will prompt the user for input and only confirm the action if the input matches (case-sensitive) the provided string (designated by a \"|\" (pipe) character at the end if the `wire:confirm` value):\n\n```blade\n<button\n    type=\"button\"\n    wire:click=\"delete\"\n    wire:confirm.prompt=\"Are you sure?\\n\\nType DELETE to confirm|DELETE\"\n>\n    Delete account <!-- [tl! highlight:-2,1] -->\n</button>\n```\n\nWhen a user presses \"Delete account\", the action will only be performed if \"DELETE\" is entered into the prompt, otherwise, the action will be cancelled.\n\n---\n\n## Components Button > Full width\nComponents Button | livewire/flux@2.x | Source: https://fluxui.dev/components/button#livewire/flux\n\n
Full width

A button that spans the full width of the container.

<flux:button variant=\"primary\" class=\"w-full\">Send invite</flux:button>

\n\n---\n\n## Attribute Title Learn more\nAttribute Title | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/attribute-title\n\n## Learn more\n\nFor more information about full-page components, layouts, and routing, see the [Pages documentation](/docs/4.x/pages#setting-a-page-title).\n\n---\n\n## Understanding Nesting Performance implications\nUnderstanding Nesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/understanding-nesting\n\n## Performance implications\n\nLivewire's independent component architecture can have both positive and negative implications for your application.\n\nAn advantage of this architecture is it allows you to isolate expensive portions of your application. For example, you can quarantine a slow database query to its own independent component, and its performance overhead won't impact the rest of the page.\n\nHowever, the biggest drawback of this approach is that because components are entirely separate, inter-component communication/dependencies becomes more difficult.\n\nFor example, if you had a property passed down from the above parent `Posts` component to the nested `ShowPost` component, it wouldn't be \"reactive\". Because each component is independent, if a request to the parent component changed the value of the property being passed into `ShowPost`, it wouldn't update inside `ShowPost`.\n\nLivewire has overcome a number of these hurdles and provides dedicated APIs for these scenarios like: [Reactive properties](/docs/4.x/nesting#reactive-props), [Modelable components](/docs/4.x/nesting#binding-to-child-data-using-wiremodel), and [the `$parent` object](/docs/4.x/nesting#directly-accessing-the-parent-from-the-child).\n\nArmed with this knowledge of how nested Livewire components operate, you will be able to make more informed decisions about when and how to nest components within your application.\n\n---\n\n## Forms Throttling input\nForms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms\n\n## Throttling input\n\nAs stated previously, when an input debounce is applied to a field, a network request will not be sent until the user has stopped typing for a certain amount of time. This means if the user continues typing a long message, a network request won't be sent until the user is finished.\n\nSometimes this isn't the desired behavior, and you would rather send a request as the user types, not when they've finished or taken a break.\n\nIn these cases, you can instead use `.throttle` to signify a time interval to send network requests:\n\n```blade\n<input type=\"text\" wire:model.live.throttle.150ms=\"title\" >\n```\n\nIn the above example, as a user is typing continuously in the \"title\" field, a network request will be sent every 150 milliseconds until the user is finished.\n\n---\n\n## Validation Form Request Validation Preparing Input for Validation\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n### Preparing Input for Validation\n\nIf you need to prepare or sanitize any data from the request before you apply your validation rules, you may use the `prepareForValidation` method:\n\n```php\nuse Illuminate\\Support\\Str;\n\n/**\n * Prepare the data for validation.\n */\nprotected function prepareForValidation(): void\n{\n    $this->merge([\n        'slug' => Str::slug($this->slug),\n    ]);\n}\n```\n\nLikewise, if you need to normalize any request data after validation is complete, you may use the `passedValidation` method:\n\n```php\n/**\n * Handle a passed validation attempt.\n */\nprotected function passedValidation(): void\n{\n    $this->replace(['name' => 'Taylor']);\n}\n```\n\n_[Truncated]_"}]


message: The settings slice can stay compact: add two full-page Livewire settings screens, wire routes/sidebar/tabs, persist JSON under `store_settings.settings_json`, and add focused feature tests. I’m reading the existing settings components and spec details before creating files with Artisan.

phase: commentary


type: message

role: assistant

text: The settings slice can stay compact: add two full-page Livewire settings screens, wire routes/sidebar/tabs, persist JSON under `store_settings.settings_json`, and add focused feature tests. I’m reading the existing settings components and spec details before creating files with Artisan.


cmd: sed -n '1380,1665p' specs/03-ADMIN-UI.md


cmd: sed -n '1,320p' app/Livewire/Admin/Settings/Index.php


cmd: sed -n '1,340p' resources/views/livewire/admin/settings/index.blade.php


cmd: sed -n '1,240p' resources/views/components/admin/sidebar.blade.php && sed -n '1,240p' tests/Feature/Admin/SettingsManagementTest.php


command: sed -n '1,320p' app/Livewire/Admin/Settings/Index.php

Index.php

type: read

aggregated_output:
<?php

namespace App\Livewire\Admin\Settings;

use App\Enums\StoreDomainType;
use App\Models\Store;
use App\Models\StoreDomain;
use App\Models\StoreSettings;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Index extends Component
{
    use AuthorizesRequests;

    #[Locked]
    public int $storeId;

    public string $storeName = '';

    public string $storeHandle = '';

    public string $defaultCurrency = 'EUR';

    public string $defaultLocale = 'en';

    public string $timezone = 'UTC';

    public bool $announcementEnabled = false;

    public string $announcementText = '';

    public bool $guestCheckoutEnabled = true;

    public string $newHostname = '';

    public string $newType = 'storefront';

    public function mount(): void
    {
        $store = $this->store();

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

        $settings = $this->storeSettings($store)->settings_json ?? [];

        $this->storeId = $store->getKey();
        $this->storeName = $store->name;
        $this->storeHandle = $store->handle;
        $this->defaultCurrency = $store->default_currency;
        $this->defaultLocale = $store->default_locale;
        $this->timezone = $store->timezone;
        $this->announcementEnabled = (bool) data_get($settings, 'announcement.enabled', false);
        $this->announcementText = (string) data_get($settings, 'announcement.text', '');
        $this->guestCheckoutEnabled = (bool) data_get($settings, 'checkout.guest_checkout_enabled', true);
    }

    public function save(): void
    {
        $store = $this->scopedStore();

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

        $validated = $this->validate([
            'storeName' => ['required', 'string', 'max:255'],
            'defaultCurrency' => ['required', Rule::in($this->currencyOptions())],
            'defaultLocale' => ['required', Rule::in(array_keys($this->localeOptions()))],
            'timezone' => ['required', Rule::in(timezone_identifiers_list())],
            'announcementEnabled' => ['boolean'],
            'announcementText' => ['nullable', 'string', 'max:255'],
            'guestCheckoutEnabled' => ['boolean'],
        ]);

        $store->forceFill([
            'name' => $validated['storeName'],
            'default_currency' => $validated['defaultCurrency'],
            'default_locale' => $validated['defaultLocale'],
            'timezone' => $validated['timezone'],
        ])->save();

        $settings = $this->storeSettings($store);
        $settings->forceFill([
            'settings_json' => array_replace_recursive($settings->settings_json ?? [], [
                'announcement' => [
                    'enabled' => $this->announcementEnabled,
                    'text' => $this->announcementText,
                ],
                'checkout' => [
                    'guest_checkout_enabled' => $this->guestCheckoutEnabled,
                ],
            ]),
            'updated_at' => now(),
        ])->save();

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

    public function addDomain(): void
    {
        $store = $this->scopedStore();

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

        $validated = $this->validate([
            'newHostname' => [
                'required',
                'string',
                'max:255',
                'regex:/^[a-z0-9.-]+$/i',
                Rule::unique('store_domains', 'hostname'),
            ],
            'newType' => ['required', Rule::in(array_column(StoreDomainType::cases(), 'value'))],
        ], [], [
            'newHostname' => 'hostname',
            'newType' => 'domain type',
        ]);

        StoreDomain::query()->create([
            'store_id' => $store->getKey(),
            'hostname' => mb_strtolower(trim($validated['newHostname'])),
            'type' => StoreDomainType::from($validated['newType']),
            'is_primary' => $this->domains()->isEmpty(),
            'tls_mode' => 'managed',
        ]);

        $this->reset('newHostname');

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

    public function setPrimary(int $domainId): void
    {
        $store = $this->scopedStore();

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

        $domain = $this->domain($domainId);

        StoreDomain::query()
            ->where('store_id', $store->getKey())
            ->update(['is_primary' => false]);

        $domain->forceFill(['is_primary' => true])->save();

        session()->flash('status', 'Primary domain updated');
    }

    public function removeDomain(int $domainId): void
    {
        $store = $this->scopedStore();

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

        $domain = $this->domain($domainId);
        $wasPrimary = $domain->is_primary;

        $domain->delete();

        if ($wasPrimary) {
            StoreDomain::query()
                ->where('store_id', $store->getKey())
                ->orderBy('id')
                ->first()
                ?->forceFill(['is_primary' => true])
                ->save();
        }

        session()->flash('status', 'Domain removed');
    }

    /**
     * @return Collection<int, StoreDomain>
     */
    public function domains(): Collection
    {
        return StoreDomain::query()
            ->where('store_id', $this->storeId)
            ->orderByDesc('is_primary')
            ->orderBy('hostname')
            ->get();
    }

    /**
     * @return array<string, string>
     */
    public function localeOptions(): array
    {
        return [
            'en' => 'English',
            'de' => 'German',
            'fr' => 'French',
        ];
    }

    /**
     * @return list<string>
     */
    public function currencyOptions(): array
    {
        return ['EUR', 'USD', 'GBP', 'CHF'];
    }

    /**
     * @return list<string>
     */
    public function timezoneOptions(): array
    {
        return collect(timezone_identifiers_list())
            ->filter(fn (string $timezone): bool => str_starts_with($timezone, 'Europe/') || str_starts_with($timezone, 'America/'))
            ->values()
            ->all();
    }

    public function render(): mixed
    {
        return view('livewire.admin.settings.index', [
            'domains' => $this->domains(),
            'currencyOptions' => $this->currencyOptions(),
            'localeOptions' => $this->localeOptions(),
            'timezoneOptions' => $this->timezoneOptions(),
        ])->layout('layouts.app', [
            'title' => __('Settings'),
        ]);
    }

    private function store(): Store
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        return $store;
    }

    private function scopedStore(): Store
    {
        return Store::query()->whereKey($this->storeId)->firstOrFail();
    }

    private function storeSettings(Store $store): StoreSettings
    {
        return StoreSettings::query()->firstOrCreate(
            ['store_id' => $store->getKey()],
            ['settings_json' => []],
        );
    }

    private function domain(int $domainId): StoreDomain
    {
        return StoreDomain::query()
            ->where('store_id', $this->storeId)
            ->whereKey($domainId)
            ->firstOrFail();
    }
}

status: completed


command: sed -n '1380,1665p' specs/03-ADMIN-UI.md

aggregated_output:
### 11.1 General Settings

**Route:** `GET /admin/settings`

**Livewire component:** `App\Livewire\Admin\Settings\General`

**Blade view:** `resources/views/livewire/admin/settings/general.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `storeName` | `string` | Store display name |
| `storeHandle` | `string` | Store handle (read-only or with warning) |
| `defaultCurrency` | `string` | Currency code (e.g., "EUR") |
| `defaultLocale` | `string` | Locale code (e.g., "en") |
| `timezone` | `string` | Timezone string |

| Action | Behavior |
|--------|----------|
| `save()` | Validates and saves store settings |

#### Layout

Two-column layout per section: left side has section label and description, right side has form fields.

**Store details section:**
- Left: Heading "Store details" + description "Basic information about your store."
- Right: Store name input, Store handle input (disabled, with description: "The store handle cannot be changed after creation.")

Separator between sections.

**Defaults section:**
- Left: Heading "Defaults" + description "Currency, language, and timezone settings."
- Right:
  - Default currency: Select (EUR/USD/GBP and more)
  - Default locale: Select (English/German/French and more)
  - Timezone: Select (populated from PHP timezone list)

Save button at the bottom (primary variant, right-aligned).

### 11.2 Domains Settings

Domains is a tab within the Settings page (`/admin/settings`), not a separate route. The Settings page has tabs: General, Domains, Shipping, Taxes, Checkout, Notifications.

**Livewire component:** `App\Livewire\Admin\Settings\Domains`

**Blade view:** `resources/views/livewire/admin/settings/domains.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `domains` | `Collection` | Store domains |
| `newHostname` | `string` | Hostname for new domain |
| `newType` | `string` | Type for new domain: `storefront`, `admin`, `api` |

| Action | Behavior |
|--------|----------|
| `addDomain()` | Creates a new domain record |
| `removeDomain(int $domainId)` | Removes a domain |
| `setPrimary(int $domainId)` | Sets domain as primary |

#### Layout

- Heading "Domains"
- Table of existing domains:

| Column | Content |
|--------|---------|
| Hostname | Domain hostname |
| Type | Flux badge: storefront/admin/api |
| Primary | Flux badge (green) "Primary" if `is_primary` |
| TLS | Flux badge showing TLS mode |
| Actions | "Set Primary" button, "Delete" button |

- "Add domain" button opens a modal

#### Add Domain Modal

| Property | Value |
|----------|-------|
| Modal name | `add-domain` |
| Max width | md |
| Heading | "Add domain" |

**Form fields:**
- Hostname input (placeholder: "shop.example.com")
- Type select: Storefront, Admin, API
- Cancel button (ghost) and "Add domain" button (primary)

### 11.3 Shipping Settings

**Route:** `GET /admin/settings/shipping`

**Livewire component:** `App\Livewire\Admin\Settings\Shipping`

**Blade view:** `resources/views/livewire/admin/settings/shipping.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `zones` | `Collection` | Shipping zones with rates |
| `editingZone` | `?ShippingZone` | Zone being edited |
| `zoneName` | `string` | Name for zone form |
| `zoneCountries` | `array` | Selected countries for zone |
| `editingRate` | `?ShippingRate` | Rate being edited |
| `rateName` | `string` | Rate name |
| `rateType` | `string` | `flat`, `weight`, `price`, `carrier` |
| `rateConfig` | `array` | Rate configuration (depends on type) |
| `rateActive` | `bool` | Whether rate is active |
| `testAddress` | `array` | Address fields for test tool |
| `testResult` | `?array` | Result of test address lookup |

| Action | Behavior |
|--------|----------|
| `openZoneModal(?ShippingZone $zone)` | Opens zone create/edit modal |
| `saveZone()` | Saves shipping zone |
| `deleteZone(int $zoneId)` | Deletes a shipping zone |
| `openRateModal(int $zoneId, ?ShippingRate $rate)` | Opens rate create/edit modal |
| `saveRate()` | Saves shipping rate |
| `deleteRate(int $rateId)` | Deletes a rate |
| `testShippingAddress()` | Tests which zone/rate matches a given address |

#### Layout Wireframe

```
+==================================================================+
| Shipping                                         [+ Add zone]    |
+==================================================================+
|                                                                   |
| +--------------------------------------------------------------+ |
| | ZONE: Domestic                            [Edit] [Delete]    | |
| | Countries: US, CA                                             | |
| |                                                               | |
| | Name      | Type    | Config           | Active | Actions    | |
| |-----------|---------|------------------|--------|------------|  |
| | Standard …1745 chars truncated…ut (placeholder: "Domestic, Europe, International...")
- Countries: scrollable checklist of all available countries with checkboxes
- Cancel button (ghost) and "Save zone" button (primary)

#### Rate Modal

| Property | Value |
|----------|-------|
| Modal name | `rate-form` |
| Max width | md |
| Heading | "Edit shipping rate" or "Add shipping rate" (dynamic) |

**Form fields:**
- Rate name input (placeholder: "Standard, Express...")
- Rate type select: Flat rate, Weight-based, Price-based, Carrier-calculated
- Dynamic config fields based on rate type:
  - **Flat:** Price input
  - **Weight-based:** Min weight (g) input, Max weight (g) input, Price input
  - **Price-based:** Min order amount input, Max order amount input, Price input
  - **Carrier:** Info callout: "Carrier-calculated rates require a carrier integration to be configured."
- Active toggle switch with "Active" text label
- Cancel button (ghost) and "Save rate" button (primary)

#### Test Shipping Address Tool

A card section at the bottom of the page:
- Heading "Test shipping address"
- Description: "Enter an address to see which shipping zone and rates match."
- 2-column grid of fields: Country (select), State/Region (input), City (input), ZIP/Postal code (input)
- "Test" button
- Result area (shown after test):
  - If matched: "Matched zone: [zone name]" and list of available rates with prices
  - If no match: Warning callout "No shipping zone matches this address."

### 11.4 Tax Settings

**Route:** `GET /admin/settings/taxes`

**Livewire component:** `App\Livewire\Admin\Settings\Taxes`

**Blade view:** `resources/views/livewire/admin/settings/taxes.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `mode` | `string` | `manual` or `provider` |
| `pricesIncludeTax` | `bool` | Whether prices include tax |
| `provider` | `string` | Provider name (e.g., `stripe_tax`) |
| `providerApiKey` | `string` | API key for tax provider |
| `manualRates` | `array` | Array of `{zone_name, rate_percentage}` |

| Action | Behavior |
|--------|----------|
| `save()` | Saves tax settings |
| `addManualRate()` | Adds a new empty rate row |
| `removeManualRate(int $index)` | Removes a manual rate |

#### Layout Wireframe

```
+==================================================================+
| TAXES                                                             |
+==================================================================+
| MODE SELECTION                                                    |
|  ( ) Manual tax rates - Define tax rates per zone manually       |
|  (o) Tax provider - Use an automated tax calculation service     |
+==================================================================+
| MANUAL RATES TABLE (shown when mode = "manual")                  |
| Zone name       | Rate (%)    |                                   |
|-----------------|-------------|                                   |
| [EU           ] | [19.00    ] | [trash]                          |
| [US-CA        ] | [7.25     ] | [trash]                          |
| [+ Add rate]                                                      |
+==================================================================+
| PROVIDER CONFIG (shown when mode = "provider")                   |
|  Provider: [Stripe Tax v]                                        |
|  API key:  [*************]                                       |
+==================================================================+
| [separator]                                                       |
| TAX-INCLUSIVE TOGGLE                                              |
|  [switch: ON]  Prices include tax                                |
|  Description: When enabled, the listed price includes tax.       |
|  Tax is calculated backwards from the price.                      |
+==================================================================+
| [Save]                                                            |
+==================================================================+
```

**Mode selection:** Two radio buttons - "Manual tax rates" (description: "Define tax rates per zone manually") and "Tax provider" (description: "Use an automated tax calculation service"). Live-bound.

**Manual rates table:** Shown when mode = "manual". Inline-editable table with columns: Zone name (text input), Rate % (number input with step 0.01). Each row has a trash button. "Add rate" ghost button with plus icon.

**Provider config:** Shown when mode = "provider". Provider select (Stripe Tax / None) and API key password input.

**Tax-inclusive toggle:** Toggle switch with "Prices include tax" label and description text explaining the behavior.

Save button at the bottom.

---

## 12. Themes Management

### 12.1 Themes List

**Route:** `GET /admin/themes`

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

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

#### Component Requirements


output:
Chunk ID: 2d6e6f
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2937
Output:
### 11.1 General Settings

**Route:** `GET /admin/settings`

**Livewire component:** `App\Livewire\Admin\Settings\General`

**Blade view:** `resources/views/livewire/admin/settings/general.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `storeName` | `string` | Store display name |
| `storeHandle` | `string` | Store handle (read-only or with warning) |
| `defaultCurrency` | `string` | Currency code (e.g., "EUR") |
| `defaultLocale` | `string` | Locale code (e.g., "en") |
| `timezone` | `string` | Timezone string |

| Action | Behavior |
|--------|----------|
| `save()` | Validates and saves store settings |

#### Layout

Two-column layout per section: left side has section label and description, right side has form fields.

**Store details section:**
- Left: Heading "Store details" + description "Basic information about your store."
- Right: Store name input, Store handle input (disabled, with description: "The store handle cannot be changed after creation.")

Separator between sections.

**Defaults section:**
- Left: Heading "Defaults" + description "Currency, language, and timezone settings."
- Right:
  - Default currency: Select (EUR/USD/GBP and more)
  - Default locale: Select (English/German/French and more)
  - Timezone: Select (populated from PHP timezone list)

Save button at the bottom (primary variant, right-aligned).

### 11.2 Domains Settings

Domains is a tab within the Settings page (`/admin/settings`), not a separate route. The Settings page has tabs: General, Domains, Shipping, Taxes, Checkout, Notifications.

**Livewire component:** `App\Livewire\Admin\Settings\Domains`

**Blade view:** `resources/views/livewire/admin/settings/domains.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `domains` | `Collection` | Store domains |
| `newHostname` | `string` | Hostname for new domain |
| `newType` | `string` | Type for new domain: `storefront`, `admin`, `api` |

| Action | Behavior |
|--------|----------|
| `addDomain()` | Creates a new domain record |
| `removeDomain(int $domainId)` | Removes a domain |
| `setPrimary(int $domainId)` | Sets domain as primary |

#### Layout

- Heading "Domains"
- Table of existing domains:

| Column | Content |
|--------|---------|
| Hostname | Domain hostname |
| Type | Flux badge: storefront/admin/api |
| Primary | Flux badge (green) "Primary" if `is_primary` |
| TLS | Flux badge showing TLS mode |
| Actions | "Set Primary" button, "Delete" button |

- "Add domain" button opens a modal

#### Add Domain Modal

| Property | Value |
|----------|-------|
| Modal name | `add-domain` |
| Max width | md |
| Heading | "Add domain" |

**Form fields:**
- Hostname input (placeholder: "shop.example.com")
- Type select: Storefront, Admin, API
- Cancel button (ghost) and "Add domain" button (primary)

### 11.3 Shipping Settings

**Route:** `GET /admin/settings/shipping`

**Livewire component:** `App\Livewire\Admin\Settings\Shipping`

**Blade view:** `resources/views/livewire/admin/settings/shipping.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `zones` | `Collection` | Shipping zones with rates |
| `editingZone` | `?ShippingZone` | Zone being edited |
| `zoneName` | `string` | Name for zone form |
| `zoneCountries` | `array` | Selected countries for zone |
| `editingRate` | `?ShippingRate` | Rate being edited |
| `rateName` | `string` | Rate name |
| `rateType` | `string` | `flat`, `weight`, `price`, `carrier` |
| `rateConfig` | `array` | Rate configuration (depends on type) |
| `rateActive` | `bool` | Whether rate is active |
| `testAddress` | `array` | Address fields for test tool |
| `testResult` | `?array` | Result of test address lookup |

| Action | Behavior |
|--------|----------|
| `openZoneModal(?ShippingZone $zone)` | Opens zone create/edit modal |
| `saveZone()` | Saves shipping zone |
| `deleteZone(int $zoneId)` | Deletes a shipping zone |
| `openRateModal(int $zoneId, ?ShippingRate $rate)` | Opens rate create/edit modal |
| `saveRate()` | Saves shipping rate |
| `deleteRate(int $rateId)` | Deletes a rate |
| `testShippingAddress()` | Tests which zone/rate matches a given address |

#### Layout Wireframe

```
+==================================================================+
| Shipping                                         [+ Add zone]    |
+==================================================================+
|                                                                   |
| +--------------------------------------------------------------+ |
| | ZONE: Domestic                            [Edit] [Delete]    | |
| | Countries: US, CA                                             | |
| |                                                               | |
| | Name      | Type    | Config           | Active | Actions    | |
| |-----------|---------|------------------|--------|------------|  |
| | Standard  | flat    | $5.00            | [on]   | [Ed] [Del]| |
| | Express   | flat    | $15.00           | [on]   | [Ed] [Del]| |
| | [+ Add rate]                                                  | |
| +--------------------------------------------------------------+ |
|                                                                   |
| +--------------------------------------------------------------+ |
| | ZONE: Europe                              [Edit] [Delete]    | |
| | Countries: DE, FR, GB, ...                                    | |
| | (rates table...)                                              | |
| +--------------------------------------------------------------+ |
|                                                                   |
| TEST SHIPPING ADDRESS                                             |
| Country: [v]  State: [    ]  City: [    ]  ZIP: [    ]           |
| [Test]                                                            |
| Result: Matched zone: Domestic                                    |
|         Standard - $5.00 / Express - $15.00                      |
+==================================================================+
```

Each zone is displayed as a card with:
- Zone name heading and Edit/Delete buttons in the header
- Countries list as muted text
- Rates table inside the zone card with columns: Name, Type (as badge), Config summary, Active (toggle switch), Actions (Edit/Delete buttons)
- "Add rate" button at the bottom of each zone card

"Add zone" button at the top of the page.

#### Zone Modal

| Property | Value |
|----------|-------|
| Modal name | `zone-form` |
| Max width | md |
| Heading | "Edit shipping zone" or "Add shipping zone" (dynamic) |

**Form fields:**
- Zone name input (placeholder: "Domestic, Europe, International...")
- Countries: scrollable checklist of all available countries with checkboxes
- Cancel button (ghost) and "Save zone" button (primary)

#### Rate Modal

| Property | Value |
|----------|-------|
| Modal name | `rate-form` |
| Max width | md |
| Heading | "Edit shipping rate" or "Add shipping rate" (dynamic) |

**Form fields:**
- Rate name input (placeholder: "Standard, Express...")
- Rate type select: Flat rate, Weight-based, Price-based, Carrier-calculated
- Dynamic config fields based on rate type:
  - **Flat:** Price input
  - **Weight-based:** Min weight (g) input, Max weight (g) input, Price input
  - **Price-based:** Min order amount input, Max order amount input, Price input
  - **Carrier:** Info callout: "Carrier-calculated rates require a carrier integration to be configured."
- Active toggle switch with "Active" text label
- Cancel button (ghost) and "Save rate" button (primary)

#### Test Shipping Address Tool

A card section at the bottom of the page:
- Heading "Test shipping address"
- Description: "Enter an address to see which shipping zone and rates match."
- 2-column grid of fields: Country (select), State/Region (input), City (input), ZIP/Postal code (input)
- "Test" button
- Result area (shown after test):
  - If matched: "Matched zone: [zone name]" and list of available rates with prices
  - If no match: Warning callout "No shipping zone matches this address."

### 11.4 Tax Settings

**Route:** `GET /admin/settings/taxes`

**Livewire component:** `App\Livewire\Admin\Settings\Taxes`

**Blade view:** `resources/views/livewire/admin/settings/taxes.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `mode` | `string` | `manual` or `provider` |
| `pricesIncludeTax` | `bool` | Whether prices include tax |
| `provider` | `string` | Provider name (e.g., `stripe_tax`) |
| `providerApiKey` | `string` | API key for tax provider |
| `manualRates` | `array` | Array of `{zone_name, rate_percentage}` |

| Action | Behavior |
|--------|----------|
| `save()` | Saves tax settings |
| `addManualRate()` | Adds a new empty rate row |
| `removeManualRate(int $index)` | Removes a manual rate |

#### Layout Wireframe

```
+==================================================================+
| TAXES                                                             |
+==================================================================+
| MODE SELECTION                                                    |
|  ( ) Manual tax rates - Define tax rates per zone manually       |
|  (o) Tax provider - Use an automated tax calculation service     |
+==================================================================+
| MANUAL RATES TABLE (shown when mode = "manual")                  |
| Zone name       | Rate (%)    |                                   |
|-----------------|-------------|                                   |
| [EU           ] | [19.00    ] | [trash]                          |
| [US-CA        ] | [7.25     ] | [trash]                          |
| [+ Add rate]                                                      |
+==================================================================+
| PROVIDER CONFIG (shown when mode = "provider")                   |
|  Provider: [Stripe Tax v]                                        |
|  API key:  [*************]                                       |
+==================================================================+
| [separator]                                                       |
| TAX-INCLUSIVE TOGGLE                                              |
|  [switch: ON]  Prices include tax                                |
|  Description: When enabled, the listed price includes tax.       |
|  Tax is calculated backwards from the price.                      |
+==================================================================+
| [Save]                                                            |
+==================================================================+
```

**Mode selection:** Two radio buttons - "Manual tax rates" (description: "Define tax rates per zone manually") and "Tax provider" (description: "Use an automated tax calculation service"). Live-bound.

**Manual rates table:** Shown when mode = "manual". Inline-editable table with columns: Zone name (text input), Rate % (number input with step 0.01). Each row has a trash button. "Add rate" ghost button with plus icon.

**Provider config:** Shown when mode = "provider". Provider select (Stripe Tax / None) and API key password input.

**Tax-inclusive toggle:** Toggle switch with "Prices include tax" label and description text explaining the behavior.

Save button at the bottom.

---

## 12. Themes Management

### 12.1 Themes List

**Route:** `GET /admin/themes`

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

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

#### Component Requirements