# RULE FILE 09 — UI, Livewire & Frontend Rules

---

## Livewire Component Structure — THE PATTERN

Every Livewire component follows this exact pattern. No creative variations.

### List Component

```php
namespace App\Livewire\{Module};

use Livewire\Component;
use Livewire\WithPagination;
use Livewire\Attributes\Url;

class {Entity}List extends Component
{
    use WithPagination;

    #[Url]
    public string $search = '';
    
    #[Url]
    public string $status = '';
    
    #[Url]
    public string $sortBy = 'created_at';
    
    #[Url]
    public string $sortDir = 'desc';
    
    public int $perPage = 25;

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function updatedStatus(): void
    {
        $this->resetPage();
    }

    public function sort(string $column): void
    {
        if ($this->sortBy === $column) {
            $this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
        } else {
            $this->sortBy = $column;
            $this->sortDir = 'asc';
        }
    }

    public function render()
    {
        $query = Entity::query()
            ->when($this->search, fn ($q, $s) => $q->where('name_ar', 'ilike', "%{$s}%")
                ->orWhere('name', 'ilike', "%{$s}%")
                ->orWhere('phone', 'like', "%{$s}%"))
            ->when($this->status, fn ($q, $s) => $q->where('status', $s))
            ->orderBy($this->sortBy, $this->sortDir);

        // Apply permission scope
        $query = PermissionService::applyScope($query, auth()->user(), 'entities.list');

        return view('livewire.module.entity-list', [
            'entities' => $query->paginate($this->perPage),
        ]);
    }
}
```

### Create/Edit Component

```php
namespace App\Livewire\{Module};

use Livewire\Component;

class {Entity}Create extends Component
{
    // Public properties = form fields
    public string $name = '';
    public string $name_ar = '';
    // ... all fields

    // Optional: for edit mode
    public ?Entity $entity = null;

    public function mount(?Entity $entity = null): void
    {
        $this->authorize('entities.create'); // or 'entities.update'
        
        if ($entity) {
            $this->entity = $entity;
            $this->name = $entity->name;
            $this->name_ar = $entity->name_ar;
            // ... fill from entity
        }
    }

    protected function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'name_ar' => 'required|string|max:255',
            // ... validation rules matching migration constraints EXACTLY
        ];
    }

    protected function messages(): array
    {
        return [
            'name.required' => __('الاسم مطلوب'),
            'name_ar.required' => __('الاسم بالعربية مطلوب'),
            // ... Arabic error messages
        ];
    }

    public function save(): void
    {
        $validated = $this->validate();

        try {
            if ($this->entity) {
                $entity = app(EntityService::class)->update($this->entity, $validated, auth()->user());
                session()->flash('success', __('تم التحديث بنجاح'));
            } else {
                $entity = app(EntityService::class)->create($validated, auth()->user());
                session()->flash('success', __('تم الإضافة بنجاح'));
            }
            $this->redirect(route('entities.show', $entity));
        } catch (DomainException $e) {
            $this->addError('general', $e->getMessage());
        }
    }

    public function render()
    {
        return view('livewire.module.entity-create');
    }
}
```

---

## Blade Component Rules

### Component Naming Convention

```
x-ui.button          → resources/views/components/ui/button.blade.php
x-ui.input           → resources/views/components/ui/input.blade.php
x-ui.modal           → resources/views/components/ui/modal.blade.php
x-layouts.sidebar    → resources/views/components/layouts/sidebar.blade.php
x-forms.field-group  → resources/views/components/forms/field-group.blade.php
x-data.data-table    → resources/views/components/data/data-table.blade.php
```

### Component Props — The Required Interface

**Button:**
```php
@props([
    'type' => 'button',        // button, submit, link
    'variant' => 'primary',    // primary, secondary, danger, ghost, outline
    'size' => 'md',            // sm, md, lg
    'href' => null,            // if type=link
    'disabled' => false,
    'loading' => false,        // shows spinner, disables
])
```

**Input:**
```php
@props([
    'type' => 'text',          // text, email, number, tel, password
    'name' => '',              // wire:model target
    'label' => '',             // Arabic label
    'placeholder' => '',
    'required' => false,
    'disabled' => false,
    'error' => null,           // from $errors->first('field')
    'hint' => null,            // helper text below input
])
```

**Modal:**
```php
@props([
    'maxWidth' => 'md',        // sm, md, lg, xl, 2xl
    'title' => '',
    'closeable' => true,
])
// Uses Alpine x-show + x-transition
// Parent owns the visibility state (x-data in parent)
```

