`.
:::
::: details View translations, permissions and seeding
```php
// lang/en/modules/dashboard.php (repeat for every locale)
'reports' => [
'title' => 'Reports',
'description' => 'Exported analytics reports.',
'empty' => 'No reports yet.',
],
```
```php
// config/permissions/dashboard.php
[
'permission_name' => 'View Reports',
'associated_roles' => ['Super Admin', 'Admin'],
],
// config/permissions/tenant/dashboard.php
[
'permission_name' => 'View Tenant Reports',
'associated_roles' => ['Super Admin', 'Admin'],
],
```
```bash
php artisan db:seed --class=PermissionSeeder
php artisan tenants:seed --class="Database\Seeders\PermissionSeeder"
php artisan db:seed --class=MenuSeeder
```
In Blade and PHP the keys work the same way: `__('modules/dashboard.reports.title')`. Menu titles are translated separately, from the `nav.
` keys in `lang//modules/common.php`.
Anyone who gets a system role from now on receives the permission through `associated_roles`. For users who already exist, grant it from Users → Assign permissions. To hide actions inside the page, use `@canany(['View Reports', 'View Tenant Reports'])` (see [Architecture → Permissions](/docs/livewire/architecture#permissions-on-the-frontend)).
Roles and tenant seeding are covered in more depth in [Users, roles & permissions](/docs/core/users-roles-permissions).
:::
::: details View a feature test
```php
use App\Models\User;
use Spatie\Permission\Models\Permission;
test('users with the permission can visit the reports page', function () {
$user = User::factory()->create();
$user->givePermissionTo(Permission::findOrCreate('View Reports', 'web'));
$this->actingAs($user)
->get(route('reports.index'))
->assertOk();
});
```
```bash
php artisan test --compact --filter=reports
```
:::
## Adding a form
A form binds fields to public properties and submits to a component method. The method validates through a Form Request, calls a service and shows a toast.
```text
```
For create/edit modals, Form objects, events and confirm dialogs, head over to [Livewire → Forms](/docs/livewire/livewire#forms).
::: details View a component test
```php
use App\Models\User;
use Livewire\Livewire;
use Modules\Dashboard\Livewire\Reports;
test('a report can be created', function () {
$this->actingAs(User::factory()->create());
Livewire::test(Reports::class)
->set('name', 'Quarterly revenue')
->call('save')
->assertHasNoErrors();
});
```
:::
## Adding a module
1. Create `Modules/Reports/Providers/ReportsServiceProvider.php` extending `App\Providers\ModuleServiceProvider`. An empty class is enough.
2. Add it to `bootstrap/providers.php`.
3. Put routes in `Modules/Reports/routes/web.php` (or `tenant.php`) and components in `Modules/Reports/Livewire`.
```php
namespace Modules\Reports\Providers;
use App\Providers\ModuleServiceProvider;
class ReportsServiceProvider extends ModuleServiceProvider {}
```
The provider loads the routes and registers the `reports` Livewire namespace, so `Modules\Reports\Livewire\ExportModal` becomes ``. Views still go in `resources/views/pages/reports`. See [Architecture → Module registration](/docs/livewire/architecture#module-registration).
## Linting & static analysis
| Command | When |
| --- | --- |
| `composer lint` | Fix PHP style with Pint and format every Blade view. Run before committing |
| `composer lint:check` | Check both without changing files |
| `composer types:check` | Run PHPStan with Larastan |
| `composer test` | Everything above plus the Pest suite. CI runs it through `composer ci:check` in `.github/workflows/tests.yml` |
Pint uses the Laravel preset with stricter rules in `pint.json` (strict comparison, imported global classes and functions, `mb_*` string functions, ordered class elements).
Blade views are formatted by `blade-formatter`. `.bladeformatterrc.json` sets 4-space indents, a 120-character line length and sorted Tailwind classes. `.bladeformatterignore` skips `resources/views/flux/**`, `vendor/**` and `node_modules/**`.
PHPStan reads `phpstan.neon`, which analyses `app/`, `bootstrap/app.php`, `config/`, `database/` and `routes/` at level 7.
## AI guidelines
The kit ships with Laravel Boost, so AI agents get the project's conventions:
| File | Contents |
| --- | --- |
| `CLAUDE.md`, `AGENTS.md` | Boost guidelines for the installed package versions |
| `.ai/rules/index.md` | Which rule file applies to which paths |
| `.ai/rules/general.md` | No code comments |
| `.ai/rules/livewire.md` | Form Requests, thin components, `authorizePermission()`, the `x-common.*` wrappers, `Js::from()` |
| `.claude/skills`, `.agents/skills` | Skills such as `livewire-development`, `fluxui-development`, `tailwindcss-development`, `fortify-development`, `laravel-tenancy`, `laravel-permission-development` and `pest-testing` |
| `boost.json` | Boost settings; `composer update` runs `boost:update` afterwards |
## Conventions
- Don't hard-code text. Every string the user sees goes through `__('modules/.')`, and that includes page titles, breadcrumbs, placeholders, toasts and confirm dialogs.
- Don't write code comments; give things clear names instead (`.ai/rules/general.md`).
- Keep components thin: validate with a Form Request through `validateFormRequest()`, authorize with `authorizePermission()`, then call a service. No queries or transactions in components.
- Never use Flux tags in pages or feature components. Use the `x-common.*` wrappers, and ask before destructive actions with `x-common.confirm-dialog`.
- Use `route()` for URLs and `wire:navigate` for links between pages.
- Keep IDs in `#[Locked]` properties and reload models inside each action.
- Put views in `resources/views/pages/`, page-only modals and Blade pieces in that area's `partials/` folder, and widgets used on several pages in `resources/views/livewire`.