# New Module Guide for AI Agents

Use this guide to scaffold a complete module (admin or client) in this Laravel 12 + Vue 3 + Inertia.js project.
All conventions here match the existing codebase exactly.

---

## Variables

Throughout this guide, replace:
- `{Model}` → PascalCase singular, e.g. `Product`
- `{models}` → kebab-case plural, e.g. `products`
- `{model}` → camelCase singular, e.g. `product`
- `{models_snake}` → snake_case plural, e.g. `products` (or `product_orders`)
- `{Context}` → `Admin` or `Client`
- `{context}` → `admin` or `client`

The user will tell you:
1. The **fillable fields** (go in Model, Resource, Store/Update Requests, Form vue)
2. The **table columns** to show in the Index page
3. The **form fields** to show in the Form page

---

## Files to Create

### 1. Model — `app/Models/{Model}.php`

```php
<?php

namespace App\Models;

use App\Traits\HasDynamicFilters;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class {Model} extends Model
{
    use HasFactory, HasDynamicFilters, SoftDeletes;

    protected $fillable = [
        // ← INSERT the user-provided fillable fields here
    ];

    protected $casts = [
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    // Fields the HasDynamicFilters trait uses for ?search=
    protected $searchable = [
        // e.g. 'name', 'email'
    ];

    // Fields the HasDynamicFilters trait uses for ?status=active
    protected $filterable = [
        // e.g. 'status'
    ];
}
```

---

### 2. Migration — `database/migrations/{timestamp}_create_{models_snake}_table.php`

Run: `php artisan make:migration create_{models_snake}_table`

Add a column for every fillable field. Always include:
```php
$table->id();
$table->timestamps();
$table->softDeletes();
```

---

### 3. Controller — `app/Http/Controllers/Web/{Context}/{Model}Controller.php`

```php
<?php

namespace App\Http\Controllers\Web\{Context};

use App\Http\Controllers\Controller;
use App\Http\Requests\{Context}\Store{Model}Request;
use App\Http\Requests\{Context}\Update{Model}Request;
use App\Http\Resources\{Context}\{Model}Resource;
use App\Models\{Model};
use App\Services\{Context}\{Model}Service;
use App\Traits\HasDataTableInertia;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;
use Inertia\Inertia;
use Inertia\Response;

class {Model}Controller extends Controller implements HasMiddleware
{
    use HasDataTableInertia;

    public static function middleware(): array
    {
        return [
            'auth',
            new Middleware('permission:{models}.view', only: ['index']),
            new Middleware('permission:{models}.create', only: ['create', 'store']),
            new Middleware('permission:{models}.edit', only: ['edit', 'update']),
            new Middleware('permission:{models}.delete', only: ['destroy']),
        ];
    }

    public function __construct(protected {Model}Service ${model}Service) {}

    public function index(Request $request): Response
    {
        return $this->inertiaDataTable(
            page: 'Modules/{context}/{Model}/{Model}Index',
            query: {Model}::query()->applyFilters($request->all()),
            request: $request,
            resource: {Model}Resource::class,
            additionalProps: []
        );
    }

    public function create(): Response
    {
        return Inertia::render('Modules/{context}/{Model}/{Model}Form');
    }

    public function store(Store{Model}Request $request): RedirectResponse
    {
        try {
            $this->{model}Service->create{Model}($request->validated());
            return redirect()->route('{context}.{models}.index')
                ->with('success', __('{models}.created'));
        } catch (\Exception $e) {
            return redirect()->back()->withInput()->with('error', $e->getMessage());
        }
    }

    public function edit({Model} ${model}): Response
    {
        return Inertia::render('Modules/{context}/{Model}/{Model}Form', [
            '{model}' => (new {Model}Resource(${model}))->resolve(),
        ]);
    }

    public function update(Update{Model}Request $request, {Model} ${model}): RedirectResponse
    {
        try {
            $this->{model}Service->update{Model}(${model}->id, $request->validated());
            return redirect()->route('{context}.{models}.index')
                ->with('success', __('{models}.updated'));
        } catch (\Exception $e) {
            return redirect()->back()->withInput()->with('error', $e->getMessage());
        }
    }

    public function destroy({Model} ${model}): RedirectResponse
    {
        try {
            $this->{model}Service->delete{Model}(${model}->id);
            return redirect()->back()->with('success', __('{models}.deleted'));
        } catch (\Exception $e) {
            return redirect()->back()->with('error', $e->getMessage());
        }
    }

    public function bulkDelete(Request $request): RedirectResponse
    {
        $validated = $request->validate([
            'ids' => 'required|array',
            'ids.*' => 'exists:{models_snake},id',
        ]);
        $this->{model}Service->bulkDelete{Model}($validated['ids']);
        return redirect()->back()->with('success', 'Items deleted successfully');
    }
}
```