**Data Table:**
```php
@props([
    'columns' => [],           // [{key, label, sortable, class}]
    'sortBy' => '',
    'sortDir' => 'asc',
    'emptyMessage' => 'لا توجد بيانات',
])
```

---

## Alpine.js Rules — ZERO AMBIGUITY

### Rule 1: ONE x-data scope per feature

```html
<!-- CORRECT: single scope owns all tab state -->
<div x-data="{ activeTab: 'info', showModal: false }">
    <button @click="activeTab = 'info'">المعلومات</button>
    <button @click="activeTab = 'financial'">المالية</button>
    <div x-show="activeTab === 'info'">...</div>
    <div x-show="activeTab === 'financial'">...</div>
    <x-ui.modal x-show="showModal" @close="showModal = false">...</x-ui.modal>
</div>

<!-- WRONG: nested x-data shadows parent -->
<div x-data="{ activeTab: 'info' }">
    <div x-data="{ activeTab: 'sub1' }"> <!-- SHADOWS PARENT! -->
    </div>
</div>
```

### Rule 2: Alpine for UI state, Livewire for server state

- Alpine owns: tabs, dropdowns, modals, tooltips, accordions, visual toggles
- Livewire owns: data, search, filters, pagination, form submission, CRUD

### Rule 3: Communication between Alpine and Livewire

```html
<!-- Alpine → Livewire -->
<button @click="$wire.deleteItem(itemId)">حذف</button>

<!-- Livewire → Alpine (via events) -->
<div x-data="{ open: false }" @item-saved.window="open = false">

<!-- In Livewire component: -->
$this->dispatch('item-saved');
```

### Rule 4: No inline complex logic

```html
<!-- WRONG -->
<button @click="items = items.filter(i => i.id !== item.id); total = items.reduce((s, i) => s + i.price, 0)">

<!-- CORRECT: use Alpine method -->
<div x-data="cartManager()">
    <button @click="removeItem(item.id)">
</div>

<script>
function cartManager() {
    return {
        items: [],
        removeItem(id) {
            this.items = this.items.filter(i => i.id !== id);
        },
        get total() {
            return this.items.reduce((s, i) => s + i.price, 0);
        }
    }
}
</script>
```

---

## RTL Rules — ENFORCED EVERYWHERE

### Tailwind Classes — ALWAYS Use Logical Properties

| NEVER use | ALWAYS use | Reason |
|-----------|-----------|--------|
| `ml-*` | `ms-*` | margin-start (right in RTL) |
| `mr-*` | `me-*` | margin-end (left in RTL) |
| `pl-*` | `ps-*` | padding-start |
| `pr-*` | `pe-*` | padding-end |
| `text-left` | `text-start` | start = right in RTL |
| `text-right` | `text-end` | end = left in RTL |
| `left-*` | `start-*` | positioning |
| `right-*` | `end-*` | positioning |
| `border-l-*` | `border-s-*` | border-start |
| `border-r-*` | `border-e-*` | border-end |
| `rounded-l-*` | `rounded-s-*` | border-radius start |
| `rounded-r-*` | `rounded-e-*` | border-radius end |
| `float-left` | `float-start` | float direction |
| `float-right` | `float-end` | float direction |

### Exceptions (physical, not logical — KEEP these)

- `rotate-180` for icons that should flip
- `ltr:` / `rtl:` variants for edge cases (e.g., phone number inputs always LTR)
- Number inputs: `dir="ltr"` on the input element (numbers read LTR even in Arabic)

### Layout Direction

```html
<html dir="rtl" lang="ar">
<!-- DEFAULT is RTL. LTR is the override. -->
```

### Font Loading

```css
@layer base {
    html {
        font-family: 'Cairo', 'Noto Sans Arabic', system-ui, sans-serif;
    }
    /* Numbers and English text within Arabic context */
    .font-latin {
        font-family: 'Inter', system-ui, sans-serif;
    }
}
```

---

## Bilingual Display Rules

### Which language to show?

```php
// Helper used throughout Blade
function localized_name(Model $model): string
{
    if (app()->getLocale() === 'ar') {
        return $model->name_ar ?: $model->name;
    }
    return $model->name ?: $model->name_ar;
}
```

### Form fields — ALWAYS BOTH

Every form that has a `name` field also has `name_ar`:
```html
<x-forms.field-group name="name" label="Name (English)" />
<x-forms.field-group name="name_ar" label="الاسم بالعربية" required />
```

`name_ar` is REQUIRED (primary language). `name` can be optional (for internal/English reference).

