New documentation: - docs/frontend/shared/jinja-macros.md: Complete reference for all 94 macros - Alerts, Avatars, Badges, Buttons, Cards, Charts, Datepicker - Dropdowns, Forms, Headers, Modals, Pagination, Tables - Usage examples and parameter documentation - Complete page example showing all macros together Updated documentation: - docs/frontend/cdn-fallback-strategy.md: - Add Chart.js (v4.4.1) and Flatpickr (v4.6.13) sections - Document optional-libs.html loader macros - Update file structure and deployment checklist - docs/frontend/shared/ui-components.md: - Add tip box recommending Jinja macros for new development - Update related documentation links - mkdocs.yml: - Add Jinja Macros Library to navigation (top of Shared Components) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
15 KiB
15 KiB
UI Components Library
Version: 2.0
Last Updated: December 2024
Live Reference: /admin/components
Overview
The admin panel uses a consistent set of UI components built with Tailwind CSS and Alpine.js. All components support dark mode and are fully accessible.
Live Component Library: Visit /admin/components in the admin panel to see all components with copy-paste ready code.
!!! tip "Use Jinja Macros Instead" For new development, prefer using the Jinja Macros Library instead of copy-pasting HTML. Macros provide:
- **Consistency:** Same styling across all pages
- **Maintainability:** Update once, apply everywhere
- **Less code:** Parameters instead of full HTML
- **Type safety:** Built-in Alpine.js bindings
```jinja
{# Instead of 20 lines of HTML... #}
{% from 'shared/macros/buttons.html' import btn_primary %}
{{ btn_primary('Save Changes', icon='check', onclick='save()') }}
```
See [Jinja Macros Library](jinja-macros.md) for full documentation.
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 │
└─────────────────────────────────────────────────────┘
Form Components
Basic Input
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">Field Label</span>
<input
type="text"
x-model="formData.field"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-input"
/>
</label>
Required Field with Validation
<label class="block mb-4 text-sm">
<span class="text-gray-700 dark:text-gray-400">
Email Address <span class="text-red-600">*</span>
</span>
<input
type="email"
x-model="formData.email"
required
:class="{ 'border-red-600 focus:border-red-400 focus:shadow-outline-red': errors.email }"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input"
/>
<span x-show="errors.email" class="text-xs text-red-600 dark:text-red-400 mt-1" x-text="errors.email"></span>
</label>
Disabled/Read-Only Input
<input
type="text"
x-model="data.field"
disabled
class="block w-full mt-1 text-sm bg-gray-100 border-gray-300 rounded-md dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 cursor-not-allowed"
/>
Textarea
<textarea
x-model="formData.description"
rows="3"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-textarea"
></textarea>
Select
<select
x-model="formData.option"
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray form-select"
>
<option value="">Select an option</option>
<option value="option1">Option 1</option>
<option value="option2">Option 2</option>
</select>
Button Components
Primary Button
<button class="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">
Primary Button
</button>
Button with Icon
<button 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">
<span x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
Add Item
</button>
Secondary Button
<button class="px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition-colors duration-150 bg-white border border-gray-300 rounded-lg hover:border-gray-400 focus:outline-none dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800">
Cancel
</button>
Danger Button
<button class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700">
Delete
</button>
Card Components
Stats Card
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
<span x-html="$icon('user-group', 'w-5 h-5')"></span>
</div>
<div>
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
Total Users
</p>
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total">
0
</p>
</div>
</div>
Stats Card Colors
| Color | Use Case | Classes |
|---|---|---|
| Blue | Total counts | text-blue-500 bg-blue-100 |
| Green | Active/Success | text-green-500 bg-green-100 |
| Red | Inactive/Errors | text-red-500 bg-red-100 |
| Orange | Warnings/Admin | text-orange-500 bg-orange-100 |
| Purple | Primary metrics | text-purple-500 bg-purple-100 |
Info Card
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800">
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
Card Title
</h3>
<div class="space-y-3">
<div>
<p class="text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Field Label</p>
<p class="text-sm text-gray-700 dark:text-gray-300">Field Value</p>
</div>
</div>
</div>
Status Badges
Success Badge
<span class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-green-700 bg-green-100 rounded-full dark:bg-green-700 dark:text-green-100">
<span x-html="$icon('check-circle', 'w-3 h-3 mr-1')"></span>
Active
</span>
Warning Badge
<span class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-orange-700 bg-orange-100 rounded-full dark:bg-orange-700 dark:text-orange-100">
<span x-html="$icon('clock', 'w-3 h-3 mr-1')"></span>
Pending
</span>
Danger Badge
<span class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-red-700 bg-red-100 rounded-full dark:bg-red-700 dark:text-red-100">
<span x-html="$icon('x-circle', 'w-3 h-3 mr-1')"></span>
Inactive
</span>
Dynamic Badge
<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>
Data Tables
Basic Table Structure
<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">Name</th>
<th class="px-4 py-3">Status</th>
<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">
<td class="px-4 py-3" x-text="item.name"></td>
<td class="px-4 py-3"><!-- Status badge --></td>
<td class="px-4 py-3"><!-- Action buttons --></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
Action Buttons
<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>
Loading & Error States
Loading State
<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 Alert
<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
<div class="flex flex-col items-center py-12">
<span x-html="$icon('inbox', 'w-12 h-12 text-gray-400 mb-4')"></span>
<p class="text-lg font-medium text-gray-600 dark:text-gray-400">No items found</p>
<p class="text-sm text-gray-500" x-text="filters.search ? 'Try adjusting your search' : 'Create your first item'"></p>
</div>
Modal Components
Confirmation Modal
<div
x-show="showModal"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
@click.self="showModal = false">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 m-4 max-w-md w-full">
<div class="flex items-start justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">
Confirm Action
</h3>
<button @click="showModal = false" class="text-gray-400 hover:text-gray-600">
<span x-html="$icon('x', 'w-5 h-5')"></span>
</button>
</div>
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">
Are you sure you want to perform this action?
</p>
<div class="flex justify-end gap-3">
<button @click="showModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600">
Cancel
</button>
<button @click="confirmAction()"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700">
Confirm
</button>
</div>
</div>
</div>
Toast Notifications
Use the global Utils helper:
Utils.showToast('Operation successful!', 'success');
Utils.showToast('Something went wrong', 'error');
Utils.showToast('Please check your input', 'warning');
Utils.showToast('Here is some information', 'info');
Grid Layouts
2 Columns (Desktop)
<div class="grid gap-6 md:grid-cols-2">
<div>Column 1</div>
<div>Column 2</div>
</div>
4 Columns (Stats Cards)
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
<!-- Stats cards here -->
</div>
Color Reference
| Type | Primary | Success | Warning | Danger | Info |
|---|---|---|---|---|---|
| Background | bg-purple-600 |
bg-green-600 |
bg-orange-600 |
bg-red-600 |
bg-blue-600 |
| Text | text-purple-600 |
text-green-600 |
text-orange-600 |
text-red-600 |
text-blue-600 |
| Light BG | bg-purple-100 |
bg-green-100 |
bg-orange-100 |
bg-red-100 |
bg-blue-100 |
Common Icons
| Icon | Use Case |
|---|---|
user-group |
Users/Teams |
badge-check |
Verified |
check-circle |
Success |
x-circle |
Error/Inactive |
clock |
Pending |
calendar |
Dates |
edit |
Edit |
delete |
Delete |
plus |
Add |
arrow-left |
Back |
exclamation |
Warning |
spinner |
Loading |
JavaScript Patterns
List Page Structure
function adminResourceList() {
return {
...data(), // Inherit base layout
currentPage: 'resource-name',
// State
items: [],
loading: false,
error: null,
filters: { search: '', status: '' },
stats: {},
pagination: { page: 1, per_page: 10, total: 0 },
async init() {
await this.loadItems();
await this.loadStats();
},
async loadItems() { /* ... */ },
debouncedSearch() { /* ... */ },
async deleteItem(item) { /* ... */ }
};
}
Related Documentation
- Jinja Macros Library - Reusable macro components (recommended)
- Pagination Guide
- CDN Fallback Strategy
- Tailwind CSS
- Icons Guide
- Tailwind CSS Official Docs
- Alpine.js Official Docs