---

### 4. Service — `app/Services/{Context}/{Model}Service.php`

```php
<?php

namespace App\Services\{Context};

use App\Models\{Model};
use Illuminate\Support\Facades\DB;

class {Model}Service
{
    public function create{Model}(array $data): {Model}
    {
        return DB::transaction(fn () => {Model}::create($data));
    }

    public function update{Model}(int $id, array $data): {Model}
    {
        return DB::transaction(function () use ($id, $data) {
            $record = {Model}::findOrFail($id);
            $record->update($data);
            return $record;
        });
    }

    public function delete{Model}(int $id): bool
    {
        return DB::transaction(function () use ($id) {
            return {Model}::findOrFail($id)->delete();
        });
    }

    public function bulkDelete{Model}(array $ids): void
    {
        DB::transaction(fn () => {Model}::whereIn('id', $ids)->delete());
    }
}
```

---

### 5. Store Request — `app/Http/Requests/{Context}/Store{Model}Request.php`

```php
<?php

namespace App\Http\Requests\{Context};

use Illuminate\Foundation\Http\FormRequest;

class Store{Model}Request extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            // ← INSERT validation rules for every fillable field the user provides
            // Example:
            // 'name'   => ['required', 'string', 'max:255'],
            // 'email'  => ['required', 'email', 'unique:{models_snake},email'],
            // 'status' => ['required', 'in:active,inactive'],
        ];
    }
}
```

---

### 6. Update Request — `app/Http/Requests/{Context}/Update{Model}Request.php`

```php
<?php

namespace App\Http\Requests\{Context};

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class Update{Model}Request extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        $id = $this->route('{model}');

        return [
            // ← SAME fields as StoreRequest, but unique rules use ->ignore($id)
            // Example:
            // 'name'   => ['required', 'string', 'max:255'],
            // 'email'  => ['required', 'email', Rule::unique('{models_snake}', 'email')->ignore($id)],
            // 'status' => ['required', 'in:active,inactive'],
        ];
    }
}
```

---

### 7. Resource — `app/Http/Resources/{Context}/{Model}Resource.php`

```php
<?php

namespace App\Http\Resources\{Context};

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class {Model}Resource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'         => $this->id,
            // ← INSERT one key per fillable field the user provides
            // 'name'   => $this->name,
            // 'email'  => $this->email,
            // 'status' => $this->status,
            'created_at' => $this->created_at->toISOString(),
            'updated_at' => $this->updated_at->toISOString(),
        ];
    }
}
```

---

### 8. Permission Seeder — `database/seeders/{Model}PermissionSeeder.php`

```php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

class {Model}PermissionSeeder extends Seeder
{
    public function run(): void
    {
        $permissions = [
            '{models}.view',
            '{models}.create',
            '{models}.edit',
            '{models}.delete',
        ];

        foreach ($permissions as $permission) {
            Permission::findOrCreate($permission);
        }

        $adminRole = Role::findByName('admin');
        if ($adminRole) {
            $adminRole->givePermissionTo($permissions);
        }
    }
}
```

Run: `php artisan db:seed --class={Model}PermissionSeeder`

---

### 9. Routes — add to `routes/web.php`

Add inside the existing `Route::middleware('auth')->group(...)` block, alongside the other admin/client routes:

```php
use App\Http\Controllers\Web\{Context}\{Model}Controller;

// {Model} routes
Route::prefix('{context}')->name('{context}.')->group(function () {
    Route::middleware('permission:{models}.view')->group(function () {
        Route::resource('{models}', {Model}Controller::class);
        Route::post('{models}/bulk-delete', [{Model}Controller::class, 'bulkDelete'])
            ->name('{models}.bulk-delete')
            ->withoutMiddleware('permission:{models}.view')
            ->middleware('permission:{models}.delete');
    });
});
```

> **Note:** The existing admin routes already have a `Route::prefix('admin')->name('admin.')` group at line ~55. Add the resource inside that existing group — do not create a duplicate prefix group.

---

### 10. Sidebar entry — `resources/js/components/layout/Sidebar.vue`

Inside `adminMenuItems` (or `clientMenuItems`), add a permission-guarded entry.

**Standalone item (no group):**
```js
if (hasPermission('{models}.view')) {
    items.push({
        name: '{models}',
        label: t('sidebar.{models}'),
        route: '/{context}/{models}',
        icon: SomeIcon,  // import from @heroicons/vue/24/outline
    });
}
```

**Or inside an existing group's children array:**
```js
if (hasPermission('{models}.view')) {
    someGroupChildren.push({
        name: '{models}',
        label: t('sidebar.{models}'),
        route: '/{context}/{models}',
    });
}
```

Also add the translation key `sidebar.{models}` to both `en.json` and `ar.json`.

---

### 11. Index Page — `resources/js/pages/Modules/{context}/{Model}/{Model}Index.vue`

The user will specify which columns appear in the table. The structure is fixed:

