# Cycle 9: UI Component Library

## Goal
Create the full `x-ui.*` Blade component library, form components, data components, toast system, Chart.js integration, dark mode, and language switcher.

---

## Directory Structure

```
resources/views/components/
├── ui/
│   ├── button.blade.php
│   ├── input.blade.php
│   ├── select.blade.php
│   ├── textarea.blade.php
│   ├── checkbox.blade.php
│   ├── radio.blade.php
│   ├── toggle.blade.php
│   ├── badge.blade.php
│   ├── alert.blade.php
│   ├── card.blade.php
│   ├── modal.blade.php
│   ├── dropdown.blade.php
│   ├── table.blade.php
│   ├── pagination.blade.php
│   ├── tabs.blade.php
│   ├── stat-card.blade.php
│   ├── avatar.blade.php
│   ├── empty-state.blade.php
│   └── loading-spinner.blade.php
├── forms/
│   ├── field-group.blade.php
│   ├── date-picker.blade.php
│   ├── file-upload.blade.php
│   ├── rich-text.blade.php
│   └── search-select.blade.php
└── data/
    ├── data-table.blade.php
    ├── filter-bar.blade.php
    └── bulk-actions.blade.php
```

---

## Component Specifications

### `x-ui.button`
**Props:** `type` (button|submit|link), `variant` (primary|secondary|danger|ghost|outline), `size` (sm|md|lg), `disabled`, `loading`, `href`, `icon`

```blade
@props([
    'type' => 'button',
    'variant' => 'primary',
    'size' => 'md',
    'disabled' => false,
    'loading' => false,
    'href' => null,
    'icon' => null,
])

@php
$baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed';

$variants = [
    'primary' => 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
    'secondary' => 'bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-500',
    'danger' => 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
    'ghost' => 'text-gray-600 hover:bg-gray-100 focus:ring-gray-500',
    'outline' => 'border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-blue-500',
];

$sizes = [
    'sm' => 'px-3 py-1.5 text-sm gap-1.5',
    'md' => 'px-4 py-2 text-sm gap-2',
    'lg' => 'px-6 py-3 text-base gap-2.5',
];

$classes = $baseClasses . ' ' . $variants[$variant] . ' ' . $sizes[$size];
@endphp

@if($href)
    <a href="{{ $href }}" {{ $attributes->merge(['class' => $classes]) }} wire:navigate>
        {{ $slot }}
    </a>
@else
    <button type="{{ $type }}" {{ $attributes->merge(['class' => $classes, 'disabled' => $disabled || $loading]) }}>
        @if($loading)
            <x-ui.loading-spinner size="sm" />
        @endif
        @if($icon && !$loading)
            {!! $icon !!}
        @endif
        {{ $slot }}
    </button>
@endif
```

---

### `x-ui.input`
**Props:** `name`, `label`, `type`, `error`, `required`, `disabled`, `hint`, `dir`

```blade
@props([
    'name',
    'label' => null,
    'type' => 'text',
    'error' => null,
    'required' => false,
    'disabled' => false,
    'hint' => null,
    'dir' => null,
])

<div>
    @if($label)
        <label for="{{ $name }}" class="block text-sm font-medium text-gray-700 mb-1">
            {{ $label }}
            @if($required)<span class="text-red-500">*</span>@endif
        </label>
    @endif

    <input
        type="{{ $type }}"
        id="{{ $name }}"
        name="{{ $name }}"
        @if($dir) dir="{{ $dir }}" @endif
        {{ $attributes->merge([
            'class' => 'block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm'
                . ($error ? ' border-red-500 focus:border-red-500 focus:ring-red-500' : '')
                . ($disabled ? ' bg-gray-50 cursor-not-allowed' : ''),
            'disabled' => $disabled,
        ]) }}
    />

    @if($hint && !$error)
        <p class="mt-1 text-xs text-gray-500">{{ $hint }}</p>
    @endif

    @if($error)
        <p class="mt-1 text-xs text-red-600">{{ $error }}</p>
    @endif
</div>
```

---

### `x-ui.select`
**Props:** `name`, `label`, `options` (array), `error`, `required`, `disabled`, `placeholder`

---

### `x-ui.modal`
**Props:** `title`, `maxWidth` (sm|md|lg|xl|2xl), `closeable`

Uses Alpine.js `x-show` with transitions. Emits `close` event.

