# Search Results

## Testing See also
Testing | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing

## See also

- **[Actions](/docs/4.x/actions)** — Test component actions and interactions
- **[Forms](/docs/4.x/forms)** — Test form submissions and validation
- **[Events](/docs/4.x/events)** — Test event dispatching and listening
- **[Components](/docs/4.x/components)** — Create testable component structure

---

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

    Checkbox

Select one or multiple options from a set.

<flux:field variant="inline">    <flux:checkbox wire:model="terms" />    <flux:label>I agree to the terms and conditions</flux:label>    <flux:error name="terms" /></flux:field>

---

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

---

## Quickstart Test it out
Quickstart | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/quickstart

## Test it out

With everything in place, let's test the component!

Start your Laravel development server if it's not already running:

```shell
php artisan serve
```

Visit `http://localhost:8000/post/create` in your browser (or `http://yourapp.test/post/create` if using Valet, Herd, or a similar tool).

You should see a simple form with two fields and a submit button.

**Try the following:**

1. **Test validation:** Click "Save Post" without filling in any fields. You'll see red error messages appear instantly below each field—no page reload required.

2. **Test submission:** Fill in both fields and click "Save Post". You should see a debug screen showing the values you entered.

This demonstrates Livewire's core power: reactive data binding, real-time validation, and form handling—all written in PHP without touching JavaScript.

---

## Http Tests Validation Assertions assertInvalid
Http Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md

#### assertInvalid

Assert that the response has validation errors for the given keys. This method may be used for asserting against responses where the validation errors are returned as a JSON structure or where the validation errors have been flashed to the session:

```php
$response->assertInvalid(['name', 'email']);
```

You may also assert that a given key has a particular validation error message. When doing so, you may provide the entire message or only a small portion of the message:

```php
$response->assertInvalid([
    'name' => 'The name field is required.',
    'email' => 'valid email address',
]);
```

---

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

---

## Components Checkbox > Buttons
Components Checkbox | livewire/flux@2.x | Source: https://fluxui.dev/components/checkbox#livewire/flux

Buttons

Button-style checkbox options that look like a toolbar. Perfect for toggle controls and feature selections where you want a more prominent visual style.

<flux:checkbox.group wire:model="features" label="Features" variant="buttons">    <flux:checkbox value="notifications" icon="bell" label="Notifications" />    <flux:checkbox value="analytics" icon="chart-bar" label="Analytics" />    <flux:checkbox value="backups" icon="cloud-arrow-up" label="Backups" /></flux:checkbox.group>

---

## Testing Creating your first test
Testing | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing

## Creating your first test

You can generate a test file alongside a component by appending the `--test` flag to the `make:livewire` command:

```shell
php artisan make:livewire post.create --test
```

For multi-file components, this will create a test file at `resources/views/components/post/create.test.php`:

```php
<?php

use Livewire\Livewire;

it('renders successfully', function () {
    Livewire::test('post.create')
        ->assertStatus(200);
});
```

For class-based components, this creates a PHPUnit test file at `tests/Feature/Livewire/Post/CreateTest.php`. You can convert it to Pest syntax or keep using PHPUnit—both work great with Livewire.

### Testing a page contains a component

The simplest Livewire test you can write is asserting that a given endpoint includes and successfully renders a Livewire component.

```php
it('component exists on the page', function () {
    $this->get('/posts/create')
        ->assertSeeLivewire('post.create');
});
```

> [!tip] Smoke tests provide huge value
> Tests like these are called "smoke tests"—they ensure there are no catastrophic problems in your application. Although simple, these tests provide enormous value as they require very little maintenance and give you a base level of confidence that your pages render successfully.

---

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

## See also

- **[Forms](/docs/4.x/forms)** — Validate form inputs with real-time feedback
- **[Properties](/docs/4.x/properties)** — Validate property values before persisting
- **[Validate Attribute](/docs/4.x/attribute-validate)** — Use #[Validate] for property validation
- **[Actions](/docs/4.x/actions)** — Validate data in action methods

---

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

---

## Validation Available Validation Rules in_array_keys:_value_.*
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### in_array_keys:_value_.*

The field under validation must be an array having at least one of the given _values_ as a key within the array:

```php
'config' => 'array|in_array_keys:timezone'
```

---

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

flux:textarea

<ui-table-scroll-area>

Prop

Description

    wire:model

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

    placeholder

Placeholder text displayed when the textarea is empty.

    label

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

    description

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

    description:trailing

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

    badge

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

    rows

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

    resize

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

    invalid

If true, applies error styling to the textarea.

</ui-table-scroll-area>

<ui-table-scroll-area>

Attribute

Description

    data-flux-textarea

Applied to the textarea element for styling and identification.

</ui-table-scroll-area>

---

## Test Impact Analysis
Test Impact Analysis | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/test-impact-analysis.md

---
title: Test Impact Analysis (TIA)
description: Test Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes.
---

# Test Impact Analysis (TIA)

Test Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes. The first time you run with `--tia`, Pest records a graph of which tests depend on which files. Every run after that, Pest looks at what you changed, runs only the tests that touched those files, and replays cached results for everything else.

A typical Laravel suite that takes 15 seconds replays in under a second. Edits to a single Blade template re-run a handful of feature tests. Comment-only edits, formatter passes, and README touches re-run nothing at all.

To get started, just add the `--tia` flag to any Pest invocation:

```bash
./vendor/bin/pest --parallel --tia
```

The first run is the **baseline** — Pest enables a coverage driver (PCOV or Xdebug) and records the dependency graph as your tests execute. Expect a small overhead on this run only.

Every subsequent run is a **replay**. Pest compares your working tree against the baseline and re-runs only the tests affected by your changes:

```
Tests:    774 passed (2658 assertions, 7 affected, 767 replayed)
Duration: 0.74s
```

`affected` is the set of tests Pest actually executed. `replayed` is the set whose results were served from cache.

## How Pest Decides What To Run

For each file you've changed, Pest looks for tests that depend on it:

- **PHP source files** — your `app/` classes, controllers, models, helpers — are tracked through the coverage driver. A change to `app/Models/User.php` re-runs only the tests that touched `User`.
- **Migrations** are intersected with the tables each test queried during the baseline. A column rename in `create_users_table.php` re-runs only the tests that queried the `users` table.
- **Inertia pages** under `resources/js/Pages` re-run only the tests that server-side-rendered them.
- **Shared JS components** under `resources/js/Components`, `Layouts`, and friends re-run only the tests whose pages import them — Pest walks Vite's module graph to figure this out.
- **Blade templates** re-run only the tests that rendered them, including renders triggered by browser tests.
- **Anything else** — config files, route files, `.env`, files outside the recorded graph — falls through to a broader pattern. Editing `config/app.php` re-runs every test, because Pest can't statically prove which tests depend on it.

## Cosmetic Edits Don't Run Anything

Pest normalises file content before comparing, so cosmetic changes don't trigger any tests. PHP files have whitespace, line comments, and docblocks stripped before hashing. Blade strips `{{-- … --}}` comments. JS, TS, Vue, and Svelte files have line and block comments removed too.

The result: a comment-only edit, a Prettier reformat, a Pint pass, or a README tweak produces an identical hash, and the file never enters the changed set. Zero tests run.

## Modes

Pest supports a few flags alongside `--tia`:

| Flag | Behaviour |
|---|---|
| `--tia` | Replay if a baseline graph exists, otherwise record. |
| `--tia --fresh` | Discard any existing graph and re-record from scratch. Use this after large refactors or when the graph feels stale. |
| `--tia --refetch` | Force a CI baseline fetch even within the 24-hour cooldown after a previous failed fetch.

## Sharing The Baseline From CI

Recording the baseline locally takes minutes on large suites. Instead, you can have CI record it once per merge to `main`, and every developer downloads the result.

When Pest detects no local graph (or the local graph is out of date), it uses GitHub's CLI to download the latest successful run of a `tia-baseline.yml` workflow's `pest-tia-baseline` artifact. Pest validates the fetched graph against your project state — if it matches, it's adopted. Otherwise it's discarded and a local rebuild proceeds.

Here's a starter workflow you can drop into `.github/workflows/tia-baseline.yml`:

```yaml
name: TIA Baseline
on:
  push: { branches: [main] }
  schedule: [{ cron: '0 3 * * *' }]
  workflow_dispatch:
jobs:
  baseline:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: shivammathur/setup-php@v2
        with: { php-version: '8.5', coverage: xdebug }
      - run: composer install --no-interaction --prefer-dist
      - run: ./vendor/bin/pest --parallel --tia --coverage
      - name: Stage baseline for upload
        shell: bash
        run: |
          mkdir -p .pest-tia-baseline
          cp -R "$HOME/.pest/tia"/*/. .pest-tia-baseline/
      - uses: actions/upload-artifact@v4
        with:
          name: pest-tia-baseline
          path: .pest-tia-baseline/
          retention-days: 30
```

After CI runs, every developer who runs `pest --tia` for the first time on the repo will download this baseline and start replaying immediately, paying no record cost.

## Storage

Pest stores its state at `~/.pest/tia/<project-key>/`, where the project key is derived from your normalised git remote URL — so `git@github.com:foo/bar.git` and `https://github.com/foo/bar` produce the same key. A non-git project falls back to a hash of the project's absolute path.

Sharing state per remote URL means multiple worktrees of the same repository share one cache, while unrelated projects on the same machine stay isolated.

## Custom Watch Patterns

If your project has a directory layout that doesn't match the framework defaults, you can register custom watch patterns in `tests/Pest.php`:

```php
pest()->tia()->watch([
    'config/billing/**/*.php' => ['tests/Feature/Billing'],

    //
]);
```

Each glob maps to a list of test directories; whenever a matching file changes, every test under those directories is invalidated. Use this when you have specific test directories that genuinely depend on a file Pest can't see otherwise.

---

Now that you've learned how to use Test Impact Analysis to speed up your test suite, let's discuss how to integrate Pest with your continuous integration workflow: [Continuous Integration](/docs/continuous-integration)

---

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

---

## Validation Deprecated `[#Rule]` attribute
Validation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation

## Deprecated `[#Rule]` attribute

When Livewire v3 first launched, it used the term "Rule" instead of "Validate" for it's validation attributes (`#[Rule]`).

Because of naming conflicts with Laravel rule objects, this has since been changed to `#[Validate]`. Both are supported in Livewire v3, however it is recommended that you change all occurrences of `#[Rule]` with `#[Validate]` to stay current.

---

## Installation Next Steps Laravel the Full Stack Framework
Installation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/installation.md

### Laravel the Full Stack Framework