```vue
<template>
    <div>
        <Head :title="$t('{models}.title')" />
        <div class="flex items-center justify-between mb-6">
            <div class="flex items-center gap-4">
                <h1 class="text-3xl font-bold transition-colors text-fg dark:text-fg-dark">
                    {{ $t('{models}.title') }}
                </h1>
                <div v-if="selectedItems.length > 0" class="flex items-center gap-2">
                    <span class="text-sm text-fg-secondary dark:text-fg-dark-muted">
                        {{ selectedItems.length }} selected
                    </span>
                    <Button variant="danger" size="sm" @click="showBulkDeleteModal = true">
                        {{ $t('common.delete') }} ({{ selectedItems.length }})
                    </Button>
                </div>
            </div>
            <Link :href="`/{context}/{models}/create`">
                <Button variant="primary">{{ $t('{models}.addNew') }}</Button>
            </Link>
        </div>

        <DataTable
            :columns="columns"
            :data="items"
            :meta="pagination"
            :filters="filters"
            :filterable="true"
            :filterConfig="filterConfig"
            @search="handleSearch"
            @sort="handleSort"
            @page-change="handlePageChange"
            @filter="handleFilter"
        >
            <template #checkbox="{ row }">
                <input type="checkbox" :checked="isSelected(row.id)" @change="toggleSelection(row.id)"
                    class="w-4 h-4 rounded border-gray-300 text-primary-600 dark:bg-background-dark-tertiary dark:border-gray-600" />
            </template>
            <template #header-checkbox>
                <input type="checkbox" :checked="allSelected" :indeterminate="someSelected" @change="toggleSelectAll"
                    class="w-4 h-4 rounded border-gray-300 text-primary-600 dark:bg-background-dark-tertiary dark:border-gray-600" />
            </template>
            <template #actions="{ row }">
                <div class="flex items-center gap-2">
                    <Link :href="`/{context}/{models}/${row.id}/edit`">
                        <button class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
                            <!-- pencil icon svg -->
                        </button>
                    </Link>
                    <button @click="confirmDelete(row)" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 cursor-pointer">
                        <!-- trash icon svg -->
                    </button>
                </div>
            </template>
        </DataTable>

        <Modal v-model="showDeleteModal" :title="$t('{models}.deleteConfirm')" size="sm">
            <p class="text-fg-secondary dark:text-fg-dark-muted">
                {{ $t('{models}.deleteConfirm') }} <strong>{{ {model}ToDelete?.name }}</strong>?
            </p>
            <template #footer>
                <Button variant="secondary" @click="showDeleteModal = false">{{ $t('common.cancel') }}</Button>
                <Button variant="danger" :loading="deleteProcessing" @click="handleDelete">{{ $t('common.delete') }}</Button>
            </template>
        </Modal>

        <Modal v-model="showBulkDeleteModal" :title="$t('{models}.bulkDeleteConfirm')" size="sm">
            <p class="text-fg-secondary dark:text-fg-dark-muted">
                Delete <strong>{{ selectedItems.length }}</strong> items? This cannot be undone.
            </p>
            <template #footer>
                <Button variant="secondary" @click="showBulkDeleteModal = false">{{ $t('common.cancel') }}</Button>
                <Button variant="danger" :loading="bulkDeleteProcessing" @click="handleBulkDelete">{{ $t('common.delete') }}</Button>
            </template>
        </Modal>
    </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { Head, Link, router } from '@inertiajs/vue3'
import { useI18n } from 'vue-i18n'
import DataTable from '@/components/tables/DataTable.vue'
import Button from '@/components/ui/Button.vue'
import Modal from '@/components/ui/Modal.vue'

const props = defineProps({
    items:      { type: Array,  required: true },
    pagination: { type: Object, required: true },
    filters:    { type: Object, required: true },
})

const { t } = useI18n()

// ← columns: user specifies which fields appear here
const columns = computed(() => [
    { key: 'checkbox',    label: '',                                     sortable: false, width: '50px' },
    // { key: 'name',     label: t('{models}.fields.name'),              sortable: true },
    // { key: 'status',   label: t('{models}.fields.status'),            sortable: true },
    { key: 'created_at', label: t('{models}.fields.createdAt'),          sortable: true },
    { key: 'actions',    label: '',                                       sortable: false },
])

// ← filterConfig: mirrors the columns the user wants to filter by
const filterConfig = computed(() => [
    // { key: 'name', type: 'text', label: t('{models}.fields.name'), placeholder: '...', defaultOperator: 'like' },
    // { key: 'status', type: 'select', label: t('{models}.fields.status'), options: [{value:'active',label:'Active'}] },
    { key: 'created_at', type: 'date-range', label: t('{models}.fields.createdAt') },
])

const selectedItems      = ref([])
const showDeleteModal    = ref(false)
const showBulkDeleteModal = ref(false)
const {model}ToDelete    = ref(null)
const deleteProcessing   = ref(false)
const bulkDeleteProcessing = ref(false)

const allSelected  = computed(() => props.items.length > 0 && selectedItems.value.length === props.items.length)
const someSelected = computed(() => selectedItems.value.length > 0 && selectedItems.value.length < props.items.length)
const isSelected   = (id) => selectedItems.value.includes(id)

const toggleSelection = (id) => {
    const i = selectedItems.value.indexOf(id)
    i > -1 ? selectedItems.value.splice(i, 1) : selectedItems.value.push(id)
}

const toggleSelectAll = () => {
    selectedItems.value = allSelected.value ? [] : props.items.map(i => i.id)
}

const handleSearch = (query) => {
    selectedItems.value = []
    router.get('/{context}/{models}', { ...props.filters, search: query, page: 1 }, { preserveState: true, preserveScroll: true })
}

const handleSort = ({ column, order }) => {
    router.get('/{context}/{models}', { ...props.filters, sort_by: column, sort_order: order }, { preserveState: true, preserveScroll: true })
}

const handlePageChange = (page) => {
    selectedItems.value = []
    router.get('/{context}/{models}', { ...props.filters, page }, { preserveState: true, preserveScroll: true })
}

const handleFilter = (filterData) => {
    selectedItems.value = []
    router.get('/{context}/{models}', { ...props.filters, ...filterData, page: 1 }, { preserveState: true, preserveScroll: true })
}

const confirmDelete = (row) => {
    {model}ToDelete.value = row
    showDeleteModal.value = true
}

const handleDelete = () => {
    if (!{model}ToDelete.value) return
    deleteProcessing.value = true
    router.delete(`/{context}/{models}/${  {model}ToDelete.value.id}`, {
        onSuccess: () => { showDeleteModal.value = false; {model}ToDelete.value = null },
        onFinish:  () => { deleteProcessing.value = false },
    })
}

const handleBulkDelete = () => {
    if (!selectedItems.value.length) return
    bulkDeleteProcessing.value = true
    router.post('/{context}/{models}/bulk-delete', { ids: selectedItems.value }, {
        onSuccess: () => { showBulkDeleteModal.value = false; selectedItems.value = [] },
        onFinish:  () => { bulkDeleteProcessing.value = false },
    })
}
</script>
```

