Output:
Wall time: 4.6414 seconds
Output:
[{"type":"text","text":"# Search Results\n\n## Forms Extracting input fields to Blade components\nForms | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/forms\n\n## Extracting input fields to Blade components\n\nEven in a small component such as the `post.create` example we've been discussing, we end up duplicating lots of form field boilerplate like validation messages and labels.\n\nIt can be helpful to extract repetitive UI elements such as these into dedicated [Blade components](https://laravel.com/docs/blade#components) to be shared across your application.\n\nFor example, below is the original Blade template from the `post.create` component. We will be extracting the following two text inputs into dedicated Blade components:\n\n```blade\n<form wire:submit=\"save\">\n    <input type=\"text\" wire:model=\"title\"> <!-- [tl! highlight:3] -->\n    <div>\n        @error('title') <span class=\"error\">{{ $message }}</span> @enderror\n    </div>\n\n    <input type=\"text\" wire:model=\"content\"> <!-- [tl! highlight:3] -->\n    <div>\n        @error('content') <span class=\"error\">{{ $message }}</span> @enderror\n    </div>\n\n    <button type=\"submit\">Save</button>\n</form>\n```\n\nHere's what the template will look like after extracting a re-usable Blade component called `<x-input-text>`:\n\n```blade\n<form wire:submit=\"save\">\n    <x-input-text name=\"title\" wire:model=\"title\" /> <!-- [tl! highlight] -->\n\n    <x-input-text name=\"content\" wire:model=\"content\" /> <!-- [tl! highlight] -->\n\n    <button type=\"submit\">Save</button>\n</form>\n```\n\nNext, here's the source for the `x-input-text` component:\n\n```blade\n<!-- resources/views/components/input-text.blade.php -->\n\n@props(['name'])\n\n<input type=\"text\" name=\"{{ $name }}\" {{ $attributes }}>\n\n<div>\n    @error($name) <span class=\"error\">{{ $message }}</span> @enderror\n</div>\n```\n\nAs you can see, we took the repetitive HTML and placed it inside a dedicated Blade component.\n\nFor the most part, the Blade component contains only the extracted HTML from the original component. However, we have added two things:\n\n* The `@props` directive\n* The `{{ $attributes }}` statement on the input\n\nLet's discuss each of these additions:\n\nBy specifying `name` as a \"prop\" using `@props(['name'])` we are telling Blade: if an attribute called \"name\" is set on this component, take its value and make it available inside this component as `$name`.\n\nFor other attributes that don't have an explicit purpose, we used the `{{ $attributes }}` statement. This is used for \"attribute forwarding\", or in other words, taking any HTML attributes written on the Blade component and forwarding them onto an element within the component.\n\nThis ensures `wire:model=\"title\"` and any other extra attributes such as `disabled`, `class=\"...\"`, or `required` still get forwarded to the actual `<input>` element.\n\n### Custom form controls\n\nIn the previous example, we \"wrapped\" an input element into a re-usable Blade component we can use as if it was a native HTML input element.\n\nThis pattern is very useful; however, there might be some cases where you want to create an entire input component from scratch (without an underlying native input element), but still be able to bind its value to Livewire properties using `wire:model`.\n\nFor example, let's imagine you wanted to create an `<x-input-counter />` component that was a simple \"counter\" input written in Alpine.\n\nBefore we create a Blade component, let's first look at a simple, pure-Alpine, \"counter\" component for reference:\n\n```blade\n<div x-data=\"{ count: 0 }\">\n    <button x-on:click=\"count--\">-</button>\n\n    <span x-text=\"count\"></span>\n\n    <button x-on:click=\"count++\">+</button>\n</div>\n```\n\nAs you can see, the component above shows a number alongside two buttons to increment and decrement that number.\n\nNow, let's imagine we want to extract this component into a Blade component called `<x-input-counter />` that we would use within a component like so:\n\n```blade\n<x-input-counter wire:model=\"quantity\" />\n```\n\nCreating this component is mostly simple. We take the HTML of the counter and place it inside a Blade component template like `resources/views/components/input-counter.blade.php`.\n\nHowever, making it work with `wire:model=\"quantity\"` so that you can easily bind data from your Livewire component to the \"count\" inside this Alpine component needs one extra step.\n\nHere's the source for the component:\n\n```blade\n<!-- resources/view/components/input-counter.blade.php -->\n\n<div x-data=\"{ count: 0 }\" x-modelable=\"count\" {{ $attributes}}>\n    <button x-on:click=\"count--\">-</button>\n\n    <span x-text=\"count\"></span>\n\n    <button x-on:click=\"count++\">+</button>\n</div>\n```\n\nAs you can see, the only different bit about this HTML is the `x-modelable=\"count\"` and `{{ $attributes }}`.\n\n`x-modelable` is a utility in Alpine that tells Alpine to make a certain piece of data available for binding from outside. [The Alpine documentation has more information on this directive.](https://alpinejs.dev/directives/modelable)\n\n`{{ $attributes }}`, as we explored earlier, forwards any attributes passed into the Blade component from outside. In this case, the `wire:model` directive will be forwarded.\n\nBecause of `{{ $attributes }}`, when the HTML is rendered in the browser, `wire:model=\"quantity\"` will be rendered alongside `x-modelable=\"count\"` on the root `<div>` of the Alpine component like so:\n\n```blade\n<div x-data=\"{ count: 0 }\" x-modelable=\"count\" wire:model=\"quantity\">\n```\n\n`x-modelable=\"count\"` tells Alpine to look for any `x-model` or `wire:model` statements and use \"count\" as the data to bind them to.\n\nBecause `x-modelable` works for both `wire:model` and `x-model`, you can also use this Blade component interchangeably with Livewire and Alpine. Here’s an example of using this Blade component in a purely Alpine context:\n\n```blade\n<x-input-counter x-model=\"quantity\" />\n```\n\nCreating custom input elements in your application is extremely powerful but requires a deeper understanding of the utilities Livewire and Alpine provide and how they interact with each other.\n\n---\n\n## Wire Model Reference\nWire Model | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/wire-model\n\n## Reference\n\n```blade\nwire:model=\"propertyName\"\nwire:model=\"property.nested\"\nwire:model=\"property['nested']\"\nwire:model=\"property[0]\"\n```\n\n### Modifiers\n\n| Modifier | Description |\n|----------|-------------|\n| `.live` | Send updates to the server |\n| `.blur` | Only update on blur |\n| `.change` | Only update on change |\n| `.enter` | Only update on enter key |\n| `.lazy` | Update on change and send network request (v3 compatible) |\n| `.debounce.Xms` | Debounce updates (use with `.live`) |\n| `.throttle.Xms` | Throttle updates (use with `.live`) |\n| `.number` | Cast value to `int` on the server |\n| `.boolean` | Cast value to `bool` on the server |\n| `.fill` | Use initial value from HTML `value` attribute |\n| `.deep` | Also listen to events from child elements |\n| `.preserve-scroll` | Maintain scroll position during updates |\n\n---\n\n## Upgrade Guide > Changes from v3 > Individual transform properties\nUpgrade Guide | tailwindcss@4.x | Source: https://github.com/tailwindlabs/tailwindcss.com/blob/main/src/docs/upgrade-guide.mdx\n\n### Individual transform properties\n\nThe `rotate-*`, `scale-*`, and `translate-*` utilities are now based on the individual `rotate`, `scale`, and `translate` properties in CSS. Normally this shouldn't affect the behavior but there's a couple of cases to look out for:\n\n#### Resetting Transforms\n\nYou previously would've been able to \"reset\" your rotate, scale, and translate utilities via `transform-none`. This no longer works and you will need to reset the individual properties instead:\n\n```html\n<!-- [!code filename:HTML] -->\n<!-- [!code --:2] -->\n<button class=\"scale-150 focus:transform-none\"></button>\n<!-- [!code ++:2] -->\n<button class=\"scale-150 focus:scale-none\"></button>\n```\n\n#### Transitions\n\nIf you customize the list of transitioned properties and include `transform` (e.g. by writing `transition-[opacity,transform]`) then these utilities will no longer transition. To fix this, include the individual properties in the list. For example, if you want to transition changes when using `opacity-*` and `scale-*` utilities you should use `transition-[opacity,scale]` instead.\n\n```html\n<!-- [!code filename:HTML] -->\n<!-- [!code --:2] -->\n<button class=\"transition-[opacity,transform] hover:scale-150\"></button>\n<!-- [!code ++:2] -->\n<button class=\"transition-[opacity,scale] hover:scale-150\"></button>\n```\n\n---\n\n## Properties Security concerns\nProperties | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/properties\n\n## Security concerns\n\nWhile Livewire properties are a powerful feature, there are a few security considerations that you should be aware of before using them.\n\nIn short, always treat public properties as user input — as if they were request input from a traditional endpoint. In light of this, it's essential to validate and authorize properties before persisting them to a database — just like you would do when working with request input in a controller.\n\n### Don't trust property values\n\nTo demonstrate how neglecting to authorize and validate properties can introduce security holes in your application, the following `post.edit` component is vulnerable to attack:\n\n```php\n<?php // resources/views/components/post/⚡edit.blade.php\n\nuse Livewire\\Component;\nuse App\\Models\\Post;\n\nnew class extends Component {\n    public $id;\n    public $title;\n    public $content;\n\n    public function mount(Post $post)\n    {\n        $this->id = $post->id;\n        $this->title = $post->title;\n        $this->content = $post->content;\n    }\n\n    public function update()\n    {\n        $post = Post::findOrFail($this->id);\n\n        $post->update([\n            'title' => $this->title,\n            'content' => $this->content,\n        ]);\n\n        session()->flash('message', 'Post updated successfully!');\n    }\n};\n```\n\n```blade\n<form wire:submit=\"update\">\n    <input type=\"text\" wire:model=\"title\">\n    <input type=\"text\" wire:model=\"content\">\n\n    <button type=\"submit\">Update</button>\n</form>\n```\n\nAt first glance, this component may look completely fine. But, let's walk through how an attacker could use the component to do unauthorized things in your application.\n\nBecause we are storing the `id` of the post as a public property on the component, it can be manipulated on the client just the same as the `title` and `content` properties.\n\nIt doesn't matter that we didn't write an input with `wire:model=\"id\"`. A malicious user can easily change the view to the following using their browser DevTools:\n\n```blade\n<form wire:submit=\"update\">\n    <input type=\"text\" wire:model=\"id\"> <!-- [tl! highlight] -->\n    <input type=\"text\" wire:model=\"title\">\n    <input type=\"text\" wire:model=\"content\">\n\n    <button type=\"submit\">Update</button>\n</form>\n```\n\nNow the malicious user can update the `id` input to the ID of a different post model. When the form is submitted and `update()` is called, `Post::findOrFail()` will return and update a post the user is not the owner of.\n\nTo prevent this kind of attack, we can use one or both of the following strategies:\n\n* Authorize the input\n* Lock the property from updates\n\n#### Authorizing the input\n\nBecause `$id` can be manipulated client-side with something like `wire:model`, just like in a controller, we can use [Laravel's authorization](https://laravel.com/docs/authorization) to make sure the current user can update the post:\n\n```php\npublic function update()\n{\n    $post = Post::findOrFail($this->id);\n\n    $this->authorize('update', $post); // [tl! highlight]\n\n    $post->update(...);\n}\n```\n\nIf a malicious user mutates the `$id` property, the added authorization will catch it and throw an error.\n\n#### Locking the property\n\nLivewire also allows you to \"lock\" properties in order to prevent properties from being modified on the client-side. You can \"lock\" a property from client-side manipulation using the `#[Locked]` attribute:\n\n```php\nuse Livewire\\Attributes\\Locked;\nuse Livewire\\Component;\n\nnew class extends Component {\n    #[Locked] // [tl! highlight]\n    public $id;\n\n    // ...\n};\n```\n\nNow, if a user tries to modify `$id` on the front end, an error will be thrown.\n\nBy using `#[Locked]`, you can assume this property has not been manipulated anywhere outside your component's class.\n\nFor more information on locking properties, [consult the Locked attribute documentation](/docs/4.x/attribute-locked).\n\n#### Eloquent models and locking\n\nWhen an Eloquent model is assigned to a Livewire component property, Livewire will automatically lock the property and ensure the ID isn't changed, so that you are safe from these kinds of attacks:\n\n```php\n<?php // resources/views/components/post/⚡edit.blade.php\n\nuse Livewire\\Component;\nuse App\\Models\\Post;\n\nnew class extends Component {\n    public Post $post; // [tl! highlight]\n    public $title;\n    public $content;\n\n    public function mount(Post $post)\n    {\n        $this->post = $post;\n        $this->title = $post->title;\n        $this->content = $post->content;\n    }\n\n    public function update()\n    {\n        $this->post->update([\n            'title' => $this->title,\n            'content' => $this->content,\n        ]);\n\n        session()->flash('message', 'Post updated successfully!');\n    }\n};\n```\n\n### Properties expose system information to the browser\n\nAnother essential thing to remember is that Livewire properties are serialized or \"dehydrated\" before they are sent to the browser. This means that their values are converted to a format that can be sent over the wire and understood by JavaScript. This format can expose information about your application to the browser, including the names and class names of your properties.\n\nFor example, suppose you have a Livewire component that defines a public property named `$post`. This property contains an instance of a `Post` model from your database. In this case, the dehydrated value of this property sent over the wire might look something like this:\n\n```json\n{\n    \"type\": \"model\",\n    \"class\": \"App\\Models\\Post\",\n    \"key\": 1,\n    \"relationships\": []\n}\n```\n\nAs you can see, the dehydrated value of the `$post` property includes the class name of the model (`App\\Models\\Post`) as well as the ID and any relationships that have been eager-loaded.\n\nIf you don't want to expose the class name of the model, you can use Laravel's \"morphMap\" functionality from a service provider to assign an alias to a model class name:\n\n```php\n<?php\n\nnamespace App\\Providers;\n\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation;\n\nclass AppServiceProvider extends ServiceProvider\n{\n    public function boot()\n    {\n        Relation::morphMap([\n            'post' => 'App\\Models\\Post',\n        ]);\n    }\n}\n```\n\nNow, when the Eloquent model is \"dehydrated\" (serialized), the original class name won't be exposed, only the \"post\" alias:\n\n```json\n{\n    \"type\": \"model\",\n    \"class\": \"App\\Models\\Post\", // [tl! remove]\n    \"class\": \"post\", // [tl! add]\n    \"key\": 1,\n    \"relationships\": []\n}\n```\n\n### Eloquent constraints aren't preserved between requests\n\nTypically, Livewire is able to preserve and recreate server-side properties between requests; however, there are certain scenarios where preserving values are impossible between requests.\n\nFor example, when storing Eloquent collections as Livewire properties, additional query constraints like `select(...)` will not be re-applied on subsequent requests.\n\nTo demonstrate, consider the following `show-todos` component with a `select()` constraint applied to the `Todos` Eloquent collection:\n\n```php\n<?php // resources/views/components/⚡show-todos.blade.php\n\nuse Illuminate\\Support\\Facades\\Auth;\nuse Livewire\\Component;\n\nnew class extends Component {\n    public $todos;\n\n    public function mount()\n    {\n        $this->todos = Auth::user()\n            ->todos()\n            ->select(['title', 'content']) // [tl! highlight]\n            ->get();\n    }\n};\n```\n\nWhen this component is initially loaded, the `$todos` property will be set to an Eloquent collection of the user's todos; however, only the `title` and `content` fields of each row in the database will have been queried and loaded into each of the models.\n\nWhen Livewire _hydrates_ the JSON of this property back into PHP on a subsequent request, the select constraint will have been lost.\n\nTo ensure the integrity of Eloquent queries, we recommend that you use [computed properties](/docs/4.x/computed-properties) instead of properties.\n\nComputed properties are methods in your component marked with the `#[Computed]` attribute. They can be accessed as a dynamic property that isn't stored as part of the component's state but is instead evaluated on-the-fly.\n\nHere's the above example re-written using a computed property:\n\n```php\n<?php // resources/views/components/⚡show-todos.blade.php\n\nuse Illuminate\\Support\\Facades\\Auth;\nuse Livewire\\Attributes\\Computed;\nuse Livewire\\Component;\n\nnew class extends Component {\n    #[Computed] // [tl! highlight]\n    public function todos()\n    {\n        return Auth::user()\n            ->todos()\n            ->select(['title', 'content'])\n            ->get();\n    }\n};\n```\n\nHere's how you would access these _todos_ from the Blade view:\n\n```blade\n<ul>\n    @foreach ($this->todos as $todo)\n        <li wire:key=\"{{ $loop->index }}\">{{ $todo }}</li>\n    @endforeach\n</ul>\n```\n\nNotice, inside your views, you can only access computed properties on the `$this` object like so: `$this->todos`.\n\nYou can also access `$todos` from inside your class. For example, if you had a `markAllAsComplete()` action:\n\n```php\n<?php // resources/views/components/⚡show-todos.blade.php\n\nuse Illuminate\\Support\\Facades\\Auth;\nuse Livewire\\Attributes\\Computed;\nuse Livewire\\Component;\n\nnew class extends Component {\n    #[Computed]\n    public function todos()\n    {\n        return Auth::user()\n            ->todos()\n            ->select(['title', 'content'])\n            ->get();\n    }\n\n    public function markAllComplete() // [tl! highlight:3]\n    {\n        $this->todos->each->complete();\n    }\n};\n```\n\nYou might wonder why not just call `$this->todos()` as a method directly where you need to? Why use `#[Computed]` in the first place?\n\nThe reason is that computed properties have a performance advantage, since they are automatically memoized after their first usage during a single request. This means you can freely access `$this->todos` within your component and be assured that the actual method will only be called once, so that you don't run an expensive query multiple times in the same request.\n\nFor more information, [visit the computed properties documentation](/docs/4.x/computed-properties).\n\n---\n\n## Migrating From Phpunit Guide\nMigrating From Phpunit Guide | pestphp/pest@4.x | Source: https://github.com/pestphp/docs/blob/4.x/migrating-from-phpunit-guide.md\n\n---\ntitle: Migrating from PHPUnit\ndescription: Migrating from PHPUnit to Pest is a simple process that can be completed in just a few steps.\n---\n\n# Migrating from PHPUnit\n\nPest is built on top of PHPUnit, so migrating from PHPUnit to Pest is a simple process that can be completed in just a few steps. Once you have Pest installed, you should require the `pestphp/pest-plugin-drift` package as a \"dev\" dependency in your project.\n\n```bash\ncomposer require pestphp/pest-plugin-drift --dev\n```\n\nDrift is a simple yet powerful plugin that will automatically convert your PHPUnit tests to Pest, simply by running the `--drift` option.\n\n```bash\n./vendor/bin/pest --drift\n```\n\nSo, typically, a PHPUnit test looks like this:\n\n```php\n<?php\n\nnamespace Tests\\Unit;\n\nuse PHPUnit\\Framework\\TestCase;\n\nclass ExampleTest extends TestCase\n{\n    public function test_that_true_is_true(): void\n    {\n        $this->assertTrue(true);\n    }\n}\n```\n\nShould look like this after running `--drift`:\n\n```php\ntest('true is true', function () {\n    expect(true)->toBeTrue();\n});\n```\n\n---\n## Convert Phpunit tests only for certain folder\n\nIf you want to convert Phpunit tests only for certain folder, you can pass _path_ as first argument when calling `--drift`:\n\nExample call, if you want to run the conversion for folder `/tests/Helpers`: \n```console \n/vendor/bin/pest --drift tests/Helpers\n```\n\nOutput:\n```console\n/vendor/bin/pest --drift tests/Helpers\n\n✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔✔\n\nINFO  The [tests/Helpers] directory has been migrated to PEST with XY files changed.\n```\n\n---\n\nThe output will contain a summary of the conversion process, as well as a list of the files that were converted.\n\nWhile most of your tests should be converted automatically, and you should be able to run them without any issues, there are some cases where you may need to manually convert some of your tests.\n\n---\n\nOf course, this particular chapter is only for those who are migrating from PHPUnit. Next, let's learn how you can contribute to the growth of Pest: [Community Guide](/docs/community-guide)\n\n\n---\n\n## Migrations Indexes Foreign Key Constraints\nMigrations | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/migrations.md\n\n<a name=\"foreign-key-constraints\"></a>\n### Foreign Key Constraints\n\nLaravel also provides support for creating foreign key constraints, which are used to force referential integrity at the database level. For example, let's define a `user_id` column on the `posts` table that references the `id` column on a `users` table:\n\n```php\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Support\\Facades\\Schema;\n\nSchema::table('posts', function (Blueprint $table) {\n    $table->unsignedBigInteger('user_id');\n\n    $table->foreign('user_id')->references('id')->on('users');\n});\n```\n\nSince this syntax is rather verbose, Laravel provides additional, terser methods that use conventions to provide a better developer experience. When using the `foreignId` method to create your column, the example above can be rewritten like so:\n\n```php\nSchema::table('posts', function (Blueprint $table) {\n    $table->foreignId('user_id')->constrained();\n});\n```\n\nThe `foreignId` method creates an `UNSIGNED BIGINT` equivalent column, while the `constrained` method will use conventions to determine the table and column being referenced. If your table name does not match Laravel's conventions, you may manually provide it to the `constrained` method. In addition, the name that should be assigned to the generated index may be specified as well:\n\n```php\nSchema::table('posts', function (Blueprint $table) {\n    $table->foreignId('user_id')->constrained(\n        table: 'users', indexName: 'posts_user_id'\n    );\n});\n```\n\nYou may also specify the desired action for the \"on delete\" and \"on update\" properties of the constraint:\n\n```php\n$table->foreignId('user_id')\n    ->constrained()\n    ->onUpdate('cascade')\n    ->onDelete('cascade');\n```\n\nAn alternative, expressive syntax is also provided for these actions:\n\n<div class=\"overflow-auto\">\n\n| Method                        | Description                                       |\n| ----------------------------- | ------------------------------------------------- |\n| `$table->cascadeOnUpdate();`  | Updates should cascade.                           |\n| `$table->restrictOnUpdate();` | Updates should be restricted.                     |\n| `$table->nullOnUpdate();`     | Updates should set the foreign key value to null. |\n| `$table->noActionOnUpdate();` | No action on updates.                             |\n| `$table->cascadeOnDelete();`  | Deletes should cascade.                           |\n| `$table->restrictOnDelete();` | Deletes should be restricted.                     |\n| `$table->nullOnDelete();`     | Deletes should set the foreign key value to null. |\n| `$table->noActionOnDelete();` | Prevents deletes if child records exist.          |\n\n</div>\n\nAny additional [column modifiers](#column-modifiers) must be called before the `constrained` method:\n\n```php\n$table->foreignId('user_id')\n    ->nullable()\n    ->constrained();\n```\n\n---\n\n## Transform Examples\nTransform | tailwindcss@4.x | Source: https://github.com/tailwindlabs/tailwindcss.com/blob/main/src/docs/transform.mdx\n\n## Examples\n\n### Hardware acceleration\n\nIf your transition performs better when rendered by the GPU instead of the CPU, you can force hardware acceleration by adding the `transform-gpu` utility:\n\n```html\n<!-- [!code classes:transform-gpu] -->\n<div class=\"scale-150 transform-gpu\">\n  <!-- ... -->\n</div>\n```\n\nUse the `transform-cpu` utility to force things back to the CPU if you need to undo this conditionally.\n\n### Removing transforms\n\nUse the `transform-none` utility to remove all of the transforms on an element at once:\n\n```html\n<!-- [!code classes:transform-none] -->\n<div class=\"skew-y-3 md:transform-none\">\n  <!-- ... -->\n</div>\n```\n\n### Using a custom value\n\n<UsingACustomValue utility=\"transform\" value=\"matrix(1,2,3,4,5,6)\" />\n\n\n---\n\n## Uploads\nUploads | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/uploads\n\nLivewire offers powerful support for uploading files within your components.\n\nFirst, add the `WithFileUploads` trait to your component. Once this trait has been added to your component, you can use `wire:model` on file inputs as if they were any other input type and Livewire will take care of the rest.\n\nHere's an example of a simple component that handles uploading a photo:\n\n```php\n<?php // resources/views/components/⚡upload-photo.blade.php\n\nuse Livewire\\Attributes\\Validate;\nuse Livewire\\WithFileUploads;\nuse Livewire\\Component;\n\nnew class extends Component {\n    use WithFileUploads;\n\n    #[Validate('image|max:1024')] // 1MB Max\n    public $photo;\n\n    public function save()\n    {\n        $this->photo->store(path: 'photos');\n    }\n};\n```\n\n```blade\n<form wire:submit=\"save\">\n    <input type=\"file\" wire:model=\"photo\">\n\n    @error('photo') <span class=\"error\">{{ $message }}</span> @enderror\n\n    <button type=\"submit\">Save photo</button>\n</form>\n```\n\n> [!warning] The \"upload\" method is reserved\n> Notice the above example uses a \"save\" method instead of an \"upload\" method. This is a common \"gotcha\". The term \"upload\" is reserved by Livewire. You cannot use it as a method or property name in your component.\n\nFrom the developer's perspective, handling file inputs is no different than handling any other input type: Add `wire:model` to the `<input>` tag and everything else is taken care of for you.\n\nHowever, more is happening under the hood to make file uploads work in Livewire. Here's a glimpse at what goes on when a user selects a file to upload:\n\n1. When a new file is selected, Livewire's JavaScript makes an initial request to the component on the server to get a temporary \"signed\" upload URL.\n2. Once the URL is received, JavaScript does the actual \"upload\" to the signed URL, storing the upload in a temporary directory designated by Livewire and returning the new temporary file's unique hash ID.\n3. Once the file is uploaded and the unique hash ID is generated, Livewire's JavaScript makes a final request to the component on the server, telling it to \"set\" the desired public property to the new temporary file.\n4. Now, the public property (in this case, `$photo`) is set to the temporary file upload and is ready to be stored or validated at any point.\n\n---\n\n## Events Dispatching events from Blade templates\nEvents | livewire/livewire@4.x | Source: https://livewire.laravel.com/docs/4.x/events\n\n## Dispatching events from Blade templates\n\nYou can dispatch events directly from your Blade templates using the `$dispatch` JavaScript function. This is useful when you want to trigger an event from a user interaction, such as a button click:\n\n```blade\n<button wire:click=\"$dispatch('show-post-modal', { id: {{ $post->id }} })\">\n    EditPost\n</button>\n```\n\nIn this example, when the button is clicked, the `show-post-modal` event will be dispatched with the specified data.\n\nIf you want to dispatch an event directly to another component you can use the `$dispatchTo()` JavaScript function:\n\n```blade\n<button wire:click=\"$dispatchTo('posts', 'show-post-modal', { id: {{ $post->id }} })\">\n    EditPost\n</button>\n```\n\nIn this example, when the button is clicked, the `show-post-modal` event will be dispatched directly to the `Posts` component.\n\n_[Truncated]_"}]