Laravel may serve as a full stack framework. By "full stack" framework we mean that you are going to use Laravel to route requests to your application and render your frontend via [Blade templates](/docs/{{version}}/blade) or a single-page application hybrid technology like [Inertia](https://inertiajs.com). This is the most common way to use the Laravel framework, and, in our opinion, the most productive way to use Laravel.

If this is how you plan to use Laravel, you may want to check out our documentation on [frontend development](/docs/{{version}}/frontend), [routing](/docs/{{version}}/routing), [views](/docs/{{version}}/views), or the [Eloquent ORM](/docs/{{version}}/eloquent). In addition, you might be interested in learning about community packages like [Livewire](https://livewire.laravel.com) and [Inertia](https://inertiajs.com). These packages allow you to use Laravel as a full-stack framework while enjoying many of the UI benefits provided by single-page JavaScript applications.

If you are using Laravel as a full stack framework, we also strongly encourage you to learn how to compile your application's CSS and JavaScript using [Vite](/docs/{{version}}/vite).

> [!NOTE]
> If you want to get a head start building your application, check out one of our official [application starter kits](/docs/{{version}}/starter-kits).

---

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

    Reference

---

## Testing All available testing methods
Testing | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing

## All available testing methods

Below is a comprehensive reference of every Livewire testing method available to you:

### Setup methods

| Method                                                  | Description                                                                                |
|---------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| `Livewire::test('post.create')`                      | Test the `post.create` component |
| `Livewire::test(UpdatePost::class, ['post' => $post])`                      | Test the `UpdatePost` component with parameters passed to `mount()` |
| `Livewire::actingAs($user)`                      | Set the authenticated user for the test |
| `Livewire::withQueryParams(['search' => '...'])`                      | Set URL query parameters (ex. `?search=...`) |
| `Livewire::withCookie('name', 'value')`                      | Set a cookie for the test |
| `Livewire::withCookies(['color' => 'blue', 'name' => 'Taylor'])`                      | Set multiple cookies |
| `Livewire::withHeaders(['X-Header' => 'value'])`                      | Set custom headers |
| `Livewire::withoutLazyLoading()`                      | Disable lazy loading for all components in this test |

### Interacting with components

| Method                                                  | Description                                                                                |
|---------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
| `set('title', '...')`                      | Set the `title` property to the provided value |
| `set(['title' => '...', 'content' => '...'])`                      | Set multiple properties using an array |
| `toggle('sortAsc')`                      | Toggle a boolean property between `true` and `false`  |
| `call('save')`                      | Call the `save` action/method |
| `call('remove', $postId)`                      | Call a method with parameters |
| `refresh()`                      | Trigger a component re-render |
| `dispatch('post-created')`                      | Dispatch an event from the component  |
| `dispatch('post-created', postId: $post->id)`                      | Dispatch an event with parameters |

### Assertions

| Method                                                | Description                                                                                |
|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `assertSet('title', '...')`                           | Assert that a property equals the provided value                                                                                |
| `assertNotSet('title', '...')`                        | Assert that a property does not equal the provided value                                                                                |
| `assertCount('posts', 3)`                             | Assert that a property contains 3 items                                                                                |
| `assertSee('...')`                             | Assert that the rendered HTML contains the provided text                                                                                |
| `assertDontSee('...')`                         | Assert that the rendered HTML does not contain the provided text                                                                                |
| `assertSeeHtml('<div>...</div>')`                     | Assert that raw HTML is present in the rendered output |
| `assertDontSeeHtml('<div>...</div>')`                 | Assert that raw HTML is not present in the rendered output                                                                                |
| `assertSeeInOrder(['first', 'second'])`                    | Assert that strings appear in order in the rendered output                                                                                |
| `assertDispatched('post-created')`                    | Assert that an event was dispatched                                                                                |
| `assertNotDispatched('post-created')`                 | Assert that an event was not dispatched                                                                                |
| `assertHasErrors('title')`                            | Assert that validation failed for a property                                                                                |
| `assertHasErrors(['title' => ['required', 'min:6']])`   | Assert that specific validation rules failed                                                                                |
| `assertHasNoErrors('title')`                          | Assert that there are no validation errors for a property                                                                                |
| `assertRedirect()`                                    | Assert that a redirect was triggered                                                                                |
| `assertRedirect('/posts')`                            | Assert a redirect to a specific URL                                                                                |
| `assertRedirectToRoute('posts.index')`       | Assert a redirect to a named route                                                                                |
| `assertNoRedirect()`                                  | Assert that no redirect was triggered                                                                                |
| `assertViewHas('posts')`                              | Assert that data was passed to the view                                                                                |
| `assertViewHas('postCount', 3)`                       | Assert that view data has a specific value                                                                                |
| `assertViewHas('posts', function ($posts) { ... })`   | Assert view data passes custom validation                                                                         |
| `assertViewIs('livewire.show-posts')`                 | Assert that a specific view was rendered                                                                                |
| `assertJs("alert('...')")`                            | Assert that a JS expression was evaluated                                                                                |
| `assertNoJs()`                                        | Assert that no JS was evaluated                                                                                |
| `assertFileDownloaded()`                              | Assert that a file download was triggered                                                                                |
| `assertFileDownloaded($filename)`                     | Assert that a specific file was downloaded                                                                                |
| `assertUnauthorized()`                                | Assert that an authorization exception was thrown (401)                                                                                |
| `assertForbidden()`                                   | Assert that access was forbidden (403)                                                                                |
| `assertStatus(500)`                                   | Assert that a specific status code was returned                                                                                |

_[Truncated]_


Output:
Wall time: 4.6471 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Testing See also\nTesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing\n\n## See also\n\n- **[Actions](/docs/4.x/actions)** — Test component actions and interactions\n- **[Forms](/docs/4.x/forms)** — Test form submissions and validation\n- **[Events](/docs/4.x/events)** — Test event dispatching and listening\n- **[Components](/docs/4.x/components)** — Create testable component structure\n\n---\n\n## Components Checkbox > Checkbox\nComponents Checkbox | livewire/flux@2.x | Source: https://fluxui.dev/components/checkbox#livewire/flux\n\n

\n    Checkbox\n

Select one or multiple options from a set.

<flux:field variant=\"inline\">    <flux:checkbox wire:model=\"terms\" />    <flux:label>I agree to the terms and conditions</flux:label>    <flux:error name=\"terms\" /></flux:field>

\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## Quickstart Test it out\nQuickstart | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/quickstart\n\n## Test it out\n\nWith everything in place, let's test the component!\n\nStart your Laravel development server if it's not already running:\n\n```shell\nphp artisan serve\n```\n\nVisit `http://localhost:8000/post/create` in your browser (or `http://yourapp.test/post/create` if using Valet, Herd, or a similar tool).\n\nYou should see a simple form with two fields and a submit button.\n\n**Try the following:**\n\n1. **Test validation:** Click \"Save Post\" without filling in any fields. You'll see red error messages appear instantly below each field—no page reload required.\n\n2. **Test submission:** Fill in both fields and click \"Save Post\". You should see a debug screen showing the values you entered.\n\nThis demonstrates Livewire's core power: reactive data binding, real-time validation, and form handling—all written in PHP without touching JavaScript.\n\n---\n\n## Http Tests Validation Assertions assertInvalid\nHttp Tests | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/http-tests.md\n\n\n#### assertInvalid\n\nAssert that the response has validation errors for the given keys. This method may be used for asserting against responses where the validation errors are returned as a JSON structure or where the validation errors have been flashed to the session:\n\n```php\n$response->assertInvalid(['name', 'email']);\n```\n\nYou may also assert that a given key has a particular validation error message. When doing so, you may provide the entire message or only a small portion of the message:\n\n```php\n$response->assertInvalid([\n    'name' => 'The name field is required.',\n    'email' => 'valid email address',\n]);\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## Components Checkbox > Buttons\nComponents Checkbox | livewire/flux@2.x | Source: https://fluxui.dev/components/checkbox#livewire/flux\n\n
Buttons

Button-style checkbox options that look like a toolbar. Perfect for toggle controls and feature selections where you want a more prominent visual style.

<flux:checkbox.group wire:model=\"features\" label=\"Features\" variant=\"buttons\">    <flux:checkbox value=\"notifications\" icon=\"bell\" label=\"Notifications\" />    <flux:checkbox value=\"analytics\" icon=\"chart-bar\" label=\"Analytics\" />    <flux:checkbox value=\"backups\" icon=\"cloud-arrow-up\" label=\"Backups\" /></flux:checkbox.group>

\n\n---\n\n## Testing Creating your first test\nTesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing\n\n## Creating your first test\n\nYou can generate a test file alongside a component by appending the `--test` flag to the `make:livewire` command:\n\n```shell\nphp artisan make:livewire post.create --test\n```\n\nFor multi-file components, this will create a test file at `resources/views/components/post/create.test.php`:\n\n```php\n<?php\n\nuse Livewire\\Livewire;\n\nit('renders successfully', function () {\n    Livewire::test('post.create')\n        ->assertStatus(200);\n});\n```\n\nFor class-based components, this creates a PHPUnit test file at `tests/Feature/Livewire/Post/CreateTest.php`. You can convert it to Pest syntax or keep using PHPUnit—both work great with Livewire.\n\n### Testing a page contains a component\n\nThe simplest Livewire test you can write is asserting that a given endpoint includes and successfully renders a Livewire component.\n\n```php\nit('component exists on the page', function () {\n    $this->get('/posts/create')\n        ->assertSeeLivewire('post.create');\n});\n```\n\n> [!tip] Smoke tests provide huge value\n> Tests like these are called \"smoke tests\"—they ensure there are no catastrophic problems in your application. Although simple, these tests provide enormous value as they require very little maintenance and give you a base level of confidence that your pages render successfully.\n\n---\n\n## Validation See also\nValidation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation\n\n## See also\n\n- **[Forms](/docs/4.x/forms)** — Validate form inputs with real-time feedback\n- **[Properties](/docs/4.x/properties)** — Validate property values before persisting\n- **[Validate Attribute](/docs/4.x/attribute-validate)** — Use #[Validate] for property validation\n- **[Actions](/docs/4.x/actions)** — Validate data in action methods\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## Validation Available Validation Rules in_array_keys:_value_.*\nValidation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md\n\n\n#### in_array_keys:_value_.*\n\nThe field under validation must be an array having at least one of the given _values_ as a key within the array:\n\n```php\n'config' => 'array|in_array_keys:timezone'\n```\n\n---\n\n## Components Textarea > flux:textarea\nComponents Textarea | livewire/flux@2.x | Source: https://fluxui.dev/components/textarea#livewire/flux\n\n
flux:textarea

<ui-table-scroll-area>

Prop

Description

\n    wire:model\n

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

\n    placeholder\n

Placeholder text displayed when the textarea is empty.

\n    label\n

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

\n    description\n

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

\n    description:trailing\n

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

\n    badge\n

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

\n    rows\n

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

\n    resize\n

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

\n    invalid\n

If true, applies error styling to the textarea.

</ui-table-scroll-area>

<ui-table-scroll-area>

Attribute

Description

\n    data-flux-textarea\n

Applied to the textarea element for styling and identification.

</ui-table-scroll-area>

\n\n---\n\n## Test Impact Analysis\nTest Impact Analysis | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/test-impact-analysis.md\n\n---\ntitle: Test Impact Analysis (TIA)\ndescription: Test Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes.\n---\n\n# Test Impact Analysis (TIA)\n\nTest Impact Analysis is a great way to drastically reduce the time it takes to run your test suite by re-running only the tests affected by your latest changes. The first time you run with `--tia`, Pest records a graph of which tests depend on which files. Every run after that, Pest looks at what you changed, runs only the tests that touched those files, and replays cached results for everything else.\n\nA typical Laravel suite that takes 15 seconds replays in under a second. Edits to a single Blade template re-run a handful of feature tests. Comment-only edits, formatter passes, and README touches re-run nothing at all.\n\nTo get started, just add the `--tia` flag to any Pest invocation:\n\n```bash\n./vendor/bin/pest --parallel --tia\n```\n\nThe first run is the **baseline** — Pest enables a coverage driver (PCOV or Xdebug) and records the dependency graph as your tests execute. Expect a small overhead on this run only.\n\nEvery subsequent run is a **replay**. Pest compares your working tree against the baseline and re-runs only the tests affected by your changes:\n\n```\nTests:    774 passed (2658 assertions, 7 affected, 767 replayed)\nDuration: 0.74s\n```\n\n`affected` is the set of tests Pest actually executed. `replayed` is the set whose results were served from cache.\n\n## How Pest Decides What To Run\n\nFor each file you've changed, Pest looks for tests that depend on it:\n\n- **PHP source files** — your `app/` classes, controllers, models, helpers — are tracked through the coverage driver. A change to `app/Models/User.php` re-runs only the tests that touched `User`.\n- **Migrations** are intersected with the tables each test queried during the baseline. A column rename in `create_users_table.php` re-runs only the tests that queried the `users` table.\n- **Inertia pages** under `resources/js/Pages` re-run only the tests that server-side-rendered them.\n- **Shared JS components** under `resources/js/Components`, `Layouts`, and friends re-run only the tests whose pages import them — Pest walks Vite's module graph to figure this out.\n- **Blade templates** re-run only the tests that rendered them, including renders triggered by browser tests.\n- **Anything else** — config files, route files, `.env`, files outside the recorded graph — falls through to a broader pattern. Editing `config/app.php` re-runs every test, because Pest can't statically prove which tests depend on it.\n\n## Cosmetic Edits Don't Run Anything\n\nPest normalises file content before comparing, so cosmetic changes don't trigger any tests. PHP files have whitespace, line comments, and docblocks stripped before hashing. Blade strips `{{-- … --}}` comments. JS, TS, Vue, and Svelte files have line and block comments removed too.\n\nThe result: a comment-only edit, a Prettier reformat, a Pint pass, or a README tweak produces an identical hash, and the file never enters the changed set. Zero tests run.\n\n## Modes\n\nPest supports a few flags alongside `--tia`:\n\n| Flag | Behaviour |\n|---|---|\n| `--tia` | Replay if a baseline graph exists, otherwise record. |\n| `--tia --fresh` | Discard any existing graph and re-record from scratch. Use this after large refactors or when the graph feels stale. |\n| `--tia --refetch` | Force a CI baseline fetch even within the 24-hour cooldown after a previous failed fetch.\n\n## Sharing The Baseline From CI\n\nRecording the baseline locally takes minutes on large suites. Instead, you can have CI record it once per merge to `main`, and every developer downloads the result.\n\nWhen Pest detects no local graph (or the local graph is out of date), it uses GitHub's CLI to download the latest successful run of a `tia-baseline.yml` workflow's `pest-tia-baseline` artifact. Pest validates the fetched graph against your project state — if it matches, it's adopted. Otherwise it's discarded and a local rebuild proceeds.\n\nHere's a starter workflow you can drop into `.github/workflows/tia-baseline.yml`:\n\n```yaml\nname: TIA Baseline\non:\n  push: { branches: [main] }\n  schedule: [{ cron: '0 3 * * *' }]\n  workflow_dispatch:\njobs:\n  baseline:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with: { fetch-depth: 0 }\n      - uses: shivammathur/setup-php@v2\n        with: { php-version: '8.5', coverage: xdebug }\n      - run: composer install --no-interaction --prefer-dist\n      - run: ./vendor/bin/pest --parallel --tia --coverage\n      - name: Stage baseline for upload\n        shell: bash\n        run: |\n          mkdir -p .pest-tia-baseline\n          cp -R \"$HOME/.pest/tia\"/*/. .pest-tia-baseline/\n      - uses: actions/upload-artifact@v4\n        with:\n          name: pest-tia-baseline\n          path: .pest-tia-baseline/\n          retention-days: 30\n```\n\nAfter CI runs, every developer who runs `pest --tia` for the first time on the repo will download this baseline and start replaying immediately, paying no record cost.\n\n## Storage\n\nPest stores its state at `~/.pest/tia/<project-key>/`, where the project key is derived from your normalised git remote URL — so `git@github.com:foo/bar.git` and `https://github.com/foo/bar` produce the same key. A non-git project falls back to a hash of the project's absolute path.\n\nSharing state per remote URL means multiple worktrees of the same repository share one cache, while unrelated projects on the same machine stay isolated.\n\n## Custom Watch Patterns\n\nIf your project has a directory layout that doesn't match the framework defaults, you can register custom watch patterns in `tests/Pest.php`:\n\n```php\npest()->tia()->watch([\n    'config/billing/**/*.php' => ['tests/Feature/Billing'],\n\n    //\n]);\n```\n\nEach glob maps to a list of test directories; whenever a matching file changes, every test under those directories is invalidated. Use this when you have specific test directories that genuinely depend on a file Pest can't see otherwise.\n\n---\n\nNow that you've learned how to use Test Impact Analysis to speed up your test suite, let's discuss how to integrate Pest with your continuous integration workflow: [Continuous Integration](/docs/continuous-integration)\n\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## Validation Deprecated `[#Rule]` attribute\nValidation | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/validation\n\n## Deprecated `[#Rule]` attribute\n\nWhen Livewire v3 first launched, it used the term \"Rule\" instead of \"Validate\" for it's validation attributes (`#[Rule]`).\n\nBecause of naming conflicts with Laravel rule objects, this has since been changed to `#[Validate]`. Both are supported in Livewire v3, however it is recommended that you change all occurrences of `#[Rule]` with `#[Validate]` to stay current.\n\n---\n\n## Installation Next Steps Laravel the Full Stack Framework\nInstallation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/installation.md\n\n\n### Laravel the Full Stack Framework\n\nLaravel may serve as a full stack framework. By \"full stack\" framework we mean that you are going to use Laravel to route requests to your application and render your frontend via [Blade templates](/docs/{{version}}/blade) or a single-page application hybrid technology like [Inertia](https://inertiajs.com). This is the most common way to use the Laravel framework, and, in our opinion, the most productive way to use Laravel.\n\nIf this is how you plan to use Laravel, you may want to check out our documentation on [frontend development](/docs/{{version}}/frontend), [routing](/docs/{{version}}/routing), [views](/docs/{{version}}/views), or the [Eloquent ORM](/docs/{{version}}/eloquent). In addition, you might be interested in learning about community packages like [Livewire](https://livewire.laravel.com) and [Inertia](https://inertiajs.com). These packages allow you to use Laravel as a full-stack framework while enjoying many of the UI benefits provided by single-page JavaScript applications.\n\nIf you are using Laravel as a full stack framework, we also strongly encourage you to learn how to compile your application's CSS and JavaScript using [Vite](/docs/{{version}}/vite).\n\n> [!NOTE]\n> If you want to get a head start building your application, check out one of our official [application starter kits](/docs/{{version}}/starter-kits).\n\n---\n\n## Components Textarea > Reference\nComponents Textarea | livewire/flux@2.x | Source: https://fluxui.dev/components/textarea#livewire/flux\n\n
\n    Reference\n

\n\n---\n\n## Testing All available testing methods\nTesting | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/testing\n\n## All available testing methods\n\nBelow is a comprehensive reference of every Livewire testing method available to you:\n\n### Setup methods\n\n| Method                                                  | Description                                                                                |\n|---------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|\n| `Livewire::test('post.create')`                      | Test the `post.create` component |\n| `Livewire::test(UpdatePost::class, ['post' => $post])`                      | Test the `UpdatePost` component with parameters passed to `mount()` |\n| `Livewire::actingAs($user)`                      | Set the authenticated user for the test |\n| `Livewire::withQueryParams(['search' => '...'])`                      | Set URL query parameters (ex. `?search=...`) |\n| `Livewire::withCookie('name', 'value')`                      | Set a cookie for the test |\n| `Livewire::withCookies(['color' => 'blue', 'name' => 'Taylor'])`                      | Set multiple cookies |\n| `Livewire::withHeaders(['X-Header' => 'value'])`                      | Set custom headers |\n| `Livewire::withoutLazyLoading()`                      | Disable lazy loading for all components in this test |\n\n### Interacting with components\n\n| Method                                                  | Description                                                                                |\n|---------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|\n| `set('title', '...')`                      | Set the `title` property to the provided value |\n| `set(['title' => '...', 'content' => '...'])`                      | Set multiple properties using an array |\n| `toggle('sortAsc')`                      | Toggle a boolean property between `true` and `false`  |\n| `call('save')`                      | Call the `save` action/method |\n| `call('remove', $postId)`                      | Call a method with parameters |\n| `refresh()`                      | Trigger a component re-render |\n| `dispatch('post-created')`                      | Dispatch an event from the component  |\n| `dispatch('post-created', postId: $post->id)`                      | Dispatch an event with parameters |\n\n### Assertions\n\n| Method                                                | Description                                                                                |\n|-------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `assertSet('title', '...')`                           | Assert that a property equals the provided value                                                                                |\n| `assertNotSet('title', '...')`                        | Assert that a property does not equal the provided value                                                                                |\n| `assertCount('posts', 3)`                             | Assert that a property contains 3 items                                                                                |\n| `assertSee('...')`                             | Assert that the rendered HTML contains the provided text                                                                                |\n| `assertDontSee('...')`                         | Assert that the rendered HTML does not contain the provided text                                                                                |\n| `assertSeeHtml('<div>...</div>')`                     | Assert that raw HTML is present in the rendered output |\n| `assertDontSeeHtml('<div>...</div>')`                 | Assert that raw HTML is not present in the rendered output                                                                                |\n| `assertSeeInOrder(['first', 'second'])`                    | Assert that strings appear in order in the rendered output                                                                                |\n| `assertDispatched('post-created')`                    | Assert that an event was dispatched                                                                                |\n| `assertNotDispatched('post-created')`                 | Assert that an event was not dispatched                                                                                |\n| `assertHasErrors('title')`                            | Assert that validation failed for a property                                                                                |\n| `assertHasErrors(['title' => ['required', 'min:6']])`   | Assert that specific validation rules failed                                                                                |\n| `assertHasNoErrors('title')`                          | Assert that there are no validation errors for a property                                                                                |\n| `assertRedirect()`                                    | Assert that a redirect was triggered                                                                                |\n| `assertRedirect('/posts')`                            | Assert a redirect to a specific URL                                                                                |\n| `assertRedirectToRoute('posts.index')`       | Assert a redirect to a named route                                                                                |\n| `assertNoRedirect()`                                  | Assert that no redirect was triggered                                                                                |\n| `assertViewHas('posts')`                              | Assert that data was passed to the view                                                                                |\n| `assertViewHas('postCount', 3)`                       | Assert that view data has a specific value                                                                                |\n| `assertViewHas('posts', function ($posts) { ... })`   | Assert view data passes custom validation                                                                         |\n| `assertViewIs('livewire.show-posts')`                 | Assert that a specific view was rendered                                                                                |\n| `assertJs(\"alert('...')\")`                            | Assert that a JS expression was evaluated                                                                                |\n| `assertNoJs()`                                        | Assert that no JS was evaluated                                                                                |\n| `assertFileDownloaded()`                              | Assert that a file download was triggered                                                                                |\n| `assertFileDownloaded($filename)`                     | Assert that a specific file was downloaded                                                                                |\n| `assertUnauthorized()`                                | Assert that an authorization exception was thrown (401)                                                                                |\n| `assertForbidden()`                                   | Assert that access was forbidden (403)                                                                                |\n| `assertStatus(500)`                                   | Assert that a specific status code was returned                                                                                |\n\n_[Truncated]_"}]


varchar

varchar

varchar

varchar


The specs point at a fairly large admin area: settings, themes/editor, pages, and navigation. I’m narrowing this slice to a coherent first pass that matches the existing data layer and can be fully tested and smoked in one commit.


sed -n '1378,1915p' specs/03-ADMIN-UI.md


sed -n '830,850p' specs/09-IMPLEMENTATION-ROADMAP.md


sed -n '1550,1565p' specs/09-IMPLEMENTATION-ROADMAP.md


rg --files app/Models app/Policies app/Services database/factories database/seeders | rg "StoreSettings|Theme|Page|Navigation|TaxSettings|Shipping"


sed -n '830,850p' specs/09-IMPLEMENTATION-ROADMAP.md

**Files to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Collections\Index` | Collection list |
| `Admin\Collections\Form` | Shared form component for create/edit collection with product picker, reorder products |
| `Admin\Customers\Index` | Customer list with search |
| `Admin\Customers\Show` | Customer detail: info, orders, addresses |
| `Admin\Discounts\Index` | Discount list with status/type filters |
| `Admin\Discounts\Form` | Shared form component for create/edit discount: code, type, value, dates, rules |
| `Admin\Settings\Index` | Tabbed settings: General, Domains, Shipping, Taxes, Checkout, Notifications |
| `Admin\Settings\Shipping` | Shipping zones CRUD, rates CRUD per zone |
| `Admin\Settings\Taxes` | Tax mode toggle, manual rate configuration |
| `Admin\Themes\Index` | Theme cards with Publish/Duplicate/Delete actions |
| `Admin\Themes\Editor` | Left: sections. Center: preview. Right: settings. |
| `Admin\Pages\Index` | Content pages list |
| `Admin\Pages\Form` | Shared form component for create/edit page with rich text editor |
| `Admin\Navigation\Index` | Menu management with drag-and-drop item ordering |
| `Admin\Analytics\Index` | Sales chart, traffic, funnel visualization, date range |
| `Admin\Search\Settings` | Synonyms, stop words, reindex button |
| `Admin\Apps\Index` | App directory |


sed -n '1378,1915p' specs/03-ADMIN-UI.md

## 11. Settings Pages

### 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    | |
| |-----------|---------|------------------|--------|-------…11351 chars truncated…ighted)
```

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

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

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

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

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

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

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

---

## 13. Pages Management

### 13.1 Pages List

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

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

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

#### Component Requirements

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

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

#### Layout

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

### 13.2 Page Form

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

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

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

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `page` | `?Page` | Page being edited |
| `title` | `string` | Page title |
| `handle` | `string` | URL handle |
| `bodyHtml` | `string` | Page body content (HTML) |
| `status` | `string` | `draft` or `active` |
| `publishedAt` | `?string` | Publish date |

| Action | Behavior |
|--------|----------|
| `save()` | Validates and saves the page |
| `deletePage()` | Deletes the page |

#### Layout (Two-Column)

**Left column (2/3):**
- Title: Flux input
- Handle: Flux input
- Body: Flux textarea (16 rows)

**Right column (1/3):**
- Status: Flux select with Draft/Active
- Published at: Datetime-local input

Sticky save bar (same pattern as other forms).

---

## 14. Navigation Management

**Route:** `GET /admin/navigation`

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

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

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `menus` | `Collection` | Navigation menus (Main Menu, Footer Menu) |
| `editingMenu` | `?NavigationMenu` | Menu being edited |
| `menuItems` | `array` | Nested array of menu items for the editing menu |
| `editingItem` | `?array` | Item currently being edited in the modal |
| `editingItemIndex` | `?int` | Index of item being edited |
| `itemLabel` | `string` | Item label |
| `itemType` | `string` | `link`, `page`, `collection`, `product` |
| `itemUrl` | `string` | URL if type is link |
| `itemResourceId` | `?int` | Resource ID if type is page/collection/product |

| Action | Behavior |
|--------|----------|
| `selectMenu(int $menuId)` | Loads a menu for editing |
| `addItem()` | Opens the item form modal in create mode |
| `editItem(int $index)` | Opens the item form modal in edit mode |
| `saveItem()` | Saves the item (add or update) |
| `removeItem(int $index)` | Removes an item |
| `reorderItems(array $order)` | Updates item positions (supports nesting) |
| `saveMenu()` | Persists all menu items |

### Layout Wireframe

```
+==================================================================+
| Navigation                                                        |
+==================================================================+
|                                                                   |
| MENUS                                                             |
| +----------------------------+  +----------------------------+   |
| | Main Menu        [Edit]    |  | Footer Menu       [Edit]   |   |
| +----------------------------+  +----------------------------+   |
|                                                                   |
| MENU EDITOR (shown when a menu is selected)                      |
| +--------------------------------------------------------------+ |
| | Main Menu                                    [+ Add item]    | |


Output:
Chunk ID: f8850b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 5338
Output:
## 11. Settings Pages

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

| Property | Type | Purpose |
|----------|------|---------|
| `themes` | `Collection` | All themes for the store |

| Action | Behavior |
|--------|----------|
| `publishTheme(int $themeId)` | Publishes a theme (sets as active) |
| `duplicateTheme(int $themeId)` | Creates a copy of the theme |
| `deleteTheme(int $themeId)` | Deletes a theme |

#### Layout Wireframe

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

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

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

The published theme card has a blue ring border highlight.

### 12.2 Theme Editor

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

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

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

#### Component Requirements

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

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

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

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

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

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

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

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

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

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

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

---

## 13. Pages Management

### 13.1 Pages List

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

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

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

#### Component Requirements

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

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

#### Layout

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

### 13.2 Page Form

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

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

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

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `page` | `?Page` | Page being edited |
| `title` | `string` | Page title |
| `handle` | `string` | URL handle |
| `bodyHtml` | `string` | Page body content (HTML) |
| `status` | `string` | `draft` or `active` |
| `publishedAt` | `?string` | Publish date |

| Action | Behavior |
|--------|----------|
| `save()` | Validates and saves the page |
| `deletePage()` | Deletes the page |

#### Layout (Two-Column)

**Left column (2/3):**
- Title: Flux input
- Handle: Flux input
- Body: Flux textarea (16 rows)

**Right column (1/3):**
- Status: Flux select with Draft/Active
- Published at: Datetime-local input

Sticky save bar (same pattern as other forms).

---

## 14. Navigation Management

**Route:** `GET /admin/navigation`

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

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

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `menus` | `Collection` | Navigation menus (Main Menu, Footer Menu) |
| `editingMenu` | `?NavigationMenu` | Menu being edited |
| `menuItems` | `array` | Nested array of menu items for the editing menu |
| `editingItem` | `?array` | Item currently being edited in the modal |
| `editingItemIndex` | `?int` | Index of item being edited |
| `itemLabel` | `string` | Item label |
| `itemType` | `string` | `link`, `page`, `collection`, `product` |
| `itemUrl` | `string` | URL if type is link |
| `itemResourceId` | `?int` | Resource ID if type is page/collection/product |

| Action | Behavior |
|--------|----------|
| `selectMenu(int $menuId)` | Loads a menu for editing |
| `addItem()` | Opens the item form modal in create mode |
| `editItem(int $index)` | Opens the item form modal in edit mode |
| `saveItem()` | Saves the item (add or update) |
| `removeItem(int $index)` | Removes an item |
| `reorderItems(array $order)` | Updates item positions (supports nesting) |
| `saveMenu()` | Persists all menu items |

### Layout Wireframe

```
+==================================================================+
| Navigation                                                        |
+==================================================================+
|                                                                   |
| MENUS                                                             |
| +----------------------------+  +----------------------------+   |
| | Main Menu        [Edit]    |  | Footer Menu       [Edit]   |   |
| +----------------------------+  +----------------------------+   |
|                                                                   |
| MENU EDITOR (shown when a menu is selected)                      |
| +--------------------------------------------------------------+ |
| | Main Menu                                    [+ Add item]    | |


Output:
Chunk ID: 627d57
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 340
Output:
**Files to create:**

| Component | Description |
|-----------|-------------|
| `Admin\Collections\Index` | Collection list |
| `Admin\Collections\Form` | Shared form component for create/edit collection with product picker, reorder products |
| `Admin\Customers\Index` | Customer list with search |
| `Admin\Customers\Show` | Customer detail: info, orders, addresses |
| `Admin\Discounts\Index` | Discount list with status/type filters |
| `Admin\Discounts\Form` | Shared form component for create/edit discount: code, type, value, dates, rules |
| `Admin\Settings\Index` | Tabbed settings: General, Domains, Shipping, Taxes, Checkout, Notifications |
| `Admin\Settings\Shipping` | Shipping zones CRUD, rates CRUD per zone |
| `Admin\Settings\Taxes` | Tax mode toggle, manual rate configuration |
| `Admin\Themes\Index` | Theme cards with Publish/Duplicate/Delete actions |
| `Admin\Themes\Editor` | Left: sections. Center: preview. Right: settings. |
| `Admin\Pages\Index` | Content pages list |
| `Admin\Pages\Form` | Shared form component for create/edit page with rich text editor |
| `Admin\Navigation\Index` | Menu management with drag-and-drop item ordering |
| `Admin\Analytics\Index` | Sales chart, traffic, funnel visualization, date range |
| `Admin\Search\Settings` | Synonyms, stop words, reindex button |
| `Admin\Apps\Index` | App directory |


sed -n '1550,1565p' specs/09-IMPLEMENTATION-ROADMAP.md

---

#### tests/Feature/Admin/SettingsTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the settings page | GET /admin/settings | 200 response, sees settings tabs | happy path |
| updates general store settings | update store name | store_settings updated | happy path |
| configures shipping zones | create zone with countries and rates | shipping_zone and shipping_rates in DB | happy path |
| configures tax settings | set mode=manual, rate=1900, prices_include_tax=true | tax_settings updated | happy path |
| restricts settings to owner and admin roles | staff user GET /admin/settings | 403 response | authorization |
| manages store domains | add a new domain | store_domain created | happy path |

---


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

---

#### tests/Feature/Admin/SettingsTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the settings page | GET /admin/settings | 200 response, sees settings tabs | happy path |
| updates general store settings | update store name | store_settings updated | happy path |
| configures shipping zones | create zone with countries and rates | shipping_zone and shipping_rates in DB | happy path |
| configures tax settings | set mode=manual, rate=1900, prices_include_tax=true | tax_settings updated | happy path |
| restricts settings to owner and admin roles | staff user GET /admin/settings | 403 response | authorization |
| manages store domains | add a new domain | store_domain created | happy path |

---


rg --files app/Models app/Policies app/Services database/factories database/seeders | rg "StoreSettings|Theme|Page|Navigation|TaxSettings|Shipping"

Question: StoreSettings|Theme|Page|Navigation|TaxSettings|Shipping

app/Policies/PagePolicy.php
app/Policies/ThemePolicy.php
app/Services/ShippingCalculator.php
app/Services/NavigationService.php
database/seeders/ThemeFileSeeder.php
database/seeders/ThemeSeeder.php
database/seeders/NavigationMenuSeeder.php
database/seeders/ShippingRateSeeder.php
database/seeders/TaxSettingsSeeder.php
database/seeders/ShippingZoneSeeder.php
database/seeders/NavigationItemSeeder.php
database/seeders/PageSeeder.php
database/seeders/StoreSettingsSeeder.php
database/seeders/ThemeSettingsSeeder.php
database/factories/PageFactory.php
database/factories/NavigationItemFactory.php
database/factories/ThemeSettingsFactory.php
database/factories/ThemeFactory.php
database/factories/StoreSettingsFactory.php
database/factories/TaxSettingsFactory.php
database/factories/ThemeFileFactory.php
app/Models/Theme.php
database/factories/NavigationMenuFactory.php
database/factories/ShippingRateFactory.php
app/Models/NavigationMenu.php
app/Models/NavigationItem.php
database/factories/ShippingZoneFactory.php
app/Models/ThemeFile.php
app/Models/ShippingZone.php
app/Models/TaxSettings.php
app/Models/ThemeSettings.php
app/Models/StoreSettings.php
app/Models/ShippingRate.php
app/Models/Page.php
app/Services/ThemeSettingsService.php


Output:
Chunk ID: 96a0e5
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 310
Output:
app/Policies/PagePolicy.php
app/Policies/ThemePolicy.php
app/Services/ShippingCalculator.php
app/Services/NavigationService.php
database/seeders/ThemeFileSeeder.php
database/seeders/ThemeSeeder.php
database/seeders/NavigationMenuSeeder.php
database/seeders/ShippingRateSeeder.php
database/seeders/TaxSettingsSeeder.php
database/seeders/ShippingZoneSeeder.php
database/seeders/NavigationItemSeeder.php
database/seeders/PageSeeder.php
database/seeders/StoreSettingsSeeder.php
database/seeders/ThemeSettingsSeeder.php
database/factories/PageFactory.php
database/factories/NavigationItemFactory.php
database/factories/ThemeSettingsFactory.php
database/factories/ThemeFactory.php
database/factories/StoreSettingsFactory.php
database/factories/TaxSettingsFactory.php
database/factories/ThemeFileFactory.php
app/Models/Theme.php
database/factories/NavigationMenuFactory.php
database/factories/ShippingRateFactory.php
app/Models/NavigationMenu.php
app/Models/NavigationItem.php
database/factories/ShippingZoneFactory.php
app/Models/ThemeFile.php
app/Models/ShippingZone.php
app/Models/TaxSettings.php
app/Models/ThemeSettings.php
app/Models/StoreSettings.php
app/Models/ShippingRate.php
app/Models/Page.php
app/Services/ThemeSettingsService.php


sed -n '1905,1970p' specs/03-ADMIN-UI.md


sed -n '1,220p' app/Policies/PagePolicy.php app/Policies/ThemePolicy.php app/Models/Page.php app/Models/NavigationMenu.php app/Models/NavigationItem.php


sed -n '1,220p' app/Models/Theme.php app/Models/ThemeSettings.php app/Models/StoreSettings.php app/Models/TaxSettings.php app/Models/ShippingZone.php app/Models/ShippingRate.php


sed -n '1905,1970p' specs/03-ADMIN-UI.md

| Navigation                                                        |
+==================================================================+
|                                                                   |
| MENUS                                                             |
| +----------------------------+  +----------------------------+   |
| | Main Menu        [Edit]    |  | Footer Menu       [Edit]   |   |
| +----------------------------+  +----------------------------+   |
|                                                                   |
| MENU EDITOR (shown when a menu is selected)                      |
| +--------------------------------------------------------------+ |
| | Main Menu                                    [+ Add item]    | |
| |                                                               | |
| | [drag] Home               link: /            [edit] [trash]  | |
| | [drag] Products           collection: All    [edit] [trash]  | |
| | [drag] About Us           page: About        [edit] [trash]  | |
| | [drag] Contact            link: /contact     [edit] [trash]  | |
| |                                                               | |
| |                                            [Save menu]       | |
| +--------------------------------------------------------------+ |
+==================================================================+
```

**Menu list:** Each menu displayed as a card with title and "Edit" button.

**Menu editor (shown when a menu is selected):**
- Menu title as heading
- "Add item" button (ghost, plus icon) in the header
- Sortable list of menu items, each row containing:
  - Drag handle (bars icon)
  - Item label (bold text)
  - Item type and target (small, muted text: e.g., "link: /about" or "collection: Summer Sale")
  - Edit button (pencil icon) and Delete button (trash icon)
- "Save menu" primary button at the bottom

#### Menu Item Form Modal

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

**Form fields:**
- Label input (placeholder: "About Us")
- Type select: Custom link, Page, Collection, Product (live-bound)
- Conditional fields based on type:
  - **Custom link:** URL input (placeholder: "https://...")
  - **Page/Collection/Product:** Resource select dropdown populated with available resources of that type
- Cancel button (ghost) and "Save item" button (primary)

---

## 15. Apps Page

**Route:** `GET /admin/apps`

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

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

### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `installedApps` | `Collection` | Apps installed for this store |