---

### 12. Form Page — `resources/js/pages/Modules/{context}/{Model}/{Model}Form.vue`

The user will specify which input fields appear in the form. The structure is fixed:

```vue
<template>
    <div>
        <Head :title="isEditing ? $t('{models}.edit') : $t('{models}.addNew')" />
        <div class="flex items-center gap-4 mb-6">
            <Link :href="`/{context}/{models}`">
                <button class="p-2 rounded-lg transition-colors hover:bg-background-secondary text-fg-secondary dark:hover:bg-background-dark-tertiary dark:text-fg-dark-muted">
                    <!-- back arrow svg -->
                </button>
            </Link>
            <h1 class="text-3xl font-bold transition-colors text-fg dark:text-fg-dark">
                {{ isEditing ? $t('{models}.edit') : $t('{models}.addNew') }}
            </h1>
        </div>

        <div class="rounded-lg shadow p-6 bg-white border transition-colors dark:bg-background-dark-secondary dark:border-gray-700">
            <form @submit.prevent="handleSubmit">
                <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                    <!-- ← User-specified form fields go here -->

                    <!-- TextInput example -->
                    <!-- <TextInput
                        v-model="form.name"
                        :label="$t('{models}.fields.name')"
                        :placeholder="$t('{models}.placeholders.name')"
                        :error="form.errors.name"
                        required
                    /> -->

                    <!-- Select example -->
                    <!-- <Select
                        v-model="form.status"
                        :label="$t('{models}.fields.status')"
                        :options="statusOptions"
                        :error="form.errors.status"
                        required
                    /> -->
                </div>

                <div class="flex items-center justify-end gap-4 mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
                    <Link :href="`/{context}/{models}`">
                        <Button type="button" variant="secondary">{{ $t('common.cancel') }}</Button>
                    </Link>
                    <Button type="submit" variant="primary" :loading="form.processing">
                        {{ isEditing ? $t('common.update') : $t('common.create') }}
                    </Button>
                </div>
            </form>
        </div>
    </div>
</template>

<script setup>
import { computed } from 'vue'
import { Head, Link, useForm } from '@inertiajs/vue3'
import { useI18n } from 'vue-i18n'
import TextInput from '@/components/inputs/TextInput.vue'
import Select from '@/components/inputs/Select.vue'
import Button from '@/components/ui/Button.vue'

const props = defineProps({
    {model}: { type: Object, default: null },
    // options: { type: Array, required: true }  ← if the form needs select data
})

const { t } = useI18n()

const isEditing = computed(() => !!props.{model})

// ← Initialize every fillable field the user specifies
const form = useForm({
    // name:   props.{model}?.name   || '',
    // status: props.{model}?.status || 'active',
})

const handleSubmit = () => {
    if (isEditing.value) {
        form.put(`/{context}/{models}/${props.{model}.id}`)
    } else {
        form.post(`/{context}/{models}`)
    }
}
</script>
```

---

### 13. Backend Flash Messages — `lang/en/{models}.php` and `lang/ar/{models}.php`