```blade
@props([
    'title' => '',
    'maxWidth' => 'lg',
    'closeable' => true,
])

@php
$maxWidths = [
    'sm' => 'sm:max-w-sm',
    'md' => 'sm:max-w-md',
    'lg' => 'sm:max-w-lg',
    'xl' => 'sm:max-w-xl',
    '2xl' => 'sm:max-w-2xl',
];
@endphp

<div
    x-data="{ show: @entangle($attributes->wire('model')) }"
    x-show="show"
    x-transition:enter="ease-out duration-300"
    x-transition:enter-start="opacity-0"
    x-transition:enter-end="opacity-100"
    x-transition:leave="ease-in duration-200"
    x-transition:leave-start="opacity-100"
    x-transition:leave-end="opacity-0"
    class="fixed inset-0 z-50 overflow-y-auto"
    style="display: none;"
>
    <!-- Backdrop -->
    <div class="fixed inset-0 bg-gray-500/75" @if($closeable) @click="show = false" @endif></div>

    <!-- Panel -->
    <div class="flex min-h-full items-center justify-center p-4">
        <div class="relative w-full {{ $maxWidths[$maxWidth] }} bg-white rounded-xl shadow-xl">
            <!-- Header -->
            <div class="flex items-center justify-between px-6 py-4 border-b">
                <h3 class="text-lg font-semibold text-gray-900">{{ $title }}</h3>
                @if($closeable)
                    <button @click="show = false" class="text-gray-400 hover:text-gray-600">
                        <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
                        </svg>
                    </button>
                @endif
            </div>

            <!-- Body -->
            <div class="px-6 py-4">{{ $slot }}</div>

            <!-- Footer (optional named slot) -->
            @if(isset($footer))
                <div class="px-6 py-4 border-t bg-gray-50 rounded-b-xl flex justify-end gap-3">
                    {{ $footer }}
                </div>
            @endif
        </div>
    </div>
</div>
```

---

### `x-ui.stat-card`
**Props:** `title`, `value`, `icon`, `trend` (up|down|neutral), `trendValue`, `color`

---

### `x-ui.badge`
**Props:** `color` (green|red|yellow|blue|gray|purple), `size` (sm|md)

---

### `x-ui.empty-state`
**Props:** `title`, `description`, `icon`, `actionLabel`, `actionRoute`

---

### `x-ui.tabs`
Alpine-controlled tabs component:
```blade
@props(['active' => null, 'tabs' => []])

<div x-data="{ activeTab: '{{ $active ?? $tabs[0]['key'] ?? '' }}' }">
    <!-- Tab headers -->
    <div class="border-b flex gap-1 overflow-x-auto">
        @foreach($tabs as $tab)
            <button
                @click="activeTab = '{{ $tab['key'] }}'"
                :class="activeTab === '{{ $tab['key'] }}'
                    ? 'border-blue-500 text-blue-600'
                    : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
                class="px-4 py-2 text-sm font-medium border-b-2 whitespace-nowrap transition-colors"
            >
                {{ $tab['label'] }}
            </button>
        @endforeach
    </div>

    <!-- Tab panels -->
    {{ $slot }}
</div>
```

---

### `x-data.data-table`
**Props:** `columns` (array), `sortable`, `selectable`

Provides: header row with sort indicators, slot for body rows, responsive overflow, loading overlay.

---

### `x-data.filter-bar`
**Props:** slot-based, wraps filter controls in a consistent row with "clear all" button.

---

### `x-forms.search-select`
Alpine-powered searchable dropdown with remote data fetching:
- Text input with debounced search
- Dropdown results from `wire:model` or static options
- Selected item display with clear button

---

## Toast Notification System

### Component: `resources/views/components/ui/toast.blade.php`

Alpine.js global toast store:
```blade
<div
    x-data="toastStore()"
    @toast.window="add($event.detail)"
    class="fixed top-4 start-4 z-[9999] flex flex-col gap-2 pointer-events-none"
>
    <template x-for="toast in toasts" :key="toast.id">
        <div
            x-show="toast.visible"
            x-transition:enter="transform ease-out duration-300 transition"
            x-transition:enter-start="translate-x-full opacity-0"
            x-transition:enter-end="translate-x-0 opacity-100"
            x-transition:leave="transition ease-in duration-200"
            x-transition:leave-start="opacity-100"
            x-transition:leave-end="opacity-0"
            class="pointer-events-auto max-w-sm w-full bg-white rounded-lg shadow-lg ring-1 ring-black/5 p-4"
            :class="{
                'border-s-4 border-green-500': toast.type === 'success',
                'border-s-4 border-red-500': toast.type === 'error',
                'border-s-4 border-yellow-500': toast.type === 'warning',
                'border-s-4 border-blue-500': toast.type === 'info',
            }"
        >
            <div class="flex items-start gap-3">
                <p class="text-sm text-gray-800 flex-1" x-text="toast.message"></p>
                <button @click="remove(toast.id)" class="text-gray-400 hover:text-gray-600">
                    &times;
                </button>
            </div>
        </div>
    </template>
</div>

<script>
function toastStore() {
    return {
        toasts: [],
        add(toast) {
            const id = Date.now();
            this.toasts.push({ ...toast, id, visible: true });
            setTimeout(() => this.remove(id), toast.duration || 5000);
        },
        remove(id) {
            const toast = this.toasts.find(t => t.id === id);
            if (toast) toast.visible = false;
            setTimeout(() => { this.toasts = this.toasts.filter(t => t.id !== id); }, 300);
        }
    };
}
</script>
```