---

## Navigation & Routing Rules

### Route Naming Convention

```php
Route::prefix('{module}')->name('{module}.')->group(function () {
    Route::get('/', ModuleList::class)->name('list');
    Route::get('/create', ModuleCreate::class)->name('create');
    Route::get('/{entity:uuid}', ModuleShow::class)->name('show');
    Route::get('/{entity:uuid}/edit', ModuleEdit::class)->name('edit');
});
```

Pattern: `{module}.{action}` → `participants.list`, `participants.show`, `invoices.create`

### Route Resolution — UUID, Not ID

All public-facing routes use UUID: `/participants/{uuid}` not `/participants/{id}`

The `HasUuid` trait sets `getRouteKeyName() => 'uuid'`

### Sidebar Rules

1. ONE sidebar partial per layout: `components/layouts/sidebar.blade.php`
2. Active state: `request()->routeIs('participants.*')` for highlight
3. Permission-gated: `@can('participants.list')` wraps each nav item
4. If user has NO permission for a section: item is NOT rendered (not greyed out)
5. Sections are grouped logically (see UI Architecture doc)
6. Badge counts (e.g., pending approvals): loaded via Livewire poll, not on every page load

### Link Rules — ABSOLUTE

1. Every `<a href>` uses `route('name')` or `route('name', $param)` — NEVER hardcoded URLs
2. `href="#"` is FORBIDDEN. If the feature isn't built → don't render the link.
3. External links get `target="_blank" rel="noopener"` — internal links do not.
4. Active navigation: `:class="request()->routeIs('route.*') ? 'bg-primary-100 text-primary-700' : 'text-gray-600'"` pattern

---

## Toast Notifications (Flash Messages)

### Setting (in Livewire component or controller)

```php
session()->flash('success', __('تم الحفظ بنجاح'));
session()->flash('error', __('حدث خطأ — حاول مرة أخرى'));
session()->flash('warning', __('تنبيه: الرصيد منخفض'));
session()->flash('info', __('تم إرسال الإشعار'));
```

### Display (in layout)

```html
<!-- In app.blade.php layout, once -->
<div x-data="{ toasts: [] }" 
     @toast.window="toasts.push($event.detail); setTimeout(() => toasts.shift(), 5000)">
    <template x-for="toast in toasts">
        <div :class="toast.type === 'success' ? 'border-green-500' : 'border-red-500'"
             class="fixed top-4 start-4 ...">
            <span x-text="toast.message"></span>
        </div>
    </template>
</div>
```

### Rules

1. Success: green. Error: red. Warning: yellow. Info: blue.
2. Auto-dismiss after 5 seconds
3. Stack up to 3 visible (older ones removed)
4. Position: top-start (top-right in RTL)
5. Must be accessible: `role="alert"` for errors, `role="status"` for success

---

## Loading States

### On form submit

```html
<x-ui.button type="submit" wire:loading.attr="disabled" wire:target="save">
    <span wire:loading.remove wire:target="save">حفظ</span>
    <span wire:loading wire:target="save">جارٍ الحفظ...</span>
</x-ui.button>
```

### On table loading

```html
<div wire:loading.class="opacity-50 pointer-events-none" wire:target="search, status, sort">
    <table>...</table>
</div>
```

### Rules

1. EVERY form submit button disables during submission
2. EVERY table/list shows loading indicator during filter/search/sort
3. NEVER use full-page spinners — use inline indicators
4. Loading must be targeted (`wire:target`) — not global

---

## Empty States

Every list view has an empty state when no records exist:

```html
@if($entities->isEmpty())
    <x-ui.empty-state
        icon="heroicon-o-users"
        title="لا يوجد مشتركين"
        description="ابدأ بإضافة أول مشترك في الأكاديمية"
        :action-url="route('participants.create')"
        action-label="إضافة مشترك"
    />
@else
    {{-- table --}}
@endif
```

---

## Form Validation Display

```html
<x-forms.field-group 
    name="phone" 
    label="رقم الهاتف" 
    :error="$errors->first('phone')">
    <input type="tel" wire:model="phone" dir="ltr" 
           class="@error('phone') border-red-500 @enderror">
</x-forms.field-group>
```

Rules:
1. Error text appears BELOW the field, in red, font-size smaller
2. Field border turns red on error
3. Error messages are in Arabic (from `messages()` method)
4. Real-time validation: `wire:model.blur` for expensive checks, `wire:model.live.debounce.300ms` for search
5. Top-of-form alert for general errors (non-field-specific)
