Files
orion/docs/frontend/shared/ui-components.md
Samir Boulahtit dad0989948 docs: add comprehensive Jinja macros documentation
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>
2025-12-06 19:15:27 +01:00

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) { /* ... */ }
    };
}