### Usage from Livewire:
```php
$this->dispatch('toast', type: 'success', message: __('تم الحفظ بنجاح'));
```

---

## Chart.js Integration

### Install:
```bash
npm install chart.js
```

### Alpine Chart Component: `resources/js/chart.js`
```js
import { Chart, registerables } from 'chart.js';
Chart.register(...registerables);

// Set RTL defaults
Chart.defaults.font.family = "'Cairo', sans-serif";

document.addEventListener('alpine:init', () => {
    Alpine.data('chartComponent', (config) => ({
        chart: null,
        init() {
            this.chart = new Chart(this.$refs.canvas, config);
        },
        update(newData) {
            this.chart.data = newData;
            this.chart.update();
        },
        destroy() {
            this.chart?.destroy();
        }
    }));
});
```

### Usage in Blade:
```blade
<div x-data="chartComponent({
    type: 'line',
    data: @js($chartData),
    options: { responsive: true, plugins: { legend: { rtl: true } } }
})">
    <canvas x-ref="canvas" class="w-full h-64"></canvas>
</div>
```

---

## Dark Mode

### Tailwind 4 Configuration (in `app.css`):
```css
@import 'tailwindcss';
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
```

### Toggle (in topbar):
```blade
<button
    x-data="{ dark: localStorage.getItem('theme') === 'dark' }"
    x-init="$watch('dark', v => { document.documentElement.dataset.theme = v ? 'dark' : 'light'; localStorage.setItem('theme', v ? 'dark' : 'light'); })"
    @click="dark = !dark"
    class="p-2 rounded-lg text-gray-500 hover:bg-gray-100"
>
    <svg x-show="!dark" class="w-5 h-5"><!-- moon icon --></svg>
    <svg x-show="dark" class="w-5 h-5"><!-- sun icon --></svg>
</button>
```

### Add dark variants to components:
- `bg-white` → `bg-white dark:bg-gray-800`
- `text-gray-900` → `text-gray-900 dark:text-gray-100`
- `border-gray-200` → `border-gray-200 dark:border-gray-700`

---

## Language Switcher

### Middleware: `app/Http/Middleware/SetLocale.php`
```php
public function handle($request, Closure $next)
{
    $locale = session('locale', config('app.locale'));
    app()->setLocale($locale);
    return $next($request);
}
```

### Livewire Component (inline in topbar):
```blade
<div x-data="{ open: false }" class="relative">
    <button @click="open = !open" class="text-sm text-gray-600 hover:text-gray-900">
        {{ app()->getLocale() === 'ar' ? 'عربي' : 'EN' }}
    </button>
    <div x-show="open" @click.away="open = false" class="absolute end-0 mt-2 w-24 bg-white border rounded-lg shadow-lg">
        <a href="{{ route('locale.switch', 'ar') }}" class="block px-4 py-2 text-sm hover:bg-gray-100" wire:navigate>عربي</a>
        <a href="{{ route('locale.switch', 'en') }}" class="block px-4 py-2 text-sm hover:bg-gray-100" wire:navigate>English</a>
    </div>
</div>
```

### Route:
```php
Route::get('/locale/{locale}', function (string $locale) {
    if (in_array($locale, ['ar', 'en'])) {
        session(['locale' => $locale]);
    }
    return redirect()->back();
})->name('locale.switch');
```

---

## Refactoring Plan

After creating the component library, refactor existing views to use it. Priority order:
1. All `<input>` → `<x-ui.input>`
2. All `<button>` → `<x-ui.button>`
3. All modals → `<x-ui.modal>`
4. All stat sections → `<x-ui.stat-card>`
5. All status badges → `<x-ui.badge>`
6. All empty states → `<x-ui.empty-state>`
7. All tables → `<x-data.data-table>` wrapper

---

## Verification

- [ ] All 18 UI components render correctly in isolation
- [ ] `<x-ui.button variant="primary" loading>` shows spinner
- [ ] `<x-ui.modal>` opens/closes with transitions
- [ ] `<x-ui.input error="required">` shows red border + message
- [ ] Toast dispatched from Livewire → appears and auto-dismisses
- [ ] Chart renders revenue data on dashboard
- [ ] Dark mode toggle persists across page loads
- [ ] Language switcher changes all UI strings
- [ ] All components respect RTL (logical properties only)