⚠️ **CRITICAL — easy to miss:** the Controller's `__('{models}.created')` / `.updated` / `.deleted` calls
(and the singleton-form-controller's `__('{model}.updated')`, see Hero/AboutSection pattern) read from
**`lang/{locale}/{models}.php`** — a completely different tree from the frontend's
`resources/js/i18n/locales/*.json`. Creating only the JSON file leaves the flash message untranslated
(Laravel falls back to printing the raw key, e.g. `courseSection.updated`, on screen).

Create **both** files — one per locale, no nesting, no `common.` prefix:

`lang/en/{models}.php`:
```php
<?php

return [
    'created' => '{Model} created successfully.',
    'updated' => '{Model} updated successfully.',
    'deleted' => '{Model} deleted successfully.',
];
```

`lang/ar/{models}.php`:
```php
<?php

return [
    'created' => 'تم إنشاء {Model} بنجاح.',
    'updated' => 'تم تحديث {Model} بنجاح.',
    'deleted' => 'تم حذف {Model} بنجاح.',
];
```

For a single-content-block module (Hero/AboutSection/CourseSection style — only `edit`/`update`), only
the `updated` key is needed. Match every `__('...')` string literally used in the Controller — grep the
controller for `__(` and confirm each key exists in both files.

---

### 14. Frontend Translations — `resources/js/i18n/locales/en.json` and `ar.json`

Merge this structure into the existing JSON (do not replace the whole file):

```json
"{models}": {
  "title":             "{Model} List",
  "addNew":            "Add {Model}",
  "edit":              "Edit {Model}",
  "deleteConfirm":     "Are you sure you want to delete",
  "bulkDeleteConfirm": "Bulk Delete {Model}s",
  "created":           "{Model} created successfully",
  "updated":           "{Model} updated successfully",
  "deleted":           "{Model} deleted successfully",
  "fields": {
    "name":       "Name",
    "createdAt":  "Created At"
    // ← add one entry per fillable field
  },
  "placeholders": {
    "name": "Enter name"
    // ← add one entry per fillable field
  }
}
```

Also add to the `"sidebar"` key:
```json
"sidebar": {
  "{models}": "{Model}s"
}
```

Arabic (`ar.json`) follows the same structure with Arabic text.

---

## Checklist

Before considering a module done, verify:

- [ ] Migration created and run (`php artisan migrate`)
- [ ] Model has correct `$fillable`, `$searchable`, `$filterable`
- [ ] Controller uses `HasDataTableInertia` and `HasMiddleware`
- [ ] Service has create / update / delete / bulkDelete methods, all wrapped in `DB::transaction`
- [ ] Store & Update Requests have rules for every fillable field
- [ ] Resource returns every fillable field + `id`, `created_at`, `updated_at`
- [ ] Permission seeder created and run
- [ ] Routes added inside the existing `auth` middleware group (not a duplicate group)
- [ ] Sidebar entry added with `hasPermission('{models}.view')` guard
- [ ] `lang/en/{models}.php` and `lang/ar/{models}.php` created with every key the Controller's `__(...)` calls reference (flash messages — separate from the JSON i18n files, easy to forget)
- [ ] `en.json` and `ar.json` updated with all translation keys
- [ ] Index page: `columns` and `filterConfig` match user-specified fields
- [ ] Form page: `useForm` initialized with all fillable fields, inputs rendered
- [ ] Dark mode classes used on every container (`dark:bg-background-dark-secondary`, etc.)
- [ ] No hardcoded text — every string uses `$t()`
- [ ] No API service files or Pinia data stores created (data comes from Inertia props only)

---

## Common Field Types Reference

### Validation rules (Store/Update Requests)
| Field type      | Rule example                                              |
|-----------------|-----------------------------------------------------------|
| String          | `['required', 'string', 'max:255']`                      |
| Email (unique)  | `['required', 'email', 'unique:{models_snake},email']`   |
| Enum/status     | `['required', 'in:active,inactive']`                     |
| Integer FK      | `['required', 'integer', 'exists:other_table,id']`       |
| Nullable text   | `['nullable', 'string']`                                 |
| Boolean         | `['boolean']`                                            |
| Date            | `['required', 'date']`                                   |
| File/image      | `['nullable', 'image', 'max:2048']`                      |

### Vue input components
| Field type  | Component                     |
|-------------|-------------------------------|
| Text/email  | `<TextInput type="text" />`   |
| Password    | `<TextInput type="password" />`|
| Textarea    | `<TextInput type="textarea" />`|
| Select/enum | `<Select :options="..." />`   |
| Date        | `<DatePicker />`              |
| Image       | `<ImagePicker />`             |

All components live in `resources/js/components/inputs/`.