rg -n "navigation|menu|parent|drag|drop" specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md specs/04-STOREFRONT-UI.md specs/07-SEEDERS-AND-TEST-DATA.md specs/08-PLAYWRIGHT-E2E-PLAN.md specs/09-IMPLEMENTATION-ROADMAP.md


sed -n '1,260p' app/Livewire/Admin/Navigation/Index.php && sed -n '1,260p' resources/views/livewire/admin/navigation/index.blade.php


sed -n '1,220p' app/Services/NavigationService.php && sed -n '1,220p' app/Models/NavigationItem.php && sed -n '1,160p' database/migrations/2026_05_03_223848_create_navigation_items_table.php


sed -n '1,260p' app/Livewire/Admin/Navigation/Index.php && sed -n '1,260p' resources/views/livewire/admin/navigation/index.blade.php

index.blade.php

<?php

namespace App\Livewire\Admin\Navigation;

use App\Enums\NavigationItemType;
use App\Models\Collection as ProductCollection;
use App\Models\NavigationItem;
use App\Models\NavigationMenu;
use App\Models\Page;
use App\Models\Product;
use App\Models\Store;
use App\Services\NavigationService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Index extends Component
{
    use AuthorizesRequests;

    #[Locked]
    public int $storeId;

    public ?int $selectedMenuId = null;

    /**
     * @var array<int, array{id: int|null, label: string, type: string, url: string|null, resource_id: int|null}>
     */
    public array $menuItems = [];

    public ?int $editingItemIndex = null;

    public string $itemLabel = '';

    public string $itemType = 'link';

    public string $itemUrl = '';

    public ?int $itemResourceId = null;

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

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

        $this->storeId = $store->getKey();
        $this->selectedMenuId = $this->menus()->first()?->getKey();

        if ($this->selectedMenuId !== null) {
            $this->loadMenuItems();
        }
    }

    public function selectMenu(int $menuId): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->selectedMenuId = $this->menu($menuId)->getKey();
        $this->loadMenuItems();
        $this->cancelItem();
    }

    public function addItem(): void
    {
        $this->editingItemIndex = null;
        $this->itemLabel = '';
        $this->itemType = 'link';
        $this->itemUrl = '';
        $this->itemResourceId = null;
    }

    public function editItem(int $index): void
    {
        abort_unless(isset($this->menuItems[$index]), 404);

        $item = $this->menuItems[$index];

        $this->editingItemIndex = $index;
        $this->itemLabel = $item['label'];
        $this->itemType = $item['type'];
        $this->itemUrl = (string) ($item['url'] ?? '');
        $this->itemResourceId = $item['resource_id'];
    }

    public function saveItem(): void
    {
        $this->authorize('update', $this->scopedStore());

        $validated = $this->validate([
            'itemLabel' => ['required', 'string', 'max:255'],
            'itemType' => ['required', Rule::in(array_column(NavigationItemType::cases(), 'value'))],
            'itemUrl' => [Rule::requiredIf($this->itemType === NavigationItemType::Link->value), 'nullable', 'string', 'max:255'],
            'itemResourceId' => [Rule::requiredIf($this->itemType !== NavigationItemType::Link->value), 'nullable', 'integer'],
        ], [], [
            'itemLabel' => 'label',
            'itemType' => 'type',
            'itemUrl' => 'URL',
            'itemResourceId' => 'resource',
        ]);

        if ($this->itemType !== NavigationItemType::Link->value && ! $this->resourceExists($this->itemType, (int) $this->itemResourceId)) {
            $this->addError('itemResourceId', __('Select a valid resource for this store.'));

            return;
        }

        $item = [
            'id' => $this->editingItemIndex !== null ? $this->menuItems[$this->editingItemIndex]['id'] : null,
            'label' => $validated['itemLabel'],
            'type' => $validated['itemType'],
            'url' => $this->itemType === NavigationItemType::Link->value ? $validated['itemUrl'] : null,
            'resource_id' => $this->itemType === NavigationItemType::Link->value ? null : (int) $this->itemResourceId,
        ];

        if ($this->editingItemIndex === null) {
            $this->menuItems[] = $item;
        } else {
            $this->menuItems[$this->editingItemIndex] = $item;
        }

        $this->cancelItem();
    }

    public function removeItem(int $index): void
    {
        unset($this->menuItems[$index]);

        $this->menuItems = array_values($this->menuItems);
    }

    public function moveItemUp(int $index): void
    {
        if ($index <= 0 || ! isset($this->menuItems[$index])) {
            return;
        }

        [$this->menuItems[$index - 1], $this->menuItems[$index]] = [$this->menuItems[$index], $this->menuItems[$index - 1]];
    }

    public function moveItemDown(int $index): void
    {
        if (! isset($this->menuItems[$index], $this->menuItems[$index + 1])) {
            return;
        }

        [$this->menuItems[$index], $this->menuItems[$index + 1]] = [$this->menuItems[$index + 1], $this->menuItems[$index]];
    }

    public function saveMenu(NavigationService $navigation): void
    {
        $this->authorize('update', $this->scopedStore());

        $menu = $this->selectedMenu();

        DB::transaction(function () use ($menu): void {
            NavigationItem::withoutGlobalScopes()
                ->where('menu_id', $menu->getKey())
                ->delete();

       …4153 chars truncated… px-4 py-3 text-left text-sm {{ $selectedMenuId === $menu->getKey() ? 'border-blue-500 bg-blue-50 text-blue-950 dark:border-blue-400 dark:bg-blue-950/40 dark:text-blue-100' : 'border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800' }}"
                >
                    {{ $menu->title }}
                    {{ $menu->handle }}
                </button>
            @endforeach

                    <flux:heading size="lg">{{ $selectedMenu->title }}</flux:heading>
                    <flux:text class="mt-1">{{ count($menuItems) }} items</flux:text>

                <flux:button type="button" wire:click="addItem" variant="filled" icon="plus">Add item</flux:button>

                @forelse ($menuItems as $index => $item)

                                <flux:button type="button" wire:click="moveItemUp({{ $index }})" size="sm" variant="ghost" icon="arrow-up" aria-label="Move {{ $item['label'] }} up" />
                                <flux:button type="button" wire:click="moveItemDown({{ $index }})" size="sm" variant="ghost" icon="arrow-down" aria-label="Move {{ $item['label'] }} down" />

{{ $item['label'] }}

{{ $this->targetLabel($item) }}

                            <flux:button type="button" wire:click="editItem({{ $index }})" size="sm" variant="filled">Edit</flux:button>
                            <flux:button type="button" wire:click="removeItem({{ $index }})" size="sm" variant="danger">Remove</flux:button>

                @empty

No menu items.

                @endforelse

                <flux:button type="button" wire:click="saveMenu" variant="primary">Save menu</flux:button>

        <form wire:submit="saveItem" class="rounded-lg border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900">
            <flux:heading size="lg">{{ $editingItemIndex === null ? 'Add item' : 'Edit item' }}</flux:heading>

                <flux:input wire:model="itemLabel" label="Label" />
                <flux:error name="itemLabel" />

                <flux:select wire:model.live="itemType" label="Type">
                    <flux:select.option value="link">Custom link</flux:select.option>
                    <flux:select.option value="page">Page</flux:select.option>
                    <flux:select.option value="collection">Collection</flux:select.option>
                    <flux:select.option value="product">Product</flux:select.option>
                </flux:select>

                @if ($itemType === 'link')
                    <flux:input wire:model="itemUrl" label="URL" placeholder="/collections" />
                    <flux:error name="itemUrl" />
                @else
                    <flux:select wire:model="itemResourceId" label="Resource">
                        <flux:select.option value="">Select resource</flux:select.option>
                        @foreach ($resources as $resource)
                            <flux:select.option value="{{ $resource->getKey() }}">{{ $resource->title }}</flux:select.option>
                        @endforeach
                    </flux:select>
                    <flux:error name="itemResourceId" />
                @endif

                    <flux:button type="button" wire:click="cancelItem" variant="ghost">Cancel</flux:button>
                    <flux:button type="submit" variant="primary">{{ $editingItemIndex === null ? 'Add item' : 'Update item' }}</flux:button>

        </form>


sed -n '1,220p' app/Services/NavigationService.php && sed -n '1,220p' app/Models/NavigationItem.php && sed -n '1,160p' database/migrations/2026_05_03_223848_create_navigation_items_table.php

NavigationItem.php

<?php

namespace App\Services;

use App\Enums\NavigationItemType;
use App\Models\Collection as ProductCollection;
use App\Models\NavigationItem;
use App\Models\NavigationMenu;
use App\Models\Page;
use App\Models\Product;
use App\Models\Store;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

class NavigationService
{
    /**
     * @return array<int, array{label: string, url: string, type: string, external: bool, children: array<int, mixed>}>
     */
    public function forHandle(Store $store, string $handle): array
    {
        $menu = NavigationMenu::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('handle', $handle)
            ->first();

        if ($menu === null) {
            return [];
        }

        return $this->buildTree($menu);
    }

    /**
     * @return array<int, array{label: string, url: string, type: string, external: bool, children: array<int, mixed>}>
     */
    public function buildTree(NavigationMenu $menu): array
    {
        return Cache::remember($this->cacheKey($menu), now()->addMinutes(5), function () use ($menu): array {
            return $menu->items()
                ->with('menu')
                ->get()
                ->map(fn (NavigationItem $item): array => [
                    'label' => $item->label,
                    'url' => $this->resolveUrl($item),
                    'type' => $item->type->value,
                    'external' => $this->isExternal((string) ($item->url ?? '')),
                    'children' => [],
                ])
                ->all();
        });
    }

    public function resolveUrl(NavigationItem $item): string
    {
        return match ($item->type) {
            NavigationItemType::Link => $item->url ?: '#',
            NavigationItemType::Page => $this->pageUrl($item),
            NavigationItemType::Collection => $this->collectionUrl($item),
            NavigationItemType::Product => $this->productUrl($item),
        };
    }

    public function forget(NavigationMenu $menu): void
    {
        Cache::forget($this->cacheKey($menu));
    }

    private function pageUrl(NavigationItem $item): string
    {
        $page = Page::withoutGlobalScopes()
            ->where('store_id', $this->storeIdFor($item))
            ->find($item->resource_id);

        return $page ? route('pages.show', $page->handle, false) : '#';
    }

    private function collectionUrl(NavigationItem $item): string
    {
        $collection = ProductCollection::withoutGlobalScopes()
            ->where('store_id', $this->storeIdFor($item))
            ->find($item->resource_id);

        return $collection ? route('collections.show', $collection->handle, false) : '#';
    }

    private function productUrl(NavigationItem $item): string
    {
        $product = Product::withoutGlobalScopes()
            ->where('store_id', $this->storeIdFor($item))
            ->find($item->resource_id);

        return $product ? route('products.show', $product->handle, false) : '#';
    }

    private function storeIdFor(NavigationItem $item): ?int
    {
        if ($item->relationLoaded('menu') && $item->menu !== null) {
            return (int) $item->menu->store_id;
        }

        return NavigationMenu::withoutGlobalScopes()
            ->whereKey($item->menu_id)
            ->value('store_id');
    }

    private function isExternal(string $url): bool
    {
        return Str::startsWith($url, ['http://', 'https://', 'mailto:', 'tel:']);
    }

    private function cacheKey(NavigationMenu $menu): string
    {
        return "navigation:{$menu->store_id}:{$menu->handle}";
    }
}
<?php

namespace App\Models;

use App\Enums\NavigationItemType;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class NavigationItem extends Model
{
    /** @use HasFactory<\Database\Factories\NavigationItemFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'menu_id',
        'type',
        'label',
        'url',
        'resource_id',
        'position',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'type' => 'link',
        'position' => 0,
    ];

    protected static function booted(): void
    {
        static::addGlobalScope('current_store', function (Builder $builder): void {
            if (! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if (! $store instanceof Store) {
                return;
            }

            $builder->whereHas('menu', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

    /**
     * @return BelongsTo<NavigationMenu, $this>
     */
    public function menu(): BelongsTo
    {
        return $this->belongsTo(NavigationMenu::class, 'menu_id');
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'type' => NavigationItemType::class,
            'resource_id' => 'integer',
            'position' => 'integer',
        ];
    }
}
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('navigation_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('menu_id')->constrained('navigation_menus')->cascadeOnDelete();
            $table->enum('type', ['link', 'page', 'collection', 'product'])->default('link');
            $table->string('label');
            $table->string('url')->nullable();
            $table->unsignedBigInteger('resource_id')->nullable();
            $table->unsignedInteger('position')->default(0);

            $table->index('menu_id');
            $table->index(['menu_id', 'position']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('navigation_items');
    }
};


rg -n "navigation|menu|parent|drag|drop" specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md specs/04-STOREFRONT-UI.md specs/07-SEEDERS-AND-TEST-DATA.md specs/08-PLAYWRIGHT-E2E-PLAN.md specs/09-IMPLEMENTATION-ROADMAP.md

rg -n 'navigation|menu|parent|drag|drop' specs/01-DATABASE-SCHEMA.md specs/03-ADMIN-UI.md specs/04-STOREFRONT-UI.md specs/07-SEEDERS-AND-TEST-DATA.md specs/08-PLAYWRIGHT-E2E-PLAN.md specs/09-IMPLEMENTATION-ROADMAP.md

Question: navigation|menu|parent|drag|drop

specs/01-DATABASE-SCHEMA.md:447:### navigation_menus
specs/01-DATABASE-SCHEMA.md:449:Named menus (e.g. main-menu, footer-menu).
specs/01-DATABASE-SCHEMA.md:455:| handle | TEXT | No | - | UNIQUE per (store_id, handle) | e.g. "main-menu" |
specs/01-DATABASE-SCHEMA.md:462:- `idx_navigation_menus_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store
specs/01-DATABASE-SCHEMA.md:463:- `idx_navigation_menus_store_id` on (`store_id`) - FK lookup, listing menus per store
specs/01-DATABASE-SCHEMA.md:467:### navigation_items
specs/01-DATABASE-SCHEMA.md:469:Individual links within a navigation menu (supports nesting via position).
specs/01-DATABASE-SCHEMA.md:474:| menu_id | INTEGER | No | - | FK -> navigation_menus(id) ON DELETE CASCADE | Parent menu |
specs/01-DATABASE-SCHEMA.md:483:- `idx_navigation_items_menu_id` on (`menu_id`) - FK lookup, listing items per menu
specs/01-DATABASE-SCHEMA.md:484:- `idx_navigation_items_menu_position` on (`menu_id`, `position`) - Ordered item listing
specs/01-DATABASE-SCHEMA.md:1326:    stores ||--o{ navigation_menus : "has"
specs/01-DATABASE-SCHEMA.md:1327:    navigation_menus ||--o{ navigation_items : "contains"
specs/01-DATABASE-SCHEMA.md:1352:    navigation_menus {
specs/01-DATABASE-SCHEMA.md:1358:    navigation_items {
specs/01-DATABASE-SCHEMA.md:1360:        INTEGER menu_id FK
specs/01-DATABASE-SCHEMA.md:1530:        navigation_menus
specs/01-DATABASE-SCHEMA.md:1548:        navigation_items
specs/01-DATABASE-SCHEMA.md:1590:    stores --> navigation_menus
specs/01-DATABASE-SCHEMA.md:1606:    navigation_menus --> navigation_items
specs/01-DATABASE-SCHEMA.md:1645:- **Batch 3** (depends on Batch 2): `store_domains`, `store_users`, `store_settings`, `customers`, `themes`, `pages`, `navigation_menus`, `search_settings`, `shipping_zones`, `tax_settings`, `discounts`, `products`, `collections`, `app_installations`, `oauth_clients`
specs/01-DATABASE-SCHEMA.md:1646:- **Batch 4** (depends on Batch 3): `product_options`, `product_variants`, `product_media`, `collection_products`, `customer_addresses`, `carts`, `navigation_items`, `theme_files`, `theme_settings`, `search_queries`, `shipping_rates`, `oauth_tokens`, `webhook_subscriptions`
specs/01-DATABASE-SCHEMA.md:1665:6. **Selective indexing** - Not every column is indexed. Columns queried only via their parent's primary key (e.g., `cart_lines` always accessed through `cart_id`) rely on the foreign key index alone. This keeps write overhead and database size manageable.
specs/01-DATABASE-SCHEMA.md:1675:| 3 - Themes, Storefront, Localization | themes, theme_files, theme_settings, pages, navigation_menus, navigation_items | 6 |
specs/07-SEEDERS-AND-TEST-DATA.md:9:The main `DatabaseSeeder` calls child seeders in a strict order that respects foreign key dependencies. Each seeder targets a specific entity type. The ordering ensures parent records exist before children are created.
specs/07-SEEDERS-AND-TEST-DATA.md:30:| 16 | NavigationSeeder | Stores, Collections, Pages | Creates menus linking to collections and pages |
specs/07-SEEDERS-AND-TEST-DATA.md:701:| menu_id | Creates a new NavigationMenu via its factory |
specs/07-SEEDERS-AND-TEST-DATA.md:1973:**Acme Fashion - Main Menu (handle: main-menu, title: "Main Menu"):**
specs/07-SEEDERS-AND-TEST-DATA.md:1983:**Acme Fashion - Footer Menu (handle: footer-menu, title: "Footer Menu"):**
specs/07-SEEDERS-AND-TEST-DATA.md:1993:**Acme Electronics - Main Menu (handle: main-menu, title: "Main Menu"):**
specs/04-STOREFRONT-UI.md:114:**Accessibility:** Allows keyboard users to bypass the header navigation and jump directly to the main content area.
specs/04-STOREFRONT-UI.md:131:The header serves as the primary navigation banner.
specs/04-STOREFRONT-UI.md:144:| Main Navigation | Center | Horizontal nav with links sourced from the "main-menu" navigation menu. Supports one level of dropdown submenus (revealed on hover/focus) |
specs/04-STOREFRONT-UI.md:159:| Hamburger menu | Left | Toggle button for mobile navigation drawer |
specs/04-STOREFRONT-UI.md:165:When enabled in theme settings, the header sticks to the top of the viewport with a semi-transparent background and backdrop blur. A subtle bottom border appears on scroll.
specs/04-STOREFRONT-UI.md:173:| Backdrop | Semi-transparent dark overlay behind the drawer |
specs/04-STOREFRONT-UI.md:176:| Submenus | Collapsible accordion pattern with chevron icons |
specs/04-STOREFRONT-UI.md:177:| Account link | Positioned at the bottom of the navigation list |
specs/04-STOREFRONT-UI.md:179:| ARIA | Labeled as "Mobile navigation" |
specs/04-STOREFRONT-UI.md:202:| Link columns | Responsive grid: 2 columns on mobile, 3 on tablet, up to 4 on desktop. Links sourced from "footer-menu" navigation menu. Each column has an uppercase heading and a list of links |
specs/04-STOREFRONT-UI.md:267:| Background | Full-width image (cover, centered) or gradient. Image set via theme settings. Semi-transparent dark overlay for text readability |
specs/04-STOREFRONT…6053 chars truncated…ecs/03-ADMIN-UI.md:1899:| `saveMenu()` | Persists all menu items |
specs/03-ADMIN-UI.md:1913:| MENU EDITOR (shown when a menu is selected)                      |
specs/03-ADMIN-UI.md:1917:| | [drag] Home               link: /            [edit] [trash]  | |
specs/03-ADMIN-UI.md:1918:| | [drag] Products           collection: All    [edit] [trash]  | |
specs/03-ADMIN-UI.md:1919:| | [drag] About Us           page: About        [edit] [trash]  | |
specs/03-ADMIN-UI.md:1920:| | [drag] Contact            link: /contact     [edit] [trash]  | |
specs/03-ADMIN-UI.md:1922:| |                                            [Save menu]       | |
specs/03-ADMIN-UI.md:1927:**Menu list:** Each menu displayed as a card with title and "Edit" button.
specs/03-ADMIN-UI.md:1929:**Menu editor (shown when a menu is selected):**
specs/03-ADMIN-UI.md:1932:- Sortable list of menu items, each row containing:
specs/03-ADMIN-UI.md:1937:- "Save menu" primary button at the bottom
specs/03-ADMIN-UI.md:1945:| Heading | "Edit menu item" or "Add menu item" (dynamic) |
specs/03-ADMIN-UI.md:1952:  - **Page/Collection/Product:** Resource select dropdown populated with available resources of that type
specs/03-ADMIN-UI.md:2192:- Date range dropdown (same as Dashboard: Today, Last 7 days, Last 30 days, Custom)
specs/03-ADMIN-UI.md:2299:3. Filter controls (dropdowns, tabs, or both)
specs/03-ADMIN-UI.md:2334:- Intermediate items: parent section (e.g., "Products" linking to products index)
specs/03-ADMIN-UI.md:2367:- Semantic HTML: headings hierarchy, landmark regions, lists for navigation
specs/03-ADMIN-UI.md:2424:| `App\Livewire\Admin\Navigation\Index` | `livewire.admin.navigation.index` | `GET /admin/navigation` |
specs/09-IMPLEMENTATION-ROADMAP.md:176:Applied on: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
specs/09-IMPLEMENTATION-ROADMAP.md:359:5. `create_navigation_menus_table` - FK: `store_id`
specs/09-IMPLEMENTATION-ROADMAP.md:360:6. `create_navigation_items_table` - FK: `menu_id`
specs/09-IMPLEMENTATION-ROADMAP.md:372:| Model (with migration, factory, seeder) | `NavigationMenu` | Navigation menu entity |
specs/09-IMPLEMENTATION-ROADMAP.md:373:| Model (with migration, factory, seeder) | `NavigationItem` | Individual navigation item |
specs/09-IMPLEMENTATION-ROADMAP.md:435:| `buildTree(NavigationMenu $menu): array` | Builds navigation trees from navigation_menus / navigation_items |
specs/09-IMPLEMENTATION-ROADMAP.md:785:| `Admin\Layout\Sidebar` | Fixed left sidebar (256px desktop, overlay mobile), navigation groups, Flux `brand`, `icon`, `separator` |
specs/09-IMPLEMENTATION-ROADMAP.md:786:| `Admin\Layout\TopBar` | Store selector dropdown, user profile, hamburger toggle (mobile) |
specs/09-IMPLEMENTATION-ROADMAP.md:814:Media upload: Livewire `WithFileUploads`, drag-and-drop, reorder, alt text editing.
specs/09-IMPLEMENTATION-ROADMAP.md:847:| `Admin\Navigation\Index` | Menu management with drag-and-drop item ordering |
specs/09-IMPLEMENTATION-ROADMAP.md:1713:| `NavigationMenu` | Navigation menu entity |
specs/09-IMPLEMENTATION-ROADMAP.md:1714:| `NavigationItem` | Individual navigation item |
specs/09-IMPLEMENTATION-ROADMAP.md:1747:| `Admin/Layout/Sidebar` | Admin sidebar navigation |
specs/09-IMPLEMENTATION-ROADMAP.md:1767:| `Admin/Navigation/Index` | Navigation menu management |
specs/09-IMPLEMENTATION-ROADMAP.md:2061:16. `NavigationSeeder` - creates navigation menus and items
specs/08-PLAYWRIGHT-E2E-PLAN.md:104:| **Navigation** | Main menu with links to collections, about page | 7 |
specs/08-PLAYWRIGHT-E2E-PLAN.md:316:**Purpose:** Verify the admin login flow, credential validation, session management, logout, and sidebar navigation after successful login.
specs/08-PLAYWRIGHT-E2E-PLAN.md:333:    Dashboard --> LoginPage: click profile dropdown then "Logout"
specs/08-PLAYWRIGHT-E2E-PLAN.md:436:6. Click on "Admin User" (profile/user menu)
specs/08-PLAYWRIGHT-E2E-PLAN.md:1056:> **Note:** Domains tab is covered by Test 6.7. Checkout and Notifications tabs follow the same tabbed navigation pattern; E2E coverage for these tabs should be added when their functionality is implemented.
specs/08-PLAYWRIGHT-E2E-PLAN.md:1404:### Test 7.15: Navigates between pages using the main navigation
specs/08-PLAYWRIGHT-E2E-PLAN.md:1406:**Preconditions:** Seeded store with navigation
specs/08-PLAYWRIGHT-E2E-PLAN.md:1410:2. Click "T-Shirts" in the main navigation menu
specs/08-PLAYWRIGHT-E2E-PLAN.md:2539:- Mobile menu (hamburger) is visible
specs/08-PLAYWRIGHT-E2E-PLAN.md:2540:- Desktop navigation is hidden
specs/08-PLAYWRIGHT-E2E-PLAN.md:2643:### Test 13.7: Admin sidebar navigation works on tablet
specs/08-PLAYWRIGHT-E2E-PLAN.md:2695:| Keyboard navigation | Interactive elements reachable via Tab key; focus indicators visible |
specs/08-PLAYWRIGHT-E2E-PLAN.md:2829:3. Press Enter on a focused navigation link


Output:
Chunk ID: e1d969
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4014
Output:
specs/01-DATABASE-SCHEMA.md:447:### navigation_menus
specs/01-DATABASE-SCHEMA.md:449:Named menus (e.g. main-menu, footer-menu).
specs/01-DATABASE-SCHEMA.md:455:| handle | TEXT | No | - | UNIQUE per (store_id, handle) | e.g. "main-menu" |
specs/01-DATABASE-SCHEMA.md:462:- `idx_navigation_menus_store_handle` UNIQUE on (`store_id`, `handle`) - Unique handle per store
specs/01-DATABASE-SCHEMA.md:463:- `idx_navigation_menus_store_id` on (`store_id`) - FK lookup, listing menus per store
specs/01-DATABASE-SCHEMA.md:467:### navigation_items
specs/01-DATABASE-SCHEMA.md:469:Individual links within a navigation menu (supports nesting via position).
specs/01-DATABASE-SCHEMA.md:474:| menu_id | INTEGER | No | - | FK -> navigation_menus(id) ON DELETE CASCADE | Parent menu |
specs/01-DATABASE-SCHEMA.md:483:- `idx_navigation_items_menu_id` on (`menu_id`) - FK lookup, listing items per menu
specs/01-DATABASE-SCHEMA.md:484:- `idx_navigation_items_menu_position` on (`menu_id`, `position`) - Ordered item listing
specs/01-DATABASE-SCHEMA.md:1326:    stores ||--o{ navigation_menus : "has"
specs/01-DATABASE-SCHEMA.md:1327:    navigation_menus ||--o{ navigation_items : "contains"
specs/01-DATABASE-SCHEMA.md:1352:    navigation_menus {
specs/01-DATABASE-SCHEMA.md:1358:    navigation_items {
specs/01-DATABASE-SCHEMA.md:1360:        INTEGER menu_id FK
specs/01-DATABASE-SCHEMA.md:1530:        navigation_menus
specs/01-DATABASE-SCHEMA.md:1548:        navigation_items
specs/01-DATABASE-SCHEMA.md:1590:    stores --> navigation_menus
specs/01-DATABASE-SCHEMA.md:1606:    navigation_menus --> navigation_items
specs/01-DATABASE-SCHEMA.md:1645:- **Batch 3** (depends on Batch 2): `store_domains`, `store_users`, `store_settings`, `customers`, `themes`, `pages`, `navigation_menus`, `search_settings`, `shipping_zones`, `tax_settings`, `discounts`, `products`, `collections`, `app_installations`, `oauth_clients`
specs/01-DATABASE-SCHEMA.md:1646:- **Batch 4** (depends on Batch 3): `product_options`, `product_variants`, `product_media`, `collection_products`, `customer_addresses`, `carts`, `navigation_items`, `theme_files`, `theme_settings`, `search_queries`, `shipping_rates`, `oauth_tokens`, `webhook_subscriptions`
specs/01-DATABASE-SCHEMA.md:1665:6. **Selective indexing** - Not every column is indexed. Columns queried only via their parent's primary key (e.g., `cart_lines` always accessed through `cart_id`) rely on the foreign key index alone. This keeps write overhead and database size manageable.
specs/01-DATABASE-SCHEMA.md:1675:| 3 - Themes, Storefront, Localization | themes, theme_files, theme_settings, pages, navigation_menus, navigation_items | 6 |
specs/07-SEEDERS-AND-TEST-DATA.md:9:The main `DatabaseSeeder` calls child seeders in a strict order that respects foreign key dependencies. Each seeder targets a specific entity type. The ordering ensures parent records exist before children are created.
specs/07-SEEDERS-AND-TEST-DATA.md:30:| 16 | NavigationSeeder | Stores, Collections, Pages | Creates menus linking to collections and pages |
specs/07-SEEDERS-AND-TEST-DATA.md:701:| menu_id | Creates a new NavigationMenu via its factory |
specs/07-SEEDERS-AND-TEST-DATA.md:1973:**Acme Fashion - Main Menu (handle: main-menu, title: "Main Menu"):**
specs/07-SEEDERS-AND-TEST-DATA.md:1983:**Acme Fashion - Footer Menu (handle: footer-menu, title: "Footer Menu"):**
specs/07-SEEDERS-AND-TEST-DATA.md:1993:**Acme Electronics - Main Menu (handle: main-menu, title: "Main Menu"):**
specs/04-STOREFRONT-UI.md:114:**Accessibility:** Allows keyboard users to bypass the header navigation and jump directly to the main content area.
specs/04-STOREFRONT-UI.md:131:The header serves as the primary navigation banner.
specs/04-STOREFRONT-UI.md:144:| Main Navigation | Center | Horizontal nav with links sourced from the "main-menu" navigation menu. Supports one level of dropdown submenus (revealed on hover/focus) |
specs/04-STOREFRONT-UI.md:159:| Hamburger menu | Left | Toggle button for mobile navigation drawer |
specs/04-STOREFRONT-UI.md:165:When enabled in theme settings, the header sticks to the top of the viewport with a semi-transparent background and backdrop blur. A subtle bottom border appears on scroll.
specs/04-STOREFRONT-UI.md:173:| Backdrop | Semi-transparent dark overlay behind the drawer |
specs/04-STOREFRONT-UI.md:176:| Submenus | Collapsible accordion pattern with chevron icons |
specs/04-STOREFRONT-UI.md:177:| Account link | Positioned at the bottom of the navigation list |
specs/04-STOREFRONT-UI.md:179:| ARIA | Labeled as "Mobile navigation" |
specs/04-STOREFRONT-UI.md:202:| Link columns | Responsive grid: 2 columns on mobile, 3 on tablet, up to 4 on desktop. Links sourced from "footer-menu" navigation menu. Each column has an uppercase heading and a list of links |
specs/04-STOREFRONT-UI.md:267:| Background | Full-width image (cover, centered) or gradient. Image set via theme settings. Semi-transparent dark overlay for text readability |
specs/04-STOREFRONT-UI.md:307:| Image | 3:4 aspect ratio, cover-fit, rounded corners. Gradient overlay at bottom fading from transparent to semi-transparent dark |
specs/04-STOREFRONT-UI.md:309:| "Shop now" link | Small white text below title, slightly transparent, underlined. Becomes fully opaque on hover |
specs/04-STOREFRONT-UI.md:397:| [Filter toggle button]          [X products] [Sort dropdown]  |
specs/04-STOREFRONT-UI.md:405:| [Filter button]                           [Sort dropdown]     |
specs/04-STOREFRONT-UI.md:412:| Sort dropdown | Select element with live Livewire binding |
specs/04-STOREFRONT-UI.md:519:- Price changes announced via parent live region when filtering changes results
specs/04-STOREFRONT-UI.md:658:**Select dropdown style:** Used when the option has more than 6 values.
specs/04-STOREFRONT-UI.md:756:| Backdrop | Fixed full-screen semi-transparent dark overlay. Click to close |
specs/04-STOREFRONT-UI.md:771:| Item count | In parentheses after heading |
specs/04-STOREFRONT-UI.md:1434:| Overlay | Full-screen semi-transparent dark backdrop |
specs/04-STOREFRONT-UI.md:1459:| Sort | Same sort dropdown |
specs/04-STOREFRONT-UI.md:1571:| Keyboard navigation | All interactive elements reachable via Tab, operable via Enter/Space |
specs/04-STOREFRONT-UI.md:1580:| Collection Page | Live region for product count/grid on filter change. Filter sidebar labeled as navigation. Sort dropdown has associated label |
specs/04-STOREFRONT-UI.md:1584:| Search Modal | Dialog role with modal behavior. Results as listbox with option items. Active descendant tracking for keyboard navigation |
specs/04-STOREFRONT-UI.md:1591:| Hamburger menu | Enter/Space | Toggle open/close |
specs/04-STOREFRONT-UI.md:1593:| Dropdown menus | Arrow Up/Down | Navigate items |
specs/04-STOREFRONT-UI.md:1594:| Dropdown menus | Escape | Close |
specs/04-STOREFRONT-UI.md:1692:Renders a breadcrumb navigation trail.
specs/04-STOREFRONT-UI.md:1790:| BreadcrumbList | Breadcrumb navigation on all pages |
specs/03-ADMIN-UI.md:5:**Flux UI Free components available:** avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip
specs/03-ADMIN-UI.md:93:  Navigation                     -> admin.navigation.index
specs/03-ADMIN-UI.md:101:Each nav item displays an icon (via Flux icon component) and a text label. The active item receives a highlighted background and bold text. All navigation links use Livewire SPA-style page transitions. Groups are visually separated with Flux separator components.
specs/03-ADMIN-UI.md:114:- **Mobile (below lg):** Sidebar is hidden off-screen. A hamburger button (icon: bars-3) appears in the top bar. When user clicks it, the sidebar slides in as an overlay with a semi-transparent backdrop. Clicking the backdrop or a nav item closes it.
specs/03-ADMIN-UI.md:142:  Store Name dropdown:
specs/03-ADMIN-UI.md:147:  Avatar dropdown:
specs/03-ADMIN-UI.md:155:- When user clicks the store name, a dropdown appears listing all stores. Clicking a store calls `switchStore()` which updates the session and redirects to the dashboard.
specs/03-ADMIN-UI.md:157:- Profile dropdown is triggered by a Flux profile component showing the user's avatar and name.
specs/03-ADMIN-UI.md:163:| Store selector | `dropdown` |
specs/03-ADMIN-UI.md:168:| Profile menu | `dropdown` |
specs/03-ADMIN-UI.md:196:The admin supports dark mode via Tailwind's `dark:` variant. The mode follows the user's system preference by default. A toggle can be added in the profile dropdown later. All components and custom markup must include dark mode variants.
specs/03-ADMIN-UI.md:275:**Date range filter:** Positioned top-right. A Flux dropdown triggered by a button showing the current range label and a calendar icon. Options: Today, Last 7 days, Last 30 days, Custom range (reveals two date input fields when selected).
specs/03-ADMIN-UI.md:319:| Date range selector | `dropdown`, `button`, `icon` |
specs/03-ADMIN-UI.md:372:| `productTypes` | Distinct product types for the type filter dropdown |
specs/03-ADMIN-UI.md:553:|  | [--- Drag & drop or click ---]    |  |  [x] New Arrivals |
specs/03-ADMIN-UI.md:594:**Upload area:** A dashed-border drop zone with upload icon and text "Drag and drop images or click to upload". A hidden file input (multiple, accept images) triggered by clicking the zone.
specs/03-ADMIN-UI.md:731:- Status filter dropdown
specs/03-ADMIN-UI.md:795:|  [drag] [img] Sneakers         [remove]  |                   |
specs/03-ADMIN-UI.md:796:|  [drag] [img] Blue Shirt       [remove]  |                   |
specs/03-ADMIN-UI.md:810:  - Search results dropdown (conditionally shown): list of matching products with "Add" button
specs/03-ADMIN-UI.md:811:  - Assigned products list: each row shows product title, thumbnail, and a remove button. Rows are sortable via drag-to-reorder.
specs/03-ADMIN-UI.md:857:- Search input and stock filter dropdown
specs/03-ADMIN-UI.md:1331:|  +---results dropdown---+                                         |
specs/03-ADMIN-UI.md:1361:- Specific products: search input with live debounce. Results appear in a scrollable dropdown. Selected products shown as a list with remove (X) buttons.
specs/03-ADMIN-UI.md:1705:  - More actions dropdown (triggered by ellipsis icon button):
specs/03-ADMIN-UI.md:1706:    - "Preview" menu item
specs/03-ADMIN-UI.md:1707:    - "Publish" menu item (hidden if already published)
specs/03-ADMIN-UI.md:1708:    - "Duplicate" menu item
specs/03-ADMIN-UI.md:1710:    - "Delete" menu item (styled red/danger)
specs/03-ADMIN-UI.md:1871:**Route:** `GET /admin/navigation`
specs/03-ADMIN-UI.md:1875:**Blade view:** `resources/views/livewire/admin/navigation/index.blade.php`
specs/03-ADMIN-UI.md:1881:| `menus` | `Collection` | Navigation menus (Main Menu, Footer Menu) |
specs/03-ADMIN-UI.md:1883:| `menuItems` | `array` | Nested array of menu items for the editing menu |
specs/03-ADMIN-UI.md:1893:| `selectMenu(int $menuId)` | Loads a menu for editing |
specs/03-ADMIN-UI.md:1899:| `saveMenu()` | Persists all menu items |
specs/03-ADMIN-UI.md:1913:| MENU EDITOR (shown when a menu is selected)                      |
specs/03-ADMIN-UI.md:1917:| | [drag] Home               link: /            [edit] [trash]  | |
specs/03-ADMIN-UI.md:1918:| | [drag] Products           collection: All    [edit] [trash]  | |
specs/03-ADMIN-UI.md:1919:| | [drag] About Us           page: About        [edit] [trash]  | |
specs/03-ADMIN-UI.md:1920:| | [drag] Contact            link: /contact     [edit] [trash]  | |
specs/03-ADMIN-UI.md:1922:| |                                            [Save menu]       | |
specs/03-ADMIN-UI.md:1927:**Menu list:** Each menu displayed as a card with title and "Edit" button.
specs/03-ADMIN-UI.md:1929:**Menu editor (shown when a menu is selected):**
specs/03-ADMIN-UI.md:1932:- Sortable list of menu items, each row containing:
specs/03-ADMIN-UI.md:1937:- "Save menu" primary button at the bottom
specs/03-ADMIN-UI.md:1945:| Heading | "Edit menu item" or "Add menu item" (dynamic) |
specs/03-ADMIN-UI.md:1952:  - **Page/Collection/Product:** Resource select dropdown populated with available resources of that type
specs/03-ADMIN-UI.md:2192:- Date range dropdown (same as Dashboard: Today, Last 7 days, Last 30 days, Custom)
specs/03-ADMIN-UI.md:2299:3. Filter controls (dropdowns, tabs, or both)
specs/03-ADMIN-UI.md:2334:- Intermediate items: parent section (e.g., "Products" linking to products index)
specs/03-ADMIN-UI.md:2367:- Semantic HTML: headings hierarchy, landmark regions, lists for navigation
specs/03-ADMIN-UI.md:2424:| `App\Livewire\Admin\Navigation\Index` | `livewire.admin.navigation.index` | `GET /admin/navigation` |
specs/09-IMPLEMENTATION-ROADMAP.md:176:Applied on: products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
specs/09-IMPLEMENTATION-ROADMAP.md:359:5. `create_navigation_menus_table` - FK: `store_id`
specs/09-IMPLEMENTATION-ROADMAP.md:360:6. `create_navigation_items_table` - FK: `menu_id`
specs/09-IMPLEMENTATION-ROADMAP.md:372:| Model (with migration, factory, seeder) | `NavigationMenu` | Navigation menu entity |
specs/09-IMPLEMENTATION-ROADMAP.md:373:| Model (with migration, factory, seeder) | `NavigationItem` | Individual navigation item |
specs/09-IMPLEMENTATION-ROADMAP.md:435:| `buildTree(NavigationMenu $menu): array` | Builds navigation trees from navigation_menus / navigation_items |
specs/09-IMPLEMENTATION-ROADMAP.md:785:| `Admin\Layout\Sidebar` | Fixed left sidebar (256px desktop, overlay mobile), navigation groups, Flux `brand`, `icon`, `separator` |
specs/09-IMPLEMENTATION-ROADMAP.md:786:| `Admin\Layout\TopBar` | Store selector dropdown, user profile, hamburger toggle (mobile) |
specs/09-IMPLEMENTATION-ROADMAP.md:814:Media upload: Livewire `WithFileUploads`, drag-and-drop, reorder, alt text editing.
specs/09-IMPLEMENTATION-ROADMAP.md:847:| `Admin\Navigation\Index` | Menu management with drag-and-drop item ordering |
specs/09-IMPLEMENTATION-ROADMAP.md:1713:| `NavigationMenu` | Navigation menu entity |
specs/09-IMPLEMENTATION-ROADMAP.md:1714:| `NavigationItem` | Individual navigation item |
specs/09-IMPLEMENTATION-ROADMAP.md:1747:| `Admin/Layout/Sidebar` | Admin sidebar navigation |
specs/09-IMPLEMENTATION-ROADMAP.md:1767:| `Admin/Navigation/Index` | Navigation menu management |
specs/09-IMPLEMENTATION-ROADMAP.md:2061:16. `NavigationSeeder` - creates navigation menus and items
specs/08-PLAYWRIGHT-E2E-PLAN.md:104:| **Navigation** | Main menu with links to collections, about page | 7 |
specs/08-PLAYWRIGHT-E2E-PLAN.md:316:**Purpose:** Verify the admin login flow, credential validation, session management, logout, and sidebar navigation after successful login.
specs/08-PLAYWRIGHT-E2E-PLAN.md:333:    Dashboard --> LoginPage: click profile dropdown then "Logout"
specs/08-PLAYWRIGHT-E2E-PLAN.md:436:6. Click on "Admin User" (profile/user menu)
specs/08-PLAYWRIGHT-E2E-PLAN.md:1056:> **Note:** Domains tab is covered by Test 6.7. Checkout and Notifications tabs follow the same tabbed navigation pattern; E2E coverage for these tabs should be added when their functionality is implemented.
specs/08-PLAYWRIGHT-E2E-PLAN.md:1404:### Test 7.15: Navigates between pages using the main navigation
specs/08-PLAYWRIGHT-E2E-PLAN.md:1406:**Preconditions:** Seeded store with navigation
specs/08-PLAYWRIGHT-E2E-PLAN.md:1410:2. Click "T-Shirts" in the main navigation menu
specs/08-PLAYWRIGHT-E2E-PLAN.md:2539:- Mobile menu (hamburger) is visible
specs/08-PLAYWRIGHT-E2E-PLAN.md:2540:- Desktop navigation is hidden
specs/08-PLAYWRIGHT-E2E-PLAN.md:2643:### Test 13.7: Admin sidebar navigation works on tablet
specs/08-PLAYWRIGHT-E2E-PLAN.md:2695:| Keyboard navigation | Interactive elements reachable via Tab key; focus indicators visible |
specs/08-PLAYWRIGHT-E2E-PLAN.md:2829:3. Press Enter on a focused navigation link


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

namespace App\Livewire\Admin\Navigation;

use App\Enums\NavigationItemType;
use App\Models\Collection as ProductCollection;
use App\Models\NavigationItem;
use App\Models\NavigationMenu;
use App\Models\Page;
use App\Models\Product;
use App\Models\Store;
use App\Services\NavigationService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Index extends Component
{
    use AuthorizesRequests;

    #[Locked]
    public int $storeId;

    public ?int $selectedMenuId = null;

    /**
     * @var array<int, array{id: int|null, label: string, type: string, url: string|null, resource_id: int|null}>
     */
    public array $menuItems = [];

    public ?int $editingItemIndex = null;

    public string $itemLabel = '';

    public string $itemType = 'link';

    public string $itemUrl = '';

    public ?int $itemResourceId = null;

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

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

        $this->storeId = $store->getKey();
        $this->selectedMenuId = $this->menus()->first()?->getKey();

        if ($this->selectedMenuId !== null) {
            $this->loadMenuItems();
        }
    }

    public function selectMenu(int $menuId): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->selectedMenuId = $this->menu($menuId)->getKey();
        $this->loadMenuItems();
        $this->cancelItem();
    }

    public function addItem(): void
    {
        $this->editingItemIndex = null;
        $this->itemLabel = '';
        $this->itemType = 'link';
        $this->itemUrl = '';
        $this->itemResourceId = null;
    }

    public function editItem(int $index): void
    {
        abort_unless(isset($this->menuItems[$index]), 404);

        $item = $this->menuItems[$index];

        $this->editingItemIndex = $index;
        $this->itemLabel = $item['label'];
        $this->itemType = $item['type'];
        $this->itemUrl = (string) ($item['url'] ?? '');
        $this->itemResourceId = $item['resource_id'];
    }

    public function saveItem(): void
    {
        $this->authorize('update', $this->scopedStore());

        $validated = $this->validate([
            'itemLabel' => ['required', 'string', 'max:255'],
            'itemType' => ['required', Rule::in(array_column(NavigationItemType::cases(), 'value'))],
            'itemUrl' => [Rule::requiredIf($this->itemType === NavigationItemType::Link->value), 'nullable', 'string', 'max:255'],
            'itemResourceId' => [Rule::requiredIf($this->itemType !== NavigationItemType::Link->value), 'nullable', 'integer'],
        ], [], [
            'itemLabel' => 'label',
            'itemType' => 'type',
            'itemUrl' => 'URL',
            'itemResourceId' => 'resource',
        ]);

        if ($this->itemType !== NavigationItemType::Link->value && ! $this->resourceExists($this->itemType, (int) $this->itemResourceId)) {
            $this->addError('itemResourceId', __('Select a valid resource for this store.'));

            return;
        }

        $item = [
            'id' => $this->editingItemIndex !== null ? $this->menuItems[$this->editingItemIndex]['id'] : null,
            'label' => $validated['itemLabel'],
            'type' => $validated['itemType'],
            'url' => $this->itemType === NavigationItemType::Link->value ? $validated['itemUrl'] : null,
            'resource_id' => $this->itemType === NavigationItemType::Link->value ? null : (int) $this->itemResourceId,
        ];

        if ($this->editingItemIndex === null) {
            $this->menuItems[] = $item;
        } else {
            $this->menuItems[$this->editingItemIndex] = $item;
        }

        $this->cancelItem();
    }

    public function removeItem(int $index): void
    {
        unset($this->menuItems[$index]);

        $this->menuItems = array_values($this->menuItems);
    }

    public function moveItemUp(int $index): void
    {
        if ($index <= 0 || ! isset($this->menuItems[$index])) {
            return;
        }

        [$this->menuItems[$index - 1], $this->menuItems[$index]] = [$this->menuItems[$index], $this->menuItems[$index - 1]];
    }

    public function moveItemDown(int $index): void
    {
        if (! isset($this->menuItems[$index], $this->menuItems[$index + 1])) {
            return;
        }

        [$this->menuItems[$index], $this->menuItems[$index + 1]] = [$this->menuItems[$index + 1], $this->menuItems[$index]];
    }

    public function saveMenu(NavigationService $navigation): void
    {
        $this->authorize('update', $this->scopedStore());

        $menu = $this->selectedMenu();

        DB::transaction(function () use ($menu): void {
            NavigationItem::withoutGlobalScopes()
                ->where('menu_id', $menu->getKey())
                ->delete();

            foreach ($this->menuItems as $position => $item) {
                NavigationItem::withoutGlobalScopes()->create([
                    'menu_id' => $menu->getKey(),
                    'type' => NavigationItemType::from($item['type']),
                    'label' => $item['label'],
                    'url' => $item['url'],
                    'resource_id' => $item['resource_id'],
                    'position' => $position,
                ]);
            }
        });

        $navigation->forget($menu);
        $this->loadMenuItems();

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

    public function cancelItem(): void
    {
        $this->editingItemIndex = null;
        $this->itemLabel = '';
        $this->itemType = 'link';
        $this->itemUrl = '';
        $this->itemResourceId = null;
        $this->resetErrorBag(['itemLabel', 'itemType', 'itemUrl', 'itemResourceId']);
    }

    /**
     * @return Collection<int, NavigationMenu>
     */
    public function menus(): Collection
    {
        return NavigationMenu::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->orderByRaw("case when handle = 'main-menu' then 0 when handle = 'footer-menu' then 1 else 2 end")
            ->orderBy('title')
            ->get();
    }

    /**
     * @return Collection<int, Page|ProductCollection|Product>
     */
    public function resourcesForType(): Collection
    {
        return match ($this->itemType) {
            NavigationItemType::Page->value => Page::withoutGlobalScopes()
                ->where('store_id', $this->storeId)
                ->orderBy('title')
                ->get(['id', 'title']),
            NavigationItemType::Collection->value => ProductCollection::withoutGlobalScopes()
                ->where('store_id', $this->storeId)
                ->orderBy('title')
                ->get(['id', 'title']),
            NavigationItemType::Product->value => Product::withoutGlobalScopes()
                ->where('store_id', $this->storeId)
                ->orderBy('title')
                ->limit(100)
                ->get(['id', 'title']),
            default => collect(),
        };
    }

    public function targetLabel(array $item): string
    {
        return match ($item['type']) {
            NavigationItemType::Page->value => 'page: '.$this->resourceTitle(Page::class, $item['resource_id']),
            NavigationItemType::Collection->value => 'collection: '.$this->resourceTitle(ProductCollection::class, $item['resource_id']),
            NavigationItemType::Product->value => 'product: '.$this->resourceTitle(Product::class, $item['resource_id']),
            default => 'link: '.($item['url'] ?: '#'),
        };
    }

    public function render(): mixed
    {
        return view('livewire.admin.navigation.index', [
            'menus' => $this->menus(),
            'selectedMenu' => $this->selectedMenu(),
            'resources' => $this->resourcesForType(),
        ])->layout('layouts.app', [
            'title' => __('Navigation'),
        ]);
    }

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

        abort_unless($store instanceof Store, 404);

        return $store;
    }

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

        <flux:heading size="xl">Navigation</flux:heading>
        <flux:text class="mt-1">Storefront menus and ordered links.</flux:text>

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

            @foreach ($menus as $menu)
                <button
                    type="button"
                    wire:click="selectMenu({{ $menu->getKey() }})"
                    wire:key="navigation-menu-{{ $menu->getKey() }}"
                    class="block w-full rounded-lg border px-4 py-3 text-left text-sm {{ $selectedMenuId === $menu->getKey() ? 'border-blue-500 bg-blue-50 text-blue-950 dark:border-blue-400 dark:bg-blue-950/40 dark:text-blue-100' : 'border-zinc-200 bg-white text-zinc-700 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800' }}"
                >
                    {{ $menu->title }}
                    {{ $menu->handle }}
                </button>
            @endforeach

                    <flux:heading size="lg">{{ $selectedMenu->title }}</flux:heading>
                    <flux:text class="mt-1">{{ count($menuItems) }} items</flux:text>

                <flux:button type="button" wire:click="addItem" variant="filled" icon="plus">Add item</flux:button>

                @forelse ($menuItems as $index => $item)

                                <flux:button type="button" wire:click="moveItemUp({{ $index }})" size="sm" variant="ghost" icon="arrow-up" aria-label="Move {{ $item['label'] }} up" />
                                <flux:button type="button" wire:click="moveItemDown({{ $index }})" size="sm" variant="ghost" icon="arrow-down" aria-label="Move {{ $item['label'] }} down" />

{{ $item['label'] }}

{{ $this->targetLabel($item) }}

                            <flux:button type="button" wire:click="editItem({{ $index }})" size="sm" variant="filled">Edit</flux:button>
                            <flux:button type="button" wire:click="removeItem({{ $index }})" size="sm" variant="danger">Remove</flux:button>

                @empty

No menu items.

                @endforelse

                <flux:button type="button" wire:click="saveMenu" variant="primary">Save menu</flux:button>

        <form wire:submit="saveItem" class="rounded-lg border border-zinc-200 bg-white p-5 dark:border-zinc-700 dark:bg-zinc-900">
            <flux:heading size="lg">{{ $editingItemIndex === null ? 'Add item' : 'Edit item' }}</flux:heading>

                <flux:input wire:model="itemLabel" label="Label" />
                <flux:error name="itemLabel" />

                <flux:select wire:model.live="itemType" label="Type">
                    <flux:select.option value="link">Custom link</flux:select.option>
                    <flux:select.option value="page">Page</flux:select.option>
                    <flux:select.option value="collection">Collection</flux:select.option>
                    <flux:select.option value="product">Product</flux:select.option>
                </flux:select>

                @if ($itemType === 'link')
                    <flux:input wire:model="itemUrl" label="URL" placeholder="/collections" />
                    <flux:error name="itemUrl" />
                @else
                    <flux:select wire:model="itemResourceId" label="Resource">
                        <flux:select.option value="">Select resource</flux:select.option>
                        @foreach ($resources as $resource)
                            <flux:select.option value="{{ $resource->getKey() }}">{{ $resource->title }}</flux:select.option>
                        @endforeach
                    </flux:select>
                    <flux:error name="itemResourceId" />
                @endif

                    <flux:button type="button" wire:click="cancelItem" variant="ghost">Cancel</flux:button>
                    <flux:button type="submit" variant="primary">{{ $editingItemIndex === null ? 'Add item' : 'Update item' }}</flux:button>

        </form>


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

namespace App\Services;

use App\Enums\NavigationItemType;
use App\Models\Collection as ProductCollection;
use App\Models\NavigationItem;
use App\Models\NavigationMenu;
use App\Models\Page;
use App\Models\Product;
use App\Models\Store;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

class NavigationService
{
    /**
     * @return array<int, array{label: string, url: string, type: string, external: bool, children: array<int, mixed>}>
     */
    public function forHandle(Store $store, string $handle): array
    {
        $menu = NavigationMenu::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('handle', $handle)
            ->first();

        if ($menu === null) {
            return [];
        }

        return $this->buildTree($menu);
    }

    /**
     * @return array<int, array{label: string, url: string, type: string, external: bool, children: array<int, mixed>}>
     */
    public function buildTree(NavigationMenu $menu): array
    {
        return Cache::remember($this->cacheKey($menu), now()->addMinutes(5), function () use ($menu): array {
            return $menu->items()
                ->with('menu')
                ->get()
                ->map(fn (NavigationItem $item): array => [
                    'label' => $item->label,
                    'url' => $this->resolveUrl($item),
                    'type' => $item->type->value,
                    'external' => $this->isExternal((string) ($item->url ?? '')),
                    'children' => [],
                ])
                ->all();
        });
    }

    public function resolveUrl(NavigationItem $item): string
    {
        return match ($item->type) {
            NavigationItemType::Link => $item->url ?: '#',
            NavigationItemType::Page => $this->pageUrl($item),
            NavigationItemType::Collection => $this->collectionUrl($item),
            NavigationItemType::Product => $this->productUrl($item),
        };
    }

    public function forget(NavigationMenu $menu): void
    {
        Cache::forget($this->cacheKey($menu));
    }

    private function pageUrl(NavigationItem $item): string
    {
        $page = Page::withoutGlobalScopes()
            ->where('store_id', $this->storeIdFor($item))
            ->find($item->resource_id);

        return $page ? route('pages.show', $page->handle, false) : '#';
    }

    private function collectionUrl(NavigationItem $item): string
    {
        $collection = ProductCollection::withoutGlobalScopes()
            ->where('store_id', $this->storeIdFor($item))
            ->find($item->resource_id);

        return $collection ? route('collections.show', $collection->handle, false) : '#';
    }

    private function productUrl(NavigationItem $item): string
    {
        $product = Product::withoutGlobalScopes()
            ->where('store_id', $this->storeIdFor($item))
            ->find($item->resource_id);

        return $product ? route('products.show', $product->handle, false) : '#';
    }

    private function storeIdFor(NavigationItem $item): ?int
    {
        if ($item->relationLoaded('menu') && $item->menu !== null) {
            return (int) $item->menu->store_id;
        }

        return NavigationMenu::withoutGlobalScopes()
            ->whereKey($item->menu_id)
            ->value('store_id');
    }

    private function isExternal(string $url): bool
    {
        return Str::startsWith($url, ['http://', 'https://', 'mailto:', 'tel:']);
    }

    private function cacheKey(NavigationMenu $menu): string
    {
        return "navigation:{$menu->store_id}:{$menu->handle}";
    }
}
<?php

namespace App\Models;

use App\Enums\NavigationItemType;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class NavigationItem extends Model
{
    /** @use HasFactory<\Database\Factories\NavigationItemFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'menu_id',
        'type',
        'label',
        'url',
        'resource_id',
        'position',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'type' => 'link',
        'position' => 0,
    ];

    protected static function booted(): void
    {
        static::addGlobalScope('current_store', function (Builder $builder): void {
            if (! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if (! $store instanceof Store) {
                return;
            }

            $builder->whereHas('menu', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

    /**
     * @return BelongsTo<NavigationMenu, $this>
     */
    public function menu(): BelongsTo
    {
        return $this->belongsTo(NavigationMenu::class, 'menu_id');
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'type' => NavigationItemType::class,
            'resource_id' => 'integer',
            'position' => 'integer',
        ];
    }
}
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('navigation_items', function (Blueprint $table) {
            $table->id();
            $table->foreignId('menu_id')->constrained('navigation_menus')->cascadeOnDelete();
            $table->enum('type', ['link', 'page', 'collection', 'product'])->default('link');
            $table->string('label');
            $table->string('url')->nullable();
            $table->unsignedBigInteger('resource_id')->nullable();
            $table->unsignedInteger('position')->default(0);

            $table->index('menu_id');
            $table->index(['menu_id', 'position']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('navigation_items');
    }
};


Output: failed to parse function arguments: missing field `cmd` at line 1 column 309


sed -n '220,520p' app/Livewire/Admin/Navigation/Index.php


sed -n '1968,2005p' specs/07-SEEDERS-AND-TEST-DATA.md && sed -n '1871,1965p' specs/03-ADMIN-UI.md


sed -n '467,485p' specs/01-DATABASE-SCHEMA.md && sed -n '1356,1368p' specs/01-DATABASE-SCHEMA.md


sed -n '220,520p' app/Livewire/Admin/Navigation/Index.php

                ->where('store_id', $this->storeId)
                ->orderBy('title')
                ->limit(100)
                ->get(['id', 'title']),
            default => collect(),
        };
    }

    public function targetLabel(array $item): string
    {
        return match ($item['type']) {
            NavigationItemType::Page->value => 'page: '.$this->resourceTitle(Page::class, $item['resource_id']),
            NavigationItemType::Collection->value => 'collection: '.$this->resourceTitle(ProductCollection::class, $item['resource_id']),
            NavigationItemType::Product->value => 'product: '.$this->resourceTitle(Product::class, $item['resource_id']),
            default => 'link: '.($item['url'] ?: '#'),
        };
    }

    public function render(): mixed
    {
        return view('livewire.admin.navigation.index', [
            'menus' => $this->menus(),
            'selectedMenu' => $this->selectedMenu(),
            'resources' => $this->resourcesForType(),
        ])->layout('layouts.app', [
            'title' => __('Navigation'),
        ]);
    }

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

        abort_unless($store instanceof Store, 404);

        return $store;
    }

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

    private function selectedMenu(): NavigationMenu
    {
        abort_unless($this->selectedMenuId !== null, 404);

        return $this->menu($this->selectedMenuId);
    }

    private function menu(int $menuId): NavigationMenu
    {
        return NavigationMenu::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->whereKey($menuId)
            ->firstOrFail();
    }

    private function loadMenuItems(): void
    {
        $this->menuItems = NavigationItem::withoutGlobalScopes()
            ->where('menu_id', $this->selectedMenu()->getKey())
            ->orderBy('position')
            ->get()
            ->map(fn (NavigationItem $item): array => [
                'id' => $item->getKey(),
                'label' => $item->label,
                'type' => $item->type->value,
                'url' => $item->url,
                'resource_id' => $item->resource_id,
            ])
            ->all();
    }

    private function resourceExists(string $type, int $resourceId): bool
    {
        return match ($type) {
            NavigationItemType::Page->value => Page::withoutGlobalScopes()->where('store_id', $this->storeId)->whereKey($resourceId)->exists(),
            NavigationItemType::Collection->value => ProductCollection::withoutGlobalScopes()->where('store_id', $this->storeId)->whereKey($resourceId)->exists(),
            NavigationItemType::Product->value => Product::withoutGlobalScopes()->where('store_id', $this->storeId)->whereKey($resourceId)->exists(),
            default => true,
        };
    }

    private function resourceTitle(string $modelClass, mixed $resourceId): string
    {
        if (! is_numeric($resourceId)) {
            return 'Missing';
        }

        return $modelClass::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->whereKey((int) $resourceId)
            ->value('title') ?? 'Missing';
    }
}


sed -n '467,485p' specs/01-DATABASE-SCHEMA.md && sed -n '1356,1368p' specs/01-DATABASE-SCHEMA.md

### navigation_items

Individual links within a navigation menu (supports nesting via position).

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| menu_id | INTEGER | No | - | FK -> navigation_menus(id) ON DELETE CASCADE | Parent menu |
| type | TEXT | No | 'link' | CHECK IN ('link', 'page', 'collection', 'product') | Item type |
| label | TEXT | No | - | - | Display label |
| url | TEXT | Yes | NULL | - | Direct URL (used when type = 'link') |
| resource_id | INTEGER | Yes | NULL | - | FK to pages/collections/products (polymorphic) |
| position | INTEGER | No | 0 | - | Display ordering |

**Indexes:**

- `idx_navigation_items_menu_id` on (`menu_id`) - FK lookup, listing items per menu
- `idx_navigation_items_menu_position` on (`menu_id`, `position`) - Ordered item listing

        TEXT title
    }
    navigation_items {
        INTEGER id PK
        INTEGER menu_id FK
        TEXT type
        TEXT label
        INTEGER position
    }
```

### Epics 5-6 - Cart, Checkout, Orders, and Payments