docs: add UI components documentation
Document reusable UI components and patterns: - Page layout structure - Page header component - Stats cards - Search and filters bar - Data table structure - Pagination component Provides reference for consistent UI development. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
387
docs/frontend/ui-components.md
Normal file
387
docs/frontend/ui-components.md
Normal file
@@ -0,0 +1,387 @@
|
||||
# UI Components
|
||||
|
||||
This document describes the reusable UI components and patterns used in the Wizamart admin panel.
|
||||
|
||||
## Page Layout Structure
|
||||
|
||||
All admin list pages follow a consistent structure:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Page Header (Title + Action Button) │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ Stats Cards (4 columns on desktop) │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ Search & Filters Bar │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ Data Table │
|
||||
│ ├── Table Header │
|
||||
│ ├── Table Rows │
|
||||
│ └── Pagination │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Page Header
|
||||
|
||||
The page header contains the page title and primary action button.
|
||||
|
||||
```html
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Page Title
|
||||
</h2>
|
||||
<a href="/admin/resource/create"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
||||
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
||||
Create Resource
|
||||
</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Stats Cards
|
||||
|
||||
Stats cards display key metrics in a 4-column grid layout.
|
||||
|
||||
### Structure
|
||||
|
||||
```html
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<!-- Card Template -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-{color}-500 bg-{color}-100 rounded-full dark:text-{color}-100 dark:bg-{color}-500">
|
||||
<span x-html="$icon('icon-name', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Label
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="value">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Color Options
|
||||
|
||||
| Color | Use Case | Example |
|
||||
|-------|----------|---------|
|
||||
| `blue` | Total counts | Total Users, Total Companies |
|
||||
| `green` | Positive status | Active, Verified |
|
||||
| `red` | Negative status | Inactive, Errors |
|
||||
| `orange` | Special/Admin | Admin users, Warnings |
|
||||
| `purple` | Primary/Vendors | Active vendors, Main metrics |
|
||||
|
||||
### Icon Style
|
||||
|
||||
- Icons should be inside a **circular** container (`rounded-full`)
|
||||
- Icon size: `w-5 h-5`
|
||||
- Container padding: `p-3`
|
||||
|
||||
## Search & Filters Bar
|
||||
|
||||
The search and filters bar provides filtering capabilities for list pages.
|
||||
|
||||
### Structure
|
||||
|
||||
```html
|
||||
<div class="mb-6 px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<!-- Search Input -->
|
||||
<div class="flex-1 max-w-md">
|
||||
<div class="relative">
|
||||
<input
|
||||
type="text"
|
||||
x-model="filters.search"
|
||||
@input="debouncedSearch()"
|
||||
placeholder="Search..."
|
||||
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:bg-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Dropdowns -->
|
||||
<div class="flex gap-3">
|
||||
<select
|
||||
x-model="filters.filterName"
|
||||
@change="loadData()"
|
||||
class="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300"
|
||||
>
|
||||
<option value="">All Options</option>
|
||||
<option value="value1">Option 1</option>
|
||||
<option value="value2">Option 2</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### JavaScript Implementation
|
||||
|
||||
```javascript
|
||||
// State
|
||||
filters: {
|
||||
search: '',
|
||||
status: '',
|
||||
type: ''
|
||||
},
|
||||
|
||||
// Debounced search function
|
||||
debouncedSearch() {
|
||||
if (this._searchTimeout) {
|
||||
clearTimeout(this._searchTimeout);
|
||||
}
|
||||
this._searchTimeout = setTimeout(() => {
|
||||
this.pagination.page = 1; // Reset to first page
|
||||
this.loadData();
|
||||
}, 300);
|
||||
},
|
||||
|
||||
// Load data with filters
|
||||
async loadData() {
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', this.pagination.page);
|
||||
params.append('per_page', this.pagination.per_page);
|
||||
|
||||
if (this.filters.search) {
|
||||
params.append('search', this.filters.search);
|
||||
}
|
||||
if (this.filters.status) {
|
||||
params.append('status', this.filters.status);
|
||||
}
|
||||
|
||||
const response = await apiClient.get(`/admin/resource?${params}`);
|
||||
// Handle response...
|
||||
}
|
||||
```
|
||||
|
||||
### Backend API Support
|
||||
|
||||
The API endpoint should support these query parameters:
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `page` | int | Page number (1-based) |
|
||||
| `per_page` | int | Items per page |
|
||||
| `search` | string | Search term (searches multiple fields) |
|
||||
| `{filter}` | string | Filter by specific field |
|
||||
|
||||
Example API implementation:
|
||||
|
||||
```python
|
||||
@router.get("", response_model=ListResponse)
|
||||
def get_all(
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(10, ge=1, le=100),
|
||||
search: str = Query("", description="Search term"),
|
||||
status: str = Query("", description="Filter by status"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
query = db.query(Model)
|
||||
|
||||
# Apply search
|
||||
if search:
|
||||
search_term = f"%{search.lower()}%"
|
||||
query = query.filter(
|
||||
(Model.name.ilike(search_term)) |
|
||||
(Model.email.ilike(search_term))
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if status:
|
||||
query = query.filter(Model.status == status)
|
||||
|
||||
# Pagination
|
||||
total = query.count()
|
||||
items = query.offset((page - 1) * per_page).limit(per_page).all()
|
||||
|
||||
return ListResponse(items=items, total=total, page=page, ...)
|
||||
```
|
||||
|
||||
## Data Table
|
||||
|
||||
### Structure
|
||||
|
||||
```html
|
||||
<div class="w-full overflow-hidden rounded-lg shadow-xs">
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="w-full whitespace-no-wrap">
|
||||
<thead>
|
||||
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
||||
<th class="px-4 py-3">Column</th>
|
||||
<!-- More columns -->
|
||||
<th class="px-4 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-for="item in items" :key="item.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<!-- Columns -->
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="px-4 py-3 text-xs font-semibold tracking-wide text-gray-500 uppercase border-t dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
|
||||
<!-- Pagination controls -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Action Buttons
|
||||
|
||||
Standard action buttons for table rows:
|
||||
|
||||
```html
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<!-- View -->
|
||||
<a :href="'/admin/resource/' + item.id"
|
||||
class="flex items-center justify-center p-2 text-blue-600 rounded-lg hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-700"
|
||||
title="View">
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</a>
|
||||
|
||||
<!-- Edit -->
|
||||
<a :href="'/admin/resource/' + item.id + '/edit'"
|
||||
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700"
|
||||
title="Edit">
|
||||
<span x-html="$icon('edit', 'w-5 h-5')"></span>
|
||||
</a>
|
||||
|
||||
<!-- Delete -->
|
||||
<button @click="deleteItem(item)"
|
||||
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700"
|
||||
title="Delete">
|
||||
<span x-html="$icon('delete', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Status Badges
|
||||
|
||||
```html
|
||||
<!-- Active/Inactive -->
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full text-xs"
|
||||
:class="item.is_active
|
||||
? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100'
|
||||
: 'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100'"
|
||||
x-text="item.is_active ? 'Active' : 'Inactive'">
|
||||
</span>
|
||||
|
||||
<!-- Role Badge -->
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full text-xs capitalize"
|
||||
:class="{
|
||||
'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100': item.role === 'admin',
|
||||
'text-purple-700 bg-purple-100 dark:bg-purple-700 dark:text-purple-100': item.role === 'vendor'
|
||||
}"
|
||||
x-text="item.role">
|
||||
</span>
|
||||
```
|
||||
|
||||
## Loading & Error States
|
||||
|
||||
### Loading State
|
||||
|
||||
```html
|
||||
<div x-show="loading" class="text-center py-12">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading...</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Error State
|
||||
|
||||
```html
|
||||
<div x-show="error && !loading" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
|
||||
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
|
||||
<div>
|
||||
<p class="font-semibold">Error loading data</p>
|
||||
<p class="text-sm" x-text="error"></p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Empty State
|
||||
|
||||
```html
|
||||
<template x-if="items.length === 0">
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('inbox', 'w-12 h-12 text-gray-400 mb-4')"></span>
|
||||
<p class="text-lg font-medium">No items found</p>
|
||||
<p class="text-sm" x-text="filters.search ? 'Try adjusting your search' : 'Create your first item'"></p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Pages Using These Components
|
||||
|
||||
| Page | Stats | Search | Filters |
|
||||
|------|-------|--------|---------|
|
||||
| `/admin/users` | Total, Active, Inactive, Admins | Name, Email, Username | Role, Status |
|
||||
| `/admin/companies` | Total, Verified, Active, Vendors | Name, Email, Owner | Status |
|
||||
| `/admin/vendors` | Total, Verified, Active, Products | Name, Code, Subdomain | Status, Company |
|
||||
|
||||
## JavaScript Module Structure
|
||||
|
||||
Each list page follows this pattern:
|
||||
|
||||
```javascript
|
||||
function adminResourceList() {
|
||||
return {
|
||||
// Inherit base layout
|
||||
...data(),
|
||||
|
||||
// Page identifier (for sidebar highlighting)
|
||||
currentPage: 'resource-name',
|
||||
|
||||
// State
|
||||
items: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
filters: { search: '', status: '' },
|
||||
stats: {},
|
||||
pagination: { page: 1, per_page: 10, total: 0, pages: 0 },
|
||||
|
||||
// Initialize
|
||||
async init() {
|
||||
await this.loadItems();
|
||||
await this.loadStats();
|
||||
},
|
||||
|
||||
// Format helpers
|
||||
formatDate(dateString) {
|
||||
return dateString ? Utils.formatDate(dateString) : '-';
|
||||
},
|
||||
|
||||
// Data loading
|
||||
async loadItems() { /* ... */ },
|
||||
async loadStats() { /* ... */ },
|
||||
|
||||
// Search & filters
|
||||
debouncedSearch() { /* ... */ },
|
||||
|
||||
// Pagination
|
||||
nextPage() { /* ... */ },
|
||||
previousPage() { /* ... */ },
|
||||
|
||||
// Actions
|
||||
async deleteItem(item) { /* ... */ }
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Icons Reference](./icons.md)
|
||||
- [Alpine.js Integration](./alpine-integration.md)
|
||||
- [Tailwind CSS](./tailwind-css.md)
|
||||
Reference in New Issue
Block a user